fix: include room ID in message permalink for cross-room navigation

Links now use #roomId/messageHash format so the app can load the correct
room before scrolling to the target message. Handles hashchange events
and auto-navigates on page load.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jason Tudisco 2026-03-16 17:31:12 -06:00
parent 2e1a0ac858
commit 7210acf032
2 changed files with 33 additions and 13 deletions

View File

@ -190,10 +190,9 @@
// Register global 401 handler so any expired-token API call triggers logout // Register global 401 handler so any expired-token API call triggers logout
setOnUnauthorized(() => this.handleLogout()) setOnUnauthorized(() => this.handleLogout())
// Listen for hash changes to scroll to linked messages // Listen for hash changes to navigate to linked messages
this._onHashChange = () => { this._onHashChange = () => {
const hash = window.location.hash?.slice(1) this.navigateToMessageLink()
if (hash) this.scrollToHash(hash)
} }
window.addEventListener('hashchange', this._onHashChange) window.addEventListener('hashchange', this._onHashChange)
@ -365,6 +364,9 @@
// Process any pending invite token // Process any pending invite token
await this.processInviteToken() await this.processInviteToken()
// Check for a message permalink in the URL hash (e.g. #roomId/messageHash)
this.navigateToMessageLink()
}, },
handleLogin(data) { handleLogin(data) {
@ -408,14 +410,7 @@
typingUsers: [], typingUsers: [],
}) })
ws.joinRoom(roomId) ws.joinRoom(roomId)
// Check for hash in URL to scroll to a specific message
const targetHash = window.location.hash?.slice(1)
if (targetHash) {
this.scrollToHash(targetHash)
} else {
this.scrollToBottom() this.scrollToBottom()
}
} catch (e) { } catch (e) {
console.error('Failed to load room:', e) console.error('Failed to load room:', e)
} }
@ -504,12 +499,35 @@
}) })
}, },
/** Parse #roomId/messageHash from URL, load the room, and scroll to the message */
async navigateToMessageLink() {
const fragment = window.location.hash?.slice(1)
if (!fragment) return
const slashIdx = fragment.indexOf('/')
if (slashIdx === -1) return // not a room/hash link
const roomId = fragment.slice(0, slashIdx)
const msgHash = fragment.slice(slashIdx + 1)
if (!roomId || !msgHash) return
// Store the target hash before selecting the room
this._pendingScrollHash = msgHash
// Select the room if not already active
if (this.state.activeRoomId !== roomId) {
await this.selectRoom(roomId)
}
// Scroll to the message after render
this.scrollToHash(this._pendingScrollHash)
this._pendingScrollHash = null
},
scrollToHash(hash) { scrollToHash(hash) {
requestAnimationFrame(() => { requestAnimationFrame(() => {
const el = document.getElementById('msg-' + hash) const el = document.getElementById('msg-' + hash)
if (el) { if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'center' }) el.scrollIntoView({ behavior: 'smooth', block: 'center' })
// Briefly highlight the message
el.classList.add('hash-highlight') el.classList.add('hash-highlight')
setTimeout(() => el.classList.remove('hash-highlight'), 3000) setTimeout(() => el.classList.remove('hash-highlight'), 3000)
} }

View File

@ -478,7 +478,9 @@
e.stopPropagation() e.stopPropagation()
const hash = this.props.message?.hash const hash = this.props.message?.hash
if (!hash) return if (!hash) return
const url = `${window.location.origin}${window.location.pathname}#${hash}` const roomId = this.props.message?.room_id
if (!roomId) return
const url = `${window.location.origin}${window.location.pathname}#${roomId}/${hash}`
const btn = e.currentTarget const btn = e.currentTarget
navigator.clipboard.writeText(url).then(() => { navigator.clipboard.writeText(url).then(() => {
btn.classList.add('copied') btn.classList.add('copied')