From d599d817f4e93217ea4686b1196e7b1f76b9105d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Dec 2025 09:23:56 +0000 Subject: [PATCH 1/2] Improve rooms sidebar load performance with lazy loading - Add lazy loading for involvement buttons, deferring their load until user hovers over a room item. This reduces initial DOM size and speeds up sidebar rendering. - Create lazy_involvement_controller.js Stimulus controller to handle on-demand loading of involvement buttons via Turbo Frames - Create sidebar_loading_controller.js for managing loading states - Add CSS for loading states, involvement placeholders, and smooth transitions when buttons load - Display static involvement icon placeholder initially, replaced by interactive button on hover/focus Fixes #23 --- .../stylesheets/application/sidebar.css | 74 +++++++++++++++++++ .../lazy_involvement_controller.js | 30 ++++++++ .../controllers/sidebar_loading_controller.js | 33 +++++++++ .../users/sidebars/rooms/_shared.html.erb | 15 +++- 4 files changed, 148 insertions(+), 4 deletions(-) create mode 100644 app/frontend/controllers/lazy_involvement_controller.js create mode 100644 app/frontend/controllers/sidebar_loading_controller.js diff --git a/app/assets/stylesheets/application/sidebar.css b/app/assets/stylesheets/application/sidebar.css index 5a9e69f7..0cc4304e 100644 --- a/app/assets/stylesheets/application/sidebar.css +++ b/app/assets/stylesheets/application/sidebar.css @@ -436,6 +436,80 @@ margin-bottom: calc(var(--block-space) * 2); } +/* Room item with lazy loading for involvement buttons */ +.room-item { + position: relative; +} + +.room-involvement { + display: flex; + align-items: center; + min-width: 20px; +} + +.involvement-frame { + display: flex; + align-items: center; +} + +.involvement-placeholder { + display: flex; + align-items: center; + justify-content: center; + opacity: 0.5; + transition: opacity 0.15s ease; + cursor: pointer; + padding: 0.25em; + border-radius: 0.25em; +} + +.room-item:hover .involvement-placeholder, +.room-item:focus-within .involvement-placeholder { + opacity: 0.8; +} + +.involvement-icon { + display: block; +} + +/* Hide placeholder once the frame loads the actual button */ +.involvement-frame:has(.btn) .involvement-placeholder { + display: none; +} + +/* Sidebar loading state */ +.sidebar--loading { + opacity: 0; + transition: opacity 0.2s ease; +} + +.sidebar--loaded { + opacity: 1; +} + +.sidebar-spinner { + display: flex; + align-items: center; + justify-content: center; + padding: var(--block-space); + min-height: 200px; +} + +.sidebar-spinner__icon { + width: 24px; + height: 24px; + border: 2px solid var(--color-border); + border-top-color: var(--color-text); + border-radius: 50%; + animation: sidebar-spin 0.8s linear infinite; +} + +@keyframes sidebar-spin { + to { + transform: rotate(360deg); + } +} + .room { color: var(--color-text); font-weight: normal; diff --git a/app/frontend/controllers/lazy_involvement_controller.js b/app/frontend/controllers/lazy_involvement_controller.js new file mode 100644 index 00000000..7db6596f --- /dev/null +++ b/app/frontend/controllers/lazy_involvement_controller.js @@ -0,0 +1,30 @@ +import { Controller } from "@hotwired/stimulus" + +/** + * Lazy loads involvement buttons when the user hovers over a room item. + * This reduces initial page load time by deferring the rendering of + * involvement buttons until they're actually needed. + */ +export default class extends Controller { + static targets = ["trigger", "frame"] + static values = { loaded: Boolean, src: String } + + connect() { + this.loadedValue = false + } + + load() { + if (this.loadedValue || !this.hasSrcValue) return + + this.loadedValue = true + + if (this.hasFrameTarget) { + this.frameTarget.src = this.srcValue + } + } + + // Also load on focus for keyboard accessibility + loadOnFocus() { + this.load() + } +} diff --git a/app/frontend/controllers/sidebar_loading_controller.js b/app/frontend/controllers/sidebar_loading_controller.js new file mode 100644 index 00000000..c355f4b1 --- /dev/null +++ b/app/frontend/controllers/sidebar_loading_controller.js @@ -0,0 +1,33 @@ +import { Controller } from "@hotwired/stimulus" + +/** + * Controls the loading state of the sidebar. + * Shows a loading spinner while the sidebar content is being loaded, + * then transitions to showing the content once ready. + */ +export default class extends Controller { + static targets = ["spinner", "content"] + static values = { loaded: Boolean } + + connect() { + // Check if content is already loaded (turbo cache hit) + if (this.hasContentTarget && this.contentTarget.children.length > 0) { + this.showContent() + } + } + + showContent() { + this.loadedValue = true + if (this.hasSpinnerTarget) { + this.spinnerTarget.hidden = true + } + if (this.hasContentTarget) { + this.contentTarget.classList.remove("sidebar--loading") + } + } + + frameLoaded() { + // Called when the turbo frame finishes loading + this.showContent() + } +} diff --git a/app/views/users/sidebars/rooms/_shared.html.erb b/app/views/users/sidebars/rooms/_shared.html.erb index 90fa1012..827ec4a7 100644 --- a/app/views/users/sidebars/rooms/_shared.html.erb +++ b/app/views/users/sidebars/rooms/_shared.html.erb @@ -13,7 +13,10 @@ data-sidebar-starred-rooms-target="room" data-involvement="<%= involvement %>" data-type="list_node" <%= "hidden" if hide_in_starred_list %> - class="flex gap align-center justify-space-between"> + class="flex gap align-center justify-space-between room-item" + data-controller="lazy-involvement" + data-lazy-involvement-src-value="<%= room_involvement_path(room, from_sidebar: true) unless room.conversation_room? %>" + data-action="mouseenter->lazy-involvement#load focusin->lazy-involvement#load"> <%= link_to_room room, class: [ "flex flex-item-grow gap align-center justify-space-between room full-height txt-nowrap overflow-ellipsis txt-lighter txt-undecorated pad-block position-relative", "unread": unread, "badge": has_notifications ], style: "padding-block: calc(var(--block-space) / 4);" do %> @@ -25,9 +28,13 @@ <% end %> <% unless room.conversation_room? %> - - <%= turbo_frame_tag dom_id(room, dom_prefix(:sidebar, :involvement)) do %> - <%= button_to_change_involvement(room, involvement, from_sidebar: true) %> + + <%= turbo_frame_tag dom_id(room, dom_prefix(:sidebar, :involvement)), + data: { lazy_involvement_target: "frame" }, + class: "involvement-frame" do %> + + <%= image_tag("notification-bell-#{involvement}.svg", aria: { hidden: "true" }, size: 20, class: "involvement-icon") %> + <% end %> <% end %> From ae5f740b2807b76269f190fe072a4fae00aaf436 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Dec 2025 09:41:12 +0000 Subject: [PATCH 2/2] Add system test for sidebar lazy loading Verifies that involvement buttons load lazily on hover and that the sidebar displays correctly on initial load. --- test/system/sidebar_lazy_loading_test.rb | 33 ++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 test/system/sidebar_lazy_loading_test.rb diff --git a/test/system/sidebar_lazy_loading_test.rb b/test/system/sidebar_lazy_loading_test.rb new file mode 100644 index 00000000..fdbe30c4 --- /dev/null +++ b/test/system/sidebar_lazy_loading_test.rb @@ -0,0 +1,33 @@ +require "application_system_test_case" + +class SidebarLazyLoadingTest < ApplicationSystemTestCase + setup do + sign_in "jz@37signals.com" + end + + test "involvement buttons load lazily on hover" do + # Initially, sidebar shows placeholder icons (not interactive buttons) + within ".rooms" do + assert_selector ".involvement-placeholder", minimum: 1 + + # Find a room item and hover over it + room_item = first(".room-item") + room_item.hover + + # After hover, the turbo frame should load the actual button + within room_item do + assert_selector "turbo-frame .btn", wait: 5 + end + end + end + + test "sidebar rooms display correctly on initial load" do + # Verify rooms are visible in the sidebar + within ".rooms" do + assert_selector "[data-type='list_node']", minimum: 1 + end + + # Verify direct messages section is visible + assert_selector ".directs", visible: true + end +end