mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
fix(app): unify guild acronyms and shrink them to fit (#3462)
This commit is contained in:
1 parent
069b13c382
commit
d7bbe933fa
22 files changed
+267
-311
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
import {getInitialsFromName, truncateInitials} from '@app/features/guild/utils/GuildInitialsUtils';
|
import {GuildAcronym} from '@app/features/guild/components/GuildAcronym';
|
||||||
import type {ComboboxOption} from '@app/features/ui/components/form/FormCombobox';
|
import type {ComboboxOption} from '@app/features/ui/components/form/FormCombobox';
|
||||||
import {clsx} from 'clsx';
|
import {clsx} from 'clsx';
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
@@ -26,8 +26,6 @@ export interface GuildComboboxRenderersConfig<T extends GuildComboboxOption> {
|
|||||||
getNotice?: (option: T, disabled: boolean) => React.ReactNode;
|
getNotice?: (option: T, disabled: boolean) => React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ROW_INITIALS_MAX_LENGTH = 1;
|
|
||||||
|
|
||||||
const renderRow = <T extends GuildComboboxOption>(
|
const renderRow = <T extends GuildComboboxOption>(
|
||||||
option: T,
|
option: T,
|
||||||
disabled: boolean,
|
disabled: boolean,
|
||||||
@@ -37,7 +35,6 @@ const renderRow = <T extends GuildComboboxOption>(
|
|||||||
) => {
|
) => {
|
||||||
const isGlobal = !option.value;
|
const isGlobal = !option.value;
|
||||||
const iconUrl = option.iconUrl ?? null;
|
const iconUrl = option.iconUrl ?? null;
|
||||||
const initial = truncateInitials(getInitialsFromName(option.label), ROW_INITIALS_MAX_LENGTH).toUpperCase();
|
|
||||||
const notice = getNotice?.(option, disabled);
|
const notice = getNotice?.(option, disabled);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -58,7 +55,10 @@ const renderRow = <T extends GuildComboboxOption>(
|
|||||||
aria-hidden
|
aria-hidden
|
||||||
data-flx="app.guild-combobox-renderers.render-row.avatar-placeholder"
|
data-flx="app.guild-combobox-renderers.render-row.avatar-placeholder"
|
||||||
>
|
>
|
||||||
{initial}
|
<GuildAcronym
|
||||||
|
name={option.label}
|
||||||
|
data-flx="app.guild-combobox-renderers.render-row.avatar-placeholder-acronym"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<span className={styles.label} data-flx="app.guild-combobox-renderers.render-row.label">
|
<span className={styles.label} data-flx="app.guild-combobox-renderers.render-row.label">
|
||||||
|
|||||||
@@ -185,19 +185,12 @@
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
font-weight: 600;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition:
|
transition:
|
||||||
border-radius 70ms ease-out,
|
border-radius 70ms ease-out,
|
||||||
background-color 70ms ease-out,
|
background-color 70ms ease-out,
|
||||||
color 70ms ease-out;
|
color 70ms ease-out;
|
||||||
container-type: size;
|
|
||||||
--initials-font-size: clamp(0.85rem, 45cqi, 1.35rem);
|
|
||||||
--initials-fit-inline-size: 92cqi;
|
|
||||||
--initials-letter-spacing: 0px;
|
|
||||||
--initials-scale: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.guildIconSelected:not(.guildIconNoImage) {
|
.guildIconSelected:not(.guildIconNoImage) {
|
||||||
@@ -242,28 +235,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.guildIconInitials {
|
|
||||||
white-space: nowrap;
|
|
||||||
display: inline-block;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1;
|
|
||||||
color: inherit;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: min(
|
|
||||||
var(--initials-font-size),
|
|
||||||
calc(var(--initials-fit-inline-size) / (var(--initials-width-em, 0.01) * var(--initials-scale)))
|
|
||||||
);
|
|
||||||
letter-spacing: var(--initials-letter-spacing);
|
|
||||||
transform: scale(var(--initials-scale));
|
|
||||||
transform-origin: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.guildIcon[data-initials-length='medium'] .guildIconInitials {
|
|
||||||
--initials-font-size: clamp(0.85rem, 38cqi, 1.11rem);
|
|
||||||
--initials-letter-spacing: 0px;
|
|
||||||
--initials-scale: 0.92;
|
|
||||||
}
|
|
||||||
|
|
||||||
.guildIndicator {
|
.guildIndicator {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: var(--guild-list-indicator-inset);
|
left: var(--guild-list-indicator-inset);
|
||||||
@@ -765,10 +736,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-size: 0.5rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
line-height: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.combinePreviewIconInitials {
|
.combinePreviewIconInitials {
|
||||||
|
|||||||
@@ -124,15 +124,7 @@
|
|||||||
|
|
||||||
.miniGuildIconWithInitials {
|
.miniGuildIconWithInitials {
|
||||||
background-color: var(--folder-mini-surface);
|
background-color: var(--folder-mini-surface);
|
||||||
}
|
|
||||||
|
|
||||||
.miniGuildInitials {
|
|
||||||
font-size: 0.5rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
line-height: 1;
|
|
||||||
text-align: center;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.folderBadge {
|
.folderBadge {
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ import {
|
|||||||
import {useContextMenuHoverState} from '@app/features/app/hooks/useContextMenuHoverState';
|
import {useContextMenuHoverState} from '@app/features/app/hooks/useContextMenuHoverState';
|
||||||
import {useHover} from '@app/features/app/hooks/useHover';
|
import {useHover} from '@app/features/app/hooks/useHover';
|
||||||
import {useMergeRefs} from '@app/features/app/hooks/useMergeRefs';
|
import {useMergeRefs} from '@app/features/app/hooks/useMergeRefs';
|
||||||
|
import {GuildAcronym} from '@app/features/guild/components/GuildAcronym';
|
||||||
import type {Guild} from '@app/features/guild/models/Guild';
|
import type {Guild} from '@app/features/guild/models/Guild';
|
||||||
import GuildFolderExpanded from '@app/features/guild/state/GuildFolderExpanded';
|
import GuildFolderExpanded from '@app/features/guild/state/GuildFolderExpanded';
|
||||||
import GuildReadState from '@app/features/guild/state/GuildReadState';
|
import GuildReadState from '@app/features/guild/state/GuildReadState';
|
||||||
import {getInitialsFromName, truncateInitials} from '@app/features/guild/utils/GuildInitialsUtils';
|
|
||||||
import {MENTION_COUNT_ARIA_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
import {MENTION_COUNT_ARIA_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
||||||
import {isKeyboardActivationKey} from '@app/features/input/utils/KeyboardUtils';
|
import {isKeyboardActivationKey} from '@app/features/input/utils/KeyboardUtils';
|
||||||
import {useRecoveringBackgroundImageURL} from '@app/features/messaging/hooks/useImageRecovery';
|
import {useRecoveringBackgroundImageURL} from '@app/features/messaging/hooks/useImageRecovery';
|
||||||
@@ -760,8 +760,6 @@ export const GuildFolderItem = observer((props: GuildFolderItemProps) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const MINI_GUILD_INITIALS_MAX_LENGTH = 2;
|
|
||||||
|
|
||||||
interface MiniGuildIconProps {
|
interface MiniGuildIconProps {
|
||||||
readonly guild: Guild;
|
readonly guild: Guild;
|
||||||
}
|
}
|
||||||
@@ -782,12 +780,7 @@ const MiniGuildIcon = observer(({guild}: MiniGuildIconProps) => {
|
|||||||
className={flxElementClassName(styles.miniGuildIcon, styles.miniGuildIconWithInitials)}
|
className={flxElementClassName(styles.miniGuildIcon, styles.miniGuildIconWithInitials)}
|
||||||
data-flx="app.sidebar-nav.guild-folder-item.mini-guild-icon.mini-guild-icon--2"
|
data-flx="app.sidebar-nav.guild-folder-item.mini-guild-icon.mini-guild-icon--2"
|
||||||
>
|
>
|
||||||
<span
|
<GuildAcronym name={guild.name} data-flx="app.sidebar-nav.guild-folder-item.mini-guild-icon.acronym" />
|
||||||
className={styles.miniGuildInitials}
|
|
||||||
data-flx="app.sidebar-nav.guild-folder-item.mini-guild-icon.mini-guild-initials"
|
|
||||||
>
|
|
||||||
{truncateInitials(getInitialsFromName(guild.name), MINI_GUILD_INITIALS_MAX_LENGTH)}
|
|
||||||
</span>
|
|
||||||
</flx-app-mini-guild-icon>
|
</flx-app-mini-guild-icon>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
+9
-27
@@ -9,16 +9,9 @@ import type {GuildListItemDragAndDrop} from '@app/features/app/components/layout
|
|||||||
import {VoiceBadge, type VoiceBadgeActivity} from '@app/features/app/components/layout/sidebar_nav/VoiceBadge';
|
import {VoiceBadge, type VoiceBadgeActivity} from '@app/features/app/components/layout/sidebar_nav/VoiceBadge';
|
||||||
import {DropPlacement} from '@app/features/app/components/layout/types/DndTypes';
|
import {DropPlacement} from '@app/features/app/components/layout/types/DndTypes';
|
||||||
import {useMergeRefs} from '@app/features/app/hooks/useMergeRefs';
|
import {useMergeRefs} from '@app/features/app/hooks/useMergeRefs';
|
||||||
|
import {GuildAcronym} from '@app/features/guild/components/GuildAcronym';
|
||||||
import type {Guild} from '@app/features/guild/models/Guild';
|
import type {Guild} from '@app/features/guild/models/Guild';
|
||||||
import Guilds from '@app/features/guild/state/Guilds';
|
import Guilds from '@app/features/guild/state/Guilds';
|
||||||
import {
|
|
||||||
type GuildInitialsLength,
|
|
||||||
getGuildIconDisplayInitials,
|
|
||||||
getGuildInitialsFitStyle,
|
|
||||||
getInitialsFromName,
|
|
||||||
getInitialsLength,
|
|
||||||
truncateInitials,
|
|
||||||
} from '@app/features/guild/utils/GuildInitialsUtils';
|
|
||||||
import {ariaCurrentPage} from '@app/features/platform/ARIAProps';
|
import {ariaCurrentPage} from '@app/features/platform/ARIAProps';
|
||||||
import {Edge} from '@app/features/ui/AxisOrientation';
|
import {Edge} from '@app/features/ui/AxisOrientation';
|
||||||
import {MentionBadgeAnimated} from '@app/features/ui/components/MentionBadge';
|
import {MentionBadgeAnimated} from '@app/features/ui/components/MentionBadge';
|
||||||
@@ -58,13 +51,6 @@ function resolveGuildListItemMotion(useReducedMotion: boolean): GuildListItemMot
|
|||||||
return STANDARD_GUILD_LIST_ITEM_MOTION;
|
return STANDARD_GUILD_LIST_ITEM_MOTION;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveInitialsLength(displayInitials: string): GuildInitialsLength | null {
|
|
||||||
if (displayInitials.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return getInitialsLength(displayInitials);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveDraggingCursor(dragAndDrop: GuildListItemDragAndDrop | null): 'grabbing' | null {
|
function resolveDraggingCursor(dragAndDrop: GuildListItemDragAndDrop | null): 'grabbing' | null {
|
||||||
if (dragAndDrop == null) {
|
if (dragAndDrop == null) {
|
||||||
return null;
|
return null;
|
||||||
@@ -164,7 +150,6 @@ function GuildListItemBadges({
|
|||||||
|
|
||||||
function CombinePreviewIcon({guild}: {readonly guild: Guild}) {
|
function CombinePreviewIcon({guild}: {readonly guild: Guild}) {
|
||||||
const url = AvatarSourceUtils.getGuildIconURL(guild, false);
|
const url = AvatarSourceUtils.getGuildIconURL(guild, false);
|
||||||
const initials = truncateInitials(getInitialsFromName(guild.name), 2);
|
|
||||||
const backgroundStyle: React.CSSProperties = {};
|
const backgroundStyle: React.CSSProperties = {};
|
||||||
if (url.length > 0) {
|
if (url.length > 0) {
|
||||||
backgroundStyle.backgroundImage = `url(${url})`;
|
backgroundStyle.backgroundImage = `url(${url})`;
|
||||||
@@ -176,7 +161,10 @@ function CombinePreviewIcon({guild}: {readonly guild: Guild}) {
|
|||||||
data-flx="app.sidebar-nav.guild-list-item-presentation.combine-preview-icon.combine-preview-icon"
|
data-flx="app.sidebar-nav.guild-list-item-presentation.combine-preview-icon.combine-preview-icon"
|
||||||
>
|
>
|
||||||
{url.length === 0 && (
|
{url.length === 0 && (
|
||||||
<span data-flx="app.sidebar-nav.guild-list-item-presentation.combine-preview-icon.span">{initials}</span>
|
<GuildAcronym
|
||||||
|
name={guild.name}
|
||||||
|
data-flx="app.sidebar-nav.guild-list-item-presentation.combine-preview-icon.acronym"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</flx-app-combine-preview-icon>
|
</flx-app-combine-preview-icon>
|
||||||
);
|
);
|
||||||
@@ -304,8 +292,6 @@ export const GuildListItemPresentation = forwardRef<
|
|||||||
forwardedRef,
|
forwardedRef,
|
||||||
) {
|
) {
|
||||||
const rootRef = useMergeRefs([surfaceRef, forwardedRef]);
|
const rootRef = useMergeRefs([surfaceRef, forwardedRef]);
|
||||||
const displayInitials = getGuildIconDisplayInitials(getInitialsFromName(guild.name));
|
|
||||||
const initialsLength = resolveInitialsLength(displayInitials);
|
|
||||||
const dropIndicator = resolveDropIndicator(dragAndDrop);
|
const dropIndicator = resolveDropIndicator(dragAndDrop);
|
||||||
const motion = resolveGuildListItemMotion(Accessibility.useReducedMotion);
|
const motion = resolveGuildListItemMotion(Accessibility.useReducedMotion);
|
||||||
const draggingCursor = resolveDraggingCursor(dragAndDrop);
|
const draggingCursor = resolveDraggingCursor(dragAndDrop);
|
||||||
@@ -372,18 +358,14 @@ export const GuildListItemPresentation = forwardRef<
|
|||||||
to={guildIconTarget}
|
to={guildIconTarget}
|
||||||
from={false}
|
from={false}
|
||||||
tween={{duration: motion.iconTweenDuration, ease: 'easeOut'}}
|
tween={{duration: motion.iconTweenDuration, ease: 'easeOut'}}
|
||||||
data-initials-length={initialsLength}
|
|
||||||
style={guildIconStyle}
|
style={guildIconStyle}
|
||||||
data-flx="app.sidebar-nav.guild-list-item-presentation.guild-icon"
|
data-flx="app.sidebar-nav.guild-list-item-presentation.guild-icon"
|
||||||
>
|
>
|
||||||
{!hasPaintedIcon && (
|
{!hasPaintedIcon && (
|
||||||
<span
|
<GuildAcronym
|
||||||
className={styles.guildIconInitials}
|
name={guild.name}
|
||||||
style={getGuildInitialsFitStyle(displayInitials)}
|
data-flx="app.sidebar-nav.guild-list-item-presentation.guild-icon-acronym"
|
||||||
data-flx="app.sidebar-nav.guild-list-item-presentation.guild-icon-initials"
|
/>
|
||||||
>
|
|
||||||
{displayInitials}
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</GuildListItemIcon>
|
</GuildListItemIcon>
|
||||||
<GuildListItemBadges
|
<GuildListItemBadges
|
||||||
|
|||||||
@@ -320,9 +320,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: white;
|
color: white;
|
||||||
font-weight: 700;
|
overflow: hidden;
|
||||||
font-size: 0.9rem;
|
|
||||||
line-height: 1.75rem;
|
|
||||||
border: 0.0625rem solid var(--background-modifier-accent);
|
border: 0.0625rem solid var(--background-modifier-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -137,8 +137,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
background-color: var(--background-modifier-accent);
|
background-color: var(--background-modifier-accent);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
font-weight: 700;
|
overflow: hidden;
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.optionLabel {
|
.optionLabel {
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
||||||
|
|
||||||
|
.acronym {
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: inherit;
|
||||||
|
container-type: inline-size;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
font-size: max(0.5rem, calc(var(--guild-acronym-font-scale) * 100cqi));
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: normal;
|
||||||
|
line-height: 1;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
import styles from '@app/features/guild/components/GuildAcronym.module.css';
|
||||||
|
import {
|
||||||
|
GUILD_ACRONYM_FALLBACK,
|
||||||
|
getFittedGuildAcronymFontScale,
|
||||||
|
getGuildAcronym,
|
||||||
|
} from '@app/features/guild/utils/GuildAcronymUtils';
|
||||||
|
import {clsx} from 'clsx';
|
||||||
|
import type React from 'react';
|
||||||
|
import {useMemo} from 'react';
|
||||||
|
|
||||||
|
interface GuildAcronymProps {
|
||||||
|
readonly name: string;
|
||||||
|
readonly className?: string;
|
||||||
|
readonly 'data-flx'?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type GuildAcronymStyle = React.CSSProperties & {
|
||||||
|
'--guild-acronym-font-scale': string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function GuildAcronym({name, className, 'data-flx': dataFlx}: GuildAcronymProps) {
|
||||||
|
const acronym = useMemo(() => getGuildAcronym(name) || GUILD_ACRONYM_FALLBACK, [name]);
|
||||||
|
const style = useMemo<GuildAcronymStyle>(
|
||||||
|
() => ({'--guild-acronym-font-scale': getFittedGuildAcronymFontScale(acronym).toFixed(4)}),
|
||||||
|
[acronym],
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<span className={clsx(styles.acronym, className)} style={style} data-flx={dataFlx ?? 'guild.guild-acronym.acronym'}>
|
||||||
|
<span className={styles.text} data-flx="guild.guild-acronym.text">
|
||||||
|
{acronym}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -131,35 +131,7 @@
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: 0.0625rem solid var(--background-modifier-accent);
|
border: 0.0625rem solid var(--background-modifier-accent);
|
||||||
background: var(--background-tertiary);
|
background: var(--background-tertiary);
|
||||||
container-type: size;
|
|
||||||
--initials-font-size: clamp(0.85rem, 45cqi, 1.35rem);
|
|
||||||
--initials-fit-inline-size: 92cqi;
|
|
||||||
--initials-letter-spacing: 0px;
|
|
||||||
--initials-scale: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.iconInitials {
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
line-height: 1;
|
|
||||||
white-space: nowrap;
|
|
||||||
display: inline-block;
|
|
||||||
text-align: center;
|
|
||||||
font-size: min(
|
|
||||||
var(--initials-font-size),
|
|
||||||
calc(var(--initials-fit-inline-size) / (var(--initials-width-em, 0.01) * var(--initials-scale)))
|
|
||||||
);
|
|
||||||
letter-spacing: var(--initials-letter-spacing);
|
|
||||||
transform: scale(var(--initials-scale));
|
|
||||||
transform-origin: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.iconPlaceholder[data-initials-length='medium'] .iconInitials {
|
|
||||||
--initials-font-size: clamp(0.85rem, 38cqi, 1.11rem);
|
|
||||||
--initials-letter-spacing: 0px;
|
|
||||||
--initials-scale: 0.92;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconActions {
|
.iconActions {
|
||||||
|
|||||||
+7
-30
@@ -13,18 +13,13 @@ import {openAssetSourceModal} from '@app/features/expressions/components/modals/
|
|||||||
import {isAnimatedFile} from '@app/features/expressions/utils/AnimatedImageUtils';
|
import {isAnimatedFile} from '@app/features/expressions/utils/AnimatedImageUtils';
|
||||||
import {formatImageUploadRecommendedHint} from '@app/features/expressions/utils/AssetUploadHintCopy';
|
import {formatImageUploadRecommendedHint} from '@app/features/expressions/utils/AssetUploadHintCopy';
|
||||||
import {isSvgFile, readImageFileAsUploadDataUrl} from '@app/features/expressions/utils/ImageUploadFileUtils';
|
import {isSvgFile, readImageFileAsUploadDataUrl} from '@app/features/expressions/utils/ImageUploadFileUtils';
|
||||||
|
import {GuildAcronym} from '@app/features/guild/components/GuildAcronym';
|
||||||
import styles from '@app/features/guild/components/modals/AddGuildModal.module.css';
|
import styles from '@app/features/guild/components/modals/AddGuildModal.module.css';
|
||||||
import {
|
import {
|
||||||
ANIMATED_ICONS_ARE_NOT_SUPPORTED_WHEN_CREATING_A_DESCRIPTOR,
|
ANIMATED_ICONS_ARE_NOT_SUPPORTED_WHEN_CREATING_A_DESCRIPTOR,
|
||||||
COMMUNITY_NAME_DESCRIPTOR,
|
COMMUNITY_NAME_DESCRIPTOR,
|
||||||
ICON_FILE_IS_TOO_LARGE_PLEASE_CHOOSE_A_DESCRIPTOR,
|
ICON_FILE_IS_TOO_LARGE_PLEASE_CHOOSE_A_DESCRIPTOR,
|
||||||
} from '@app/features/guild/components/modals/add_guild_modal/shared';
|
} from '@app/features/guild/components/modals/add_guild_modal/shared';
|
||||||
import * as StringUtils from '@app/features/guild/utils/GuildInitialsUtils';
|
|
||||||
import {
|
|
||||||
getGuildIconDisplayInitials,
|
|
||||||
getGuildInitialsFitStyle,
|
|
||||||
getInitialsLength,
|
|
||||||
} from '@app/features/guild/utils/GuildInitialsUtils';
|
|
||||||
import {
|
import {
|
||||||
FAILED_TO_PROCESS_CROPPED_IMAGE_DESCRIPTOR,
|
FAILED_TO_PROCESS_CROPPED_IMAGE_DESCRIPTOR,
|
||||||
INVALID_IMAGE_TRY_ANOTHER_DESCRIPTOR,
|
INVALID_IMAGE_TRY_ANOTHER_DESCRIPTOR,
|
||||||
@@ -45,7 +40,7 @@ import {ph} from '@lingui/core/macro';
|
|||||||
import {Trans, useLingui} from '@lingui/react/macro';
|
import {Trans, useLingui} from '@lingui/react/macro';
|
||||||
import {EnvelopeSimpleIcon} from '@phosphor-icons/react';
|
import {EnvelopeSimpleIcon} from '@phosphor-icons/react';
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import {useCallback, useMemo, useState} from 'react';
|
import {useCallback, useState} from 'react';
|
||||||
import type {UseFormReturn} from 'react-hook-form';
|
import type {UseFormReturn} from 'react-hook-form';
|
||||||
|
|
||||||
interface GuildCreateFormValues {
|
interface GuildCreateFormValues {
|
||||||
@@ -72,12 +67,7 @@ export function useGuildCreateIconField({
|
|||||||
}: GuildCreateIconFieldOptions) {
|
}: GuildCreateIconFieldOptions) {
|
||||||
const {i18n} = useLingui();
|
const {i18n} = useLingui();
|
||||||
const [previewIconUrl, setPreviewIconUrl] = useState<string | null>(null);
|
const [previewIconUrl, setPreviewIconUrl] = useState<string | null>(null);
|
||||||
const initials = useMemo(() => {
|
const iconName = (nameValue || '').trim();
|
||||||
const raw = (nameValue || '').trim();
|
|
||||||
if (!raw) return '';
|
|
||||||
return getGuildIconDisplayInitials(StringUtils.getInitialsFromName(raw));
|
|
||||||
}, [nameValue]);
|
|
||||||
const initialsLength = initials ? getInitialsLength(initials) : null;
|
|
||||||
const showIconUploadErrorModal = useCallback(
|
const showIconUploadErrorModal = useCallback(
|
||||||
(message: string) => {
|
(message: string) => {
|
||||||
showGenericErrorModal({
|
showGenericErrorModal({
|
||||||
@@ -156,7 +146,7 @@ export function useGuildCreateIconField({
|
|||||||
showGifOption: false,
|
showGifOption: false,
|
||||||
});
|
});
|
||||||
}, [handleIconUpload, i18n, messages]);
|
}, [handleIconUpload, i18n, messages]);
|
||||||
return {previewIconUrl, initials, initialsLength, handleOpenIconUpload, handleClearIcon};
|
return {previewIconUrl, iconName, handleOpenIconUpload, handleClearIcon};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderGuildCreateAccountGate(i18n: I18n, dataFlx: string): React.ReactNode {
|
export function renderGuildCreateAccountGate(i18n: I18n, dataFlx: string): React.ReactNode {
|
||||||
@@ -224,8 +214,7 @@ export function renderGuildCreateFields({
|
|||||||
nameInputDataFlx,
|
nameInputDataFlx,
|
||||||
showIconError,
|
showIconError,
|
||||||
previewIconUrl,
|
previewIconUrl,
|
||||||
initials,
|
iconName,
|
||||||
initialsLength,
|
|
||||||
handleOpenIconUpload,
|
handleOpenIconUpload,
|
||||||
handleClearIcon,
|
handleClearIcon,
|
||||||
}: GuildCreateFieldsOptions): React.ReactNode {
|
}: GuildCreateFieldsOptions): React.ReactNode {
|
||||||
@@ -244,20 +233,8 @@ export function renderGuildCreateFields({
|
|||||||
data-flx={`${dataFlx}.icon-image`}
|
data-flx={`${dataFlx}.icon-image`}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div className={styles.iconPlaceholder} data-flx={`${dataFlx}.icon-placeholder`}>
|
||||||
className={styles.iconPlaceholder}
|
{iconName ? <GuildAcronym name={iconName} data-flx={`${dataFlx}.icon-acronym`} /> : null}
|
||||||
data-initials-length={initialsLength}
|
|
||||||
data-flx={`${dataFlx}.icon-placeholder`}
|
|
||||||
>
|
|
||||||
{initials ? (
|
|
||||||
<span
|
|
||||||
className={styles.iconInitials}
|
|
||||||
style={getGuildInitialsFitStyle(initials)}
|
|
||||||
data-flx={`${dataFlx}.icon-initials`}
|
|
||||||
>
|
|
||||||
{initials}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={styles.iconActions} data-flx={`${dataFlx}.icon-actions`}>
|
<div className={styles.iconActions} data-flx={`${dataFlx}.icon-actions`}>
|
||||||
|
|||||||
@@ -11,54 +11,14 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 624.9375rem;
|
border-radius: 624.9375rem;
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-tertiary);
|
color: var(--text-tertiary);
|
||||||
background-color: var(--background-tertiary);
|
background-color: var(--background-tertiary);
|
||||||
background-image: var(--guild-icon-image, none);
|
background-image: var(--guild-icon-image, none);
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
container-type: inline-size;
|
|
||||||
--initials-font-size: clamp(0.85rem, 45cqi, 1.35rem);
|
|
||||||
--initials-fit-inline-size: 92cqi;
|
|
||||||
--initials-letter-spacing: 0px;
|
|
||||||
--initials-scale: 1;
|
|
||||||
--initials-scale-cap: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.containerNoIcon {
|
.containerNoIcon {
|
||||||
background-image: none;
|
background-image: none;
|
||||||
background-color: var(--background-tertiary);
|
background-color: var(--background-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.initials {
|
|
||||||
align-self: center;
|
|
||||||
line-height: 1;
|
|
||||||
white-space: nowrap;
|
|
||||||
display: inline-block;
|
|
||||||
text-align: center;
|
|
||||||
font-size: min(
|
|
||||||
var(--initials-font-size),
|
|
||||||
calc(var(--initials-fit-inline-size) / (var(--initials-width-em, 0.01) * var(--initials-scale)))
|
|
||||||
);
|
|
||||||
letter-spacing: var(--initials-letter-spacing);
|
|
||||||
transform: scale(calc(var(--initials-scale) * var(--initials-scale-cap)));
|
|
||||||
transform-origin: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container[data-initials-length='medium'] .initials {
|
|
||||||
--initials-font-size: clamp(0.85rem, 38cqi, 1.11rem);
|
|
||||||
--initials-scale: 0.92;
|
|
||||||
--initials-letter-spacing: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@container (max-width: 32px) {
|
|
||||||
.initials {
|
|
||||||
--initials-scale-cap: 0.88;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@container (max-width: 26px) {
|
|
||||||
.initials {
|
|
||||||
--initials-scale-cap: 0.78;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,13 +2,8 @@
|
|||||||
|
|
||||||
import {useHover} from '@app/features/app/hooks/useHover';
|
import {useHover} from '@app/features/app/hooks/useHover';
|
||||||
import {useShouldAnimate} from '@app/features/app/hooks/useShouldAnimate';
|
import {useShouldAnimate} from '@app/features/app/hooks/useShouldAnimate';
|
||||||
|
import {GuildAcronym} from '@app/features/guild/components/GuildAcronym';
|
||||||
import styles from '@app/features/guild/components/popouts/GuildIcon.module.css';
|
import styles from '@app/features/guild/components/popouts/GuildIcon.module.css';
|
||||||
import * as StringUtils from '@app/features/guild/utils/GuildInitialsUtils';
|
|
||||||
import {
|
|
||||||
getGuildIconDisplayInitials,
|
|
||||||
getGuildInitialsFitStyle,
|
|
||||||
getInitialsLength,
|
|
||||||
} from '@app/features/guild/utils/GuildInitialsUtils';
|
|
||||||
import {useRecoveringBackgroundImageURL} from '@app/features/messaging/hooks/useImageRecovery';
|
import {useRecoveringBackgroundImageURL} from '@app/features/messaging/hooks/useImageRecovery';
|
||||||
import * as ImageCacheUtils from '@app/features/messaging/utils/ImageCacheUtils';
|
import * as ImageCacheUtils from '@app/features/messaging/utils/ImageCacheUtils';
|
||||||
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
|
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
|
||||||
@@ -16,7 +11,7 @@ import * as AvatarUtils from '@app/features/user/utils/AvatarUtils';
|
|||||||
import {clsx} from 'clsx';
|
import {clsx} from 'clsx';
|
||||||
import {observer} from 'mobx-react-lite';
|
import {observer} from 'mobx-react-lite';
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import {useEffect, useMemo, useState} from 'react';
|
import {useEffect, useState} from 'react';
|
||||||
|
|
||||||
interface GuildIconProps {
|
interface GuildIconProps {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -43,8 +38,6 @@ export const GuildIcon = observer(function GuildIcon({
|
|||||||
sizePx,
|
sizePx,
|
||||||
containerProps,
|
containerProps,
|
||||||
}: GuildIconProps) {
|
}: GuildIconProps) {
|
||||||
const initials = useMemo(() => getGuildIconDisplayInitials(StringUtils.getInitialsFromName(name)), [name]);
|
|
||||||
const initialsLength = getInitialsLength(initials);
|
|
||||||
const [hoverRef, isHovering] = useHover();
|
const [hoverRef, isHovering] = useHover();
|
||||||
const iconUrl = icon ? AvatarUtils.getGuildIconURL({id, icon}) : null;
|
const iconUrl = icon ? AvatarUtils.getGuildIconURL({id, icon}) : null;
|
||||||
const hoverIconUrl = icon ? AvatarUtils.getGuildIconURL({id, icon}, true) : null;
|
const hoverIconUrl = icon ? AvatarUtils.getGuildIconURL({id, icon}, true) : null;
|
||||||
@@ -72,18 +65,9 @@ export const GuildIcon = observer(function GuildIcon({
|
|||||||
className={clsx(styles.container, className, paintedUrl == null && styles.containerNoIcon)}
|
className={clsx(styles.container, className, paintedUrl == null && styles.containerNoIcon)}
|
||||||
data-flx="guild.guild-icon.container"
|
data-flx="guild.guild-icon.container"
|
||||||
{...containerProps}
|
{...containerProps}
|
||||||
data-initials-length={initialsLength}
|
|
||||||
style={styleVars}
|
style={styleVars}
|
||||||
>
|
>
|
||||||
{paintedUrl == null && (
|
{paintedUrl == null && <GuildAcronym name={name} data-flx="guild.guild-icon.acronym" />}
|
||||||
<span
|
|
||||||
className={styles.initials}
|
|
||||||
style={getGuildInitialsFitStyle(initials)}
|
|
||||||
data-flx="guild.guild-icon.initials"
|
|
||||||
>
|
|
||||||
{initials}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
import {
|
||||||
|
getGuildAcronym,
|
||||||
|
getGuildAcronymFontScale,
|
||||||
|
getGuildAcronymLength,
|
||||||
|
} from '@app/features/guild/utils/GuildAcronymUtils';
|
||||||
|
import {describe, expect, test} from 'vitest';
|
||||||
|
|
||||||
|
const ACRONYM_VECTORS: ReadonlyArray<readonly [name: string, acronym: string, length: number]> = [
|
||||||
|
['Movies? We Like Movies', 'M?WLM', 5],
|
||||||
|
['Movies Movies', 'MM', 2],
|
||||||
|
['(Internal) Fluxer DevOps', '(I)FD', 5],
|
||||||
|
['secret server', 'ss', 2],
|
||||||
|
['Hampus server', 'Hs', 2],
|
||||||
|
['Fluxer', 'F', 1],
|
||||||
|
[' Fluxer ', 'F', 1],
|
||||||
|
['a b\tc\nd', 'abcd', 4],
|
||||||
|
["Bob's Burgers", 'BB', 2],
|
||||||
|
['Bob’s Burgers', 'BB', 2],
|
||||||
|
["Bob's", "B's", 3],
|
||||||
|
["Don't Panic", "D'tP", 4],
|
||||||
|
['Area 51', 'A5', 2],
|
||||||
|
['2024 Reunion', '2R', 2],
|
||||||
|
['Rock-n-Roll', 'R-n-R', 5],
|
||||||
|
['Spider-Man Fans', 'S-MF', 4],
|
||||||
|
['snake_case_name', 's', 1],
|
||||||
|
['C++ & Rust', 'C++&R', 5],
|
||||||
|
['@everyone', '@e', 2],
|
||||||
|
['!!!', '!!!', 3],
|
||||||
|
['\u{1F3AE}\u{1F3B2}', '\u{1F3AE}\u{1F3B2}', 2],
|
||||||
|
['\u{1F496}tes tes', '\u{1F496}tt', 3],
|
||||||
|
['❤️love', '❤️l', 2],
|
||||||
|
['\u{1F468}\u{1F469}\u{1F467} Family', '\u{1F468}\u{1F469}\u{1F467}F', 2],
|
||||||
|
['\u{1F1F8}\u{1F1EA} Sweden', '\u{1F1F8}\u{1F1EA}S', 2],
|
||||||
|
['1️⃣ One', '1️⃣O', 2],
|
||||||
|
['日本語 サーバー', '日サ', 2],
|
||||||
|
['原神交流群', '原', 1],
|
||||||
|
['한국 서버', '한서', 2],
|
||||||
|
['Привет Мир', 'ПМ', 2],
|
||||||
|
['مرحبا بالعالم', 'مب', 2],
|
||||||
|
['שלום עולם', 'שע', 2],
|
||||||
|
['میخواهم بازی', 'مب', 2],
|
||||||
|
['École Normale', 'ÉN', 2],
|
||||||
|
['नमस्ते दुनिया', 'नदु', 2],
|
||||||
|
['one two three four five six seven eight nine ten', 'ottffssent', 10],
|
||||||
|
['', '', 0],
|
||||||
|
[' \t ', '', 0],
|
||||||
|
];
|
||||||
|
|
||||||
|
const FONT_SCALE_VECTORS: ReadonlyArray<readonly [length: number, fontSizePxAt48: number]> = [
|
||||||
|
[0, 18],
|
||||||
|
[1, 18],
|
||||||
|
[2, 18],
|
||||||
|
[3, 16],
|
||||||
|
[4, 14],
|
||||||
|
[5, 12],
|
||||||
|
[6, 10],
|
||||||
|
[10, 10],
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('getGuildAcronym', () => {
|
||||||
|
test.each(ACRONYM_VECTORS)('%j -> %j', (name, acronym, length) => {
|
||||||
|
expect(getGuildAcronym(name)).toBe(acronym);
|
||||||
|
expect(getGuildAcronymLength(acronym)).toBe(length);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getGuildAcronymFontScale', () => {
|
||||||
|
test.each(FONT_SCALE_VECTORS)('%i graphemes -> %ipx on a 48px icon', (length, fontSizePx) => {
|
||||||
|
expect(getGuildAcronymFontScale(length) * 48).toBeCloseTo(fontSizePx);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
const POSSESSIVE_SUFFIX = /['’]s(?=\s)/gu;
|
||||||
|
const WORD_RUN = /([\p{L}\p{N}_]\p{M}*)[\p{L}\p{N}\p{M}_]*/gu;
|
||||||
|
const WHITESPACE = /\s+/gu;
|
||||||
|
const GUILD_ACRONYM_REFERENCE_ICON_SIZE_PX = 48;
|
||||||
|
const GUILD_ACRONYM_FONT_SIZES_PX = [18, 18, 16, 14, 12, 10];
|
||||||
|
const GUILD_ACRONYM_FIT_PADDING_PX = 4;
|
||||||
|
const GUILD_ACRONYM_FIT_WIDTH_RATIO =
|
||||||
|
(GUILD_ACRONYM_REFERENCE_ICON_SIZE_PX - GUILD_ACRONYM_FIT_PADDING_PX * 2) / GUILD_ACRONYM_REFERENCE_ICON_SIZE_PX;
|
||||||
|
const GUILD_ACRONYM_MEASURE_FONT_SIZE_PX = 100;
|
||||||
|
const GUILD_ACRONYM_WIDTH_CACHE_CAPACITY = 512;
|
||||||
|
|
||||||
|
const acronymWidthCache = new Map<string, number>();
|
||||||
|
let measureContext: CanvasRenderingContext2D | null | undefined;
|
||||||
|
|
||||||
|
export const GUILD_ACRONYM_FALLBACK = '?';
|
||||||
|
|
||||||
|
const graphemeSegmenter =
|
||||||
|
typeof Intl !== 'undefined' && typeof Intl.Segmenter === 'function'
|
||||||
|
? new Intl.Segmenter(undefined, {granularity: 'grapheme'})
|
||||||
|
: null;
|
||||||
|
|
||||||
|
function firstGrapheme(wordRun: string, leadingCharacterWithMarks: string): string {
|
||||||
|
if (graphemeSegmenter == null) return leadingCharacterWithMarks;
|
||||||
|
for (const {segment} of graphemeSegmenter.segment(wordRun)) {
|
||||||
|
return segment;
|
||||||
|
}
|
||||||
|
return leadingCharacterWithMarks;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getGuildAcronym(name: string): string {
|
||||||
|
return name.replace(POSSESSIVE_SUFFIX, '').replace(WORD_RUN, firstGrapheme).replace(WHITESPACE, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getGuildAcronymLength(acronym: string): number {
|
||||||
|
if (graphemeSegmenter == null) return Array.from(acronym).length;
|
||||||
|
let length = 0;
|
||||||
|
for (const _segment of graphemeSegmenter.segment(acronym)) {
|
||||||
|
length += 1;
|
||||||
|
}
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getGuildAcronymFontScale(acronymLength: number): number {
|
||||||
|
const index = Math.min(Math.max(acronymLength, 1), GUILD_ACRONYM_FONT_SIZES_PX.length) - 1;
|
||||||
|
return GUILD_ACRONYM_FONT_SIZES_PX[index] / GUILD_ACRONYM_REFERENCE_ICON_SIZE_PX;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMeasureContext(): CanvasRenderingContext2D | null {
|
||||||
|
if (measureContext !== undefined) return measureContext;
|
||||||
|
if (typeof document === 'undefined' || document.body == null) return null;
|
||||||
|
const context = document.createElement('canvas').getContext('2d');
|
||||||
|
if (context != null) {
|
||||||
|
context.font = `500 ${GUILD_ACRONYM_MEASURE_FONT_SIZE_PX}px ${getComputedStyle(document.body).fontFamily}`;
|
||||||
|
}
|
||||||
|
measureContext = context;
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGuildAcronymWidthPerEm(acronym: string): number | null {
|
||||||
|
const cached = acronymWidthCache.get(acronym);
|
||||||
|
if (cached !== undefined) return cached;
|
||||||
|
const context = getMeasureContext();
|
||||||
|
if (context == null) return null;
|
||||||
|
const widthPerEm = context.measureText(acronym).width / GUILD_ACRONYM_MEASURE_FONT_SIZE_PX;
|
||||||
|
if (acronymWidthCache.size >= GUILD_ACRONYM_WIDTH_CACHE_CAPACITY) acronymWidthCache.clear();
|
||||||
|
acronymWidthCache.set(acronym, widthPerEm);
|
||||||
|
return widthPerEm;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fitGuildAcronymFontScale(acronymLength: number, widthPerEm: number | null): number {
|
||||||
|
const scale = getGuildAcronymFontScale(acronymLength);
|
||||||
|
if (widthPerEm == null || widthPerEm <= 0) return scale;
|
||||||
|
const maxScale = GUILD_ACRONYM_FIT_WIDTH_RATIO / widthPerEm;
|
||||||
|
if (scale <= maxScale) return scale;
|
||||||
|
for (const fontSize of GUILD_ACRONYM_FONT_SIZES_PX) {
|
||||||
|
const candidate = fontSize / GUILD_ACRONYM_REFERENCE_ICON_SIZE_PX;
|
||||||
|
if (candidate < scale && candidate <= maxScale) return candidate;
|
||||||
|
}
|
||||||
|
return maxScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFittedGuildAcronymFontScale(acronym: string): number {
|
||||||
|
return fitGuildAcronymFontScale(getGuildAcronymLength(acronym), getGuildAcronymWidthPerEm(acronym));
|
||||||
|
}
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
import type React from 'react';
|
|
||||||
|
|
||||||
const GUILD_ICON_INITIALS_MAX_LENGTH = 4;
|
|
||||||
const MEASURE_FONT_SIZES_PX = [10, 12, 14, 16, 20, 24];
|
|
||||||
const MEASURE_FONT_WEIGHT = 600;
|
|
||||||
|
|
||||||
export const GuildInitialsLength = Object.freeze({
|
|
||||||
SHORT: 'short',
|
|
||||||
MEDIUM: 'medium',
|
|
||||||
} as const);
|
|
||||||
|
|
||||||
export type GuildInitialsLength = (typeof GuildInitialsLength)[keyof typeof GuildInitialsLength];
|
|
||||||
|
|
||||||
export type GuildInitialsFitStyle = React.CSSProperties & {
|
|
||||||
'--initials-width-em'?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const measuredWidthsEm = new Map<string, number>();
|
|
||||||
let measureContext: OffscreenCanvasRenderingContext2D | null | undefined;
|
|
||||||
|
|
||||||
export function getInitialsFromName(name: string): string {
|
|
||||||
const words = name.split(/\s+/u).filter(Boolean);
|
|
||||||
return words
|
|
||||||
.map((word) => Array.from(word)[0])
|
|
||||||
.filter(Boolean)
|
|
||||||
.join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function truncateInitials(initials: string, maxLength: number): string {
|
|
||||||
if (maxLength <= 0) return '';
|
|
||||||
return Array.from(initials).slice(0, maxLength).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getGuildIconDisplayInitials(initials: string): string {
|
|
||||||
return truncateInitials(initials, GUILD_ICON_INITIALS_MAX_LENGTH);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getInitialsLength(displayInitials: string): GuildInitialsLength {
|
|
||||||
return Array.from(displayInitials).length <= 2 ? GuildInitialsLength.SHORT : GuildInitialsLength.MEDIUM;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMeasureContext(): OffscreenCanvasRenderingContext2D | null {
|
|
||||||
if (measureContext !== undefined) return measureContext;
|
|
||||||
if (typeof document === 'undefined' || typeof OffscreenCanvas === 'undefined') {
|
|
||||||
measureContext = null;
|
|
||||||
return measureContext;
|
|
||||||
}
|
|
||||||
measureContext = new OffscreenCanvas(1, 1).getContext('2d');
|
|
||||||
document.fonts?.addEventListener('loadingdone', () => measuredWidthsEm.clear());
|
|
||||||
return measureContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
function measureInitialsWidthEm(initials: string): number | null {
|
|
||||||
const context = getMeasureContext();
|
|
||||||
if (context == null || document.body == null) return null;
|
|
||||||
const fontFamily = getComputedStyle(document.body).fontFamily;
|
|
||||||
const key = `${fontFamily}\u0000${initials}`;
|
|
||||||
const cached = measuredWidthsEm.get(key);
|
|
||||||
if (cached !== undefined) return cached;
|
|
||||||
let widthEm = 0;
|
|
||||||
for (const fontSize of MEASURE_FONT_SIZES_PX) {
|
|
||||||
context.font = `${MEASURE_FONT_WEIGHT} ${fontSize}px ${fontFamily}`;
|
|
||||||
widthEm = Math.max(widthEm, context.measureText(initials).width / fontSize);
|
|
||||||
}
|
|
||||||
measuredWidthsEm.set(key, widthEm);
|
|
||||||
return widthEm;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getGuildInitialsFitStyle(displayInitials: string): GuildInitialsFitStyle | undefined {
|
|
||||||
if (!displayInitials) return undefined;
|
|
||||||
const widthEm = measureInitialsWidthEm(displayInitials);
|
|
||||||
if (widthEm == null || widthEm <= 0) return undefined;
|
|
||||||
return {'--initials-width-em': widthEm.toFixed(3)};
|
|
||||||
}
|
|
||||||
@@ -14,10 +14,9 @@ import {getDMDisplayName, getIcon, getName} from '@app/features/channel/utils/Ch
|
|||||||
import DeveloperMode from '@app/features/devtools/state/DeveloperMode';
|
import DeveloperMode from '@app/features/devtools/state/DeveloperMode';
|
||||||
import {joinDiscoveryGuild} from '@app/features/discovery/commands/DiscoveryJoinCommands';
|
import {joinDiscoveryGuild} from '@app/features/discovery/commands/DiscoveryJoinCommands';
|
||||||
import DiscoveryChannelPreviews from '@app/features/discovery/state/DiscoveryChannelPreviews';
|
import DiscoveryChannelPreviews from '@app/features/discovery/state/DiscoveryChannelPreviews';
|
||||||
|
import {GuildAcronym} from '@app/features/guild/components/GuildAcronym';
|
||||||
import type {Guild} from '@app/features/guild/models/Guild';
|
import type {Guild} from '@app/features/guild/models/Guild';
|
||||||
import Guilds from '@app/features/guild/state/Guilds';
|
import Guilds from '@app/features/guild/state/Guilds';
|
||||||
import * as StringUtils from '@app/features/guild/utils/GuildInitialsUtils';
|
|
||||||
import {getGuildIconDisplayInitials} from '@app/features/guild/utils/GuildInitialsUtils';
|
|
||||||
import {
|
import {
|
||||||
COPY_LINK_DESCRIPTOR,
|
COPY_LINK_DESCRIPTOR,
|
||||||
OKAY_DESCRIPTOR,
|
OKAY_DESCRIPTOR,
|
||||||
@@ -228,7 +227,6 @@ function CompactGuildMentionIcon({guild}: {guild: Pick<Guild, 'id' | 'name' | 'i
|
|||||||
const style: CompactGuildMentionIconStyle | undefined = iconUrl
|
const style: CompactGuildMentionIconStyle | undefined = iconUrl
|
||||||
? {'--jump-link-guild-icon-image': `url(${iconUrl})`}
|
? {'--jump-link-guild-icon-image': `url(${iconUrl})`}
|
||||||
: undefined;
|
: undefined;
|
||||||
const initials = getGuildIconDisplayInitials(StringUtils.getInitialsFromName(guild.name));
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={clsx(jumpLinkStyles.icon, jumpLinkStyles.guildIcon)}
|
className={clsx(jumpLinkStyles.icon, jumpLinkStyles.guildIcon)}
|
||||||
@@ -238,12 +236,10 @@ function CompactGuildMentionIcon({guild}: {guild: Pick<Guild, 'id' | 'name' | 'i
|
|||||||
data-flx="messaging.markdown.renderers.link-renderer.compact-guild-mention-icon"
|
data-flx="messaging.markdown.renderers.link-renderer.compact-guild-mention-icon"
|
||||||
>
|
>
|
||||||
{!iconUrl && (
|
{!iconUrl && (
|
||||||
<span
|
<GuildAcronym
|
||||||
className={jumpLinkStyles.guildInitials}
|
name={guild.name}
|
||||||
data-flx="messaging.markdown.renderers.link-renderer.compact-guild-mention-icon.initials"
|
data-flx="messaging.markdown.renderers.link-renderer.compact-guild-mention-icon.acronym"
|
||||||
>
|
/>
|
||||||
{initials}
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
-15
@@ -59,21 +59,6 @@
|
|||||||
background-position: center;
|
background-position: center;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.guildInitials {
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
font-size: 0.56em;
|
|
||||||
line-height: 1;
|
|
||||||
max-width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
text-align: center;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider {
|
.divider {
|
||||||
|
|||||||
+1
-2
@@ -203,8 +203,7 @@
|
|||||||
border: 0.0625rem solid var(--entrance-sound-border);
|
border: 0.0625rem solid var(--entrance-sound-border);
|
||||||
background: color-mix(in srgb, #0f1014 80%, var(--background-header-secondary) 20%);
|
background: color-mix(in srgb, #0f1014 80%, var(--background-header-secondary) 20%);
|
||||||
color: white;
|
color: white;
|
||||||
font-size: 0.75rem;
|
overflow: hidden;
|
||||||
font-weight: 700;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.scopeTileText {
|
.scopeTileText {
|
||||||
|
|||||||
+5
-2
@@ -3,6 +3,7 @@
|
|||||||
import RuntimeConfig from '@app/features/app/state/RuntimeConfig';
|
import RuntimeConfig from '@app/features/app/state/RuntimeConfig';
|
||||||
import {LimitResolver} from '@app/features/app/utils/LimitResolverAdapter';
|
import {LimitResolver} from '@app/features/app/utils/LimitResolverAdapter';
|
||||||
import {isLimitToggleEnabled} from '@app/features/app/utils/LimitUtils';
|
import {isLimitToggleEnabled} from '@app/features/app/utils/LimitUtils';
|
||||||
|
import {GuildAcronym} from '@app/features/guild/components/GuildAcronym';
|
||||||
import Guilds from '@app/features/guild/state/Guilds';
|
import Guilds from '@app/features/guild/state/Guilds';
|
||||||
import {DIRECT_MESSAGES_DESCRIPTOR, GET_PREMIUM_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
import {DIRECT_MESSAGES_DESCRIPTOR, GET_PREMIUM_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
||||||
import {getCachedNumberFormat} from '@app/features/i18n/utils/IntlCache';
|
import {getCachedNumberFormat} from '@app/features/i18n/utils/IntlCache';
|
||||||
@@ -186,7 +187,6 @@ interface ScopeArtworkProps {
|
|||||||
function ScopeArtwork({option, selected, compact}: ScopeArtworkProps): React.ReactElement {
|
function ScopeArtwork({option, selected, compact}: ScopeArtworkProps): React.ReactElement {
|
||||||
if (option.guildId) {
|
if (option.guildId) {
|
||||||
const iconUrl = option.guildIcon ? AvatarUtils.getGuildIconURL({id: option.guildId, icon: option.guildIcon}) : null;
|
const iconUrl = option.guildIcon ? AvatarUtils.getGuildIconURL({id: option.guildId, icon: option.guildIcon}) : null;
|
||||||
const initial = option.label.trim().charAt(0).toUpperCase() || '?';
|
|
||||||
return iconUrl ? (
|
return iconUrl ? (
|
||||||
<span
|
<span
|
||||||
className={clsx(styles.scopeAvatar, compact && styles.scopeAvatarCompact)}
|
className={clsx(styles.scopeAvatar, compact && styles.scopeAvatarCompact)}
|
||||||
@@ -200,7 +200,10 @@ function ScopeArtwork({option, selected, compact}: ScopeArtworkProps): React.Rea
|
|||||||
aria-hidden
|
aria-hidden
|
||||||
data-flx="user.entrance-sound-section.scope-artwork.scope-avatar-placeholder"
|
data-flx="user.entrance-sound-section.scope-artwork.scope-avatar-placeholder"
|
||||||
>
|
>
|
||||||
{initial}
|
<GuildAcronym
|
||||||
|
name={option.label}
|
||||||
|
data-flx="user.entrance-sound-section.scope-artwork.scope-avatar-placeholder-acronym"
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -48,8 +48,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: white;
|
color: white;
|
||||||
font-weight: 700;
|
overflow: hidden;
|
||||||
font-size: 0.85rem;
|
|
||||||
border: 0.0625rem solid var(--background-modifier-accent);
|
border: 0.0625rem solid var(--background-modifier-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-2
@@ -1,6 +1,7 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
import type {GuildComboboxOption} from '@app/features/app/components/dialogs/shared/GuildComboboxRenderers';
|
import type {GuildComboboxOption} from '@app/features/app/components/dialogs/shared/GuildComboboxRenderers';
|
||||||
|
import {GuildAcronym} from '@app/features/guild/components/GuildAcronym';
|
||||||
import Guilds from '@app/features/guild/state/Guilds';
|
import Guilds from '@app/features/guild/state/Guilds';
|
||||||
import {Combobox} from '@app/features/ui/components/form/FormCombobox';
|
import {Combobox} from '@app/features/ui/components/form/FormCombobox';
|
||||||
import styles from '@app/features/user/components/modals/tabs/my_profile_tab/ProfileTypeSelector.module.css';
|
import styles from '@app/features/user/components/modals/tabs/my_profile_tab/ProfileTypeSelector.module.css';
|
||||||
@@ -31,7 +32,6 @@ export const ProfileTypeSelector = observer(({selectedGuildId, onChange, disable
|
|||||||
const renderGuildRow = useCallback((option: GuildComboboxOption) => {
|
const renderGuildRow = useCallback((option: GuildComboboxOption) => {
|
||||||
const isGlobal = !option.value;
|
const isGlobal = !option.value;
|
||||||
const iconUrl = option.icon ? AvatarUtils.getGuildIconURL({id: option.value, icon: option.icon}) : null;
|
const iconUrl = option.icon ? AvatarUtils.getGuildIconURL({id: option.value, icon: option.icon}) : null;
|
||||||
const initial = option.label.charAt(0).toUpperCase();
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(styles.guildOption, isGlobal && styles.guildOptionGlobal)}
|
className={clsx(styles.guildOption, isGlobal && styles.guildOptionGlobal)}
|
||||||
@@ -51,7 +51,10 @@ export const ProfileTypeSelector = observer(({selectedGuildId, onChange, disable
|
|||||||
aria-hidden
|
aria-hidden
|
||||||
data-flx="user.my-profile-tab.profile-type-selector.render-guild-row.guild-avatar-placeholder"
|
data-flx="user.my-profile-tab.profile-type-selector.render-guild-row.guild-avatar-placeholder"
|
||||||
>
|
>
|
||||||
{initial}
|
<GuildAcronym
|
||||||
|
name={option.label}
|
||||||
|
data-flx="user.my-profile-tab.profile-type-selector.render-guild-row.guild-avatar-placeholder-acronym"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<span
|
<span
|
||||||
|
|||||||
Reference in new issue
Block a user