Update static/js/02-chat.js
This commit is contained in:
parent
f684f6ff69
commit
c5ba80129e
@ -2,6 +2,29 @@
|
||||
"use strict";
|
||||
window.Glow = window.Glow || {};
|
||||
|
||||
Glow.EMOJI_SHORTCODES = {
|
||||
smile: "🙂",
|
||||
grin: "😁",
|
||||
joy: "😂",
|
||||
heart: "❤️",
|
||||
fire: "🔥",
|
||||
thumbsup: "👍",
|
||||
thumbsup: "👍",
|
||||
clap: "👏",
|
||||
eyes: "👀",
|
||||
check: "✅",
|
||||
rocket: "🚀",
|
||||
thinking: "🤔",
|
||||
wave: "👋",
|
||||
tada: "🎉",
|
||||
sparkles: "✨",
|
||||
cry: "😢",
|
||||
laugh: "😄",
|
||||
sob: "😭",
|
||||
star: "⭐",
|
||||
warning: "⚠️"
|
||||
};
|
||||
|
||||
Glow.clearMessages = () => {
|
||||
Glow.state.lastSeenId = 0;
|
||||
Glow.state.knownMessageIds.clear();
|
||||
@ -446,7 +469,7 @@ Glow.handleSlashCommand = async text => {
|
||||
if (!trimmed.startsWith("/")) return false;
|
||||
|
||||
if (trimmed === "/shrug") {
|
||||
Glow.els.messageInput.value = "¯\\_(ツ)_/¯";
|
||||
Glow.els.messageInput.value = "¯\\_(ツ)_/¯";
|
||||
Glow.autoResizeComposer();
|
||||
Glow.updateMessageCount();
|
||||
return true;
|
||||
@ -592,9 +615,6 @@ Glow.buildAttachments = attachments => {
|
||||
link.rel = "noopener noreferrer";
|
||||
link.download = attachment.original_name;
|
||||
link.textContent = `Attachment: ${attachment.original_name} (${Glow.formatBytes(attachment.size_bytes)})`;
|
||||
|
||||
|
||||
|
||||
list.append(link);
|
||||
}
|
||||
return list;
|
||||
@ -605,11 +625,13 @@ Glow.buildMessage = (message, grouped = false) => {
|
||||
const isOwnMessage =
|
||||
Glow.state.currentUser &&
|
||||
Number(message.user_id) === Number(Glow.state.currentUser.id);
|
||||
const isDmView = Boolean(Glow.state.activeDm);
|
||||
|
||||
row.className =
|
||||
`message-row${isOwnMessage ? " self" : ""}` +
|
||||
`${grouped ? " grouped" : ""}` +
|
||||
`${message.is_pinned ? " pinned" : ""}`;
|
||||
`${message.is_pinned ? " pinned" : ""}` +
|
||||
`${isDmView ? " is-dm-view" : ""}`;
|
||||
|
||||
row.dataset.messageId = String(message.id);
|
||||
row.dataset.userId = String(message.user_id);
|
||||
@ -647,9 +669,9 @@ Glow.buildMessage = (message, grouped = false) => {
|
||||
|
||||
if (Glow.reactions?.enabled !== false) {
|
||||
const react = document.createElement("button");
|
||||
react.className = "reaction-trigger";
|
||||
react.className = "reaction-trigger primary";
|
||||
react.type = "button";
|
||||
react.textContent = "😊";
|
||||
react.textContent = "😊";
|
||||
react.setAttribute("aria-label", "Add reaction");
|
||||
react.addEventListener("click", () => {
|
||||
Glow.openReactionPopup?.(message);
|
||||
@ -659,7 +681,7 @@ Glow.buildMessage = (message, grouped = false) => {
|
||||
|
||||
if (!message.parent_message_id) {
|
||||
const thread = document.createElement("button");
|
||||
thread.className = "message-action";
|
||||
thread.className = "message-action primary";
|
||||
thread.type = "button";
|
||||
const replyCount = Number(message.thread_reply_count) || 0;
|
||||
thread.textContent =
|
||||
@ -675,7 +697,7 @@ Glow.buildMessage = (message, grouped = false) => {
|
||||
|
||||
if (Glow.canEditMessage(message)) {
|
||||
const edit = document.createElement("button");
|
||||
edit.className = "message-action";
|
||||
edit.className = "message-action secondary";
|
||||
edit.type = "button";
|
||||
edit.textContent = "Edit";
|
||||
edit.addEventListener("click", () => {
|
||||
@ -685,7 +707,7 @@ Glow.buildMessage = (message, grouped = false) => {
|
||||
}
|
||||
|
||||
const copy = document.createElement("button");
|
||||
copy.className = "message-action";
|
||||
copy.className = "message-action secondary";
|
||||
copy.type = "button";
|
||||
copy.textContent = "Copy";
|
||||
copy.addEventListener("click", () => {
|
||||
@ -693,11 +715,10 @@ Glow.buildMessage = (message, grouped = false) => {
|
||||
});
|
||||
actions.append(copy);
|
||||
|
||||
|
||||
const urls = Glow.extractUrls(message.text);
|
||||
if (urls.length) {
|
||||
const copyUrl = document.createElement("button");
|
||||
copyUrl.className = "message-action";
|
||||
copyUrl.className = "message-action secondary";
|
||||
copyUrl.type = "button";
|
||||
copyUrl.textContent = "URL";
|
||||
copyUrl.addEventListener("click", () => {
|
||||
@ -707,7 +728,7 @@ Glow.buildMessage = (message, grouped = false) => {
|
||||
}
|
||||
|
||||
const receipts = document.createElement("button");
|
||||
receipts.className = "message-action";
|
||||
receipts.className = "message-action secondary";
|
||||
receipts.type = "button";
|
||||
receipts.textContent = `Seen ${Number(message.read_receipt_count) || 0}`;
|
||||
receipts.addEventListener("click", () => Glow.openReceiptDetails(message));
|
||||
@ -715,9 +736,9 @@ Glow.buildMessage = (message, grouped = false) => {
|
||||
|
||||
if (Glow.canModerate()) {
|
||||
const remove = document.createElement("button");
|
||||
remove.className = "admin-delete";
|
||||
remove.className = "admin-delete secondary";
|
||||
remove.type = "button";
|
||||
remove.textContent = "×";
|
||||
remove.textContent = "×";
|
||||
remove.setAttribute("aria-label", "Delete message");
|
||||
remove.addEventListener("click", () => {
|
||||
Glow.deleteMessage(message.id);
|
||||
@ -729,6 +750,7 @@ Glow.buildMessage = (message, grouped = false) => {
|
||||
bubble.className = "message-bubble";
|
||||
|
||||
const text = document.createElement("div");
|
||||
text.className = "message-text";
|
||||
text.textContent = message.text;
|
||||
bubble.append(text);
|
||||
|
||||
@ -748,7 +770,10 @@ Glow.buildMessage = (message, grouped = false) => {
|
||||
? [message.attachment]
|
||||
: [];
|
||||
if (attachments.length) {
|
||||
bubble.append(Glow.buildAttachments(attachments));
|
||||
const mediaSection = document.createElement("div");
|
||||
mediaSection.className = "message-media-section";
|
||||
mediaSection.append(Glow.buildAttachments(attachments));
|
||||
bubble.append(mediaSection);
|
||||
}
|
||||
|
||||
Glow.renderReactionBar(message, bubble);
|
||||
@ -1079,6 +1104,38 @@ Glow.loadDmMessages = async dm => {
|
||||
}
|
||||
};
|
||||
|
||||
Glow.renderDmToolbar = () => {
|
||||
const toolbar = document.createElement("div");
|
||||
toolbar.className = "dm-toolbar";
|
||||
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "dm-toolbar-actions";
|
||||
|
||||
const create = document.createElement("button");
|
||||
create.className = "button";
|
||||
create.type = "button";
|
||||
create.textContent = "New DM";
|
||||
create.addEventListener("click", () => Glow.openCreateDmDialog());
|
||||
|
||||
const layout = document.createElement("button");
|
||||
layout.className = "button secondary";
|
||||
layout.type = "button";
|
||||
layout.id = "dmLayoutToggle";
|
||||
layout.textContent =
|
||||
Glow.state.dmLayoutMode === "compact"
|
||||
? "Layout: compact"
|
||||
: "Layout: cards";
|
||||
layout.addEventListener("click", () => {
|
||||
const next = Glow.state.dmLayoutMode === "compact" ? "cards" : "compact";
|
||||
Glow.saveDmLayoutPreference(next);
|
||||
Glow.renderDmList();
|
||||
});
|
||||
|
||||
actions.append(create, layout);
|
||||
toolbar.append(actions);
|
||||
return toolbar;
|
||||
};
|
||||
|
||||
Glow.renderDmList = () => {
|
||||
const box = Glow.els.dmBox;
|
||||
box.replaceChildren();
|
||||
@ -1087,22 +1144,7 @@ Glow.renderDmList = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const header = document.createElement("div");
|
||||
header.className = "pages-header";
|
||||
const copy = document.createElement("div");
|
||||
const title = document.createElement("h2");
|
||||
title.textContent = "Direct messages";
|
||||
const subtitle = document.createElement("p");
|
||||
subtitle.textContent = "Private conversations with other users.";
|
||||
copy.append(title, subtitle);
|
||||
|
||||
const create = document.createElement("button");
|
||||
create.className = "button";
|
||||
create.type = "button";
|
||||
create.textContent = "New DM";
|
||||
create.addEventListener("click", () => Glow.openCreateDmDialog());
|
||||
header.append(copy, create);
|
||||
box.append(header);
|
||||
box.append(Glow.renderDmToolbar());
|
||||
|
||||
if (!(Glow.state.dms || []).length) {
|
||||
const empty = document.createElement("div");
|
||||
@ -1116,7 +1158,8 @@ Glow.renderDmList = () => {
|
||||
}
|
||||
|
||||
const list = document.createElement("div");
|
||||
list.className = "dm-list";
|
||||
list.className =
|
||||
`dm-list ${Glow.state.dmLayoutMode === "compact" ? "compact" : "cards"}`;
|
||||
for (const dm of Glow.state.dms) {
|
||||
list.append(Glow.buildDmCard(dm));
|
||||
}
|
||||
@ -1124,8 +1167,9 @@ Glow.renderDmList = () => {
|
||||
};
|
||||
|
||||
Glow.buildDmCard = dm => {
|
||||
const compact = Glow.state.dmLayoutMode === "compact";
|
||||
const article = document.createElement("article");
|
||||
article.className = "page-card dm-card";
|
||||
article.className = `page-card dm-card${compact ? " compact" : ""}`;
|
||||
article.tabIndex = 0;
|
||||
|
||||
const peer =
|
||||
@ -1133,19 +1177,31 @@ Glow.buildDmCard = dm => {
|
||||
member => Number(member.id) !== Number(Glow.state.currentUser?.id)
|
||||
) || dm.other_user || dm.members?.[0];
|
||||
|
||||
const unread = Number(dm.unread_count || 0) > 0;
|
||||
if (unread) {
|
||||
article.classList.add("unread");
|
||||
}
|
||||
|
||||
const title = document.createElement("h3");
|
||||
title.textContent = peer?.display_name || peer?.username || "Conversation";
|
||||
if (peer) Glow.applyUserColor(title, peer);
|
||||
|
||||
const sender = document.createElement("div");
|
||||
sender.className = "dm-card-sender";
|
||||
sender.textContent = peer?.username ? `@${peer.username}` : "direct message";
|
||||
|
||||
const subtitle = document.createElement("p");
|
||||
subtitle.className = "page-card-excerpt";
|
||||
subtitle.className = "page-card-excerpt dm-card-preview";
|
||||
subtitle.textContent =
|
||||
dm.latest_message?.text || dm.last_message_text || "No messages yet.";
|
||||
|
||||
const footer = document.createElement("div");
|
||||
footer.className = "page-card-footer";
|
||||
footer.className = "page-card-footer dm-card-footer";
|
||||
|
||||
const left = document.createElement("span");
|
||||
left.textContent = peer?.username ? `@${peer.username}` : "DM";
|
||||
left.textContent =
|
||||
unread ? `${dm.unread_count} unread` : (peer?.status_message || " ");
|
||||
|
||||
const updated = document.createElement("time");
|
||||
updated.dateTime = dm.latest_message?.created_at || dm.last_message_created_at || dm.created_at;
|
||||
updated.title = new Date(dm.latest_message?.created_at || dm.last_message_created_at || dm.created_at).toLocaleString();
|
||||
@ -1153,6 +1209,7 @@ Glow.buildDmCard = dm => {
|
||||
dm.latest_message?.created_at || dm.last_message_created_at
|
||||
? `Updated ${Glow.relativeTime(dm.latest_message?.created_at || dm.last_message_created_at)}`
|
||||
: `Created ${Glow.relativeTime(dm.created_at)}`;
|
||||
|
||||
footer.append(left, updated);
|
||||
|
||||
const open = async () => {
|
||||
@ -1179,7 +1236,7 @@ Glow.buildDmCard = dm => {
|
||||
}
|
||||
});
|
||||
|
||||
article.append(title, subtitle, footer);
|
||||
article.append(title, sender, subtitle, footer);
|
||||
return article;
|
||||
};
|
||||
|
||||
@ -1357,6 +1414,198 @@ Glow.uploadSelectedAttachments = async files => {
|
||||
return uploaded;
|
||||
};
|
||||
|
||||
Glow.parseEmojiShortcodeAtCursor = input => {
|
||||
if (!(input instanceof HTMLTextAreaElement)) return null;
|
||||
const value = input.value || "";
|
||||
const caret = input.selectionStart ?? value.length;
|
||||
const before = value.slice(0, caret);
|
||||
const match = before.match(/::([a-z0-9_+-]{0,24})$/i);
|
||||
if (!match) return null;
|
||||
return {
|
||||
query: match[1].toLowerCase(),
|
||||
start: caret - match[0].length,
|
||||
end: caret
|
||||
};
|
||||
};
|
||||
|
||||
Glow.getEmojiSuggestions = query => {
|
||||
const entries = Object.entries(Glow.EMOJI_SHORTCODES);
|
||||
if (!query) {
|
||||
return entries.slice(0, 8).map(([name, emoji]) => ({ name, emoji }));
|
||||
}
|
||||
return entries
|
||||
.filter(([name]) => name.includes(query))
|
||||
.slice(0, 8)
|
||||
.map(([name, emoji]) => ({ name, emoji }));
|
||||
};
|
||||
|
||||
Glow.renderEmojiAutocomplete = () => {
|
||||
const state = Glow.state.emojiAutocomplete;
|
||||
const panel =
|
||||
state.owner === "thread"
|
||||
? Glow.els.threadEmojiAutocomplete
|
||||
: Glow.els.emojiAutocomplete;
|
||||
|
||||
const otherPanel =
|
||||
state.owner === "thread"
|
||||
? Glow.els.emojiAutocomplete
|
||||
: Glow.els.threadEmojiAutocomplete;
|
||||
|
||||
otherPanel?.classList.add("hidden");
|
||||
otherPanel?.replaceChildren();
|
||||
|
||||
if (!panel) return;
|
||||
|
||||
panel.replaceChildren();
|
||||
|
||||
if (!state.open || !state.suggestions.length) {
|
||||
panel.classList.add("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
state.suggestions.forEach((item, index) => {
|
||||
const button = document.createElement("button");
|
||||
button.className = "emoji-suggestion";
|
||||
button.type = "button";
|
||||
button.textContent = `${item.emoji} ::${item.name}`;
|
||||
if (index === state.selectedIndex) {
|
||||
button.classList.add("active");
|
||||
}
|
||||
button.addEventListener("click", () => {
|
||||
Glow.applyEmojiSuggestion(index);
|
||||
});
|
||||
panel.append(button);
|
||||
});
|
||||
|
||||
panel.classList.remove("hidden");
|
||||
};
|
||||
|
||||
Glow.openEmojiAutocomplete = (owner, query, triggerIndex) => {
|
||||
const suggestions = Glow.getEmojiSuggestions(query);
|
||||
Glow.state.emojiAutocomplete = {
|
||||
open: suggestions.length > 0,
|
||||
owner,
|
||||
query,
|
||||
suggestions,
|
||||
selectedIndex: 0,
|
||||
triggerIndex
|
||||
};
|
||||
Glow.renderEmojiAutocomplete();
|
||||
};
|
||||
|
||||
Glow.closeEmojiAutocomplete = () => {
|
||||
Glow.state.emojiAutocomplete = {
|
||||
open: false,
|
||||
owner: "main",
|
||||
query: "",
|
||||
suggestions: [],
|
||||
selectedIndex: 0,
|
||||
triggerIndex: -1
|
||||
};
|
||||
Glow.els.emojiAutocomplete?.classList.add("hidden");
|
||||
Glow.els.threadEmojiAutocomplete?.classList.add("hidden");
|
||||
};
|
||||
|
||||
Glow.moveEmojiSelection = delta => {
|
||||
const state = Glow.state.emojiAutocomplete;
|
||||
if (!state.open || !state.suggestions.length) return;
|
||||
const size = state.suggestions.length;
|
||||
state.selectedIndex = (state.selectedIndex + delta + size) % size;
|
||||
Glow.renderEmojiAutocomplete();
|
||||
};
|
||||
|
||||
Glow.applyEmojiSuggestion = (forcedIndex = null) => {
|
||||
const state = Glow.state.emojiAutocomplete;
|
||||
if (!state.open || !state.suggestions.length) return false;
|
||||
|
||||
const index =
|
||||
forcedIndex == null ? state.selectedIndex : forcedIndex;
|
||||
const chosen = state.suggestions[index];
|
||||
if (!chosen) return false;
|
||||
|
||||
const input =
|
||||
state.owner === "thread"
|
||||
? Glow.els.threadInput
|
||||
: Glow.els.messageInput;
|
||||
|
||||
if (!(input instanceof HTMLTextAreaElement)) return false;
|
||||
|
||||
const parsed = Glow.parseEmojiShortcodeAtCursor(input);
|
||||
if (!parsed) {
|
||||
Glow.closeEmojiAutocomplete();
|
||||
return false;
|
||||
}
|
||||
|
||||
const value = input.value;
|
||||
input.value =
|
||||
value.slice(0, parsed.start) +
|
||||
chosen.emoji +
|
||||
value.slice(parsed.end);
|
||||
|
||||
const nextPos = parsed.start + chosen.emoji.length;
|
||||
input.setSelectionRange(nextPos, nextPos);
|
||||
|
||||
if (state.owner === "thread") {
|
||||
Glow.autoResizeThreadComposer?.();
|
||||
Glow.updateThreadMessageCount?.();
|
||||
} else {
|
||||
Glow.autoResizeComposer();
|
||||
Glow.updateMessageCount();
|
||||
}
|
||||
|
||||
Glow.closeEmojiAutocomplete();
|
||||
input.focus();
|
||||
return true;
|
||||
};
|
||||
|
||||
Glow.handleEmojiAutocompleteInput = owner => {
|
||||
const input =
|
||||
owner === "thread"
|
||||
? Glow.els.threadInput
|
||||
: Glow.els.messageInput;
|
||||
|
||||
const parsed = Glow.parseEmojiShortcodeAtCursor(input);
|
||||
if (!parsed) {
|
||||
if (Glow.state.emojiAutocomplete.open && Glow.state.emojiAutocomplete.owner === owner) {
|
||||
Glow.closeEmojiAutocomplete();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Glow.openEmojiAutocomplete(owner, parsed.query, parsed.start);
|
||||
};
|
||||
|
||||
Glow.handleEmojiAutocompleteKeydown = event, owner => {
|
||||
const state = Glow.state.emojiAutocomplete;
|
||||
if (!state.open || state.owner !== owner) return false;
|
||||
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
Glow.moveEmojiSelection(1);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
Glow.moveEmojiSelection(-1);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
Glow.closeEmojiAutocomplete();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.key === "Enter" || event.key === "Tab") {
|
||||
event.preventDefault();
|
||||
Glow.applyEmojiSuggestion();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
Glow.sendTyping = async (isTyping = true) => {
|
||||
const channel = Glow.getActiveChannel();
|
||||
if (
|
||||
@ -1416,8 +1665,8 @@ Glow.renderTyping = () => {
|
||||
const names = users.map(user => user.username);
|
||||
Glow.els.typingBar.textContent =
|
||||
names.length === 1
|
||||
? `${names[0]} is typing…`
|
||||
: `${names.slice(0, -1).join(", ")} and ${names.at(-1)} are typing…`;
|
||||
? `${names[0]} is typing…`
|
||||
: `${names.slice(0, -1).join(", ")} and ${names.at(-1)} are typing…`;
|
||||
Glow.els.typingBar.classList.remove("hidden");
|
||||
};
|
||||
|
||||
@ -1481,6 +1730,7 @@ Glow.sendMessage = async event => {
|
||||
Glow.updateMessageCount();
|
||||
Glow.els.attachmentInput.value = "";
|
||||
Glow.setSelectedAttachment([]);
|
||||
Glow.closeEmojiAutocomplete();
|
||||
Glow.appendMessages([data.message], true, false);
|
||||
Glow.sendTyping(false);
|
||||
} catch (error) {
|
||||
@ -1743,6 +1993,7 @@ Glow.sendThreadReply = async event => {
|
||||
Glow.updateThreadMessageCount();
|
||||
Glow.els.threadAttachmentInput.value = "";
|
||||
Glow.setSelectedThreadAttachment([]);
|
||||
Glow.closeEmojiAutocomplete();
|
||||
Glow.renderThread();
|
||||
Glow.refreshRootReplyCount(root.id, Glow.state.threadReplies.length);
|
||||
} catch (error) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user