diff --git a/packages/pl-fe/package.json b/packages/pl-fe/package.json index 7415cb95a..54e1bf872 100644 --- a/packages/pl-fe/package.json +++ b/packages/pl-fe/package.json @@ -84,6 +84,7 @@ "emoji-datasource": "15.0.1", "emoji-mart": "^5.6.0", "exifr": "^7.1.3", + "fast-average-color": "^9.5.0", "fasttext.wasm.js": "^1.0.0", "flexsearch": "^0.7.43", "fuzzysort": "^3.1.0", diff --git a/packages/pl-fe/src/components/account.tsx b/packages/pl-fe/src/components/account.tsx index 94b85bd4b..f2a4884a3 100644 --- a/packages/pl-fe/src/components/account.tsx +++ b/packages/pl-fe/src/components/account.tsx @@ -193,7 +193,7 @@ const Account = ({
- + {emoji && ( - + {withAvatar && ( {children}} > - + {emoji && ( = ({ accountIds, limit = 3 }) => { src={account.avatar} alt={account.avatar_description} size={20} + isCat={account.is_cat} />
))} diff --git a/packages/pl-fe/src/components/still-image.tsx b/packages/pl-fe/src/components/still-image.tsx index fd4c27217..1c5942463 100644 --- a/packages/pl-fe/src/components/still-image.tsx +++ b/packages/pl-fe/src/components/still-image.tsx @@ -8,6 +8,8 @@ interface IStillImage { alt?: string; /** Extra class names for the outer
container. */ className?: string; + /** Extra class names for the inner element. */ + innerClassName?: string; /** URL to the image */ src: string; /** Extra CSS styles on the outer
element. */ @@ -18,6 +20,8 @@ interface IStillImage { showExt?: boolean; /** Callback function if the image fails to load */ onError?(): void; + /** Callback function if the image loads successfully */ + onLoad?: React.ReactEventHandler; /** Treat as animated, no matter the extension */ isGif?: boolean; /** Specify that the group is defined by the parent */ @@ -26,7 +30,7 @@ interface IStillImage { /** Renders images on a canvas, only playing GIFs if autoPlayGif is enabled. */ const StillImage: React.FC = ({ - alt, className, src, style, letterboxed = false, showExt = false, onError, isGif, noGroup, + alt, className, innerClassName, src, style, letterboxed = false, showExt = false, onError, onLoad, isGif, noGroup, }) => { const { autoPlayGif } = useSettings(); @@ -37,16 +41,20 @@ const StillImage: React.FC = ({ src && !autoPlayGif && ((isGif) || src.endsWith('.gif') || src.startsWith('blob:')) ); - const handleImageLoad = () => { + const handleImageLoad: React.ReactEventHandler = (e) => { if (hoverToPlay && canvas.current && img.current) { canvas.current.width = img.current.naturalWidth; canvas.current.height = img.current.naturalHeight; canvas.current.getContext('2d')?.drawImage(img.current, 0, 0); } + + if (onLoad) { + onLoad(e); + } }; /** ClassNames shared between the `` and `` elements. */ - const baseClassName = clsx('block size-full', { + const baseClassName = clsx('block size-full', innerClassName, { 'object-contain': letterboxed, 'object-cover': !letterboxed, }); @@ -54,7 +62,7 @@ const StillImage: React.FC = ({ return (
= ({ className={clsx(baseClassName, { 'invisible group-hover:visible': hoverToPlay, })} + crossOrigin='anonymous' /> {hoverToPlay && ( diff --git a/packages/pl-fe/src/components/ui/avatar.tsx b/packages/pl-fe/src/components/ui/avatar.tsx index 7374c1f9e..8c87c36c8 100644 --- a/packages/pl-fe/src/components/ui/avatar.tsx +++ b/packages/pl-fe/src/components/ui/avatar.tsx @@ -1,4 +1,5 @@ import clsx from 'clsx'; +import { FastAverageColor } from 'fast-average-color'; import React, { useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; @@ -15,22 +16,36 @@ const messages = defineMessages({ interface IAvatar extends Pick { /** Width and height of the avatar in pixels. */ size?: number; + /** Whether the user is a cat. */ + isCat?: boolean; } +const fac = new FastAverageColor(); + /** Round profile avatar for accounts. */ const Avatar = (props: IAvatar) => { const intl = useIntl(); - const { alt, src, size = AVATAR_SIZE, className } = props; + const { alt, src, size = AVATAR_SIZE, className, isCat } = props; + const [color, setColor] = useState(undefined); const [isAvatarMissing, setIsAvatarMissing] = useState(false); const handleLoadFailure = () => setIsAvatarMissing(true); + const handleLoad = (e: React.SyntheticEvent) => { + const color = fac.getColor(e.currentTarget); + if (!color.error) { + setColor(color.hex); + } + }; + const style: React.CSSProperties = React.useMemo(() => ({ width: size, height: size, - }), [size]); + fontSize: size, + color, + }), [size, color]); if (isAvatarMissing) { return ( @@ -38,8 +53,9 @@ const Avatar = (props: IAvatar) => { style={{ width: size, height: size, + color, }} - className={clsx('flex items-center justify-center rounded-lg bg-gray-200 dark:bg-gray-900', className)} + className={clsx('flex items-center justify-center rounded-lg bg-gray-200 leading-[0] dark:bg-gray-900', isCat && 'avatar__cat', className)} > { return ( ); }; diff --git a/packages/pl-fe/src/features/account/components/header.tsx b/packages/pl-fe/src/features/account/components/header.tsx index 3b74b4770..e76e31a0f 100644 --- a/packages/pl-fe/src/features/account/components/header.tsx +++ b/packages/pl-fe/src/features/account/components/header.tsx @@ -657,6 +657,7 @@ const Header: React.FC = ({ account }) => { alt={account.avatar_description} size={96} className='relative size-24 rounded-lg bg-white ring-4 ring-white black:ring-black dark:bg-primary-900 dark:ring-primary-900' + isCat={account.is_cat} /> {account.verified && ( diff --git a/packages/pl-fe/src/features/admin/components/report.tsx b/packages/pl-fe/src/features/admin/components/report.tsx index e0eb2e20d..1276f1785 100644 --- a/packages/pl-fe/src/features/admin/components/report.tsx +++ b/packages/pl-fe/src/features/admin/components/report.tsx @@ -91,6 +91,7 @@ const Report: React.FC = ({ id }) => { alt={targetAccount.avatar_description} size={32} className='overflow-hidden' + isCat={targetAccount.is_cat} /> diff --git a/packages/pl-fe/src/features/chats/components/chat-list-item.tsx b/packages/pl-fe/src/features/chats/components/chat-list-item.tsx index 50094be7b..30be45ba5 100644 --- a/packages/pl-fe/src/features/chats/components/chat-list-item.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-list-item.tsx @@ -94,6 +94,7 @@ const ChatListItem: React.FC = ({ chat, onClick }) => { alt={chat.account.avatar_description} size={40} className='flex-none' + isCat={chat.account.is_cat} /> diff --git a/packages/pl-fe/src/features/chats/components/chat-message-list.tsx b/packages/pl-fe/src/features/chats/components/chat-message-list.tsx index 84779fe6c..9ca0e3e89 100644 --- a/packages/pl-fe/src/features/chats/components/chat-message-list.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-message-list.tsx @@ -186,7 +186,7 @@ const ChatMessageList: React.FC = ({ chat }) => { return ( - + <> {intl.formatMessage(messages.blockedBy)} diff --git a/packages/pl-fe/src/features/chats/components/chat-page/components/chat-page-main.tsx b/packages/pl-fe/src/features/chats/components/chat-page/components/chat-page-main.tsx index a02691f69..07e7aa017 100644 --- a/packages/pl-fe/src/features/chats/components/chat-page/components/chat-page-main.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-page/components/chat-page-main.tsx @@ -137,7 +137,7 @@ const ChatPageMain = () => { /> - + @@ -157,7 +157,7 @@ const ChatPageMain = () => { src={require('@tabler/icons/outline/info-circle.svg')} component={() => ( - + {chat.account.display_name} @{chat.account.acct} diff --git a/packages/pl-fe/src/features/chats/components/chat-search/results.tsx b/packages/pl-fe/src/features/chats/components/chat-search/results.tsx index f7f33a336..e1f8b32e4 100644 --- a/packages/pl-fe/src/features/chats/components/chat-search/results.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-search/results.tsx @@ -41,7 +41,7 @@ const Results = ({ accountSearchResult, onSelect, parentRef }: IResults) => { data-testid='account' > - +
diff --git a/packages/pl-fe/src/features/chats/components/chat-widget/chat-settings.tsx b/packages/pl-fe/src/features/chats/components/chat-widget/chat-settings.tsx index 78ed96db8..d28169b5e 100644 --- a/packages/pl-fe/src/features/chats/components/chat-widget/chat-settings.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-widget/chat-settings.tsx @@ -113,7 +113,7 @@ const ChatSettings = () => { - + {chat.account.display_name} @{chat.account.acct} diff --git a/packages/pl-fe/src/features/chats/components/chat-widget/chat-window.tsx b/packages/pl-fe/src/features/chats/components/chat-widget/chat-window.tsx index 2ebb08a89..93434b498 100644 --- a/packages/pl-fe/src/features/chats/components/chat-widget/chat-window.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-widget/chat-window.tsx @@ -70,7 +70,7 @@ const ChatWindow = () => { {isOpen && ( - + )} diff --git a/packages/pl-fe/src/features/directory/components/account-card.tsx b/packages/pl-fe/src/features/directory/components/account-card.tsx index f06868295..73ce96767 100644 --- a/packages/pl-fe/src/features/directory/components/account-card.tsx +++ b/packages/pl-fe/src/features/directory/components/account-card.tsx @@ -58,6 +58,7 @@ const AccountCard: React.FC = ({ id }) => { alt={account.avatar_description} className='!absolute bottom-0 left-3 translate-y-1/2 bg-white ring-2 ring-white dark:bg-primary-900 dark:ring-primary-900' size={64} + isCat={account.is_cat} /> diff --git a/packages/pl-fe/src/features/group/group-timeline.tsx b/packages/pl-fe/src/features/group/group-timeline.tsx index 34efe6d63..2d4314a91 100644 --- a/packages/pl-fe/src/features/group/group-timeline.tsx +++ b/packages/pl-fe/src/features/group/group-timeline.tsx @@ -77,7 +77,7 @@ const GroupTimeline: React.FC = (props) => { })} > - + void }) => {
{account && ( - + )} {isSubmitting && ( diff --git a/packages/pl-fe/src/features/onboarding/steps/cover-photo-selection-step.tsx b/packages/pl-fe/src/features/onboarding/steps/cover-photo-selection-step.tsx index 55232eca9..422660b4a 100644 --- a/packages/pl-fe/src/features/onboarding/steps/cover-photo-selection-step.tsx +++ b/packages/pl-fe/src/features/onboarding/steps/cover-photo-selection-step.tsx @@ -118,6 +118,7 @@ const CoverPhotoSelectionStep = ({ onNext }: { onNext: () => void }) => { diff --git a/packages/pl-fe/src/features/ui/components/panels/user-panel.tsx b/packages/pl-fe/src/features/ui/components/panels/user-panel.tsx index bc5cb8e5c..9aad816cc 100644 --- a/packages/pl-fe/src/features/ui/components/panels/user-panel.tsx +++ b/packages/pl-fe/src/features/ui/components/panels/user-panel.tsx @@ -53,8 +53,9 @@ const UserPanel: React.FC = ({ accountId, action, badges, domain }) diff --git a/packages/pl-fe/src/layouts/home-layout.tsx b/packages/pl-fe/src/layouts/home-layout.tsx index b213a1819..37f26690a 100644 --- a/packages/pl-fe/src/layouts/home-layout.tsx +++ b/packages/pl-fe/src/layouts/home-layout.tsx @@ -70,7 +70,7 @@ const HomeLayout: React.FC = ({ children }) => { - +
diff --git a/packages/pl-fe/src/styles/ui.scss b/packages/pl-fe/src/styles/ui.scss index 4ae5e1263..9b5242a6f 100644 --- a/packages/pl-fe/src/styles/ui.scss +++ b/packages/pl-fe/src/styles/ui.scss @@ -42,4 +42,61 @@ .underline-line-through { text-decoration: underline line-through; -} \ No newline at end of file +} + +// Adapted from Iceshrimp which I guess took it from Misskey +// https://iceshrimp.dev/iceshrimp/iceshrimp/src/branch/dev/packages/client/src/components/global/MkAvatar.vue +.avatar__cat { + img { + position: absolute; + inset: 0; + z-index: 1; + } + + &::before, + &::after { + background: #ebbcba; + border: solid 0.05em currentcolor; + box-sizing: border-box; + content: ""; + display: inline-block; + height: 50%; + width: 50%; + } + + &::before { + border-radius: 0 75% 75%; + transform: rotate(37.5deg) skew(30deg); + } + + &::after { + border-radius: 75% 0 75% 75%; + transform: rotate(-37.5deg) skew(-30deg); + } + + &:hover { + &::before { + animation: earwiggleleft 1s infinite; + } + + &::after { + animation: earwiggleright 1s infinite; + } + } +} + +@keyframes earwiggleleft { + 0% { transform: rotate(37.6deg) skew(30deg); } + 25% { transform: rotate(10deg) skew(30deg); } + 50% { transform: rotate(20deg) skew(30deg); } + 75% { transform: rotate(0deg) skew(30deg); } + 100% { transform: rotate(37.6deg) skew(30deg); } +} + +@keyframes earwiggleright { + 0% { transform: rotate(-37.6deg) skew(-30deg); } + 30% { transform: rotate(-10deg) skew(-30deg); } + 55% { transform: rotate(-20deg) skew(-30deg); } + 75% { transform: rotate(0deg) skew(-30deg); } + 100% { transform: rotate(-37.6deg) skew(-30deg); } +} diff --git a/packages/pl-fe/yarn.lock b/packages/pl-fe/yarn.lock index 43151bc6c..f642b4ba6 100644 --- a/packages/pl-fe/yarn.lock +++ b/packages/pl-fe/yarn.lock @@ -4787,6 +4787,11 @@ fake-indexeddb@^6.0.0: resolved "https://registry.yarnpkg.com/fake-indexeddb/-/fake-indexeddb-6.0.0.tgz#3173d5ad141436dace95f8de6e9ecdc3d9787d5d" integrity sha512-YEboHE5VfopUclOck7LncgIqskAqnv4q0EWbYCaxKKjAvO93c+TJIaBuGy8CBFdbg9nKdpN3AuPRwVBJ4k7NrQ== +fast-average-color@^9.5.0: + version "9.5.0" + resolved "https://registry.yarnpkg.com/fast-average-color/-/fast-average-color-9.5.0.tgz#deade6bd998c0ea399a519ed1e2cd67ef673f112" + integrity sha512-nC6x2YIlJ9xxgkMFMd1BNoM1ctMjNoRKfRliPmiEWW3S6rLTHiQcy9g3pt/xiKv/D0NAAkhb9VyV+WJFvTqMGg== + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"