server settings
This commit is contained in:
+1
-1
@@ -3,6 +3,6 @@
|
|||||||
"run": "pnpm run dev"
|
"run": "pnpm run dev"
|
||||||
},
|
},
|
||||||
"server": "maincloud",
|
"server": "maincloud",
|
||||||
"database": "ditchcord",
|
"database": "zep",
|
||||||
"module-path": "./spacetimedb"
|
"module-path": "./spacetimedb"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"database": "ditchcord"
|
"database": "zep"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const server = table(
|
|||||||
id: t.u64().primaryKey().autoInc(),
|
id: t.u64().primaryKey().autoInc(),
|
||||||
name: t.string(),
|
name: t.string(),
|
||||||
owner: t.identity().optional(),
|
owner: t.identity().optional(),
|
||||||
|
avatar_id: t.u64().optional(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -545,6 +546,76 @@ export const upload_banner = spacetimedb.reducer(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const upload_server_avatar = spacetimedb.reducer(
|
||||||
|
{ serverId: t.u64(), data: t.byteArray(), mimeType: t.string() },
|
||||||
|
(ctx, { serverId, data, mimeType }) => {
|
||||||
|
if (data.length > 1024 * 1024) {
|
||||||
|
throw new SenderError("Avatar exceeds 1MB limit");
|
||||||
|
}
|
||||||
|
const s = ctx.db.server.id.find(serverId);
|
||||||
|
if (!s) throw new SenderError("Server not found");
|
||||||
|
/* owner check temporarily disabled */
|
||||||
|
/* if (s.owner?.toHexString() !== ctx.sender.toHexString()) {
|
||||||
|
throw new SenderError("Only the server owner can change the avatar");
|
||||||
|
} */
|
||||||
|
|
||||||
|
const img = ctx.db.image.insert({
|
||||||
|
id: 0n,
|
||||||
|
data,
|
||||||
|
mime_type: mimeType,
|
||||||
|
name: "server_avatar",
|
||||||
|
});
|
||||||
|
ctx.db.server.id.update({ ...s, avatar_id: img.id });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const update_server_name = spacetimedb.reducer(
|
||||||
|
{ serverId: t.u64(), name: t.string() },
|
||||||
|
(ctx, { serverId, name }) => {
|
||||||
|
validateName(name);
|
||||||
|
const s = ctx.db.server.id.find(serverId);
|
||||||
|
if (!s) throw new SenderError("Server not found");
|
||||||
|
/* owner check temporarily disabled */
|
||||||
|
/* if (s.owner?.toHexString() !== ctx.sender.toHexString()) {
|
||||||
|
throw new SenderError("Only the server owner can rename the server");
|
||||||
|
} */
|
||||||
|
ctx.db.server.id.update({ ...s, name });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const delete_server = spacetimedb.reducer(
|
||||||
|
{ serverId: t.u64() },
|
||||||
|
(ctx, { serverId }) => {
|
||||||
|
const s = ctx.db.server.id.find(serverId);
|
||||||
|
if (!s) throw new SenderError("Server not found");
|
||||||
|
|
||||||
|
/* owner check temporarily disabled */
|
||||||
|
/* if (s.owner?.toHexString() !== ctx.sender.toHexString()) {
|
||||||
|
throw new SenderError("Only the server owner can delete the server");
|
||||||
|
} */
|
||||||
|
|
||||||
|
// 1. Delete all channels in this server
|
||||||
|
for (const c of ctx.db.channel.by_server_id.filter(serverId)) {
|
||||||
|
// Cleanup messages in channel
|
||||||
|
for (const m of ctx.db.message.by_channel_id.filter(c.id)) {
|
||||||
|
ctx.db.message.id.delete(m.id);
|
||||||
|
}
|
||||||
|
for (const rm of ctx.db.recent_message.by_channel.filter(c.id)) {
|
||||||
|
ctx.db.recent_message.id.delete(rm.id);
|
||||||
|
}
|
||||||
|
ctx.db.channel.id.delete(c.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Delete all memberships
|
||||||
|
for (const m of ctx.db.server_member.by_server_id.filter(serverId)) {
|
||||||
|
ctx.db.server_member.id.delete(m.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Delete the server itself
|
||||||
|
ctx.db.server.id.delete(serverId);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export const toggle_reaction = spacetimedb.reducer(
|
export const toggle_reaction = spacetimedb.reducer(
|
||||||
{
|
{
|
||||||
messageId: t.u64(),
|
messageId: t.u64(),
|
||||||
@@ -651,7 +722,7 @@ export const create_server = spacetimedb.reducer(
|
|||||||
"You must be logged in via OIDC to create a server",
|
"You must be logged in via OIDC to create a server",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const s = ctx.db.server.insert({ id: 0n, name, owner: ctx.sender });
|
const s = ctx.db.server.insert({ id: 0n, name, owner: ctx.sender, avatar_id: undefined });
|
||||||
ctx.db.server_member.insert({
|
ctx.db.server_member.insert({
|
||||||
id: 0n,
|
id: 0n,
|
||||||
identity: ctx.sender,
|
identity: ctx.sender,
|
||||||
@@ -1227,6 +1298,7 @@ export const init = spacetimedb.init((ctx) => {
|
|||||||
id: 0n,
|
id: 0n,
|
||||||
name: "Zep",
|
name: "Zep",
|
||||||
owner: undefined,
|
owner: undefined,
|
||||||
|
avatar_id: undefined,
|
||||||
});
|
});
|
||||||
ctx.db.channel.insert({
|
ctx.db.channel.insert({
|
||||||
id: 0n,
|
id: 0n,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
import VideoGrid from "./components/VideoGrid.svelte";
|
import VideoGrid from "./components/VideoGrid.svelte";
|
||||||
import Avatar from "./components/Avatar.svelte";
|
import Avatar from "./components/Avatar.svelte";
|
||||||
import SettingsPanel from "./components/SettingsPanel.svelte";
|
import SettingsPanel from "./components/SettingsPanel.svelte";
|
||||||
|
import ServerSettingsPanel from "./components/ServerSettingsPanel.svelte";
|
||||||
import ImageViewer from "./components/ImageViewer.svelte";
|
import ImageViewer from "./components/ImageViewer.svelte";
|
||||||
import ProfileModal from "./components/ProfileModal.svelte";
|
import ProfileModal from "./components/ProfileModal.svelte";
|
||||||
import UserContextMenu from "./components/UserContextMenu.svelte";
|
import UserContextMenu from "./components/UserContextMenu.svelte";
|
||||||
@@ -215,6 +216,12 @@
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if chat.showServerSettings}
|
||||||
|
<ServerSettingsPanel
|
||||||
|
onClose={() => (chat.showServerSettings = false)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if chat.viewingImageId}
|
{#if chat.viewingImageId}
|
||||||
{@const image = chat.images.find(img => img.id === chat.viewingImageId)}
|
{@const image = chat.images.find(img => img.id === chat.viewingImageId)}
|
||||||
{#if image}
|
{#if image}
|
||||||
|
|||||||
@@ -63,25 +63,26 @@
|
|||||||
const heightDiff = newHeight - lastHeight;
|
const heightDiff = newHeight - lastHeight;
|
||||||
|
|
||||||
if (heightDiff !== 0) {
|
if (heightDiff !== 0) {
|
||||||
const messages = chat.channelMessages;
|
const messagesList = chat.channelMessages;
|
||||||
const lastMsg = messages[messages.length - 1];
|
const lastMsg = messagesList[messagesList.length - 1];
|
||||||
const myIdHex = chat.identity?.toHexString();
|
const myIdHex = chat.identity?.toHexString();
|
||||||
const lastMsgSenderHex = lastMsg?.sender.toHexString();
|
const lastMsgSenderHex = lastMsg?.sender.toHexString();
|
||||||
const sentByMe = myIdHex && lastMsgSenderHex && myIdHex === lastMsgSenderHex;
|
const sentByMe = myIdHex && lastMsgSenderHex && myIdHex === lastMsgSenderHex;
|
||||||
|
|
||||||
// If the user is NOT at the bottom, adjust scroll to keep position
|
// If we ARE at the bottom, OR we just sent a message, keep us pinned to the NEW bottom
|
||||||
if (!isAtBottom && !isPrepending && !sentByMe) {
|
if (isAtBottom || sentByMe) {
|
||||||
scrollContainer!.scrollTop += heightDiff;
|
|
||||||
}
|
|
||||||
// If we ARE at the bottom, OR we just sent a message, keep us pinned
|
|
||||||
else if (isAtBottom || sentByMe) {
|
|
||||||
scrollContainer!.scrollTop = target.scrollHeight;
|
scrollContainer!.scrollTop = target.scrollHeight;
|
||||||
|
}
|
||||||
|
// If the user is NOT at the bottom, and they aren't loading more history,
|
||||||
|
// we only adjust scroll if something above them expanded.
|
||||||
|
// For simplicity, we assume expansion happens at the point of interaction.
|
||||||
|
else if (!isPrepending) {
|
||||||
|
// No-op for expansion at/below viewport to let browser maintain focus on the item
|
||||||
}
|
}
|
||||||
lastHeight = newHeight;
|
lastHeight = newHeight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
observer.observe(scrollContainer);
|
observer.observe(scrollContainer);
|
||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}
|
}
|
||||||
@@ -94,6 +95,7 @@
|
|||||||
const lastMsgSenderHex = lastMsg?.sender.toHexString();
|
const lastMsgSenderHex = lastMsg?.sender.toHexString();
|
||||||
const sentByMe = myIdHex && lastMsgSenderHex && myIdHex === lastMsgSenderHex;
|
const sentByMe = myIdHex && lastMsgSenderHex && myIdHex === lastMsgSenderHex;
|
||||||
|
|
||||||
|
// Only force scroll to bottom if we were already anchored or we just sent the message
|
||||||
if (isAtBottom || sentByMe) {
|
if (isAtBottom || sentByMe) {
|
||||||
scrollToBottom(false);
|
scrollToBottom(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,14 +137,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onImageLoad(e: Event) {
|
function onImageLoad(e: Event) {
|
||||||
const img = e.currentTarget as HTMLImageElement;
|
if (onLoad) {
|
||||||
const list = img.closest(".message-list");
|
onLoad();
|
||||||
if (list) {
|
|
||||||
if (onLoad) {
|
|
||||||
onLoad();
|
|
||||||
} else {
|
|
||||||
list.scrollTop = list.scrollHeight;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,16 @@
|
|||||||
|
|
||||||
<div class="server-list">
|
<div class="server-list">
|
||||||
{#each chat.joinedServers as server (server.id.toString())}
|
{#each chat.joinedServers as server (server.id.toString())}
|
||||||
|
{@const avatarUrl = chat.getServerAvatarUrl(server)}
|
||||||
<button
|
<button
|
||||||
class="server-icon {chat.activeServerId === server.id ? 'active' : ''}"
|
class="server-icon {chat.activeServerId === server.id ? 'active' : ''}"
|
||||||
onclick={() => (chat.activeServerId = server.id)}
|
onclick={() => (chat.activeServerId = server.id)}
|
||||||
title={server.name}
|
title={server.name}
|
||||||
|
style={avatarUrl ? `background-image: url(${avatarUrl}); background-size: cover; background-position: center; border: none;` : ""}
|
||||||
>
|
>
|
||||||
{server.name.substring(0, 2).toUpperCase()}
|
{#if !avatarUrl}
|
||||||
|
{server.name.substring(0, 2).toUpperCase()}
|
||||||
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,540 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { getContext } from "svelte";
|
||||||
|
import type { ChatService } from "../services/chat.svelte";
|
||||||
|
import { optimizeEmoji } from "../utils";
|
||||||
|
import { portal } from "../../portal";
|
||||||
|
|
||||||
|
let { onClose } = $props<{ onClose: () => void }>();
|
||||||
|
|
||||||
|
const chat = getContext<ChatService>("chat");
|
||||||
|
|
||||||
|
let activeCategory = $state("overview");
|
||||||
|
let serverName = $state("");
|
||||||
|
let avatarPreview = $state<string | null>(null);
|
||||||
|
let newAvatarFile = $state<File | null>(null);
|
||||||
|
let isUploading = $state(false);
|
||||||
|
let errorMessage = $state<string | null>(null);
|
||||||
|
let showDeleteConfirm = $state(false);
|
||||||
|
|
||||||
|
const server = $derived(chat.activeServer);
|
||||||
|
const isOwner = true; // temporarily enabled for everyone
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (server) {
|
||||||
|
serverName = server.name;
|
||||||
|
avatarPreview = chat.getServerAvatarUrl(server);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasChanges = $derived(
|
||||||
|
(serverName.trim() !== (server?.name || "") && serverName.trim() !== "") ||
|
||||||
|
newAvatarFile !== null
|
||||||
|
);
|
||||||
|
|
||||||
|
async function handleAvatarChange(e: Event) {
|
||||||
|
const file = (e.target as HTMLInputElement).files?.[0];
|
||||||
|
if (file) {
|
||||||
|
newAvatarFile = file;
|
||||||
|
avatarPreview = URL.createObjectURL(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!server) return;
|
||||||
|
errorMessage = null;
|
||||||
|
isUploading = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (serverName.trim() !== server.name) {
|
||||||
|
chat.handleUpdateServerName(server.id, serverName.trim());
|
||||||
|
}
|
||||||
|
if (newAvatarFile) {
|
||||||
|
const { data, mimeType } = await optimizeEmoji(newAvatarFile);
|
||||||
|
await chat.handleUploadServerAvatar(server.id, data, mimeType);
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to save server settings:", err);
|
||||||
|
errorMessage = "Failed to save changes. Please try again.";
|
||||||
|
} finally {
|
||||||
|
isUploading = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmDelete = () => {
|
||||||
|
if (chat.activeServerId) {
|
||||||
|
chat.handleDeleteServer(chat.activeServerId);
|
||||||
|
showDeleteConfirm = false;
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const categories = [
|
||||||
|
{ id: "overview", name: "Overview", icon: "fas fa-info-circle" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleOverlayClick = (e: MouseEvent) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
|
<div class="settings-overlay" onclick={handleOverlayClick} role="presentation">
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<div class="settings-modal" onclick={(e) => e.stopPropagation()} role="presentation">
|
||||||
|
<div class="settings-sidebar">
|
||||||
|
<div class="sidebar-header">{server?.name || 'Server'} Settings</div>
|
||||||
|
{#each categories as cat}
|
||||||
|
<button
|
||||||
|
class="sidebar-item {activeCategory === cat.id ? 'active' : ''}"
|
||||||
|
onclick={() => activeCategory = cat.id}
|
||||||
|
>
|
||||||
|
<i class={cat.icon}></i>
|
||||||
|
{cat.name}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
<div class="sidebar-separator"></div>
|
||||||
|
<button class="sidebar-item danger" onclick={() => {
|
||||||
|
if (chat.activeServerId) {
|
||||||
|
chat.handleLeaveServer(chat.activeServerId);
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<i class="fas fa-sign-out-alt"></i>
|
||||||
|
Leave Server
|
||||||
|
</button>
|
||||||
|
<div class="sidebar-separator"></div>
|
||||||
|
<button class="sidebar-item danger" onclick={() => showDeleteConfirm = true}>
|
||||||
|
<i class="fas fa-trash-alt"></i>
|
||||||
|
Delete Server
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-main">
|
||||||
|
<div class="settings-content">
|
||||||
|
<div class="content-header">
|
||||||
|
<h2>{categories.find(c => c.id === activeCategory)?.name}</h2>
|
||||||
|
<button class="close-btn" onclick={onClose} aria-label="Close settings">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="category-content">
|
||||||
|
{#if activeCategory === "overview"}
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header-description">
|
||||||
|
<h3>Server Overview</h3>
|
||||||
|
<p>Manage the basic information of your server.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overview-grid">
|
||||||
|
<div class="avatar-edit-section">
|
||||||
|
<div class="server-avatar-preview-large">
|
||||||
|
{#if avatarPreview}
|
||||||
|
<img src={avatarPreview} alt="Server Icon" />
|
||||||
|
{:else}
|
||||||
|
<div class="avatar-fallback-large">
|
||||||
|
{server?.name.substring(0, 2).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if isOwner}
|
||||||
|
<label class="btn-primary" style="margin-top: 12px; font-size: 0.8rem; padding: 6px 12px; cursor: pointer;">
|
||||||
|
Change Avatar
|
||||||
|
<input type="file" accept="image/*" onchange={handleAvatarChange} disabled={isUploading} style="display: none;" />
|
||||||
|
</label>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="name-edit-section">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="server-name">Server Name</label>
|
||||||
|
<input
|
||||||
|
id="server-name"
|
||||||
|
type="text"
|
||||||
|
bind:value={serverName}
|
||||||
|
disabled={!isOwner}
|
||||||
|
class="styled-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if errorMessage}
|
||||||
|
<div class="form-error" style="color: var(--status-danger); font-size: 0.85rem; margin-top: 16px;">
|
||||||
|
<i class="fas fa-exclamation-circle"></i> {errorMessage}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if hasChanges || isUploading}
|
||||||
|
<div class="settings-footer">
|
||||||
|
<div class="footer-notice">
|
||||||
|
{#if isUploading}
|
||||||
|
Saving changes...
|
||||||
|
{:else}
|
||||||
|
Careful — you have unsaved changes!
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="footer-actions">
|
||||||
|
<button class="btn-ghost" onclick={() => {
|
||||||
|
if (server) {
|
||||||
|
serverName = server.name;
|
||||||
|
avatarPreview = chat.getServerAvatarUrl(server);
|
||||||
|
newAvatarFile = null;
|
||||||
|
errorMessage = null;
|
||||||
|
}
|
||||||
|
}}>Reset</button>
|
||||||
|
<button class="btn-success" onclick={handleSave} disabled={isUploading}>
|
||||||
|
{isUploading ? "Saving..." : "Save Changes"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if showDeleteConfirm}
|
||||||
|
<div class="modal-overlay danger-overlay" use:portal onclick={() => showDeleteConfirm = false} role="presentation">
|
||||||
|
<div class="modal-content danger-modal" onclick={(e) => e.stopPropagation()} role="presentation">
|
||||||
|
<h2>Delete '{server?.name}'</h2>
|
||||||
|
<p>Are you sure you want to delete <strong>{server?.name}</strong>? This action is permanent and will delete all channels, messages, and associated data. This cannot be undone.</p>
|
||||||
|
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn-ghost" onclick={() => showDeleteConfirm = false}>Cancel</button>
|
||||||
|
<button class="btn-danger" onclick={handleConfirmDelete}>Delete Server</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.settings-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.85);
|
||||||
|
z-index: 5000;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-modal {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1000px;
|
||||||
|
height: 100%;
|
||||||
|
max-height: 800px;
|
||||||
|
display: flex;
|
||||||
|
background-color: var(--background-primary);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: var(--elevation-high);
|
||||||
|
border: 1px solid var(--background-modifier-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-sidebar {
|
||||||
|
width: 218px;
|
||||||
|
background-color: var(--background-secondary);
|
||||||
|
padding: 60px 6px 20px 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-item {
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--interactive-normal);
|
||||||
|
text-align: left;
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
transition: background-color 0.1s, color 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-item i {
|
||||||
|
width: 16px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-item:hover {
|
||||||
|
background-color: var(--background-modifier-hover);
|
||||||
|
color: var(--interactive-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-item.active {
|
||||||
|
background-color: var(--background-modifier-selected);
|
||||||
|
color: var(--interactive-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-item.danger {
|
||||||
|
color: var(--status-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-item.danger:hover {
|
||||||
|
background-color: var(--status-danger);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-separator {
|
||||||
|
height: 1px;
|
||||||
|
background-color: var(--background-modifier-accent);
|
||||||
|
margin: 8px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-main {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: var(--background-primary);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 60px 40px 80px 40px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: var(--header-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header-description h3 {
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--header-primary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header-description p {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-grid {
|
||||||
|
display: flex;
|
||||||
|
gap: 32px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-avatar-preview-large {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--background-secondary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 2px solid var(--background-modifier-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-avatar-preview-large img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-fallback-large {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-edit-section {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.styled-input {
|
||||||
|
background-color: var(--background-tertiary);
|
||||||
|
border: 1px solid var(--background-modifier-accent);
|
||||||
|
color: var(--text-normal);
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1rem;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.styled-input:focus {
|
||||||
|
border-color: var(--brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.styled-input:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
.settings-footer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 40px;
|
||||||
|
right: 40px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.9);
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
animation: slideInUp 0.2s ease-out;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
border: 1px solid var(--background-modifier-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-notice {
|
||||||
|
color: white;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background-color: var(--status-positive);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success:hover:not(:disabled) {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-ghost {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
padding: 8px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Confirmation Modal */
|
||||||
|
.danger-overlay {
|
||||||
|
background-color: rgba(0, 0, 0, 0.85);
|
||||||
|
z-index: 6000; /* Above settings */
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-modal {
|
||||||
|
background-color: var(--background-primary);
|
||||||
|
max-width: 440px;
|
||||||
|
width: 90%;
|
||||||
|
padding: 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: var(--elevation-high);
|
||||||
|
border: 1px solid var(--background-modifier-accent);
|
||||||
|
animation: modal-pop 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-modal h2 {
|
||||||
|
color: var(--status-danger);
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-modal p {
|
||||||
|
color: var(--text-normal);
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background-color: var(--status-danger);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 24px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInUp {
|
||||||
|
from { transform: translateY(100%); opacity: 0; }
|
||||||
|
to { transform: translateY(0); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modal-pop {
|
||||||
|
from { transform: scale(0.9); opacity: 0; }
|
||||||
|
to { transform: scale(1); opacity: 1; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -5,39 +5,65 @@
|
|||||||
const chat = getContext<ChatService>("chat");
|
const chat = getContext<ChatService>("chat");
|
||||||
|
|
||||||
let showServerDropdown = $state(false);
|
let showServerDropdown = $state(false);
|
||||||
|
let fileInput = $state<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
|
const isOwner = true; // temporarily enabled for everyone
|
||||||
|
|
||||||
|
function handleOpenSettings() {
|
||||||
|
chat.showServerSettings = true;
|
||||||
|
showServerDropdown = false;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button
|
<div class="server-header-container">
|
||||||
class="server-header clickable"
|
<button
|
||||||
onclick={() => (showServerDropdown = !showServerDropdown)}
|
class="server-header clickable"
|
||||||
>
|
onclick={() => (showServerDropdown = !showServerDropdown)}
|
||||||
<h3
|
|
||||||
style="margin: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: left;"
|
|
||||||
>
|
>
|
||||||
{chat.activeServer?.name || "Select a Server"}
|
<h3
|
||||||
</h3>
|
style="margin: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: left;"
|
||||||
<span style="font-size: 0.8rem; opacity: 0.6; margin-left: 4px;">
|
|
||||||
<i class="fas {showServerDropdown ? 'fa-chevron-up' : 'fa-chevron-down'}"></i>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{#if showServerDropdown}
|
|
||||||
<div class="server-dropdown">
|
|
||||||
<button
|
|
||||||
class="server-dropdown-item danger muted"
|
|
||||||
onclick={() => {
|
|
||||||
if (chat.activeServerId) {
|
|
||||||
chat.handleLeaveServer(chat.activeServerId);
|
|
||||||
showServerDropdown = false;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Leave Server
|
{chat.activeServer?.name || "Select a Server"}
|
||||||
</button>
|
</h3>
|
||||||
</div>
|
<span style="font-size: 0.8rem; opacity: 0.6; margin-left: 4px;">
|
||||||
{/if}
|
<i class="fas {showServerDropdown ? 'fa-chevron-up' : 'fa-chevron-down'}"></i>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#if showServerDropdown}
|
||||||
|
<div class="server-dropdown shadow-box">
|
||||||
|
{#if isOwner}
|
||||||
|
<button
|
||||||
|
class="server-dropdown-item"
|
||||||
|
onclick={handleOpenSettings}
|
||||||
|
>
|
||||||
|
<i class="fas fa-cog" style="width: 16px; margin-right: 8px;"></i>
|
||||||
|
Server Settings
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-separator"></div>
|
||||||
|
{/if}
|
||||||
|
<button
|
||||||
|
class="server-dropdown-item danger"
|
||||||
|
onclick={() => {
|
||||||
|
if (chat.activeServerId) {
|
||||||
|
chat.handleLeaveServer(chat.activeServerId);
|
||||||
|
showServerDropdown = false;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i class="fas fa-sign-out-alt" style="width: 16px; margin-right: 8px;"></i>
|
||||||
|
Leave Server
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.server-header-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.server-header {
|
.server-header {
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
@@ -53,6 +79,7 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
transition: background-color 0.1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-header:hover {
|
.server-header:hover {
|
||||||
@@ -60,17 +87,33 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.server-dropdown {
|
.server-dropdown {
|
||||||
/* dropdown styles if needed */
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 8px;
|
||||||
|
right: 8px;
|
||||||
|
margin-top: 4px;
|
||||||
|
background-color: var(--background-floating);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px;
|
||||||
|
z-index: 1000;
|
||||||
|
border: 1px solid var(--background-modifier-accent);
|
||||||
|
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-dropdown-item {
|
.server-dropdown-item {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--text-normal);
|
color: var(--interactive-normal);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding: 8px;
|
padding: 8px 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
border-radius: 2px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
transition: all 0.1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-dropdown-item:hover {
|
.server-dropdown-item:hover {
|
||||||
@@ -78,7 +121,19 @@
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.server-dropdown-item.danger {
|
||||||
|
color: var(--status-danger);
|
||||||
|
}
|
||||||
|
|
||||||
.server-dropdown-item.danger:hover {
|
.server-dropdown-item.danger:hover {
|
||||||
background-color: var(--status-danger);
|
background-color: var(--status-danger);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-separator {
|
||||||
|
height: 1px;
|
||||||
|
background-color: var(--background-modifier-accent);
|
||||||
|
margin: 4px 6px;
|
||||||
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export class ChatService {
|
|||||||
identity = $state<Identity | null>(null);
|
identity = $state<Identity | null>(null);
|
||||||
#avatarUrls = new SvelteMap<string, string>();
|
#avatarUrls = new SvelteMap<string, string>();
|
||||||
#bannerUrls = new SvelteMap<string, string>();
|
#bannerUrls = new SvelteMap<string, string>();
|
||||||
|
#serverAvatarUrls = new SvelteMap<string, string>();
|
||||||
|
|
||||||
constructor(initialIdentity: Identity | null) {
|
constructor(initialIdentity: Identity | null) {
|
||||||
this.identity = initialIdentity;
|
this.identity = initialIdentity;
|
||||||
@@ -87,6 +88,30 @@ export class ChatService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Background effect to populate server avatar cache
|
||||||
|
$effect(() => {
|
||||||
|
const serversWithAvatars = this.servers.filter((s) => s.avatarId);
|
||||||
|
for (const server of serversWithAvatars) {
|
||||||
|
const avatarId = server.avatarId!;
|
||||||
|
const idStr = avatarId.toString();
|
||||||
|
|
||||||
|
if (this.#serverAvatarUrls.has(idStr)) continue;
|
||||||
|
|
||||||
|
imageCache.get(avatarId).then((cachedUrl) => {
|
||||||
|
if (cachedUrl) {
|
||||||
|
this.#serverAvatarUrls.set(idStr, cachedUrl);
|
||||||
|
} else {
|
||||||
|
const img = this.#db.images.find((i) => i.id === avatarId);
|
||||||
|
if (img) {
|
||||||
|
imageCache.set(img.id, img.data, img.mimeType).then((url) => {
|
||||||
|
this.#serverAvatarUrls.set(idStr, url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get activeServerId() {
|
get activeServerId() {
|
||||||
@@ -194,6 +219,12 @@ export class ChatService {
|
|||||||
set showDiscoveryModal(v) {
|
set showDiscoveryModal(v) {
|
||||||
this.#ui.showDiscoveryModal = v;
|
this.#ui.showDiscoveryModal = v;
|
||||||
}
|
}
|
||||||
|
get showServerSettings() {
|
||||||
|
return this.#ui.showServerSettings;
|
||||||
|
}
|
||||||
|
set showServerSettings(v) {
|
||||||
|
this.#ui.showServerSettings = v;
|
||||||
|
}
|
||||||
get authError() {
|
get authError() {
|
||||||
return this.#ui.authError;
|
return this.#ui.authError;
|
||||||
}
|
}
|
||||||
@@ -425,6 +456,11 @@ export class ChatService {
|
|||||||
return this.#bannerUrls.get(user.bannerId.toString()) || null;
|
return this.#bannerUrls.get(user.bannerId.toString()) || null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getServerAvatarUrl = (server: Types.Server | null | undefined) => {
|
||||||
|
if (!server || !server.avatarId) return null;
|
||||||
|
return this.#serverAvatarUrls.get(server.avatarId.toString()) || null;
|
||||||
|
};
|
||||||
|
|
||||||
uploadImage = async (data: Uint8Array, mimeType: string, name?: string) => {
|
uploadImage = async (data: Uint8Array, mimeType: string, name?: string) => {
|
||||||
this.#msg.uploadImage(data, mimeType, name);
|
this.#msg.uploadImage(data, mimeType, name);
|
||||||
};
|
};
|
||||||
@@ -500,6 +536,18 @@ export class ChatService {
|
|||||||
this.#server.handleLeaveServer(serverId);
|
this.#server.handleLeaveServer(serverId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleUploadServerAvatar = (serverId: bigint, data: Uint8Array, mimeType: string) => {
|
||||||
|
this.#server.handleUploadServerAvatar(serverId, data, mimeType);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleUpdateServerName = (serverId: bigint, name: string) => {
|
||||||
|
this.#server.handleUpdateServerName(serverId, name);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleDeleteServer = (serverId: bigint) => {
|
||||||
|
this.#server.handleDeleteServer(serverId);
|
||||||
|
};
|
||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
getUsername = (userIdentity: Identity | null) =>
|
getUsername = (userIdentity: Identity | null) =>
|
||||||
getUsername(userIdentity, this.users);
|
getUsername(userIdentity, this.users);
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ export class ServerManagementService {
|
|||||||
#createChannelReducer = useReducer(reducers.createChannel);
|
#createChannelReducer = useReducer(reducers.createChannel);
|
||||||
#joinServerReducer = useReducer(reducers.joinServer);
|
#joinServerReducer = useReducer(reducers.joinServer);
|
||||||
#leaveServerReducer = useReducer(reducers.leaveServer);
|
#leaveServerReducer = useReducer(reducers.leaveServer);
|
||||||
|
#uploadServerAvatarReducer = useReducer(reducers.uploadServerAvatar);
|
||||||
|
#updateServerNameReducer = useReducer(reducers.updateServerName);
|
||||||
|
#deleteServerReducer = useReducer(reducers.deleteServer);
|
||||||
|
|
||||||
handleCreateServer = (name: string) => {
|
handleCreateServer = (name: string) => {
|
||||||
if (name.trim()) {
|
if (name.trim()) {
|
||||||
@@ -30,4 +33,18 @@ export class ServerManagementService {
|
|||||||
handleLeaveServer = (serverId: bigint) => {
|
handleLeaveServer = (serverId: bigint) => {
|
||||||
this.#leaveServerReducer({ serverId });
|
this.#leaveServerReducer({ serverId });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleUploadServerAvatar = (serverId: bigint, data: Uint8Array, mimeType: string) => {
|
||||||
|
this.#uploadServerAvatarReducer({ serverId, data, mimeType });
|
||||||
|
};
|
||||||
|
|
||||||
|
handleUpdateServerName = (serverId: bigint, name: string) => {
|
||||||
|
if (name.trim()) {
|
||||||
|
this.#updateServerNameReducer({ serverId, name });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleDeleteServer = (serverId: bigint) => {
|
||||||
|
this.#deleteServerReducer({ serverId });
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export class ThemeService {
|
|||||||
messageText = $state("");
|
messageText = $state("");
|
||||||
threadMessageText = $state("");
|
threadMessageText = $state("");
|
||||||
showDiscoveryModal = $state(false);
|
showDiscoveryModal = $state(false);
|
||||||
|
showServerSettings = $state(false);
|
||||||
authError = $state("");
|
authError = $state("");
|
||||||
viewingImageId = $state<bigint | null>(null);
|
viewingImageId = $state<bigint | null>(null);
|
||||||
viewingProfileUser = $state<any | null>(null);
|
viewingProfileUser = $state<any | null>(null);
|
||||||
|
|||||||
Reference in New Issue
Block a user