From ac05412e8ae4299c9324c22064593bdeb5a8c70f Mon Sep 17 00:00:00 2001 From: JohnyTheCarrot Date: Wed, 27 Sep 2023 14:24:26 +0200 Subject: [PATCH] fix!: avoid rerender on hover for animated avatars --- src/Message/MessageAuthor.tsx | 51 ++++++++++++++++++-------- src/Message/index.tsx | 1 - src/Message/style/author.ts | 29 ++++++++++++--- src/Message/variants/NormalMessage.tsx | 6 +-- src/core/ConfigContext.ts | 3 +- src/index.tsx | 30 ++++++++++----- src/stories/Wrapper.tsx | 2 +- src/utils/getAvatar.ts | 36 ++++++++++++------ 8 files changed, 111 insertions(+), 47 deletions(-) diff --git a/src/Message/MessageAuthor.tsx b/src/Message/MessageAuthor.tsx index e3dd86e..6b27365 100644 --- a/src/Message/MessageAuthor.tsx +++ b/src/Message/MessageAuthor.tsx @@ -22,7 +22,6 @@ interface MessageAuthorProps function MessageAuthor({ onlyShowUsername, author, - avatarAnimated, crossPost, referenceGuild, guildId, @@ -33,11 +32,8 @@ function MessageAuthor({ const member = guildId ? resolveMember(author, guildId) : null; const isGuildMember = member !== null; - const avatarUrl = - avatarUrlOverride?.(author) ?? - getAvatar(author, { - animated: avatarAnimated ?? false, - }); + const { stillAvatarUrl, animatedAvatarUrl } = + avatarUrlOverride?.(author) ?? getAvatar(author); const displayName = isGuildMember ? member.nick ?? getDisplayName(author) @@ -98,15 +94,40 @@ function MessageAuthor({ {...props} onClick={() => userOnClick?.(author)} > - - - + + + + + {animatedAvatarUrl && ( + + + + )} + {displayName} diff --git a/src/Message/index.tsx b/src/Message/index.tsx index 82f974c..ea0455c 100644 --- a/src/Message/index.tsx +++ b/src/Message/index.tsx @@ -158,7 +158,6 @@ function MessageTypeSwitch(props: Omit) { ); } diff --git a/src/Message/style/author.ts b/src/Message/style/author.ts index ca78d98..ee263a4 100644 --- a/src/Message/style/author.ts +++ b/src/Message/style/author.ts @@ -39,16 +39,35 @@ export const Avatar = styled.withConfig({ displayName: "message-author-avatar", componentId: commonComponentId, })("object", { - position: "absolute", - left: `calc(${theme.sizes.messageLeftPadding} / 2)`, - transform: "translateX(-50%)", - marginTop: "calc(4px - .125rem)", borderRadius: "100%", width: 40, height: 40, - zIndex: 1, backgroundColor: theme.colors.backgroundSecondary, // when the avatar is loading outline: "none", + position: "absolute", + left: `calc(${theme.sizes.messageLeftPadding} / 2)`, + transform: "translateX(-50%)", + marginTop: "calc(4px - .125rem)", + zIndex: 1, +}); + +export const StillAvatar = styled.withConfig({ + displayName: "message-author-still-avatar", + componentId: commonComponentId, +})(Avatar, {}); + +export const AnimatedAvatar = styled.withConfig({ + displayName: "message-author-animated-avatar", + componentId: commonComponentId, +})(Avatar, {}); + +export const AnimatedAvatarTrigger = styled.withConfig({ + displayName: "message-author-animated-avatar-trigger", + componentId: commonComponentId, +})("span", { + [`& ${AnimatedAvatar}`]: { + display: "none", + }, }); export const AvatarFallback = styled.withConfig({ diff --git a/src/Message/variants/NormalMessage.tsx b/src/Message/variants/NormalMessage.tsx index 25142e0..0a9071d 100644 --- a/src/Message/variants/NormalMessage.tsx +++ b/src/Message/variants/NormalMessage.tsx @@ -30,7 +30,6 @@ interface ReplyInfoProps { function getMiniAvatarUrl(user: APIUser) { const getAvatarSettings: GetAvatarOptions = { size: 16, - animated: false, }; return getAvatar(user, getAvatarSettings); @@ -125,7 +124,9 @@ const ReplyInfo = memo((props: ReplyInfoProps) => { ) : ( - {miniAvatarUrl && } + {miniAvatarUrl && ( + + )} {props.referencedMessage && ( diff --git a/src/core/ConfigContext.ts b/src/core/ConfigContext.ts index e05860f..06f5789 100644 --- a/src/core/ConfigContext.ts +++ b/src/core/ConfigContext.ts @@ -13,6 +13,7 @@ import type { import type { SvgConfig } from "./svgs"; import type { Tag } from "../ChatTag/style"; import type { APIAttachment } from "discord-api-types/v10"; +import type { UserAvatar } from "../utils/getAvatar"; export type PartialSvgConfig = Partial; @@ -36,7 +37,7 @@ export type Config = { resolveGuild(id: Snowflake): APIGuild | null; resolveUser(id: Snowflake): APIUser | null; chatBadge?({ user, TagWrapper }: ChatBadgeProps): ReactElement | null; - avatarUrlOverride?(user: APIUser): string | null; + avatarUrlOverride?(user: APIUser): UserAvatar | null; themeOverrideClassName?: string; // Click handlers diff --git a/src/index.tsx b/src/index.tsx index be5baa4..afbd45e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,8 +1,10 @@ import "./i18n"; import type { CSSProperties } from "react"; -import React, { useState } from "react"; +import React from "react"; import Message from "./Message"; import type { APIMessage } from "discord-api-types/v10"; +import { commonComponentId, styled } from "./Stitches/stitches.config"; +import * as MessageAuthorStyles from "./Message/style/author"; export interface MessageProps { messages: APIMessage[]; @@ -11,21 +13,29 @@ export interface MessageProps { thread: boolean; } +const MessageGroupStyle = styled.withConfig({ + displayName: "message-group", + componentId: commonComponentId, +})("div", { + [`&:hover ${MessageAuthorStyles.AnimatedAvatarTrigger}[data-is-animated='true']`]: + { + [`& ${MessageAuthorStyles.Avatar}`]: { + display: "none", + }, + [`& ${MessageAuthorStyles.AnimatedAvatar}`]: { + display: "unset", + }, + }, +}); + export function MessageGroup(props: MessageProps) { const [firstMessage, ...otherMessages] = props.messages; - const [isHovered, setIsHovered] = useState(false); return ( -
setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - > + @@ -37,7 +47,7 @@ export function MessageGroup(props: MessageProps) { thread={props.thread} /> ))} -
+ ); } diff --git a/src/stories/Wrapper.tsx b/src/stories/Wrapper.tsx index 133b05d..5c4d129 100644 --- a/src/stories/Wrapper.tsx +++ b/src/stories/Wrapper.tsx @@ -412,7 +412,7 @@ const Wrapper: Decorator = (Story) => { }} // avatarUrlOverride={(user) => { // if (user.id === "132819036282159104") - // return "https://cdn.discordapp.com/emojis/698964060770926684.png"; + // return { still: "https://cdn.discordapp.com/emojis/698964060770926684.png" }; // // return null; // }} diff --git a/src/utils/getAvatar.ts b/src/utils/getAvatar.ts index ff1283e..ae73e0b 100644 --- a/src/utils/getAvatar.ts +++ b/src/utils/getAvatar.ts @@ -33,41 +33,55 @@ type AvatarSize = | 3072 | 4096; -function gifCheck(url: string) { - return url?.includes("/a_") ? url.replace("webp", "gif") : url; +function checkIfAnimatedAvatar(url: string) { + return url?.includes("/a_") ?? false; } function getAvatarProperty( user: APIUser, - avatarSize: AvatarSize = 80 + avatarSize: AvatarSize = 80, + fileType: "webp" | "gif" = "webp" ): string | null { if (!user.avatar) return null; // todo: allow custom CDN - return `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.webp?size=${avatarSize}`; + return `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.${fileType}?size=${avatarSize}`; } export interface GetAvatarOptions { - animated?: boolean; size?: AvatarSize; forceDefault?: boolean; } +export interface UserAvatar { + stillAvatarUrl: string; + animatedAvatarUrl?: string; +} + function getAvatar( user: APIUser, - { animated = false, size = 80, forceDefault = false }: GetAvatarOptions = {} -): string { + { size = 80, forceDefault = false }: GetAvatarOptions = {} +): UserAvatar { const defaultAvatar = `https://cdn.discordapp.com/embed/avatars/${ Number(BigInt(user.id) >> 22n) % 6 }.png`; - const avatarUrl = getAvatarProperty(user, size); + const stillAvatarUrl = getAvatarProperty(user, size); + + if (forceDefault || stillAvatarUrl === null) + return { stillAvatarUrl: defaultAvatar }; + + const isAnimatedAvatar = checkIfAnimatedAvatar(stillAvatarUrl); - if (forceDefault || avatarUrl === null) return defaultAvatar; + if (!isAnimatedAvatar) return { stillAvatarUrl: stillAvatarUrl }; - const potentialGif = animated ? gifCheck(avatarUrl) : avatarUrl; + const animatedAvatarUrl = + getAvatarProperty(user, size, "gif") ?? defaultAvatar; - return avatarUrl ? potentialGif.replace("webp", "png") : defaultAvatar; + return { + stillAvatarUrl: stillAvatarUrl, + animatedAvatarUrl: animatedAvatarUrl, + }; } export default getAvatar;