lint fixes
This commit is contained in:
@@ -54,3 +54,9 @@ src-tauri/gen/
|
||||
# Ignore this file
|
||||
.gitignore
|
||||
|
||||
|
||||
# wrangler files
|
||||
.wrangler
|
||||
.dev.vars*
|
||||
!.dev.vars.example
|
||||
!.env.example
|
||||
|
||||
@@ -1,2 +1,10 @@
|
||||
dist/
|
||||
node_modules/
|
||||
.git/
|
||||
.cursor/
|
||||
coverage/
|
||||
spacetimedb/dist/
|
||||
src/module_bindings/
|
||||
src-tauri/target/
|
||||
src-tauri/gen/
|
||||
pnpm-lock.yaml
|
||||
|
||||
+4
-3
@@ -3,7 +3,7 @@ import js from "@eslint/js";
|
||||
import tseslint from "typescript-eslint";
|
||||
import svelte from "eslint-plugin-svelte";
|
||||
import svelteParser from "svelte-eslint-parser";
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { defineConfig } from "eslint/config";
|
||||
|
||||
export default defineConfig([
|
||||
tseslint.configs.recommended,
|
||||
@@ -32,7 +32,7 @@ export default defineConfig([
|
||||
"README.md",
|
||||
".github/",
|
||||
".cursor/",
|
||||
"spacetimedb/dist",
|
||||
"spacetimedb/",
|
||||
"src/module_bindings/",
|
||||
"src-tauri/**",
|
||||
],
|
||||
@@ -64,13 +64,14 @@ export default defineConfig([
|
||||
projectService: true,
|
||||
extraFileExtensions: [".svelte"],
|
||||
svelteFeatures: {
|
||||
runes: true
|
||||
runes: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@/no-trailing-spaces": "warn",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
|
||||
+7
-3
@@ -9,13 +9,15 @@
|
||||
"build": "tsc -b && vite build",
|
||||
"format": "prettier . --write --ignore-path ../../.prettierignore",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"preview": "pnpm run build && wrangler dev",
|
||||
"test": "vitest run",
|
||||
"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": "spacetime publish --module-path spacetimedb --server maincloud",
|
||||
"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": {
|
||||
"@fortawesome/fontawesome-free": "^7.2.0",
|
||||
@@ -27,6 +29,7 @@
|
||||
"svelte-check": "^4.4.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/vite-plugin": "^1.31.0",
|
||||
"@eslint/js": "^9.17.0",
|
||||
"@tauri-apps/cli": "^2.10.1",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
@@ -44,6 +47,7 @@
|
||||
"typescript": "~5.6.2",
|
||||
"typescript-eslint": "^8.18.2",
|
||||
"vite": "^8.0.3",
|
||||
"vitest": "3.2.4"
|
||||
"vitest": "3.2.4",
|
||||
"wrangler": "^4.80.0"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+826
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,9 @@ const user = table(
|
||||
{
|
||||
name: "user",
|
||||
public: true,
|
||||
indexes: [{ accessor: "by_online", algorithm: "btree", columns: ["online"] }],
|
||||
indexes: [
|
||||
{ accessor: "by_online", algorithm: "btree", columns: ["online"] },
|
||||
],
|
||||
},
|
||||
{
|
||||
identity: t.identity().primaryKey(),
|
||||
@@ -467,7 +469,12 @@ export const upload_avatar = spacetimedb.reducer(
|
||||
if (data.length > 1024 * 1024) {
|
||||
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);
|
||||
if (!user) throw new SenderError("User not found");
|
||||
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) => {
|
||||
let hasServers = false;
|
||||
for (const _ of ctx.db.server.iter()) {
|
||||
for (const _server of ctx.db.server.iter()) {
|
||||
hasServers = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "enables the default permissions",
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"core:default"
|
||||
]
|
||||
"windows": ["main"],
|
||||
"permissions": ["core:default"]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "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 SpacetimeProvider from "../SpacetimeProvider.svelte";
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
children: any,
|
||||
reconnect: () => void,
|
||||
showServerSettings: boolean,
|
||||
onToggleServerSettings: (val: boolean) => void,
|
||||
onToggleServerSettings: (_val: boolean) => void,
|
||||
reconnectKey: number
|
||||
}>();
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
const MAINCLOUD_URI = "https://maincloud.spacetimedb.com";
|
||||
let isMaincloud = $state(true);
|
||||
|
||||
let userWantsToConnect = $state(false);
|
||||
let isSettingsExpanded = $state(false);
|
||||
|
||||
onMount(() => {
|
||||
@@ -48,10 +47,6 @@
|
||||
if (stdbDbName) localStorage.setItem(DB_NAME_KEY, stdbDbName);
|
||||
});
|
||||
|
||||
function handleAuthError(error: string | null) {
|
||||
authError = error;
|
||||
}
|
||||
|
||||
const isBypassEnabled =
|
||||
import.meta.env.VITE_BYPASS_AUTH === "true" ||
|
||||
new URLSearchParams(window.location.search).has("bypass_auth");
|
||||
@@ -77,7 +72,6 @@
|
||||
<SpacetimeProvider
|
||||
onReconnectTrigger={reconnect}
|
||||
onCancel={() => {
|
||||
userWantsToConnect = false;
|
||||
isGuest = false;
|
||||
}}
|
||||
>
|
||||
@@ -106,7 +100,6 @@
|
||||
<div style="display: flex; flex-direction: column; gap: 12px; width: 100%">
|
||||
<button
|
||||
onclick={() => {
|
||||
userWantsToConnect = true;
|
||||
auth.signinRedirect();
|
||||
}}
|
||||
disabled={auth.isLoading}
|
||||
@@ -119,7 +112,6 @@
|
||||
<button
|
||||
onclick={() => {
|
||||
isGuest = true;
|
||||
userWantsToConnect = true;
|
||||
}}
|
||||
class="btn-secondary"
|
||||
style="width: 100%"
|
||||
|
||||
@@ -105,7 +105,7 @@ class AuthStore {
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
keysToRemove.forEach(key => localStorage.removeItem(key));
|
||||
keysToRemove.forEach((key) => localStorage.removeItem(key));
|
||||
|
||||
if (this.#user) {
|
||||
await this.#userManager.signoutRedirect();
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
import { portal } from "../../portal";
|
||||
import type { ChatService } from "../services/chat.svelte";
|
||||
import type { WebRTCService } from "../services/webrtc/webrtc.svelte";
|
||||
import type { WebRTCStats } from "../services/webrtc/types";
|
||||
import Avatar from "./Avatar.svelte";
|
||||
import type * as Types from "../../module_bindings/types";
|
||||
|
||||
const chat = getContext<ChatService>("chat");
|
||||
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 amISharing = chat.currentVoiceState?.isSharingScreen}
|
||||
{@const voiceStatusColor = isMe ? "green" : getStatusColor(status)}
|
||||
{@const videoStatusColor = isMe ? (isSharing ? "green" : undefined) : (isSharing ? getStatusColor(status) : undefined)}
|
||||
{@const isLocalUserInThisChannel = chat.connectedVoiceChannel?.id === channel.id}
|
||||
|
||||
<div
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onMount } from "svelte";
|
||||
import { EMOJIS, type EmojiInfo } from "../emojis";
|
||||
import { EMOJIS } from "../emojis";
|
||||
import type { ChatService } from "../services/chat.svelte";
|
||||
import type * as Types from "../../module_bindings/types";
|
||||
import { optimizeEmoji } from "../utils";
|
||||
|
||||
let { onSelect, onClose } = $props<{
|
||||
onSelect: (emoji?: string, customEmojiId?: bigint) => void;
|
||||
onClose: () => void;
|
||||
let { onSelect } = $props<{
|
||||
onSelect: (_emoji?: string, _customEmojiId?: bigint) => void;
|
||||
}>();
|
||||
|
||||
const chat = getContext<ChatService>("chat");
|
||||
@@ -22,7 +20,7 @@
|
||||
recentEmojis = JSON.parse(saved).map((id: any) =>
|
||||
typeof id === 'string' && id.startsWith('custom:') ? BigInt(id.split(':')[1]) : id
|
||||
);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
recentEmojis = [];
|
||||
}
|
||||
}
|
||||
@@ -65,8 +63,8 @@
|
||||
{ id: 'custom', name: 'Custom', icon: '✨' },
|
||||
];
|
||||
|
||||
async function handleCustomUpload(e: Event) {
|
||||
const file = (e.target as HTMLInputElement).files?.[0];
|
||||
async function handleCustomUpload(_e: Event) {
|
||||
const file = (_e.target as HTMLInputElement).files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
// We'll assume the user provides a name or we use the filename
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import type * as Types from "../../module_bindings/types";
|
||||
|
||||
let { image, onClose }: { image: Types.Image, onClose: () => void } = $props();
|
||||
@@ -20,7 +19,6 @@
|
||||
let hasMoved = false;
|
||||
|
||||
const isZoomed = $derived(zoomLevel > baseScale + 0.001);
|
||||
const currentActualScale = $derived(zoomLevel);
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") {
|
||||
@@ -130,7 +128,7 @@
|
||||
isDragging = false;
|
||||
}
|
||||
|
||||
function handleOverlayClick(e: MouseEvent) {
|
||||
function handleOverlayClick() {
|
||||
const clickDuration = Date.now() - mousedownTime;
|
||||
if (hasMoved || clickDuration > 300) return;
|
||||
onClose();
|
||||
@@ -424,6 +422,6 @@
|
||||
}
|
||||
|
||||
.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>
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
if (!title && !description && !image) {
|
||||
throw new Error("No useful metadata found");
|
||||
}
|
||||
} catch (err) {
|
||||
} catch {
|
||||
if (!isCancelled) {
|
||||
error = true;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
let {
|
||||
activeChannelId,
|
||||
activeThreadId,
|
||||
activeThreadId: _activeThreadId,
|
||||
isFullyAuthenticated
|
||||
} = $props<{
|
||||
activeChannelId: bigint | null,
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
let {
|
||||
threadMessages,
|
||||
users,
|
||||
identity,
|
||||
images
|
||||
identity: _identity,
|
||||
images: _images
|
||||
}: {
|
||||
threadMessages: readonly Types.Message[],
|
||||
users: readonly Types.User[],
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Identity } from "spacetimedb";
|
||||
import type * as Types from "../../module_bindings/types";
|
||||
import { useReducer } from "spacetimedb/svelte";
|
||||
import { reducers } from "../../module_bindings";
|
||||
import ThreadMessageList from "./ThreadMessageList.svelte";
|
||||
import ThreadMessageInput from "./ThreadMessageInput.svelte";
|
||||
import RichText from "./RichText.svelte";
|
||||
@@ -15,7 +13,7 @@
|
||||
pendingThreadParentMessageId,
|
||||
setPendingThreadParentMessageId,
|
||||
activeChannelId,
|
||||
activeServer,
|
||||
activeServer: _activeServer,
|
||||
isFullyAuthenticated,
|
||||
users,
|
||||
identity,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Identity } from "spacetimedb";
|
||||
import { getContext, onMount, onDestroy } from "svelte";
|
||||
import { onMount } from "svelte";
|
||||
import Avatar from "./Avatar.svelte";
|
||||
import type { WebRTCService } from "../services/webrtc/webrtc.svelte";
|
||||
import type * as Types from "../../module_bindings/types";
|
||||
|
||||
let {
|
||||
@@ -27,8 +26,6 @@
|
||||
users: readonly Types.User[];
|
||||
}>();
|
||||
|
||||
const webrtc = getContext<WebRTCService>("webrtc");
|
||||
|
||||
let videoRef = $state<HTMLVideoElement | null>(null);
|
||||
let containerRef = $state<HTMLDivElement | null>(null);
|
||||
let isMuted = $state(false);
|
||||
|
||||
+15
-3
@@ -1024,7 +1024,11 @@ export const EMOJIS: EmojiInfo[] = [
|
||||
{ char: "🈁", name: "Japanese “here” button", category: "symbols" },
|
||||
{ char: "🈂️", name: "Japanese “service charge” 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 “bargain” 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 “passing grade” 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 “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: "red circle", category: "symbols" },
|
||||
{ char: "🟠", name: "orange circle", category: "symbols" },
|
||||
|
||||
@@ -39,7 +39,7 @@ export class ChatService {
|
||||
|
||||
// Background effect to populate avatar cache
|
||||
$effect(() => {
|
||||
const usersWithAvatars = this.users.filter(u => u.avatarId);
|
||||
const usersWithAvatars = this.users.filter((u) => u.avatarId);
|
||||
for (const user of usersWithAvatars) {
|
||||
const avatarId = user.avatarId!;
|
||||
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
|
||||
const img = this.images.find(i => i.id === id);
|
||||
const img = this.images.find((i) => i.id === id);
|
||||
if (img) {
|
||||
const url = await imageCache.set(id, img.data, img.mimeType);
|
||||
this.#avatarUrls.set(idStr, url);
|
||||
@@ -356,7 +356,11 @@ export class ChatService {
|
||||
this.#msg.handleLoadMoreMessages();
|
||||
};
|
||||
|
||||
handleToggleReaction = (messageId: bigint, emoji?: string, customEmojiId?: bigint) => {
|
||||
handleToggleReaction = (
|
||||
messageId: bigint,
|
||||
emoji?: string,
|
||||
customEmojiId?: bigint,
|
||||
) => {
|
||||
this.#msg.toggleReaction(messageId, emoji, customEmojiId);
|
||||
};
|
||||
|
||||
@@ -377,7 +381,11 @@ export class ChatService {
|
||||
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);
|
||||
};
|
||||
|
||||
@@ -420,7 +428,12 @@ export class ChatService {
|
||||
})
|
||||
.map((ta) => {
|
||||
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)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -26,12 +26,18 @@ export class MessagingService {
|
||||
messageLimit = $state(50);
|
||||
isLoadingMore = $state(false);
|
||||
|
||||
constructor(db: DatabaseService, nav: NavigationService, identity: () => Identity | null) {
|
||||
constructor(
|
||||
db: DatabaseService,
|
||||
nav: NavigationService,
|
||||
identity: () => Identity | null,
|
||||
) {
|
||||
this.#db = db;
|
||||
this.#nav = nav;
|
||||
this.#identity = identity;
|
||||
|
||||
this.#createThreadWithMessageReducer = useReducer(reducers.createThreadWithMessage);
|
||||
this.#createThreadWithMessageReducer = useReducer(
|
||||
reducers.createThreadWithMessage,
|
||||
);
|
||||
this.#sendMessageReducer = useReducer(reducers.sendMessage);
|
||||
this.#uploadImageReducer = useReducer(reducers.uploadImage);
|
||||
this.#uploadCustomEmojiReducer = useReducer(reducers.uploadCustomEmoji);
|
||||
@@ -42,9 +48,15 @@ export class MessagingService {
|
||||
const [messageImagesStore] = useTable(tables.message_image);
|
||||
const [messageReactionsStore] = useTable(tables.message_reaction);
|
||||
|
||||
messagesStore.subscribe((v: readonly Types.Message[]) => (this.#allMessages = v));
|
||||
messageImagesStore.subscribe((v: readonly Types.MessageImage[]) => (this.#messageImages = v));
|
||||
messageReactionsStore.subscribe((v: readonly Types.MessageReaction[]) => (this.#messageReactions = v));
|
||||
messagesStore.subscribe(
|
||||
(v: readonly Types.Message[]) => (this.#allMessages = v),
|
||||
);
|
||||
messageImagesStore.subscribe(
|
||||
(v: readonly Types.MessageImage[]) => (this.#messageImages = v),
|
||||
);
|
||||
messageReactionsStore.subscribe(
|
||||
(v: readonly Types.MessageReaction[]) => (this.#messageReactions = v),
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
const channelId = this.#nav.activeChannelId;
|
||||
@@ -57,38 +69,53 @@ export class MessagingService {
|
||||
if (!conn) return;
|
||||
|
||||
untrack(() => {
|
||||
const queries = [
|
||||
"SELECT * FROM server",
|
||||
"SELECT * FROM custom_emoji",
|
||||
];
|
||||
const queries = ["SELECT * FROM server", "SELECT * FROM custom_emoji"];
|
||||
|
||||
// 1. Surgical Membership & Identity Pruning
|
||||
// Only load our own memberships and those of the active server
|
||||
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) {
|
||||
queries.push(`SELECT * FROM server_member WHERE identity = 0x${identity.toHexString()}`);
|
||||
queries.push(
|
||||
`SELECT * FROM server_member WHERE identity = 0x${identity.toHexString()}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (serverId) {
|
||||
// 2. Metadata for the Active Server
|
||||
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
|
||||
queries.push(`SELECT * FROM voice_state 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})`);
|
||||
queries.push(
|
||||
`SELECT * FROM voice_state 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)
|
||||
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 (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
|
||||
@@ -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 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(`SELECT * FROM message_reaction WHERE message_id IN ${visibleMsgSubquery}`);
|
||||
queries.push(
|
||||
`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
|
||||
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
|
||||
$effect(() => {
|
||||
const _ = this.#nav.activeChannelId;
|
||||
void this.#nav.activeChannelId;
|
||||
untrack(() => {
|
||||
this.messageLimit = 50;
|
||||
});
|
||||
@@ -216,11 +247,19 @@ export class MessagingService {
|
||||
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 });
|
||||
};
|
||||
|
||||
toggleReaction = (messageId: bigint, emoji?: string, customEmojiId?: bigint) => {
|
||||
toggleReaction = (
|
||||
messageId: bigint,
|
||||
emoji?: string,
|
||||
customEmojiId?: bigint,
|
||||
) => {
|
||||
this.#toggleReactionReducer({ messageId, emoji, customEmojiId });
|
||||
};
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ export class ChannelAudioWebRTCService {
|
||||
$effect(() => {
|
||||
const audioTrack = this.localStream?.getAudioTracks()[0] || null;
|
||||
// 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) => {
|
||||
const transceivers = peer.pc.getTransceivers();
|
||||
if (transceivers[0] && transceivers[0].sender.track !== audioTrack) {
|
||||
|
||||
@@ -224,7 +224,10 @@ export class LocalMediaService {
|
||||
|
||||
// Persist screen share settings
|
||||
$effect(() => {
|
||||
localStorage.setItem("screen_share_resolution", this.screenShareResolution);
|
||||
localStorage.setItem(
|
||||
"screen_share_resolution",
|
||||
this.screenShareResolution,
|
||||
);
|
||||
});
|
||||
$effect(() => {
|
||||
localStorage.setItem("screen_share_framerate", this.screenShareFramerate);
|
||||
@@ -331,7 +334,8 @@ export class LocalMediaService {
|
||||
try {
|
||||
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 frameRate = parseInt(this.screenShareFramerate);
|
||||
|
||||
@@ -363,10 +367,10 @@ export class LocalMediaService {
|
||||
};
|
||||
|
||||
stopScreenShare = (
|
||||
onTrackCleared: (track: MediaStreamTrack | null) => void,
|
||||
onTrackCleared: (_track: MediaStreamTrack | null) => void,
|
||||
) => {
|
||||
if (this.localScreenStream) {
|
||||
this.localScreenStream.getTracks().forEach((track) => track.stop());
|
||||
this.localScreenStream.getTracks().forEach((t) => t.stop());
|
||||
this.localScreenStream = null;
|
||||
this.#setSharingScreen({ sharing: false });
|
||||
onTrackCleared(null);
|
||||
|
||||
@@ -17,15 +17,15 @@ export class PeerManagerService {
|
||||
mediaType = $state<"voice" | "screen">("voice");
|
||||
isDeafened = $state(false);
|
||||
targetBitrate = $state<number>(5000000);
|
||||
onNegotiationNeeded: (peerIdHex: string, pc: RTCPeerConnection) => void;
|
||||
onIceCandidate: (peerIdHex: string, candidate: RTCIceCandidate) => void;
|
||||
onNegotiationNeeded: (_peerIdHex: string, _pc: RTCPeerConnection) => void;
|
||||
onIceCandidate: (_peerIdHex: string, _candidate: RTCIceCandidate) => void;
|
||||
|
||||
constructor(
|
||||
identity: Identity | null,
|
||||
mediaType: "voice" | "screen",
|
||||
isDeafened: boolean,
|
||||
onNegotiationNeeded: (peerIdHex: string, pc: RTCPeerConnection) => void,
|
||||
onIceCandidate: (peerIdHex: string, candidate: RTCIceCandidate) => void,
|
||||
onNegotiationNeeded: (_peerIdHex: string, _pc: RTCPeerConnection) => void,
|
||||
onIceCandidate: (_peerIdHex: string, _candidate: RTCIceCandidate) => void,
|
||||
) {
|
||||
this.identity = identity;
|
||||
this.mediaType = mediaType;
|
||||
|
||||
@@ -68,7 +68,7 @@ export class ScreenSharingWebRTCService {
|
||||
const videoTrack = this.localScreenStream?.getVideoTracks()[0] || null;
|
||||
const audioTrack = this.localScreenStream?.getAudioTracks()[0] || null;
|
||||
// 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) => {
|
||||
const transceivers = peer.pc.getTransceivers();
|
||||
let changed = false;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Identity } from "spacetimedb";
|
||||
|
||||
export interface Peer {
|
||||
pc: RTCPeerConnection;
|
||||
audio?: HTMLAudioElement;
|
||||
@@ -33,8 +31,12 @@ export interface LocalMediaState {
|
||||
isSharingScreen: boolean;
|
||||
toggleMute: () => void;
|
||||
toggleDeafen: () => void;
|
||||
startScreenShare: (peerManager: any) => Promise<void>;
|
||||
stopScreenShare: (peerManager: any) => void;
|
||||
startScreenShare: (
|
||||
onTrackReady: (_track: MediaStreamTrack) => void,
|
||||
) => Promise<void>;
|
||||
stopScreenShare: (
|
||||
onTrackCleared: (_track: MediaStreamTrack | null) => void,
|
||||
) => void;
|
||||
requestMic: () => Promise<void>;
|
||||
releaseMic: () => void;
|
||||
}
|
||||
|
||||
@@ -91,7 +91,8 @@ export class WebRTCService {
|
||||
this.screen.identity = this.identity;
|
||||
this.screen.connectedChannelId = this.connectedChannelId;
|
||||
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
|
||||
|
||||
+2
-2
@@ -113,8 +113,8 @@ export const optimizeEmoji = async (
|
||||
|
||||
// Draw image centered and cropped to square
|
||||
const scale = Math.max(DIM / img.width, DIM / img.height);
|
||||
const x = (DIM / 2) - (img.width / 2) * scale;
|
||||
const y = (DIM / 2) - (img.height / 2) * scale;
|
||||
const x = DIM / 2 - (img.width / 2) * scale;
|
||||
const y = DIM / 2 - (img.height / 2) * scale;
|
||||
ctx.drawImage(img, x, y, img.width * scale, img.height * scale);
|
||||
|
||||
canvas.toBlob(
|
||||
|
||||
+13
-6
@@ -7,8 +7,8 @@ export { HOST_KEY, DB_NAME_KEY, getEnv };
|
||||
const normalizeHost = (host: string) => {
|
||||
try {
|
||||
const url = new URL(host.includes("://") ? host : `https://${host}`);
|
||||
return url.origin.replace(/^http/, 'ws'); // Ensure ws/wss
|
||||
} catch (e) {
|
||||
return url.origin.replace(/^http/, "ws"); // Ensure ws/wss
|
||||
} catch {
|
||||
return host.trim().replace(/\/+$/, ""); // Fallback
|
||||
}
|
||||
};
|
||||
@@ -25,11 +25,15 @@ export const TokenStore = {
|
||||
clear: (host: string, dbName: string) => {
|
||||
const key = `${normalizeHost(host)}/${dbName}/auth_token`;
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const getStdbHost = () => 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");
|
||||
export const getStdbHost = () =>
|
||||
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;
|
||||
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 dbName = getStdbDbName();
|
||||
|
||||
|
||||
+6
-3
@@ -1,12 +1,15 @@
|
||||
import { defineConfig } from "vite";
|
||||
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/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
process.env.VITE_USE_SSL === 'true' ? basicSsl() : [],
|
||||
process.env.VITE_USE_SSL === "true" ? basicSsl() : [],
|
||||
svelte(),
|
||||
cloudflare()
|
||||
],
|
||||
// Prevent vite from obscuring rust errors
|
||||
clearScreen: false,
|
||||
@@ -14,7 +17,7 @@ export default defineConfig({
|
||||
// Tauri expects a fixed port, fail if that port is not available
|
||||
server: {
|
||||
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",
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user