fix(app): draw the mention badge cutout from the badge itself (#1868)

This commit is contained in:
Hampus authored and GitHub committed 2026-08-24 12:34:37 +02:00
1 parent 677e6e5d6e
commit 5b8a46d087
7 files changed
+69 -73

No files matched your search

@@ -306,14 +306,9 @@
isolation: isolate; isolation: isolate;
} }
.guildBadgeActive::before, .guildBadgeActive,
.guildBadge:not(:empty)::before { .guildBadge:not(:empty) {
content: ''; --mention-badge-cutout-surface: var(--guild-badge-surface);
position: absolute;
inset: -0.1875rem;
z-index: -1;
border-radius: calc(var(--radius-md) + 0.1875rem);
background-color: var(--guild-badge-surface);
} }
.guildVoiceBadge { .guildVoiceBadge {
@@ -145,14 +145,9 @@
isolation: isolate; isolation: isolate;
} }
.folderBadgeActive::before, .folderBadgeActive,
.folderBadge:not(:empty)::before { .folderBadge:not(:empty) {
content: ''; --mention-badge-cutout-surface: var(--guild-badge-surface);
position: absolute;
inset: -0.1875rem;
z-index: -1;
border-radius: calc(var(--radius-md) + 0.1875rem);
background-color: var(--guild-badge-surface);
} }
.expandedFolderBackground { .expandedFolderBackground {
@@ -759,14 +759,14 @@ export const GuildFolderItem = observer((props: GuildFolderItemProps) => {
); );
}); });
const MINI_GUILD_INITIALS_MAX_LENGTH = 2;
interface MiniGuildIconProps { interface MiniGuildIconProps {
readonly guild: Guild; readonly guild: Guild;
} }
const MiniGuildIcon = observer(({guild}: MiniGuildIconProps) => { const MiniGuildIcon = observer(({guild}: MiniGuildIconProps) => {
const iconURL = AvatarSourceUtils.getGuildIconURL(guild, false); const iconURL = AvatarSourceUtils.getGuildIconURL(guild, false);
const initials = getInitialsFromName(guild.name);
const displayInitials = truncateInitials(initials, 2);
if (iconURL !== '') { if (iconURL !== '') {
return ( return (
<flx-app-mini-guild-icon <flx-app-mini-guild-icon
@@ -785,7 +785,7 @@ const MiniGuildIcon = observer(({guild}: MiniGuildIconProps) => {
className={styles.miniGuildInitials} className={styles.miniGuildInitials}
data-flx="app.sidebar-nav.guild-folder-item.mini-guild-icon.mini-guild-initials" data-flx="app.sidebar-nav.guild-folder-item.mini-guild-icon.mini-guild-initials"
> >
{displayInitials} {truncateInitials(getInitialsFromName(guild.name), MINI_GUILD_INITIALS_MAX_LENGTH)}
</span> </span>
</flx-app-mini-guild-icon> </flx-app-mini-guild-icon>
); );
@@ -59,7 +59,6 @@
} }
.itemSlot { .itemSlot {
--guild-rail-skeleton-badge-size: var(--mention-badge-small-size);
position: relative; position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -82,18 +81,6 @@
min-height: var(--guild-list-item-box-size); min-height: var(--guild-list-item-box-size);
} }
.mentionBadge {
position: absolute;
right: calc(50% - var(--guild-icon-size) / 2 - 0.25rem);
bottom: calc(50% - var(--guild-icon-size) / 2 - 0.25rem);
box-shadow: 0 0 0 0.1875rem var(--guild-badge-surface);
}
.folderSlot .mentionBadge {
right: -0.25rem;
bottom: -0.25rem;
}
.itemsTrailingGapCancel { .itemsTrailingGapCancel {
margin-bottom: calc(-1 * var(--guild-list-item-gap)); margin-bottom: calc(-1 * var(--guild-list-item-gap));
} }
@@ -34,7 +34,6 @@ import {useState} from 'react';
const GUILD_ICON_SIZE = skeletonSurfaceVar('--guild-icon-size'); const GUILD_ICON_SIZE = skeletonSurfaceVar('--guild-icon-size');
const FOLDER_ICON_SIZE = skeletonSurfaceVar('--guild-list-item-box-size'); const FOLDER_ICON_SIZE = skeletonSurfaceVar('--guild-list-item-box-size');
const MENTION_BADGE_SIZE = 'var(--guild-rail-skeleton-badge-size)';
const GUILD_PLACEHOLDER_COUNT = 6; const GUILD_PLACEHOLDER_COUNT = 6;
const SHOWS_DOWNLOAD_ACTION = !Platform.isElectron && !Platform.isPWA; const SHOWS_DOWNLOAD_ACTION = !Platform.isElectron && !Platform.isPWA;
const FALLBACK_ORGANIZED_ITEMS: ReadonlyArray<RememberedSkeletonGuildRailItem> = Object.freeze( const FALLBACK_ORGANIZED_ITEMS: ReadonlyArray<RememberedSkeletonGuildRailItem> = Object.freeze(
@@ -190,17 +189,6 @@ function showsSkeletonIndicator({indicator, selected}: SkeletonIndicatorVisibili
return indicator !== SkeletonGuildRailItemIndicator.NONE; return indicator !== SkeletonGuildRailItemIndicator.NONE;
} }
const MentionBadgePlaceholder = () => (
<SkeletonBlock
width={MENTION_BADGE_SIZE}
height={MENTION_BADGE_SIZE}
radius={SkeletonRadius.MEDIUM}
emphasis={SkeletonEmphasis.MUTED}
className={styles.mentionBadge}
data-flx="app.skeleton.guild-rail-skeleton.mention-badge-placeholder.mention-badge"
/>
);
const GuildPlaceholder = ({ const GuildPlaceholder = ({
indicator, indicator,
selected, selected,
@@ -223,9 +211,6 @@ const GuildPlaceholder = ({
size={GUILD_ICON_SIZE} size={GUILD_ICON_SIZE}
data-flx="app.skeleton.guild-rail-skeleton.guild-placeholder.skeleton-circle" data-flx="app.skeleton.guild-rail-skeleton.guild-placeholder.skeleton-circle"
/> />
{indicator === SkeletonGuildRailItemIndicator.MENTION && (
<MentionBadgePlaceholder data-flx="app.skeleton.guild-rail-skeleton.guild-placeholder.mention-badge-placeholder" />
)}
</flx-app-guild-rail-skeleton-item-slot> </flx-app-guild-rail-skeleton-item-slot>
); );
@@ -326,9 +311,6 @@ const CollapsedFolderPlaceholder = ({
</flx-app-guild-rail-skeleton-collapsed-folder-grid> </flx-app-guild-rail-skeleton-collapsed-folder-grid>
)} )}
</flx-app-guild-rail-skeleton-collapsed-folder> </flx-app-guild-rail-skeleton-collapsed-folder>
{indicator === SkeletonGuildRailItemIndicator.MENTION && (
<MentionBadgePlaceholder data-flx="app.skeleton.guild-rail-skeleton.collapsed-folder-placeholder.mention-badge-placeholder" />
)}
</flx-app-guild-rail-skeleton-item-slot> </flx-app-guild-rail-skeleton-item-slot>
); );
@@ -354,9 +336,6 @@ const ExpandedFolderGuildPlaceholder = ({
size={GUILD_ICON_SIZE} size={GUILD_ICON_SIZE}
data-flx="app.skeleton.guild-rail-skeleton.expanded-folder-guild-placeholder.skeleton-circle" data-flx="app.skeleton.guild-rail-skeleton.expanded-folder-guild-placeholder.skeleton-circle"
/> />
{indicator === SkeletonGuildRailItemIndicator.MENTION && (
<MentionBadgePlaceholder data-flx="app.skeleton.guild-rail-skeleton.expanded-folder-guild-placeholder.mention-badge-placeholder" />
)}
</flx-app-guild-rail-skeleton-expanded-folder-guild> </flx-app-guild-rail-skeleton-expanded-folder-guild>
); );
@@ -32,5 +32,17 @@
} }
.animatedWrapper { .animatedWrapper {
position: relative;
display: flex;
pointer-events: none; pointer-events: none;
isolation: isolate;
}
.animatedWrapper::before {
content: '';
position: absolute;
inset: -0.1875rem;
z-index: -1;
border-radius: calc(var(--radius-md) + 0.1875rem);
background-color: var(--mention-badge-cutout-surface, transparent);
} }
@@ -2,12 +2,49 @@
import Accessibility from '@app/features/accessibility/state/Accessibility'; import Accessibility from '@app/features/accessibility/state/Accessibility';
import styles from '@app/features/ui/components/MentionBadge.module.css'; import styles from '@app/features/ui/components/MentionBadge.module.css';
import {
AnimePresence,
type AnimeTarget,
type AnimeTween,
AnimeTweenType,
createAnimeFlxElement,
} from '@app/features/ui/motion/AnimeElement';
import {getCurrentLocale} from '@app/features/user/utils/LocaleUtils'; import {getCurrentLocale} from '@app/features/user/utils/LocaleUtils';
import {formatCompactNumber, formatNumber} from '@pkgs/number_utils/src/NumberFormatting'; import {formatCompactNumber, formatNumber} from '@pkgs/number_utils/src/NumberFormatting';
import {clsx} from 'clsx'; import {clsx} from 'clsx';
import {AnimatePresence, motion} from 'framer-motion';
import {observer} from 'mobx-react-lite'; import {observer} from 'mobx-react-lite';
const MentionBadgeSurface = createAnimeFlxElement('flx-ui-mention-badge-surface');
const HIDDEN_MENTION_BADGE: AnimeTarget = Object.freeze({opacity: 0, scale: 0.85});
const VISIBLE_MENTION_BADGE: AnimeTarget = Object.freeze({opacity: 1, scale: 1});
interface MentionBadgeMotion {
readonly from: AnimeTarget;
readonly to: AnimeTarget;
readonly tween: AnimeTween;
}
const MENTION_BADGE_LEAVE = false as const;
const STANDARD_MENTION_BADGE_MOTION: MentionBadgeMotion = Object.freeze({
from: HIDDEN_MENTION_BADGE,
to: VISIBLE_MENTION_BADGE,
tween: Object.freeze({type: AnimeTweenType.SPRING, stiffness: 500, damping: 22}),
});
const REDUCED_MENTION_BADGE_MOTION: MentionBadgeMotion = Object.freeze({
from: VISIBLE_MENTION_BADGE,
to: VISIBLE_MENTION_BADGE,
tween: Object.freeze({duration: 0}),
});
function resolveMentionBadgeMotion(useReducedMotion: boolean): MentionBadgeMotion {
if (useReducedMotion) {
return REDUCED_MENTION_BADGE_MOTION;
}
return STANDARD_MENTION_BADGE_MOTION;
}
const formatMentionCount = (mentionCount: number) => { const formatMentionCount = (mentionCount: number) => {
const locale = getCurrentLocale(); const locale = getCurrentLocale();
if (mentionCount > 99 && mentionCount < 1000) { if (mentionCount > 99 && mentionCount < 1000) {
@@ -38,34 +75,25 @@ export const MentionBadge = observer(({mentionCount, size = 'medium'}: MentionBa
); );
}); });
export const MentionBadgeAnimated = observer(({mentionCount, size = 'medium'}: MentionBadgeProps) => { export const MentionBadgeAnimated = observer(({mentionCount, size = 'medium'}: MentionBadgeProps) => {
const shouldAnimate = !Accessibility.useReducedMotion; const motion = resolveMentionBadgeMotion(Accessibility.useReducedMotion);
if (!shouldAnimate) {
return mentionCount > 0 ? (
<MentionBadge
mentionCount={mentionCount}
size={size}
data-flx="ui.mention-badge.mention-badge-animated.mention-badge"
/>
) : null;
}
return ( return (
<AnimatePresence initial={false} mode="wait" data-flx="ui.mention-badge.mention-badge-animated.animate-presence"> <AnimePresence enterOnMount={false} data-flx="ui.mention-badge.mention-badge-animated.anime-presence">
{mentionCount > 0 && ( {mentionCount > 0 && (
<motion.div <MentionBadgeSurface
className={styles.animatedWrapper} className={styles.animatedWrapper}
initial={{opacity: 0, scale: 0.85}} from={motion.from}
animate={{opacity: 1, scale: 1}} to={motion.to}
exit={{opacity: 0, scale: 0.85}} leave={MENTION_BADGE_LEAVE}
transition={{type: 'spring', stiffness: 500, damping: 22}} tween={motion.tween}
data-flx="ui.mention-badge.mention-badge-animated.animated-wrapper" data-flx="ui.mention-badge.mention-badge-animated.animated-wrapper"
> >
<MentionBadge <MentionBadge
mentionCount={mentionCount} mentionCount={mentionCount}
size={size} size={size}
data-flx="ui.mention-badge.mention-badge-animated.mention-badge--2" data-flx="ui.mention-badge.mention-badge-animated.mention-badge"
/> />
</motion.div> </MentionBadgeSurface>
)} )}
</AnimatePresence> </AnimePresence>
); );
}); });