g
All checks were successful
/ upload (release) Successful in 22s

This commit is contained in:
pavel 2026-02-24 23:39:27 +01:00
commit 92cba49b0d
6 changed files with 472 additions and 108 deletions

View file

@ -132,6 +132,13 @@ const el = {
mobileMembersBtn: document.getElementById("mobile-members-btn"), mobileMembersBtn: document.getElementById("mobile-members-btn"),
overlay: document.getElementById("overlay"), overlay: document.getElementById("overlay"),
utilitySidebar: document.getElementById("utility-sidebar"), utilitySidebar: document.getElementById("utility-sidebar"),
// GIF Picker
gifBtn: document.getElementById("gif-btn"),
gifPicker: document.getElementById("gif-picker"),
gifPickerClose: document.getElementById("gif-picker-close"),
gifSearchInput: document.getElementById("gif-search-input"),
gifResults: document.getElementById("gif-results"),
}; };
// --- API Helpers --- // --- API Helpers ---
@ -222,6 +229,18 @@ function escapeHtml(s) {
.replaceAll("'", "'"); .replaceAll("'", "'");
} }
function formatMessageBody(body) {
const escaped = escapeHtml(body);
// Detect GIF URLs (simple regex for demo)
const urlRegex = /(https?:\/\/[^\s]+(?:\.gif|\/giphy\.gif)[^\s]*)/gi;
if (urlRegex.test(body)) {
return escaped.replace(urlRegex, (url) => {
return `<div class="msg-gif"><img src="${url}" loading="lazy" /></div>`;
});
}
return escaped;
}
function formatDate(isoString) { function formatDate(isoString) {
const d = new Date(isoString); const d = new Date(isoString);
const now = new Date(); const now = new Date();
@ -423,7 +442,7 @@ function renderMessages(messages) {
const displayName = m.author_display_name || "Unknown User"; const displayName = m.author_display_name || "Unknown User";
if (isGrouped) { if (isGrouped) {
row.innerHTML = `<div class="msg-content"><div class="msg-body">${escapeHtml(m.body)}</div></div>`; row.innerHTML = `<div class="msg-content"><div class="msg-body">${formatMessageBody(m.body)}</div></div>`;
} else { } else {
row.innerHTML = ` row.innerHTML = `
<div class="msg-avatar">${shortName(displayName)}</div> <div class="msg-avatar">${shortName(displayName)}</div>
@ -432,7 +451,7 @@ function renderMessages(messages) {
<span class="msg-author">${escapeHtml(displayName)}</span> <span class="msg-author">${escapeHtml(displayName)}</span>
<span class="msg-time">${formatDate(m.created_at)}</span> <span class="msg-time">${formatDate(m.created_at)}</span>
</div> </div>
<div class="msg-body">${escapeHtml(m.body)}</div> <div class="msg-body">${formatMessageBody(m.body)}</div>
</div> </div>
`; `;
} }
@ -1435,6 +1454,84 @@ async function init() {
} }
}; };
// --- GIF Picker Logic ---
const GIPHY_API_KEY = "dc6zaTOxFJmzC"; // Public beta key
el.gifBtn.onclick = () => {
el.gifPicker.classList.remove("hidden");
searchGifs(""); // Initial trending search
};
el.gifPickerClose.onclick = () => {
el.gifPicker.classList.add("hidden");
};
let searchTimeout = null;
el.gifSearchInput.oninput = () => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
searchGifs(el.gifSearchInput.value);
}, 500);
};
async function searchGifs(query) {
el.gifResults.innerHTML = '<div class="gif-loading">Searching...</div>';
const endpoint = query
? `https://api.giphy.com/v1/gifs/search?api_key=${GIPHY_API_KEY}&q=${encodeURIComponent(query)}&limit=20`
: `https://api.giphy.com/v1/gifs/trending?api_key=${GIPHY_API_KEY}&limit=20`;
try {
const res = await fetch(endpoint);
const data = await res.json();
renderGifs(data.data);
} catch (err) {
el.gifResults.innerHTML = '<div class="gif-loading">Error loading GIFs</div>';
}
}
function renderGifs(gifs) {
el.gifResults.innerHTML = "";
if (!gifs || gifs.length === 0) {
el.gifResults.innerHTML = '<div class="gif-loading">No GIFs found</div>';
return;
}
gifs.forEach(gif => {
const imgUrl = gif.images.fixed_height.url;
const item = document.createElement("div");
item.className = "gif-item";
item.innerHTML = `<img src="${imgUrl}" loading="lazy" />`;
item.onclick = () => {
sendGif(imgUrl);
el.gifPicker.classList.add("hidden");
el.gifSearchInput.value = "";
};
el.gifResults.appendChild(item);
});
}
async function sendGif(url) {
try {
if (state.selectedTextChannelId) {
await api(`/channels/${state.selectedTextChannelId}/messages`, {
method: "POST",
body: JSON.stringify({ body: url }),
});
} else if (state.selectedDmUserId) {
await api(`/dms/${state.selectedDmUserId}/messages`, {
method: "POST",
body: JSON.stringify({ body: url }),
});
}
const messages = state.selectedTextChannelId
? await api(`/channels/${state.selectedTextChannelId}/messages?limit=100`)
: await api(`/dms/${state.selectedDmUserId}/messages?limit=100`);
renderMessages(messages);
} catch (err) {
console.error("Failed to send GIF", err);
}
}
try { try {
state.me = await api("/me"); state.me = await api("/me");
if (state.me && state.me.display_name) { if (state.me && state.me.display_name) {

View file

@ -138,6 +138,9 @@
<div class="chat-input-wrapper"> <div class="chat-input-wrapper">
<form id="message-form" class="message-form"> <form id="message-form" class="message-form">
<button type="button" id="gif-btn" class="input-action-btn" title="Open GIF Picker">
<i data-lucide="image"></i>
</button>
<input id="message-body" autocomplete="off" placeholder="Message #channel" maxlength="4000" required /> <input id="message-body" autocomplete="off" placeholder="Message #channel" maxlength="4000" required />
<button type="submit" class="hidden"></button> <button type="submit" class="hidden"></button>
</form> </form>
@ -157,6 +160,24 @@
<div id="overlay" class="overlay hidden"></div> <div id="overlay" class="overlay hidden"></div>
<!-- Modals --> <!-- Modals -->
<div id="gif-picker" class="modal-container hidden">
<div class="modal gif-modal">
<div class="modal-header">
<h2>Select GIF</h2>
<button type="button" class="close-btn" id="gif-picker-close">
<i data-lucide="x"></i>
</button>
</div>
<div class="gif-search-wrapper">
<input type="text" id="gif-search-input" placeholder="Search for GIFs..." autocomplete="off">
<i data-lucide="search" class="search-icon"></i>
</div>
<div id="gif-results" class="gif-results-grid">
<!-- GIF results will be injected here -->
<div class="gif-loading hidden">Loading...</div>
</div>
</div>
</div>
<div id="modal-container" class="modal-container hidden"> <div id="modal-container" class="modal-container hidden">
<div class="modal"> <div class="modal">
<h2 id="modal-title">Create Server</h2> <h2 id="modal-title">Create Server</h2>

View file

@ -715,89 +715,153 @@ select {
} }
/* ═══════════════════════════════════════════════════════════ */ /* ═══════════════════════════════════════════════════════════ */
/* Sound Board */ /* Chat Input */
/* ═══════════════════════════════════════════════════════════ */ /* ═══════════════════════════════════════════════════════════ */
.soundboard-container { .chat-input-wrapper {
padding: 12px; padding: 0 16px 24px;
background: var(--bg-secondary);
border-top: 1px solid rgba(255, 255, 255, 0.03);
display: flex;
flex-direction: column;
gap: 8px;
} }
.soundboard-header { .message-form {
background: var(--bg-input);
border-radius: var(--radius-md);
padding: 2px 12px;
display: flex;
align-items: center;
gap: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
}
.input-action-btn {
color: var(--text-muted);
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-sm);
}
.input-action-btn:hover {
color: var(--text-normal);
background: var(--bg-modifier-hover);
}
.message-form input {
flex: 1;
background: none;
border: 0;
padding: 12px 0;
font-size: 15px;
color: var(--text-normal);
}
/* ═══════════════════════════════════════════════════════════ */
/* GIF Picker Modal */
/* ═══════════════════════════════════════════════════════════ */
.gif-modal {
width: min(500px, 95vw) !important;
max-height: 600px;
display: flex;
flex-direction: column;
padding: 0 !important;
overflow: hidden;
}
.gif-modal .modal-header {
padding: 16px 20px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
font-size: 12px; border-bottom: 1px solid rgba(255, 255, 255, 0.05);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.4px;
color: var(--text-muted);
} }
.soundboard-grid { .gif-modal .modal-header h2 {
display: grid; margin: 0;
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); font-size: 18px;
gap: 8px;
max-height: 200px;
overflow-y: auto;
} }
.sound-item { .gif-search-wrapper {
display: flex; padding: 16px 20px;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 10px 8px;
background: var(--bg-darker);
border-radius: var(--radius-md);
cursor: pointer;
transition: all var(--transition);
border: 1px solid transparent;
position: relative; position: relative;
} }
.sound-item:hover { .gif-search-wrapper input {
background: var(--bg-modifier-hover); width: 100%;
transform: translateY(-2px); background: var(--bg-darker);
border-color: rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); border-radius: var(--radius-md);
padding: 12px 14px 12px 40px;
color: #fff;
font-size: 14px;
transition: border-color var(--transition);
} }
.sound-item:active { .gif-search-wrapper input:focus {
transform: scale(0.95); border-color: var(--brand);
} }
.sound-delete { .gif-search-wrapper .search-icon {
position: absolute; position: absolute;
top: 4px; left: 32px;
right: 4px; top: 50%;
transform: translateY(-50%);
width: 18px; width: 18px;
height: 18px; height: 18px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.6);
color: var(--text-muted); color: var(--text-muted);
}
.gif-results-grid {
flex: 1;
overflow-y: auto;
padding: 0 20px 20px;
display: grid; display: grid;
place-items: center; grid-template-columns: repeat(2, 1fr);
opacity: 0; gap: 8px;
transition: all var(--transition); min-height: 200px;
z-index: 2;
} }
.sound-delete:hover { .gif-item {
background: var(--danger); border-radius: var(--radius-sm);
color: #fff; overflow: hidden;
transform: scale(1.1); cursor: pointer;
background: var(--bg-sidebar);
aspect-ratio: 16 / 9;
transition: transform var(--transition);
} }
.sound-item:hover .sound-delete { .gif-item:hover {
opacity: 1; transform: scale(1.02);
filter: brightness(1.1);
} }
.sound-icon { .gif-item img {
font-size: 24px; width: 100%;
height: 100%;
object-fit: cover;
}
.gif-loading {
grid-column: 1 / -1;
text-align: center;
padding: 40px;
color: var(--text-muted);
}
/* ── GIF in messages ────────────────────────────────────────── */
.msg-gif {
margin-top: 8px;
max-width: 100%;
max-height: 300px;
border-radius: var(--radius-md);
overflow: hidden;
display: block;
}
.msg-gif img {
max-width: 100%;
max-height: 300px;
display: block;
object-fit: contain;
} }
.sound-name { .sound-name {

View file

@ -106,6 +106,13 @@ const el = {
mobileMembersBtn: document.getElementById("mobile-members-btn"), mobileMembersBtn: document.getElementById("mobile-members-btn"),
overlay: document.getElementById("overlay"), overlay: document.getElementById("overlay"),
utilitySidebar: document.getElementById("utility-sidebar"), utilitySidebar: document.getElementById("utility-sidebar"),
// GIF Picker
gifBtn: document.getElementById("gif-btn"),
gifPicker: document.getElementById("gif-picker"),
gifPickerClose: document.getElementById("gif-picker-close"),
gifSearchInput: document.getElementById("gif-search-input"),
gifResults: document.getElementById("gif-results"),
}; };
// --- API Helpers --- // --- API Helpers ---
@ -193,6 +200,18 @@ function escapeHtml(s) {
.replaceAll("'", "&#39;"); .replaceAll("'", "&#39;");
} }
function formatMessageBody(body) {
const escaped = escapeHtml(body);
// Detect GIF URLs (simple regex for demo)
const urlRegex = /(https?:\/\/[^\s]+(?:\.gif|\/giphy\.gif)[^\s]*)/gi;
if (urlRegex.test(body)) {
return escaped.replace(urlRegex, (url) => {
return `<div class="msg-gif"><img src="${url}" loading="lazy" /></div>`;
});
}
return escaped;
}
function formatDate(isoString) { function formatDate(isoString) {
const d = new Date(isoString); const d = new Date(isoString);
const now = new Date(); const now = new Date();
@ -394,7 +413,7 @@ function renderMessages(messages) {
const displayName = m.author_display_name || "Unknown User"; const displayName = m.author_display_name || "Unknown User";
if (isGrouped) { if (isGrouped) {
row.innerHTML = `<div class="msg-content"><div class="msg-body">${escapeHtml(m.body)}</div></div>`; row.innerHTML = `<div class="msg-content"><div class="msg-body">${formatMessageBody(m.body)}</div></div>`;
} else { } else {
row.innerHTML = ` row.innerHTML = `
<div class="msg-avatar">${shortName(displayName)}</div> <div class="msg-avatar">${shortName(displayName)}</div>
@ -403,7 +422,7 @@ function renderMessages(messages) {
<span class="msg-author">${escapeHtml(displayName)}</span> <span class="msg-author">${escapeHtml(displayName)}</span>
<span class="msg-time">${formatDate(m.created_at)}</span> <span class="msg-time">${formatDate(m.created_at)}</span>
</div> </div>
<div class="msg-body">${escapeHtml(m.body)}</div> <div class="msg-body">${formatMessageBody(m.body)}</div>
</div> </div>
`; `;
} }
@ -1414,6 +1433,84 @@ async function init() {
} }
}; };
// --- GIF Picker Logic ---
const GIPHY_API_KEY = "dc6zaTOxFJmzC"; // Public beta key
el.gifBtn.onclick = () => {
el.gifPicker.classList.remove("hidden");
searchGifs(""); // Initial trending search
};
el.gifPickerClose.onclick = () => {
el.gifPicker.classList.add("hidden");
};
let searchTimeout = null;
el.gifSearchInput.oninput = () => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
searchGifs(el.gifSearchInput.value);
}, 500);
};
async function searchGifs(query) {
el.gifResults.innerHTML = '<div class="gif-loading">Searching...</div>';
const endpoint = query
? `https://api.giphy.com/v1/gifs/search?api_key=${GIPHY_API_KEY}&q=${encodeURIComponent(query)}&limit=20`
: `https://api.giphy.com/v1/gifs/trending?api_key=${GIPHY_API_KEY}&limit=20`;
try {
const res = await fetch(endpoint);
const data = await res.json();
renderGifs(data.data);
} catch (err) {
el.gifResults.innerHTML = '<div class="gif-loading">Error loading GIFs</div>';
}
}
function renderGifs(gifs) {
el.gifResults.innerHTML = "";
if (!gifs || gifs.length === 0) {
el.gifResults.innerHTML = '<div class="gif-loading">No GIFs found</div>';
return;
}
gifs.forEach(gif => {
const imgUrl = gif.images.fixed_height.url;
const item = document.createElement("div");
item.className = "gif-item";
item.innerHTML = `<img src="${imgUrl}" loading="lazy" />`;
item.onclick = () => {
sendGif(imgUrl);
el.gifPicker.classList.add("hidden");
el.gifSearchInput.value = "";
};
el.gifResults.appendChild(item);
});
}
async function sendGif(url) {
try {
if (state.selectedTextChannelId) {
await api(`/channels/${state.selectedTextChannelId}/messages`, {
method: "POST",
body: JSON.stringify({ body: url }),
});
} else if (state.selectedDmUserId) {
await api(`/dms/${state.selectedDmUserId}/messages`, {
method: "POST",
body: JSON.stringify({ body: url }),
});
}
const messages = state.selectedTextChannelId
? await api(`/channels/${state.selectedTextChannelId}/messages?limit=100`)
: await api(`/dms/${state.selectedDmUserId}/messages?limit=100`);
renderMessages(messages);
} catch (err) {
console.error("Failed to send GIF", err);
}
}
try { try {
state.me = await api("/me"); state.me = await api("/me");
if (state.me && state.me.display_name) { if (state.me && state.me.display_name) {

View file

@ -138,6 +138,9 @@
<div class="chat-input-wrapper"> <div class="chat-input-wrapper">
<form id="message-form" class="message-form"> <form id="message-form" class="message-form">
<button type="button" id="gif-btn" class="input-action-btn" title="Open GIF Picker">
<i data-lucide="image"></i>
</button>
<input id="message-body" autocomplete="off" placeholder="Message #channel" maxlength="4000" required /> <input id="message-body" autocomplete="off" placeholder="Message #channel" maxlength="4000" required />
<button type="submit" class="hidden"></button> <button type="submit" class="hidden"></button>
</form> </form>
@ -157,6 +160,24 @@
<div id="overlay" class="overlay hidden"></div> <div id="overlay" class="overlay hidden"></div>
<!-- Modals --> <!-- Modals -->
<div id="gif-picker" class="modal-container hidden">
<div class="modal gif-modal">
<div class="modal-header">
<h2>Select GIF</h2>
<button type="button" class="close-btn" id="gif-picker-close">
<i data-lucide="x"></i>
</button>
</div>
<div class="gif-search-wrapper">
<input type="text" id="gif-search-input" placeholder="Search for GIFs..." autocomplete="off">
<i data-lucide="search" class="search-icon"></i>
</div>
<div id="gif-results" class="gif-results-grid">
<!-- GIF results will be injected here -->
<div class="gif-loading hidden">Loading...</div>
</div>
</div>
</div>
<div id="modal-container" class="modal-container hidden"> <div id="modal-container" class="modal-container hidden">
<div class="modal"> <div class="modal">
<h2 id="modal-title">Create Server</h2> <h2 id="modal-title">Create Server</h2>

View file

@ -715,89 +715,153 @@ select {
} }
/* ═══════════════════════════════════════════════════════════ */ /* ═══════════════════════════════════════════════════════════ */
/* Sound Board */ /* Chat Input */
/* ═══════════════════════════════════════════════════════════ */ /* ═══════════════════════════════════════════════════════════ */
.soundboard-container { .chat-input-wrapper {
padding: 12px; padding: 0 16px 24px;
background: var(--bg-secondary);
border-top: 1px solid rgba(255, 255, 255, 0.03);
display: flex;
flex-direction: column;
gap: 8px;
} }
.soundboard-header { .message-form {
background: var(--bg-input);
border-radius: var(--radius-md);
padding: 2px 12px;
display: flex;
align-items: center;
gap: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
}
.input-action-btn {
color: var(--text-muted);
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-sm);
}
.input-action-btn:hover {
color: var(--text-normal);
background: var(--bg-modifier-hover);
}
.message-form input {
flex: 1;
background: none;
border: 0;
padding: 12px 0;
font-size: 15px;
color: var(--text-normal);
}
/* ═══════════════════════════════════════════════════════════ */
/* GIF Picker Modal */
/* ═══════════════════════════════════════════════════════════ */
.gif-modal {
width: min(500px, 95vw) !important;
max-height: 600px;
display: flex;
flex-direction: column;
padding: 0 !important;
overflow: hidden;
}
.gif-modal .modal-header {
padding: 16px 20px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
font-size: 12px; border-bottom: 1px solid rgba(255, 255, 255, 0.05);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.4px;
color: var(--text-muted);
} }
.soundboard-grid { .gif-modal .modal-header h2 {
display: grid; margin: 0;
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); font-size: 18px;
gap: 8px;
max-height: 200px;
overflow-y: auto;
} }
.sound-item { .gif-search-wrapper {
display: flex; padding: 16px 20px;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 10px 8px;
background: var(--bg-darker);
border-radius: var(--radius-md);
cursor: pointer;
transition: all var(--transition);
border: 1px solid transparent;
position: relative; position: relative;
} }
.sound-item:hover { .gif-search-wrapper input {
background: var(--bg-modifier-hover); width: 100%;
transform: translateY(-2px); background: var(--bg-darker);
border-color: rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); border-radius: var(--radius-md);
padding: 12px 14px 12px 40px;
color: #fff;
font-size: 14px;
transition: border-color var(--transition);
} }
.sound-item:active { .gif-search-wrapper input:focus {
transform: scale(0.95); border-color: var(--brand);
} }
.sound-delete { .gif-search-wrapper .search-icon {
position: absolute; position: absolute;
top: 4px; left: 32px;
right: 4px; top: 50%;
transform: translateY(-50%);
width: 18px; width: 18px;
height: 18px; height: 18px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.6);
color: var(--text-muted); color: var(--text-muted);
}
.gif-results-grid {
flex: 1;
overflow-y: auto;
padding: 0 20px 20px;
display: grid; display: grid;
place-items: center; grid-template-columns: repeat(2, 1fr);
opacity: 0; gap: 8px;
transition: all var(--transition); min-height: 200px;
z-index: 2;
} }
.sound-delete:hover { .gif-item {
background: var(--danger); border-radius: var(--radius-sm);
color: #fff; overflow: hidden;
transform: scale(1.1); cursor: pointer;
background: var(--bg-sidebar);
aspect-ratio: 16 / 9;
transition: transform var(--transition);
} }
.sound-item:hover .sound-delete { .gif-item:hover {
opacity: 1; transform: scale(1.02);
filter: brightness(1.1);
} }
.sound-icon { .gif-item img {
font-size: 24px; width: 100%;
height: 100%;
object-fit: cover;
}
.gif-loading {
grid-column: 1 / -1;
text-align: center;
padding: 40px;
color: var(--text-muted);
}
/* ── GIF in messages ────────────────────────────────────────── */
.msg-gif {
margin-top: 8px;
max-width: 100%;
max-height: 300px;
border-radius: var(--radius-md);
overflow: hidden;
display: block;
}
.msg-gif img {
max-width: 100%;
max-height: 300px;
display: block;
object-fit: contain;
} }
.sound-name { .sound-name {