compute and cache popover props in helper to avoid react_on_rails cache bugs

This commit is contained in:
Dylan Knutson
2025-03-02 02:05:09 +00:00
parent 171b2a72c2
commit 23188f948f
5 changed files with 108 additions and 45 deletions

View File

@@ -8,6 +8,7 @@ module Domain::DescriptionsHelper
include HelpersInterface
include Domain::PostsHelper
include Domain::DomainsHelper
include Domain::UsersHelper
requires_ancestor { Object }
abstract!
@@ -204,4 +205,76 @@ module Domain::DescriptionsHelper
"blue-link"
end
end
sig do
params(user: Domain::User, visual_style: String, icon_size: String).returns(
T::Hash[Symbol, T.untyped],
)
end
def props_for_user_hover_preview(user, visual_style, icon_size)
Rails
.cache
.fetch(
[user, "popover_inline_link_domain_user", visual_style, icon_size],
) do
num_posts =
user.has_created_posts? ? user.user_post_creations.count : nil
registered_at = domain_user_registered_at_string_for_view(user)
num_followed_by =
user.has_followed_by_users? ? user.user_user_follows_to.count : nil
num_followed =
user.has_followed_users? ? user.user_user_follows_from.count : nil
avatar_thumb_size = icon_size == "large" ? "64-avatar" : "32-avatar"
{
visualStyle: visual_style,
iconSize: icon_size,
linkText: user.name_for_view,
userId: user.to_param,
userName: user.name_for_view,
userPath: domain_user_path(user),
userSmallAvatarPath:
domain_user_avatar_img_src_path(
user.avatar,
thumb: avatar_thumb_size,
),
userAvatarPath: domain_user_avatar_img_src_path(user.avatar),
userAvatarAlt: "View #{user.name_for_view}'s profile",
userDomainIcon: domain_model_icon_path(user),
userNumPosts: num_posts,
userRegisteredAt: registered_at,
userNumFollowedBy: num_followed_by,
userNumFollowed: num_followed,
}
end
end
sig do
params(post: Domain::Post, link_text: String, visual_style: String).returns(
T::Hash[Symbol, T.untyped],
)
end
def props_for_post_hover_preview(post, link_text, visual_style)
Rails
.cache
.fetch([post, "popover_inline_link_domain_post", visual_style]) do
props = {
visualStyle: visual_style,
linkText: link_text,
postId: post.to_param,
postTitle: post.title,
postPath: Rails.application.routes.url_helpers.domain_post_path(post),
postThumbnailPath: thumbnail_for_post_path(post),
postThumbnailAlt: "View on #{domain_name_for_model(post)}",
postDomainIcon: domain_model_icon_path(post),
}
if creator = post.primary_creator_for_view
props[:creatorName] = creator.name_for_view
props[:creatorAvatarPath] = user_avatar_path_for_view(creator)
end
props
end
end
end

View File

@@ -29,6 +29,21 @@ export const PostHoverPreview: React.FC<PostHoverPreviewProps> = ({
creatorName,
creatorAvatarPath,
}) => {
// Force eager loading of images
React.useEffect(() => {
// Preload post thumbnail
if (postThumbnailPath) {
const thumbnailImg = new Image();
thumbnailImg.src = postThumbnailPath;
}
// Preload creator avatar
if (creatorAvatarPath) {
const avatarImg = new Image();
avatarImg.src = creatorAvatarPath;
}
}, [postThumbnailPath, creatorAvatarPath]);
// Add extra classes for PostHoverPreview's header/footer
const postHeaderFooterClassName = `${getHeaderFooterClassName()} justify-between p-3`;

View File

@@ -35,6 +35,14 @@ export const UserHoverPreview: React.FC<UserHoverPreviewProps> = ({
userNumFollowedBy,
userNumFollowed,
}) => {
// Force eager loading of the avatar image
React.useEffect(() => {
if (userAvatarPath) {
const img = new Image();
img.src = userAvatarPath;
}
}, [userAvatarPath]);
const previewContent = (
<>
{/* Header: User Name and Domain Icon */}

View File

@@ -2,27 +2,15 @@
<%# sky-link (default, normal blue link) %>
<%# description-section-link (smaller and has a border, for use in description section) %>
<% visual_style = local_assigns[:visual_style] || "sky-link" %>
<% cache [post, "popover_inline_link_domain_post", visual_style] do %>
<% link_classes = link_classes_for_visual_style(visual_style) %>
<%= react_component(
<%=
react_component(
"PostHoverPreviewWrapper",
{
prerender: false,
props: {
visualStyle: visual_style,
linkText: link_text,
postId: post.to_param,
postTitle: post.title,
postPath: domain_post_path(post),
postThumbnailPath: thumbnail_for_post_path(post),
postThumbnailAlt: "View on #{domain_name_for_model(post)}",
postDomainIcon: domain_model_icon_path(post),
creatorName: post.primary_creator_for_view&.name_for_view,
creatorAvatarPath: post.primary_creator_for_view ? user_avatar_path_for_view(post.primary_creator_for_view) : nil,
},
props: props_for_post_hover_preview(post, link_text, visual_style),
html_options: {
class: link_classes
class: link_classes_for_visual_style(visual_style)
}
}
) %>
<% end %>
)
%>

View File

@@ -6,36 +6,15 @@
<%# large %>
<% visual_style = local_assigns[:visual_style] || "sky-link" %>
<% icon_size = local_assigns[:icon_size] || "small" %>
<% cache [user, "popover_inline_link_domain_user", visual_style, icon_size] do %>
<% link_classes = link_classes_for_visual_style(visual_style) %>
<% num_posts = user.has_created_posts? ? user.user_post_creations.count : nil %>
<% registered_at = domain_user_registered_at_string_for_view(user) %>
<% num_followed_by = user.has_followed_by_users? ? user.user_user_follows_to.count : nil %>
<% num_followed = user.has_followed_users? ? user.user_user_follows_from.count : nil %>
<% avatar_thumb_size = icon_size == "large" ? "64-avatar" : "32-avatar" %>
<%= react_component(
<%=
react_component(
"UserHoverPreviewWrapper",
{
prerender: false,
props: {
visualStyle: visual_style,
iconSize: icon_size,
linkText: user.name_for_view,
userId: user.to_param,
userName: user.name,
userPath: domain_user_path(user),
userSmallAvatarPath: domain_user_avatar_img_src_path(user.avatar, thumb: avatar_thumb_size),
userAvatarPath: domain_user_avatar_img_src_path(user.avatar),
userAvatarAlt: "View #{user.name}'s profile",
userDomainIcon: domain_model_icon_path(user),
userNumPosts: num_posts,
userRegisteredAt: registered_at,
userNumFollowedBy: num_followed_by,
userNumFollowed: num_followed,
},
props: props_for_user_hover_preview(user, visual_style, icon_size),
html_options: {
class: link_classes
class: link_classes_for_visual_style(visual_style)
}
}
) %>
<% end %>
)
%>