Add the agent scaffold, mobile web UI, voice auto-leave, and text notifications
This commit is contained in:
+108
-3
@@ -45,6 +45,7 @@ const state = {
|
||||
takeover: localStorage.getItem("veskslop-takeover") === "1",
|
||||
consoleOpen: false,
|
||||
consoleTab: "events",
|
||||
mobileTab: localStorage.getItem("veskslop-mobile-tab") ?? "chat",
|
||||
voiceReloadAt: 0
|
||||
};
|
||||
|
||||
@@ -419,9 +420,10 @@ async function loadDms() {
|
||||
} catch {
|
||||
state.dms = [];
|
||||
}
|
||||
renderDirect();
|
||||
renderDmRail();
|
||||
renderRightDms();
|
||||
renderRail();
|
||||
updateMobileBadge();
|
||||
}
|
||||
|
||||
async function loadVoice() {
|
||||
@@ -529,6 +531,7 @@ async function selectGuild(guildId) {
|
||||
async function selectChannel(channelId, guildId) {
|
||||
if (guildId && guildId !== state.guildId) state.guildId = guildId;
|
||||
|
||||
const switched = Boolean(state.channelId) && state.channelId !== channelId;
|
||||
state.channelId = channelId;
|
||||
state.channel = findGuildChannel(channelId);
|
||||
|
||||
@@ -544,6 +547,10 @@ async function selectChannel(channelId, guildId) {
|
||||
renderComposer();
|
||||
await loadMessages({ refresh: true });
|
||||
await loadVoice();
|
||||
|
||||
if (switched && isMobileLayout()) {
|
||||
setMobileTab(VOICE_TYPES.has(state.channel?.type) ? "call" : "chat");
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMessage() {
|
||||
@@ -1068,8 +1075,15 @@ function renderMessages() {
|
||||
{ class: "vk-message-head" },
|
||||
el("span", { class: "vk-message-author", text: author }),
|
||||
isAgent ? el("span", { class: "tag tag-outline vk-tag-agent", text: "agent" }) : null,
|
||||
el("span", { class: "vk-mono vk-dim", text: formatTime(message.timestamp) })
|
||||
el("span", { class: "vk-mono vk-dim", text: formatTime(message.timestamp) }),
|
||||
message.editedAt ? el("span", { class: "vk-mono vk-dim", text: "· edited" }) : null
|
||||
),
|
||||
message.replyTo
|
||||
? el("span", {
|
||||
class: "vk-message-reply",
|
||||
text: `↳ replying to ${message.replyTo.authorName ?? message.replyTo.authorId ?? "a message"}`
|
||||
})
|
||||
: null,
|
||||
el("span", { class: "vk-message-body", text: message.content })
|
||||
);
|
||||
|
||||
@@ -1203,6 +1217,46 @@ function renderRightVoice() {
|
||||
if (!root) return;
|
||||
root.replaceChildren();
|
||||
|
||||
// The phone Call tab shows the live call, not just favourites.
|
||||
const callMembers = isMobileLayout() && state.mobileTab === "call" ? (state.voiceSelf?.members ?? []) : [];
|
||||
|
||||
if (callMembers.length) {
|
||||
$("#voice-count").textContent = `${callMembers.length} in call`;
|
||||
const card = el(
|
||||
"div",
|
||||
{ class: "vk-right-group" },
|
||||
el(
|
||||
"div",
|
||||
{ class: "vk-right-group-head" },
|
||||
el("span", { class: "vk-icon", style: "color: var(--color-accent)", html: VK.icon("volume") }),
|
||||
el("span", { class: "vk-name", text: state.voiceSelf?.channelName ?? "call" })
|
||||
),
|
||||
...callMembers.map(member =>
|
||||
el(
|
||||
"div",
|
||||
{ class: "vk-right-row" },
|
||||
avatar(member, { size: "sm" }),
|
||||
el(
|
||||
"span",
|
||||
{ class: "vk-right-name" },
|
||||
el("span", {
|
||||
class: `vk-name ${state.speaking.has(member.id) ? "speaking" : ""}`.trim(),
|
||||
text: member.username ?? member.id
|
||||
}),
|
||||
el("span", {
|
||||
class: "vk-sub",
|
||||
text: member.id === state.user?.id ? "this client" : member.status ?? ""
|
||||
})
|
||||
),
|
||||
el("span", { class: "vk-right-trail" }, voiceMarks(member))
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
root.append(card);
|
||||
return;
|
||||
}
|
||||
|
||||
const inCall = state.favouriteUsers.filter(user => user.channelId);
|
||||
$("#voice-count").textContent = inCall.length ? `${inCall.length} in call` : "";
|
||||
|
||||
@@ -1403,6 +1457,48 @@ function setConsoleTab(tab) {
|
||||
if (tab === "transcript") void refreshTranscript();
|
||||
}
|
||||
|
||||
/* Mobile layout (1c): one pane at a time behind a four-tab bar. */
|
||||
|
||||
const MOBILE_QUERY = window.matchMedia("(max-width: 768px)");
|
||||
const MOBILE_TABS = new Set(["users", "chat", "call", "activity"]);
|
||||
|
||||
function isMobileLayout() {
|
||||
return MOBILE_QUERY.matches;
|
||||
}
|
||||
|
||||
function setMobileTab(tab) {
|
||||
if (!MOBILE_TABS.has(tab)) return;
|
||||
|
||||
state.mobileTab = tab;
|
||||
localStorage.setItem("veskslop-mobile-tab", tab);
|
||||
|
||||
for (const button of $$(".vk-tab")) button.classList.toggle("active", button.dataset.mobileTab === tab);
|
||||
if (isMobileLayout()) $(".vk-app").dataset.mobileTab = tab;
|
||||
renderRightVoice();
|
||||
updateMobileBadge();
|
||||
}
|
||||
|
||||
function syncMobileLayout() {
|
||||
const app = $(".vk-app");
|
||||
if (!app) return;
|
||||
|
||||
if (isMobileLayout()) app.dataset.mobileTab = state.mobileTab;
|
||||
else delete app.dataset.mobileTab;
|
||||
|
||||
for (const button of $$(".vk-tab")) {
|
||||
button.classList.toggle("active", button.dataset.mobileTab === state.mobileTab);
|
||||
}
|
||||
}
|
||||
|
||||
function updateMobileBadge() {
|
||||
const badgeEl = $("#mobile-chat-badge");
|
||||
if (!badgeEl) return;
|
||||
|
||||
const total = state.dms.reduce((sum, dm) => sum + ((dm.unread ?? 0) || (dm.mentions ?? 0)), 0);
|
||||
badgeEl.hidden = total === 0;
|
||||
badgeEl.textContent = total > 99 ? "99+" : String(total);
|
||||
}
|
||||
|
||||
/* Settings */
|
||||
|
||||
function openSettings() {
|
||||
@@ -2175,7 +2271,16 @@ function init() {
|
||||
tab.addEventListener("click", () => setConsoleTab(tab.dataset.consoleTab));
|
||||
}
|
||||
|
||||
$("#raw-events").addEventListener("click", () => setConsoleTab("events"));
|
||||
for (const tab of $$(".vk-tab")) {
|
||||
tab.addEventListener("click", () => setMobileTab(tab.dataset.mobileTab));
|
||||
}
|
||||
MOBILE_QUERY.addEventListener("change", syncMobileLayout);
|
||||
syncMobileLayout();
|
||||
|
||||
$("#raw-events").addEventListener("click", () => {
|
||||
setConsoleTab("events");
|
||||
if (isMobileLayout()) setMobileTab("activity");
|
||||
});
|
||||
$("#transcript-refresh").addEventListener("click", () => void refreshTranscript());
|
||||
$("#channel-refresh").addEventListener("click", () => void loadMessages({ refresh: true }));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user