lint fixes

This commit is contained in:
2026-04-04 17:41:43 -04:00
parent 9d031c394d
commit 11274c0ce6
29 changed files with 1019 additions and 110 deletions
+6
View File
@@ -54,3 +54,9 @@ src-tauri/gen/
# Ignore this file # Ignore this file
.gitignore .gitignore
# wrangler files
.wrangler
.dev.vars*
!.dev.vars.example
!.env.example
+8
View File
@@ -1,2 +1,10 @@
dist/ dist/
node_modules/ node_modules/
.git/
.cursor/
coverage/
spacetimedb/dist/
src/module_bindings/
src-tauri/target/
src-tauri/gen/
pnpm-lock.yaml
+4 -3
View File
@@ -3,7 +3,7 @@ import js from "@eslint/js";
import tseslint from "typescript-eslint"; import tseslint from "typescript-eslint";
import svelte from "eslint-plugin-svelte"; import svelte from "eslint-plugin-svelte";
import svelteParser from "svelte-eslint-parser"; import svelteParser from "svelte-eslint-parser";
import { defineConfig } from 'eslint/config'; import { defineConfig } from "eslint/config";
export default defineConfig([ export default defineConfig([
tseslint.configs.recommended, tseslint.configs.recommended,
@@ -32,7 +32,7 @@ export default defineConfig([
"README.md", "README.md",
".github/", ".github/",
".cursor/", ".cursor/",
"spacetimedb/dist", "spacetimedb/",
"src/module_bindings/", "src/module_bindings/",
"src-tauri/**", "src-tauri/**",
], ],
@@ -64,13 +64,14 @@ export default defineConfig([
projectService: true, projectService: true,
extraFileExtensions: [".svelte"], extraFileExtensions: [".svelte"],
svelteFeatures: { svelteFeatures: {
runes: true runes: true,
}, },
}, },
}, },
}, },
{ {
rules: { rules: {
"no-unused-vars": "off",
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "off",
"@/no-trailing-spaces": "warn", "@/no-trailing-spaces": "warn",
"@typescript-eslint/no-unused-vars": [ "@typescript-eslint/no-unused-vars": [
+7 -3
View File
@@ -9,13 +9,15 @@
"build": "tsc -b && vite build", "build": "tsc -b && vite build",
"format": "prettier . --write --ignore-path ../../.prettierignore", "format": "prettier . --write --ignore-path ../../.prettierignore",
"lint": "eslint .", "lint": "eslint .",
"preview": "vite preview", "preview": "pnpm run build && wrangler dev",
"test": "vitest run", "test": "vitest run",
"spacetime:generate": "spacetime generate --lang typescript --out-dir src/module_bindings --module-path spacetimedb", "spacetime:generate": "spacetime generate --lang typescript --out-dir src/module_bindings --module-path spacetimedb",
"spacetime:publish:local": "spacetime publish --module-path spacetimedb --server local", "spacetime:publish:local": "spacetime publish --module-path spacetimedb --server local",
"spacetime:publish": "spacetime publish --module-path spacetimedb --server maincloud", "spacetime:publish": "spacetime publish --module-path spacetimedb --server maincloud",
"deploy:local": "docker compose -f docker-compose.local.yml up --build", "deploy:local": "docker compose -f docker-compose.local.yml up --build",
"deploy:maincloud": "docker compose -f docker-compose.maincloud.yml up --build" "deploy:maincloud": "docker compose -f docker-compose.maincloud.yml up --build",
"deploy:cloudflare": "wrangler deploy",
"deploy": "pnpm run build && wrangler deploy"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^7.2.0", "@fortawesome/fontawesome-free": "^7.2.0",
@@ -27,6 +29,7 @@
"svelte-check": "^4.4.6" "svelte-check": "^4.4.6"
}, },
"devDependencies": { "devDependencies": {
"@cloudflare/vite-plugin": "^1.31.0",
"@eslint/js": "^9.17.0", "@eslint/js": "^9.17.0",
"@tauri-apps/cli": "^2.10.1", "@tauri-apps/cli": "^2.10.1",
"@testing-library/jest-dom": "^6.6.3", "@testing-library/jest-dom": "^6.6.3",
@@ -44,6 +47,7 @@
"typescript": "~5.6.2", "typescript": "~5.6.2",
"typescript-eslint": "^8.18.2", "typescript-eslint": "^8.18.2",
"vite": "^8.0.3", "vite": "^8.0.3",
"vitest": "3.2.4" "vitest": "3.2.4",
"wrangler": "^4.80.0"
} }
} }
+826
View File
File diff suppressed because it is too large Load Diff
+10 -3
View File
@@ -6,7 +6,9 @@ const user = table(
{ {
name: "user", name: "user",
public: true, public: true,
indexes: [{ accessor: "by_online", algorithm: "btree", columns: ["online"] }], indexes: [
{ accessor: "by_online", algorithm: "btree", columns: ["online"] },
],
}, },
{ {
identity: t.identity().primaryKey(), identity: t.identity().primaryKey(),
@@ -467,7 +469,12 @@ export const upload_avatar = spacetimedb.reducer(
if (data.length > 1024 * 1024) { if (data.length > 1024 * 1024) {
throw new SenderError("Avatar exceeds 1MB limit"); throw new SenderError("Avatar exceeds 1MB limit");
} }
const img = ctx.db.image.insert({ id: 0n, data, mime_type: mimeType, name: "avatar" }); const img = ctx.db.image.insert({
id: 0n,
data,
mime_type: mimeType,
name: "avatar",
});
const user = ctx.db.user.identity.find(ctx.sender); const user = ctx.db.user.identity.find(ctx.sender);
if (!user) throw new SenderError("User not found"); if (!user) throw new SenderError("User not found");
ctx.db.user.identity.update({ ...user, avatar_id: img.id }); ctx.db.user.identity.update({ ...user, avatar_id: img.id });
@@ -1049,7 +1056,7 @@ export const send_message = spacetimedb.reducer(
export const init = spacetimedb.init((ctx) => { export const init = spacetimedb.init((ctx) => {
let hasServers = false; let hasServers = false;
for (const _ of ctx.db.server.iter()) { for (const _server of ctx.db.server.iter()) {
hasServers = true; hasServers = true;
break; break;
} }
+2 -6
View File
@@ -2,10 +2,6 @@
"$schema": "../gen/schemas/desktop-schema.json", "$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default", "identifier": "default",
"description": "enables the default permissions", "description": "enables the default permissions",
"windows": [ "windows": ["main"],
"main" "permissions": ["core:default"]
],
"permissions": [
"core:default"
]
} }
+2 -10
View File
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import { auth } from "./auth.svelte"; import { auth } from "./auth.svelte";
import { HOST_KEY, DB_NAME_KEY, getEnv } from "../env"; import { HOST_KEY, DB_NAME_KEY } from "../env";
import { TokenStore, getStdbHost, getStdbDbName } from "../config"; import { TokenStore, getStdbHost, getStdbDbName } from "../config";
import SpacetimeProvider from "../SpacetimeProvider.svelte"; import SpacetimeProvider from "../SpacetimeProvider.svelte";
@@ -9,7 +9,7 @@
children: any, children: any,
reconnect: () => void, reconnect: () => void,
showServerSettings: boolean, showServerSettings: boolean,
onToggleServerSettings: (val: boolean) => void, onToggleServerSettings: (_val: boolean) => void,
reconnectKey: number reconnectKey: number
}>(); }>();
@@ -22,7 +22,6 @@
const MAINCLOUD_URI = "https://maincloud.spacetimedb.com"; const MAINCLOUD_URI = "https://maincloud.spacetimedb.com";
let isMaincloud = $state(true); let isMaincloud = $state(true);
let userWantsToConnect = $state(false);
let isSettingsExpanded = $state(false); let isSettingsExpanded = $state(false);
onMount(() => { onMount(() => {
@@ -48,10 +47,6 @@
if (stdbDbName) localStorage.setItem(DB_NAME_KEY, stdbDbName); if (stdbDbName) localStorage.setItem(DB_NAME_KEY, stdbDbName);
}); });
function handleAuthError(error: string | null) {
authError = error;
}
const isBypassEnabled = const isBypassEnabled =
import.meta.env.VITE_BYPASS_AUTH === "true" || import.meta.env.VITE_BYPASS_AUTH === "true" ||
new URLSearchParams(window.location.search).has("bypass_auth"); new URLSearchParams(window.location.search).has("bypass_auth");
@@ -77,7 +72,6 @@
<SpacetimeProvider <SpacetimeProvider
onReconnectTrigger={reconnect} onReconnectTrigger={reconnect}
onCancel={() => { onCancel={() => {
userWantsToConnect = false;
isGuest = false; isGuest = false;
}} }}
> >
@@ -106,7 +100,6 @@
<div style="display: flex; flex-direction: column; gap: 12px; width: 100%"> <div style="display: flex; flex-direction: column; gap: 12px; width: 100%">
<button <button
onclick={() => { onclick={() => {
userWantsToConnect = true;
auth.signinRedirect(); auth.signinRedirect();
}} }}
disabled={auth.isLoading} disabled={auth.isLoading}
@@ -119,7 +112,6 @@
<button <button
onclick={() => { onclick={() => {
isGuest = true; isGuest = true;
userWantsToConnect = true;
}} }}
class="btn-secondary" class="btn-secondary"
style="width: 100%" style="width: 100%"
+1 -1
View File
@@ -105,7 +105,7 @@ class AuthStore {
keysToRemove.push(key); keysToRemove.push(key);
} }
} }
keysToRemove.forEach(key => localStorage.removeItem(key)); keysToRemove.forEach((key) => localStorage.removeItem(key));
if (this.#user) { if (this.#user) {
await this.#userManager.signoutRedirect(); await this.#userManager.signoutRedirect();
-3
View File
@@ -4,9 +4,7 @@
import { portal } from "../../portal"; import { portal } from "../../portal";
import type { ChatService } from "../services/chat.svelte"; import type { ChatService } from "../services/chat.svelte";
import type { WebRTCService } from "../services/webrtc/webrtc.svelte"; import type { WebRTCService } from "../services/webrtc/webrtc.svelte";
import type { WebRTCStats } from "../services/webrtc/types";
import Avatar from "./Avatar.svelte"; import Avatar from "./Avatar.svelte";
import type * as Types from "../../module_bindings/types";
const chat = getContext<ChatService>("chat"); const chat = getContext<ChatService>("chat");
const webrtc = getContext<WebRTCService>("webrtc"); const webrtc = getContext<WebRTCService>("webrtc");
@@ -183,7 +181,6 @@
{@const isWatchingMe = chat.watching.some(w => w.watcher.isEqual(vs.identity) && w.watchee.isEqual(chat.identity))} {@const isWatchingMe = chat.watching.some(w => w.watcher.isEqual(vs.identity) && w.watchee.isEqual(chat.identity))}
{@const amISharing = chat.currentVoiceState?.isSharingScreen} {@const amISharing = chat.currentVoiceState?.isSharingScreen}
{@const voiceStatusColor = isMe ? "green" : getStatusColor(status)} {@const voiceStatusColor = isMe ? "green" : getStatusColor(status)}
{@const videoStatusColor = isMe ? (isSharing ? "green" : undefined) : (isSharing ? getStatusColor(status) : undefined)}
{@const isLocalUserInThisChannel = chat.connectedVoiceChannel?.id === channel.id} {@const isLocalUserInThisChannel = chat.connectedVoiceChannel?.id === channel.id}
<div <div
+6 -8
View File
@@ -1,13 +1,11 @@
<script lang="ts"> <script lang="ts">
import { getContext, onMount } from "svelte"; import { getContext, onMount } from "svelte";
import { EMOJIS, type EmojiInfo } from "../emojis"; import { EMOJIS } from "../emojis";
import type { ChatService } from "../services/chat.svelte"; import type { ChatService } from "../services/chat.svelte";
import type * as Types from "../../module_bindings/types";
import { optimizeEmoji } from "../utils"; import { optimizeEmoji } from "../utils";
let { onSelect, onClose } = $props<{ let { onSelect } = $props<{
onSelect: (emoji?: string, customEmojiId?: bigint) => void; onSelect: (_emoji?: string, _customEmojiId?: bigint) => void;
onClose: () => void;
}>(); }>();
const chat = getContext<ChatService>("chat"); const chat = getContext<ChatService>("chat");
@@ -22,7 +20,7 @@
recentEmojis = JSON.parse(saved).map((id: any) => recentEmojis = JSON.parse(saved).map((id: any) =>
typeof id === 'string' && id.startsWith('custom:') ? BigInt(id.split(':')[1]) : id typeof id === 'string' && id.startsWith('custom:') ? BigInt(id.split(':')[1]) : id
); );
} catch (e) { } catch {
recentEmojis = []; recentEmojis = [];
} }
} }
@@ -65,8 +63,8 @@
{ id: 'custom', name: 'Custom', icon: '✨' }, { id: 'custom', name: 'Custom', icon: '✨' },
]; ];
async function handleCustomUpload(e: Event) { async function handleCustomUpload(_e: Event) {
const file = (e.target as HTMLInputElement).files?.[0]; const file = (_e.target as HTMLInputElement).files?.[0];
if (!file) return; if (!file) return;
// We'll assume the user provides a name or we use the filename // We'll assume the user provides a name or we use the filename
+2 -4
View File
@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte";
import type * as Types from "../../module_bindings/types"; import type * as Types from "../../module_bindings/types";
let { image, onClose }: { image: Types.Image, onClose: () => void } = $props(); let { image, onClose }: { image: Types.Image, onClose: () => void } = $props();
@@ -20,7 +19,6 @@
let hasMoved = false; let hasMoved = false;
const isZoomed = $derived(zoomLevel > baseScale + 0.001); const isZoomed = $derived(zoomLevel > baseScale + 0.001);
const currentActualScale = $derived(zoomLevel);
function handleKeydown(e: KeyboardEvent) { function handleKeydown(e: KeyboardEvent) {
if (e.key === "Escape") { if (e.key === "Escape") {
@@ -130,7 +128,7 @@
isDragging = false; isDragging = false;
} }
function handleOverlayClick(e: MouseEvent) { function handleOverlayClick() {
const clickDuration = Date.now() - mousedownTime; const clickDuration = Date.now() - mousedownTime;
if (hasMoved || clickDuration > 300) return; if (hasMoved || clickDuration > 300) return;
onClose(); onClose();
@@ -424,6 +422,6 @@
} }
.full-image.animate { .full-image.animate {
transition: transform 0.15s cubic-bezier(0.4, 0, 0.2, 1); transition: transform 0.05s cubic-bezier(0.4, 0, 0.2, 1);
} }
</style> </style>
+1 -1
View File
@@ -43,7 +43,7 @@
if (!title && !description && !image) { if (!title && !description && !image) {
throw new Error("No useful metadata found"); throw new Error("No useful metadata found");
} }
} catch (err) { } catch {
if (!isCancelled) { if (!isCancelled) {
error = true; error = true;
} }
+1 -1
View File
@@ -5,7 +5,7 @@
let { let {
activeChannelId, activeChannelId,
activeThreadId, activeThreadId: _activeThreadId,
isFullyAuthenticated isFullyAuthenticated
} = $props<{ } = $props<{
activeChannelId: bigint | null, activeChannelId: bigint | null,
+2 -2
View File
@@ -13,8 +13,8 @@
let { let {
threadMessages, threadMessages,
users, users,
identity, identity: _identity,
images images: _images
}: { }: {
threadMessages: readonly Types.Message[], threadMessages: readonly Types.Message[],
users: readonly Types.User[], users: readonly Types.User[],
+1 -3
View File
@@ -1,8 +1,6 @@
<script lang="ts"> <script lang="ts">
import { Identity } from "spacetimedb"; import { Identity } from "spacetimedb";
import type * as Types from "../../module_bindings/types"; import type * as Types from "../../module_bindings/types";
import { useReducer } from "spacetimedb/svelte";
import { reducers } from "../../module_bindings";
import ThreadMessageList from "./ThreadMessageList.svelte"; import ThreadMessageList from "./ThreadMessageList.svelte";
import ThreadMessageInput from "./ThreadMessageInput.svelte"; import ThreadMessageInput from "./ThreadMessageInput.svelte";
import RichText from "./RichText.svelte"; import RichText from "./RichText.svelte";
@@ -15,7 +13,7 @@
pendingThreadParentMessageId, pendingThreadParentMessageId,
setPendingThreadParentMessageId, setPendingThreadParentMessageId,
activeChannelId, activeChannelId,
activeServer, activeServer: _activeServer,
isFullyAuthenticated, isFullyAuthenticated,
users, users,
identity, identity,
+1 -4
View File
@@ -1,8 +1,7 @@
<script lang="ts"> <script lang="ts">
import { Identity } from "spacetimedb"; import { Identity } from "spacetimedb";
import { getContext, onMount, onDestroy } from "svelte"; import { onMount } from "svelte";
import Avatar from "./Avatar.svelte"; import Avatar from "./Avatar.svelte";
import type { WebRTCService } from "../services/webrtc/webrtc.svelte";
import type * as Types from "../../module_bindings/types"; import type * as Types from "../../module_bindings/types";
let { let {
@@ -27,8 +26,6 @@
users: readonly Types.User[]; users: readonly Types.User[];
}>(); }>();
const webrtc = getContext<WebRTCService>("webrtc");
let videoRef = $state<HTMLVideoElement | null>(null); let videoRef = $state<HTMLVideoElement | null>(null);
let containerRef = $state<HTMLDivElement | null>(null); let containerRef = $state<HTMLDivElement | null>(null);
let isMuted = $state(false); let isMuted = $state(false);
+15 -3
View File
@@ -1024,7 +1024,11 @@ export const EMOJIS: EmojiInfo[] = [
{ char: "🈁", name: "Japanese “here” button", category: "symbols" }, { char: "🈁", name: "Japanese “here” button", category: "symbols" },
{ char: "🈂️", name: "Japanese “service charge” button", category: "symbols" }, { char: "🈂️", name: "Japanese “service charge” button", category: "symbols" },
{ char: "🈷️", name: "Japanese “monthly amount” button", category: "symbols" }, { char: "🈷️", name: "Japanese “monthly amount” button", category: "symbols" },
{ char: "🈶", name: "Japanese “not free of charge” button", category: "symbols" }, {
char: "🈶",
name: "Japanese “not free of charge” button",
category: "symbols",
},
{ char: "🈯", name: "Japanese “reserved” button", category: "symbols" }, { char: "🈯", name: "Japanese “reserved” button", category: "symbols" },
{ char: "🉐", name: "Japanese “bargain” button", category: "symbols" }, { char: "🉐", name: "Japanese “bargain” button", category: "symbols" },
{ char: "🈹", name: "Japanese “discount” button", category: "symbols" }, { char: "🈹", name: "Japanese “discount” button", category: "symbols" },
@@ -1034,9 +1038,17 @@ export const EMOJIS: EmojiInfo[] = [
{ char: "🈸", name: "Japanese “application” button", category: "symbols" }, { char: "🈸", name: "Japanese “application” button", category: "symbols" },
{ char: "🈴", name: "Japanese “passing grade” button", category: "symbols" }, { char: "🈴", name: "Japanese “passing grade” button", category: "symbols" },
{ char: "🈳", name: "Japanese “vacancy” button", category: "symbols" }, { char: "🈳", name: "Japanese “vacancy” button", category: "symbols" },
{ char: "㊗️", name: "Japanese “congratulations” button", category: "symbols" }, {
char: "㊗️",
name: "Japanese “congratulations” button",
category: "symbols",
},
{ char: "㊙️", name: "Japanese “secret” button", category: "symbols" }, { char: "㊙️", name: "Japanese “secret” button", category: "symbols" },
{ char: "🈺", name: "Japanese “open for business” button", category: "symbols" }, {
char: "🈺",
name: "Japanese “open for business” button",
category: "symbols",
},
{ char: "🈵", name: "Japanese “no vacancy” button", category: "symbols" }, { char: "🈵", name: "Japanese “no vacancy” button", category: "symbols" },
{ char: "🔴", name: "red circle", category: "symbols" }, { char: "🔴", name: "red circle", category: "symbols" },
{ char: "🟠", name: "orange circle", category: "symbols" }, { char: "🟠", name: "orange circle", category: "symbols" },
+18 -5
View File
@@ -39,7 +39,7 @@ export class ChatService {
// Background effect to populate avatar cache // Background effect to populate avatar cache
$effect(() => { $effect(() => {
const usersWithAvatars = this.users.filter(u => u.avatarId); const usersWithAvatars = this.users.filter((u) => u.avatarId);
for (const user of usersWithAvatars) { for (const user of usersWithAvatars) {
const avatarId = user.avatarId!; const avatarId = user.avatarId!;
const idStr = avatarId.toString(); const idStr = avatarId.toString();
@@ -64,7 +64,7 @@ export class ChatService {
} }
// 2. If not in cache, check if we have the image data in SpacetimeDB sync // 2. If not in cache, check if we have the image data in SpacetimeDB sync
const img = this.images.find(i => i.id === id); const img = this.images.find((i) => i.id === id);
if (img) { if (img) {
const url = await imageCache.set(id, img.data, img.mimeType); const url = await imageCache.set(id, img.data, img.mimeType);
this.#avatarUrls.set(idStr, url); this.#avatarUrls.set(idStr, url);
@@ -356,7 +356,11 @@ export class ChatService {
this.#msg.handleLoadMoreMessages(); this.#msg.handleLoadMoreMessages();
}; };
handleToggleReaction = (messageId: bigint, emoji?: string, customEmojiId?: bigint) => { handleToggleReaction = (
messageId: bigint,
emoji?: string,
customEmojiId?: bigint,
) => {
this.#msg.toggleReaction(messageId, emoji, customEmojiId); this.#msg.toggleReaction(messageId, emoji, customEmojiId);
}; };
@@ -377,7 +381,11 @@ export class ChatService {
this.#msg.uploadImage(data, mimeType, name); this.#msg.uploadImage(data, mimeType, name);
}; };
uploadCustomEmoji = async (name: string, category: string, data: Uint8Array) => { uploadCustomEmoji = async (
name: string,
category: string,
data: Uint8Array,
) => {
this.#msg.uploadCustomEmoji(name, category, data); this.#msg.uploadCustomEmoji(name, category, data);
}; };
@@ -420,7 +428,12 @@ export class ChatService {
}) })
.map((ta) => { .map((ta) => {
const user = this.users.find((u) => u.identity.isEqual(ta.identity)); const user = this.users.find((u) => u.identity.isEqual(ta.identity));
return user || { name: `User ${ta.identity.toHexString().substring(0, 8)}` } as any as Types.User; return (
user ||
({
name: `User ${ta.identity.toHexString().substring(0, 8)}`,
} as any as Types.User)
);
}); });
} }
+62 -23
View File
@@ -26,12 +26,18 @@ export class MessagingService {
messageLimit = $state(50); messageLimit = $state(50);
isLoadingMore = $state(false); isLoadingMore = $state(false);
constructor(db: DatabaseService, nav: NavigationService, identity: () => Identity | null) { constructor(
db: DatabaseService,
nav: NavigationService,
identity: () => Identity | null,
) {
this.#db = db; this.#db = db;
this.#nav = nav; this.#nav = nav;
this.#identity = identity; this.#identity = identity;
this.#createThreadWithMessageReducer = useReducer(reducers.createThreadWithMessage); this.#createThreadWithMessageReducer = useReducer(
reducers.createThreadWithMessage,
);
this.#sendMessageReducer = useReducer(reducers.sendMessage); this.#sendMessageReducer = useReducer(reducers.sendMessage);
this.#uploadImageReducer = useReducer(reducers.uploadImage); this.#uploadImageReducer = useReducer(reducers.uploadImage);
this.#uploadCustomEmojiReducer = useReducer(reducers.uploadCustomEmoji); this.#uploadCustomEmojiReducer = useReducer(reducers.uploadCustomEmoji);
@@ -42,9 +48,15 @@ export class MessagingService {
const [messageImagesStore] = useTable(tables.message_image); const [messageImagesStore] = useTable(tables.message_image);
const [messageReactionsStore] = useTable(tables.message_reaction); const [messageReactionsStore] = useTable(tables.message_reaction);
messagesStore.subscribe((v: readonly Types.Message[]) => (this.#allMessages = v)); messagesStore.subscribe(
messageImagesStore.subscribe((v: readonly Types.MessageImage[]) => (this.#messageImages = v)); (v: readonly Types.Message[]) => (this.#allMessages = v),
messageReactionsStore.subscribe((v: readonly Types.MessageReaction[]) => (this.#messageReactions = v)); );
messageImagesStore.subscribe(
(v: readonly Types.MessageImage[]) => (this.#messageImages = v),
);
messageReactionsStore.subscribe(
(v: readonly Types.MessageReaction[]) => (this.#messageReactions = v),
);
$effect(() => { $effect(() => {
const channelId = this.#nav.activeChannelId; const channelId = this.#nav.activeChannelId;
@@ -57,38 +69,53 @@ export class MessagingService {
if (!conn) return; if (!conn) return;
untrack(() => { untrack(() => {
const queries = [ const queries = ["SELECT * FROM server", "SELECT * FROM custom_emoji"];
"SELECT * FROM server",
"SELECT * FROM custom_emoji",
];
// 1. Surgical Membership & Identity Pruning // 1. Surgical Membership & Identity Pruning
// Only load our own memberships and those of the active server // Only load our own memberships and those of the active server
if (identity && serverId) { if (identity && serverId) {
queries.push(`SELECT * FROM server_member WHERE identity = 0x${identity.toHexString()} OR server_id = ${serverId}`); queries.push(
`SELECT * FROM server_member WHERE identity = 0x${identity.toHexString()} OR server_id = ${serverId}`,
);
} else if (identity) { } else if (identity) {
queries.push(`SELECT * FROM server_member WHERE identity = 0x${identity.toHexString()}`); queries.push(
`SELECT * FROM server_member WHERE identity = 0x${identity.toHexString()}`,
);
} }
if (serverId) { if (serverId) {
// 2. Metadata for the Active Server // 2. Metadata for the Active Server
queries.push(`SELECT * FROM channel WHERE server_id = ${serverId}`); queries.push(`SELECT * FROM channel WHERE server_id = ${serverId}`);
queries.push(`SELECT * FROM thread WHERE channel_id IN (SELECT id FROM channel WHERE server_id = ${serverId})`); queries.push(
`SELECT * FROM thread WHERE channel_id IN (SELECT id FROM channel WHERE server_id = ${serverId})`,
);
// Voice states and activity are lightweight and indexed by channel_id // Voice states and activity are lightweight and indexed by channel_id
queries.push(`SELECT * FROM voice_state WHERE channel_id IN (SELECT id FROM channel WHERE server_id = ${serverId})`); queries.push(
queries.push(`SELECT * FROM voice_activity WHERE channel_id IN (SELECT id FROM channel WHERE server_id = ${serverId})`); `SELECT * FROM voice_state WHERE channel_id IN (SELECT id FROM channel WHERE server_id = ${serverId})`,
queries.push(`SELECT * FROM typing_activity WHERE channel_id IN (SELECT id FROM channel WHERE server_id = ${serverId})`); );
queries.push(`SELECT * FROM watching WHERE channel_id IN (SELECT id FROM channel WHERE server_id = ${serverId})`); queries.push(
`SELECT * FROM voice_activity WHERE channel_id IN (SELECT id FROM channel WHERE server_id = ${serverId})`,
);
queries.push(
`SELECT * FROM typing_activity WHERE channel_id IN (SELECT id FROM channel WHERE server_id = ${serverId})`,
);
queries.push(
`SELECT * FROM watching WHERE channel_id IN (SELECT id FROM channel WHERE server_id = ${serverId})`,
);
// 3. Load the actual messages for the active channel (with pagination) // 3. Load the actual messages for the active channel (with pagination)
if (channelId) { if (channelId) {
queries.push(`SELECT * FROM message WHERE channel_id = ${channelId} AND thread_id IS NULL ORDER BY sent DESC LIMIT ${limit}`); queries.push(
`SELECT * FROM message WHERE channel_id = ${channelId} AND thread_id IS NULL ORDER BY sent DESC LIMIT ${limit}`,
);
} }
// If viewing a thread, pull those specific messages too // If viewing a thread, pull those specific messages too
if (threadId) { if (threadId) {
queries.push(`SELECT * FROM message WHERE thread_id = ${threadId} OR id = (SELECT parent_message_id FROM thread WHERE id = ${threadId})`); queries.push(
`SELECT * FROM message WHERE thread_id = ${threadId} OR id = (SELECT parent_message_id FROM thread WHERE id = ${threadId})`,
);
} }
// 4. Surgical Image & Reaction Sync // 4. Surgical Image & Reaction Sync
@@ -98,8 +125,12 @@ export class MessagingService {
? `(SELECT id FROM message WHERE thread_id = ${threadId} OR id = (SELECT parent_message_id FROM thread WHERE id = ${threadId}))` ? `(SELECT id FROM message WHERE thread_id = ${threadId} OR id = (SELECT parent_message_id FROM thread WHERE id = ${threadId}))`
: `(SELECT id FROM message WHERE channel_id = ${channelId} AND thread_id IS NULL ORDER BY sent DESC LIMIT ${limit})`; : `(SELECT id FROM message WHERE channel_id = ${channelId} AND thread_id IS NULL ORDER BY sent DESC LIMIT ${limit})`;
queries.push(`SELECT * FROM message_image WHERE message_id IN ${visibleMsgSubquery}`); queries.push(
queries.push(`SELECT * FROM message_reaction WHERE message_id IN ${visibleMsgSubquery}`); `SELECT * FROM message_image WHERE message_id IN ${visibleMsgSubquery}`,
);
queries.push(
`SELECT * FROM message_reaction WHERE message_id IN ${visibleMsgSubquery}`,
);
// Image Sync: Message Images + User Avatars // Image Sync: Message Images + User Avatars
const userSubquery = `(SELECT identity FROM user WHERE online = true OR identity IN (SELECT identity FROM server_member WHERE server_id = ${serverId}) OR identity IN (SELECT sender FROM message WHERE id IN ${visibleMsgSubquery}))`; const userSubquery = `(SELECT identity FROM user WHERE online = true OR identity IN (SELECT identity FROM server_member WHERE server_id = ${serverId}) OR identity IN (SELECT sender FROM message WHERE id IN ${visibleMsgSubquery}))`;
@@ -130,7 +161,7 @@ export class MessagingService {
}); });
// Reset limit on channel switch // Reset limit on channel switch
$effect(() => { $effect(() => {
const _ = this.#nav.activeChannelId; void this.#nav.activeChannelId;
untrack(() => { untrack(() => {
this.messageLimit = 50; this.messageLimit = 50;
}); });
@@ -216,11 +247,19 @@ export class MessagingService {
this.#uploadImageReducer({ data, mimeType, name }); this.#uploadImageReducer({ data, mimeType, name });
}; };
uploadCustomEmoji = async (name: string, category: string, data: Uint8Array) => { uploadCustomEmoji = async (
name: string,
category: string,
data: Uint8Array,
) => {
this.#uploadCustomEmojiReducer({ name, category, data }); this.#uploadCustomEmojiReducer({ name, category, data });
}; };
toggleReaction = (messageId: bigint, emoji?: string, customEmojiId?: bigint) => { toggleReaction = (
messageId: bigint,
emoji?: string,
customEmojiId?: bigint,
) => {
this.#toggleReactionReducer({ messageId, emoji, customEmojiId }); this.#toggleReactionReducer({ messageId, emoji, customEmojiId });
}; };
@@ -71,7 +71,7 @@ export class ChannelAudioWebRTCService {
$effect(() => { $effect(() => {
const audioTrack = this.localStream?.getAudioTracks()[0] || null; const audioTrack = this.localStream?.getAudioTracks()[0] || null;
// Accessing peers and peerStatuses to trigger effect on changes // Accessing peers and peerStatuses to trigger effect on changes
const _statuses = this.peerManager.peerStatuses; void this.peerManager.peerStatuses;
this.peerManager.peers.forEach(async (peer, peerIdHex) => { this.peerManager.peers.forEach(async (peer, peerIdHex) => {
const transceivers = peer.pc.getTransceivers(); const transceivers = peer.pc.getTransceivers();
if (transceivers[0] && transceivers[0].sender.track !== audioTrack) { if (transceivers[0] && transceivers[0].sender.track !== audioTrack) {
@@ -224,7 +224,10 @@ export class LocalMediaService {
// Persist screen share settings // Persist screen share settings
$effect(() => { $effect(() => {
localStorage.setItem("screen_share_resolution", this.screenShareResolution); localStorage.setItem(
"screen_share_resolution",
this.screenShareResolution,
);
}); });
$effect(() => { $effect(() => {
localStorage.setItem("screen_share_framerate", this.screenShareFramerate); localStorage.setItem("screen_share_framerate", this.screenShareFramerate);
@@ -331,7 +334,8 @@ export class LocalMediaService {
try { try {
console.log("[local-media] Requesting screen share..."); console.log("[local-media] Requesting screen share...");
const res = RESOLUTIONS[this.screenShareResolution] || RESOLUTIONS["1080"]; const res =
RESOLUTIONS[this.screenShareResolution] || RESOLUTIONS["1080"];
const { width, height } = res; const { width, height } = res;
const frameRate = parseInt(this.screenShareFramerate); const frameRate = parseInt(this.screenShareFramerate);
@@ -363,10 +367,10 @@ export class LocalMediaService {
}; };
stopScreenShare = ( stopScreenShare = (
onTrackCleared: (track: MediaStreamTrack | null) => void, onTrackCleared: (_track: MediaStreamTrack | null) => void,
) => { ) => {
if (this.localScreenStream) { if (this.localScreenStream) {
this.localScreenStream.getTracks().forEach((track) => track.stop()); this.localScreenStream.getTracks().forEach((t) => t.stop());
this.localScreenStream = null; this.localScreenStream = null;
this.#setSharingScreen({ sharing: false }); this.#setSharingScreen({ sharing: false });
onTrackCleared(null); onTrackCleared(null);
@@ -17,15 +17,15 @@ export class PeerManagerService {
mediaType = $state<"voice" | "screen">("voice"); mediaType = $state<"voice" | "screen">("voice");
isDeafened = $state(false); isDeafened = $state(false);
targetBitrate = $state<number>(5000000); targetBitrate = $state<number>(5000000);
onNegotiationNeeded: (peerIdHex: string, pc: RTCPeerConnection) => void; onNegotiationNeeded: (_peerIdHex: string, _pc: RTCPeerConnection) => void;
onIceCandidate: (peerIdHex: string, candidate: RTCIceCandidate) => void; onIceCandidate: (_peerIdHex: string, _candidate: RTCIceCandidate) => void;
constructor( constructor(
identity: Identity | null, identity: Identity | null,
mediaType: "voice" | "screen", mediaType: "voice" | "screen",
isDeafened: boolean, isDeafened: boolean,
onNegotiationNeeded: (peerIdHex: string, pc: RTCPeerConnection) => void, onNegotiationNeeded: (_peerIdHex: string, _pc: RTCPeerConnection) => void,
onIceCandidate: (peerIdHex: string, candidate: RTCIceCandidate) => void, onIceCandidate: (_peerIdHex: string, _candidate: RTCIceCandidate) => void,
) { ) {
this.identity = identity; this.identity = identity;
this.mediaType = mediaType; this.mediaType = mediaType;
@@ -68,7 +68,7 @@ export class ScreenSharingWebRTCService {
const videoTrack = this.localScreenStream?.getVideoTracks()[0] || null; const videoTrack = this.localScreenStream?.getVideoTracks()[0] || null;
const audioTrack = this.localScreenStream?.getAudioTracks()[0] || null; const audioTrack = this.localScreenStream?.getAudioTracks()[0] || null;
// Accessing peers and peerStatuses to trigger effect on changes // Accessing peers and peerStatuses to trigger effect on changes
const _statuses = this.peerManager.peerStatuses; void this.peerManager.peerStatuses;
this.peerManager.peers.forEach(async (peer, peerIdHex) => { this.peerManager.peers.forEach(async (peer, peerIdHex) => {
const transceivers = peer.pc.getTransceivers(); const transceivers = peer.pc.getTransceivers();
let changed = false; let changed = false;
+6 -4
View File
@@ -1,5 +1,3 @@
import { Identity } from "spacetimedb";
export interface Peer { export interface Peer {
pc: RTCPeerConnection; pc: RTCPeerConnection;
audio?: HTMLAudioElement; audio?: HTMLAudioElement;
@@ -33,8 +31,12 @@ export interface LocalMediaState {
isSharingScreen: boolean; isSharingScreen: boolean;
toggleMute: () => void; toggleMute: () => void;
toggleDeafen: () => void; toggleDeafen: () => void;
startScreenShare: (peerManager: any) => Promise<void>; startScreenShare: (
stopScreenShare: (peerManager: any) => void; onTrackReady: (_track: MediaStreamTrack) => void,
) => Promise<void>;
stopScreenShare: (
onTrackCleared: (_track: MediaStreamTrack | null) => void,
) => void;
requestMic: () => Promise<void>; requestMic: () => Promise<void>;
releaseMic: () => void; releaseMic: () => void;
} }
+2 -1
View File
@@ -91,7 +91,8 @@ export class WebRTCService {
this.screen.identity = this.identity; this.screen.identity = this.identity;
this.screen.connectedChannelId = this.connectedChannelId; this.screen.connectedChannelId = this.connectedChannelId;
this.screen.localScreenStream = this.localMedia.localScreenStream; this.screen.localScreenStream = this.localMedia.localScreenStream;
this.screen.peerManager.targetBitrate = parseFloat(this.localMedia.screenShareBitrate) * 1000000; this.screen.peerManager.targetBitrate =
parseFloat(this.localMedia.screenShareBitrate) * 1000000;
}); });
// Sync mute/deafen to DB // Sync mute/deafen to DB
+2 -2
View File
@@ -113,8 +113,8 @@ export const optimizeEmoji = async (
// Draw image centered and cropped to square // Draw image centered and cropped to square
const scale = Math.max(DIM / img.width, DIM / img.height); const scale = Math.max(DIM / img.width, DIM / img.height);
const x = (DIM / 2) - (img.width / 2) * scale; const x = DIM / 2 - (img.width / 2) * scale;
const y = (DIM / 2) - (img.height / 2) * scale; const y = DIM / 2 - (img.height / 2) * scale;
ctx.drawImage(img, x, y, img.width * scale, img.height * scale); ctx.drawImage(img, x, y, img.width * scale, img.height * scale);
canvas.toBlob( canvas.toBlob(
+13 -6
View File
@@ -7,8 +7,8 @@ export { HOST_KEY, DB_NAME_KEY, getEnv };
const normalizeHost = (host: string) => { const normalizeHost = (host: string) => {
try { try {
const url = new URL(host.includes("://") ? host : `https://${host}`); const url = new URL(host.includes("://") ? host : `https://${host}`);
return url.origin.replace(/^http/, 'ws'); // Ensure ws/wss return url.origin.replace(/^http/, "ws"); // Ensure ws/wss
} catch (e) { } catch {
return host.trim().replace(/\/+$/, ""); // Fallback return host.trim().replace(/\/+$/, ""); // Fallback
} }
}; };
@@ -25,11 +25,15 @@ export const TokenStore = {
clear: (host: string, dbName: string) => { clear: (host: string, dbName: string) => {
const key = `${normalizeHost(host)}/${dbName}/auth_token`; const key = `${normalizeHost(host)}/${dbName}/auth_token`;
localStorage.removeItem(key); localStorage.removeItem(key);
} },
}; };
export const getStdbHost = () => localStorage.getItem(HOST_KEY) || getEnv("VITE_SPACETIMEDB_HOST", "https://maincloud.spacetimedb.com"); export const getStdbHost = () =>
export const getStdbDbName = () => localStorage.getItem(DB_NAME_KEY) || getEnv("VITE_SPACETIMEDB_DB_NAME", "ditchcord"); localStorage.getItem(HOST_KEY) ||
getEnv("VITE_SPACETIMEDB_HOST", "https://maincloud.spacetimedb.com");
export const getStdbDbName = () =>
localStorage.getItem(DB_NAME_KEY) ||
getEnv("VITE_SPACETIMEDB_DB_NAME", "ditchcord");
let _connection: DbConnection | null = null; let _connection: DbConnection | null = null;
export const getConnection = () => _connection; export const getConnection = () => _connection;
@@ -89,7 +93,10 @@ class ConnectionManager {
}; };
} }
export const connectionBuilder = (onReconnectTrigger: () => void, oidcToken?: string) => { export const connectionBuilder = (
onReconnectTrigger: () => void,
oidcToken?: string,
) => {
const host = getStdbHost(); const host = getStdbHost();
const dbName = getStdbDbName(); const dbName = getStdbDbName();
+6 -3
View File
@@ -1,12 +1,15 @@
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import { svelte } from "@sveltejs/vite-plugin-svelte"; import { svelte } from "@sveltejs/vite-plugin-svelte";
import basicSsl from '@vitejs/plugin-basic-ssl'; import basicSsl from "@vitejs/plugin-basic-ssl";
import { cloudflare } from "@cloudflare/vite-plugin";
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
process.env.VITE_USE_SSL === 'true' ? basicSsl() : [], process.env.VITE_USE_SSL === "true" ? basicSsl() : [],
svelte(), svelte(),
cloudflare()
], ],
// Prevent vite from obscuring rust errors // Prevent vite from obscuring rust errors
clearScreen: false, clearScreen: false,
@@ -14,7 +17,7 @@ export default defineConfig({
// Tauri expects a fixed port, fail if that port is not available // Tauri expects a fixed port, fail if that port is not available
server: { server: {
strictPort: true, strictPort: true,
port: process.env.VITE_USE_SSL === 'true' ? 5174 : 5173, port: process.env.VITE_USE_SSL === "true" ? 5174 : 5173,
host: "0.0.0.0", host: "0.0.0.0",
}, },
}); });