settings
This commit is contained in:
@@ -396,6 +396,17 @@ const typing_activity = table(
|
||||
},
|
||||
);
|
||||
|
||||
const system_configuration = table(
|
||||
{
|
||||
name: "system_configuration",
|
||||
public: true,
|
||||
},
|
||||
{
|
||||
key: t.string().primaryKey(),
|
||||
value: t.string(),
|
||||
},
|
||||
);
|
||||
|
||||
const spacetimedb = schema({
|
||||
user,
|
||||
server,
|
||||
@@ -417,6 +428,7 @@ const spacetimedb = schema({
|
||||
custom_emoji,
|
||||
image,
|
||||
typing_activity,
|
||||
system_configuration,
|
||||
});
|
||||
export default spacetimedb;
|
||||
|
||||
@@ -425,6 +437,18 @@ function validateName(name: string) {
|
||||
throw new SenderError("Names must not be empty");
|
||||
}
|
||||
|
||||
function validateMessageLength(ctx: any, text: string) {
|
||||
const maxLengthConf = ctx.db.system_configuration.key.find("max_message_length");
|
||||
const maxLength = maxLengthConf ? parseInt(maxLengthConf.value) : 262144;
|
||||
|
||||
|
||||
// Approximate byte length check
|
||||
const byteLength = new TextEncoder().encode(text).length;
|
||||
if (byteLength > maxLength) {
|
||||
throw new SenderError(`Message exceeds maximum length of ${maxLength} bytes (${Math.round(maxLength / 1024)}KB).`);
|
||||
}
|
||||
}
|
||||
|
||||
export const set_typing = spacetimedb.reducer(
|
||||
{ channelId: t.u64(), typing: t.bool() },
|
||||
(ctx, { channelId, typing }) => {
|
||||
@@ -996,6 +1020,19 @@ function clearSignalingForUser(ctx: any, identity: any) {
|
||||
ctx.db.screen_ice_candidate.id.delete(row.id);
|
||||
}
|
||||
|
||||
export const set_configuration = spacetimedb.reducer(
|
||||
{ key: t.string(), value: t.string() },
|
||||
(ctx, { key, value }) => {
|
||||
// Basic auth check: only a known 'admin' or perhaps just anyone for now as requested
|
||||
const existing = ctx.db.system_configuration.key.find(key);
|
||||
if (existing) {
|
||||
ctx.db.system_configuration.key.update({ key, value });
|
||||
} else {
|
||||
ctx.db.system_configuration.insert({ key, value });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const create_thread = spacetimedb.reducer(
|
||||
{ name: t.string(), channelId: t.u64(), parentMessageId: t.u64() },
|
||||
(ctx, { name, channelId, parentMessageId }) => {
|
||||
@@ -1068,6 +1105,10 @@ export const send_message = spacetimedb.reducer(
|
||||
if ((!text || text.trim().length === 0) && imageIds.length === 0)
|
||||
throw new SenderError("Messages must not be empty");
|
||||
|
||||
if (text) {
|
||||
validateMessageLength(ctx, text);
|
||||
}
|
||||
|
||||
const user = ctx.db.user.identity.find(ctx.sender);
|
||||
if (!user || !user.subject) {
|
||||
throw new SenderError("You must be logged in via OIDC to send messages");
|
||||
@@ -1093,6 +1134,10 @@ export const send_message = spacetimedb.reducer(
|
||||
);
|
||||
|
||||
export const init = spacetimedb.init((ctx) => {
|
||||
if (!ctx.db.system_configuration.key.find("max_message_length")) {
|
||||
ctx.db.system_configuration.insert({ key: "max_message_length", value: "262144" });
|
||||
}
|
||||
|
||||
let hasServers = false;
|
||||
for (const _server of ctx.db.server.iter()) {
|
||||
hasServers = true;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { useSpacetimeDB } from "spacetimedb/svelte";
|
||||
import { setContext, onMount } from "svelte";
|
||||
import { setContext } from "svelte";
|
||||
import { ChatService } from "./services/chat.svelte";
|
||||
import { WebRTCService } from "./services/webrtc/webrtc.svelte";
|
||||
import ServerList from "./components/ServerList.svelte";
|
||||
|
||||
@@ -175,6 +175,12 @@
|
||||
const currentText = messageText;
|
||||
const currentStaged = [...stagedImages];
|
||||
|
||||
const byteLength = new TextEncoder().encode(currentText).length;
|
||||
if (byteLength > chat.maxMessageLength) {
|
||||
uploadError = `Message exceeds maximum length of ${chat.maxMessageLength} bytes (${Math.round(chat.maxMessageLength / 1024)}KB).`;
|
||||
return;
|
||||
}
|
||||
|
||||
isUploading = true;
|
||||
uploadError = null;
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<div class="profile-overlay" onclick={handleOverlayClick} role="presentation">
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="profile-modal" onclick={(e) => e.stopPropagation()} role="presentation">
|
||||
<div class="profile-banner" style="background-image: {bannerUrl ? `url(${bannerUrl})` : 'none'}; background-color: {bannerUrl ? 'transparent' : 'var(--brand)'};">
|
||||
<div class="profile-banner" style="background-image: {bannerUrl ? `url(${bannerUrl})` : 'none'}; background-color: {bannerUrl ? 'transparent' : 'var(--background-tertiary)'};">
|
||||
<button class="close-btn" onclick={onClose} aria-label="Close profile">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
@@ -89,7 +89,7 @@
|
||||
}
|
||||
|
||||
.profile-banner {
|
||||
height: 120px;
|
||||
height: 140px;
|
||||
position: relative;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
</script>
|
||||
|
||||
<div class="account-profile-card">
|
||||
<div class="profile-banner" style="background-image: {bannerPreview ? `url(${bannerPreview})` : 'none'}; background-color: {bannerPreview ? 'transparent' : 'var(--brand)'};">
|
||||
<div class="profile-banner" style="background-image: {bannerPreview ? `url(${bannerPreview})` : 'none'}; background-color: {bannerPreview ? 'transparent' : 'var(--background-tertiary)'};">
|
||||
<label class="banner-upload-overlay">
|
||||
<i class="fas fa-camera"></i>
|
||||
<span>Change Banner</span>
|
||||
@@ -122,7 +122,7 @@
|
||||
}
|
||||
|
||||
.profile-banner {
|
||||
height: 100px;
|
||||
height: 140px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
position: relative;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from "svelte";
|
||||
import type { ChatService } from "../../services/chat.svelte";
|
||||
import type { WebRTCService } from "../../services/webrtc/webrtc.svelte";
|
||||
|
||||
const chat = getContext<ChatService>("chat");
|
||||
const webrtc = getContext<WebRTCService>("webrtc");
|
||||
|
||||
const resolutions = [
|
||||
@@ -29,7 +27,7 @@
|
||||
|
||||
<div class="settings-section">
|
||||
<div class="section-title">Screen Share Quality</div>
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label for="resolution">Resolution</label>
|
||||
<select id="resolution" bind:value={webrtc.localMedia.screenShareResolution}>
|
||||
|
||||
@@ -273,6 +273,11 @@ export class ChatService {
|
||||
return this.#db.isUsersReady;
|
||||
}
|
||||
|
||||
get maxMessageLength() {
|
||||
const config = this.#db.systemConfiguration.find(c => c.key === "max_message_length");
|
||||
return config ? parseInt(config.value) : 262144;
|
||||
}
|
||||
|
||||
// Auth Context
|
||||
get currentUser() {
|
||||
return this.#auth.currentUser;
|
||||
|
||||
@@ -17,6 +17,7 @@ export class DatabaseService {
|
||||
voiceActivity = $state<readonly Types.VoiceActivity[]>([]);
|
||||
typingActivity = $state<readonly Types.TypingActivity[]>([]);
|
||||
watching = $state<readonly Types.Watching[]>([]);
|
||||
systemConfiguration = $state<readonly Types.SystemConfiguration[]>([]);
|
||||
isUsersReady = $state(false);
|
||||
|
||||
constructor() {
|
||||
@@ -31,6 +32,7 @@ export class DatabaseService {
|
||||
const [voiceActivityStore] = useTable(tables.voice_activity);
|
||||
const [typingActivityStore] = useTable(tables.typing_activity);
|
||||
const [watchingStore] = useTable(tables.watching);
|
||||
const [systemConfigStore] = useTable(tables.system_configuration);
|
||||
|
||||
serversStore.subscribe((v) => (this.servers = v));
|
||||
channelsStore.subscribe((v) => (this.channels = v));
|
||||
@@ -44,5 +46,6 @@ export class DatabaseService {
|
||||
voiceActivityStore.subscribe((v) => (this.voiceActivity = v));
|
||||
typingActivityStore.subscribe((v) => (this.typingActivity = v));
|
||||
watchingStore.subscribe((v) => (this.watching = v));
|
||||
systemConfigStore.subscribe((v) => (this.systemConfiguration = v));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ 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", "SELECT * FROM system_configuration"];
|
||||
|
||||
// 1. Surgical Membership & Identity Pruning
|
||||
if (identity) {
|
||||
|
||||
@@ -373,7 +373,9 @@ export class LocalMediaService {
|
||||
this.localScreenStream.getTracks().forEach((t) => t.stop());
|
||||
this.localScreenStream = null;
|
||||
this.#setSharingScreen({ sharing: false });
|
||||
sounds.playStopWatching();
|
||||
onTrackCleared(null);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -81,6 +81,37 @@ export class WebRTCService {
|
||||
this.localMedia.localScreenStream,
|
||||
);
|
||||
|
||||
// Sound for streams I am watching
|
||||
let lastWatched = new Set<string>();
|
||||
$effect(() => {
|
||||
const currentWatched = new Set(
|
||||
this.watching
|
||||
.filter((w) => this.identity && w.watcher.isEqual(this.identity))
|
||||
.map((w) => w.watchee.toHexString()),
|
||||
);
|
||||
|
||||
if (this.identity) {
|
||||
// I started watching someone (manual or auto)
|
||||
for (const watchee of currentWatched) {
|
||||
if (!lastWatched.has(watchee)) {
|
||||
untrack(() => {
|
||||
sounds.playStartWatching();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// I stopped watching someone (or they stopped sharing)
|
||||
for (const watchee of lastWatched) {
|
||||
if (!currentWatched.has(watchee)) {
|
||||
untrack(() => {
|
||||
sounds.playStopWatching();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
lastWatched = currentWatched;
|
||||
});
|
||||
|
||||
// Sync state to sub-services
|
||||
$effect(() => {
|
||||
this.localMedia.connectedChannelId = this.connectedChannelId;
|
||||
@@ -132,7 +163,6 @@ export class WebRTCService {
|
||||
|
||||
startWatching = (peerIdentity: Identity) => {
|
||||
if (this.connectedChannelId) {
|
||||
sounds.playStartWatching();
|
||||
this.#startWatchingReducer({
|
||||
watchee: peerIdentity,
|
||||
channelId: this.connectedChannelId,
|
||||
@@ -141,7 +171,6 @@ export class WebRTCService {
|
||||
};
|
||||
|
||||
stopWatching = (peerIdentity: Identity) => {
|
||||
sounds.playStopWatching();
|
||||
this.#stopWatchingReducer({ watchee: peerIdentity });
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user