From c43178fc09232d2a4d974f3cbf0aca7ad1b7de75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sun, 1 Sep 2024 14:57:41 +0200 Subject: [PATCH] Display scrobbles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- packages/pl-api/lib/client.ts | 31 +++++++- packages/pl-api/lib/entities/index.ts | 1 + packages/pl-api/lib/entities/scrobble.ts | 21 ++++++ packages/pl-api/lib/features.ts | 7 ++ packages/pl-api/lib/params/accounts.ts | 17 +++++ packages/pl-api/package.json | 2 +- .../src/api/hooks/accounts/useAccount.ts | 18 +++-- .../api/hooks/accounts/useAccountLookup.ts | 12 +++- .../api/hooks/accounts/useAccountScrobble.ts | 27 +++++++ .../hooks/announcements/useAnnouncements.ts | 2 +- packages/pl-fe/src/api/hooks/index.ts | 1 + .../src/components/profile-hover-card.tsx | 7 +- packages/pl-fe/src/components/scrobble.tsx | 70 +++++++++++++++++++ packages/pl-fe/src/entity-store/entities.ts | 4 +- packages/pl-fe/src/features/circle/index.tsx | 4 +- .../ui/components/profile-info-panel.tsx | 6 +- packages/pl-fe/src/layouts/profile-layout.tsx | 2 +- packages/pl-fe/src/locales/en.json | 6 +- packages/pl-fe/src/locales/pl.json | 8 ++- packages/pl-fe/tailwind.config.ts | 6 ++ 20 files changed, 230 insertions(+), 22 deletions(-) create mode 100644 packages/pl-api/lib/entities/scrobble.ts create mode 100644 packages/pl-fe/src/api/hooks/accounts/useAccountScrobble.ts create mode 100644 packages/pl-fe/src/components/scrobble.tsx diff --git a/packages/pl-api/lib/client.ts b/packages/pl-api/lib/client.ts index 3e5989778..ac6a7c65a 100644 --- a/packages/pl-api/lib/client.ts +++ b/packages/pl-api/lib/client.ts @@ -57,6 +57,7 @@ import { reportSchema, ruleSchema, scheduledStatusSchema, + scrobbleSchema, searchSchema, statusEditSchema, statusSchema, @@ -93,6 +94,7 @@ import type { Notification, PleromaConfig, ScheduledStatus, + Scrobble, Status, StreamingEvent, Tag, @@ -135,6 +137,7 @@ import type { CreateGroupParams, CreateListParams, CreatePushNotificationsSubscriptionParams, + CreateScrobbleParams, CreateStatusParams, EditEventParams, EditStatusParams, @@ -170,6 +173,7 @@ import type { GetRebloggedByParams, GetRelationshipsParams, GetScheduledStatusesParams, + GetScrobblesParams, GetStatusContextParams, GetStatusesParams, GetStatusParams, @@ -662,11 +666,34 @@ class PlApiClient { * Requires features{@link Features['bites']}. * @see {@link https://github.com/purifetchi/Toki/blob/master/Toki/Controllers/MastodonApi/Bite/BiteController.cs} */ - biteAccount: async (id: string) => { - const response = await this.request('/api/v1/bite', { method: 'POST', params: { id } }); + biteAccount: async (accountId: string) => { + const response = await this.request('/api/v1/bite', { method: 'POST', params: { id: accountId } }); return response.json as {}; }, + + /** + * Requests a list of current and recent Listen activities for an account + * + * Requires features{@link Features['scrobbles']} + * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#get-apiv1pleromaaccountsidscrobbles} + */ + getScrobbles: async (accountId: string, params?: GetScrobblesParams) => + this.#paginatedGet(`/api/v1/pleroma/accounts/${accountId}/scrobbles`, { params }, scrobbleSchema), + + /** + * Creates a new Listen activity for an account + * + * Requires features{@link Features['scrobbles']} + * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#post-apiv1pleromascrobble} + */ + createScrobble: async (params: CreateScrobbleParams) => { + if (params.external_link) (params as any).externalLink = params.external_link; + + const response = await this.request('/api/v1/pleroma/scrobble', { body: params }); + + return scrobbleSchema.parse(response.json); + }, }; public readonly myAccount = { diff --git a/packages/pl-api/lib/entities/index.ts b/packages/pl-api/lib/entities/index.ts index 6fd08cbf9..1411ead7f 100644 --- a/packages/pl-api/lib/entities/index.ts +++ b/packages/pl-api/lib/entities/index.ts @@ -61,6 +61,7 @@ export * from './report'; export * from './role'; export * from './rule'; export * from './scheduled-status'; +export * from './scrobble'; export * from './search'; export * from './status'; export * from './status-edit'; diff --git a/packages/pl-api/lib/entities/scrobble.ts b/packages/pl-api/lib/entities/scrobble.ts new file mode 100644 index 000000000..92f926bfb --- /dev/null +++ b/packages/pl-api/lib/entities/scrobble.ts @@ -0,0 +1,21 @@ +import { z } from 'zod'; + +import { accountSchema } from './account'; + +const scrobbleSchema = z.preprocess((scrobble: any) => scrobble ? { + external_link: scrobble.externalLink, + ...scrobble, +} : null, z.object({ + id: z.coerce.string(), + account: accountSchema, + created_at: z.string().datetime({ offset: true }), + title: z.string(), + artist: z.string().catch(''), + album: z.string().catch(''), + external_link: z.string().nullable().catch(null), + length: z.number().nullable().catch(null), +})); + +type Scrobble = z.infer; + +export { scrobbleSchema, type Scrobble }; diff --git a/packages/pl-api/lib/features.ts b/packages/pl-api/lib/features.ts index db4df277e..070611728 100644 --- a/packages/pl-api/lib/features.ts +++ b/packages/pl-api/lib/features.ts @@ -987,6 +987,13 @@ const getFeatures = (instance?: Instance) => { v.software === GOTOSOCIAL, ]), + /** + * Can create Listen activities + * @see GET /api/v1/pleroma/accounts/:id/scrobbles + * @see POST /api/v1/pleroma/scrobble + */ + scrobbles: v.software === PLEROMA && v.build !== AKKOMA, + /** * Ability to search statuses from the given account. * @see {@link https://docs.joinmastodon.org/methods/search/} diff --git a/packages/pl-api/lib/params/accounts.ts b/packages/pl-api/lib/params/accounts.ts index e7e412482..2caba3203 100644 --- a/packages/pl-api/lib/params/accounts.ts +++ b/packages/pl-api/lib/params/accounts.ts @@ -53,6 +53,21 @@ interface ReportAccountParams { type GetAccountEndorsementsParams = WithRelationshipsParam; type GetAccountFavouritesParams = PaginationParams; +type GetScrobblesParams = PaginationParams; + +interface CreateScrobbleParams { + /** the title of the media playing */ + title: string; + /** the album of the media playing */ + album?: string; + /** the artist of the media playing */ + artist?: string; + /** the length of the media playing */ + length?: string; + /** A URL referencing the media playing */ + external_link?: string; +} + export type { GetAccountParams, GetAccountStatusesParams, @@ -64,4 +79,6 @@ export type { ReportAccountParams, GetAccountEndorsementsParams, GetAccountFavouritesParams, + GetScrobblesParams, + CreateScrobbleParams, }; diff --git a/packages/pl-api/package.json b/packages/pl-api/package.json index ed454ec89..4375fa3c5 100644 --- a/packages/pl-api/package.json +++ b/packages/pl-api/package.json @@ -1,6 +1,6 @@ { "name": "pl-api", - "version": "0.0.25", + "version": "0.0.26", "type": "module", "homepage": "https://github.com/mkljczk/pl-fe/tree/fork/packages/pl-api", "repository": { diff --git a/packages/pl-fe/src/api/hooks/accounts/useAccount.ts b/packages/pl-fe/src/api/hooks/accounts/useAccount.ts index f42a4aa1d..179709474 100644 --- a/packages/pl-fe/src/api/hooks/accounts/useAccount.ts +++ b/packages/pl-fe/src/api/hooks/accounts/useAccount.ts @@ -1,4 +1,4 @@ -import { type Account as BaseAccount, accountSchema } from 'pl-api'; +import { type Account as BaseAccount } from 'pl-api'; import { useEffect, useMemo } from 'react'; import { useHistory } from 'react-router-dom'; @@ -7,10 +7,12 @@ import { useEntity } from 'pl-fe/entity-store/hooks'; import { useAppSelector, useClient, useFeatures, useLoggedIn } from 'pl-fe/hooks'; import { type Account, normalizeAccount } from 'pl-fe/normalizers'; +import { useAccountScrobble } from './useAccountScrobble'; import { useRelationship } from './useRelationship'; interface UseAccountOpts { withRelationship?: boolean; + withScrobble?: boolean; } const useAccount = (accountId?: string, opts: UseAccountOpts = {}) => { @@ -18,12 +20,12 @@ const useAccount = (accountId?: string, opts: UseAccountOpts = {}) => { const history = useHistory(); const features = useFeatures(); const { me } = useLoggedIn(); - const { withRelationship } = opts; + const { withRelationship, withScrobble } = opts; const { entity, isUnauthorized, ...result } = useEntity( [Entities.ACCOUNTS, accountId!], () => client.accounts.getAccount(accountId!), - { schema: accountSchema, enabled: !!accountId, transform: normalizeAccount }, + { enabled: !!accountId, transform: normalizeAccount }, ); const meta = useAppSelector((state) => accountId && state.accounts_meta[accountId] || {}); @@ -33,12 +35,17 @@ const useAccount = (accountId?: string, opts: UseAccountOpts = {}) => { isLoading: isRelationshipLoading, } = useRelationship(accountId, { enabled: withRelationship }); + const { + scrobble, + isLoading: isScrobbleLoading, + } = useAccountScrobble(accountId, { enabled: withScrobble }); + const isBlocked = entity?.relationship?.blocked_by === true; const isUnavailable = (me === entity?.id) ? false : (isBlocked && !features.blockersVisible); const account = useMemo( - () => entity ? { ...entity, relationship, __meta: { meta, ...entity.__meta } } : undefined, - [entity, relationship], + () => entity ? { ...entity, relationship, scrobble, __meta: { meta, ...entity.__meta } } : undefined, + [entity, relationship, scrobble], ); useEffect(() => { @@ -51,6 +58,7 @@ const useAccount = (accountId?: string, opts: UseAccountOpts = {}) => { ...result, isLoading: result.isLoading, isRelationshipLoading, + isScrobbleLoading, isUnauthorized, isUnavailable, account, diff --git a/packages/pl-fe/src/api/hooks/accounts/useAccountLookup.ts b/packages/pl-fe/src/api/hooks/accounts/useAccountLookup.ts index 13905bf4d..e59353fc2 100644 --- a/packages/pl-fe/src/api/hooks/accounts/useAccountLookup.ts +++ b/packages/pl-fe/src/api/hooks/accounts/useAccountLookup.ts @@ -7,10 +7,12 @@ import { useEntityLookup } from 'pl-fe/entity-store/hooks'; import { useClient, useFeatures, useLoggedIn } from 'pl-fe/hooks'; import { type Account, normalizeAccount } from 'pl-fe/normalizers'; +import { useAccountScrobble } from './useAccountScrobble'; import { useRelationship } from './useRelationship'; interface UseAccountLookupOpts { withRelationship?: boolean; + withScrobble?: boolean; } const useAccountLookup = (acct: string | undefined, opts: UseAccountLookupOpts = {}) => { @@ -18,7 +20,7 @@ const useAccountLookup = (acct: string | undefined, opts: UseAccountLookupOpts = const features = useFeatures(); const history = useHistory(); const { me } = useLoggedIn(); - const { withRelationship } = opts; + const { withRelationship, withScrobble } = opts; const { entity: account, isUnauthorized, ...result } = useEntityLookup( Entities.ACCOUNTS, @@ -32,6 +34,11 @@ const useAccountLookup = (acct: string | undefined, opts: UseAccountLookupOpts = isLoading: isRelationshipLoading, } = useRelationship(account?.id, { enabled: withRelationship }); + const { + scrobble, + isLoading: isScrobbleLoading, + } = useAccountScrobble(account?.id, { enabled: withScrobble }); + const isBlocked = account?.relationship?.blocked_by === true; const isUnavailable = (me === account?.id) ? false : (isBlocked && !features.blockersVisible); @@ -45,9 +52,10 @@ const useAccountLookup = (acct: string | undefined, opts: UseAccountLookupOpts = ...result, isLoading: result.isLoading, isRelationshipLoading, + isScrobbleLoading, isUnauthorized, isUnavailable, - account: account ? { ...account, relationship } : undefined, + account: account ? { ...account, relationship, scrobble } : undefined, }; }; diff --git a/packages/pl-fe/src/api/hooks/accounts/useAccountScrobble.ts b/packages/pl-fe/src/api/hooks/accounts/useAccountScrobble.ts new file mode 100644 index 000000000..2d17cfdfe --- /dev/null +++ b/packages/pl-fe/src/api/hooks/accounts/useAccountScrobble.ts @@ -0,0 +1,27 @@ +import { useQuery } from '@tanstack/react-query'; + +import { useClient, useFeatures } from 'pl-fe/hooks'; + +import type { Scrobble } from 'pl-api'; + +interface UseScrobblesOpts { + enabled?: boolean; +} + +const useAccountScrobble = (accountId?: string, opts: UseScrobblesOpts = {}) => { + const client = useClient(); + const features = useFeatures(); + const { enabled = false } = opts; + + const { data: scrobble, ...result } = useQuery({ + queryKey: ['scrobbles', accountId!], + queryFn: async () => (await client.accounts.getScrobbles(accountId!, { limit: 1 })).items[0], + placeholderData: undefined, + enabled: enabled && !!accountId && features.scrobbles, + staleTime: 3 * 60 * 1000, + }); + + return { scrobble, ...result }; +}; + +export { useAccountScrobble }; diff --git a/packages/pl-fe/src/api/hooks/announcements/useAnnouncements.ts b/packages/pl-fe/src/api/hooks/announcements/useAnnouncements.ts index 6edb5268b..44fda104b 100644 --- a/packages/pl-fe/src/api/hooks/announcements/useAnnouncements.ts +++ b/packages/pl-fe/src/api/hooks/announcements/useAnnouncements.ts @@ -92,6 +92,6 @@ const useAnnouncements = () => { }; const compareAnnouncements = (a: Announcement, b: Announcement): number => - new Date(a.starts_at || a.published_at).getDate() - new Date(b.starts_at || b.published_at).getDate(); + new Date(a.starts_at || a.published_at).getTime() - new Date(b.starts_at || b.published_at).getTime(); export { updateReactions, useAnnouncements }; diff --git a/packages/pl-fe/src/api/hooks/index.ts b/packages/pl-fe/src/api/hooks/index.ts index ba23f625e..0a22c9e7c 100644 --- a/packages/pl-fe/src/api/hooks/index.ts +++ b/packages/pl-fe/src/api/hooks/index.ts @@ -2,6 +2,7 @@ // Accounts export { useAccount } from './accounts/useAccount'; export { useAccountLookup } from './accounts/useAccountLookup'; +export { useAccountScrobble } from './accounts/useAccountScrobble'; export { useBlocks, useMutes, diff --git a/packages/pl-fe/src/components/profile-hover-card.tsx b/packages/pl-fe/src/components/profile-hover-card.tsx index ed852d555..33a266691 100644 --- a/packages/pl-fe/src/components/profile-hover-card.tsx +++ b/packages/pl-fe/src/components/profile-hover-card.tsx @@ -14,6 +14,7 @@ import { useAppSelector, useAppDispatch } from 'pl-fe/hooks'; import { showProfileHoverCard } from './hover-ref-wrapper'; import { dateFormatOptions } from './relative-timestamp'; +import Scrobble from './scrobble'; import { Card, CardBody, HStack, Icon, Stack, Text } from './ui'; import type { Account } from 'pl-fe/normalizers'; @@ -55,7 +56,7 @@ const ProfileHoverCard: React.FC = ({ visible = true }) => { const me = useAppSelector(state => state.me); const accountId: string | undefined = useAppSelector(state => state.profile_hover_card.accountId || undefined); - const { account } = useAccount(accountId, { withRelationship: true }); + const { account } = useAccount(accountId, { withRelationship: true, withScrobble: true }); const targetRef = useAppSelector(state => state.profile_hover_card.ref?.current); const badges = getBadges(account); @@ -120,6 +121,10 @@ const ProfileHoverCard: React.FC = ({ visible = true }) => { ) : null} + {!!account.scrobble && ( + + )} + {account.note.length > 0 && ( = ({ scrobble }) => { + const textRef = useRef(null); + + const isRecent = (new Date().getTime() - new Date(scrobble.created_at).getTime()) <= 60 * 60 * 1000; + + const song = scrobble.artist ? ( + + ) : scrobble.title; + + const animate = useMemo( + () => textRef.current && textRef.current.parentElement && textRef.current.clientWidth > textRef.current.parentElement.clientWidth, + [textRef.current], + ); + + if (!isRecent) return null; + + return ( + + + +
+ + e.stopPropagation()} + rel='nofollow noopener' + target='_blank' + > + {song} + + ) : song, + }} + /> + +
+
+ ); +}; + +export { Scrobble as default }; diff --git a/packages/pl-fe/src/entity-store/entities.ts b/packages/pl-fe/src/entity-store/entities.ts index 6706540e1..3c25874e3 100644 --- a/packages/pl-fe/src/entity-store/entities.ts +++ b/packages/pl-fe/src/entity-store/entities.ts @@ -1,4 +1,4 @@ -import type { AdminDomain, AdminRelay, AdminRule, BookmarkFolder, GroupMember, GroupRelationship, Relationship, TrendsLink } from 'pl-api'; +import type { AdminDomain, AdminRelay, AdminRule, BookmarkFolder, GroupMember, GroupRelationship, Relationship, Scrobble, TrendsLink } from 'pl-api'; import type { Account, Group, Status } from 'pl-fe/normalizers'; enum Entities { @@ -12,6 +12,7 @@ enum Entities { RELATIONSHIPS = 'Relationships', RELAYS = 'Relays', RULES = 'Rules', + SCROBBLES = 'Scrobbles', STATUSES = 'Statuses', TRENDS_LINKS = 'TrendsLinks', } @@ -26,6 +27,7 @@ interface EntityTypes { [Entities.RELATIONSHIPS]: Relationship; [Entities.RELAYS]: AdminRelay; [Entities.RULES]: AdminRule; + [Entities.SCROBBLES]: Scrobble; [Entities.STATUSES]: Status; [Entities.TRENDS_LINKS]: TrendsLink; } diff --git a/packages/pl-fe/src/features/circle/index.tsx b/packages/pl-fe/src/features/circle/index.tsx index f53f5832a..402f1c5f1 100644 --- a/packages/pl-fe/src/features/circle/index.tsx +++ b/packages/pl-fe/src/features/circle/index.tsx @@ -135,7 +135,7 @@ const Circle: React.FC = () => {
- +
@@ -144,7 +144,7 @@ const Circle: React.FC = () => { diff --git a/packages/pl-fe/src/features/ui/components/profile-info-panel.tsx b/packages/pl-fe/src/features/ui/components/profile-info-panel.tsx index 73be0a355..55654349d 100644 --- a/packages/pl-fe/src/features/ui/components/profile-info-panel.tsx +++ b/packages/pl-fe/src/features/ui/components/profile-info-panel.tsx @@ -4,6 +4,7 @@ import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; import Badge from 'pl-fe/components/badge'; import Markup from 'pl-fe/components/markup'; import { dateFormatOptions } from 'pl-fe/components/relative-timestamp'; +import Scrobble from 'pl-fe/components/scrobble'; import { Icon, HStack, Stack, Text } from 'pl-fe/components/ui'; import { useAppSelector, usePlFeConfig } from 'pl-fe/hooks'; import { capitalize } from 'pl-fe/utils/strings'; @@ -12,6 +13,7 @@ import ProfileFamiliarFollowers from './profile-familiar-followers'; import ProfileField from './profile-field'; import ProfileStats from './profile-stats'; +import type { Scrobble as ScrobbleEntity } from 'pl-api'; import type { Account } from 'pl-fe/normalizers'; const messages = defineMessages({ @@ -22,7 +24,7 @@ const messages = defineMessages({ }); interface IProfileInfoPanel { - account?: Account; + account?: Account & { scrobble?: ScrobbleEntity }; /** Username from URL params, in case the account isn't found. */ username: string; } @@ -191,6 +193,8 @@ const ProfileInfoPanel: React.FC = ({ account, username }) => {renderBirthday()}
+ {account.scrobble && } + {ownAccount ? null : } diff --git a/packages/pl-fe/src/layouts/profile-layout.tsx b/packages/pl-fe/src/layouts/profile-layout.tsx index 8574fdef6..46500b701 100644 --- a/packages/pl-fe/src/layouts/profile-layout.tsx +++ b/packages/pl-fe/src/layouts/profile-layout.tsx @@ -31,7 +31,7 @@ const ProfileLayout: React.FC = ({ params, children }) => { const history = useHistory(); const username = params?.username || ''; - const { account } = useAccountLookup(username, { withRelationship: true }); + const { account } = useAccountLookup(username, { withRelationship: true, withScrobble: true }); const me = useAppSelector(state => state.me); const features = useFeatures(); diff --git a/packages/pl-fe/src/locales/en.json b/packages/pl-fe/src/locales/en.json index acfebd950..04c81a22e 100644 --- a/packages/pl-fe/src/locales/en.json +++ b/packages/pl-fe/src/locales/en.json @@ -53,6 +53,8 @@ "account.requested": "Awaiting approval. Click to cancel follow request", "account.requested_small": "Awaiting approval", "account.rss_feed": "Subscribe to RSS feed", + "account.scrobbling": "Playing {song}", + "account.scrobbling.title": "{title} by {artist}", "account.search": "Search from @{name}", "account.search_self": "Search your posts", "account.share": "Share @{name}'s profile", @@ -894,8 +896,6 @@ "input.copy": "Copy", "input.password.hide_password": "Hide password", "input.password.show_password": "Show password", - "interaction_circle.confirmation_heading": "Do you want to generate an interaction circle for the user @{username}?", - "interaction_circle.start": "Generate", "interaction_policies.entry.followers": "Followers", "interaction_policies.entry.following": "People I follow", "interaction_policies.entry.mentioned": "Mentioned", @@ -919,7 +919,9 @@ "interaction_policies.title.unlisted.can_reply": "Who can reply to an unlisted post?", "interaction_policies.update": "Update", "interactions_circle.compose": "Share", + "interactions_circle.confirmation_heading": "Do you want to generate an interaction circle for the user @{username}?", "interactions_circle.download": "Download", + "interactions_circle.start": "Generate", "interactions_circle.state.done": "Finalizing…", "interactions_circle.state.drawing": "Drawing circle", "interactions_circle.state.fetching_avatars": "Fetching avatars", diff --git a/packages/pl-fe/src/locales/pl.json b/packages/pl-fe/src/locales/pl.json index 02775117d..f69cf031c 100644 --- a/packages/pl-fe/src/locales/pl.json +++ b/packages/pl-fe/src/locales/pl.json @@ -53,6 +53,8 @@ "account.requested": "Oczekująca prośba, kliknij aby anulować", "account.requested_small": "Oczekująca prośba", "account.rss_feed": "Subskrybuj kanał RSS", + "account.scrobbling": "Słucha {song}", + "account.scrobbling.title": "{title} — {artist}", "account.search": "Szukaj wpisów @{name}", "account.search_self": "Szukaj własnych wpisów", "account.share": "Udostępnij profil @{name}", @@ -483,8 +485,6 @@ "compose_form.publish_loud": "{publish}!", "compose_form.save_changes": "Zapisz zmiany", "compose_form.schedule": "Zaplanuj", - "compose_form.scheduled_statuses.click_here": "Naciśnij tutaj", - "compose_form.scheduled_statuses.message": "Masz zaplanowane wpisy. {click_here}, aby je zobaczyć.", "compose_form.spoiler.marked": "Media są oznaczone jako wrażliwe", "compose_form.spoiler.unmarked": "Media nie są oznaczone jako wrażliwe", "compose_form.spoiler_placeholder": "Temat (nieobowiązkowy)", @@ -920,6 +920,8 @@ "interaction_policies.update": "Aktualizuj", "interactions_circle.compose": "Udostępnij", "interactions_circle.download": "Pobierz", + "interactions_circle.start": "Wygeneruj", + "interaction_circle.confirmation_heading": "Czy chcesz wygenerować koło interakcji dla użytkownika @{username}?", "interactions_circle.state.done": "Finalizowanie…", "interactions_circle.state.drawing": "Rysowanie koła…", "interactions_circle.state.fetching_avatars": "Pobieranie awatarów…", @@ -1071,6 +1073,7 @@ "navigation.home": "Główna", "navigation.notifications": "Powiadomienia", "navigation.search": "Szukaj", + "navigation.sidebar": "Otwórz menu boczne", "navigation.source_code": "Kod źródłowy", "navigation_bar.account_aliases": "Aliasy konta", "navigation_bar.blocks": "Zablokowani użytkownicy", @@ -1427,7 +1430,6 @@ "plfe_config.sentry_dsn_label": "DSN Sentry", "plfe_config.tile_server_attribution_label": "Atrybucja kafelków map", "plfe_config.tile_server_label": "Serwer kafelków map", - "plfe_config.verified_can_edit_name_label": "Pozwól zweryfikowanym użytkownikom na zmianę swojej nazwy wyświetlanej.", "status.add_known_language": "Nie tłumacz automatycznie wpisów w języku {language}.", "status.admin_account": "Moderuj @{name}", "status.admin_status": "Otwórz ten wpis w interfejsie moderacyjnym", diff --git a/packages/pl-fe/tailwind.config.ts b/packages/pl-fe/tailwind.config.ts index e63df0573..fb5b3a36b 100644 --- a/packages/pl-fe/tailwind.config.ts +++ b/packages/pl-fe/tailwind.config.ts @@ -77,6 +77,7 @@ const config: Config = { 'sonar-scale-1': 'sonar-scale-1 3s 1.5s linear infinite', 'enter': 'enter 200ms ease-out', 'leave': 'leave 150ms ease-in forwards', + 'text-overflow': 'text-overflow 8s linear infinite', }, keyframes: { 'sonar-scale-4': { @@ -103,6 +104,11 @@ const config: Config = { from: { transform: 'scale(1)', opacity: '1' }, to: { transform: 'scale(0.9)', opacity: '0' }, }, + // https://stackoverflow.com/posts/78825869/revisions + 'text-overflow': { + '10%, 90%': { transform: 'translate(0, 0)', left: '0%' }, + '40%, 60%': { transform: 'translate(-100%, 0)', left: '100%' }, + }, }, }, },