From e3a87a0326a95ced03694a274aa70c9bc17cf81c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Fri, 5 Apr 2024 17:12:12 +0200 Subject: [PATCH] Support role badges on Mastodon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- src/components/badge.tsx | 21 ++++++++++++++++--- .../ui/components/profile-info-panel.tsx | 12 +++++------ src/schemas/account.ts | 14 +++++++++++++ src/utils/theme.ts | 2 +- 4 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/components/badge.tsx b/src/components/badge.tsx index eeb03e38b..ac3c60a9e 100644 --- a/src/components/badge.tsx +++ b/src/components/badge.tsx @@ -1,18 +1,32 @@ import clsx from 'clsx'; -import React from 'react'; +import React, { useMemo } from 'react'; + +import { hexToHsl } from 'soapbox/utils/theme'; interface IBadge { title: React.ReactNode; slug: string; + color?: string; } /** Badge to display on a user's profile. */ -const Badge: React.FC = ({ title, slug }) => { +const Badge: React.FC = ({ title, slug, color }) => { const fallback = !['patron', 'admin', 'moderator', 'opaque', 'badge:donor'].includes(slug); + const isDark = useMemo(() => { + const hsl = hexToHsl(color!); + + if (hsl && hsl.l > 50) return false; + + return true; + }, [color]); + return ( = ({ title, slug }) => { 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100': fallback, 'bg-white/75 text-gray-900': slug === 'opaque', })} + style={color ? { background: color } : undefined} > {title} diff --git a/src/features/ui/components/profile-info-panel.tsx b/src/features/ui/components/profile-info-panel.tsx index a14f6bce7..9f89bfeb8 100644 --- a/src/features/ui/components/profile-info-panel.tsx +++ b/src/features/ui/components/profile-info-panel.tsx @@ -7,7 +7,6 @@ import Markup from 'soapbox/components/markup'; import { dateFormatOptions } from 'soapbox/components/relative-timestamp'; import { Icon, HStack, Stack, Text } from 'soapbox/components/ui'; import { useAppSelector, useSoapboxConfig } from 'soapbox/hooks'; -import { badgeToTag, getBadges as getAccountBadges } from 'soapbox/utils/badges'; import { capitalize } from 'soapbox/utils/strings'; import ProfileFamiliarFollowers from './profile-familiar-followers'; @@ -58,13 +57,14 @@ const ProfileInfoPanel: React.FC = ({ account, username }) => }; const getCustomBadges = (): React.ReactNode[] => { - const badges = account ? getAccountBadges(account) : []; + const badges = account?.roles || []; - return badges.map(badge => ( + return badges.filter(badge => badge.highlighted).map(badge => ( )); }; diff --git a/src/schemas/account.ts b/src/schemas/account.ts index 6065e6cc5..06252ac5a 100644 --- a/src/schemas/account.ts +++ b/src/schemas/account.ts @@ -17,12 +17,21 @@ const headerMissing = require('soapbox/assets/images/header-missing.png'); const birthdaySchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/); +const hexSchema = z.string().regex(/^#[a-f0-9]{6}$/i); + const fieldSchema = z.object({ name: z.string(), value: z.string(), verified_at: z.string().datetime().nullable().catch(null), }); +const roleSchema = z.object({ + id: z.string().catch(''), + name: z.string().catch(''), + color: hexSchema.catch(''), + highlighted: z.boolean().catch(true), +}); + const baseAccountSchema = z.object({ acct: z.string().catch(''), avatar: z.string().catch(avatarMissing), @@ -85,6 +94,7 @@ const baseAccountSchema = z.object({ relationship: relationshipSchema.optional().catch(undefined), tags: z.array(z.string()).catch([]), }).optional().catch(undefined), + roles: filteredArray(roleSchema), source: z.object({ approved: z.boolean().catch(true), chats_onboarded: z.boolean().catch(true), @@ -118,6 +128,9 @@ const getDomain = (url: string) => { } }; +const filterBadges = (tags?: string[]) => + tags?.filter(tag => tag.startsWith('badge:')).map(tag => ({ id: tag, name: tag.replace(/^badge:/, '') })); + /** Add internal fields to the account. */ const transformAccount = ({ pleroma, other_settings, fields, ...account }: T) => { const customEmojiMap = makeCustomEmojiMap(account.emojis); @@ -156,6 +169,7 @@ const transformAccount = ({ pleroma, other_setti const { relationship, ...rest } = pleroma; return rest; })(), + roles: account.roles || filterBadges(pleroma?.tags), relationship: pleroma?.relationship, staff: pleroma?.is_admin || pleroma?.is_moderator || false, suspended: account.suspended || pleroma?.deactivated || false, diff --git a/src/utils/theme.ts b/src/utils/theme.ts index 86b2c4a01..a6b80bcd6 100644 --- a/src/utils/theme.ts +++ b/src/utils/theme.ts @@ -117,7 +117,7 @@ export const generateThemeCss = (soapboxConfig: SoapboxConfig): string => { return colorsToCss(soapboxConfig.colors.toJS() as TailwindColorPalette); }; -const hexToHsl = (hex: string): Hsl | null => { +export const hexToHsl = (hex: string): Hsl | null => { const rgb = hexToRgb(hex); return rgb ? rgbToHsl(rgb) : null; };