diff --git a/client/src/components/app.riot b/client/src/components/app.riot index b96a8a9..91c1980 100644 --- a/client/src/components/app.riot +++ b/client/src/components/app.riot @@ -190,10 +190,9 @@ // Register global 401 handler so any expired-token API call triggers logout setOnUnauthorized(() => this.handleLogout()) - // Listen for hash changes to scroll to linked messages + // Listen for hash changes to navigate to linked messages this._onHashChange = () => { - const hash = window.location.hash?.slice(1) - if (hash) this.scrollToHash(hash) + this.navigateToMessageLink() } window.addEventListener('hashchange', this._onHashChange) @@ -365,6 +364,9 @@ // Process any pending invite token await this.processInviteToken() + + // Check for a message permalink in the URL hash (e.g. #roomId/messageHash) + this.navigateToMessageLink() }, handleLogin(data) { @@ -408,14 +410,7 @@ typingUsers: [], }) 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) { 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) { requestAnimationFrame(() => { const el = document.getElementById('msg-' + hash) if (el) { el.scrollIntoView({ behavior: 'smooth', block: 'center' }) - // Briefly highlight the message el.classList.add('hash-highlight') setTimeout(() => el.classList.remove('hash-highlight'), 3000) } diff --git a/client/src/components/message-bubble.riot b/client/src/components/message-bubble.riot index 1b374df..2692045 100644 --- a/client/src/components/message-bubble.riot +++ b/client/src/components/message-bubble.riot @@ -478,7 +478,9 @@ e.stopPropagation() const hash = this.props.message?.hash 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 navigator.clipboard.writeText(url).then(() => { btn.classList.add('copied')