parent
d3a109a0cc
commit
92cba49b0d
6 changed files with 472 additions and 108 deletions
101
desktop/app.js
101
desktop/app.js
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
101
static/app.js
101
static/app.js
|
|
@ -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("'", "'");
|
.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();
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue