Open chat on the newest message and keep it pinned
This commit is contained in:
@@ -40,6 +40,13 @@ mobile draft at 390px.
|
|||||||
- [x] C11 preview caching: Discord-hosted image/GIF previews (WebP 320px, hash-addressed, 5 MiB
|
- [x] C11 preview caching: Discord-hosted image/GIF previews (WebP 320px, hash-addressed, 5 MiB
|
||||||
cap, one attempt) run after every walk in both modes. Live run `...-20260916090722`:
|
cap, one attempt) run after every walk in both modes. Live run `...-20260916090722`:
|
||||||
`previewed: 2`; thumbs served over `/api/media/<scrape>/<item>/thumb` (200).
|
`previewed: 2`; thumbs served over `/api/media/<scrape>/<item>/thumb` (200).
|
||||||
|
- [x] C78 Chat starts on the newest message. `renderMessages` pins the scroll when the channel
|
||||||
|
changes or the user is already at the bottom, re-pins on the next frame and after 120/400 ms,
|
||||||
|
and re-pins on late image loads while pinned. `overflow-anchor: none` on `.vk-message-scroll`
|
||||||
|
stops Chrome's anchoring from dragging the view off the bottom. A user who scrolled up stays
|
||||||
|
put across re-renders, the mobile chat tab re-pins when it becomes visible, and opening a guild
|
||||||
|
channel or DM lands on the newest message. Verified in a browser across all six cases; the UI
|
||||||
|
smoke test passes. A future jump-to-message link must set `state.messagesPinned = false`.
|
||||||
- [x] C77 Split user audio investigation. [docs/voice-audio-split.md](docs/voice-audio-split.md)
|
- [x] C77 Split user audio investigation. [docs/voice-audio-split.md](docs/voice-audio-split.md)
|
||||||
records that the daemon sees only the mixed client output (`VeskslopCapture.monitor` → VAD →
|
records that the daemon sees only the mixed client output (`VeskslopCapture.monitor` → VAD →
|
||||||
STT) and that per-user tracks exist only in the renderer as WebRTC `MediaStreamTrack`s. The
|
STT) and that per-user tracks exist only in the renderer as WebRTC `MediaStreamTrack`s. The
|
||||||
|
|||||||
+54
-2
@@ -50,6 +50,8 @@ const state = {
|
|||||||
consoleOpen: false,
|
consoleOpen: false,
|
||||||
consoleTab: "events",
|
consoleTab: "events",
|
||||||
mobileTab: localStorage.getItem("veskslop-mobile-tab") ?? "chat",
|
mobileTab: localStorage.getItem("veskslop-mobile-tab") ?? "chat",
|
||||||
|
renderedChannelId: null,
|
||||||
|
messagesPinned: true,
|
||||||
voiceReloadAt: 0
|
voiceReloadAt: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1099,12 +1101,42 @@ function renderCenterHeader() {
|
|||||||
$("#channel-context").textContent = context;
|
$("#channel-context").textContent = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Message scroll: a channel opens on the newest message. The view stays pinned
|
||||||
|
while the user is at the bottom, and never gets dragged by content that loads
|
||||||
|
late above the viewport. */
|
||||||
|
function messagesNearBottom(slack = 80) {
|
||||||
|
const scroll = $("#message-scroll");
|
||||||
|
if (!scroll || !scroll.scrollHeight) return true;
|
||||||
|
return scroll.scrollHeight - scroll.scrollTop - scroll.clientHeight <= slack;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pinMessagesToBottom() {
|
||||||
|
const scroll = $("#message-scroll");
|
||||||
|
if (!scroll) return;
|
||||||
|
|
||||||
|
const apply = () => {
|
||||||
|
scroll.scrollTop = scroll.scrollHeight;
|
||||||
|
};
|
||||||
|
|
||||||
|
apply();
|
||||||
|
requestAnimationFrame(apply);
|
||||||
|
// Avatars, attachments, and embeds can change the height after the first paint.
|
||||||
|
setTimeout(apply, 120);
|
||||||
|
setTimeout(apply, 400);
|
||||||
|
}
|
||||||
|
|
||||||
function renderMessages() {
|
function renderMessages() {
|
||||||
const root = $("#message-list");
|
const root = $("#message-list");
|
||||||
|
const channelChanged = state.renderedChannelId !== state.channelId;
|
||||||
|
const pinned = channelChanged || messagesNearBottom();
|
||||||
|
|
||||||
|
state.renderedChannelId = state.channelId;
|
||||||
|
state.messagesPinned = pinned;
|
||||||
root.replaceChildren();
|
root.replaceChildren();
|
||||||
|
|
||||||
if (!state.channelId) {
|
if (!state.channelId) {
|
||||||
root.append(el("div", { class: "vk-empty", text: "Pick a channel on the left." }));
|
root.append(el("div", { class: "vk-empty", text: "Pick a channel on the left." }));
|
||||||
|
pinMessagesToBottom();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1115,6 +1147,7 @@ function renderMessages() {
|
|||||||
text: "No cached messages. The client only holds channels it has loaded."
|
text: "No cached messages. The client only holds channels it has loaded."
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
pinMessagesToBottom();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1176,8 +1209,7 @@ function renderMessages() {
|
|||||||
root.append(el("div", { class: "vk-message" }, avatar(message.author ?? author, { agent: isAgent, size: "lg" }), body));
|
root.append(el("div", { class: "vk-message" }, avatar(message.author ?? author, { agent: isAgent, size: "lg" }), body));
|
||||||
}
|
}
|
||||||
|
|
||||||
const scroll = $("#message-scroll");
|
if (pinned) pinMessagesToBottom();
|
||||||
scroll.scrollTop = scroll.scrollHeight;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ttsToggleButton() {
|
function ttsToggleButton() {
|
||||||
@@ -1529,6 +1561,11 @@ function setMobileTab(tab) {
|
|||||||
|
|
||||||
for (const button of $$(".vk-tab")) button.classList.toggle("active", button.dataset.mobileTab === tab);
|
for (const button of $$(".vk-tab")) button.classList.toggle("active", button.dataset.mobileTab === tab);
|
||||||
if (isMobileLayout()) $(".vk-app").dataset.mobileTab = tab;
|
if (isMobileLayout()) $(".vk-app").dataset.mobileTab = tab;
|
||||||
|
|
||||||
|
// The chat panel was hidden during the last render: put the view back on the
|
||||||
|
// newest message.
|
||||||
|
if (tab === "chat" && state.messagesPinned) pinMessagesToBottom();
|
||||||
|
|
||||||
renderRightVoice();
|
renderRightVoice();
|
||||||
updateMobileBadge();
|
updateMobileBadge();
|
||||||
}
|
}
|
||||||
@@ -2871,6 +2908,21 @@ function init() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
$("#channel-refresh").addEventListener("click", () => void loadMessages({ refresh: true }));
|
$("#channel-refresh").addEventListener("click", () => void loadMessages({ refresh: true }));
|
||||||
|
$("#message-scroll").addEventListener(
|
||||||
|
"scroll",
|
||||||
|
() => {
|
||||||
|
state.messagesPinned = messagesNearBottom();
|
||||||
|
},
|
||||||
|
{ passive: true }
|
||||||
|
);
|
||||||
|
// Re-pin while the user is at the bottom and late images grow the list.
|
||||||
|
$("#message-list").addEventListener(
|
||||||
|
"load",
|
||||||
|
() => {
|
||||||
|
if (state.messagesPinned) pinMessagesToBottom();
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
$("#guilds-show-all").addEventListener("click", () => {
|
$("#guilds-show-all").addEventListener("click", () => {
|
||||||
state.visibleGuilds = null;
|
state.visibleGuilds = null;
|
||||||
|
|||||||
@@ -797,6 +797,9 @@ button {
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 16px 18px 10px;
|
padding: 16px 18px 10px;
|
||||||
|
/* Late-loading attachments above the viewport must not anchor the scroll
|
||||||
|
away from the newest message. */
|
||||||
|
overflow-anchor: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vk-message-list {
|
.vk-message-list {
|
||||||
|
|||||||
Reference in New Issue
Block a user