fix custom emojis

This commit is contained in:
2026-04-03 03:26:25 -04:00
parent d688f223c3
commit 9379d6813f
9 changed files with 193 additions and 14 deletions
+9
View File
@@ -149,6 +149,15 @@
<option value="60">60 fps</option>
</select>
</div>
<div class="setting-group-header">
<select id="bitrate-select-header" bind:value={webrtc.localMedia.screenShareBitrate}>
<option value="1">1 Mbps</option>
<option value="2.5">2.5 Mbps</option>
<option value="5">5 Mbps</option>
<option value="10">10 Mbps</option>
<option value="20">20 Mbps</option>
</select>
</div>
</div>
<button
class="screen-share-btn {webrtc.isSharingScreen ? 'active' : ''}"
+3 -2
View File
@@ -33,8 +33,9 @@
$effect(() => {
if (tooltip.visible && tooltip.targetKey) {
const [msgIdStr, emojiKey] = tooltip.targetKey.split(":");
const msgId = BigInt(msgIdStr);
const parts = tooltip.targetKey.split(":");
const msgId = BigInt(parts[0]);
const emojiKey = parts.slice(1).join(":");
const exists = chat.messageReactions.some(r =>
r.messageId === msgId && (r.emoji || `custom:${r.customEmojiId}`) === emojiKey
);
+143 -1
View File
@@ -4,7 +4,7 @@
import type { ChatService } from "../services/chat.svelte";
import type { WebRTCService } from "../services/webrtc/webrtc.svelte";
import type * as Types from "../../module_bindings/types";
import { optimizeImage } from "../utils";
import { optimizeImage, optimizeEmoji, getCustomEmojiUrl } from "../utils";
let { onClose, currentUser }: { onClose: () => void, currentUser: Types.User | undefined } = $props();
@@ -73,8 +73,25 @@
const categories = [
{ id: "account", name: "My Account", icon: "fas fa-user" },
{ id: "customization", name: "Customization", icon: "fas fa-palette" },
{ id: "voice", name: "Voice & Video", icon: "fas fa-microphone" },
];
async function handleCustomEmojiUpload(e: Event) {
const file = (e.target as HTMLInputElement).files?.[0];
if (!file) return;
const name = prompt("Enter emoji name:", file.name.split('.')[0]) || "custom";
const category = "custom";
try {
const { data } = await optimizeEmoji(file);
await chat.uploadCustomEmoji(name, category, data);
} catch (err) {
console.error("Failed to upload custom emoji:", err);
alert("Failed to process emoji image.");
}
}
</script>
<!-- svelte-ignore a11y_click_events_have_key_events -->
@@ -145,6 +162,32 @@
/>
</div>
</div>
{:else if activeCategory === "customization"}
<div class="section">
<div class="customization-header">
<h3>Custom Emojis</h3>
<p>Upload your own emojis to use in the chat. They must be under 256KB and will be resized to 64x64.</p>
</div>
<div class="emoji-management">
<label class="emoji-upload-card">
<div class="emoji-upload-icon">
<i class="fas fa-plus"></i>
</div>
<span>Upload Emoji</span>
<input type="file" accept="image/*" onchange={handleCustomEmojiUpload} style="display: none;" />
</label>
{#each chat.customEmojis as emoji}
<div class="emoji-item-card" title=":{emoji.name}:">
<div class="emoji-preview">
<img src={getCustomEmojiUrl(emoji.data)} alt={emoji.name} />
</div>
<span class="emoji-name">:{emoji.name}:</span>
</div>
{/each}
</div>
</div>
{:else if activeCategory === "voice"}
<div class="section">
<div class="form-group">
@@ -474,6 +517,105 @@
cursor: pointer;
}
/* Customization Section */
.customization-header {
margin-bottom: 8px;
}
.customization-header h3 {
margin: 0 0 4px 0;
font-size: 1rem;
color: var(--header-primary);
}
.customization-header p {
margin: 0;
font-size: 0.85rem;
color: var(--text-muted);
}
.emoji-management {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 16px;
padding-top: 8px;
}
.emoji-upload-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
background-color: var(--background-secondary);
border: 2px dashed var(--background-modifier-accent);
border-radius: 8px;
padding: 16px;
cursor: pointer;
transition: border-color 0.2s, background-color 0.2s;
aspect-ratio: 1;
}
.emoji-upload-card:hover {
border-color: var(--brand);
background-color: var(--background-modifier-hover);
}
.emoji-upload-icon {
font-size: 1.5rem;
color: var(--text-muted);
}
.emoji-upload-card span {
font-size: 0.75rem;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
}
.emoji-item-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
background-color: var(--background-secondary);
border-radius: 8px;
padding: 12px;
aspect-ratio: 1;
border: 1px solid transparent;
transition: background-color 0.2s;
}
.emoji-item-card:hover {
background-color: var(--background-modifier-hover);
}
.emoji-preview {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
}
.emoji-preview img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.emoji-name {
font-size: 0.75rem;
color: var(--text-normal);
font-family: var(--font-code);
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
width: 100%;
text-align: center;
}
.voice-meter-container {
height: 24px;
background-color: var(--background-tertiary);
+3 -2
View File
@@ -44,8 +44,9 @@
$effect(() => {
if (tooltip.visible && tooltip.targetKey) {
const [msgIdStr, emojiKey] = tooltip.targetKey.split(":");
const msgId = BigInt(msgIdStr);
const parts = tooltip.targetKey.split(":");
const msgId = BigInt(parts[0]);
const emojiKey = parts.slice(1).join(":");
const exists = chat.messageReactions.some(r =>
r.messageId === msgId && (r.emoji || `custom:${r.customEmojiId}`) === emojiKey
);
+5 -2
View File
@@ -114,12 +114,15 @@ export class MessagingService {
`);
// 5. Indexed User Population
// Only pull users who are: Online OR Members of this server OR Senders of visible messages
// Only pull users who are: Online OR Members of this server OR Senders/Reactors of visible messages
const reactorSubquery = `(SELECT identity FROM message_reaction WHERE message_id IN ${visibleMsgSubquery})`;
queries.push(`
SELECT * 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})
identity IN (SELECT sender FROM message WHERE id IN ${visibleMsgSubquery}) OR
identity IN ${reactorSubquery}
`);
}
+4
View File
@@ -79,6 +79,10 @@ export class SoundService {
playNewWatcher() {
this.playTone([523.25, 659.25, 783.99], 0.06, "sine", 0.08); // C5, E5, G5
}
playWatcherLeft() {
this.playTone([783.99, 659.25, 523.25], 0.06, "sine", 0.08); // G5, E5, C5
}
}
export const sounds = new SoundService();
@@ -35,6 +35,9 @@ export class LocalMediaService {
screenShareFramerate = $state(
localStorage.getItem("screen_share_framerate") || "30",
);
screenShareBitrate = $state(
localStorage.getItem("screen_share_bitrate") || "5",
);
#setTalking = useReducer(reducers.setTalking);
#setSharingScreen = useReducer(reducers.setSharingScreen);
@@ -226,6 +229,9 @@ export class LocalMediaService {
$effect(() => {
localStorage.setItem("screen_share_framerate", this.screenShareFramerate);
});
$effect(() => {
localStorage.setItem("screen_share_bitrate", this.screenShareBitrate);
});
}
enumerateDevices = async () => {
@@ -16,6 +16,7 @@ export class PeerManagerService {
identity = $state<Identity | null>(null);
mediaType = $state<"voice" | "screen">("voice");
isDeafened = $state(false);
targetBitrate = $state<number>(5000000);
onNegotiationNeeded: (peerIdHex: string, pc: RTCPeerConnection) => void;
onIceCandidate: (peerIdHex: string, candidate: RTCIceCandidate) => void;
@@ -385,9 +386,6 @@ export class PeerManagerService {
getPeer = (peerIdHex: string) => this.peers.get(peerIdHex);
applyEncoderSettings = async (pc: RTCPeerConnection, peerIdHex: string) => {
console.log(`applyEncoderSettings arguments pc:`);
console.log(pc);
console.log(this);
if (this.mediaType !== "screen") return;
const senders = pc.getSenders();
@@ -398,15 +396,18 @@ export class PeerManagerService {
if (!params.encodings || params.encodings.length === 0) {
params.encodings = [{}];
}
console.log(`params.encodings: ${params.encodings}`);
// Boost bitrate for screen sharing
params.encodings[0].maxBitrate = 5000000;
params.encodings[0].maxBitrate = this.targetBitrate;
params.encodings[0].priority = "high";
// Maintain resolution over framerate during congestion
// @ts-ignore
sender.degradationPreference = "maintain-resolution";
await sender.setParameters(params);
console.log(
`[WebRTC][screen] Applied encoder settings for ${pc.remoteDescription?.type} to ${peerIdHex}`,
`[WebRTC][screen] Applied encoder settings for ${pc.remoteDescription?.type} to ${peerIdHex}: ${this.targetBitrate}bps`,
);
} catch (e) {
console.warn(`[WebRTC][screen] Failed to apply encoder settings`, e);
+13 -1
View File
@@ -32,7 +32,7 @@ export class WebRTCService {
const [wStore] = useTable(tables.watching);
wStore.subscribe((v) => (this.watching = v));
// Sound for new watchers
// Sound for new/leaving watchers
let lastWatchers = new Set<string>();
$effect(() => {
const currentWatchers = new Set(
@@ -42,6 +42,7 @@ export class WebRTCService {
);
if (this.identity) {
// New watchers
for (const watcher of currentWatchers) {
if (!lastWatchers.has(watcher)) {
// New watcher!
@@ -50,6 +51,16 @@ export class WebRTCService {
});
}
}
// Leaving watchers
for (const watcher of lastWatchers) {
if (!currentWatchers.has(watcher)) {
// Someone stopped watching!
untrack(() => {
sounds.playWatcherLeft();
});
}
}
}
lastWatchers = currentWatchers;
});
@@ -80,6 +91,7 @@ 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;
});
// Sync mute/deafen to DB