fix custom emojis
This commit is contained in:
@@ -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' : ''}"
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
`);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user