From 1deb2f9ba152ee453e94e8ca935be3ed3d4f157d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 4 Mar 2026 23:52:48 +0100 Subject: [PATCH 01/89] nicolium: move new timelines state to zustand MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/columns/timeline.tsx | 13 +- packages/nicolium/src/queries/keys.ts | 52 ----- .../src/queries/timelines/use-timeline.ts | 178 ++++-------------- .../src/queries/timelines/use-timelines.ts | 20 +- packages/nicolium/src/stores/timelines.ts | 157 +++++++++++++++ 5 files changed, 205 insertions(+), 215 deletions(-) create mode 100644 packages/nicolium/src/stores/timelines.ts diff --git a/packages/nicolium/src/columns/timeline.tsx b/packages/nicolium/src/columns/timeline.tsx index 2dfcd6c3a..df13c14e6 100644 --- a/packages/nicolium/src/columns/timeline.tsx +++ b/packages/nicolium/src/columns/timeline.tsx @@ -20,7 +20,7 @@ import { } from '@/queries/timelines/use-timelines'; import type { FilterContextType } from '@/queries/settings/use-filters'; -import type { TimelineEntry } from '@/queries/timelines/use-timeline'; +import type { TimelineEntry } from '@/stores/timelines'; interface ITimelineStatus { id: string; @@ -86,7 +86,7 @@ interface ITimeline { } const Timeline: React.FC = ({ query, contextType = 'public' }) => { - const { data, handleLoadMore, isLoading } = query; + const { entries, fetchNextPage, isFetching, isPending } = query; const renderEntry = (entry: TimelineEntry) => { if (entry.type === 'status') { @@ -102,14 +102,13 @@ const Timeline: React.FC = ({ query, contextType = 'public' }) => { // contextType={timelineId} // showGroup={showGroup} // variant={divideType === 'border' ? 'slim' : 'rounded'} - // fromBookmarks={other.scrollKey === 'bookmarked_statuses'} /> ); } if (entry.type === 'page-end' || entry.type === 'page-start') { return (
- handleLoadMore(entry)} disabled={isLoading} /> +
); } @@ -119,8 +118,8 @@ const Timeline: React.FC = ({ query, contextType = 'public' }) => { } placeholderCount={20} // className={className} @@ -132,7 +131,7 @@ const Timeline: React.FC = ({ query, contextType = 'public' }) => { // })} // {...other} > - {(data || []).map(renderEntry)} + {(entries || []).map(renderEntry)} ); }; diff --git a/packages/nicolium/src/queries/keys.ts b/packages/nicolium/src/queries/keys.ts index c63aff3a8..b8526882a 100644 --- a/packages/nicolium/src/queries/keys.ts +++ b/packages/nicolium/src/queries/keys.ts @@ -7,7 +7,6 @@ import type { MinifiedInteractionRequest } from './statuses/use-interaction-requ import type { MinifiedContext } from './statuses/use-status'; import type { MinifiedStatusEdit } from './statuses/use-status-history'; import type { MinifiedEmojiReaction } from './statuses/use-status-interactions'; -import type { TimelineEntry } from './timelines/use-timeline'; import type { MinifiedSuggestion } from './trends/use-suggested-accounts'; import type { MinifiedAdminAccount, @@ -34,10 +33,8 @@ import type { AdminRule, Announcement, Antenna, - AntennaTimelineParams, Backup, BookmarkFolder, - BubbleTimelineParams, Chat, Circle, CredentialAccount, @@ -48,29 +45,21 @@ import type { Group, GroupRelationship, GroupRole, - GroupTimelineParams, - HashtagTimelineParams, - HomeTimelineParams, InteractionPolicies, - LinkTimelineParams, List, - ListTimelineParams, Location, Marker, NotificationGroup, OauthToken, PaginatedResponse, - PaginationParams, PlApiClient, Poll, - PublicTimelineParams, Relationship, RssFeed, ScheduledStatus, Tag, Translation, TrendsLink, - WrenchedTimelineParams, } from 'pl-api'; type TaggedKey = DataTag; @@ -429,46 +418,6 @@ const suggestions = { all: ['suggestions'] as TaggedKey<['suggestions'], Array>, }; -const timelines = { - root: ['timelines'] as const, - home: (params?: Omit) => { - const key = ['timelines', 'home', params] as const; - return key as TaggedKey>; - }, - public: (local?: boolean, params?: Omit) => { - const key = ['timelines', 'public', { local, ...params }] as const; - return key as TaggedKey>; - }, - hashtag: (hashtag: string, params?: Omit) => { - const key = ['timelines', 'hashtag', hashtag, params] as const; - return key as TaggedKey>; - }, - link: (url: string, params?: Omit) => { - const key = ['timelines', 'link', url, params] as const; - return key as TaggedKey>; - }, - list: (listId: string, params?: Omit) => { - const key = ['timelines', 'list', listId, params] as const; - return key as TaggedKey>; - }, - group: (groupId: string, params?: Omit) => { - const key = ['timelines', 'group', groupId, params] as const; - return key as TaggedKey>; - }, - bubble: (params?: Omit) => { - const key = ['timelines', 'bubble', params] as const; - return key as TaggedKey>; - }, - antenna: (antennaId: string, params?: Omit) => { - const key = ['timelines', 'antenna', antennaId, params] as const; - return key as TaggedKey>; - }, - wrenched: (params?: Omit) => { - const key = ['timelines', 'wrenched', params] as const; - return key as TaggedKey>; - }, -}; - const timelineIds = { root: ['timelineIds'] as const, accountMedia: (accountId: string) => { @@ -691,7 +640,6 @@ const queryKeys = { search, trends, suggestions, - timelines, timelineIds, settings, interactionPolicies, diff --git a/packages/nicolium/src/queries/timelines/use-timeline.ts b/packages/nicolium/src/queries/timelines/use-timeline.ts index 69a08cdb6..8854a7544 100644 --- a/packages/nicolium/src/queries/timelines/use-timeline.ts +++ b/packages/nicolium/src/queries/timelines/use-timeline.ts @@ -1,103 +1,11 @@ -import { useQuery, useQueryClient } from '@tanstack/react-query'; -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; import { importEntities } from '@/actions/importer'; import { useTimelineStream } from '@/api/hooks/streaming/use-timeline-stream'; +import { useTimeline as useStoreTimeline, useTimelinesActions } from '@/stores/timelines'; -import type { DataTag } from '@tanstack/react-query'; import type { PaginatedResponse, Status, StreamingParams } from 'pl-api'; -type TimelineEntry = - | { - type: 'status'; - id: string; - rebloggedBy: Array; - isConnectedTop?: boolean; - isConnectedBottom?: boolean; - } - | { - type: 'pending-status'; - id: string; - } - | { - type: 'gap'; - } - | { - type: 'page-start'; - maxId?: string; - } - | { - type: 'page-end'; - minId?: string; - }; - -const processPage = ({ - items: statuses, - next, -}: PaginatedResponse): Array => { - const timelinePage: Array = []; - - const processStatus = (status: Status): boolean => { - if (timelinePage.some((entry) => entry.type === 'status' && entry.id === status.id)) - return false; - - let isConnectedTop = false; - const inReplyToId = (status.reblog || status).in_reply_to_id; - - if (inReplyToId) { - const foundStatus = statuses.find((s) => (s.reblog || s).id === inReplyToId); - - if (foundStatus) { - if (processStatus(foundStatus)) { - const lastEntry = timelinePage.at(-1); - // it's always of type status but doing this to satisfy ts - if (lastEntry?.type === 'status') lastEntry.isConnectedBottom = true; - isConnectedTop = true; - } - } - } - - if (status.reblog) { - const existingEntry = timelinePage.find( - (entry) => entry.type === 'status' && entry.id === status.reblog!.id, - ); - - if (existingEntry?.type === 'status') { - existingEntry.rebloggedBy.push(status.account.id); - } else { - timelinePage.push({ - type: 'status', - id: status.reblog.id, - rebloggedBy: [status.account.id], - isConnectedTop, - }); - } - return true; - } - - timelinePage.push({ - type: 'status', - id: status.id, - rebloggedBy: [], - isConnectedTop, - }); - - return true; - }; - - for (const status of statuses) { - processStatus(status); - } - - if (next) - timelinePage.push({ - type: 'page-end', - minId: statuses.at(-1)?.id, - }); - - return timelinePage; -}; - type PaginationParams = { max_id?: string; min_id?: string }; type TimelineFetcher = (params?: PaginationParams) => Promise>; @@ -106,65 +14,45 @@ interface StreamConfig { params?: StreamingParams; } -type TimelineQueryKey = DataTag>; - -const useTimeline = ( - queryKey: TimelineQueryKey, - fetcher: TimelineFetcher, - streamConfig?: StreamConfig, -) => { - const queryClient = useQueryClient(); +const useTimeline = (timelineId: string, fetcher: TimelineFetcher, streamConfig?: StreamConfig) => { + const timeline = useStoreTimeline(timelineId); + const timelineActions = useTimelinesActions(); useTimelineStream(streamConfig?.stream ?? '', streamConfig?.params, !!streamConfig?.stream); - const query = useQuery({ - queryKey, - queryFn: async () => { - setIsLoading(true); - try { - const response = await fetcher(); - importEntities({ statuses: response.items }); - return processPage(response); - } finally { - setIsLoading(false); - } - }, - }); + useEffect(() => { + if (!timeline.isPending) return; + fetchInitial(); + }, []); - const [isLoading, setIsLoading] = useState(query.isPending); + const fetchInitial = useCallback(async () => { + timelineActions.setLoading(timelineId, true); + try { + const response = await fetcher(); + importEntities({ statuses: response.items }); + timelineActions.expandTimeline(timelineId, response.items, !!response.next, true); + } catch (error) { + // + } + }, [timelineId]); - const handleLoadMore = useCallback( - async (entry: TimelineEntry) => { - if (isLoading) return; - if (entry.type !== 'page-end' && entry.type !== 'page-start') return; + const fetchNextPage = useCallback(async () => { + timelineActions.setLoading(timelineId, true); + const lastEntry = timeline.entries.at(-1); + if (!lastEntry || lastEntry.type !== 'page-end') return; - setIsLoading(true); - try { - const response = await fetcher( - entry.type === 'page-end' ? { max_id: entry.minId } : { min_id: entry.maxId }, - ); + try { + const response = await fetcher({ max_id: lastEntry.minId }); - importEntities({ statuses: response.items }); + importEntities({ statuses: response.items }); - const timelinePage = processPage(response); + timelineActions.expandTimeline(timelineId, response.items, !!response.next, false); + } catch (error) { + // + } + }, [timelineId, timeline.entries]); - queryClient.setQueryData(queryKey, (oldData) => { - if (!oldData) return timelinePage; - const index = oldData.indexOf(entry); - return oldData.toSpliced(index, 1, ...timelinePage); - }); - } catch (error) { - // - } - setIsLoading(false); - }, - [isLoading, fetcher, queryKey, queryClient], - ); - - return useMemo( - () => ({ ...query, handleLoadMore, isLoading }), - [query, handleLoadMore, isLoading], - ); + return useMemo(() => ({ ...timeline, fetchNextPage }), [timeline, fetchNextPage]); }; -export { useTimeline, type TimelineEntry }; +export { useTimeline }; diff --git a/packages/nicolium/src/queries/timelines/use-timelines.ts b/packages/nicolium/src/queries/timelines/use-timelines.ts index d20df8a87..ce28451d5 100644 --- a/packages/nicolium/src/queries/timelines/use-timelines.ts +++ b/packages/nicolium/src/queries/timelines/use-timelines.ts @@ -1,7 +1,5 @@ import { useClient } from '@/hooks/use-client'; -import { queryKeys } from '../keys'; - import { useTimeline } from './use-timeline'; import type { @@ -22,7 +20,7 @@ const useHomeTimeline = (params?: Omit client.timelines.homeTimeline({ ...params, ...paginationParams }), { stream }, ); @@ -33,7 +31,7 @@ const usePublicTimeline = (params?: Omit client.timelines.publicTimeline({ ...params, ...paginationParams }), { stream }, ); @@ -46,7 +44,7 @@ const useHashtagTimeline = ( const client = useClient(); return useTimeline( - queryKeys.timelines.hashtag(hashtag, params), + `hashtag:${hashtag}`, (paginationParams) => client.timelines.hashtagTimeline(hashtag, { ...params, ...paginationParams }), { stream: 'hashtag', params: { tag: hashtag } }, @@ -59,7 +57,7 @@ const useLinkTimeline = ( ) => { const client = useClient(); - return useTimeline(queryKeys.timelines.link(url, params), (paginationParams) => + return useTimeline(`link:${url}`, (paginationParams) => client.timelines.linkTimeline(url, { ...params, ...paginationParams }), ); }; @@ -71,7 +69,7 @@ const useListTimeline = ( const client = useClient(); return useTimeline( - queryKeys.timelines.list(listId, params), + `list:${listId}`, (paginationParams) => client.timelines.listTimeline(listId, { ...params, ...paginationParams }), { stream: 'list', params: { list: listId } }, ); @@ -84,7 +82,7 @@ const useGroupTimeline = ( const client = useClient(); return useTimeline( - queryKeys.timelines.group(groupId, params), + `group:${groupId}`, (paginationParams) => client.timelines.groupTimeline(groupId, { ...params, ...paginationParams }), { stream: 'group', params: { group: groupId } }, @@ -95,7 +93,7 @@ const useBubbleTimeline = (params?: Omit client.timelines.bubbleTimeline({ ...params, ...paginationParams }), { stream: 'bubble' }, ); @@ -107,7 +105,7 @@ const useAntennaTimeline = ( ) => { const client = useClient(); - return useTimeline(queryKeys.timelines.antenna(antennaId, params), (paginationParams) => + return useTimeline(`antenna:${antennaId}`, (paginationParams) => client.timelines.antennaTimeline(antennaId, { ...params, ...paginationParams }), ); }; @@ -115,7 +113,7 @@ const useAntennaTimeline = ( const useWrenchedTimeline = (params?: Omit) => { const client = useClient(); - return useTimeline(queryKeys.timelines.wrenched(params), (paginationParams) => + return useTimeline(`wrenched`, (paginationParams) => client.timelines.wrenchedTimeline({ ...params, ...paginationParams }), ); }; diff --git a/packages/nicolium/src/stores/timelines.ts b/packages/nicolium/src/stores/timelines.ts new file mode 100644 index 000000000..eb06bcecf --- /dev/null +++ b/packages/nicolium/src/stores/timelines.ts @@ -0,0 +1,157 @@ +import { create } from 'zustand'; +import { mutative } from 'zustand-mutative'; + +import type { Status } from 'pl-api'; + +type TimelineEntry = + | { + type: 'status'; + id: string; + rebloggedBy: Array; + isConnectedTop?: boolean; + isConnectedBottom?: boolean; + } + | { + type: 'pending-status'; + id: string; + } + | { + type: 'gap'; + sinceId: string; + maxId: string; + } + | { + type: 'page-start'; + maxId?: string; + } + | { + type: 'page-end'; + minId?: string; + }; + +interface TimelineData { + entries: Array; + isFetching: boolean; + isPending: boolean; +} + +interface State { + timelines: Record; + actions: { + expandTimeline: ( + timelineId: string, + statuses: Array, + hasMore: boolean, + initialFetch: boolean, + ) => void; + setLoading: (timelineId: string, isFetching: boolean) => void; + }; +} + +const processPage = (statuses: Array, hasMore: boolean): Array => { + const timelinePage: Array = []; + + const processStatus = (status: Status): boolean => { + if (timelinePage.some((entry) => entry.type === 'status' && entry.id === status.id)) + return false; + + let isConnectedTop = false; + const inReplyToId = (status.reblog || status).in_reply_to_id; + + if (inReplyToId) { + const foundStatus = statuses.find((s) => (s.reblog || s).id === inReplyToId); + + if (foundStatus) { + if (processStatus(foundStatus)) { + const lastEntry = timelinePage.at(-1); + // it's always of type status but doing this to satisfy ts + if (lastEntry?.type === 'status') lastEntry.isConnectedBottom = true; + isConnectedTop = true; + } + } + } + + if (status.reblog) { + const existingEntry = timelinePage.find( + (entry) => entry.type === 'status' && entry.id === status.reblog!.id, + ); + + if (existingEntry?.type === 'status') { + existingEntry.rebloggedBy.push(status.account.id); + } else { + timelinePage.push({ + type: 'status', + id: status.reblog.id, + rebloggedBy: [status.account.id], + isConnectedTop, + }); + } + return true; + } + + timelinePage.push({ + type: 'status', + id: status.id, + rebloggedBy: [], + isConnectedTop, + }); + + return true; + }; + + for (const status of statuses) { + processStatus(status); + } + + if (hasMore) + timelinePage.push({ + type: 'page-end', + minId: statuses.at(-1)?.id, + }); + + return timelinePage; +}; + +const useTimelinesStore = create()( + mutative((set) => ({ + timelines: {} as Record, + actions: { + expandTimeline: (timelineId, statuses, hasMore, initialFetch) => + set((state) => { + const timeline = state.timelines[timelineId] ?? createEmptyTimeline(); + const entries = processPage(statuses, hasMore); + + if (initialFetch) timeline.entries = []; + else if (timeline.entries.at(-1)?.type === 'page-end') timeline.entries.pop(); + timeline.entries.push(...entries); + timeline.isPending = false; + timeline.isFetching = false; + state.timelines[timelineId] = timeline; + }), + setLoading: (timelineId, isFetching) => + set((state) => { + const timeline = state.timelines[timelineId]; + + if (!timeline) return; + + timeline.isFetching = isFetching; + if (!isFetching) timeline.isPending = false; + }), + }, + })), +); + +const createEmptyTimeline = (): TimelineData => ({ + entries: [], + isFetching: false, + isPending: true, +}); + +const emptyTimeline = createEmptyTimeline(); + +const useTimelinesActions = () => useTimelinesStore((state) => state.actions); + +const useTimeline = (timelineId: string) => + useTimelinesStore((state) => state.timelines[timelineId] ?? emptyTimeline); + +export { useTimelinesStore, useTimelinesActions, useTimeline, type TimelineEntry }; From d4a92abf9daf70473cd09b6e60521b81edbba579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 10:46:29 +0100 Subject: [PATCH 02/89] nicolium: experimental timeline: hotkey navigation, queue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/columns/timeline.tsx | 88 ++++++++++++++----- .../src/features/ui/components/hotkeys.tsx | 2 +- packages/nicolium/src/pages/fun/circle.tsx | 5 +- .../src/queries/timelines/use-timeline.ts | 9 +- packages/nicolium/src/stores/timelines.ts | 15 ++++ packages/pl-api/lib/main.ts | 40 ++++++++- packages/pl-api/package.json | 2 +- 7 files changed, 131 insertions(+), 30 deletions(-) diff --git a/packages/nicolium/src/columns/timeline.tsx b/packages/nicolium/src/columns/timeline.tsx index df13c14e6..edea4b39c 100644 --- a/packages/nicolium/src/columns/timeline.tsx +++ b/packages/nicolium/src/columns/timeline.tsx @@ -1,10 +1,13 @@ import clsx from 'clsx'; -import React from 'react'; +import React, { useRef } from 'react'; +import { defineMessages } from 'react-intl'; import LoadMore from '@/components/load-more'; +import ScrollTopButton from '@/components/scroll-top-button'; import ScrollableList from '@/components/scrollable-list'; import Status from '@/components/statuses/status'; import Tombstone from '@/components/statuses/tombstone'; +import Portal from '@/components/ui/portal'; import PlaceholderStatus from '@/features/placeholder/components/placeholder-status'; import { useStatus } from '@/queries/statuses/use-status'; import { @@ -18,9 +21,22 @@ import { usePublicTimeline, useWrenchedTimeline, } from '@/queries/timelines/use-timelines'; +import { selectChild } from '@/utils/scroll-utils'; import type { FilterContextType } from '@/queries/settings/use-filters'; import type { TimelineEntry } from '@/stores/timelines'; +import type { VirtuosoHandle } from 'react-virtuoso'; + +const messages = defineMessages({ + queue: { + id: 'status_list.queue_label', + defaultMessage: 'Click to see {count} new {count, plural, one {post} other {posts}}', + }, + queueLiveRegion: { + id: 'status_list.queue_label.live_region', + defaultMessage: '{count} new {count, plural, one {post} other {posts}}.', + }, +}); interface ITimelineStatus { id: string; @@ -86,9 +102,24 @@ interface ITimeline { } const Timeline: React.FC = ({ query, contextType = 'public' }) => { - const { entries, fetchNextPage, isFetching, isPending } = query; + const node = useRef(null); - const renderEntry = (entry: TimelineEntry) => { + const { entries, queuedCount, fetchNextPage, dequeueEntries, isFetching, isPending } = query; + + const handleMoveUp = (index: number) => { + selectChild(index - 1, node, document.getElementById('status-list') ?? undefined); + }; + + const handleMoveDown = (index: number) => { + selectChild( + index + 1, + node, + document.getElementById('status-list') ?? undefined, + entries.length, + ); + }; + + const renderEntry = (entry: TimelineEntry, index: number) => { if (entry.type === 'status') { return ( = ({ query, contextType = 'public' }) => { isConnectedTop={entry.isConnectedTop} isConnectedBottom={entry.isConnectedBottom} contextType={contextType} - // onMoveUp={handleMoveUp} - // onMoveDown={handleMoveDown} + onMoveUp={() => handleMoveUp(index)} + onMoveDown={() => handleMoveDown(index)} // contextType={timelineId} // showGroup={showGroup} // variant={divideType === 'border' ? 'slim' : 'rounded'} @@ -115,24 +146,35 @@ const Timeline: React.FC = ({ query, contextType = 'public' }) => { }; return ( - } - placeholderCount={20} - // className={className} - // listClassName={clsx('divide-y divide-solid divide-gray-200 dark:divide-gray-800', { - // 'divide-none': divideType !== 'border', - // })} - // itemClassName={clsx({ - // 'pb-3': divideType !== 'border', - // })} - // {...other} - > - {(entries || []).map(renderEntry)} - + <> + + + + } + placeholderCount={20} + ref={node} + // className={className} + // listClassName={clsx('divide-y divide-solid divide-gray-200 dark:divide-gray-800', { + // 'divide-none': divideType !== 'border', + // })} + // itemClassName={clsx({ + // 'pb-3': divideType !== 'border', + // })} + // {...other} + > + {(entries || []).map(renderEntry)} + + ); }; diff --git a/packages/nicolium/src/features/ui/components/hotkeys.tsx b/packages/nicolium/src/features/ui/components/hotkeys.tsx index 7be8f4720..89fb67241 100644 --- a/packages/nicolium/src/features/ui/components/hotkeys.tsx +++ b/packages/nicolium/src/features/ui/components/hotkeys.tsx @@ -167,7 +167,7 @@ const hotkeyMatcherMap = { type HotkeyName = keyof typeof hotkeyMatcherMap; -type HandlerMap = Partial void>>; +type HandlerMap = Partial void | boolean>>; function useHotkeys(handlers: HandlerMap) { const ref = useRef(null); diff --git a/packages/nicolium/src/pages/fun/circle.tsx b/packages/nicolium/src/pages/fun/circle.tsx index 42e64ae9b..fc3b2f23a 100644 --- a/packages/nicolium/src/pages/fun/circle.tsx +++ b/packages/nicolium/src/pages/fun/circle.tsx @@ -68,9 +68,8 @@ const CirclePage: React.FC = () => { progress: number; }>({ state: 'unrequested', progress: 0 }); const [expanded, setExpanded] = useState(false); - const [users, setUsers] = useState< - Array<{ id: string; avatar?: string; avatar_description?: string; acct: string }> - >(); + const [users, setUsers] = + useState>(); const intl = useIntl(); const dispatch = useAppDispatch(); diff --git a/packages/nicolium/src/queries/timelines/use-timeline.ts b/packages/nicolium/src/queries/timelines/use-timeline.ts index 8854a7544..ddae246ca 100644 --- a/packages/nicolium/src/queries/timelines/use-timeline.ts +++ b/packages/nicolium/src/queries/timelines/use-timeline.ts @@ -52,7 +52,14 @@ const useTimeline = (timelineId: string, fetcher: TimelineFetcher, streamConfig? } }, [timelineId, timeline.entries]); - return useMemo(() => ({ ...timeline, fetchNextPage }), [timeline, fetchNextPage]); + const dequeueEntries = useCallback(() => { + timelineActions.dequeueEntries(timelineId); + }, [timelineId]); + + return useMemo( + () => ({ ...timeline, fetchNextPage, dequeueEntries }), + [timeline, fetchNextPage, dequeueEntries], + ); }; export { useTimeline }; diff --git a/packages/nicolium/src/stores/timelines.ts b/packages/nicolium/src/stores/timelines.ts index eb06bcecf..f18135bcb 100644 --- a/packages/nicolium/src/stores/timelines.ts +++ b/packages/nicolium/src/stores/timelines.ts @@ -31,6 +31,8 @@ type TimelineEntry = interface TimelineData { entries: Array; + queuedEntries: Array; + queuedCount: number; isFetching: boolean; isPending: boolean; } @@ -45,6 +47,7 @@ interface State { initialFetch: boolean, ) => void; setLoading: (timelineId: string, isFetching: boolean) => void; + dequeueEntries: (timelineId: string) => void; }; } @@ -137,12 +140,24 @@ const useTimelinesStore = create()( timeline.isFetching = isFetching; if (!isFetching) timeline.isPending = false; }), + dequeueEntries: (timelineId) => + set((state) => { + const timeline = state.timelines[timelineId]; + + if (!timeline || timeline.queuedEntries.length === 0) return; + + timeline.entries.unshift(...timeline.queuedEntries); + timeline.queuedEntries = []; + timeline.queuedCount = 0; + }), }, })), ); const createEmptyTimeline = (): TimelineData => ({ entries: [], + queuedEntries: [], + queuedCount: 0, isFetching: false, isPending: true, }); diff --git a/packages/pl-api/lib/main.ts b/packages/pl-api/lib/main.ts index 5707875c1..7d9494316 100644 --- a/packages/pl-api/lib/main.ts +++ b/packages/pl-api/lib/main.ts @@ -1,4 +1,42 @@ -export { default as PlApiClient } from '@/client'; +export { + default as PlApiClient, + accounts as accountsCategory, + admin as adminCategory, + announcements as announcementsCategory, + antennas as antennasCategory, + apps as appsCategory, + asyncRefreshes as asyncRefreshesCategory, + chats as chatsCategory, + circles as circlesCategory, + drive as driveCategory, + emails as emailsCategory, + events as eventsCategory, + experimental as experimentalCategory, + filtering as filteringCategory, + groupedNotifications as groupedNotificationsCategory, + instance as instanceCategory, + interactionRequests as interactionRequestsCategory, + lists as listsCategory, + media as mediaCategory, + myAccount as myAccountCategory, + notifications as notificationsCategory, + oauth as oauthCategory, + oembed as oembedCategory, + polls as pollsCategory, + pushNotifications as pushNotificationsCategory, + rssFeedSubscriptions as rssFeedSubscriptionsCategory, + scheduledStatuses as scheduledStatusesCategory, + search as searchCategory, + settings as settingsCategory, + shoutbox as shoutboxCategory, + statuses as statusesCategory, + stories as storiesCategory, + streaming as streamingCategory, + subscriptions as subscriptionsCategory, + timelines as timelinesCategory, + trends as trendsCategory, + utils as utilsCategory, +} from '@/client'; export { PlApiBaseClient } from '@/client-base'; export { PlApiDirectoryClient } from '@/directory-client'; export { type Response as PlApiResponse, type AsyncRefreshHeader } from '@/request'; diff --git a/packages/pl-api/package.json b/packages/pl-api/package.json index 0ab837b49..8a57bcc89 100644 --- a/packages/pl-api/package.json +++ b/packages/pl-api/package.json @@ -1,6 +1,6 @@ { "name": "pl-api", - "version": "1.0.0-rc.98", + "version": "1.0.0-rc.99", "homepage": "https://codeberg.org/mkljczk/nicolium/src/branch/develop/packages/pl-api", "bugs": { "url": "https://codeberg.org/mkljczk/nicolium/issues" From 395553bc6be4c9cf06f97287e1646caafa746a3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 10:49:41 +0100 Subject: [PATCH 03/89] nicolium: allow hotkeys to continue propagating MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../nicolium/src/features/ui/components/hotkeys.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/nicolium/src/features/ui/components/hotkeys.tsx b/packages/nicolium/src/features/ui/components/hotkeys.tsx index 89fb67241..ec70798cd 100644 --- a/packages/nicolium/src/features/ui/components/hotkeys.tsx +++ b/packages/nicolium/src/features/ui/components/hotkeys.tsx @@ -200,7 +200,7 @@ function useHotkeys(handlers: HandlerMap) { const matchCandidates: { // A candidate will be have an undefined handler if it's matched, // but handled in a parent component rather than this one. - handler: ((event: KeyboardEvent) => void) | undefined; + handler: ((event: KeyboardEvent) => void | boolean) | undefined; priority: number; }[] = []; @@ -220,9 +220,11 @@ function useHotkeys(handlers: HandlerMap) { const bestMatchingHandler = matchCandidates.at(0)?.handler; if (bestMatchingHandler) { - bestMatchingHandler(event); - event.stopPropagation(); - event.preventDefault(); + const result = bestMatchingHandler(event); + if (result !== false) { + event.stopPropagation(); + event.preventDefault(); + } } // Add last keypress to buffer From 05b44067c2201493de9e01ae9d13c56ae00ad1a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 10:54:38 +0100 Subject: [PATCH 04/89] nicolium: apply this hotkey thing to new timeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/columns/timeline.tsx | 6 ++---- packages/nicolium/src/components/statuses/status.tsx | 6 ++++-- packages/nicolium/src/components/statuses/tombstone.tsx | 4 ++-- packages/nicolium/src/utils/scroll-utils.ts | 3 +++ 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/nicolium/src/columns/timeline.tsx b/packages/nicolium/src/columns/timeline.tsx index edea4b39c..752e5aa3f 100644 --- a/packages/nicolium/src/columns/timeline.tsx +++ b/packages/nicolium/src/columns/timeline.tsx @@ -106,18 +106,16 @@ const Timeline: React.FC = ({ query, contextType = 'public' }) => { const { entries, queuedCount, fetchNextPage, dequeueEntries, isFetching, isPending } = query; - const handleMoveUp = (index: number) => { + const handleMoveUp = (index: number) => selectChild(index - 1, node, document.getElementById('status-list') ?? undefined); - }; - const handleMoveDown = (index: number) => { + const handleMoveDown = (index: number) => selectChild( index + 1, node, document.getElementById('status-list') ?? undefined, entries.length, ); - }; const renderEntry = (entry: TimelineEntry, index: number) => { if (entry.type === 'status') { diff --git a/packages/nicolium/src/components/statuses/status.tsx b/packages/nicolium/src/components/statuses/status.tsx index e3fe97789..721719695 100644 --- a/packages/nicolium/src/components/statuses/status.tsx +++ b/packages/nicolium/src/components/statuses/status.tsx @@ -330,14 +330,16 @@ const Status: React.FC = React.memo((props) => { const handleHotkeyMoveUp = () => { if (onMoveUp) { - onMoveUp(status.id, featured); + return onMoveUp(status.id, featured); } + return false; }; const handleHotkeyMoveDown = () => { if (onMoveDown) { - onMoveDown(status.id, featured); + return onMoveDown(status.id, featured); } + return false; }; const handleHotkeyToggleSensitive = () => { diff --git a/packages/nicolium/src/components/statuses/tombstone.tsx b/packages/nicolium/src/components/statuses/tombstone.tsx index 702b63893..cf52fcf49 100644 --- a/packages/nicolium/src/components/statuses/tombstone.tsx +++ b/packages/nicolium/src/components/statuses/tombstone.tsx @@ -6,8 +6,8 @@ import { Hotkeys } from '@/features/ui/components/hotkeys'; interface ITombstone { id: string; - onMoveUp?: (statusId: string) => void; - onMoveDown?: (statusId: string) => void; + onMoveUp?: (statusId: string) => void | boolean; + onMoveDown?: (statusId: string) => void | boolean; deleted?: boolean; } diff --git a/packages/nicolium/src/utils/scroll-utils.ts b/packages/nicolium/src/utils/scroll-utils.ts index c13159596..fcfa39b2e 100644 --- a/packages/nicolium/src/utils/scroll-utils.ts +++ b/packages/nicolium/src/utils/scroll-utils.ts @@ -8,12 +8,15 @@ const selectChild = ( count?: number, align?: 'start' | 'center' | 'end', ) => { + if (index < 0) return false; + if (count !== undefined && index === count) { const loadMoreButton = node.querySelector('.⁂-load-more'); if (loadMoreButton) { loadMoreButton.focus({ preventScroll: false }); return; } + return false; } const selector = `[data-index="${index}"] .focusable`; From a596585339205c06066b2d5d05912787391ee81f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 11:00:03 +0100 Subject: [PATCH 05/89] nicolium: forgot to hit save lol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/columns/timeline.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nicolium/src/columns/timeline.tsx b/packages/nicolium/src/columns/timeline.tsx index 752e5aa3f..475a072c6 100644 --- a/packages/nicolium/src/columns/timeline.tsx +++ b/packages/nicolium/src/columns/timeline.tsx @@ -43,8 +43,8 @@ interface ITimelineStatus { contextType?: FilterContextType; isConnectedTop?: boolean; isConnectedBottom?: boolean; - onMoveUp?: (id: string) => void; - onMoveDown?: (id: string) => void; + onMoveUp?: (id: string) => void | boolean; + onMoveDown?: (id: string) => void | boolean; } /** Status with reply-connector in threads. */ From 40aa74fcb5d178bb6a8e12742f29982e88ede875 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 11:55:37 +0100 Subject: [PATCH 06/89] nicolium: attempts at modal accessibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/components/modal-root.tsx | 7 ++++++- packages/nicolium/src/styles/new/components.scss | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/nicolium/src/components/modal-root.tsx b/packages/nicolium/src/components/modal-root.tsx index 95448c96e..68fe8cc19 100644 --- a/packages/nicolium/src/components/modal-root.tsx +++ b/packages/nicolium/src/components/modal-root.tsx @@ -255,7 +255,12 @@ const ModalRoot: React.FC = ({ children, onCancel, onClose, type, mo onClick={handleOnClose} /> -
+
{children}
diff --git a/packages/nicolium/src/styles/new/components.scss b/packages/nicolium/src/styles/new/components.scss index 685d37e89..a18f23961 100644 --- a/packages/nicolium/src/styles/new/components.scss +++ b/packages/nicolium/src/styles/new/components.scss @@ -358,7 +358,8 @@ div[data-viewport-type='window']:has(.⁂-empty-message) { @apply flex w-full items-center gap-2; } - h3 { + h3, + h2 { @apply grow text-lg font-bold leading-6 text-gray-900 dark:text-white; } From b56278144ce0309018af28cfcdd2c999cbad2c2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 12:04:43 +0100 Subject: [PATCH 07/89] nicolium: forgot to hit save MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/components/ui/modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nicolium/src/components/ui/modal.tsx b/packages/nicolium/src/components/ui/modal.tsx index b775e11be..d10c06c7c 100644 --- a/packages/nicolium/src/components/ui/modal.tsx +++ b/packages/nicolium/src/components/ui/modal.tsx @@ -126,7 +126,7 @@ const Modal = React.forwardRef( /> )} -

{title}

+ {onClose && ( Date: Thu, 5 Mar 2026 12:09:09 +0100 Subject: [PATCH 08/89] nicolium: make hotkey modal more screen reader-readable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/locales/en.json | 12 ++ .../nicolium/src/modals/hotkeys-modal.tsx | 179 ++++++++++-------- 2 files changed, 117 insertions(+), 74 deletions(-) diff --git a/packages/nicolium/src/locales/en.json b/packages/nicolium/src/locales/en.json index c04f17633..dd5bf1b81 100644 --- a/packages/nicolium/src/locales/en.json +++ b/packages/nicolium/src/locales/en.json @@ -1282,6 +1282,7 @@ "join_event.request_success": "Requested to join the event", "join_event.success": "Joined the event", "join_event.title": "Join event", + "keyboard_shortcuts.action": "Action", "keyboard_shortcuts.back": "to navigate back", "keyboard_shortcuts.blocked": "to open blocked users list", "keyboard_shortcuts.boost": "to repost", @@ -1293,6 +1294,17 @@ "keyboard_shortcuts.heading": "Keyboard shortcuts", "keyboard_shortcuts.home": "to open home timeline", "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.joiners.or": "or", + "keyboard_shortcuts.joiners.plus": "plus", + "keyboard_shortcuts.joiners.then": "then", + "keyboard_shortcuts.key_names.alt": "Alt", + "keyboard_shortcuts.key_names.backspace": "Backspace", + "keyboard_shortcuts.key_names.down": "Arrow down", + "keyboard_shortcuts.key_names.enter": "Enter", + "keyboard_shortcuts.key_names.esc": "Escape", + "keyboard_shortcuts.key_names.question_mark": "Question mark", + "keyboard_shortcuts.key_names.slash": "Slash", + "keyboard_shortcuts.key_names.up": "Arrow up", "keyboard_shortcuts.legend": "to display this legend", "keyboard_shortcuts.mention": "to mention author", "keyboard_shortcuts.muted": "to open muted users list", diff --git a/packages/nicolium/src/modals/hotkeys-modal.tsx b/packages/nicolium/src/modals/hotkeys-modal.tsx index 72b3277a3..d6f9d297e 100644 --- a/packages/nicolium/src/modals/hotkeys-modal.tsx +++ b/packages/nicolium/src/modals/hotkeys-modal.tsx @@ -1,6 +1,6 @@ import clsx from 'clsx'; import React from 'react'; -import { FormattedMessage } from 'react-intl'; +import { defineMessages, FormattedMessage, type MessageDescriptor, useIntl } from 'react-intl'; import Modal from '@/components/ui/modal'; import { useFeatures } from '@/hooks/use-features'; @@ -8,12 +8,88 @@ import { useLoggedIn } from '@/hooks/use-logged-in'; import type { BaseModalProps } from '@/features/ui/components/modal-root'; +const messages = defineMessages({ + keyNameSlash: { id: 'keyboard_shortcuts.key_names.slash', defaultMessage: 'Slash' }, + keyNameQuestionMark: { + id: 'keyboard_shortcuts.key_names.question_mark', + defaultMessage: 'Question mark', + }, + keyNameAlt: { id: 'keyboard_shortcuts.key_names.alt', defaultMessage: 'Alt' }, + keyNameBackspace: { id: 'keyboard_shortcuts.key_names.backspace', defaultMessage: 'Backspace' }, + keyNameDown: { id: 'keyboard_shortcuts.key_names.down', defaultMessage: 'Arrow down' }, + keyNameEnter: { id: 'keyboard_shortcuts.key_names.enter', defaultMessage: 'Enter' }, + keyNameEsc: { id: 'keyboard_shortcuts.key_names.esc', defaultMessage: 'Escape' }, + keyNameUp: { id: 'keyboard_shortcuts.key_names.up', defaultMessage: 'Arrow up' }, + joinerOr: { id: 'keyboard_shortcuts.joiners.or', defaultMessage: 'or' }, + joinerPlus: { id: 'keyboard_shortcuts.joiners.plus', defaultMessage: 'plus' }, + joinerThen: { id: 'keyboard_shortcuts.joiners.then', defaultMessage: 'then' }, +}); + const Hotkey: React.FC<{ children: React.ReactNode }> = ({ children }) => ( {children} ); +const spokenKeyNames: Record = { + '/': messages.keyNameSlash, + '?': messages.keyNameQuestionMark, + alt: messages.keyNameAlt, + backspace: messages.keyNameBackspace, + down: messages.keyNameDown, + enter: messages.keyNameEnter, + esc: messages.keyNameEsc, + up: messages.keyNameUp, +}; + +const getSpokenKeyName = (keyName: string) => { + if (spokenKeyNames[keyName]) return spokenKeyNames[keyName]; + if (/^[a-z]$/i.test(keyName)) return keyName.toUpperCase(); + return keyName; +}; + +type KeyJoiner = 'or' | 'plus' | 'then'; + +const visualJoiners: Record = { + or: ', ', + plus: ' + ', + then: ' + ', +}; + +const spokenJoiners: Record = { + or: messages.joinerOr, + plus: messages.joinerPlus, + then: messages.joinerThen, +}; + +const HotkeyBinding: React.FC<{ keys: string[]; joiner?: KeyJoiner }> = ({ + keys, + joiner = 'or', +}) => { + const intl = useIntl(); + + const spokenBinding = keys + .map((keyName) => { + const spokenKey = getSpokenKeyName(keyName); + return typeof spokenKey === 'string' ? spokenKey : intl.formatMessage(spokenKey); + }) + .join(` ${intl.formatMessage(spokenJoiners[joiner])} `); + + return ( + + + {spokenBinding} + + ); +}; + const TableCell: React.FC<{ className?: string; children: React.ReactNode }> = ({ className, children, @@ -41,17 +117,17 @@ const HotkeysModal: React.FC = ({ onClose }) => { const hotkeys = [ isLoggedIn && { - key: r, + key: , label: , }, isLoggedIn && { - key: m, + key: , label: ( ), }, { - key: p, + key: , label: ( = ({ onClose }) => { ), }, isLoggedIn && { - key: f, + key: , label: , }, isLoggedIn && features.emojiReacts && { - key: e, + key: , label: , }, isLoggedIn && { - key: b, + key: , label: , }, { - key: ( - <> - enter, o - - ), + key: , label: , }, { - key: a, + key: , label: , }, features.spoilers && { - key: x, + key: , label: ( = ({ onClose }) => { ), }, features.spoilers && { - key: h, + key: , label: ( = ({ onClose }) => { ), }, { - key: ( - <> - up, k - - ), + key: , label: ( ), }, { - key: ( - <> - down, j - - ), + key: , label: ( ), }, isLoggedIn && { - key: n, + key: , label: ( = ({ onClose }) => { ), }, isLoggedIn && { - key: ( - <> - alt + n - - ), + key: , label: , }, { - key: backspace, + key: , label: , }, isLoggedIn && { - key: ( - <> - s, / - - ), + key: , label: document.querySelector('#search') ? ( ) : ( @@ -159,7 +215,7 @@ const HotkeysModal: React.FC = ({ onClose }) => { ), }, { - key: esc, + key: , label: ( = ({ onClose }) => { ), }, isLoggedIn && { - key: ( - <> - g + h - - ), + key: , label: ( ), }, isLoggedIn && { - key: ( - <> - g + n - - ), + key: , label: ( = ({ onClose }) => { ), }, isLoggedIn && { - key: ( - <> - g + f - - ), + key: , label: ( ), }, isLoggedIn && { - key: ( - <> - g + u - - ), + key: , label: ( = ({ onClose }) => { ), }, isLoggedIn && { - key: ( - <> - g + b - - ), + key: , label: ( = ({ onClose }) => { ), }, isLoggedIn && { - key: ( - <> - g + m - - ), + key: , label: ( ), }, isLoggedIn && features.followRequests && { - key: ( - <> - g + r - - ), + key: , label: ( = ({ onClose }) => { ), }, { - key: ?, + key: , label: ( ), @@ -291,6 +319,9 @@ const HotkeysModal: React.FC = ({ onClose }) => { + + + From 28de0cce6eb6ff3ffe4645d6d5150a84a35337c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 12:12:55 +0100 Subject: [PATCH 09/89] nicolium: hate it tbh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/styles/new/layout.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/nicolium/src/styles/new/layout.scss b/packages/nicolium/src/styles/new/layout.scss index f60ef84bf..c957cffef 100644 --- a/packages/nicolium/src/styles/new/layout.scss +++ b/packages/nicolium/src/styles/new/layout.scss @@ -291,7 +291,6 @@ body { justify-content: center; align-content: space-around; pointer-events: none; - user-select: none; overscroll-behavior: none; min-height: 100dvh; z-index: 9999; From a2463b9a82347bbebf2df834804c227ed367ebcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 12:15:41 +0100 Subject: [PATCH 10/89] nicolium: WHY MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/components/ui/button/index.tsx | 1 - .../nicolium/src/features/auth-login/components/login-form.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/nicolium/src/components/ui/button/index.tsx b/packages/nicolium/src/components/ui/button/index.tsx index 8ebd0f180..4a9fca70e 100644 --- a/packages/nicolium/src/components/ui/button/index.tsx +++ b/packages/nicolium/src/components/ui/button/index.tsx @@ -111,7 +111,6 @@ const Button = React.forwardRef( to={props.to} params={props.params} search={props.search} - tabIndex={-1} > {buttonChildren} diff --git a/packages/nicolium/src/features/auth-login/components/login-form.tsx b/packages/nicolium/src/features/auth-login/components/login-form.tsx index 15630e9ee..95d440a4c 100644 --- a/packages/nicolium/src/features/auth-login/components/login-form.tsx +++ b/packages/nicolium/src/features/auth-login/components/login-form.tsx @@ -55,7 +55,7 @@ const LoginForm: React.FC = ({ isLoading, handleSubmit }) => { + } From 3192fec19d92f2c7e0ea071a23197dd539b0345a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 12:23:16 +0100 Subject: [PATCH 11/89] nicolium: already a crime? MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/components/modal-root.tsx | 1 + packages/nicolium/src/components/ui/modal.tsx | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/nicolium/src/components/modal-root.tsx b/packages/nicolium/src/components/modal-root.tsx index 68fe8cc19..6eaf5c521 100644 --- a/packages/nicolium/src/components/modal-root.tsx +++ b/packages/nicolium/src/components/modal-root.tsx @@ -260,6 +260,7 @@ const ModalRoot: React.FC = ({ children, onCancel, onClose, type, mo className='⁂-modal-root__container' aria-modal aria-labelledby='modal-title' + aria-describedby='modal-body' > {children}
diff --git a/packages/nicolium/src/components/ui/modal.tsx b/packages/nicolium/src/components/ui/modal.tsx index d10c06c7c..5115c4006 100644 --- a/packages/nicolium/src/components/ui/modal.tsx +++ b/packages/nicolium/src/components/ui/modal.tsx @@ -140,7 +140,9 @@ const Modal = React.forwardRef( )}
-
{children}
+ {confirmationAction && (
From 0d89f53a910c3939e331a08311e7d17c59c88555 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 12:44:19 +0100 Subject: [PATCH 12/89] nicolium: fix profile dropdown hotkey navigation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../src/components/dropdown-menu/dropdown-menu.tsx | 12 ++++++++++++ packages/nicolium/src/components/modal-root.tsx | 2 +- packages/nicolium/src/components/ui/modal.tsx | 4 +--- .../src/features/ui/components/profile-dropdown.tsx | 3 +++ packages/nicolium/src/modals/confirmation-modal.tsx | 2 +- .../src/modals/missing-description-modal.tsx | 2 +- 6 files changed, 19 insertions(+), 6 deletions(-) diff --git a/packages/nicolium/src/components/dropdown-menu/dropdown-menu.tsx b/packages/nicolium/src/components/dropdown-menu/dropdown-menu.tsx index b160ee8b2..172814b1a 100644 --- a/packages/nicolium/src/components/dropdown-menu/dropdown-menu.tsx +++ b/packages/nicolium/src/components/dropdown-menu/dropdown-menu.tsx @@ -148,6 +148,18 @@ const DropdownMenuContent: React.FC = ({ } document.addEventListener('keydown', handleKeyDown, false); + if (Component && !items?.length) { + const elements = Array.from( + ref.current?.querySelectorAll( + 'a, button:not([disabled]), input:not([disabled]), select:not([disabled])', + ) ?? [], + ).filter((element) => !element.hasAttribute('aria-hidden')); + const firstElement = elements[0]; + if (firstElement) { + firstElement.focus(); + } + } + return () => { document.removeEventListener('click', handleDocumentClick); document.removeEventListener('touchend', handleDocumentClick); diff --git a/packages/nicolium/src/components/modal-root.tsx b/packages/nicolium/src/components/modal-root.tsx index 6eaf5c521..fce5dc8af 100644 --- a/packages/nicolium/src/components/modal-root.tsx +++ b/packages/nicolium/src/components/modal-root.tsx @@ -260,7 +260,7 @@ const ModalRoot: React.FC = ({ children, onCancel, onClose, type, mo className='⁂-modal-root__container' aria-modal aria-labelledby='modal-title' - aria-describedby='modal-body' + aria-describedby='modal-description' > {children}
diff --git a/packages/nicolium/src/components/ui/modal.tsx b/packages/nicolium/src/components/ui/modal.tsx index 5115c4006..d10c06c7c 100644 --- a/packages/nicolium/src/components/ui/modal.tsx +++ b/packages/nicolium/src/components/ui/modal.tsx @@ -140,9 +140,7 @@ const Modal = React.forwardRef( )}
- +
{children}
{confirmationAction && (
diff --git a/packages/nicolium/src/features/ui/components/profile-dropdown.tsx b/packages/nicolium/src/features/ui/components/profile-dropdown.tsx index 78148db3e..26c8e0c64 100644 --- a/packages/nicolium/src/features/ui/components/profile-dropdown.tsx +++ b/packages/nicolium/src/features/ui/components/profile-dropdown.tsx @@ -19,6 +19,7 @@ import ThemeToggle from './theme-toggle'; import type { Account as AccountEntity } from 'pl-api'; const messages = defineMessages({ + openDropdown: { id: 'profile_dropdown.open_dropdown', defaultMessage: 'Open profile dropdown' }, add: { id: 'profile_dropdown.add_account', defaultMessage: 'Add an existing account' }, theme: { id: 'profile_dropdown.theme', defaultMessage: 'Theme' }, logout: { id: 'profile_dropdown.logout', defaultMessage: 'Log out @{acct}' }, @@ -116,6 +117,8 @@ const ProfileDropdown: React.FC = ({ account, children }) => { diff --git a/packages/nicolium/src/modals/confirmation-modal.tsx b/packages/nicolium/src/modals/confirmation-modal.tsx index d82d1d899..010bf50dd 100644 --- a/packages/nicolium/src/modals/confirmation-modal.tsx +++ b/packages/nicolium/src/modals/confirmation-modal.tsx @@ -68,7 +68,7 @@ const ConfirmationModal: React.FC = ({ secondaryAction={onSecondary && handleSecondary} > - {message} + {message} {checkbox && ( diff --git a/packages/nicolium/src/modals/missing-description-modal.tsx b/packages/nicolium/src/modals/missing-description-modal.tsx index ed072c1fa..32394bdd7 100644 --- a/packages/nicolium/src/modals/missing-description-modal.tsx +++ b/packages/nicolium/src/modals/missing-description-modal.tsx @@ -35,7 +35,7 @@ const MissingDescriptionModal: React.FC -

+

+ +
+); + interface ITimelineStatus { id: string; contextType?: FilterContextType; @@ -134,10 +140,10 @@ const Timeline: React.FC = ({ query, contextType = 'public' }) => { /> ); } - if (entry.type === 'page-end' || entry.type === 'page-start') { + if ((entry.type === 'page-end' || entry.type === 'page-start') && !isFetching) { return (
- +
); } @@ -158,17 +164,10 @@ const Timeline: React.FC = ({ query, contextType = 'public' }) => { key='scrollable-list' isLoading={isFetching} showLoading={isPending} - placeholderComponent={() => } + placeholderComponent={() => } placeholderCount={20} ref={node} - // className={className} - // listClassName={clsx('divide-y divide-solid divide-gray-200 dark:divide-gray-800', { - // 'divide-none': divideType !== 'border', - // })} - // itemClassName={clsx({ - // 'pb-3': divideType !== 'border', - // })} - // {...other} + hasMore > {(entries || []).map(renderEntry)} diff --git a/packages/nicolium/src/stores/timelines.ts b/packages/nicolium/src/stores/timelines.ts index 4bfa788b1..5783abf00 100644 --- a/packages/nicolium/src/stores/timelines.ts +++ b/packages/nicolium/src/stores/timelines.ts @@ -166,9 +166,7 @@ const useTimelinesStore = create()( }, setLoading: (timelineId, isFetching) => set((state) => { - const timeline = state.timelines[timelineId]; - - if (!timeline) return; + const timeline = (state.timelines[timelineId] ??= createEmptyTimeline()); timeline.isFetching = isFetching; if (!isFetching) timeline.isPending = false; From 681285e42d6e2cd517ee3af82c0d41014d33f799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 13:41:42 +0100 Subject: [PATCH 16/89] nicolium: experimental timeline: display reblog/hashtag follow information MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/columns/timeline.tsx | 106 +++++++++++++++++- .../src/components/statuses/status.tsx | 13 ++- .../src/queries/timelines/use-timeline.ts | 4 +- 3 files changed, 113 insertions(+), 10 deletions(-) diff --git a/packages/nicolium/src/columns/timeline.tsx b/packages/nicolium/src/columns/timeline.tsx index 60482fd7e..c63bb3bd4 100644 --- a/packages/nicolium/src/columns/timeline.tsx +++ b/packages/nicolium/src/columns/timeline.tsx @@ -1,15 +1,21 @@ +import { Link } from '@tanstack/react-router'; import clsx from 'clsx'; import React, { useRef } from 'react'; -import { defineMessages } from 'react-intl'; +import { defineMessages, FormattedList, FormattedMessage } from 'react-intl'; import LoadMore from '@/components/load-more'; import ScrollTopButton from '@/components/scroll-top-button'; import ScrollableList from '@/components/scrollable-list'; -import Status from '@/components/statuses/status'; +import Status, { StatusFollowedTagInfo } from '@/components/statuses/status'; +import StatusInfo from '@/components/statuses/status-info'; import Tombstone from '@/components/statuses/tombstone'; +import Icon from '@/components/ui/icon'; import Portal from '@/components/ui/portal'; +import Emojify from '@/features/emoji/emojify'; import PlaceholderStatus from '@/features/placeholder/components/placeholder-status'; -import { useStatus } from '@/queries/statuses/use-status'; +import { useFeatures } from '@/hooks/use-features'; +import { useAccounts } from '@/queries/accounts/use-accounts'; +import { type SelectedStatus, useStatus } from '@/queries/statuses/use-status'; import { useAntennaTimeline, useBubbleTimeline, @@ -44,8 +50,90 @@ const PlaceholderTimelineStatus = () => (
); +interface ITimelineStatusInfo { + status: SelectedStatus; + rebloggedBy: Array; + timelineId: string; +} + +const TimelineStatusInfo: React.FC = ({ status, rebloggedBy, timelineId }) => { + const features = useFeatures(); + const isReblogged = rebloggedBy.length > 0; + + const { data: accounts } = useAccounts(rebloggedBy); + + if (isReblogged) { + const renderedAccounts = accounts.slice(0, 2).map( + (account) => + !!account && ( + + + + + + + + ), + ); + + if (accounts.length > 2) { + renderedAccounts.push( + , + ); + } + + const values = { + name: , + count: accounts.length, + }; + + return ( + + } + text={ + // status.visibility === 'private' ? ( + // + // ) : ( + + // ) + } + /> + ); + } + if (timelineId.split(':')[0] === 'home' && features.followHashtags) { + return ; + } +}; + interface ITimelineStatus { id: string; + rebloggedBy: Array; + timelineId: string; contextType?: FilterContextType; isConnectedTop?: boolean; isConnectedBottom?: boolean; @@ -92,6 +180,13 @@ const TimelineStatus: React.FC = (props): React.JSX.Element => '⁂-timeline-status--connected-top': isConnectedTop, })} > + {statusQuery.data && ( + + )} {renderConnector()} {statusQuery.isPending ? ( @@ -110,7 +205,8 @@ interface ITimeline { const Timeline: React.FC = ({ query, contextType = 'public' }) => { const node = useRef(null); - const { entries, queuedCount, fetchNextPage, dequeueEntries, isFetching, isPending } = query; + const { timelineId, entries, queuedCount, fetchNextPage, dequeueEntries, isFetching, isPending } = + query; const handleMoveUp = (index: number) => selectChild(index - 1, node, document.getElementById('status-list') ?? undefined); @@ -134,6 +230,8 @@ const Timeline: React.FC = ({ query, contextType = 'public' }) => { contextType={contextType} onMoveUp={() => handleMoveUp(index)} onMoveDown={() => handleMoveDown(index)} + rebloggedBy={entry.rebloggedBy} + timelineId={timelineId} // contextType={timelineId} // showGroup={showGroup} // variant={divideType === 'border' ? 'slim' : 'rounded'} diff --git a/packages/nicolium/src/components/statuses/status.tsx b/packages/nicolium/src/components/statuses/status.tsx index 721719695..f8cfed55f 100644 --- a/packages/nicolium/src/components/statuses/status.tsx +++ b/packages/nicolium/src/components/statuses/status.tsx @@ -98,11 +98,16 @@ const AccountInfo: React.FC = React.memo(({ status }) => { AccountInfo.displayName = 'AccountInfo'; interface IStatusFollowedTagInfo { + className?: string; status: SelectedStatus; avatarSize: number; } -const StatusFollowedTagInfo: React.FC = ({ status, avatarSize }) => { +const StatusFollowedTagInfo: React.FC = ({ + className, + status, + avatarSize, +}) => { const { data: followedTags } = useFollowedTags(); const filteredTags = status.tags.filter((tag) => @@ -130,7 +135,7 @@ const StatusFollowedTagInfo: React.FC = ({ status, avata return ( = React.memo((props) => { } else if (fromHomeTimeline) { return ( features.followHashtags && ( - + ) ); } @@ -691,4 +696,4 @@ const Status: React.FC = React.memo((props) => { Status.displayName = 'Status'; -export { type IStatus, Status as default }; +export { type IStatus, Status as default, StatusFollowedTagInfo }; diff --git a/packages/nicolium/src/queries/timelines/use-timeline.ts b/packages/nicolium/src/queries/timelines/use-timeline.ts index ddae246ca..5add2d601 100644 --- a/packages/nicolium/src/queries/timelines/use-timeline.ts +++ b/packages/nicolium/src/queries/timelines/use-timeline.ts @@ -57,8 +57,8 @@ const useTimeline = (timelineId: string, fetcher: TimelineFetcher, streamConfig? }, [timelineId]); return useMemo( - () => ({ ...timeline, fetchNextPage, dequeueEntries }), - [timeline, fetchNextPage, dequeueEntries], + () => ({ ...timeline, timelineId, fetchNextPage, dequeueEntries }), + [timeline, timelineId, fetchNextPage, dequeueEntries], ); }; From f90e11dbc40073fee48cd7170a50b502728ee0b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 13:48:46 +0100 Subject: [PATCH 17/89] nicolium: fix for multiple appearances of the same reblog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/stores/timelines.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/nicolium/src/stores/timelines.ts b/packages/nicolium/src/stores/timelines.ts index 5783abf00..4ef9db422 100644 --- a/packages/nicolium/src/stores/timelines.ts +++ b/packages/nicolium/src/stores/timelines.ts @@ -82,7 +82,10 @@ const processPage = (statuses: Array, hasMore: boolean): Array Date: Thu, 5 Mar 2026 13:52:45 +0100 Subject: [PATCH 18/89] nicolium: update en.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/locales/en.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/nicolium/src/locales/en.json b/packages/nicolium/src/locales/en.json index dd5bf1b81..91562c4d0 100644 --- a/packages/nicolium/src/locales/en.json +++ b/packages/nicolium/src/locales/en.json @@ -1642,6 +1642,7 @@ "privacy.unlisted.short": "Quiet public", "profile_dropdown.add_account": "Add an existing account", "profile_dropdown.logout": "Log out @{acct}", + "profile_dropdown.open_dropdown": "Open profile dropdown", "profile_dropdown.switch_account": "Switch accounts", "profile_dropdown.theme": "Theme", "reactions.all": "All", From 94d7187d4e2f96ff00a16c3ba1130d4a514503ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 14:05:52 +0100 Subject: [PATCH 19/89] nicolium: pinned group statuses are not supported MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/pages/timelines/group-timeline.tsx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/nicolium/src/pages/timelines/group-timeline.tsx b/packages/nicolium/src/pages/timelines/group-timeline.tsx index a5ffe5e82..c0bd20633 100644 --- a/packages/nicolium/src/pages/timelines/group-timeline.tsx +++ b/packages/nicolium/src/pages/timelines/group-timeline.tsx @@ -13,16 +13,12 @@ import Timeline from '@/features/ui/components/timeline'; import { groupTimelineRoute } from '@/features/ui/router'; import { ComposeForm } from '@/features/ui/util/async-components'; import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useAppSelector } from '@/hooks/use-app-selector'; import { useDraggedFiles } from '@/hooks/use-dragged-files'; import { useOwnAccount } from '@/hooks/use-own-account'; import { useGroupQuery } from '@/queries/groups/use-group'; -import { makeGetStatusIds } from '@/selectors'; import { useComposeActions, useUploadCompose } from '@/stores/compose'; import { useSettings } from '@/stores/settings'; -const getStatusIds = makeGetStatusIds(); - interface IGroupTimeline { groupId: string; } @@ -30,10 +26,6 @@ interface IGroupTimeline { const GroupTimeline: React.FC = ({ groupId }) => { const dispatch = useAppDispatch(); - const featuredStatusIds = useAppSelector((state) => - getStatusIds(state, { type: `group:${groupId}:pinned` }), - ); - const handleLoadMore = () => { dispatch(fetchGroupTimeline(groupId, {}, true)); }; @@ -57,7 +49,6 @@ const GroupTimeline: React.FC = ({ groupId }) => { } emptyMessageIcon={require('@phosphor-icons/core/regular/chat-centered-text.svg')} showGroup={false} - featuredStatusIds={featuredStatusIds} /> ); }; From 6c037c1a1c65c4bf8eaf11ab638cc46e0eb5f108 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 14:14:13 +0100 Subject: [PATCH 20/89] nicolium: add missing experimental circle timeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/columns/timeline.tsx | 12 +++ .../src/pages/timelines/circle-timeline.tsx | 76 ++++++++++++------- .../src/queries/timelines/use-timelines.ts | 33 +++++++- 3 files changed, 91 insertions(+), 30 deletions(-) diff --git a/packages/nicolium/src/columns/timeline.tsx b/packages/nicolium/src/columns/timeline.tsx index c63bb3bd4..b225e16c5 100644 --- a/packages/nicolium/src/columns/timeline.tsx +++ b/packages/nicolium/src/columns/timeline.tsx @@ -19,6 +19,7 @@ import { type SelectedStatus, useStatus } from '@/queries/statuses/use-status'; import { useAntennaTimeline, useBubbleTimeline, + useCircleTimeline, useGroupTimeline, useHashtagTimeline, useHomeTimeline, @@ -347,6 +348,16 @@ const AntennaTimelineColumn: React.FC = ({ antennaId }) return ; }; +interface ICircleTimelineColumn { + circleId: string; +} + +const CircleTimelineColumn: React.FC = ({ circleId }) => { + const timelineQuery = useCircleTimeline(circleId); + + return ; +}; + const WrenchedTimelineColumn = () => { const timelineQuery = useWrenchedTimeline(); @@ -362,5 +373,6 @@ export { GroupTimelineColumn, BubbleTimelineColumn, AntennaTimelineColumn, + CircleTimelineColumn, WrenchedTimelineColumn, }; diff --git a/packages/nicolium/src/pages/timelines/circle-timeline.tsx b/packages/nicolium/src/pages/timelines/circle-timeline.tsx index 6da740c20..c170266dc 100644 --- a/packages/nicolium/src/pages/timelines/circle-timeline.tsx +++ b/packages/nicolium/src/pages/timelines/circle-timeline.tsx @@ -3,6 +3,7 @@ import React, { useEffect } from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; import { fetchCircleTimeline } from '@/actions/timelines'; +import { CircleTimelineColumn } from '@/columns/timeline'; import DropdownMenu from '@/components/dropdown-menu'; import MissingIndicator from '@/components/missing-indicator'; import Button from '@/components/ui/button'; @@ -13,6 +14,7 @@ import { circleTimelineRoute } from '@/features/ui/router'; import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useCircle, useDeleteCircle } from '@/queries/accounts/use-circles'; import { useModalsActions } from '@/stores/modals'; +import { useSettings } from '@/stores/settings'; const messages = defineMessages({ deleteHeading: { id: 'confirmations.delete_circle.heading', defaultMessage: 'Delete circle' }, @@ -25,6 +27,48 @@ const messages = defineMessages({ deleteCircle: { id: 'circles.delete', defaultMessage: 'Delete circle' }, }); +interface ICircleTimeline { + circleId: string; +} + +const CircleTimeline: React.FC = ({ circleId }) => { + const dispatch = useAppDispatch(); + const { openModal } = useModalsActions(); + + const handleLoadMore = () => { + dispatch(fetchCircleTimeline(circleId, true)); + }; + + const handleEditClick = () => { + openModal('CIRCLE_EDITOR', { circleId }); + }; + + const emptyMessage = ( +
+ +
+
+ +
+ ); + + return ( + + ); +}; + const CircleTimelinePage: React.FC = () => { const { circleId } = circleTimelineRoute.useParams(); @@ -32,6 +76,7 @@ const CircleTimelinePage: React.FC = () => { const dispatch = useAppDispatch(); const { openModal } = useModalsActions(); const navigate = useNavigate(); + const { experimentalTimeline } = useSettings(); const { data: circle, isFetching } = useCircle(circleId); const { mutate: deleteCircle } = useDeleteCircle(); @@ -40,10 +85,6 @@ const CircleTimelinePage: React.FC = () => { dispatch(fetchCircleTimeline(circleId)); }, [circleId]); - const handleLoadMore = () => { - dispatch(fetchCircleTimeline(circleId, true)); - }; - const handleEditClick = () => { openModal('CIRCLE_EDITOR', { circleId }); }; @@ -79,20 +120,6 @@ const CircleTimelinePage: React.FC = () => { return ; } - const emptyMessage = ( -
- -
-
- -
- ); - const items = [ { text: intl.formatMessage(messages.editCircle), @@ -116,14 +143,11 @@ const CircleTimelinePage: React.FC = () => { /> } > - + {experimentalTimeline ? ( + + ) : ( + + )} ); }; diff --git a/packages/nicolium/src/queries/timelines/use-timelines.ts b/packages/nicolium/src/queries/timelines/use-timelines.ts index ce28451d5..b526c18ed 100644 --- a/packages/nicolium/src/queries/timelines/use-timelines.ts +++ b/packages/nicolium/src/queries/timelines/use-timelines.ts @@ -5,6 +5,7 @@ import { useTimeline } from './use-timeline'; import type { AntennaTimelineParams, BubbleTimelineParams, + GetCircleStatusesParams, GroupTimelineParams, HashtagTimelineParams, HomeTimelineParams, @@ -46,7 +47,10 @@ const useHashtagTimeline = ( return useTimeline( `hashtag:${hashtag}`, (paginationParams) => - client.timelines.hashtagTimeline(hashtag, { ...params, ...paginationParams }), + client.timelines.hashtagTimeline(hashtag, { + ...params, + ...paginationParams, + }), { stream: 'hashtag', params: { tag: hashtag } }, ); }; @@ -84,7 +88,10 @@ const useGroupTimeline = ( return useTimeline( `group:${groupId}`, (paginationParams) => - client.timelines.groupTimeline(groupId, { ...params, ...paginationParams }), + client.timelines.groupTimeline(groupId, { + ...params, + ...paginationParams, + }), { stream: 'group', params: { group: groupId } }, ); }; @@ -106,14 +113,31 @@ const useAntennaTimeline = ( const client = useClient(); return useTimeline(`antenna:${antennaId}`, (paginationParams) => - client.timelines.antennaTimeline(antennaId, { ...params, ...paginationParams }), + client.timelines.antennaTimeline(antennaId, { + ...params, + ...paginationParams, + }), + ); +}; + +const useCircleTimeline = ( + circleId: string, + params?: Omit, +) => { + const client = useClient(); + + return useTimeline(`circle:${circleId}`, (paginationParams) => + client.circles.getCircleStatuses(circleId, { + ...params, + ...paginationParams, + }), ); }; const useWrenchedTimeline = (params?: Omit) => { const client = useClient(); - return useTimeline(`wrenched`, (paginationParams) => + return useTimeline('wrenched', (paginationParams) => client.timelines.wrenchedTimeline({ ...params, ...paginationParams }), ); }; @@ -127,5 +151,6 @@ export { useGroupTimeline, useBubbleTimeline, useAntennaTimeline, + useCircleTimeline, useWrenchedTimeline, }; From 420f42d16d09f13d2b18a2e34fbaec91531c4281 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 14:30:12 +0100 Subject: [PATCH 21/89] nicolium: experimental timelines: ux improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/columns/timeline.tsx | 24 +++++------ .../src/queries/timelines/use-timeline.ts | 6 +-- packages/nicolium/src/stores/timelines.ts | 43 ++++++++----------- .../nicolium/src/styles/new/statuses.scss | 14 +++++- 4 files changed, 45 insertions(+), 42 deletions(-) diff --git a/packages/nicolium/src/columns/timeline.tsx b/packages/nicolium/src/columns/timeline.tsx index b225e16c5..2ff447a69 100644 --- a/packages/nicolium/src/columns/timeline.tsx +++ b/packages/nicolium/src/columns/timeline.tsx @@ -3,7 +3,6 @@ import clsx from 'clsx'; import React, { useRef } from 'react'; import { defineMessages, FormattedList, FormattedMessage } from 'react-intl'; -import LoadMore from '@/components/load-more'; import ScrollTopButton from '@/components/scroll-top-button'; import ScrollableList from '@/components/scrollable-list'; import Status, { StatusFollowedTagInfo } from '@/components/statuses/status'; @@ -177,7 +176,6 @@ const TimelineStatus: React.FC = (props): React.JSX.Element =>
@@ -206,8 +204,16 @@ interface ITimeline { const Timeline: React.FC = ({ query, contextType = 'public' }) => { const node = useRef(null); - const { timelineId, entries, queuedCount, fetchNextPage, dequeueEntries, isFetching, isPending } = - query; + const { + timelineId, + entries, + queuedCount, + fetchNextPage, + dequeueEntries, + isFetching, + isPending, + hasNextPage, + } = query; const handleMoveUp = (index: number) => selectChild(index - 1, node, document.getElementById('status-list') ?? undefined); @@ -239,13 +245,6 @@ const Timeline: React.FC = ({ query, contextType = 'public' }) => { /> ); } - if ((entry.type === 'page-end' || entry.type === 'page-start') && !isFetching) { - return ( -
- -
- ); - } }; return ( @@ -266,7 +265,8 @@ const Timeline: React.FC = ({ query, contextType = 'public' }) => { placeholderComponent={() => } placeholderCount={20} ref={node} - hasMore + hasMore={hasNextPage} + onLoadMore={fetchNextPage} > {(entries || []).map(renderEntry)} diff --git a/packages/nicolium/src/queries/timelines/use-timeline.ts b/packages/nicolium/src/queries/timelines/use-timeline.ts index 5add2d601..1fb836c1b 100644 --- a/packages/nicolium/src/queries/timelines/use-timeline.ts +++ b/packages/nicolium/src/queries/timelines/use-timeline.ts @@ -38,11 +38,9 @@ const useTimeline = (timelineId: string, fetcher: TimelineFetcher, streamConfig? const fetchNextPage = useCallback(async () => { timelineActions.setLoading(timelineId, true); - const lastEntry = timeline.entries.at(-1); - if (!lastEntry || lastEntry.type !== 'page-end') return; try { - const response = await fetcher({ max_id: lastEntry.minId }); + const response = await fetcher({ max_id: timeline.oldestStatusId }); importEntities({ statuses: response.items }); @@ -50,7 +48,7 @@ const useTimeline = (timelineId: string, fetcher: TimelineFetcher, streamConfig? } catch (error) { // } - }, [timelineId, timeline.entries]); + }, [timelineId, timeline.oldestStatusId]); const dequeueEntries = useCallback(() => { timelineActions.dequeueEntries(timelineId); diff --git a/packages/nicolium/src/stores/timelines.ts b/packages/nicolium/src/stores/timelines.ts index 4ef9db422..11125161d 100644 --- a/packages/nicolium/src/stores/timelines.ts +++ b/packages/nicolium/src/stores/timelines.ts @@ -19,14 +19,6 @@ type TimelineEntry = type: 'gap'; sinceId: string; maxId: string; - } - | { - type: 'page-start'; - maxId?: string; - } - | { - type: 'page-end'; - minId?: string; }; interface TimelineData { @@ -35,6 +27,8 @@ interface TimelineData { queuedCount: number; isFetching: boolean; isPending: boolean; + hasNextPage: boolean; + oldestStatusId?: string; } interface State { @@ -43,8 +37,8 @@ interface State { expandTimeline: ( timelineId: string, statuses: Array, - hasMore: boolean, - initialFetch: boolean, + hasMore?: boolean, + initialFetch?: boolean, ) => void; receiveStreamingStatus: (timelineId: string, status: Status) => void; deleteStatus: (statusId: string) => void; @@ -53,7 +47,7 @@ interface State { }; } -const processPage = (statuses: Array, hasMore: boolean): Array => { +const processPage = (statuses: Array): Array => { const timelinePage: Array = []; const processStatus = (status: Status): boolean => { @@ -111,12 +105,6 @@ const processPage = (statuses: Array, hasMore: boolean): Array()( mutative((set) => ({ timelines: {} as Record, actions: { - expandTimeline: (timelineId, statuses, hasMore, initialFetch) => + expandTimeline: (timelineId, statuses, hasMore, initialFetch = false) => set((state) => { const timeline = state.timelines[timelineId] ?? createEmptyTimeline(); - const entries = processPage(statuses, hasMore); + const entries = processPage(statuses); - if (initialFetch) timeline.entries = []; - else if (timeline.entries.at(-1)?.type === 'page-end') timeline.entries.pop(); - timeline.entries.push(...entries); + if (initialFetch) timeline.entries = entries; + else timeline.entries.push(...entries); timeline.isPending = false; timeline.isFetching = false; + if (typeof hasMore === 'boolean') { + timeline.hasNextPage = hasMore; + const oldestStatus = statuses.at(-1); + if (oldestStatus) timeline.oldestStatusId = oldestStatus.id; + } state.timelines[timelineId] = timeline; }), receiveStreamingStatus: (timelineId, status) => { @@ -169,10 +161,11 @@ const useTimelinesStore = create()( }, setLoading: (timelineId, isFetching) => set((state) => { - const timeline = (state.timelines[timelineId] ??= createEmptyTimeline()); + const timeline = state.timelines[timelineId] ?? createEmptyTimeline(); timeline.isFetching = isFetching; if (!isFetching) timeline.isPending = false; + state.timelines[timelineId] = timeline; }), dequeueEntries: (timelineId) => set((state) => { @@ -180,7 +173,7 @@ const useTimelinesStore = create()( if (!timeline || timeline.queuedEntries.length === 0) return; - const processedEntries = processPage(timeline.queuedEntries, false); + const processedEntries = processPage(timeline.queuedEntries); timeline.entries.unshift(...processedEntries); timeline.queuedEntries = []; @@ -196,6 +189,8 @@ const createEmptyTimeline = (): TimelineData => ({ queuedCount: 0, isFetching: false, isPending: true, + hasNextPage: true, + oldestStatusId: undefined, }); const emptyTimeline = createEmptyTimeline(); diff --git a/packages/nicolium/src/styles/new/statuses.scss b/packages/nicolium/src/styles/new/statuses.scss index d2f11406f..cfbd99038 100644 --- a/packages/nicolium/src/styles/new/statuses.scss +++ b/packages/nicolium/src/styles/new/statuses.scss @@ -307,6 +307,16 @@ } } -.⁂-timeline-status--connected-bottom .status__content-wrapper { - padding-left: 54px; +.⁂-timeline-status { + &--connected-bottom .status__content-wrapper { + padding-left: 54px; + } + + &:not(.⁂-timeline-status--connected-bottom) { + @apply border-b border-solid border-gray-200 dark:border-gray-800; + } +} + +div:last-child > .⁂-timeline-status { + @apply border-b-0; } From 405400e85cc51f39f84eac4b5b0c45331f5051bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 14:34:18 +0100 Subject: [PATCH 22/89] nicolium: experimental timelines: account timeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/columns/timeline.tsx | 16 ++++++++++++++++ .../src/pages/accounts/account-timeline.tsx | 17 ++++++++++------- .../src/queries/timelines/use-timelines.ts | 18 ++++++++++++++++++ 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/packages/nicolium/src/columns/timeline.tsx b/packages/nicolium/src/columns/timeline.tsx index 2ff447a69..1089357d3 100644 --- a/packages/nicolium/src/columns/timeline.tsx +++ b/packages/nicolium/src/columns/timeline.tsx @@ -16,6 +16,7 @@ import { useFeatures } from '@/hooks/use-features'; import { useAccounts } from '@/queries/accounts/use-accounts'; import { type SelectedStatus, useStatus } from '@/queries/statuses/use-status'; import { + useAccountTimeline, useAntennaTimeline, useBubbleTimeline, useCircleTimeline, @@ -364,6 +365,20 @@ const WrenchedTimelineColumn = () => { return ; }; +interface IAccountTimelineColumn { + accountId: string; + excludeReplies?: boolean; +} + +const AccountTimelineColumn: React.FC = ({ + accountId, + excludeReplies = false, +}) => { + const timelineQuery = useAccountTimeline(accountId, { exclude_replies: excludeReplies }); + + return ; +}; + export { HomeTimelineColumn, PublicTimelineColumn, @@ -375,4 +390,5 @@ export { AntennaTimelineColumn, CircleTimelineColumn, WrenchedTimelineColumn, + AccountTimelineColumn, }; diff --git a/packages/nicolium/src/pages/accounts/account-timeline.tsx b/packages/nicolium/src/pages/accounts/account-timeline.tsx index 947e54260..c37c67648 100644 --- a/packages/nicolium/src/pages/accounts/account-timeline.tsx +++ b/packages/nicolium/src/pages/accounts/account-timeline.tsx @@ -2,6 +2,7 @@ import React, { useEffect } from 'react'; import { FormattedMessage } from 'react-intl'; import { fetchAccountTimeline } from '@/actions/timelines'; +import { AccountTimelineColumn } from '@/columns/timeline'; import MissingIndicator from '@/components/missing-indicator'; import StatusList from '@/components/statuses/status-list'; import Card, { CardBody } from '@/components/ui/card'; @@ -12,6 +13,7 @@ import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useAppSelector } from '@/hooks/use-app-selector'; import { useFeatures } from '@/hooks/use-features'; import { useAccountLookup } from '@/queries/accounts/use-account-lookup'; +import { usePinnedStatuses } from '@/queries/status-lists/use-pinned-statuses'; import { makeGetStatusIds } from '@/selectors'; import { useSettings } from '@/stores/settings'; @@ -32,12 +34,7 @@ const AccountTimelinePage: React.FC = () => { const statusIds = useAppSelector((state) => getStatusIds(state, { type: `account:${path}`, prefix: 'account_timeline' }), ); - const featuredStatusIds = useAppSelector((state) => - getStatusIds(state, { - type: `account:${account?.id}:with_replies:pinned`, - prefix: 'account_timeline', - }), - ); + const { data: featuredStatusIds } = usePinnedStatuses(account?.id || ''); const isBlocked = account?.relationship?.blocked_by && !features.blockersVisible; const isLoading = useAppSelector((state) => state.timelines[`account:${path}`]?.isLoading); @@ -83,7 +80,13 @@ const AccountTimelinePage: React.FC = () => { ); } - return ( + return settings.experimentalTimeline ? ( + + ) : ( , +) => { + const client = useClient(); + + return useTimeline( + `account:${accountId}${params?.exclude_replies ? ':exclude_replies' : ''}`, + (paginationParams) => + client.accounts.getAccountStatuses(accountId, { + ...params, + ...paginationParams, + }), + ); +}; + export { useHomeTimeline, usePublicTimeline, @@ -153,4 +170,5 @@ export { useAntennaTimeline, useCircleTimeline, useWrenchedTimeline, + useAccountTimeline, }; From c7f44899ee1e0934b93487730e1ee97a198a0ad9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 14:36:31 +0100 Subject: [PATCH 23/89] nicolium: migrate pins from redux MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/actions/interactions.ts | 11 ---------- .../statuses/use-status-interactions.ts | 14 ------------- packages/nicolium/src/reducers/timelines.ts | 21 +------------------ 3 files changed, 1 insertion(+), 45 deletions(-) delete mode 100644 packages/nicolium/src/actions/interactions.ts diff --git a/packages/nicolium/src/actions/interactions.ts b/packages/nicolium/src/actions/interactions.ts deleted file mode 100644 index fa7aaaa59..000000000 --- a/packages/nicolium/src/actions/interactions.ts +++ /dev/null @@ -1,11 +0,0 @@ -const PIN_SUCCESS = 'PIN_SUCCESS' as const; - -const UNPIN_SUCCESS = 'UNPIN_SUCCESS' as const; - -type InteractionsAction = { - type: typeof PIN_SUCCESS | typeof UNPIN_SUCCESS; - statusId: string; - accountId: string; -}; - -export { PIN_SUCCESS, UNPIN_SUCCESS, type InteractionsAction }; diff --git a/packages/nicolium/src/queries/statuses/use-status-interactions.ts b/packages/nicolium/src/queries/statuses/use-status-interactions.ts index 48a6fecb6..08f5f55c9 100644 --- a/packages/nicolium/src/queries/statuses/use-status-interactions.ts +++ b/packages/nicolium/src/queries/statuses/use-status-interactions.ts @@ -9,8 +9,6 @@ import { create } from 'mutative'; import { defineMessages, useIntl } from 'react-intl'; import { importEntities } from '@/actions/importer'; -import { PIN_SUCCESS, UNPIN_SUCCESS, type InteractionsAction } from '@/actions/interactions'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useClient } from '@/hooks/use-client'; import { useFeatures } from '@/hooks/use-features'; import { useOwnAccount } from '@/hooks/use-own-account'; @@ -429,7 +427,6 @@ const useUnbookmarkStatus = (statusId: string) => { const usePinStatus = (statusId: string) => { const client = useClient(); - const dispatch = useAppDispatch(); const queryClient = useQueryClient(); const { data: account } = useOwnAccount(); @@ -441,18 +438,12 @@ const usePinStatus = (statusId: string) => { onSuccess: (status) => { importEntities({ statuses: [status] }); queryClient.invalidateQueries({ queryKey: queryKeys.statusLists.pins(account!.id) }); - dispatch({ - type: PIN_SUCCESS, - statusId: status.id, - accountId: status.account.id, - }); }, }); }; const useUnpinStatus = (statusId: string) => { const client = useClient(); - const dispatch = useAppDispatch(); const queryClient = useQueryClient(); const { data: account } = useOwnAccount(); @@ -464,11 +455,6 @@ const useUnpinStatus = (statusId: string) => { onSuccess: (status) => { importEntities({ statuses: [status] }); queryClient.setQueryData(queryKeys.statusLists.pins(account!.id), filterById(statusId)); - dispatch({ - type: UNPIN_SUCCESS, - statusId: status.id, - accountId: status.account.id, - }); }, }); }; diff --git a/packages/nicolium/src/reducers/timelines.ts b/packages/nicolium/src/reducers/timelines.ts index 654149d53..54de24704 100644 --- a/packages/nicolium/src/reducers/timelines.ts +++ b/packages/nicolium/src/reducers/timelines.ts @@ -5,7 +5,6 @@ import { ACCOUNT_MUTE_SUCCESS, type AccountsAction, } from '@/actions/accounts'; -import { PIN_SUCCESS, UNPIN_SUCCESS, type InteractionsAction } from '@/actions/interactions'; import { STATUS_CREATE_REQUEST, STATUS_CREATE_SUCCESS, @@ -131,12 +130,6 @@ const expandNormalizedTimeline = ( if (!next) timeline.hasMore = false; - // Pinned timelines can be replaced entirely - if (timelineId.endsWith(':pinned')) { - timeline.items = newIds; - return; - } - if (newIds.length) { if (pos === 'end') { timeline.items = mergeStatusIds(newIds, timeline.items); @@ -343,7 +336,7 @@ const handleExpandFail = (state: State, timelineId: string) => { const timelines = ( state: State = initialState, - action: AccountsAction | InteractionsAction | StatusesAction | TimelineAction, + action: AccountsAction | StatusesAction | TimelineAction, ): State => { switch (action.type) { case STATUS_CREATE_REQUEST: @@ -404,18 +397,6 @@ const timelines = ( return create(state, (draft) => { updateTop(draft, action.timeline, action.top); }); - case PIN_SUCCESS: - return create(state, (draft) => { - updateTimeline(draft, `account:${action.accountId}:with_replies:pinned`, (timeline) => { - timeline.items = [...new Set([action.statusId, ...timeline.items])]; - }); - }); - case UNPIN_SUCCESS: - return create(state, (draft) => { - updateTimeline(draft, `account:${action.accountId}:with_replies:pinned`, (timeline) => { - timeline.items = timeline.items.filter((id) => id !== action.statusId); - }); - }); default: return state; } From 5955bba00840f0ef5e98444d79794ceaa824906d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 14:38:11 +0100 Subject: [PATCH 24/89] nicolium: it should be the other way MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/pages/accounts/account-timeline.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nicolium/src/pages/accounts/account-timeline.tsx b/packages/nicolium/src/pages/accounts/account-timeline.tsx index c37c67648..94a674bd2 100644 --- a/packages/nicolium/src/pages/accounts/account-timeline.tsx +++ b/packages/nicolium/src/pages/accounts/account-timeline.tsx @@ -83,7 +83,7 @@ const AccountTimelinePage: React.FC = () => { return settings.experimentalTimeline ? ( ) : ( From ed53f015d6930c39c3646e1332ae93e8302e4cbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 14:45:26 +0100 Subject: [PATCH 25/89] nicolium: update dep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/nicolium/package.json b/packages/nicolium/package.json index ad70ae65d..34d2d1bc1 100644 --- a/packages/nicolium/package.json +++ b/packages/nicolium/package.json @@ -121,7 +121,7 @@ "react-sparklines": "^1.7.0", "react-sticky-box": "^2.0.5", "react-swipeable-views": "^0.14.1", - "react-virtuoso": "^4.18.1", + "react-virtuoso": "^4.18.3", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0db851512..d309428a2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -281,8 +281,8 @@ importers: specifier: ^0.14.1 version: 0.14.1(react@19.2.4) react-virtuoso: - specifier: ^4.18.1 - version: 4.18.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: ^4.18.3 + version: 4.18.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) redux: specifier: ^5.0.1 version: 5.0.1 @@ -5356,8 +5356,8 @@ packages: peerDependencies: react: ^15.3.0 || ^16.0.0 || ^17.0.0 - react-virtuoso@4.18.1: - resolution: {integrity: sha512-KF474cDwaSb9+SJ380xruBB4P+yGWcVkcu26HtMqYNMTYlYbrNy8vqMkE+GpAApPPufJqgOLMoWMFG/3pJMXUA==} + react-virtuoso@4.18.3: + resolution: {integrity: sha512-fLz/peHAx4Eu0DLHurFEEI7Y6n5CqEoxBh04rgJM9yMuOJah2a9zWg/MUOmZLcp7zuWYorXq5+5bf3IRgkNvWg==} peerDependencies: react: '>=16 || >=17 || >= 18 || >= 19' react-dom: '>=16 || >=17 || >= 18 || >=19' @@ -11560,7 +11560,7 @@ snapshots: react-swipeable-views-utils: 0.14.1(react@19.2.4) warning: 4.0.3 - react-virtuoso@4.18.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + react-virtuoso@4.18.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) From 2c53f56ff7dfe3aa5490523421fbf916e7879791 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 15:17:39 +0100 Subject: [PATCH 26/89] nicolium: experimental timelines: block/mute side-effects, pending statuses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/actions/statuses.ts | 11 +++ packages/nicolium/src/columns/timeline.tsx | 15 +++ .../features/ui/components/pending-status.tsx | 10 +- .../src/queries/accounts/use-relationship.ts | 3 + packages/nicolium/src/stores/timelines.ts | 97 ++++++++++++++++++- 5 files changed, 133 insertions(+), 3 deletions(-) diff --git a/packages/nicolium/src/actions/statuses.ts b/packages/nicolium/src/actions/statuses.ts index 9da991396..541247ea5 100644 --- a/packages/nicolium/src/actions/statuses.ts +++ b/packages/nicolium/src/actions/statuses.ts @@ -8,6 +8,7 @@ import { useContextStore } from '@/stores/contexts'; import { useModalsStore } from '@/stores/modals'; import { usePendingStatusesStore } from '@/stores/pending-statuses'; import { useSettingsStore } from '@/stores/settings'; +import { useTimelinesStore } from '@/stores/timelines'; import { isLoggedIn } from '@/utils/auth'; import { shouldHaveCard } from '@/utils/status'; @@ -86,6 +87,7 @@ const createStatus = if (!params.preview) { usePendingStatusesStore.getState().actions.importStatus(params, idempotencyKey); useContextStore.getState().actions.importPendingStatus(params.in_reply_to_id, idempotencyKey); + useTimelinesStore.getState().actions.importPendingStatus(params, idempotencyKey); if (!editedId) { incrementReplyCount(params); } @@ -137,6 +139,12 @@ const createStatus = idempotencyKey, ); + if (status.scheduled_at === null) { + useTimelinesStore.getState().actions.replacePendingStatus(idempotencyKey, status.id); + } else { + useTimelinesStore.getState().actions.deletePendingStatus(idempotencyKey); + } + // Poll the backend for the updated card if (expectsCard) { const delay = 1000; @@ -161,6 +169,7 @@ const createStatus = }) .catch((error) => { usePendingStatusesStore.getState().actions.deleteStatus(idempotencyKey); + useTimelinesStore.getState().actions.deletePendingStatus(idempotencyKey); useContextStore .getState() .actions.deletePendingStatus(params.in_reply_to_id, idempotencyKey); @@ -235,6 +244,7 @@ const deleteStatus = .statuses.deleteStatus(statusId) .then((source) => { usePendingStatusesStore.getState().actions.deleteStatus(statusId); + useTimelinesStore.getState().actions.deleteStatus(statusId); updateStatus( statusId, (s) => { @@ -267,6 +277,7 @@ const deleteStatusFromGroup = .experimental.groups.deleteGroupStatus(statusId, groupId) .then(() => { usePendingStatusesStore.getState().actions.deleteStatus(statusId); + useTimelinesStore.getState().actions.deleteStatus(statusId); updateStatus( statusId, (s) => { diff --git a/packages/nicolium/src/columns/timeline.tsx b/packages/nicolium/src/columns/timeline.tsx index 1089357d3..0d2d4596d 100644 --- a/packages/nicolium/src/columns/timeline.tsx +++ b/packages/nicolium/src/columns/timeline.tsx @@ -12,6 +12,7 @@ import Icon from '@/components/ui/icon'; import Portal from '@/components/ui/portal'; import Emojify from '@/features/emoji/emojify'; import PlaceholderStatus from '@/features/placeholder/components/placeholder-status'; +import PendingStatus from '@/features/ui/components/pending-status'; import { useFeatures } from '@/hooks/use-features'; import { useAccounts } from '@/queries/accounts/use-accounts'; import { type SelectedStatus, useStatus } from '@/queries/statuses/use-status'; @@ -51,6 +52,18 @@ const PlaceholderTimelineStatus = () => (
); +interface ITimelinePendingStatus { + idempotencyKey: string; +} + +const TimelinePendingStatus: React.FC = ({ idempotencyKey }) => { + return ( +
+ +
+ ); +}; + interface ITimelineStatusInfo { status: SelectedStatus; rebloggedBy: Array; @@ -245,6 +258,8 @@ const Timeline: React.FC = ({ query, contextType = 'public' }) => { // variant={divideType === 'border' ? 'slim' : 'rounded'} /> ); + } else if (entry.type === 'pending-status') { + return ; } }; diff --git a/packages/nicolium/src/features/ui/components/pending-status.tsx b/packages/nicolium/src/features/ui/components/pending-status.tsx index 1bf62e810..284185d9a 100644 --- a/packages/nicolium/src/features/ui/components/pending-status.tsx +++ b/packages/nicolium/src/features/ui/components/pending-status.tsx @@ -1,3 +1,4 @@ +import { skipToken, useQuery } from '@tanstack/react-query'; import clsx from 'clsx'; import React from 'react'; @@ -11,7 +12,7 @@ import PlaceholderCard from '@/features/placeholder/components/placeholder-card' import PlaceholderMediaGallery from '@/features/placeholder/components/placeholder-media-gallery'; import QuotedStatus from '@/features/status/containers/quoted-status-container'; import { useOwnAccount } from '@/hooks/use-own-account'; -import { usePollQuery } from '@/queries/statuses/use-poll'; +import { queryKeys } from '@/queries/keys'; import { usePendingStatus } from '@/stores/pending-statuses'; import { buildStatus } from '../util/pending-status-builder'; @@ -19,6 +20,7 @@ import { buildStatus } from '../util/pending-status-builder'; import PollPreview from './poll-preview'; import type { NormalizedStatus as StatusEntity } from '@/normalizers/status'; +import type { Poll } from 'pl-api'; const shouldHaveCard = (pendingStatus: StatusEntity) => Boolean(/https?:\/\/\S*/.test(pendingStatus.content)); @@ -54,7 +56,11 @@ const PendingStatus: React.FC = ({ const status = pendingStatus && ownAccount ? buildStatus(ownAccount, pendingStatus, idempotencyKey) : null; - const { data: poll } = usePollQuery(status?.poll_id ?? ''); + const { data: poll } = useQuery({ + queryKey: queryKeys.statuses.polls.show(status?.poll_id ?? ''), + queryFn: skipToken, + enabled: !!status?.poll_id, + }); if (!status) return null; if (!ownAccount) return null; diff --git a/packages/nicolium/src/queries/accounts/use-relationship.ts b/packages/nicolium/src/queries/accounts/use-relationship.ts index 6a031de49..946384340 100644 --- a/packages/nicolium/src/queries/accounts/use-relationship.ts +++ b/packages/nicolium/src/queries/accounts/use-relationship.ts @@ -14,6 +14,7 @@ import { useLoggedIn } from '@/hooks/use-logged-in'; import { useOwnAccount } from '@/hooks/use-own-account'; import { queryKeys } from '@/queries/keys'; import { useContextsActions } from '@/stores/contexts'; +import { useTimelinesStore } from '@/stores/timelines'; import type { BlockAccountParams, @@ -192,6 +193,7 @@ const useBlockAccountMutation = (accountId: string) => { // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers filterContexts(data); + useTimelinesStore.getState().actions.filterTimelines(data.id); return dispatch({ type: ACCOUNT_BLOCK_SUCCESS, @@ -260,6 +262,7 @@ const useMuteAccountMutation = (accountId: string) => { // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers filterContexts(data); + useTimelinesStore.getState().actions.filterTimelines(data.id); return dispatch({ type: ACCOUNT_MUTE_SUCCESS, diff --git a/packages/nicolium/src/stores/timelines.ts b/packages/nicolium/src/stores/timelines.ts index 11125161d..e887f366e 100644 --- a/packages/nicolium/src/stores/timelines.ts +++ b/packages/nicolium/src/stores/timelines.ts @@ -1,7 +1,10 @@ import { create } from 'zustand'; import { mutative } from 'zustand-mutative'; -import type { Status } from 'pl-api'; +import { findStatuses } from '@/queries/statuses/use-status'; + +import type { NormalizedStatus } from '@/normalizers/status'; +import type { CreateStatusParams, Status } from 'pl-api'; type TimelineEntry = | { @@ -44,6 +47,10 @@ interface State { deleteStatus: (statusId: string) => void; setLoading: (timelineId: string, isFetching: boolean) => void; dequeueEntries: (timelineId: string) => void; + importPendingStatus: (params: CreateStatusParams, idempotencyKey: string) => void; + replacePendingStatus: (idempotencyKey: string, newId: string) => void; + deletePendingStatus: (idempotencyKey: string) => void; + filterTimelines: (accountId: string) => void; }; } @@ -108,6 +115,21 @@ const processPage = (statuses: Array): Array => { return timelinePage; }; +const getTimelinesForStatus = ( + status: Pick | Pick, +): Array => { + switch (status.visibility) { + case 'group': + return [`group:${'group' in status && status.group?.id}`]; + case 'direct': + return []; + case 'public': + return ['home', 'public:local', 'public', 'bubble']; + default: + return ['home']; + } +}; + const useTimelinesStore = create()( mutative((set) => ({ timelines: {} as Record, @@ -179,6 +201,79 @@ const useTimelinesStore = create()( timeline.queuedEntries = []; timeline.queuedCount = 0; }), + importPendingStatus: (params, idempotencyKey) => + set((state) => { + if (params.scheduled_at) return; + + const timelineIds = getTimelinesForStatus(params); + + for (const timelineId of timelineIds) { + const timeline = state.timelines[timelineId]; + if (!timeline) continue; + + if ( + timeline.entries.some((e) => e.type === 'pending-status' && e.id === idempotencyKey) + ) + continue; + + timeline.entries.unshift({ type: 'pending-status', id: idempotencyKey }); + } + }), + replacePendingStatus: (idempotencyKey, newId) => + set((state) => { + for (const timeline of Object.values(state.timelines)) { + const idx = timeline.entries.findIndex( + (e) => e.type === 'pending-status' && e.id === idempotencyKey, + ); + if (idx !== -1) { + timeline.entries[idx] = { + type: 'status', + id: newId, + rebloggedBy: [], + }; + } + } + }), + deletePendingStatus: (idempotencyKey) => + set((state) => { + for (const timeline of Object.values(state.timelines)) { + const idx = timeline.entries.findIndex( + (e) => e.type === 'pending-status' && e.id === idempotencyKey, + ); + if (idx !== -1) { + timeline.entries.splice(idx, 1); + } + } + }), + filterTimelines: (accountId) => + set((state) => { + const ownedStatuses = findStatuses( + (status: NormalizedStatus) => status.account_id === accountId, + ); + + const statusIdsToRemove = new Set(); + + for (const [, status] of ownedStatuses) { + statusIdsToRemove.add(status.id); + } + + for (const timeline of Object.values(state.timelines)) { + timeline.entries = timeline.entries.filter((entry) => { + if (entry.type !== 'status') return true; + if (statusIdsToRemove.has(entry.id)) return false; + + const index = entry.rebloggedBy.indexOf(accountId); + if (index !== -1) entry.rebloggedBy.splice(index, 1); + + return true; + }); + timeline.queuedEntries = timeline.queuedEntries.filter( + (status) => + status.account.id !== accountId && status.reblog?.account.id !== accountId, + ); + timeline.queuedCount = timeline.queuedEntries.length; + } + }), }, })), ); From 9c3c7c14d50fb1b884fdce1b25be99eed6546e09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 15:21:51 +0100 Subject: [PATCH 27/89] nicolium: remove unused MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/columns/timeline.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/nicolium/src/columns/timeline.tsx b/packages/nicolium/src/columns/timeline.tsx index 0d2d4596d..26cf76500 100644 --- a/packages/nicolium/src/columns/timeline.tsx +++ b/packages/nicolium/src/columns/timeline.tsx @@ -253,9 +253,7 @@ const Timeline: React.FC = ({ query, contextType = 'public' }) => { onMoveDown={() => handleMoveDown(index)} rebloggedBy={entry.rebloggedBy} timelineId={timelineId} - // contextType={timelineId} // showGroup={showGroup} - // variant={divideType === 'border' ? 'slim' : 'rounded'} /> ); } else if (entry.type === 'pending-status') { From e37b30d7c07203a1801d8327a39b44a377536f8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 15:26:26 +0100 Subject: [PATCH 28/89] nicolium: start removing the legacy timeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/actions/admin.ts | 4 - packages/nicolium/src/actions/events.ts | 11 -- packages/nicolium/src/actions/statuses.ts | 71 +--------- packages/nicolium/src/reducers/timelines.ts | 143 +------------------- 4 files changed, 3 insertions(+), 226 deletions(-) diff --git a/packages/nicolium/src/actions/admin.ts b/packages/nicolium/src/actions/admin.ts index 44861a11f..0f5f1b13d 100644 --- a/packages/nicolium/src/actions/admin.ts +++ b/packages/nicolium/src/actions/admin.ts @@ -6,7 +6,6 @@ import { useComposeStore } from '@/stores/compose'; import { useModalsStore } from '@/stores/modals'; import { filterBadges, getTagDiff } from '@/utils/badges'; -import { STATUS_FETCH_SOURCE_FAIL, type StatusesAction } from './statuses'; import { deleteFromTimelines } from './timelines'; import type { AppDispatch, RootState } from '@/store'; @@ -149,9 +148,6 @@ const redactStatus = (statusId: string) => (dispatch: AppDispatch, getState: () .getState() .actions.setComposeToStatus(status, poll, source, false, null, null, true); useModalsStore.getState().actions.openModal('COMPOSE'); - }) - .catch((error) => { - dispatch({ type: STATUS_FETCH_SOURCE_FAIL, error }); }); }; diff --git a/packages/nicolium/src/actions/events.ts b/packages/nicolium/src/actions/events.ts index 9af044226..05f1a540b 100644 --- a/packages/nicolium/src/actions/events.ts +++ b/packages/nicolium/src/actions/events.ts @@ -5,11 +5,6 @@ import { useComposeStore } from '@/stores/compose'; import toast from '@/toast'; import { importEntities } from './importer'; -import { - STATUS_FETCH_SOURCE_FAIL, - STATUS_FETCH_SOURCE_REQUEST, - STATUS_FETCH_SOURCE_SUCCESS, -} from './statuses'; import type { AppDispatch, RootState } from '@/store'; import type { CreateEventParams, Location, MediaAttachment } from 'pl-api'; @@ -95,21 +90,15 @@ const cancelEventCompose = () => { }; const initEventEdit = (statusId: string) => (dispatch: AppDispatch, getState: () => RootState) => { - dispatch({ type: STATUS_FETCH_SOURCE_REQUEST, statusId }); - return getClient(getState()) .statuses.getStatusSource(statusId) .then((response) => { - dispatch({ type: STATUS_FETCH_SOURCE_SUCCESS, statusId }); useComposeStore .getState() .actions.updateCompose(`compose-event-modal-${statusId}`, (draft) => { draft.text = response.text; }); return response; - }) - .catch((error) => { - dispatch({ type: STATUS_FETCH_SOURCE_FAIL, statusId, error }); }); }; diff --git a/packages/nicolium/src/actions/statuses.ts b/packages/nicolium/src/actions/statuses.ts index 541247ea5..23a7e48dc 100644 --- a/packages/nicolium/src/actions/statuses.ts +++ b/packages/nicolium/src/actions/statuses.ts @@ -17,17 +17,9 @@ import { deleteFromTimelines } from './timelines'; import type { NormalizedStatus as Status } from '@/normalizers/status'; import type { AppDispatch, RootState } from '@/store'; -import type { CreateStatusParams, Status as BaseStatus, ScheduledStatus } from 'pl-api'; +import type { CreateStatusParams, Status as BaseStatus } from 'pl-api'; import type { IntlShape } from 'react-intl'; -const STATUS_CREATE_REQUEST = 'STATUS_CREATE_REQUEST' as const; -const STATUS_CREATE_SUCCESS = 'STATUS_CREATE_SUCCESS' as const; -const STATUS_CREATE_FAIL = 'STATUS_CREATE_FAIL' as const; - -const STATUS_FETCH_SOURCE_REQUEST = 'STATUS_FETCH_SOURCE_REQUEST' as const; -const STATUS_FETCH_SOURCE_SUCCESS = 'STATUS_FETCH_SOURCE_SUCCESS' as const; -const STATUS_FETCH_SOURCE_FAIL = 'STATUS_FETCH_SOURCE_FAIL' as const; - const incrementReplyCount = ( params: Pick, ) => { @@ -91,13 +83,6 @@ const createStatus = if (!editedId) { incrementReplyCount(params); } - dispatch({ - type: STATUS_CREATE_REQUEST, - params, - idempotencyKey, - editing: !!editedId, - redacting, - }); } const client = getClient(getState()); @@ -124,14 +109,6 @@ const createStatus = queryClient.invalidateQueries(scheduledStatusesQueryOptions); } - dispatch({ - type: STATUS_CREATE_SUCCESS, - status, - params, - idempotencyKey, - editing: !!editedId, - }); - useContextStore .getState() .actions.deletePendingStatus( @@ -176,13 +153,6 @@ const createStatus = if (!editedId) { decrementReplyCount(params); } - dispatch({ - type: STATUS_CREATE_FAIL, - error, - params, - idempotencyKey, - editing: !!editedId, - }); throw error; }); }; @@ -195,17 +165,11 @@ const editStatus = (statusId: string) => (dispatch: AppDispatch, getState: () => ? queryClient.getQueryData(queryKeys.statuses.polls.show(status.poll_id)) : undefined; - dispatch({ type: STATUS_FETCH_SOURCE_REQUEST }); - return getClient(getState()) .statuses.getStatusSource(statusId) .then((response) => { - dispatch({ type: STATUS_FETCH_SOURCE_SUCCESS }); useComposeStore.getState().actions.setComposeToStatus(status, poll, response); useModalsStore.getState().actions.openModal('COMPOSE'); - }) - .catch((error) => { - dispatch({ type: STATUS_FETCH_SOURCE_FAIL, error }); }); }; @@ -386,39 +350,7 @@ const unfilterStatus = (statusId: string) => { ); }; -type StatusesAction = - | { - type: typeof STATUS_CREATE_REQUEST; - params: CreateStatusParams; - idempotencyKey: string; - editing: boolean; - redacting: boolean; - } - | { - type: typeof STATUS_CREATE_SUCCESS; - status: BaseStatus | ScheduledStatus; - params: CreateStatusParams; - idempotencyKey: string; - editing: boolean; - } - | { - type: typeof STATUS_CREATE_FAIL; - error: unknown; - params: CreateStatusParams; - idempotencyKey: string; - editing: boolean; - } - | { type: typeof STATUS_FETCH_SOURCE_REQUEST } - | { type: typeof STATUS_FETCH_SOURCE_SUCCESS } - | { type: typeof STATUS_FETCH_SOURCE_FAIL; error: unknown }; - export { - STATUS_CREATE_REQUEST, - STATUS_CREATE_SUCCESS, - STATUS_CREATE_FAIL, - STATUS_FETCH_SOURCE_REQUEST, - STATUS_FETCH_SOURCE_SUCCESS, - STATUS_FETCH_SOURCE_FAIL, createStatus, editStatus, fetchStatus, @@ -426,5 +358,4 @@ export { deleteStatusFromGroup, toggleMuteStatus, unfilterStatus, - type StatusesAction, }; diff --git a/packages/nicolium/src/reducers/timelines.ts b/packages/nicolium/src/reducers/timelines.ts index 54de24704..04f303cf3 100644 --- a/packages/nicolium/src/reducers/timelines.ts +++ b/packages/nicolium/src/reducers/timelines.ts @@ -1,15 +1,5 @@ import { create } from 'mutative'; -import { - ACCOUNT_BLOCK_SUCCESS, - ACCOUNT_MUTE_SUCCESS, - type AccountsAction, -} from '@/actions/accounts'; -import { - STATUS_CREATE_REQUEST, - STATUS_CREATE_SUCCESS, - type StatusesAction, -} from '@/actions/statuses'; import { TIMELINE_UPDATE, TIMELINE_DELETE, @@ -23,15 +13,8 @@ import { TIMELINE_SCROLL_TOP, type TimelineAction, } from '@/actions/timelines'; -import { findStatuses } from '@/queries/statuses/use-status'; -import type { NormalizedStatus as Status } from '@/normalizers/status'; -import type { - PaginatedResponse, - Status as BaseStatus, - Relationship, - CreateStatusParams, -} from 'pl-api'; +import type { PaginatedResponse, Status as BaseStatus } from 'pl-api'; type ImportPosition = 'start' | 'end'; @@ -213,37 +196,6 @@ const updateTop = (state: State, timelineId: string, top: boolean) => { }); }; -const isReblogOf = (reblog: Pick, status: Pick) => - reblog.reblog_id === status.id; - -const buildReferencesTo = (status: Pick): Array<[string]> => - findStatuses((reblog) => isReblogOf(reblog, status)).map(([id]) => [id]); - -// const filterTimeline = ( -// state: State, -// timelineId: string, -// relationship: Relationship, -// statuses: Record>, -// ) => { -// const timeline = state[timelineId]; -// if (!timeline) { -// return; -// } -// timeline.items = timeline.items.filter((id) => { -// const status = statuses[id]; -// return !(status && status.account_id === relationship.id); -// }); -// }; - -const filterTimelines = (state: State, relationship: Relationship) => { - const ownedStatuses = findStatuses((status) => status.account_id === relationship.id); - - for (const [, status] of ownedStatuses) { - const references = buildReferencesTo(status); - deleteStatus(state, status.id, references, relationship.id); - } -}; - const timelineDequeue = (state: State, timelineId: string) => { updateTimeline(state, timelineId, (timeline) => { const top = timeline.top; @@ -258,97 +210,13 @@ const timelineDequeue = (state: State, timelineId: string) => { }); }; -// const timelineDisconnect = (state: State, timelineId: string) => -// state.update(timelineId, TimelineRecord(), timeline => timeline.withMutations(timeline => { -// This is causing problems. Disable for now. -// https://gitlab.com/soapbox-pub/soapbox/-/issues/716 -// timeline.set('items', addStatusId(items, null)); -// })); - -const getTimelinesForStatus = ( - status: Pick | Pick, -) => { - switch (status.visibility) { - case 'group': - return [`group:${'group' in status && status.group?.id}`]; - case 'direct': - return ['direct']; - case 'public': - return ['home', 'public:local', 'public', 'bubble']; - default: - return ['home']; - } -}; - -// Given an OrderedSet of IDs, replace oldId with newId maintaining its position -const replaceId = (ids: Array, oldId: string, newId: string) => { - if (ids.includes(newId)) return false; - - let found = false; - const index = ids.indexOf(oldId); - - if (index > -1) { - ids[index] = newId; - found = true; - } - - return found; -}; - -const importPendingStatus = (state: State, params: CreateStatusParams, idempotencyKey: string) => { - const statusId = `末pending-${idempotencyKey}`; - - const timelineIds = getTimelinesForStatus(params); - - timelineIds.forEach((timelineId) => { - updateTimelineQueue(state, timelineId, statusId); - }); -}; - -const replacePendingStatus = (state: State, idempotencyKey: string, newId: string) => { - const oldId = `末pending-${idempotencyKey}`; - - // Loop through timelines and replace the pending status with the real one - for (const timelineId in state) { - const found = replaceId(state[timelineId].items, oldId, newId); - if (found) { - state[timelineId].queuedItems = state[timelineId].queuedItems.filter((id) => id !== oldId); - } else { - replaceId(state[timelineId].queuedItems, oldId, newId); - } - } -}; - -const importStatus = (state: State, status: BaseStatus, idempotencyKey: string) => { - replacePendingStatus(state, idempotencyKey, status.id); - - const timelineIds = getTimelinesForStatus(status); - - timelineIds.forEach((timelineId) => { - appendStatus(state, timelineId, status.id); - }); -}; - const handleExpandFail = (state: State, timelineId: string) => { setLoading(state, timelineId, false); setFailed(state, timelineId, true); }; -const timelines = ( - state: State = initialState, - action: AccountsAction | StatusesAction | TimelineAction, -): State => { +const timelines = (state: State = initialState, action: TimelineAction): State => { switch (action.type) { - case STATUS_CREATE_REQUEST: - if (action.params.scheduled_at) return state; - return create(state, (draft) => { - importPendingStatus(draft, action.params, action.idempotencyKey); - }); - case STATUS_CREATE_SUCCESS: - if ('params' in action.status || action.editing) return state; - return create(state, (draft) => { - importStatus(draft, action.status as BaseStatus, action.idempotencyKey); - }); case TIMELINE_EXPAND_REQUEST: return create(state, (draft) => { setLoading(draft, action.timeline, true); @@ -386,13 +254,6 @@ const timelines = ( return create(state, (draft) => { clearTimeline(draft, action.timeline); }); - case ACCOUNT_BLOCK_SUCCESS: - case ACCOUNT_MUTE_SUCCESS: - return create(state, (draft) => { - filterTimelines(draft, action.relationship); - }); - // case ACCOUNT_UNFOLLOW_SUCCESS: - // return filterTimeline(state, 'home', action.relationship, action.statuses); case TIMELINE_SCROLL_TOP: return create(state, (draft) => { updateTop(draft, action.timeline, action.top); From 4e80c123712bf454cfe49fbd1ebd3c7586cfd37e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 15:27:05 +0100 Subject: [PATCH 29/89] nicolium: remove unused MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/components/statuses/status-list.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/nicolium/src/components/statuses/status-list.tsx b/packages/nicolium/src/components/statuses/status-list.tsx index eb60c15e5..346803169 100644 --- a/packages/nicolium/src/components/statuses/status-list.tsx +++ b/packages/nicolium/src/components/statuses/status-list.tsx @@ -44,8 +44,6 @@ interface IStatusList extends Omit { isPartial?: boolean; /** Whether we expect an additional page of data. */ hasMore: boolean; - /** Message to display when the list is loaded but empty. */ - emptyMessage?: React.ReactNode; /** ID of the timeline in Redux. */ timelineId?: string; /** Whether to show group information. */ From 0bbcf18325cf7413049ffe9ad986917f7eb363ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 15:30:13 +0100 Subject: [PATCH 30/89] nicolium: experimental timeline: support empty timeline message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/columns/timeline.tsx | 28 ++++++++++--------- .../src/pages/timelines/antenna-timeline.tsx | 2 -- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/nicolium/src/columns/timeline.tsx b/packages/nicolium/src/columns/timeline.tsx index 26cf76500..3f8480d8a 100644 --- a/packages/nicolium/src/columns/timeline.tsx +++ b/packages/nicolium/src/columns/timeline.tsx @@ -4,7 +4,7 @@ import React, { useRef } from 'react'; import { defineMessages, FormattedList, FormattedMessage } from 'react-intl'; import ScrollTopButton from '@/components/scroll-top-button'; -import ScrollableList from '@/components/scrollable-list'; +import ScrollableList, { type IScrollableList } from '@/components/scrollable-list'; import Status, { StatusFollowedTagInfo } from '@/components/statuses/status'; import StatusInfo from '@/components/statuses/status-info'; import Tombstone from '@/components/statuses/tombstone'; @@ -210,7 +210,9 @@ const TimelineStatus: React.FC = (props): React.JSX.Element => ); }; -interface ITimeline { +type IBaseTimeline = Pick; + +interface ITimeline extends IBaseTimeline { query: ReturnType; contextType?: FilterContextType; } @@ -288,13 +290,13 @@ const Timeline: React.FC = ({ query, contextType = 'public' }) => { ); }; -const HomeTimelineColumn = () => { +const HomeTimelineColumn: React.FC = () => { const timelineQuery = useHomeTimeline(); return ; }; -interface IPublicTimelineColumn { +interface IPublicTimelineColumn extends IBaseTimeline { local?: boolean; remote?: boolean; instance?: string; @@ -306,7 +308,7 @@ const PublicTimelineColumn: React.FC = (params) => { return ; }; -interface IHashtagTimelineColumn { +interface IHashtagTimelineColumn extends IBaseTimeline { hashtag: string; } @@ -316,7 +318,7 @@ const HashtagTimelineColumn: React.FC = ({ hashtag }) => return ; }; -interface ILinkTimelineColumn { +interface ILinkTimelineColumn extends IBaseTimeline { url: string; } @@ -326,7 +328,7 @@ const LinkTimelineColumn: React.FC = ({ url }) => { return ; }; -interface IListTimelineColumn { +interface IListTimelineColumn extends IBaseTimeline { listId: string; } @@ -336,7 +338,7 @@ const ListTimelineColumn: React.FC = ({ listId }) => { return ; }; -interface IGroupTimelineColumn { +interface IGroupTimelineColumn extends IBaseTimeline { groupId: string; } @@ -346,13 +348,13 @@ const GroupTimelineColumn: React.FC = ({ groupId }) => { return ; }; -const BubbleTimelineColumn = () => { +const BubbleTimelineColumn: React.FC = () => { const timelineQuery = useBubbleTimeline(); return ; }; -interface IAntennaTimelineColumn { +interface IAntennaTimelineColumn extends IBaseTimeline { antennaId: string; } @@ -362,7 +364,7 @@ const AntennaTimelineColumn: React.FC = ({ antennaId }) return ; }; -interface ICircleTimelineColumn { +interface ICircleTimelineColumn extends IBaseTimeline { circleId: string; } @@ -372,13 +374,13 @@ const CircleTimelineColumn: React.FC = ({ circleId }) => return ; }; -const WrenchedTimelineColumn = () => { +const WrenchedTimelineColumn: React.FC = () => { const timelineQuery = useWrenchedTimeline(); return ; }; -interface IAccountTimelineColumn { +interface IAccountTimelineColumn extends IBaseTimeline { accountId: string; excludeReplies?: boolean; } diff --git a/packages/nicolium/src/pages/timelines/antenna-timeline.tsx b/packages/nicolium/src/pages/timelines/antenna-timeline.tsx index 9a168c97f..41f1564b4 100644 --- a/packages/nicolium/src/pages/timelines/antenna-timeline.tsx +++ b/packages/nicolium/src/pages/timelines/antenna-timeline.tsx @@ -48,8 +48,6 @@ const AntennaTimeline: React.FC = ({ antennaId }) => { id='empty_column.antenna' defaultMessage='There is nothing in this antenna yet. When posts matching the criteria will be created, they will appear here.' /> - {/*

- */}
); From 19ea9208e25198dba92df470d0e9180e95533598 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 15:40:53 +0100 Subject: [PATCH 31/89] nicolium: remove legacy timeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../src/features/preferences/index.tsx | 23 ----- packages/nicolium/src/locales/en.json | 2 - .../src/pages/accounts/account-timeline.tsx | 34 +------- .../src/pages/timelines/antenna-timeline.tsx | 60 +++---------- .../src/pages/timelines/bubble-timeline.tsx | 53 ++---------- .../src/pages/timelines/circle-timeline.tsx | 70 +++++----------- .../pages/timelines/community-timeline.tsx | 61 ++------------ .../src/pages/timelines/group-timeline.tsx | 56 +++---------- .../src/pages/timelines/hashtag-timeline.tsx | 56 +++---------- .../src/pages/timelines/home-timeline.tsx | 84 +++++++------------ .../src/pages/timelines/landing-timeline.tsx | 22 ++--- .../src/pages/timelines/link-timeline.tsx | 55 +++--------- .../src/pages/timelines/list-timeline.tsx | 79 ++++------------- .../src/pages/timelines/public-timeline.tsx | 56 +++---------- .../src/pages/timelines/remote-timeline.tsx | 62 +++----------- .../src/pages/timelines/wrenched-timeline.tsx | 48 ++--------- .../nicolium/src/schemas/frontend-settings.ts | 2 - 17 files changed, 164 insertions(+), 659 deletions(-) diff --git a/packages/nicolium/src/features/preferences/index.tsx b/packages/nicolium/src/features/preferences/index.tsx index 2107af605..5244aab20 100644 --- a/packages/nicolium/src/features/preferences/index.tsx +++ b/packages/nicolium/src/features/preferences/index.tsx @@ -887,29 +887,6 @@ const Preferences = () => { )} - - - - } - hint={ - - } - > - - - ); }; diff --git a/packages/nicolium/src/locales/en.json b/packages/nicolium/src/locales/en.json index 91562c4d0..7b6282771 100644 --- a/packages/nicolium/src/locales/en.json +++ b/packages/nicolium/src/locales/en.json @@ -1568,8 +1568,6 @@ "preferences.fields.display_media.default": "Hide posts marked as sensitive", "preferences.fields.display_media.hide_all": "Always hide media posts", "preferences.fields.display_media.show_all": "Always show posts", - "preferences.fields.experimental_timeline_hint": "It replaces the stable timeline experience and might not offer all features.", - "preferences.fields.experimental_timeline_label": "Enable experimental timeline", "preferences.fields.implicit_addressing_label": "Include mentions in post content when replying", "preferences.fields.interface_size": "Interface size", "preferences.fields.known_languages_label": "Languages you know", diff --git a/packages/nicolium/src/pages/accounts/account-timeline.tsx b/packages/nicolium/src/pages/accounts/account-timeline.tsx index 94a674bd2..46df165e3 100644 --- a/packages/nicolium/src/pages/accounts/account-timeline.tsx +++ b/packages/nicolium/src/pages/accounts/account-timeline.tsx @@ -4,20 +4,14 @@ import { FormattedMessage } from 'react-intl'; import { fetchAccountTimeline } from '@/actions/timelines'; import { AccountTimelineColumn } from '@/columns/timeline'; import MissingIndicator from '@/components/missing-indicator'; -import StatusList from '@/components/statuses/status-list'; import Card, { CardBody } from '@/components/ui/card'; import Spinner from '@/components/ui/spinner'; import Text from '@/components/ui/text'; import { profileRoute } from '@/features/ui/router'; import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useAppSelector } from '@/hooks/use-app-selector'; import { useFeatures } from '@/hooks/use-features'; import { useAccountLookup } from '@/queries/accounts/use-account-lookup'; import { usePinnedStatuses } from '@/queries/status-lists/use-pinned-statuses'; -import { makeGetStatusIds } from '@/selectors'; -import { useSettings } from '@/stores/settings'; - -const getStatusIds = makeGetStatusIds(); const AccountTimelinePage: React.FC = () => { const { username } = profileRoute.useParams(); @@ -25,20 +19,12 @@ const AccountTimelinePage: React.FC = () => { const dispatch = useAppDispatch(); const features = useFeatures(); - const settings = useSettings(); const { data: account, isPending } = useAccountLookup(username); - const path = withReplies ? `${account?.id}:with_replies` : account?.id; - const showPins = settings.account_timeline.shows.pinned && !withReplies; - const statusIds = useAppSelector((state) => - getStatusIds(state, { type: `account:${path}`, prefix: 'account_timeline' }), - ); - const { data: featuredStatusIds } = usePinnedStatuses(account?.id || ''); + const { data: _featuredStatusIds } = usePinnedStatuses(account?.id || ''); const isBlocked = account?.relationship?.blocked_by && !features.blockersVisible; - const isLoading = useAppSelector((state) => state.timelines[`account:${path}`]?.isLoading); - const hasMore = useAppSelector((state) => state.timelines[`account:${path}`]?.hasMore); const accountUsername = account?.username ?? username; @@ -52,12 +38,6 @@ const AccountTimelinePage: React.FC = () => { } }, [account?.id, withReplies]); - const handleLoadMore = () => { - if (account) { - dispatch(fetchAccountTimeline(account.id, { exclude_replies: !withReplies }, true)); - } - }; - if (!account && isPending) { return ; } else if (!account) { @@ -80,21 +60,11 @@ const AccountTimelinePage: React.FC = () => { ); } - return settings.experimentalTimeline ? ( + return ( - ) : ( - } diff --git a/packages/nicolium/src/pages/timelines/antenna-timeline.tsx b/packages/nicolium/src/pages/timelines/antenna-timeline.tsx index 41f1564b4..9a2cf07db 100644 --- a/packages/nicolium/src/pages/timelines/antenna-timeline.tsx +++ b/packages/nicolium/src/pages/timelines/antenna-timeline.tsx @@ -1,20 +1,16 @@ import { useNavigate } from '@tanstack/react-router'; -import React, { useEffect } from 'react'; +import React from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; -import { fetchAntennaTimeline } from '@/actions/timelines'; import { AntennaTimelineColumn } from '@/columns/timeline'; import DropdownMenu from '@/components/dropdown-menu'; import MissingIndicator from '@/components/missing-indicator'; // import Button from '@/components/ui/button'; import Column from '@/components/ui/column'; import Spinner from '@/components/ui/spinner'; -import Timeline from '@/features/ui/components/timeline'; import { antennaTimelineRoute } from '@/features/ui/router'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useAntenna, useDeleteAntenna } from '@/queries/accounts/use-antennas'; import { useModalsActions } from '@/stores/modals'; -import { useSettings } from '@/stores/settings'; const messages = defineMessages({ deleteHeading: { id: 'confirmations.delete_antenna.heading', defaultMessage: 'Delete antenna' }, @@ -27,47 +23,10 @@ const messages = defineMessages({ deleteAntenna: { id: 'antennas.delete', defaultMessage: 'Delete antenna' }, }); -interface IAntennaTimeline { - antennaId: string; -} - -const AntennaTimeline: React.FC = ({ antennaId }) => { - const dispatch = useAppDispatch(); - - useEffect(() => { - dispatch(fetchAntennaTimeline(antennaId)); - }, [antennaId]); - - const handleLoadMore = () => { - dispatch(fetchAntennaTimeline(antennaId, true)); - }; - - const emptyMessage = ( -
- -
- ); - - return ( - - ); -}; - const AntennaTimelinePage: React.FC = () => { const { antennaId } = antennaTimelineRoute.useParams(); const intl = useIntl(); - const { experimentalTimeline } = useSettings(); const { openModal } = useModalsActions(); const navigate = useNavigate(); @@ -132,11 +91,18 @@ const AntennaTimelinePage: React.FC = () => { /> } > - {experimentalTimeline ? ( - - ) : ( - - )} + + +
+ } + emptyMessageIcon={require('@phosphor-icons/core/regular/chat-centered-text.svg')} + /> ); }; diff --git a/packages/nicolium/src/pages/timelines/bubble-timeline.tsx b/packages/nicolium/src/pages/timelines/bubble-timeline.tsx index 3b6cfca7e..405a971fc 100644 --- a/packages/nicolium/src/pages/timelines/bubble-timeline.tsx +++ b/packages/nicolium/src/pages/timelines/bubble-timeline.tsx @@ -1,49 +1,19 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { fetchBubbleTimeline } from '@/actions/timelines'; -import { useBubbleStream } from '@/api/hooks/streaming/use-bubble-stream'; import { BubbleTimelineColumn } from '@/columns/timeline'; -import PullToRefresh from '@/components/pull-to-refresh'; import Column from '@/components/ui/column'; -import Timeline from '@/features/ui/components/timeline'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useFeatures } from '@/hooks/use-features'; -import { useSettings } from '@/stores/settings'; const messages = defineMessages({ title: { id: 'column.bubble', defaultMessage: 'Bubble timeline' }, }); -const BubbleTimeline = () => { - const dispatch = useAppDispatch(); - const features = useFeatures(); - const settings = useSettings(); - - const onlyMedia = settings.timelines.bubble?.other.onlyMedia ?? false; - - const timelineId = 'bubble'; - - const handleLoadMore = () => { - dispatch(fetchBubbleTimeline({ onlyMedia }, true)); - }; - - const handleRefresh = () => dispatch(fetchBubbleTimeline({ onlyMedia }, true)); - - useBubbleStream({ onlyMedia, enabled: features.bubbleTimelineStreaming }); - - useEffect(() => { - dispatch(fetchBubbleTimeline({ onlyMedia })); - }, [onlyMedia]); +const BubbleTimelinePage = () => { + const intl = useIntl(); return ( - - + { } emptyMessageIcon={require('@phosphor-icons/core/regular/chat-centered-text.svg')} /> - - ); -}; - -const BubbleTimelinePage = () => { - const intl = useIntl(); - - const settings = useSettings(); - const { experimentalTimeline } = settings; - - return ( - - {experimentalTimeline ? : } ); }; diff --git a/packages/nicolium/src/pages/timelines/circle-timeline.tsx b/packages/nicolium/src/pages/timelines/circle-timeline.tsx index c170266dc..0c71a1c16 100644 --- a/packages/nicolium/src/pages/timelines/circle-timeline.tsx +++ b/packages/nicolium/src/pages/timelines/circle-timeline.tsx @@ -9,12 +9,10 @@ import MissingIndicator from '@/components/missing-indicator'; import Button from '@/components/ui/button'; import Column from '@/components/ui/column'; import Spinner from '@/components/ui/spinner'; -import Timeline from '@/features/ui/components/timeline'; import { circleTimelineRoute } from '@/features/ui/router'; import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useCircle, useDeleteCircle } from '@/queries/accounts/use-circles'; import { useModalsActions } from '@/stores/modals'; -import { useSettings } from '@/stores/settings'; const messages = defineMessages({ deleteHeading: { id: 'confirmations.delete_circle.heading', defaultMessage: 'Delete circle' }, @@ -27,48 +25,6 @@ const messages = defineMessages({ deleteCircle: { id: 'circles.delete', defaultMessage: 'Delete circle' }, }); -interface ICircleTimeline { - circleId: string; -} - -const CircleTimeline: React.FC = ({ circleId }) => { - const dispatch = useAppDispatch(); - const { openModal } = useModalsActions(); - - const handleLoadMore = () => { - dispatch(fetchCircleTimeline(circleId, true)); - }; - - const handleEditClick = () => { - openModal('CIRCLE_EDITOR', { circleId }); - }; - - const emptyMessage = ( -
- -
-
- -
- ); - - return ( - - ); -}; - const CircleTimelinePage: React.FC = () => { const { circleId } = circleTimelineRoute.useParams(); @@ -76,7 +32,6 @@ const CircleTimelinePage: React.FC = () => { const dispatch = useAppDispatch(); const { openModal } = useModalsActions(); const navigate = useNavigate(); - const { experimentalTimeline } = useSettings(); const { data: circle, isFetching } = useCircle(circleId); const { mutate: deleteCircle } = useDeleteCircle(); @@ -143,11 +98,26 @@ const CircleTimelinePage: React.FC = () => { /> } > - {experimentalTimeline ? ( - - ) : ( - - )} + + +
+
+ + + } + emptyMessageIcon={require('@phosphor-icons/core/regular/chat-centered-text.svg')} + /> ); }; diff --git a/packages/nicolium/src/pages/timelines/community-timeline.tsx b/packages/nicolium/src/pages/timelines/community-timeline.tsx index 7f7776724..d80147d4f 100644 --- a/packages/nicolium/src/pages/timelines/community-timeline.tsx +++ b/packages/nicolium/src/pages/timelines/community-timeline.tsx @@ -1,55 +1,20 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { fetchPublicTimeline } from '@/actions/timelines'; -import { useCommunityStream } from '@/api/hooks/streaming/use-community-stream'; import { PublicTimelineColumn } from '@/columns/timeline'; -import PullToRefresh from '@/components/pull-to-refresh'; import Column from '@/components/ui/column'; -import Timeline from '@/features/ui/components/timeline'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useSettings } from '@/stores/settings'; const messages = defineMessages({ title: { id: 'column.community', defaultMessage: 'Local timeline' }, }); -interface ICommunityTimeline { - onTimelineFailed?: () => void; -} - -const CommunityTimeline: React.FC = ({ onTimelineFailed }) => { - const dispatch = useAppDispatch(); - const settings = useSettings(); - - const onlyMedia = settings.timelines['public:local']?.other.onlyMedia ?? false; - - const timelineId = 'public:local'; - - const handleLoadMore = () => { - dispatch( - fetchPublicTimeline({ onlyMedia, local: true }, true, undefined, () => { - onTimelineFailed?.(); - }), - ); - }; - - const handleRefresh = () => dispatch(fetchPublicTimeline({ onlyMedia, local: true }, true)); - - useCommunityStream({ onlyMedia }); - - useEffect(() => { - dispatch(fetchPublicTimeline({ onlyMedia, local: true })); - }, [onlyMedia]); +const CommunityTimelinePage = () => { + const intl = useIntl(); return ( - - + = ({ onTimelineFailed }) = } emptyMessageIcon={require('@phosphor-icons/core/regular/chat-centered-text.svg')} /> - - ); -}; - -const CommunityTimelinePage = () => { - const intl = useIntl(); - - const { experimentalTimeline } = useSettings(); - - return ( - - {experimentalTimeline ? : } ); }; -export { CommunityTimeline, CommunityTimelinePage as default }; +export { CommunityTimelinePage as default }; diff --git a/packages/nicolium/src/pages/timelines/group-timeline.tsx b/packages/nicolium/src/pages/timelines/group-timeline.tsx index c0bd20633..253fda046 100644 --- a/packages/nicolium/src/pages/timelines/group-timeline.tsx +++ b/packages/nicolium/src/pages/timelines/group-timeline.tsx @@ -3,55 +3,16 @@ import clsx from 'clsx'; import React, { useEffect, useRef } from 'react'; import { FormattedMessage } from 'react-intl'; -import { fetchGroupTimeline } from '@/actions/timelines'; -import { useGroupStream } from '@/api/hooks/streaming/use-group-stream'; import { GroupTimelineColumn } from '@/columns/timeline'; import Avatar from '@/components/ui/avatar'; import HStack from '@/components/ui/hstack'; import Stack from '@/components/ui/stack'; -import Timeline from '@/features/ui/components/timeline'; import { groupTimelineRoute } from '@/features/ui/router'; import { ComposeForm } from '@/features/ui/util/async-components'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useDraggedFiles } from '@/hooks/use-dragged-files'; import { useOwnAccount } from '@/hooks/use-own-account'; import { useGroupQuery } from '@/queries/groups/use-group'; import { useComposeActions, useUploadCompose } from '@/stores/compose'; -import { useSettings } from '@/stores/settings'; - -interface IGroupTimeline { - groupId: string; -} - -const GroupTimeline: React.FC = ({ groupId }) => { - const dispatch = useAppDispatch(); - - const handleLoadMore = () => { - dispatch(fetchGroupTimeline(groupId, {}, true)); - }; - - useGroupStream(groupId); - - useEffect(() => { - dispatch(fetchGroupTimeline(groupId, {})); - }, [groupId]); - - return ( - - } - emptyMessageIcon={require('@phosphor-icons/core/regular/chat-centered-text.svg')} - showGroup={false} - /> - ); -}; const GroupTimelinePage: React.FC = () => { const { groupId } = groupTimelineRoute.useParams(); @@ -59,7 +20,6 @@ const GroupTimelinePage: React.FC = () => { const composeId = `group:${groupId}`; const { data: account } = useOwnAccount(); - const { experimentalTimeline } = useSettings(); const uploadCompose = useUploadCompose(composeId); const { updateCompose } = useComposeActions(); const composer = useRef(null); @@ -111,11 +71,17 @@ const GroupTimelinePage: React.FC = () => { )} - {experimentalTimeline ? ( - - ) : ( - - )} + + } + emptyMessageIcon={require('@phosphor-icons/core/regular/chat-centered-text.svg')} + // showGroup={falsse} + /> ); }; diff --git a/packages/nicolium/src/pages/timelines/hashtag-timeline.tsx b/packages/nicolium/src/pages/timelines/hashtag-timeline.tsx index 386f31653..45dd07fcb 100644 --- a/packages/nicolium/src/pages/timelines/hashtag-timeline.tsx +++ b/packages/nicolium/src/pages/timelines/hashtag-timeline.tsx @@ -1,15 +1,11 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { FormattedMessage } from 'react-intl'; -import { fetchHashtagTimeline, clearTimeline } from '@/actions/timelines'; -import { useHashtagStream } from '@/api/hooks/streaming/use-hashtag-stream'; import { HashtagTimelineColumn } from '@/columns/timeline'; import List, { ListItem } from '@/components/list'; import Column from '@/components/ui/column'; import Toggle from '@/components/ui/toggle'; -import Timeline from '@/features/ui/components/timeline'; import { hashtagTimelineRoute } from '@/features/ui/router'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useFeatures } from '@/hooks/use-features'; import { useLoggedIn } from '@/hooks/use-logged-in'; import { @@ -17,47 +13,11 @@ import { useUnfollowHashtagMutation, } from '@/queries/hashtags/use-followed-tags'; import { useHashtag } from '@/queries/hashtags/use-hashtag'; -import { useSettings } from '@/stores/settings'; - -interface IHashtagTimeline { - hashtag: string; -} - -const HashtagTimeline: React.FC = ({ hashtag }) => { - const dispatch = useAppDispatch(); - - const handleLoadMore = () => { - dispatch(fetchHashtagTimeline(hashtag, {}, true)); - }; - - useHashtagStream(hashtag); - - useEffect(() => { - dispatch(clearTimeline(`hashtag:${hashtag}`)); - dispatch(fetchHashtagTimeline(hashtag)); - }, [hashtag]); - - return ( - - } - /> - ); -}; const HashtagTimelinePage: React.FC = () => { const { hashtag } = hashtagTimelineRoute.useParams(); const features = useFeatures(); - const { experimentalTimeline } = useSettings(); const { data: tag } = useHashtag(hashtag); const { isLoggedIn } = useLoggedIn(); @@ -84,11 +44,15 @@ const HashtagTimelinePage: React.FC = () => { )} - {experimentalTimeline ? ( - - ) : ( - - )} + + } + /> ); }; diff --git a/packages/nicolium/src/pages/timelines/home-timeline.tsx b/packages/nicolium/src/pages/timelines/home-timeline.tsx index 82b4d8bd1..b9887308a 100644 --- a/packages/nicolium/src/pages/timelines/home-timeline.tsx +++ b/packages/nicolium/src/pages/timelines/home-timeline.tsx @@ -1,66 +1,49 @@ -import React, { useCallback, useEffect, useRef } from 'react'; +import React from 'react'; import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; -import { fetchHomeTimeline } from '@/actions/timelines'; import { HomeTimelineColumn } from '@/columns/timeline'; import { Link } from '@/components/link'; -import PullToRefresh from '@/components/pull-to-refresh'; import Column from '@/components/ui/column'; import Stack from '@/components/ui/stack'; import Text from '@/components/ui/text'; -import Timeline from '@/features/ui/components/timeline'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useAppSelector } from '@/hooks/use-app-selector'; import { useFeatures } from '@/hooks/use-features'; import { useInstance } from '@/hooks/use-instance'; -import { useSettings } from '@/stores/settings'; const messages = defineMessages({ title: { id: 'column.home', defaultMessage: 'Home' }, }); -const HomeTimeline: React.FC = () => { - const dispatch = useAppDispatch(); +// TODO restore this +// const isPartial = useAppSelector((state) => state.timelines.home?.isPartial); + +// // Mastodon generates the feed in Redis, and can return a partial timeline +// // (HTTP 206) for new users. Poll until we get a full page of results. +// const checkIfReloadNeeded = useCallback((isPartial: boolean) => { +// if (isPartial) { +// polling.current = setInterval(() => { +// dispatch(fetchHomeTimeline()); +// }, 3000); +// } else if (polling.current) { +// clearInterval(polling.current); +// polling.current = null; +// } + +// return () => { +// if (polling.current) { +// clearInterval(polling.current); +// polling.current = null; +// } +// }; +// }, []); + +const HomeTimelinePage: React.FC = () => { + const intl = useIntl(); const features = useFeatures(); const instance = useInstance(); - const polling = useRef(null); - - const isPartial = useAppSelector((state) => state.timelines.home?.isPartial); - - // Mastodon generates the feed in Redis, and can return a partial timeline - // (HTTP 206) for new users. Poll until we get a full page of results. - const checkIfReloadNeeded = useCallback((isPartial: boolean) => { - if (isPartial) { - polling.current = setInterval(() => { - dispatch(fetchHomeTimeline()); - }, 3000); - } else if (polling.current) { - clearInterval(polling.current); - polling.current = null; - } - - return () => { - if (polling.current) { - clearInterval(polling.current); - polling.current = null; - } - }; - }, []); - - const handleLoadMore = useCallback(() => dispatch(fetchHomeTimeline(true)), []); - - const handleRefresh = useCallback(() => dispatch(fetchHomeTimeline(false)), []); - - useEffect(() => checkIfReloadNeeded(isPartial), [isPartial]); - return ( - - + @@ -100,17 +83,6 @@ const HomeTimeline: React.FC = () => { } emptyMessageIcon={require('@phosphor-icons/core/regular/chat-centered-text.svg')} /> - - ); -}; - -const HomeTimelinePage: React.FC = () => { - const intl = useIntl(); - const { experimentalTimeline } = useSettings(); - - return ( - - {experimentalTimeline ? : } ); }; diff --git a/packages/nicolium/src/pages/timelines/landing-timeline.tsx b/packages/nicolium/src/pages/timelines/landing-timeline.tsx index 8aed62096..2b8bef735 100644 --- a/packages/nicolium/src/pages/timelines/landing-timeline.tsx +++ b/packages/nicolium/src/pages/timelines/landing-timeline.tsx @@ -12,11 +12,8 @@ import Stack from '@/components/ui/stack'; import { useInstance } from '@/hooks/use-instance'; import { useRegistrationStatus } from '@/hooks/use-registration-status'; import { About } from '@/pages/utils/about'; -import { useSettings } from '@/stores/settings'; import { getTextDirection } from '@/utils/rtl'; -import { CommunityTimeline } from './community-timeline'; - interface ILogoText extends Pick, 'className' | 'dir'> { children: React.ReactNode; } @@ -55,9 +52,9 @@ const SiteBanner: React.FC = () => { const LandingTimelinePage = () => { const instance = useInstance(); const { isOpen } = useRegistrationStatus(); - const { experimentalTimeline } = useSettings(); - const [timelineFailed, setTimelineFailed] = useState(false); + // todo fix this + const [timelineFailed, _setTimelineFailed] = useState(false); const timelineEnabled = !instance.pleroma.metadata.restrict_unauthenticated.timelines.local; @@ -79,11 +76,16 @@ const LandingTimelinePage = () => { {timelineEnabled && !timelineFailed ? ( - experimentalTimeline ? ( - - ) : ( - setTimelineFailed(true)} /> - ) + + } + emptyMessageIcon={require('@phosphor-icons/core/regular/chat-centered-text.svg')} + /> ) : ( )} diff --git a/packages/nicolium/src/pages/timelines/link-timeline.tsx b/packages/nicolium/src/pages/timelines/link-timeline.tsx index a9a864347..a94281a8a 100644 --- a/packages/nicolium/src/pages/timelines/link-timeline.tsx +++ b/packages/nicolium/src/pages/timelines/link-timeline.tsx @@ -1,67 +1,34 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { clearTimeline, fetchLinkTimeline } from '@/actions/timelines'; import { LinkTimelineColumn } from '@/columns/timeline'; import Column from '@/components/ui/column'; -import Timeline from '@/features/ui/components/timeline'; import { linkTimelineRoute } from '@/features/ui/router'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useSettings } from '@/stores/settings'; const messages = defineMessages({ header: { id: 'column.link_timeline', defaultMessage: 'Posts linking to {url}' }, }); -interface ILinkTimeline { - url: string; -} - -const LinkTimeline: React.FC = ({ url }) => { - const dispatch = useAppDispatch(); - - const handleLoadMore = () => { - dispatch(fetchLinkTimeline(url, true)); - }; - - useEffect(() => { - dispatch(clearTimeline(`link:${url}`)); - dispatch(fetchLinkTimeline(url)); - }, [url]); - - return ( - - } - emptyMessageIcon={require('@phosphor-icons/core/regular/chat-centered-text.svg')} - /> - ); -}; - const LinkTimelinePage: React.FC = () => { const { url } = linkTimelineRoute.useParams(); const decodedUrl = decodeURIComponent(url || ''); const intl = useIntl(); - const { experimentalTimeline } = useSettings(); return ( - {experimentalTimeline ? ( - - ) : ( - - )} + + } + emptyMessageIcon={require('@phosphor-icons/core/regular/chat-centered-text.svg')} + /> ); }; diff --git a/packages/nicolium/src/pages/timelines/list-timeline.tsx b/packages/nicolium/src/pages/timelines/list-timeline.tsx index 0b8c8a157..5ba36796d 100644 --- a/packages/nicolium/src/pages/timelines/list-timeline.tsx +++ b/packages/nicolium/src/pages/timelines/list-timeline.tsx @@ -1,21 +1,16 @@ import { useNavigate } from '@tanstack/react-router'; -import React, { useEffect } from 'react'; +import React from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; -import { fetchListTimeline } from '@/actions/timelines'; -import { useListStream } from '@/api/hooks/streaming/use-list-stream'; import { ListTimelineColumn } from '@/columns/timeline'; import DropdownMenu from '@/components/dropdown-menu'; import MissingIndicator from '@/components/missing-indicator'; import Button from '@/components/ui/button'; import Column from '@/components/ui/column'; import Spinner from '@/components/ui/spinner'; -import Timeline from '@/features/ui/components/timeline'; import { listTimelineRoute } from '@/features/ui/router'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useDeleteList, useList } from '@/queries/accounts/use-lists'; import { useModalsActions } from '@/stores/modals'; -import { useSettings } from '@/stores/settings'; const messages = defineMessages({ deleteHeading: { id: 'confirmations.delete_list.heading', defaultMessage: 'Delete list' }, @@ -28,60 +23,10 @@ const messages = defineMessages({ deleteList: { id: 'lists.delete', defaultMessage: 'Delete list' }, }); -interface IListTimeline { - listId: string; -} - -const ListTimeline: React.FC = ({ listId }) => { - const dispatch = useAppDispatch(); - const settings = useSettings(); - const { openModal } = useModalsActions(); - - const onlyMedia = settings.timelines[`list:${listId}`]?.other.onlyMedia ?? false; - - const handleLoadMore = () => { - dispatch(fetchListTimeline(listId, true)); - }; - - useListStream(listId); - - useEffect(() => { - dispatch(fetchListTimeline(listId, false)); - }, [listId, onlyMedia]); - - const handleEditClick = () => { - openModal('LIST_EDITOR', { listId }); - }; - - return ( - - -
-
- - - } - emptyMessageIcon={require('@phosphor-icons/core/regular/list-bullets.svg')} - /> - ); -}; - const ListTimelinePage: React.FC = () => { const { listId } = listTimelineRoute.useParams(); const intl = useIntl(); - const { experimentalTimeline } = useSettings(); const { openModal } = useModalsActions(); const navigate = useNavigate(); @@ -146,11 +91,23 @@ const ListTimelinePage: React.FC = () => { /> } > - {experimentalTimeline ? ( - - ) : ( - - )} + + +
+
+ + + } + emptyMessageIcon={require('@phosphor-icons/core/regular/list-bullets.svg')} + /> ); }; diff --git a/packages/nicolium/src/pages/timelines/public-timeline.tsx b/packages/nicolium/src/pages/timelines/public-timeline.tsx index 8558dd59e..ad2a77112 100644 --- a/packages/nicolium/src/pages/timelines/public-timeline.tsx +++ b/packages/nicolium/src/pages/timelines/public-timeline.tsx @@ -1,16 +1,12 @@ import { Link } from '@tanstack/react-router'; -import React, { useEffect } from 'react'; +import React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { changeSetting } from '@/actions/settings'; -import { fetchPublicTimeline } from '@/actions/timelines'; -import { usePublicStream } from '@/api/hooks/streaming/use-public-stream'; import { PublicTimelineColumn } from '@/columns/timeline'; -import PullToRefresh from '@/components/pull-to-refresh'; import Accordion from '@/components/ui/accordion'; import Column from '@/components/ui/column'; import PinnedHostsPicker from '@/features/remote-timeline/components/pinned-hosts-picker'; -import Timeline from '@/features/ui/components/timeline'; import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useInstance } from '@/hooks/use-instance'; import { useSettings } from '@/stores/settings'; @@ -20,52 +16,12 @@ const messages = defineMessages({ dismiss: { id: 'fediverse_tab.explanation_box.dismiss', defaultMessage: "Don't show again" }, }); -const PublicTimeline = () => { - const dispatch = useAppDispatch(); - const settings = useSettings(); - const onlyMedia = settings.timelines.public?.other.onlyMedia ?? false; - - const timelineId = 'public'; - - const handleLoadMore = () => { - dispatch(fetchPublicTimeline({ onlyMedia }, true)); - }; - - const handleRefresh = () => dispatch(fetchPublicTimeline({ onlyMedia })); - - usePublicStream({ onlyMedia }); - - useEffect(() => { - dispatch(fetchPublicTimeline({ onlyMedia }, true)); - }, [onlyMedia]); - - return ( - - - } - emptyMessageIcon={require('@phosphor-icons/core/regular/chat-centered-text.svg')} - /> - - ); -}; - const PublicTimelinePage = () => { const dispatch = useAppDispatch(); const intl = useIntl(); const instance = useInstance(); const settings = useSettings(); - const { experimentalTimeline } = settings; const explanationBoxExpanded = settings.explanationBox; const showExplanationBox = settings.showExplanationBox; @@ -115,7 +71,15 @@ const PublicTimelinePage = () => { /> )} - {experimentalTimeline ? : } + + } + emptyMessageIcon={require('@phosphor-icons/core/regular/chat-centered-text.svg')} + /> ); }; diff --git a/packages/nicolium/src/pages/timelines/remote-timeline.tsx b/packages/nicolium/src/pages/timelines/remote-timeline.tsx index 26bc776dc..d35f781ae 100644 --- a/packages/nicolium/src/pages/timelines/remote-timeline.tsx +++ b/packages/nicolium/src/pages/timelines/remote-timeline.tsx @@ -1,63 +1,20 @@ import { useNavigate } from '@tanstack/react-router'; -import React, { useEffect } from 'react'; +import React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { fetchPublicTimeline } from '@/actions/timelines'; -import { useRemoteStream } from '@/api/hooks/streaming/use-remote-stream'; import { PublicTimelineColumn } from '@/columns/timeline'; import Column from '@/components/ui/column'; import HStack from '@/components/ui/hstack'; import IconButton from '@/components/ui/icon-button'; import Text from '@/components/ui/text'; import PinnedHostsPicker from '@/features/remote-timeline/components/pinned-hosts-picker'; -import Timeline from '@/features/ui/components/timeline'; import { remoteTimelineRoute } from '@/features/ui/router'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useSettings } from '@/stores/settings'; const messages = defineMessages({ close: { id: 'remote_timeline.close', defaultMessage: 'Close remote timeline' }, }); -interface IRemoteTimeline { - instance: string; -} - -const RemoteTimeline: React.FC = ({ instance }) => { - const dispatch = useAppDispatch(); - const settings = useSettings(); - - const timelineId = 'remote'; - const onlyMedia = settings.timelines.remote?.other.onlyMedia ?? false; - - const handleLoadMore = () => { - dispatch(fetchPublicTimeline({ onlyMedia, instance }, true)); - }; - - useRemoteStream({ instance, onlyMedia }); - - useEffect(() => { - dispatch(fetchPublicTimeline({ onlyMedia, instance })); - }, [onlyMedia, instance]); - - return ( - - } - emptyMessageIcon={require('@phosphor-icons/core/regular/chat-centered-text.svg')} - /> - ); -}; - /** View statuses from a remote instance. */ const RemoteTimelinePage: React.FC = () => { const { instance } = remoteTimelineRoute.useParams(); @@ -66,7 +23,6 @@ const RemoteTimelinePage: React.FC = () => { const navigate = useNavigate(); const settings = useSettings(); - const { experimentalTimeline } = settings; const pinned = settings.remote_timeline.pinnedHosts.includes(instance); @@ -97,11 +53,17 @@ const RemoteTimelinePage: React.FC = () => { )} - {experimentalTimeline ? ( - - ) : ( - - )} + + } + emptyMessageIcon={require('@phosphor-icons/core/regular/chat-centered-text.svg')} + instance={instance} + /> ); }; diff --git a/packages/nicolium/src/pages/timelines/wrenched-timeline.tsx b/packages/nicolium/src/pages/timelines/wrenched-timeline.tsx index 0b1d47d26..1ad5876a5 100644 --- a/packages/nicolium/src/pages/timelines/wrenched-timeline.tsx +++ b/packages/nicolium/src/pages/timelines/wrenched-timeline.tsx @@ -1,44 +1,19 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { fetchWrenchedTimeline } from '@/actions/timelines'; import { WrenchedTimelineColumn } from '@/columns/timeline'; -import PullToRefresh from '@/components/pull-to-refresh'; import Column from '@/components/ui/column'; -import Timeline from '@/features/ui/components/timeline'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useSettings } from '@/stores/settings'; const messages = defineMessages({ title: { id: 'column.wrenched', defaultMessage: 'Recent wrenches timeline' }, }); -const WrenchedTimeline = () => { - const dispatch = useAppDispatch(); - const settings = useSettings(); - - const onlyMedia = settings.timelines.wrenched?.other.onlyMedia ?? false; - - const timelineId = 'wrenched'; - - const handleLoadMore = () => { - dispatch(fetchWrenchedTimeline({ onlyMedia }, true)); - }; - - const handleRefresh = () => dispatch(fetchWrenchedTimeline({ onlyMedia }, true)); - - useEffect(() => { - dispatch(fetchWrenchedTimeline({ onlyMedia })); - }, [onlyMedia]); +const WrenchedTimelinePage = () => { + const intl = useIntl(); return ( - - + { } emptyMessageIcon={require('@phosphor-icons/core/regular/wrench.svg')} /> - - ); -}; - -const WrenchedTimelinePage = () => { - const intl = useIntl(); - - const settings = useSettings(); - const { experimentalTimeline } = settings; - - return ( - - {experimentalTimeline ? : } ); }; diff --git a/packages/nicolium/src/schemas/frontend-settings.ts b/packages/nicolium/src/schemas/frontend-settings.ts index 725795c40..8107f399b 100644 --- a/packages/nicolium/src/schemas/frontend-settings.ts +++ b/packages/nicolium/src/schemas/frontend-settings.ts @@ -122,8 +122,6 @@ const settingsSchema = v.object({ saved: v.fallback(v.boolean(), true), demo: v.fallback(v.boolean(), false), - - experimentalTimeline: v.fallback(v.boolean(), false), }); type Settings = v.InferOutput; From 44788e5bf8ed56705cf7d6bcde0638ec6350f74d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 15:41:54 +0100 Subject: [PATCH 32/89] nicolium: remove test timeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../src/features/ui/components/timeline.tsx | 85 ------------------- .../nicolium/src/features/ui/router/index.tsx | 9 -- .../src/features/ui/util/async-components.ts | 1 - .../src/pages/developers/developers.tsx | 14 --- .../src/pages/timelines/test-timeline.tsx | 52 ------------ 5 files changed, 161 deletions(-) delete mode 100644 packages/nicolium/src/features/ui/components/timeline.tsx delete mode 100644 packages/nicolium/src/pages/timelines/test-timeline.tsx diff --git a/packages/nicolium/src/features/ui/components/timeline.tsx b/packages/nicolium/src/features/ui/components/timeline.tsx deleted file mode 100644 index 0749c0194..000000000 --- a/packages/nicolium/src/features/ui/components/timeline.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import debounce from 'lodash/debounce'; -import React, { useCallback, useMemo } from 'react'; -import { defineMessages } from 'react-intl'; - -import { dequeueTimeline, scrollTopTimeline } from '@/actions/timelines'; -import ScrollTopButton from '@/components/scroll-top-button'; -import StatusList, { type IStatusList } from '@/components/statuses/status-list'; -import Portal from '@/components/ui/portal'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useAppSelector } from '@/hooks/use-app-selector'; -import { makeGetStatusIds } from '@/selectors'; - -const messages = defineMessages({ - queue: { - id: 'status_list.queue_label', - defaultMessage: 'Click to see {count} new {count, plural, one {post} other {posts}}', - }, - queueLiveRegion: { - id: 'status_list.queue_label.live_region', - defaultMessage: '{count} new {count, plural, one {post} other {posts}}.', - }, -}); - -interface ITimeline extends Omit { - /** Unique key to preserve the scroll position when navigating back. */ - scrollKey: string; - /** ID of the timeline in Redux. */ - timelineId: string; - /** Settings path to use instead of the timelineId. */ - prefix?: string; -} - -/** Scrollable list of statuses from a timeline in the Redux store. */ -const Timeline: React.FC = ({ timelineId, onLoadMore, prefix, ...rest }) => { - const dispatch = useAppDispatch(); - const getStatusIds = useMemo(makeGetStatusIds, []); - - const statusIds = useAppSelector((state) => getStatusIds(state, { type: timelineId, prefix })); - const lastStatusId = statusIds.at(-1); - const isLoading = useAppSelector((state) => state.timelines[timelineId]?.isLoading); - const isPartial = useAppSelector((state) => state.timelines[timelineId]?.isPartial || false); - const hasMore = useAppSelector((state) => state.timelines[timelineId]?.hasMore); - const totalQueuedItemsCount = useAppSelector( - (state) => state.timelines[timelineId]?.totalQueuedItemsCount || 0, - ); - - const handleDequeueTimeline = useCallback(() => { - dispatch(dequeueTimeline(timelineId, onLoadMore ? () => onLoadMore(lastStatusId!) : undefined)); - }, []); - - const handleScroll = useCallback( - debounce((startIndex?: number) => { - dispatch(scrollTopTimeline(timelineId, startIndex === 0)); - }, 100), - [timelineId], - ); - - return ( - <> - - - - - - - ); -}; - -export { Timeline as default }; diff --git a/packages/nicolium/src/features/ui/router/index.tsx b/packages/nicolium/src/features/ui/router/index.tsx index 1f01f29f0..8c291d951 100644 --- a/packages/nicolium/src/features/ui/router/index.tsx +++ b/packages/nicolium/src/features/ui/router/index.tsx @@ -144,7 +144,6 @@ import { Share, Status, Subscribers, - TestTimeline, ThemeEditor, Privacy, UserIndex, @@ -1211,13 +1210,6 @@ export const developersSettingsStoreRoute = createRoute({ beforeLoad: requireAuth, }); -export const developersTimelineRoute = createRoute({ - getParentRoute: () => layouts.default, - path: '/developers/timeline', - component: TestTimeline, - beforeLoad: requireAuth, -}); - export const developersSwRoute = createRoute({ getParentRoute: () => layouts.default, path: '/developers/sw', @@ -1519,7 +1511,6 @@ const routeTree = rootRoute.addChildren([ developersRoute, developersAppsRoute, developersSettingsStoreRoute, - developersTimelineRoute, developersSwRoute, cryptoDonateRoute, federationRestrictionsRoute, diff --git a/packages/nicolium/src/features/ui/util/async-components.ts b/packages/nicolium/src/features/ui/util/async-components.ts index 0158984cd..e0c21805a 100644 --- a/packages/nicolium/src/features/ui/util/async-components.ts +++ b/packages/nicolium/src/features/ui/util/async-components.ts @@ -107,7 +107,6 @@ export const SettingsStore = lazy(() => import('@/pages/developers/settings-stor export const Share = lazy(() => import('@/pages/utils/share')); export const Status = lazy(() => import('@/pages/statuses/status')); export const Subscribers = lazy(() => import('@/pages/account-lists/subscribers')); -export const TestTimeline = lazy(() => import('@/pages/timelines/test-timeline')); export const ThemeEditor = lazy(() => import('@/pages/dashboard/theme-editor')); export const Privacy = lazy(() => import('@/pages/settings/privacy')); export const UserIndex = lazy(() => import('@/pages/dashboard/user-index')); diff --git a/packages/nicolium/src/pages/developers/developers.tsx b/packages/nicolium/src/pages/developers/developers.tsx index f3ca35861..a6b5d2e90 100644 --- a/packages/nicolium/src/pages/developers/developers.tsx +++ b/packages/nicolium/src/pages/developers/developers.tsx @@ -82,20 +82,6 @@ const DevelopersPage: React.FC = () => { - - - - - - - - { - const intl = useIntl(); - const dispatch = useAppDispatch(); - - React.useEffect(() => { - importEntities({ statuses: MOCK_STATUSES }); - dispatch(expandTimelineSuccess(timelineId, MOCK_STATUSES, null, null, false)); - }, []); - - return ( - - - } - emptyMessageIcon={require('@phosphor-icons/core/regular/chat-centered-text.svg')} - /> - - ); -}; - -export { TestTimelinePage as default }; From 63c70d22d9d115333ad2388182f8ee3586a2851d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 15:43:20 +0100 Subject: [PATCH 33/89] nicolium: disable timeline reducer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/reducers/index.ts | 2 -- packages/nicolium/src/selectors/index.ts | 29 +----------------------- 2 files changed, 1 insertion(+), 30 deletions(-) diff --git a/packages/nicolium/src/reducers/index.ts b/packages/nicolium/src/reducers/index.ts index 26dc74b2e..b911627bd 100644 --- a/packages/nicolium/src/reducers/index.ts +++ b/packages/nicolium/src/reducers/index.ts @@ -10,7 +10,6 @@ import instance from './instance'; import me from './me'; import meta from './meta'; import pushNotifications from './push-notifications'; -import timelines from './timelines'; const reducers = { admin, @@ -20,7 +19,6 @@ const reducers = { me, meta, pushNotifications, - timelines, }; const appReducer = combineReducers(reducers); diff --git a/packages/nicolium/src/selectors/index.ts b/packages/nicolium/src/selectors/index.ts index f78d6ae27..36d7ded2d 100644 --- a/packages/nicolium/src/selectors/index.ts +++ b/packages/nicolium/src/selectors/index.ts @@ -1,13 +1,9 @@ import { createSelector } from 'reselect'; import { getAccounts } from '@/queries/accounts/selectors'; -import { queryClient } from '@/queries/client'; -import { queryKeys } from '@/queries/keys'; -import { useSettingsStore } from '@/stores/settings'; import { getDomain } from '@/utils/accounts'; import ConfigDB from '@/utils/config-db'; import { regexFromFilters } from '@/utils/filters'; -import { shouldFilter } from '@/utils/timelines'; import type { MRFSimple } from '@/schemas/pleroma'; import type { RootState } from '@/store'; @@ -72,27 +68,4 @@ const makeGetRemoteInstance = () => }), ); -type ColumnQuery = { type: string; prefix?: string }; - -const makeGetStatusIds = () => - createSelector( - [ - (_state: RootState, { type, prefix }: ColumnQuery) => - useSettingsStore.getState().settings.timelines[prefix ?? type], - (state: RootState, { type }: ColumnQuery) => state.timelines[type]?.items || [], - ], - (columnSettings, statusIds: Array) => - statusIds.filter((id: string) => { - const status = queryClient.getQueryData(queryKeys.statuses.show(id)); - if (!status) return true; - return !shouldFilter(status, columnSettings); - }), - ); - -export { - type RemoteInstance, - regexFromFilters, - makeGetHosts, - makeGetRemoteInstance, - makeGetStatusIds, -}; +export { type RemoteInstance, regexFromFilters, makeGetHosts, makeGetRemoteInstance }; From 324a625af9eb54dd1687885f55e0d9dea6f63fa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 15:47:34 +0100 Subject: [PATCH 34/89] nicolium: remove timelines actions/reducer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/actions/admin.ts | 5 +- packages/nicolium/src/actions/statuses.ts | 1 - packages/nicolium/src/actions/timelines.ts | 512 ------------------ .../api/hooks/streaming/use-user-stream.ts | 3 - packages/nicolium/src/features/ui/index.tsx | 3 - .../src/modals/report-modal/index.tsx | 10 +- .../src/pages/accounts/account-timeline.tsx | 15 +- .../src/pages/timelines/circle-timeline.tsx | 9 +- packages/nicolium/src/reducers/timelines.ts | 266 --------- packages/nicolium/src/utils/timelines.ts | 26 - 10 files changed, 7 insertions(+), 843 deletions(-) delete mode 100644 packages/nicolium/src/actions/timelines.ts delete mode 100644 packages/nicolium/src/reducers/timelines.ts delete mode 100644 packages/nicolium/src/utils/timelines.ts diff --git a/packages/nicolium/src/actions/admin.ts b/packages/nicolium/src/actions/admin.ts index 0f5f1b13d..0cb1e8ab6 100644 --- a/packages/nicolium/src/actions/admin.ts +++ b/packages/nicolium/src/actions/admin.ts @@ -4,10 +4,9 @@ import { queryClient } from '@/queries/client'; import { queryKeys } from '@/queries/keys'; import { useComposeStore } from '@/stores/compose'; import { useModalsStore } from '@/stores/modals'; +import { useTimelinesStore } from '@/stores/timelines'; import { filterBadges, getTagDiff } from '@/utils/badges'; -import { deleteFromTimelines } from './timelines'; - import type { AppDispatch, RootState } from '@/store'; import type { PleromaConfig } from 'pl-api'; @@ -73,7 +72,7 @@ const deleteStatus = (statusId: string) => (dispatch: AppDispatch, getState: () getClient(getState) .admin.statuses.deleteStatus(statusId) .then(() => { - dispatch(deleteFromTimelines(statusId)); + useTimelinesStore.getState().actions.deleteStatus(statusId); return { statusId }; }); diff --git a/packages/nicolium/src/actions/statuses.ts b/packages/nicolium/src/actions/statuses.ts index 23a7e48dc..5bfc84192 100644 --- a/packages/nicolium/src/actions/statuses.ts +++ b/packages/nicolium/src/actions/statuses.ts @@ -13,7 +13,6 @@ import { isLoggedIn } from '@/utils/auth'; import { shouldHaveCard } from '@/utils/status'; import { importEntities } from './importer'; -import { deleteFromTimelines } from './timelines'; import type { NormalizedStatus as Status } from '@/normalizers/status'; import type { AppDispatch, RootState } from '@/store'; diff --git a/packages/nicolium/src/actions/timelines.ts b/packages/nicolium/src/actions/timelines.ts deleted file mode 100644 index 35725d5cf..000000000 --- a/packages/nicolium/src/actions/timelines.ts +++ /dev/null @@ -1,512 +0,0 @@ -import { getLocale } from '@/actions/settings'; -import { getClient } from '@/api'; -import { queryClient } from '@/queries/client'; -import { queryKeys } from '@/queries/keys'; -import { findStatuses } from '@/queries/statuses/use-status'; -import { useComposeStore } from '@/stores/compose'; -import { useContextStore } from '@/stores/contexts'; -import { usePendingStatusesStore } from '@/stores/pending-statuses'; -import { useSettingsStore } from '@/stores/settings'; -import { shouldFilter } from '@/utils/timelines'; - -import { importEntities } from './importer'; - -import type { AppDispatch, RootState } from '@/store'; -import type { - Account as BaseAccount, - AntennaTimelineParams, - GetAccountStatusesParams, - GetCircleStatusesParams, - GroupTimelineParams, - HashtagTimelineParams, - HomeTimelineParams, - ListTimelineParams, - PaginatedResponse, - PublicTimelineParams, - Status as BaseStatus, - LinkTimelineParams, -} from 'pl-api'; - -const TIMELINE_UPDATE = 'TIMELINE_UPDATE' as const; -const TIMELINE_DELETE = 'TIMELINE_DELETE' as const; -const TIMELINE_CLEAR = 'TIMELINE_CLEAR' as const; -const TIMELINE_UPDATE_QUEUE = 'TIMELINE_UPDATE_QUEUE' as const; -const TIMELINE_DEQUEUE = 'TIMELINE_DEQUEUE' as const; -const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP' as const; - -const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST' as const; -const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS' as const; -const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL' as const; - -const MAX_QUEUED_ITEMS = 40; - -const processTimelineUpdate = - (timeline: string, status: BaseStatus) => (dispatch: AppDispatch, getState: () => RootState) => { - const me = getState().me; - const ownStatus = status.account?.id === me; - - const hasPendingStatuses = Object.keys(usePendingStatusesStore.getState().statuses).length > 0; - - const columnSettings = useSettingsStore.getState().settings.timelines[timeline]; - const shouldSkipQueue = shouldFilter( - { - in_reply_to_id: status.in_reply_to_id, - visibility: status.visibility, - reblog_id: status.reblog?.id ?? null, - }, - columnSettings, - ); - - if (ownStatus && hasPendingStatuses) { - // WebSockets push statuses without the Idempotency-Key, - // so if we have pending statuses, don't import it from here. - // We implement optimistic non-blocking statuses. - return; - } - - if (shouldSkipQueue) { - dispatch(updateTimeline(timeline, status.id)); - } else { - dispatch(updateTimelineQueue(timeline, status.id)); - } - }; - -const updateTimeline = (timeline: string, statusId: string) => ({ - type: TIMELINE_UPDATE, - timeline, - statusId, -}); - -const updateTimelineQueue = (timeline: string, statusId: string) => ({ - // if (typeof accept === 'function' && !accept(status)) { - // return; - // } - type: TIMELINE_UPDATE_QUEUE, - timeline, - statusId, -}); - -interface TimelineDequeueAction { - type: typeof TIMELINE_DEQUEUE; - timeline: string; -} - -const dequeueTimeline = - (timelineId: string, expandFunc?: () => void) => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const queuedCount = state.timelines[timelineId]?.totalQueuedItemsCount || 0; - - if (queuedCount <= 0) return; - - if (queuedCount <= MAX_QUEUED_ITEMS) { - dispatch({ type: TIMELINE_DEQUEUE, timeline: timelineId }); - return; - } - - if (typeof expandFunc === 'function') { - dispatch(clearTimeline(timelineId)); - expandFunc(); - } else if (timelineId === 'home') { - dispatch(clearTimeline(timelineId)); - dispatch(fetchHomeTimeline()); - } else if (timelineId === 'public:local') { - dispatch(clearTimeline(timelineId)); - dispatch(fetchPublicTimeline({ local: true })); - } - }; - -interface TimelineDeleteAction { - type: typeof TIMELINE_DELETE; - statusId: string; - accountId: string; - references: Array<[string, string]>; - reblogOf: string | null; -} - -const deleteFromTimelines = (statusId: string) => (dispatch: AppDispatch) => { - const status = queryClient.getQueryData(queryKeys.statuses.show(statusId)); - const accountId = status?.account_id ?? ''; - const references: Array<[string, string]> = findStatuses((s) => s.reblog_id === statusId).map( - ([id, s]) => [id, s.account_id], - ); - const reblogOf = status?.reblog_id ?? null; - - useContextStore.getState().actions.deleteStatuses([statusId]); - useComposeStore.getState().actions.handleTimelineDelete(statusId); - - // Remove statuses from RQ cache - references.forEach(([refId]) => - queryClient.removeQueries({ queryKey: queryKeys.statuses.show(refId) }), - ); - queryClient.removeQueries({ queryKey: queryKeys.statuses.show(statusId) }); - - dispatch({ - type: TIMELINE_DELETE, - statusId, - accountId, - references, - reblogOf, - }); -}; - -const clearTimeline = (timeline: string) => ({ type: TIMELINE_CLEAR, timeline }); - -const noOp = () => {}; - -const parseTags = (tags: Record = {}, mode: 'any' | 'all' | 'none') => - (tags[mode] || []).map((tag) => tag.value); - -const deduplicateStatuses = (statuses: Array) => { - const deduplicatedStatuses: Array }> = []; - - for (const status of statuses) { - const reblogged = - status.reblog && - deduplicatedStatuses.find( - (deduplicatedStatus) => deduplicatedStatus.reblog?.id === status.reblog?.id, - ); - - if (reblogged) { - reblogged.accounts.push(status.account); - reblogged.id += ':' + status.id; - } else if ( - !deduplicatedStatuses.some( - (deduplicatedStatus) => deduplicatedStatus.reblog?.id === status.id, - ) - ) { - deduplicatedStatuses.push({ accounts: [status.account], ...status }); - } - } - - return deduplicatedStatuses; -}; - -const handleTimelineExpand = - ( - timelineId: string, - fn: Promise>, - done = noOp, - onError?: (error: any) => void, - ) => - async (dispatch: AppDispatch) => { - dispatch(expandTimelineRequest(timelineId)); - - try { - const response = await fn; - - importEntities({ statuses: response.items }); - - const statuses = deduplicateStatuses(response.items); - importEntities({ statuses: statuses.filter((status) => status.accounts) }); - - dispatch( - expandTimelineSuccess( - timelineId, - statuses, - response.next, - response.previous, - response.partial, - ), - ); - done(); - } catch (error) { - dispatch(expandTimelineFail(timelineId, error)); - done(); - onError?.(error); - } - }; - -const fetchHomeTimeline = - (expand = false, done = noOp) => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - - const params: HomeTimelineParams = {}; - if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); - - if (expand && state.timelines.home?.isLoading) return; - - const fn = - (expand && state.timelines.home?.next?.()) || getClient(state).timelines.homeTimeline(params); - - return dispatch(handleTimelineExpand('home', fn, done)); - }; - -const fetchPublicTimeline = - ( - { onlyMedia, local, instance }: Record = {}, - expand = false, - done = noOp, - onError = noOp, - ) => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const timelineId = `${instance ? 'remote' : 'public'}${local ? ':local' : ''}${onlyMedia ? ':media' : ''}${instance ? `:${instance}` : ''}`; - - const params: PublicTimelineParams = { - only_media: onlyMedia, - local: instance ? false : local, - instance, - }; - if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); - - if (expand && state.timelines[timelineId]?.isLoading) return; - - const fn = - (expand && state.timelines[timelineId]?.next?.()) || - getClient(state).timelines.publicTimeline(params); - - return dispatch(handleTimelineExpand(timelineId, fn, done, onError)); - }; - -const fetchBubbleTimeline = - ({ onlyMedia }: Record = {}, expand = false, done = noOp) => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const timelineId = `bubble${onlyMedia ? ':media' : ''}`; - - const params: PublicTimelineParams = { only_media: onlyMedia }; - if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); - - if (expand && state.timelines[timelineId]?.isLoading) return; - - const fn = - (expand && state.timelines[timelineId]?.next?.()) || - getClient(state).timelines.bubbleTimeline(params); - - return dispatch(handleTimelineExpand(timelineId, fn, done)); - }; - -const fetchWrenchedTimeline = - ({ onlyMedia }: Record = {}, expand = false, done = noOp) => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const timelineId = `wrenched${onlyMedia ? ':media' : ''}`; - - const params: PublicTimelineParams = { only_media: onlyMedia }; - if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); - - if (expand && state.timelines[timelineId]?.isLoading) return; - - const fn = - (expand && state.timelines[timelineId]?.next?.()) || - getClient(state).timelines.wrenchedTimeline(params); - - return dispatch(handleTimelineExpand(timelineId, fn, done)); - }; - -const fetchAccountTimeline = - ( - accountId: string, - { exclude_replies, pinned, only_media, limit }: Record = {}, - expand = false, - done = noOp, - ) => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const timelineId = `account:${accountId}${!exclude_replies ? ':with_replies' : ''}${pinned ? ':pinned' : only_media ? ':media' : ''}`; - - const params: GetAccountStatusesParams = { exclude_replies, pinned, only_media, limit }; - if (pinned || only_media) params.with_muted = true; - if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); - - if (!expand && state.timelines[timelineId]?.loaded) return; - if (expand && state.timelines[timelineId]?.isLoading) return; - - const fn = - (expand && state.timelines[timelineId]?.next?.()) || - getClient(state).accounts.getAccountStatuses(accountId, params); - - return dispatch(handleTimelineExpand(timelineId, fn, done)); - }; - -const fetchListTimeline = - (listId: string, expand = false, done = noOp) => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const timelineId = `list:${listId}`; - - const params: ListTimelineParams = {}; - if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); - - if (expand && state.timelines[timelineId]?.isLoading) return; - - const fn = - (expand && state.timelines[timelineId]?.next?.()) || - getClient(state).timelines.listTimeline(listId, params); - - return dispatch(handleTimelineExpand(timelineId, fn, done)); - }; - -const fetchCircleTimeline = - (circleId: string, expand = false, done = noOp) => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const timelineId = `circle:${circleId}`; - - const params: GetCircleStatusesParams = {}; - // if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); - - if (expand && state.timelines[timelineId]?.isLoading) return; - - const fn = - (expand && state.timelines[timelineId]?.next?.()) || - getClient(state).circles.getCircleStatuses(circleId, params); - - return dispatch(handleTimelineExpand(timelineId, fn, done)); - }; - -const fetchAntennaTimeline = - (antennaId: string, expand = false, done = noOp) => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const timelineId = `antenna:${antennaId}`; - - const params: AntennaTimelineParams = {}; - // if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); - - if (expand && state.timelines[timelineId]?.isLoading) return; - - const fn = - (expand && state.timelines[timelineId]?.next?.()) || - getClient(state).timelines.antennaTimeline(antennaId, params); - - return dispatch(handleTimelineExpand(timelineId, fn, done)); - }; - -const fetchGroupTimeline = - (groupId: string, { only_media, limit }: Record = {}, expand = false, done = noOp) => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const timelineId = `group:${groupId}${only_media ? ':media' : ''}`; - - const params: GroupTimelineParams = { only_media, limit }; - if (only_media) params.with_muted = true; - if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); - - if (expand && state.timelines[timelineId]?.isLoading) return; - - const fn = - (expand && state.timelines[timelineId]?.next?.()) || - getClient(state).timelines.groupTimeline(groupId, params); - - return dispatch(handleTimelineExpand(timelineId, fn, done)); - }; - -const fetchHashtagTimeline = - (hashtag: string, { tags }: Record = {}, expand = false, done = noOp) => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const timelineId = `hashtag:${hashtag}`; - - const params: HashtagTimelineParams = { - any: parseTags(tags, 'any'), - all: parseTags(tags, 'all'), - none: parseTags(tags, 'none'), - }; - - if (expand && state.timelines[timelineId]?.isLoading) return; - - if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); - - const fn = - (expand && state.timelines[timelineId]?.next?.()) || - getClient(state).timelines.hashtagTimeline(hashtag, params); - - return dispatch(handleTimelineExpand(timelineId, fn, done)); - }; - -const fetchLinkTimeline = - (url: string, expand = false, done = noOp) => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const timelineId = `link:${url}`; - - const params: LinkTimelineParams = {}; - - if (expand && state.timelines[timelineId]?.isLoading) return; - - if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); - - const fn = - (expand && state.timelines[timelineId]?.next?.()) || - getClient(state).timelines.linkTimeline(url, params); - - return dispatch(handleTimelineExpand(timelineId, fn, done)); - }; - -const expandTimelineRequest = (timeline: string) => ({ - type: TIMELINE_EXPAND_REQUEST, - timeline, -}); - -const expandTimelineSuccess = ( - timeline: string, - statuses: Array, - next: (() => Promise>) | null, - prev: (() => Promise>) | null, - partial: boolean, -) => ({ - type: TIMELINE_EXPAND_SUCCESS, - timeline, - statuses, - next, - prev, - partial, -}); - -const expandTimelineFail = (timeline: string, error: unknown) => ({ - type: TIMELINE_EXPAND_FAIL, - timeline, - error, -}); - -const scrollTopTimeline = (timeline: string, top: boolean) => ({ - type: TIMELINE_SCROLL_TOP, - timeline, - top, -}); - -// TODO: other actions -type TimelineAction = - | ReturnType - | TimelineDeleteAction - | ReturnType - | ReturnType - | TimelineDequeueAction - | ReturnType - | ReturnType - | ReturnType - | ReturnType; - -export { - TIMELINE_UPDATE, - TIMELINE_DELETE, - TIMELINE_CLEAR, - TIMELINE_UPDATE_QUEUE, - TIMELINE_DEQUEUE, - TIMELINE_SCROLL_TOP, - TIMELINE_EXPAND_REQUEST, - TIMELINE_EXPAND_SUCCESS, - TIMELINE_EXPAND_FAIL, - MAX_QUEUED_ITEMS, - processTimelineUpdate, - dequeueTimeline, - deleteFromTimelines, - clearTimeline, - fetchHomeTimeline, - fetchPublicTimeline, - fetchBubbleTimeline, - fetchWrenchedTimeline, - fetchAccountTimeline, - fetchListTimeline, - fetchCircleTimeline, - fetchAntennaTimeline, - fetchGroupTimeline, - fetchHashtagTimeline, - fetchLinkTimeline, - expandTimelineSuccess, - scrollTopTimeline, - type TimelineAction, -}; diff --git a/packages/nicolium/src/api/hooks/streaming/use-user-stream.ts b/packages/nicolium/src/api/hooks/streaming/use-user-stream.ts index d7d8fdb3b..d46fe8b82 100644 --- a/packages/nicolium/src/api/hooks/streaming/use-user-stream.ts +++ b/packages/nicolium/src/api/hooks/streaming/use-user-stream.ts @@ -1,7 +1,6 @@ import { useCallback } from 'react'; import { importEntities } from '@/actions/importer'; -import { deleteFromTimelines, processTimelineUpdate } from '@/actions/timelines'; import { useStatContext } from '@/contexts/stat-context'; import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useLoggedIn } from '@/hooks/use-logged-in'; @@ -113,7 +112,6 @@ const useUserStream = () => { switch (event.event) { case 'update': { const timelineId = getTimelineFromStream(event.stream); - dispatch(processTimelineUpdate(getTimelineFromStream(event.stream), event.payload)); importEntities({ statuses: [event.payload] }); useTimelinesStore.getState().actions.receiveStreamingStatus(timelineId, event.payload); break; @@ -122,7 +120,6 @@ const useUserStream = () => { importEntities({ statuses: [event.payload] }); break; case 'delete': - dispatch(deleteFromTimelines(event.payload)); useTimelinesStore.getState().actions.deleteStatus(event.payload); break; case 'notification': diff --git a/packages/nicolium/src/features/ui/index.tsx b/packages/nicolium/src/features/ui/index.tsx index 8153538fa..ca314889e 100644 --- a/packages/nicolium/src/features/ui/index.tsx +++ b/packages/nicolium/src/features/ui/index.tsx @@ -5,7 +5,6 @@ import { Toaster } from 'react-hot-toast'; import { fetchConfig } from '@/actions/admin'; import { register as registerPushNotifications } from '@/actions/push-notifications/registerer'; -import { fetchHomeTimeline } from '@/actions/timelines'; import { useUserStream } from '@/api/hooks/streaming/use-user-stream'; import SidebarNavigation from '@/components/navigation/sidebar-navigation'; import ThumbNavigation from '@/components/navigation/thumb-navigation'; @@ -91,8 +90,6 @@ const UI: React.FC = React.memo(() => { prefetchCustomEmojis(client); - dispatch(fetchHomeTimeline()); - if (account.is_admin && features.pleromaAdminAccounts) { dispatch(fetchConfig()); } diff --git a/packages/nicolium/src/modals/report-modal/index.tsx b/packages/nicolium/src/modals/report-modal/index.tsx index 85a3da6b1..304dd577b 100644 --- a/packages/nicolium/src/modals/report-modal/index.tsx +++ b/packages/nicolium/src/modals/report-modal/index.tsx @@ -1,8 +1,7 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { submitReport, ReportableEntities } from '@/actions/reports'; -import { fetchAccountTimeline } from '@/actions/timelines'; import AttachmentThumbs from '@/components/media/attachment-thumbs'; import StatusContent from '@/components/statuses/status-content'; import Modal from '@/components/ui/modal'; @@ -15,6 +14,7 @@ import { useInstance } from '@/hooks/use-instance'; import { useAccount } from '@/queries/accounts/use-account'; import { useBlockAccountMutation } from '@/queries/accounts/use-relationship'; import { useMinimalStatus } from '@/queries/statuses/use-status'; +import { useAccountTimeline } from '@/queries/timelines/use-timelines'; import ConfirmationStep from './steps/confirmation-step'; import OtherActionsStep from './steps/other-actions-step'; @@ -230,11 +230,7 @@ const ReportModal: React.FC = ({ } }, [currentStep]); - useEffect(() => { - if (account?.id) { - dispatch(fetchAccountTimeline(account.id, { exclude_replies: false })); - } - }, [account?.id]); + useAccountTimeline(accountId, { exclude_replies: false }); if (!account) { return null; diff --git a/packages/nicolium/src/pages/accounts/account-timeline.tsx b/packages/nicolium/src/pages/accounts/account-timeline.tsx index 46df165e3..845288d3f 100644 --- a/packages/nicolium/src/pages/accounts/account-timeline.tsx +++ b/packages/nicolium/src/pages/accounts/account-timeline.tsx @@ -1,14 +1,12 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { FormattedMessage } from 'react-intl'; -import { fetchAccountTimeline } from '@/actions/timelines'; import { AccountTimelineColumn } from '@/columns/timeline'; import MissingIndicator from '@/components/missing-indicator'; import Card, { CardBody } from '@/components/ui/card'; import Spinner from '@/components/ui/spinner'; import Text from '@/components/ui/text'; import { profileRoute } from '@/features/ui/router'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useFeatures } from '@/hooks/use-features'; import { useAccountLookup } from '@/queries/accounts/use-account-lookup'; import { usePinnedStatuses } from '@/queries/status-lists/use-pinned-statuses'; @@ -17,7 +15,6 @@ const AccountTimelinePage: React.FC = () => { const { username } = profileRoute.useParams(); const { with_replies: withReplies = false } = profileRoute.useSearch(); - const dispatch = useAppDispatch(); const features = useFeatures(); const { data: account, isPending } = useAccountLookup(username); @@ -28,16 +25,6 @@ const AccountTimelinePage: React.FC = () => { const accountUsername = account?.username ?? username; - useEffect(() => { - if (account) { - dispatch(fetchAccountTimeline(account.id, { exclude_replies: !withReplies })); - - if (!withReplies) { - dispatch(fetchAccountTimeline(account.id, { pinned: true })); - } - } - }, [account?.id, withReplies]); - if (!account && isPending) { return ; } else if (!account) { diff --git a/packages/nicolium/src/pages/timelines/circle-timeline.tsx b/packages/nicolium/src/pages/timelines/circle-timeline.tsx index 0c71a1c16..34ac50cd9 100644 --- a/packages/nicolium/src/pages/timelines/circle-timeline.tsx +++ b/packages/nicolium/src/pages/timelines/circle-timeline.tsx @@ -1,8 +1,7 @@ import { useNavigate } from '@tanstack/react-router'; -import React, { useEffect } from 'react'; +import React from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; -import { fetchCircleTimeline } from '@/actions/timelines'; import { CircleTimelineColumn } from '@/columns/timeline'; import DropdownMenu from '@/components/dropdown-menu'; import MissingIndicator from '@/components/missing-indicator'; @@ -10,7 +9,6 @@ import Button from '@/components/ui/button'; import Column from '@/components/ui/column'; import Spinner from '@/components/ui/spinner'; import { circleTimelineRoute } from '@/features/ui/router'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useCircle, useDeleteCircle } from '@/queries/accounts/use-circles'; import { useModalsActions } from '@/stores/modals'; @@ -29,17 +27,12 @@ const CircleTimelinePage: React.FC = () => { const { circleId } = circleTimelineRoute.useParams(); const intl = useIntl(); - const dispatch = useAppDispatch(); const { openModal } = useModalsActions(); const navigate = useNavigate(); const { data: circle, isFetching } = useCircle(circleId); const { mutate: deleteCircle } = useDeleteCircle(); - useEffect(() => { - dispatch(fetchCircleTimeline(circleId)); - }, [circleId]); - const handleEditClick = () => { openModal('CIRCLE_EDITOR', { circleId }); }; diff --git a/packages/nicolium/src/reducers/timelines.ts b/packages/nicolium/src/reducers/timelines.ts deleted file mode 100644 index 04f303cf3..000000000 --- a/packages/nicolium/src/reducers/timelines.ts +++ /dev/null @@ -1,266 +0,0 @@ -import { create } from 'mutative'; - -import { - TIMELINE_UPDATE, - TIMELINE_DELETE, - TIMELINE_CLEAR, - TIMELINE_EXPAND_SUCCESS, - TIMELINE_EXPAND_REQUEST, - TIMELINE_EXPAND_FAIL, - TIMELINE_UPDATE_QUEUE, - TIMELINE_DEQUEUE, - MAX_QUEUED_ITEMS, - TIMELINE_SCROLL_TOP, - type TimelineAction, -} from '@/actions/timelines'; - -import type { PaginatedResponse, Status as BaseStatus } from 'pl-api'; - -type ImportPosition = 'start' | 'end'; - -const TRUNCATE_LIMIT = 40; -const TRUNCATE_SIZE = 20; - -interface Timeline { - unread: number; - top: boolean; - isLoading: boolean; - hasMore: boolean; - next: (() => Promise>) | null; - prev: (() => Promise>) | null; - items: Array; - queuedItems: Array; //max= MAX_QUEUED_ITEMS - totalQueuedItemsCount: number; //used for queuedItems overflow for MAX_QUEUED_ITEMS+ - loadingFailed: boolean; - isPartial: boolean; - loaded: boolean; -} - -const newTimeline = (): Timeline => ({ - unread: 0, - top: true, - isLoading: false, - hasMore: true, - next: null, - prev: null, - items: [], - queuedItems: [], //max= MAX_QUEUED_ITEMS - totalQueuedItemsCount: 0, //used for queuedItems overflow for MAX_QUEUED_ITEMS+ - loadingFailed: false, - isPartial: false, - loaded: false, -}); - -const initialState: State = {}; - -type State = Record; - -const getStatusIds = (statuses: Array> = []) => - statuses.map((status) => status.id); - -const mergeStatusIds = (oldIds: Array, newIds: Array) => [ - ...new Set([...newIds, ...oldIds]), -]; - -const addStatusId = (oldIds = Array(), newId: string) => mergeStatusIds(oldIds, [newId]); - -// Like `take`, but only if the collection's size exceeds truncateLimit -const truncate = (items: Array, truncateLimit: number, newSize: number) => - items.length > truncateLimit ? items.slice(0, newSize) : items; - -const truncateIds = (items: Array) => truncate(items, TRUNCATE_LIMIT, TRUNCATE_SIZE); - -const updateTimeline = ( - state: State, - timelineId: string, - updater: (timeline: Timeline) => void, -) => { - state[timelineId] = state[timelineId] || newTimeline(); - updater(state[timelineId]); -}; - -const setLoading = (state: State, timelineId: string, loading: boolean) => { - updateTimeline(state, timelineId, (timeline) => { - timeline.isLoading = loading; - }); -}; - -// Keep track of when a timeline failed to load -const setFailed = (state: State, timelineId: string, failed: boolean) => { - updateTimeline(state, timelineId, (timeline) => { - timeline.loadingFailed = failed; - }); -}; - -const expandNormalizedTimeline = ( - state: State, - timelineId: string, - statuses: Array, - next: (() => Promise>) | null, - prev: (() => Promise>) | null, - isPartial: boolean, - pos: ImportPosition = 'end', -) => { - const newIds = getStatusIds(statuses); - - updateTimeline(state, timelineId, (timeline) => { - timeline.isLoading = false; - timeline.loadingFailed = false; - timeline.isPartial = isPartial; - timeline.next = next; - timeline.prev = prev; - timeline.loaded = true; - - if (!next) timeline.hasMore = false; - - if (newIds.length) { - if (pos === 'end') { - timeline.items = mergeStatusIds(newIds, timeline.items); - } else { - timeline.items = mergeStatusIds(timeline.items, newIds); - } - } - }); -}; - -const appendStatus = (state: State, timelineId: string, statusId: string) => { - const top = state[timelineId]?.top; - const oldIds = state[timelineId]?.items || []; - const unread = state[timelineId]?.unread || 0; - - if (oldIds.includes(statusId) || state[timelineId]?.queuedItems.includes(statusId)) return state; - - const newIds = addStatusId(oldIds, statusId); - - updateTimeline(state, timelineId, (timeline) => { - if (top) { - // For performance, truncate items if user is scrolled to the top - timeline.items = truncateIds(newIds); - } else { - timeline.unread = unread + 1; - timeline.items = newIds; - } - }); -}; - -const updateTimelineQueue = (state: State, timelineId: string, statusId: string) => { - updateTimeline(state, timelineId, (timeline) => { - const { - queuedItems: queuedIds, - items: listedIds, - totalQueuedItemsCount: queuedCount, - } = timeline; - - if (queuedIds.includes(statusId)) return; - if (listedIds.includes(statusId)) return; - - timeline.totalQueuedItemsCount = queuedCount + 1; - timeline.queuedItems = addStatusId(queuedIds, statusId).slice(0, MAX_QUEUED_ITEMS); - }); -}; - -const shouldDelete = (timelineId: string, excludeAccount: string | null) => { - if (!excludeAccount) return true; - if (timelineId === `account:${excludeAccount}`) return false; - if (timelineId.startsWith(`account:${excludeAccount}:`)) return false; - return true; -}; - -const deleteStatus = ( - state: State, - statusId: string, - references: Array<[string]> | Array<[string, string]>, - excludeAccount: string | null, -) => { - for (const timelineId in state) { - if (shouldDelete(timelineId, excludeAccount)) { - state[timelineId].items = state[timelineId].items.filter((id) => id !== statusId); - state[timelineId].queuedItems = state[timelineId].queuedItems.filter((id) => id !== statusId); - } - } - - // Remove reblogs of deleted status - references.forEach((ref) => { - deleteStatus(state, ref[0], [], excludeAccount); - }); -}; - -const clearTimeline = (state: State, timelineId: string) => { - state[timelineId] = newTimeline(); -}; - -const updateTop = (state: State, timelineId: string, top: boolean) => { - updateTimeline(state, timelineId, (timeline) => { - if (top) timeline.unread = 0; - timeline.top = top; - }); -}; - -const timelineDequeue = (state: State, timelineId: string) => { - updateTimeline(state, timelineId, (timeline) => { - const top = timeline.top; - - const queuedIds = timeline.queuedItems; - - const newIds = mergeStatusIds(timeline.items, queuedIds); - timeline.items = top ? truncateIds(newIds) : newIds; - - timeline.queuedItems = []; - timeline.totalQueuedItemsCount = 0; - }); -}; - -const handleExpandFail = (state: State, timelineId: string) => { - setLoading(state, timelineId, false); - setFailed(state, timelineId, true); -}; - -const timelines = (state: State = initialState, action: TimelineAction): State => { - switch (action.type) { - case TIMELINE_EXPAND_REQUEST: - return create(state, (draft) => { - setLoading(draft, action.timeline, true); - }); - case TIMELINE_EXPAND_FAIL: - return create(state, (draft) => { - handleExpandFail(draft, action.timeline); - }); - case TIMELINE_EXPAND_SUCCESS: - return create(state, (draft) => { - expandNormalizedTimeline( - draft, - action.timeline, - action.statuses, - action.next, - action.prev, - action.partial, - ); - }); - case TIMELINE_UPDATE: - return create(state, (draft) => appendStatus(draft, action.timeline, action.statusId)); - case TIMELINE_UPDATE_QUEUE: - return create(state, (draft) => { - updateTimelineQueue(draft, action.timeline, action.statusId); - }); - case TIMELINE_DEQUEUE: - return create(state, (draft) => { - timelineDequeue(draft, action.timeline); - }); - case TIMELINE_DELETE: - return create(state, (draft) => { - deleteStatus(draft, action.statusId, action.references, action.reblogOf); - }); - case TIMELINE_CLEAR: - return create(state, (draft) => { - clearTimeline(draft, action.timeline); - }); - case TIMELINE_SCROLL_TOP: - return create(state, (draft) => { - updateTop(draft, action.timeline, action.top); - }); - default: - return state; - } -}; - -export { timelines as default }; diff --git a/packages/nicolium/src/utils/timelines.ts b/packages/nicolium/src/utils/timelines.ts deleted file mode 100644 index 2ab93a8fd..000000000 --- a/packages/nicolium/src/utils/timelines.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { NormalizedStatus as Status } from '@/normalizers/status'; -import type { Settings } from '@/schemas/frontend-settings'; - -const shouldFilter = ( - status: Pick, - columnSettings: Settings['timelines'][''], -) => { - const fallback = { - reblog: true, - reply: true, - direct: false, - }; - - const shows = { - reblog: status.reblog_id !== null, - reply: status.in_reply_to_id !== null, - direct: status.visibility === 'direct', - }; - - return Object.entries(shows).some( - ([key, value]) => - !(columnSettings?.shows || fallback)[key as 'reblog' | 'reply' | 'direct'] && value, - ); -}; - -export { shouldFilter }; From 44af59841a813933dd20f8f9c8873cf2d76760a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 15:48:00 +0100 Subject: [PATCH 35/89] nicolium: remove fixtures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../pleroma-quote-of-quote-post.json | 371 ------------------ ...tatus-vertical-video-without-metadata.json | 108 ----- .../pleroma-status-with-poll-with-emojis.json | 234 ----------- .../pleroma-status-with-poll.json | 199 ---------- .../src/__fixtures__/pleroma-status.json | 181 --------- 5 files changed, 1093 deletions(-) delete mode 100644 packages/nicolium/src/__fixtures__/pleroma-quote-of-quote-post.json delete mode 100644 packages/nicolium/src/__fixtures__/pleroma-status-vertical-video-without-metadata.json delete mode 100644 packages/nicolium/src/__fixtures__/pleroma-status-with-poll-with-emojis.json delete mode 100644 packages/nicolium/src/__fixtures__/pleroma-status-with-poll.json delete mode 100644 packages/nicolium/src/__fixtures__/pleroma-status.json diff --git a/packages/nicolium/src/__fixtures__/pleroma-quote-of-quote-post.json b/packages/nicolium/src/__fixtures__/pleroma-quote-of-quote-post.json deleted file mode 100644 index 1156cdb3a..000000000 --- a/packages/nicolium/src/__fixtures__/pleroma-quote-of-quote-post.json +++ /dev/null @@ -1,371 +0,0 @@ -{ - "account": { - "acct": "alex", - "avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", - "avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", - "bot": false, - "created_at": "2020-01-08T01:25:43.000Z", - "display_name": "Alex Gleason", - "emojis": [], - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - }, - { - "name": "Donate (PayPal)", - "value": "https://paypal.me/gleasonator" - }, - { - "name": "$BTC", - "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" - }, - { - "name": "$ETH", - "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" - }, - { - "name": "$DOGE", - "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" - }, - { - "name": "$XMR", - "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" - } - ], - "followers_count": 2220, - "following_count": 1544, - "fqn": "alex@gleasonator.com", - "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", - "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", - "id": "9v5bmRalQvjOy0ECcC", - "last_status_at": "2022-01-24T21:02:44", - "locked": false, - "note": "I create Fediverse software that empowers people online.

I'm vegan btw

Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", - "pleroma": { - "accepts_chat_messages": true, - "also_known_as": [], - "ap_id": "https://gleasonator.com/users/alex", - "background_image": null, - "favicon": "https://gleasonator.com/favicon.png", - "hide_favorites": true, - "hide_followers": false, - "hide_followers_count": false, - "hide_follows": false, - "hide_follows_count": false, - "is_admin": true, - "is_confirmed": true, - "is_moderator": false, - "is_suggested": true, - "relationship": {}, - "skip_thread_containment": false, - "tags": [] - }, - "source": { - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - }, - { - "name": "Donate (PayPal)", - "value": "https://paypal.me/gleasonator" - }, - { - "name": "$BTC", - "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" - }, - { - "name": "$ETH", - "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" - }, - { - "name": "$DOGE", - "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" - }, - { - "name": "$XMR", - "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" - } - ], - "note": "I create Fediverse software that empowers people online.\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", - "pleroma": { - "actor_type": "Person", - "discoverable": false - }, - "sensitive": false - }, - "statuses_count": 23004, - "url": "https://gleasonator.com/users/alex", - "username": "alex" - }, - "application": { - "name": "Soapbox FE", - "website": "https://soapbox.pub/" - }, - "bookmarked": false, - "card": null, - "content": "

Quote of quote post

", - "created_at": "2022-01-24T21:02:43.000Z", - "emojis": [], - "favourited": false, - "favourites_count": 0, - "id": "AFmFNKmfrR9CxtV01g", - "in_reply_to_account_id": null, - "in_reply_to_id": null, - "language": null, - "media_attachments": [], - "mentions": [ - { - "acct": "alex", - "id": "9v5bmRalQvjOy0ECcC", - "url": "https://gleasonator.com/users/alex", - "username": "alex" - } - ], - "muted": false, - "pinned": false, - "pleroma": { - "content": { - "text/plain": "Quote of quote post" - }, - "conversation_id": "AFmFNKkXzLRirIVIi8", - "direct_conversation_id": null, - "emoji_reactions": [], - "expires_at": null, - "in_reply_to_account_acct": null, - "local": true, - "parent_visible": false, - "pinned_at": null, - "quote": { - "account": { - "acct": "alex", - "avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", - "avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", - "bot": false, - "created_at": "2020-01-08T01:25:43.000Z", - "display_name": "Alex Gleason", - "emojis": [], - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - }, - { - "name": "Donate (PayPal)", - "value": "https://paypal.me/gleasonator" - }, - { - "name": "$BTC", - "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" - }, - { - "name": "$ETH", - "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" - }, - { - "name": "$DOGE", - "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" - }, - { - "name": "$XMR", - "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" - } - ], - "followers_count": 2220, - "following_count": 1544, - "fqn": "alex@gleasonator.com", - "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", - "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", - "id": "9v5bmRalQvjOy0ECcC", - "last_status_at": "2022-01-24T21:02:44", - "locked": false, - "note": "I create Fediverse software that empowers people online.

I'm vegan btw

Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", - "pleroma": { - "accepts_chat_messages": true, - "also_known_as": [], - "ap_id": "https://gleasonator.com/users/alex", - "background_image": null, - "favicon": "https://gleasonator.com/favicon.png", - "hide_favorites": true, - "hide_followers": false, - "hide_followers_count": false, - "hide_follows": false, - "hide_follows_count": false, - "is_admin": true, - "is_confirmed": true, - "is_moderator": false, - "is_suggested": true, - "relationship": {}, - "skip_thread_containment": false, - "tags": [] - }, - "source": { - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Pleroma+Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - }, - { - "name": "Donate (PayPal)", - "value": "https://paypal.me/gleasonator" - }, - { - "name": "$BTC", - "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" - }, - { - "name": "$ETH", - "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" - }, - { - "name": "$DOGE", - "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" - }, - { - "name": "$XMR", - "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" - } - ], - "note": "I create Fediverse software that empowers people online.\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", - "pleroma": { - "actor_type": "Person", - "discoverable": false - }, - "sensitive": false - }, - "statuses_count": 23004, - "url": "https://gleasonator.com/users/alex", - "username": "alex" - }, - "application": { - "name": "Soapbox FE", - "website": "https://soapbox.pub/" - }, - "bookmarked": false, - "card": null, - "content": "

Quote post

", - "created_at": "2022-01-24T21:02:34.000Z", - "emojis": [], - "favourited": false, - "favourites_count": 0, - "id": "AFmFMSpITT9xcOJKcK", - "in_reply_to_account_id": null, - "in_reply_to_id": null, - "language": null, - "media_attachments": [], - "mentions": [ - { - "acct": "alex", - "id": "9v5bmRalQvjOy0ECcC", - "url": "https://gleasonator.com/users/alex", - "username": "alex" - } - ], - "muted": false, - "pinned": false, - "pleroma": { - "content": { - "text/plain": "Quote post" - }, - "conversation_id": "AFmFMSnWa3k3WtTur2", - "direct_conversation_id": null, - "emoji_reactions": [ - { - "count": 1, - "me": false, - "name": "👍" - } - ], - "expires_at": null, - "in_reply_to_account_acct": null, - "local": true, - "parent_visible": false, - "pinned_at": null, - "quote": null, - "quote_url": "https://gleasonator.com/objects/4f35159c-3794-4037-9269-a7c84f7137c7", - "spoiler_text": { - "text/plain": "" - }, - "thread_muted": false - }, - "poll": null, - "reblog": null, - "reblogged": false, - "reblogs_count": 0, - "replies_count": 0, - "sensitive": false, - "spoiler_text": "", - "tags": [], - "text": null, - "uri": "https://gleasonator.com/objects/54d93075-7d04-4016-a128-81f3843bca79", - "url": "https://gleasonator.com/notice/AFmFMSpITT9xcOJKcK", - "visibility": "public" - }, - "quote_url": "https://gleasonator.com/objects/54d93075-7d04-4016-a128-81f3843bca79", - "spoiler_text": { - "text/plain": "" - }, - "thread_muted": false - }, - "poll": null, - "reblog": null, - "reblogged": false, - "reblogs_count": 0, - "replies_count": 1, - "sensitive": false, - "spoiler_text": "", - "tags": [], - "text": null, - "uri": "https://gleasonator.com/objects/1e2cfb5a-ece5-42df-9ec1-13e5de6d9f5b", - "url": "https://gleasonator.com/notice/AFmFNKmfrR9CxtV01g", - "visibility": "public" -} diff --git a/packages/nicolium/src/__fixtures__/pleroma-status-vertical-video-without-metadata.json b/packages/nicolium/src/__fixtures__/pleroma-status-vertical-video-without-metadata.json deleted file mode 100644 index edb24b9ef..000000000 --- a/packages/nicolium/src/__fixtures__/pleroma-status-vertical-video-without-metadata.json +++ /dev/null @@ -1,108 +0,0 @@ -{ - "account": { - "acct": "alex", - "avatar": "https://freespeechextremist.com/images/avi.png", - "avatar_static": "https://freespeechextremist.com/images/avi.png", - "bot": false, - "created_at": "2022-02-28T01:55:05.000Z", - "display_name": "Alex Gleason", - "emojis": [], - "fields": [], - "followers_count": 1, - "following_count": 0, - "header": "https://freespeechextremist.com/images/banner.png", - "header_static": "https://freespeechextremist.com/images/banner.png", - "id": "AGv8wCadU7DqWgMqNk", - "locked": false, - "note": "I'm testing out compatibility with an older Pleroma version", - "pleroma": { - "accepts_chat_messages": true, - "ap_id": "https://freespeechextremist.com/users/alex", - "background_image": null, - "confirmation_pending": false, - "favicon": null, - "hide_favorites": true, - "hide_followers": false, - "hide_followers_count": false, - "hide_follows": false, - "hide_follows_count": false, - "is_admin": false, - "is_moderator": false, - "relationship": {}, - "skip_thread_containment": false, - "tags": [] - }, - "source": { - "fields": [], - "note": "I'm testing out compatibility with an older Pleroma version", - "pleroma": { - "actor_type": "Person", - "discoverable": true - }, - "sensitive": false - }, - "statuses_count": 1, - "url": "https://freespeechextremist.com/users/alex", - "username": "alex" - }, - "application": { - "name": "Web", - "website": null - }, - "bookmarked": false, - "card": null, - "content": "
0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm", - "created_at": "2022-04-14T19:42:48.000Z", - "emojis": [], - "favourited": false, - "favourites_count": 0, - "id": "AIRxLeIzncpCtsr2hs", - "in_reply_to_account_id": null, - "in_reply_to_id": null, - "language": null, - "media_attachments": [ - { - "description": "0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm", - "id": "1142674091", - "pleroma": { - "mime_type": "video/webm" - }, - "preview_url": "https://freespeechextremist.com/media/3e34b808-1c84-4ef3-ba56-67cc86b7911a/0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm?name=0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm", - "remote_url": "https://freespeechextremist.com/media/3e34b808-1c84-4ef3-ba56-67cc86b7911a/0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm?name=0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm", - "text_url": "https://freespeechextremist.com/media/3e34b808-1c84-4ef3-ba56-67cc86b7911a/0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm?name=0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm", - "type": "video", - "url": "https://freespeechextremist.com/media/3e34b808-1c84-4ef3-ba56-67cc86b7911a/0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm?name=0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm" - } - ], - "mentions": [], - "muted": false, - "pinned": false, - "pleroma": { - "content": { - "text/plain": "0f66e92f339705ccc03079b8f647048e15730adf2cc9eaa1071c7c7cf6884b1b.webm" - }, - "conversation_id": 97191096, - "direct_conversation_id": null, - "emoji_reactions": [], - "expires_at": null, - "in_reply_to_account_acct": null, - "local": true, - "parent_visible": false, - "spoiler_text": { - "text/plain": "" - }, - "thread_muted": false - }, - "poll": null, - "reblog": null, - "reblogged": false, - "reblogs_count": 0, - "replies_count": 0, - "sensitive": false, - "spoiler_text": "", - "tags": [], - "text": null, - "uri": "https://freespeechextremist.com/objects/419b2cad-656a-4dbc-b2b5-94bb75e0afc8", - "url": "https://freespeechextremist.com/notice/AIRxLeIzncpCtsr2hs", - "visibility": "public" -} diff --git a/packages/nicolium/src/__fixtures__/pleroma-status-with-poll-with-emojis.json b/packages/nicolium/src/__fixtures__/pleroma-status-with-poll-with-emojis.json deleted file mode 100644 index 1e1ee1f61..000000000 --- a/packages/nicolium/src/__fixtures__/pleroma-status-with-poll-with-emojis.json +++ /dev/null @@ -1,234 +0,0 @@ -{ - "account": { - "acct": "alex", - "avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", - "avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", - "bot": false, - "created_at": "2020-01-08T01:25:43.000Z", - "display_name": "Alex Gleason", - "emojis": [], - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - }, - { - "name": "Donate (PayPal)", - "value": "https://paypal.me/gleasonator" - }, - { - "name": "$BTC", - "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" - }, - { - "name": "$ETH", - "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" - }, - { - "name": "$DOGE", - "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" - }, - { - "name": "$XMR", - "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" - } - ], - "followers_count": 2467, - "following_count": 1581, - "fqn": "alex@gleasonator.com", - "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", - "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", - "id": "9v5bmRalQvjOy0ECcC", - "last_status_at": "2022-03-11T01:33:19", - "locked": false, - "note": "I create Fediverse software that empowers people online.

I'm vegan btw

Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", - "pleroma": { - "accepts_chat_messages": true, - "also_known_as": ["https://mitra.social/users/alex"], - "ap_id": "https://gleasonator.com/users/alex", - "background_image": null, - "birthday": "1993-07-03", - "favicon": "https://gleasonator.com/favicon.png", - "hide_favorites": true, - "hide_followers": false, - "hide_followers_count": false, - "hide_follows": false, - "hide_follows_count": false, - "is_admin": true, - "is_confirmed": true, - "is_moderator": false, - "is_suggested": true, - "relationship": {}, - "skip_thread_containment": false, - "tags": [] - }, - "source": { - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - }, - { - "name": "Donate (PayPal)", - "value": "https://paypal.me/gleasonator" - }, - { - "name": "$BTC", - "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" - }, - { - "name": "$ETH", - "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" - }, - { - "name": "$DOGE", - "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" - }, - { - "name": "$XMR", - "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" - } - ], - "note": "I create Fediverse software that empowers people online.\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", - "pleroma": { - "actor_type": "Person", - "discoverable": false - }, - "sensitive": false - }, - "statuses_count": 23651, - "url": "https://gleasonator.com/users/alex", - "username": "alex" - }, - "application": { - "name": "Soapbox FE", - "website": "https://soapbox.pub/" - }, - "bookmarked": false, - "card": null, - "content": "

Test poll

", - "created_at": "2022-03-11T01:33:18.000Z", - "emojis": [ - { - "shortcode": "gleason_excited", - "static_url": "https://gleasonator.com/emoji/gleason_emojis/gleason_excited.png", - "url": "https://gleasonator.com/emoji/gleason_emojis/gleason_excited.png", - "visible_in_picker": false - }, - { - "shortcode": "soapbox", - "static_url": "https://gleasonator.com/emoji/Gleasonator/soapbox.png", - "url": "https://gleasonator.com/emoji/Gleasonator/soapbox.png", - "visible_in_picker": false - } - ], - "favourited": false, - "favourites_count": 1, - "id": "AHHue68kB59xtUv7MO", - "in_reply_to_account_id": null, - "in_reply_to_id": null, - "language": null, - "media_attachments": [], - "mentions": [], - "muted": false, - "pinned": false, - "pleroma": { - "content": { - "text/plain": "Test poll" - }, - "conversation_id": "AHHue65YMwbjjbQZO4", - "direct_conversation_id": null, - "emoji_reactions": [], - "expires_at": null, - "in_reply_to_account_acct": null, - "local": true, - "parent_visible": false, - "pinned_at": null, - "quote": null, - "quote_url": null, - "quote_visible": false, - "spoiler_text": { - "text/plain": "" - }, - "thread_muted": false - }, - "poll": { - "emojis": [ - { - "shortcode": "gleason_excited", - "static_url": "https://gleasonator.com/emoji/gleason_emojis/gleason_excited.png", - "url": "https://gleasonator.com/emoji/gleason_emojis/gleason_excited.png", - "visible_in_picker": false - }, - { - "shortcode": "soapbox", - "static_url": "https://gleasonator.com/emoji/Gleasonator/soapbox.png", - "url": "https://gleasonator.com/emoji/Gleasonator/soapbox.png", - "visible_in_picker": false - } - ], - "expired": false, - "expires_at": "2022-03-12T01:33:18.000Z", - "id": "AHHue67gF2JDqCQGhc", - "multiple": false, - "options": [ - { - "title": "Regular emoji 😍 ", - "votes_count": 0 - }, - { - "title": "Custom emoji :gleason_excited: ", - "votes_count": 1 - }, - { - "title": "No emoji", - "votes_count": 0 - }, - { - "title": "🤔 😮 😠 ", - "votes_count": 1 - }, - { - "title": ":soapbox:", - "votes_count": 1 - } - ], - "voters_count": 3, - "votes_count": 3 - }, - "reblog": null, - "reblogged": false, - "reblogs_count": 1, - "replies_count": 1, - "sensitive": false, - "spoiler_text": "", - "tags": [], - "text": null, - "uri": "https://gleasonator.com/objects/46d2ab26-3497-442b-999f-612fe717b0a3", - "url": "https://gleasonator.com/notice/AHHue68kB59xtUv7MO", - "visibility": "public" -} diff --git a/packages/nicolium/src/__fixtures__/pleroma-status-with-poll.json b/packages/nicolium/src/__fixtures__/pleroma-status-with-poll.json deleted file mode 100644 index 74e464df4..000000000 --- a/packages/nicolium/src/__fixtures__/pleroma-status-with-poll.json +++ /dev/null @@ -1,199 +0,0 @@ -{ - "account": { - "acct": "alex", - "avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", - "avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", - "bot": false, - "created_at": "2020-01-08T01:25:43.000Z", - "display_name": "Alex Gleason", - "emojis": [], - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - }, - { - "name": "Donate (PayPal)", - "value": "https://paypal.me/gleasonator" - }, - { - "name": "$BTC", - "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" - }, - { - "name": "$ETH", - "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" - }, - { - "name": "$DOGE", - "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" - }, - { - "name": "$XMR", - "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" - } - ], - "followers_count": 2465, - "following_count": 1581, - "fqn": "alex@gleasonator.com", - "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", - "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", - "id": "9v5bmRalQvjOy0ECcC", - "last_status_at": "2022-03-10T18:19:50", - "locked": false, - "note": "I create Fediverse software that empowers people online.

I'm vegan btw

Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", - "pleroma": { - "accepts_chat_messages": true, - "also_known_as": ["https://mitra.social/users/alex"], - "ap_id": "https://gleasonator.com/users/alex", - "background_image": null, - "birthday": "1993-07-03", - "favicon": "https://gleasonator.com/favicon.png", - "hide_favorites": true, - "hide_followers": false, - "hide_followers_count": false, - "hide_follows": false, - "hide_follows_count": false, - "is_admin": true, - "is_confirmed": true, - "is_moderator": false, - "is_suggested": true, - "relationship": {}, - "skip_thread_containment": false, - "tags": [] - }, - "source": { - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - }, - { - "name": "Donate (PayPal)", - "value": "https://paypal.me/gleasonator" - }, - { - "name": "$BTC", - "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" - }, - { - "name": "$ETH", - "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" - }, - { - "name": "$DOGE", - "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" - }, - { - "name": "$XMR", - "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" - } - ], - "note": "I create Fediverse software that empowers people online.\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", - "pleroma": { - "actor_type": "Person", - "discoverable": false - }, - "sensitive": false - }, - "statuses_count": 23648, - "url": "https://gleasonator.com/users/alex", - "username": "alex" - }, - "application": null, - "bookmarked": false, - "card": null, - "content": "

What is tolerance?

", - "created_at": "2020-03-23T19:33:06.000Z", - "emojis": [], - "favourited": false, - "favourites_count": 49, - "id": "103874034847713213", - "in_reply_to_account_id": null, - "in_reply_to_id": null, - "language": null, - "media_attachments": [], - "mentions": [], - "muted": false, - "pinned": true, - "pleroma": { - "content": { - "text/plain": "What is tolerance?" - }, - "conversation_id": "3023268", - "direct_conversation_id": null, - "emoji_reactions": [ - { - "count": 3, - "me": false, - "name": "❤️" - } - ], - "expires_at": null, - "in_reply_to_account_acct": null, - "local": true, - "parent_visible": false, - "pinned_at": "2021-11-23T01:38:44.000Z", - "quote": null, - "quote_url": null, - "quote_visible": false, - "spoiler_text": { - "text/plain": "" - }, - "thread_muted": false - }, - "poll": { - "emojis": [], - "expired": true, - "expires_at": "2020-03-24T19:33:06.000Z", - "id": "4930", - "multiple": false, - "options": [ - { - "title": "Banning, censoring, and deplatforming anyone you disagree with", - "votes_count": 2 - }, - { - "title": "Promoting free speech, even for people and ideas you dislike", - "votes_count": 36 - } - ], - "voters_count": 2, - "votes_count": 38 - }, - "reblog": null, - "reblogged": false, - "reblogs_count": 27, - "replies_count": 15, - "sensitive": false, - "spoiler_text": "", - "tags": [], - "text": null, - "uri": "https://gleasonator.com/users/alex/statuses/103874034847713213", - "url": "https://gleasonator.com/notice/103874034847713213", - "visibility": "public" -} diff --git a/packages/nicolium/src/__fixtures__/pleroma-status.json b/packages/nicolium/src/__fixtures__/pleroma-status.json deleted file mode 100644 index bf795f5e4..000000000 --- a/packages/nicolium/src/__fixtures__/pleroma-status.json +++ /dev/null @@ -1,181 +0,0 @@ -{ - "account": { - "acct": "alex", - "avatar": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", - "avatar_static": "https://media.gleasonator.com/6d64aecb17348b23aaff78db4687b9476cb0da1c07cc6a819c2e6ec7144c18b1.png", - "bot": false, - "created_at": "2020-01-08T01:25:43.000Z", - "display_name": "Alex Gleason", - "emojis": [], - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - }, - { - "name": "Donate (PayPal)", - "value": "https://paypal.me/gleasonator" - }, - { - "name": "$BTC", - "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" - }, - { - "name": "$ETH", - "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" - }, - { - "name": "$DOGE", - "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" - }, - { - "name": "$XMR", - "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" - } - ], - "followers_count": 2465, - "following_count": 1581, - "fqn": "alex@gleasonator.com", - "header": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", - "header_static": "https://media.gleasonator.com/accounts/headers/000/000/001/original/9d0e4dbf1c9dbc8f.png", - "id": "9v5bmRalQvjOy0ECcC", - "last_status_at": "2022-03-10T18:19:50", - "locked": false, - "note": "I create Fediverse software that empowers people online.

I'm vegan btw

Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", - "pleroma": { - "accepts_chat_messages": true, - "also_known_as": ["https://mitra.social/users/alex"], - "ap_id": "https://gleasonator.com/users/alex", - "background_image": null, - "birthday": "1993-07-03", - "favicon": "https://gleasonator.com/favicon.png", - "hide_favorites": true, - "hide_followers": false, - "hide_followers_count": false, - "hide_follows": false, - "hide_follows_count": false, - "is_admin": true, - "is_confirmed": true, - "is_moderator": false, - "is_suggested": true, - "relationship": {}, - "skip_thread_containment": false, - "tags": [] - }, - "source": { - "fields": [ - { - "name": "Website", - "value": "https://alexgleason.me" - }, - { - "name": "Soapbox", - "value": "https://soapbox.pub" - }, - { - "name": "Email", - "value": "alex@alexgleason.me" - }, - { - "name": "Gender identity", - "value": "Soyboy" - }, - { - "name": "Donate (PayPal)", - "value": "https://paypal.me/gleasonator" - }, - { - "name": "$BTC", - "value": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n" - }, - { - "name": "$ETH", - "value": "0xAc9aB5Fc04Dc1cB1789Af75b523Bd23C70B2D717" - }, - { - "name": "$DOGE", - "value": "D5zVZs6jrRakaPVGiErkQiHt9sayzm6V5D" - }, - { - "name": "$XMR", - "value": "45JDCLrjJ4bgVUSbbs2yjy9m5Mf4VLPW8fG7jw9sq5u69rXZZopQogZNeyYkMBnXpkaip4p4QwaaJNhdTotPa9g44DBCzdK" - } - ], - "note": "I create Fediverse software that empowers people online.\r\n\r\nI'm vegan btw\r\n\r\nNote: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.", - "pleroma": { - "actor_type": "Person", - "discoverable": false - }, - "sensitive": false - }, - "statuses_count": 23648, - "url": "https://gleasonator.com/users/alex", - "username": "alex" - }, - "application": null, - "bookmarked": false, - "card": null, - "content": "

Good morning! Hope you have a wonderful day.

", - "created_at": "2020-03-23T19:33:06.000Z", - "emojis": [], - "favourited": false, - "favourites_count": 49, - "id": "103874034845713213", - "in_reply_to_account_id": null, - "in_reply_to_id": null, - "language": null, - "media_attachments": [], - "mentions": [], - "muted": false, - "pinned": true, - "pleroma": { - "content": { - "text/plain": "What is tolerance?" - }, - "conversation_id": "3023268", - "direct_conversation_id": null, - "emoji_reactions": [ - { - "count": 3, - "me": false, - "name": "❤️" - } - ], - "expires_at": null, - "in_reply_to_account_acct": null, - "local": true, - "parent_visible": false, - "pinned_at": "2021-11-23T01:38:44.000Z", - "quote": null, - "quote_url": null, - "quote_visible": false, - "spoiler_text": { - "text/plain": "" - }, - "thread_muted": false - }, - "poll": null, - "reblog": null, - "reblogged": false, - "reblogs_count": 27, - "replies_count": 15, - "sensitive": false, - "spoiler_text": "", - "tags": [], - "text": null, - "uri": "https://gleasonator.com/users/alex/statuses/103874034847713213", - "url": "https://gleasonator.com/notice/103874034847713213", - "visibility": "public" -} From f4de9a3c6ada84a8dbb31cc391403885dbc0377c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 15:48:07 +0100 Subject: [PATCH 36/89] nicolium: fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/actions/statuses.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/nicolium/src/actions/statuses.ts b/packages/nicolium/src/actions/statuses.ts index 5bfc84192..b9b988c1d 100644 --- a/packages/nicolium/src/actions/statuses.ts +++ b/packages/nicolium/src/actions/statuses.ts @@ -215,7 +215,6 @@ const deleteStatus = }, queryClient, ); - dispatch(deleteFromTimelines(statusId)); if (withRedraft) { useComposeStore.getState().actions.setComposeToStatus(status, poll, source, withRedraft); @@ -248,7 +247,6 @@ const deleteStatusFromGroup = }, queryClient, ); - dispatch(deleteFromTimelines(statusId)); }) .catch(() => { incrementReplyCount(status); From b66e02a6c9a5bb1fed830507adc544aa34f57f85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 15:52:16 +0100 Subject: [PATCH 37/89] nicolium: fix reports modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/locales/en.json | 3 --- .../report-modal/steps/other-actions-step.tsx | 25 +++++++++++++------ packages/nicolium/src/stores/timelines.ts | 5 ++++ 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/packages/nicolium/src/locales/en.json b/packages/nicolium/src/locales/en.json index 7b6282771..f19457ab0 100644 --- a/packages/nicolium/src/locales/en.json +++ b/packages/nicolium/src/locales/en.json @@ -565,7 +565,6 @@ "column.search": "Search", "column.settings_store": "Settings store", "column.subscribers": "Subscribers", - "column.test": "Test timeline", "column.tokens": "Active sessions", "column.wrenched": "Recent wrenches timeline", "column_forbidden.body": "You do not have permission to access this page.", @@ -804,7 +803,6 @@ "developers.navigation.service_worker_label": "Service Worker", "developers.navigation.settings_store_label": "Settings store", "developers.navigation.show_toast": "Trigger Toast", - "developers.navigation.test_timeline_label": "Test timeline", "developers.settings_store.advanced": "Advanced settings", "developers.settings_store.hint": "It is possible to directly edit your user settings here. BE CAREFUL! Editing this section can break your account, and you will only be able to recover through the API.", "direct.search_placeholder": "Send a message to…", @@ -1016,7 +1014,6 @@ "empty_column.scheduled_statuses": "You don't have any scheduled statuses yet. When you add one, it will show up here.", "empty_column.search.accounts": "There are no people results for \"{term}\"", "empty_column.search.statuses": "There are no posts results for \"{term}\"", - "empty_column.test": "The test timeline is empty.", "empty_column.wrenched": "There is nothing here! 🔧 a public post to fill it up", "event.banner": "Event banner", "event.copy": "Copy link to event", diff --git a/packages/nicolium/src/modals/report-modal/steps/other-actions-step.tsx b/packages/nicolium/src/modals/report-modal/steps/other-actions-step.tsx index 487306207..a8ae3b6b7 100644 --- a/packages/nicolium/src/modals/report-modal/steps/other-actions-step.tsx +++ b/packages/nicolium/src/modals/report-modal/steps/other-actions-step.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import Button from '@/components/ui/button'; @@ -7,9 +7,9 @@ import HStack from '@/components/ui/hstack'; import Stack from '@/components/ui/stack'; import Text from '@/components/ui/text'; import Toggle from '@/components/ui/toggle'; -import { useAppSelector } from '@/hooks/use-app-selector'; import { useFeatures } from '@/hooks/use-features'; import StatusCheckBox from '@/modals/report-modal/components/status-check-box'; +import { useAccountTimeline } from '@/queries/timelines/use-timelines'; import { getDomain } from '@/utils/accounts'; import type { Account } from 'pl-api'; @@ -58,12 +58,21 @@ const OtherActionsStep = ({ const features = useFeatures(); const intl = useIntl(); - const statusIds = useAppSelector((state) => [ - ...new Set([ - ...state.timelines[`account:${account.id}:with_replies`].items, - ...selectedStatusIds, - ]), - ]); + const { entries } = useAccountTimeline(account.id, { exclude_replies: false }); + + const statusIds = useMemo(() => { + const timelineStatusIds = entries + .map((entry) => + entry.type === 'status' + ? entry.reblogIds.length > 0 + ? entry.reblogIds[0] + : entry.id + : null, + ) + .filter((id): id is string => id !== null); + + return [...new Set([...timelineStatusIds, ...selectedStatusIds])]; + }, [entries, selectedStatusIds]); const isBlocked = block; const isForward = forward; const canForward = !account.local && features.federating; diff --git a/packages/nicolium/src/stores/timelines.ts b/packages/nicolium/src/stores/timelines.ts index e887f366e..d23070cc9 100644 --- a/packages/nicolium/src/stores/timelines.ts +++ b/packages/nicolium/src/stores/timelines.ts @@ -11,6 +11,7 @@ type TimelineEntry = type: 'status'; id: string; rebloggedBy: Array; + reblogIds: Array; isConnectedTop?: boolean; isConnectedBottom?: boolean; } @@ -86,12 +87,14 @@ const processPage = (statuses: Array): Array => { // entry connection stuff might happen to call processStatus on the same status multiple times if (!existingEntry.rebloggedBy.includes(status.account.id)) { existingEntry.rebloggedBy.push(status.account.id); + existingEntry.reblogIds.push(status.id); } } else { timelinePage.push({ type: 'status', id: status.reblog.id, rebloggedBy: [status.account.id], + reblogIds: [status.id], isConnectedTop, }); } @@ -102,6 +105,7 @@ const processPage = (statuses: Array): Array => { type: 'status', id: status.id, rebloggedBy: [], + reblogIds: [], isConnectedTop, }); @@ -230,6 +234,7 @@ const useTimelinesStore = create()( type: 'status', id: newId, rebloggedBy: [], + reblogIds: [], }; } } From a2aa7aabc27cc6a962396cfc12eeec6ca6834b68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 15:54:22 +0100 Subject: [PATCH 38/89] nicolium: remove unused MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../src/api/hooks/streaming/use-bubble-stream.ts | 11 ----------- .../src/api/hooks/streaming/use-community-stream.ts | 11 ----------- .../src/api/hooks/streaming/use-group-stream.ts | 5 ----- .../src/api/hooks/streaming/use-hashtag-stream.ts | 5 ----- .../src/api/hooks/streaming/use-list-stream.ts | 11 ----------- .../src/api/hooks/streaming/use-public-stream.ts | 10 ---------- .../src/api/hooks/streaming/use-remote-stream.ts | 11 ----------- .../nicolium/src/components/statuses/status-list.tsx | 2 +- packages/nicolium/src/queries/accounts/selectors.ts | 7 +------ 9 files changed, 2 insertions(+), 71 deletions(-) delete mode 100644 packages/nicolium/src/api/hooks/streaming/use-bubble-stream.ts delete mode 100644 packages/nicolium/src/api/hooks/streaming/use-community-stream.ts delete mode 100644 packages/nicolium/src/api/hooks/streaming/use-group-stream.ts delete mode 100644 packages/nicolium/src/api/hooks/streaming/use-hashtag-stream.ts delete mode 100644 packages/nicolium/src/api/hooks/streaming/use-list-stream.ts delete mode 100644 packages/nicolium/src/api/hooks/streaming/use-public-stream.ts delete mode 100644 packages/nicolium/src/api/hooks/streaming/use-remote-stream.ts diff --git a/packages/nicolium/src/api/hooks/streaming/use-bubble-stream.ts b/packages/nicolium/src/api/hooks/streaming/use-bubble-stream.ts deleted file mode 100644 index 7415abc46..000000000 --- a/packages/nicolium/src/api/hooks/streaming/use-bubble-stream.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useTimelineStream } from './use-timeline-stream'; - -interface UseBubbleStreamOpts { - onlyMedia?: boolean; - enabled?: boolean; -} - -const useBubbleStream = ({ onlyMedia, enabled }: UseBubbleStreamOpts = {}) => - useTimelineStream(`bubble${onlyMedia ? ':media' : ''}`, {}, enabled); - -export { useBubbleStream }; diff --git a/packages/nicolium/src/api/hooks/streaming/use-community-stream.ts b/packages/nicolium/src/api/hooks/streaming/use-community-stream.ts deleted file mode 100644 index 7ec260687..000000000 --- a/packages/nicolium/src/api/hooks/streaming/use-community-stream.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useTimelineStream } from './use-timeline-stream'; - -interface UseCommunityStreamOpts { - onlyMedia?: boolean; - enabled?: boolean; -} - -const useCommunityStream = ({ onlyMedia, enabled }: UseCommunityStreamOpts = {}) => - useTimelineStream(`public:local${onlyMedia ? ':media' : ''}`, {}, enabled); - -export { useCommunityStream }; diff --git a/packages/nicolium/src/api/hooks/streaming/use-group-stream.ts b/packages/nicolium/src/api/hooks/streaming/use-group-stream.ts deleted file mode 100644 index 63f2a1f1e..000000000 --- a/packages/nicolium/src/api/hooks/streaming/use-group-stream.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { useTimelineStream } from './use-timeline-stream'; - -const useGroupStream = (groupId: string) => useTimelineStream('group', { group: groupId }); - -export { useGroupStream }; diff --git a/packages/nicolium/src/api/hooks/streaming/use-hashtag-stream.ts b/packages/nicolium/src/api/hooks/streaming/use-hashtag-stream.ts deleted file mode 100644 index 9cd4d4115..000000000 --- a/packages/nicolium/src/api/hooks/streaming/use-hashtag-stream.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { useTimelineStream } from './use-timeline-stream'; - -const useHashtagStream = (tag: string) => useTimelineStream('hashtag', { tag }); - -export { useHashtagStream }; diff --git a/packages/nicolium/src/api/hooks/streaming/use-list-stream.ts b/packages/nicolium/src/api/hooks/streaming/use-list-stream.ts deleted file mode 100644 index 59829a0bc..000000000 --- a/packages/nicolium/src/api/hooks/streaming/use-list-stream.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useLoggedIn } from '@/hooks/use-logged-in'; - -import { useTimelineStream } from './use-timeline-stream'; - -const useListStream = (listId: string) => { - const { isLoggedIn } = useLoggedIn(); - - return useTimelineStream('list', { list: listId }, isLoggedIn); -}; - -export { useListStream }; diff --git a/packages/nicolium/src/api/hooks/streaming/use-public-stream.ts b/packages/nicolium/src/api/hooks/streaming/use-public-stream.ts deleted file mode 100644 index 18b2a6a8a..000000000 --- a/packages/nicolium/src/api/hooks/streaming/use-public-stream.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { useTimelineStream } from './use-timeline-stream'; - -interface UsePublicStreamOpts { - onlyMedia?: boolean; -} - -const usePublicStream = ({ onlyMedia }: UsePublicStreamOpts = {}) => - useTimelineStream(`public${onlyMedia ? ':media' : ''}`); - -export { usePublicStream }; diff --git a/packages/nicolium/src/api/hooks/streaming/use-remote-stream.ts b/packages/nicolium/src/api/hooks/streaming/use-remote-stream.ts deleted file mode 100644 index f737ce389..000000000 --- a/packages/nicolium/src/api/hooks/streaming/use-remote-stream.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useTimelineStream } from './use-timeline-stream'; - -interface UseRemoteStreamOpts { - instance: string; - onlyMedia?: boolean; -} - -const useRemoteStream = ({ instance, onlyMedia }: UseRemoteStreamOpts) => - useTimelineStream(`public:remote${onlyMedia ? ':media' : ''}`, { instance }); - -export { useRemoteStream }; diff --git a/packages/nicolium/src/components/statuses/status-list.tsx b/packages/nicolium/src/components/statuses/status-list.tsx index 346803169..49a85f1f2 100644 --- a/packages/nicolium/src/components/statuses/status-list.tsx +++ b/packages/nicolium/src/components/statuses/status-list.tsx @@ -247,4 +247,4 @@ const StatusList: React.FC = ({ ); }; -export { type IStatusList, StatusList as default }; +export { StatusList as default }; diff --git a/packages/nicolium/src/queries/accounts/selectors.ts b/packages/nicolium/src/queries/accounts/selectors.ts index fae26d41c..4b59bc5ec 100644 --- a/packages/nicolium/src/queries/accounts/selectors.ts +++ b/packages/nicolium/src/queries/accounts/selectors.ts @@ -13,15 +13,10 @@ const getAccounts = (): Array => const selectAccount = (accountId: string) => queryClient.getQueryData(queryKeys.accounts.show(accountId)); -const selectAccounts = (accountIds: Array) => - accountIds - .map((accountId) => selectAccount(accountId)) - .filter((account): account is Account => account !== undefined); - const selectOwnAccount = (state: RootState) => { if (state.me) { return selectAccount(state.me); } }; -export { getAccounts, selectAccount, selectAccounts, selectOwnAccount }; +export { getAccounts, selectAccount, selectOwnAccount }; From a94f3b9c336f9ba61aea92c4ef934c161dc3a9c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 15:58:00 +0100 Subject: [PATCH 39/89] nicolium: restore error handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../src/pages/timelines/landing-timeline.tsx | 8 ++++---- .../nicolium/src/queries/timelines/use-timeline.ts | 6 +++--- packages/nicolium/src/stores/timelines.ts | 12 ++++++++++++ 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/packages/nicolium/src/pages/timelines/landing-timeline.tsx b/packages/nicolium/src/pages/timelines/landing-timeline.tsx index 2b8bef735..770465dc8 100644 --- a/packages/nicolium/src/pages/timelines/landing-timeline.tsx +++ b/packages/nicolium/src/pages/timelines/landing-timeline.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx'; -import React, { useState } from 'react'; +import React from 'react'; import { FormattedMessage } from 'react-intl'; import { PublicTimelineColumn } from '@/columns/timeline'; @@ -12,6 +12,7 @@ import Stack from '@/components/ui/stack'; import { useInstance } from '@/hooks/use-instance'; import { useRegistrationStatus } from '@/hooks/use-registration-status'; import { About } from '@/pages/utils/about'; +import { usePublicTimeline } from '@/queries/timelines/use-timelines'; import { getTextDirection } from '@/utils/rtl'; interface ILogoText extends Pick, 'className' | 'dir'> { @@ -53,8 +54,7 @@ const LandingTimelinePage = () => { const instance = useInstance(); const { isOpen } = useRegistrationStatus(); - // todo fix this - const [timelineFailed, _setTimelineFailed] = useState(false); + const { isError } = usePublicTimeline({ local: true }); const timelineEnabled = !instance.pleroma.metadata.restrict_unauthenticated.timelines.local; @@ -75,7 +75,7 @@ const LandingTimelinePage = () => { )} - {timelineEnabled && !timelineFailed ? ( + {timelineEnabled && !isError ? ( { - if (!timeline.isPending) return; + if (!timeline.isPending || timeline.isFetching) return; fetchInitial(); }, []); @@ -32,7 +32,7 @@ const useTimeline = (timelineId: string, fetcher: TimelineFetcher, streamConfig? importEntities({ statuses: response.items }); timelineActions.expandTimeline(timelineId, response.items, !!response.next, true); } catch (error) { - // + timelineActions.setError(timelineId, true); } }, [timelineId]); @@ -46,7 +46,7 @@ const useTimeline = (timelineId: string, fetcher: TimelineFetcher, streamConfig? timelineActions.expandTimeline(timelineId, response.items, !!response.next, false); } catch (error) { - // + timelineActions.setError(timelineId, true); } }, [timelineId, timeline.oldestStatusId]); diff --git a/packages/nicolium/src/stores/timelines.ts b/packages/nicolium/src/stores/timelines.ts index d23070cc9..b9cb23a57 100644 --- a/packages/nicolium/src/stores/timelines.ts +++ b/packages/nicolium/src/stores/timelines.ts @@ -31,6 +31,7 @@ interface TimelineData { queuedCount: number; isFetching: boolean; isPending: boolean; + isError: boolean; hasNextPage: boolean; oldestStatusId?: string; } @@ -47,6 +48,7 @@ interface State { receiveStreamingStatus: (timelineId: string, status: Status) => void; deleteStatus: (statusId: string) => void; setLoading: (timelineId: string, isFetching: boolean) => void; + setError: (timelineId: string, isError: boolean) => void; dequeueEntries: (timelineId: string) => void; importPendingStatus: (params: CreateStatusParams, idempotencyKey: string) => void; replacePendingStatus: (idempotencyKey: string, newId: string) => void; @@ -193,6 +195,15 @@ const useTimelinesStore = create()( if (!isFetching) timeline.isPending = false; state.timelines[timelineId] = timeline; }), + setError: (timelineId, isError) => + set((state) => { + const timeline = state.timelines[timelineId] ?? createEmptyTimeline(); + + timeline.isFetching = false; + timeline.isPending = false; + timeline.isError = isError; + state.timelines[timelineId] = timeline; + }), dequeueEntries: (timelineId) => set((state) => { const timeline = state.timelines[timelineId]; @@ -289,6 +300,7 @@ const createEmptyTimeline = (): TimelineData => ({ queuedCount: 0, isFetching: false, isPending: true, + isError: false, hasNextPage: true, oldestStatusId: undefined, }); From befb60d92d24d9dd8c37fe4018ec63f9c886ce5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 16:26:48 +0100 Subject: [PATCH 40/89] nicolium: fix positioning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/columns/timeline.tsx | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/nicolium/src/columns/timeline.tsx b/packages/nicolium/src/columns/timeline.tsx index 3f8480d8a..450a20b1f 100644 --- a/packages/nicolium/src/columns/timeline.tsx +++ b/packages/nicolium/src/columns/timeline.tsx @@ -186,6 +186,14 @@ const TimelineStatus: React.FC = (props): React.JSX.Element => ); }; + const connector = renderConnector(); + + const status = statusQuery.isPending ? ( + + ) : statusQuery.data ? ( + + ) : null; + return (
= (props): React.JSX.Element => timelineId={props.timelineId} /> )} - {renderConnector()} - {statusQuery.isPending ? ( - - ) : statusQuery.data ? ( - - ) : null} + {connector ? ( +
+ {connector} + {status} +
+ ) : ( + status + )}
); }; From 96c6fd3c38db6757e57fccb535ed132dd3c4dd49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 16:29:52 +0100 Subject: [PATCH 41/89] nicolium: add 'originalId' for position remembering stuff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/stores/timelines.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/nicolium/src/stores/timelines.ts b/packages/nicolium/src/stores/timelines.ts index b9cb23a57..4c26af603 100644 --- a/packages/nicolium/src/stores/timelines.ts +++ b/packages/nicolium/src/stores/timelines.ts @@ -10,6 +10,8 @@ type TimelineEntry = | { type: 'status'; id: string; + // id of the topmost status where the target status was found, either the status itself or its reblog + originalId: string; rebloggedBy: Array; reblogIds: Array; isConnectedTop?: boolean; @@ -95,6 +97,7 @@ const processPage = (statuses: Array): Array => { timelinePage.push({ type: 'status', id: status.reblog.id, + originalId: status.id, rebloggedBy: [status.account.id], reblogIds: [status.id], isConnectedTop, @@ -106,6 +109,7 @@ const processPage = (statuses: Array): Array => { timelinePage.push({ type: 'status', id: status.id, + originalId: status.id, rebloggedBy: [], reblogIds: [], isConnectedTop, @@ -244,6 +248,7 @@ const useTimelinesStore = create()( timeline.entries[idx] = { type: 'status', id: newId, + originalId: newId, rebloggedBy: [], reblogIds: [], }; From cd9009c1128adbadadc05d30611a307505f82838 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 16:43:39 +0100 Subject: [PATCH 42/89] nicolium: fix tombstones issue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/queries/statuses/use-status.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/nicolium/src/queries/statuses/use-status.ts b/packages/nicolium/src/queries/statuses/use-status.ts index 83d04500b..3a0008322 100644 --- a/packages/nicolium/src/queries/statuses/use-status.ts +++ b/packages/nicolium/src/queries/statuses/use-status.ts @@ -38,6 +38,7 @@ type SelectedStatus = NormalizedStatus & { const useMinimalStatus = (statusId?: string) => { const client = useClient(); + const contextsActions = useContextsActions(); return useQuery({ queryKey: queryKeys.statuses.show(statusId!), @@ -45,6 +46,7 @@ const useMinimalStatus = (statusId?: string) => { client.statuses.getStatus(statusId!).then((status) => { // Import related entities (accounts, polls, etc.) into the RQ cache importEntities({ statuses: [status] }, { withParents: false }); + contextsActions.importStatus(status); return normalizeStatus(status); }), From 02c80f09487e9447ea5fede8b1944958490699ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 16:53:50 +0100 Subject: [PATCH 43/89] nicolium: some markers support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../api/hooks/streaming/use-user-stream.ts | 14 ++++--- packages/nicolium/src/features/ui/index.tsx | 13 +++---- packages/nicolium/src/queries/keys.ts | 5 ++- .../src/queries/markers/use-markers.ts | 38 ++++++++++++++++--- .../notifications/use-notifications.ts | 23 +++-------- 5 files changed, 55 insertions(+), 38 deletions(-) diff --git a/packages/nicolium/src/api/hooks/streaming/use-user-stream.ts b/packages/nicolium/src/api/hooks/streaming/use-user-stream.ts index d46fe8b82..fb6140801 100644 --- a/packages/nicolium/src/api/hooks/streaming/use-user-stream.ts +++ b/packages/nicolium/src/api/hooks/streaming/use-user-stream.ts @@ -162,12 +162,16 @@ const useUserStream = () => { case 'announcement.delete': deleteAnnouncement(event.payload); break; - case 'marker': - queryClient.setQueryData( - queryKeys.markers.notifications, - event.payload.notifications ?? null, - ); + case 'marker': { + for (const timeline in event.payload) { + queryClient.setQueryData( + queryKeys.markers.timeline(timeline as 'home' | 'notifications'), + event.payload[timeline] ?? null, + ); + } + break; + } } }, []); diff --git a/packages/nicolium/src/features/ui/index.tsx b/packages/nicolium/src/features/ui/index.tsx index ca314889e..076615877 100644 --- a/packages/nicolium/src/features/ui/index.tsx +++ b/packages/nicolium/src/features/ui/index.tsx @@ -19,17 +19,17 @@ import { useOwnAccount } from '@/hooks/use-own-account'; import { prefetchFollowRequests } from '@/queries/accounts/use-follow-requests'; import { queryClient } from '@/queries/client'; import { prefetchCustomEmojis } from '@/queries/instance/use-custom-emojis'; -import { - usePrefetchNotifications, - usePrefetchNotificationsMarker, -} from '@/queries/notifications/use-notifications'; +import { usePrefetchNotificationsMarker } from '@/queries/markers/use-markers'; +import { usePrefetchNotifications } from '@/queries/notifications/use-notifications'; import { useFilters } from '@/queries/settings/use-filters'; import { scheduledStatusesQueryOptions } from '@/queries/statuses/scheduled-statuses'; import { useShoutboxSubscription } from '@/stores/shoutbox'; import { useIsDropdownMenuOpen } from '@/stores/ui'; import { getVapidKey } from '@/utils/auth'; import { isStandalone } from '@/utils/state'; - +// Dummy import, to make sure that ends up in the application bundle. +// Without this it ends up in ~8 very commonly used bundles. +import '@/components/statuses/status'; import { ModalRoot, AccountHoverCard, @@ -37,9 +37,6 @@ import { DropdownNavigation, StatusHoverCard, } from './util/async-components'; -// Dummy import, to make sure that ends up in the application bundle. -// Without this it ends up in ~8 very commonly used bundles. -import '@/components/statuses/status'; import GlobalHotkeys from './util/global-hotkeys'; const UI: React.FC = React.memo(() => { diff --git a/packages/nicolium/src/queries/keys.ts b/packages/nicolium/src/queries/keys.ts index b8526882a..07ed0ae86 100644 --- a/packages/nicolium/src/queries/keys.ts +++ b/packages/nicolium/src/queries/keys.ts @@ -375,7 +375,10 @@ const notifications = { const markers = { root: ['markers'] as const, - notifications: ['markers', 'notifications'] as TaggedKey<['markers', 'notifications'], Marker>, + timeline: (timeline: 'home' | 'notifications') => { + const key = ['markers', timeline] as const; + return key as TaggedKey; + }, }; const search = { diff --git a/packages/nicolium/src/queries/markers/use-markers.ts b/packages/nicolium/src/queries/markers/use-markers.ts index 0beaf19c8..17d43d789 100644 --- a/packages/nicolium/src/queries/markers/use-markers.ts +++ b/packages/nicolium/src/queries/markers/use-markers.ts @@ -1,20 +1,46 @@ -import { useQuery } from '@tanstack/react-query'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useEffect } from 'react'; import { useClient } from '@/hooks/use-client'; import { useLoggedIn } from '@/hooks/use-logged-in'; import { queryKeys } from '../keys'; -const useNotificationsMarker = () => { +const useMarker = (timeline: 'home' | 'notifications') => { const client = useClient(); const { me } = useLoggedIn(); return useQuery({ - queryKey: queryKeys.markers.notifications, - queryFn: async () => - (await client.timelines.getMarkers(['notifications'])).notifications ?? null, + queryKey: queryKeys.markers.timeline(timeline), + queryFn: async () => (await client.timelines.getMarkers([timeline]))[timeline] ?? null, enabled: !!me, }); }; -export { useNotificationsMarker }; +const useNotificationsMarker = () => useMarker('notifications'); + +const useHomeTimelineMarker = () => useMarker('home'); + +const usePrefetchMarker = (timeline: 'home' | 'notifications') => { + const client = useClient(); + const queryClient = useQueryClient(); + const { me } = useLoggedIn(); + + useEffect(() => { + if (!me) return; + queryClient.prefetchQuery({ + queryKey: queryKeys.markers.timeline(timeline), + queryFn: async () => (await client.timelines.getMarkers([timeline]))[timeline] ?? null, + }); + }, [me, timeline]); +}; + +const usePrefetchHomeTimelineMarker = () => usePrefetchMarker('home'); +const usePrefetchNotificationsMarker = () => usePrefetchMarker('notifications'); + +export { + useNotificationsMarker, + useHomeTimelineMarker, + usePrefetchHomeTimelineMarker, + usePrefetchNotificationsMarker, +}; diff --git a/packages/nicolium/src/queries/notifications/use-notifications.ts b/packages/nicolium/src/queries/notifications/use-notifications.ts index a838c510d..8594c5b6e 100644 --- a/packages/nicolium/src/queries/notifications/use-notifications.ts +++ b/packages/nicolium/src/queries/notifications/use-notifications.ts @@ -149,21 +149,6 @@ const useNotification = (notification: NotificationGroup) => { }, [notification, status, target, accounts.data]); }; -const usePrefetchNotificationsMarker = () => { - const client = useClient(); - const queryClient = useQueryClient(); - const { me } = useLoggedIn(); - - useEffect(() => { - if (!me) return; - queryClient.prefetchQuery({ - queryKey: queryKeys.markers.notifications, - queryFn: async () => - (await client.timelines.getMarkers(['notifications'])).notifications ?? null, - }); - }, [me]); -}; - const useProcessStreamNotification = () => { const intl = useIntl(); const { data: filters = [] } = useFiltersByContext('notifications'); @@ -261,7 +246,7 @@ const useMarkNotificationsReadMutation = () => { mutationFn: async (lastReadId?: string | null) => { if (!lastReadId) return; - const currentMarker = queryClient.getQueryData(queryKeys.markers.notifications); + const currentMarker = queryClient.getQueryData(queryKeys.markers.timeline('notifications')); if (currentMarker && compareId(currentMarker.last_read_id, lastReadId) >= 0) { return; } @@ -274,7 +259,10 @@ const useMarkNotificationsReadMutation = () => { }, onSuccess: (markers) => { if (markers?.notifications) { - queryClient.setQueryData(queryKeys.markers.notifications, markers.notifications); + queryClient.setQueryData( + queryKeys.markers.timeline('notifications'), + markers.notifications, + ); } }, }); @@ -359,6 +347,5 @@ export { useNotification, useNotificationsUnreadCount, usePrefetchNotifications, - usePrefetchNotificationsMarker, useProcessStreamNotification, }; From fe80efd7ff0acfebda8e4e343613b281853922fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 17:08:43 +0100 Subject: [PATCH 44/89] nicolium: timeline empty message fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/columns/timeline.tsx | 50 ++++++++++++---------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/packages/nicolium/src/columns/timeline.tsx b/packages/nicolium/src/columns/timeline.tsx index 450a20b1f..e9d031df3 100644 --- a/packages/nicolium/src/columns/timeline.tsx +++ b/packages/nicolium/src/columns/timeline.tsx @@ -300,10 +300,10 @@ const Timeline: React.FC = ({ query, contextType = 'public' }) => { ); }; -const HomeTimelineColumn: React.FC = () => { +const HomeTimelineColumn: React.FC = (props) => { const timelineQuery = useHomeTimeline(); - return ; + return ; }; interface IPublicTimelineColumn extends IBaseTimeline { @@ -312,82 +312,87 @@ interface IPublicTimelineColumn extends IBaseTimeline { instance?: string; } -const PublicTimelineColumn: React.FC = (params) => { - const timelineQuery = usePublicTimeline(params); +const PublicTimelineColumn: React.FC = ({ + local, + remote, + instance, + ...props +}) => { + const timelineQuery = usePublicTimeline({ local, remote, instance }); - return ; + return ; }; interface IHashtagTimelineColumn extends IBaseTimeline { hashtag: string; } -const HashtagTimelineColumn: React.FC = ({ hashtag }) => { +const HashtagTimelineColumn: React.FC = ({ hashtag, ...props }) => { const timelineQuery = useHashtagTimeline(hashtag); - return ; + return ; }; interface ILinkTimelineColumn extends IBaseTimeline { url: string; } -const LinkTimelineColumn: React.FC = ({ url }) => { +const LinkTimelineColumn: React.FC = ({ url, ...props }) => { const timelineQuery = useLinkTimeline(url); - return ; + return ; }; interface IListTimelineColumn extends IBaseTimeline { listId: string; } -const ListTimelineColumn: React.FC = ({ listId }) => { +const ListTimelineColumn: React.FC = ({ listId, ...props }) => { const timelineQuery = useListTimeline(listId); - return ; + return ; }; interface IGroupTimelineColumn extends IBaseTimeline { groupId: string; } -const GroupTimelineColumn: React.FC = ({ groupId }) => { +const GroupTimelineColumn: React.FC = ({ groupId, ...props }) => { const timelineQuery = useGroupTimeline(groupId); - return ; + return ; }; -const BubbleTimelineColumn: React.FC = () => { +const BubbleTimelineColumn: React.FC = (props) => { const timelineQuery = useBubbleTimeline(); - return ; + return ; }; interface IAntennaTimelineColumn extends IBaseTimeline { antennaId: string; } -const AntennaTimelineColumn: React.FC = ({ antennaId }) => { +const AntennaTimelineColumn: React.FC = ({ antennaId, ...props }) => { const timelineQuery = useAntennaTimeline(antennaId); - return ; + return ; }; interface ICircleTimelineColumn extends IBaseTimeline { circleId: string; } -const CircleTimelineColumn: React.FC = ({ circleId }) => { +const CircleTimelineColumn: React.FC = ({ circleId, ...props }) => { const timelineQuery = useCircleTimeline(circleId); - return ; + return ; }; -const WrenchedTimelineColumn: React.FC = () => { +const WrenchedTimelineColumn: React.FC = (props) => { const timelineQuery = useWrenchedTimeline(); - return ; + return ; }; interface IAccountTimelineColumn extends IBaseTimeline { @@ -398,10 +403,11 @@ interface IAccountTimelineColumn extends IBaseTimeline { const AccountTimelineColumn: React.FC = ({ accountId, excludeReplies = false, + ...props }) => { const timelineQuery = useAccountTimeline(accountId, { exclude_replies: excludeReplies }); - return ; + return ; }; export { From 7984a47a29942db28a2979f464b548ee9199ddad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 17:21:21 +0100 Subject: [PATCH 45/89] nicolium: what about testing stuff before committing? MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/columns/timeline.tsx | 8 ++++++-- .../nicolium/src/components/scrollable-list.tsx | 4 ++++ .../nicolium/src/queries/timelines/use-timeline.ts | 11 ++++++++--- .../nicolium/src/queries/timelines/use-timelines.ts | 9 +++++++-- packages/nicolium/src/stores/timelines.ts | 13 ++++++++++--- 5 files changed, 35 insertions(+), 10 deletions(-) diff --git a/packages/nicolium/src/columns/timeline.tsx b/packages/nicolium/src/columns/timeline.tsx index e9d031df3..ea4b747ba 100644 --- a/packages/nicolium/src/columns/timeline.tsx +++ b/packages/nicolium/src/columns/timeline.tsx @@ -220,14 +220,17 @@ const TimelineStatus: React.FC = (props): React.JSX.Element => ); }; -type IBaseTimeline = Pick; +type IBaseTimeline = Pick< + IScrollableList, + 'emptyMessageIcon' | 'emptyMessageText' | 'onTopItemChanged' +>; interface ITimeline extends IBaseTimeline { query: ReturnType; contextType?: FilterContextType; } -const Timeline: React.FC = ({ query, contextType = 'public' }) => { +const Timeline: React.FC = ({ query, contextType = 'public', ...props }) => { const node = useRef(null); const { @@ -293,6 +296,7 @@ const Timeline: React.FC = ({ query, contextType = 'public' }) => { ref={node} hasMore={hasNextPage} onLoadMore={fetchNextPage} + {...props} > {(entries || []).map(renderEntry)} diff --git a/packages/nicolium/src/components/scrollable-list.tsx b/packages/nicolium/src/components/scrollable-list.tsx index 51275a7da..74af0acef 100644 --- a/packages/nicolium/src/components/scrollable-list.tsx +++ b/packages/nicolium/src/components/scrollable-list.tsx @@ -70,6 +70,8 @@ interface IScrollableList extends VirtuosoProps { onScrollToTop?: () => void; /** Callback when the list is scrolled. */ onScroll?: () => void; + /** Callback when the topmost visible item index changes. */ + onTopItemChanged?: (index: number) => void; /** Placeholder component to render while loading. */ placeholderComponent?: React.ComponentType | React.NamedExoticComponent; /** Number of placeholders to render while loading. */ @@ -104,6 +106,7 @@ const ScrollableList = React.forwardRef( onScroll, onScrollToTop, onLoadMore, + onTopItemChanged, className, listClassName, itemClassName, @@ -220,6 +223,7 @@ const ScrollableList = React.forwardRef( // HACK: using the first index can be buggy. // Track the second item instead, unless the endIndex comes before it (eg one 1 item in view). topIndex.current = Math.min(range.startIndex + 1, range.endIndex); + onTopItemChanged?.(topIndex.current); handleScroll(); }; diff --git a/packages/nicolium/src/queries/timelines/use-timeline.ts b/packages/nicolium/src/queries/timelines/use-timeline.ts index 8667ca0b8..fd1e23148 100644 --- a/packages/nicolium/src/queries/timelines/use-timeline.ts +++ b/packages/nicolium/src/queries/timelines/use-timeline.ts @@ -14,7 +14,12 @@ interface StreamConfig { params?: StreamingParams; } -const useTimeline = (timelineId: string, fetcher: TimelineFetcher, streamConfig?: StreamConfig) => { +const useTimeline = ( + timelineId: string, + fetcher: TimelineFetcher, + streamConfig?: StreamConfig, + restoring?: boolean, +) => { const timeline = useStoreTimeline(timelineId); const timelineActions = useTimelinesActions(); @@ -30,11 +35,11 @@ const useTimeline = (timelineId: string, fetcher: TimelineFetcher, streamConfig? try { const response = await fetcher(); importEntities({ statuses: response.items }); - timelineActions.expandTimeline(timelineId, response.items, !!response.next, true); + timelineActions.expandTimeline(timelineId, response.items, !!response.next, true, restoring); } catch (error) { timelineActions.setError(timelineId, true); } - }, [timelineId]); + }, [timelineId, restoring]); const fetchNextPage = useCallback(async () => { timelineActions.setLoading(timelineId, true); diff --git a/packages/nicolium/src/queries/timelines/use-timelines.ts b/packages/nicolium/src/queries/timelines/use-timelines.ts index f9c7a3e1a..070ae7fe7 100644 --- a/packages/nicolium/src/queries/timelines/use-timelines.ts +++ b/packages/nicolium/src/queries/timelines/use-timelines.ts @@ -17,14 +17,19 @@ import type { WrenchedTimelineParams, } from 'pl-api'; -const useHomeTimeline = (params?: Omit) => { +const useHomeTimeline = ( + params?: Omit, + maxId?: string, +) => { const client = useClient(); const stream = 'home'; return useTimeline( 'home', - (paginationParams) => client.timelines.homeTimeline({ ...params, ...paginationParams }), + (paginationParams) => + client.timelines.homeTimeline({ ...params, ...(paginationParams || { max_id: maxId }) }), { stream }, + !!maxId, ); }; diff --git a/packages/nicolium/src/stores/timelines.ts b/packages/nicolium/src/stores/timelines.ts index 4c26af603..80c0a2c00 100644 --- a/packages/nicolium/src/stores/timelines.ts +++ b/packages/nicolium/src/stores/timelines.ts @@ -23,8 +23,8 @@ type TimelineEntry = } | { type: 'gap'; - sinceId: string; - maxId: string; + sinceId?: string; + minId: string; }; interface TimelineData { @@ -46,6 +46,7 @@ interface State { statuses: Array, hasMore?: boolean, initialFetch?: boolean, + restoring?: boolean, ) => void; receiveStreamingStatus: (timelineId: string, status: Status) => void; deleteStatus: (statusId: string) => void; @@ -144,13 +145,19 @@ const useTimelinesStore = create()( mutative((set) => ({ timelines: {} as Record, actions: { - expandTimeline: (timelineId, statuses, hasMore, initialFetch = false) => + expandTimeline: (timelineId, statuses, hasMore, initialFetch = false, restoring = false) => set((state) => { const timeline = state.timelines[timelineId] ?? createEmptyTimeline(); const entries = processPage(statuses); if (initialFetch) timeline.entries = entries; else timeline.entries.push(...entries); + if (restoring) { + timeline.entries.unshift({ + type: 'gap', + minId: statuses[0].id, + }); + } timeline.isPending = false; timeline.isFetching = false; if (typeof hasMore === 'boolean') { From 0612e7303af9c562086f44247c07d7c210816ca5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 17:46:42 +0100 Subject: [PATCH 46/89] nicolium: WIP timeline position restoring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/columns/timeline.tsx | 22 +++++++++++++++++-- .../src/components/scrollable-list.tsx | 2 +- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/nicolium/src/columns/timeline.tsx b/packages/nicolium/src/columns/timeline.tsx index ea4b747ba..2d2a3110d 100644 --- a/packages/nicolium/src/columns/timeline.tsx +++ b/packages/nicolium/src/columns/timeline.tsx @@ -13,6 +13,7 @@ import Portal from '@/components/ui/portal'; import Emojify from '@/features/emoji/emojify'; import PlaceholderStatus from '@/features/placeholder/components/placeholder-status'; import PendingStatus from '@/features/ui/components/pending-status'; +import { useAppSelector } from '@/hooks/use-app-selector'; import { useFeatures } from '@/hooks/use-features'; import { useAccounts } from '@/queries/accounts/use-accounts'; import { type SelectedStatus, useStatus } from '@/queries/statuses/use-status'; @@ -305,9 +306,26 @@ const Timeline: React.FC = ({ query, contextType = 'public', ...props }; const HomeTimelineColumn: React.FC = (props) => { - const timelineQuery = useHomeTimeline(); + const me = useAppSelector((state) => state.me); + const marker = `nicolium:${me}:homeTimelinePosition`; + const restoredPosition = useRef(localStorage.getItem(marker)); - return ; + const timelineQuery = useHomeTimeline(undefined, restoredPosition.current || undefined); + + const handleTopItemChanged = (index: number) => { + const entry = timelineQuery.entries[index]; + if (!entry || entry.type !== 'status') return; + localStorage.setItem(marker, entry.originalId); + }; + + return ( + + ); }; interface IPublicTimelineColumn extends IBaseTimeline { diff --git a/packages/nicolium/src/components/scrollable-list.tsx b/packages/nicolium/src/components/scrollable-list.tsx index 74af0acef..d844cee2a 100644 --- a/packages/nicolium/src/components/scrollable-list.tsx +++ b/packages/nicolium/src/components/scrollable-list.tsx @@ -223,7 +223,7 @@ const ScrollableList = React.forwardRef( // HACK: using the first index can be buggy. // Track the second item instead, unless the endIndex comes before it (eg one 1 item in view). topIndex.current = Math.min(range.startIndex + 1, range.endIndex); - onTopItemChanged?.(topIndex.current); + onTopItemChanged?.(range.startIndex); handleScroll(); }; From a555c05900f799e4f47044a79f106616e9de763c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 18:24:22 +0100 Subject: [PATCH 47/89] nicolium: style fixes? MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/styles/new/statuses.scss | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/nicolium/src/styles/new/statuses.scss b/packages/nicolium/src/styles/new/statuses.scss index cfbd99038..76745f278 100644 --- a/packages/nicolium/src/styles/new/statuses.scss +++ b/packages/nicolium/src/styles/new/statuses.scss @@ -311,12 +311,14 @@ &--connected-bottom .status__content-wrapper { padding-left: 54px; } +} - &:not(.⁂-timeline-status--connected-bottom) { - @apply border-b border-solid border-gray-200 dark:border-gray-800; +div:has(> .⁂-timeline-status:not(.⁂-timeline-status--connected-bottom)) + div:has(> .⁂-timeline-status) { + .⁂-timeline-status { + border-top: 1px solid rgb(var(--color-gray-200)); + + &:is(.dark) { + border-top-color: rgb(var(--color-gray-800)); + } } } - -div:last-child > .⁂-timeline-status { - @apply border-b-0; -} From 8932659d46ebb81c91edb61d33e02e5de513a1ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 18:41:55 +0100 Subject: [PATCH 48/89] nicolium: make toasts not that much colorful MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/styles/new/layout.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/nicolium/src/styles/new/layout.scss b/packages/nicolium/src/styles/new/layout.scss index c957cffef..4493163df 100644 --- a/packages/nicolium/src/styles/new/layout.scss +++ b/packages/nicolium/src/styles/new/layout.scss @@ -594,15 +594,15 @@ body { @apply top-4; } - &--success svg { + &--success .⁂-toast__icon svg { @apply text-success-500 dark:text-success-400; } - &--info svg { + &--info .⁂-toast__icon svg { @apply text-primary-600 dark:text-primary-400; } - &--error svg { + &--error .⁂-toast__icon svg { @apply text-danger-600; } } From 6d331e67348c96ace8bbe048ef6b94d603ac790d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 20:41:20 +0100 Subject: [PATCH 49/89] nicolium: store home timeline position MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/columns/timeline.tsx | 144 +++++++++++++++++- packages/nicolium/src/locales/en.json | 11 ++ .../src/queries/timelines/use-timeline.ts | 33 +++- packages/nicolium/src/stores/timelines.ts | 53 ++++++- .../nicolium/src/styles/new/statuses.scss | 91 ++++++++++- 5 files changed, 322 insertions(+), 10 deletions(-) diff --git a/packages/nicolium/src/columns/timeline.tsx b/packages/nicolium/src/columns/timeline.tsx index 2d2a3110d..53375920e 100644 --- a/packages/nicolium/src/columns/timeline.tsx +++ b/packages/nicolium/src/columns/timeline.tsx @@ -1,15 +1,17 @@ import { Link } from '@tanstack/react-router'; import clsx from 'clsx'; -import React, { useRef } from 'react'; -import { defineMessages, FormattedList, FormattedMessage } from 'react-intl'; +import React, { useRef, useState } from 'react'; +import { defineMessages, FormattedList, FormattedMessage, useIntl } from 'react-intl'; import ScrollTopButton from '@/components/scroll-top-button'; import ScrollableList, { type IScrollableList } from '@/components/scrollable-list'; import Status, { StatusFollowedTagInfo } from '@/components/statuses/status'; import StatusInfo from '@/components/statuses/status-info'; import Tombstone from '@/components/statuses/tombstone'; +import Button from '@/components/ui/button'; import Icon from '@/components/ui/icon'; import Portal from '@/components/ui/portal'; +import Stack from '@/components/ui/stack'; import Emojify from '@/features/emoji/emojify'; import PlaceholderStatus from '@/features/placeholder/components/placeholder-status'; import PendingStatus from '@/features/ui/components/pending-status'; @@ -45,6 +47,14 @@ const messages = defineMessages({ id: 'status_list.queue_label.live_region', defaultMessage: '{count} new {count, plural, one {post} other {posts}}.', }, + gapExplanation: { + id: 'timeline.gap.explanation', + defaultMessage: 'Time elapsed between the two posts surrounding the gap.', + }, + gapExplanationFirst: { + id: 'timeline.gap.explanation_first', + defaultMessage: 'Time elapsed since the post before the gap.', + }, }); const PlaceholderTimelineStatus = () => ( @@ -65,6 +75,126 @@ const TimelinePendingStatus: React.FC = ({ idempotencyKe ); }; +interface ITimelineGap { + gap: Extract; + onFillGap: ( + gap: Extract, + direction: 'up' | 'down', + ) => Promise; + firstEntry: boolean; +} + +const TimelineGap: React.FC = ({ gap, onFillGap, firstEntry }) => { + const [isLoading, setIsLoading] = useState(false); + const intl = useIntl(); + + const handleFill = async (direction: 'up' | 'down') => { + setIsLoading(true); + try { + await onFillGap(gap, direction); + } finally { + setIsLoading(false); + } + }; + + const renderTimeDistance = () => { + console.log(gap); + if (!gap.minDate) return null; + + const maxDate = gap.maxIdDate ? new Date(gap.maxIdDate) : new Date(); + const minDate = new Date(gap.minDate); + + const diff = Math.abs(maxDate.getTime() - minDate.getTime()); + if (diff < 60 * 1000) { + return ( + + ); + } else if (diff < 60 * 60 * 1000) { + const minutes = Math.round(diff / (60 * 1000)); + return ( + + ); + } else if (diff < 24 * 60 * 60 * 1000) { + const hours = Math.round(diff / (60 * 60 * 1000)); + return ( + + ); + } else if (diff < 30 * 24 * 60 * 60 * 1000) { + const days = Math.round(diff / (24 * 60 * 60 * 1000)); + return ( + + ); + } else if (diff < 365 * 24 * 60 * 60 * 1000) { + const months = Math.round(diff / (30 * 24 * 60 * 60 * 1000)); + return ( + + ); + } else { + const years = Math.round(diff / (365 * 24 * 60 * 60 * 1000)); + return ( + + ); + } + }; + + return ( + + +
+ + {renderTimeDistance()} + +
+ +
+ ); +}; + interface ITimelineStatusInfo { status: SelectedStatus; rebloggedBy: Array; @@ -240,6 +370,7 @@ const Timeline: React.FC = ({ query, contextType = 'public', ...props queuedCount, fetchNextPage, dequeueEntries, + fillGap, isFetching, isPending, hasNextPage, @@ -274,6 +405,15 @@ const Timeline: React.FC = ({ query, contextType = 'public', ...props ); } else if (entry.type === 'pending-status') { return ; + } else if (entry.type === 'gap') { + return ( + + ); } }; diff --git a/packages/nicolium/src/locales/en.json b/packages/nicolium/src/locales/en.json index f19457ab0..a7be21615 100644 --- a/packages/nicolium/src/locales/en.json +++ b/packages/nicolium/src/locales/en.json @@ -797,6 +797,12 @@ "datepicker.next_year": "Next year", "datepicker.previous_month": "Previous month", "datepicker.previous_year": "Previous year", + "datetime.distance.days": "{distance} {distance, plural, one {day} other {days}}", + "datetime.distance.hours": "{distance} {distance, plural, one {hour} other {hours}}", + "datetime.distance.less_than_minute": "Less than a minute", + "datetime.distance.minutes": "{distance} {distance, plural, one {minute} other {minutes}}", + "datetime.distance.months": "{distance} {distance, plural, one {month} other {months}}", + "datetime.distance.years": "{distance} {distance, plural, one {year} other {years}}", "developers.navigation.app_create_label": "Create an app", "developers.navigation.intentional_error_label": "Trigger an error", "developers.navigation.network_error_label": "Network error", @@ -1980,6 +1986,11 @@ "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", "time_remaining.moments": "Moments remaining", "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", + "timeline.gap.explanation": "Time elapsed between the two posts surrounding the gap.", + "timeline.gap.explanation_first": "Time elapsed since the post before the gap.", + "timeline.gap.load_newer": "Load newer posts", + "timeline.gap.load_older": "Load older posts", + "timeline.gap.load_recent": "Load recent posts", "toast.view": "View", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "trends.title": "Trends", diff --git a/packages/nicolium/src/queries/timelines/use-timeline.ts b/packages/nicolium/src/queries/timelines/use-timeline.ts index fd1e23148..4aaa3aee8 100644 --- a/packages/nicolium/src/queries/timelines/use-timeline.ts +++ b/packages/nicolium/src/queries/timelines/use-timeline.ts @@ -2,11 +2,14 @@ import { useCallback, useEffect, useMemo } from 'react'; import { importEntities } from '@/actions/importer'; import { useTimelineStream } from '@/api/hooks/streaming/use-timeline-stream'; -import { useTimeline as useStoreTimeline, useTimelinesActions } from '@/stores/timelines'; +import { + useTimeline as useStoreTimeline, + useTimelinesActions, + type TimelineEntry, +} from '@/stores/timelines'; -import type { PaginatedResponse, Status, StreamingParams } from 'pl-api'; +import type { PaginatedResponse, PaginationParams, Status, StreamingParams } from 'pl-api'; -type PaginationParams = { max_id?: string; min_id?: string }; type TimelineFetcher = (params?: PaginationParams) => Promise>; interface StreamConfig { @@ -59,9 +62,29 @@ const useTimeline = ( timelineActions.dequeueEntries(timelineId); }, [timelineId]); + const fillGap = useCallback( + async (gap: Extract, direction: 'up' | 'down') => { + let params: PaginationParams; + if (direction === 'up') { + const gapIndex = timeline.entries.indexOf(gap); + const previousEntry = gapIndex > 0 ? timeline.entries[gapIndex - 1] : null; + const maxId = + previousEntry && previousEntry.type === 'status' ? previousEntry.originalId : undefined; + params = { min_id: gap.minId, max_id: maxId }; + } else { + params = { max_id: gap.maxId, since_id: gap.minId }; + } + + const response = await fetcher(params); + importEntities({ statuses: response.items }); + timelineActions.fillGap(timelineId, gap.minId, response.items, !!response.next, direction); + }, + [timelineId, fetcher], + ); + return useMemo( - () => ({ ...timeline, timelineId, fetchNextPage, dequeueEntries }), - [timeline, timelineId, fetchNextPage, dequeueEntries], + () => ({ ...timeline, timelineId, fetchNextPage, dequeueEntries, fillGap }), + [timeline, timelineId, fetchNextPage, dequeueEntries, fillGap], ); }; diff --git a/packages/nicolium/src/stores/timelines.ts b/packages/nicolium/src/stores/timelines.ts index 80c0a2c00..4a26fe9c0 100644 --- a/packages/nicolium/src/stores/timelines.ts +++ b/packages/nicolium/src/stores/timelines.ts @@ -23,8 +23,10 @@ type TimelineEntry = } | { type: 'gap'; - sinceId?: string; + maxId?: string; + maxIdDate?: string; minId: string; + minDate: string; }; interface TimelineData { @@ -53,6 +55,13 @@ interface State { setLoading: (timelineId: string, isFetching: boolean) => void; setError: (timelineId: string, isError: boolean) => void; dequeueEntries: (timelineId: string) => void; + fillGap: ( + timelineId: string, + gapMinId: string, + statuses: Array, + hasMore: boolean, + direction: 'up' | 'down', + ) => void; importPendingStatus: (params: CreateStatusParams, idempotencyKey: string) => void; replacePendingStatus: (idempotencyKey: string, newId: string) => void; deletePendingStatus: (idempotencyKey: string) => void; @@ -156,6 +165,7 @@ const useTimelinesStore = create()( timeline.entries.unshift({ type: 'gap', minId: statuses[0].id, + minDate: statuses[0].created_at, }); } timeline.isPending = false; @@ -227,6 +237,47 @@ const useTimelinesStore = create()( timeline.queuedEntries = []; timeline.queuedCount = 0; }), + fillGap: (timelineId, gapMinId, statuses, hasMore, direction) => + set((state) => { + const timeline = state.timelines[timelineId]; + if (!timeline) return; + + const gapIndex = timeline.entries.findIndex( + (e) => e.type === 'gap' && e.minId === gapMinId, + ); + if (gapIndex === -1) return; + + const gap = timeline.entries[gapIndex] as Extract; + const newEntries = processPage(statuses); + + timeline.entries.splice(gapIndex, 1); + + if (direction === 'up') { + if (hasMore && statuses.length > 0) { + const remainingGap: TimelineEntry = { + type: 'gap', + maxId: gap.maxId, + maxIdDate: gap.maxIdDate, + minId: statuses[0].id, + minDate: statuses[0].created_at, + }; + timeline.entries.splice(gapIndex, 0, remainingGap, ...newEntries); + } else { + timeline.entries.splice(gapIndex, 0, ...newEntries); + } + } else if (hasMore && statuses.length > 0) { + const remainingGap: TimelineEntry = { + type: 'gap', + maxId: statuses.at(-1)?.id, + maxIdDate: statuses.at(-1)?.created_at, + minId: gap.minId, + minDate: gap.minDate, + }; + timeline.entries.splice(gapIndex, 0, ...newEntries, remainingGap); + } else { + timeline.entries.splice(gapIndex, 0, ...newEntries); + } + }), importPendingStatus: (params, idempotencyKey) => set((state) => { if (params.scheduled_at) return; diff --git a/packages/nicolium/src/styles/new/statuses.scss b/packages/nicolium/src/styles/new/statuses.scss index 76745f278..333b8abb5 100644 --- a/packages/nicolium/src/styles/new/statuses.scss +++ b/packages/nicolium/src/styles/new/statuses.scss @@ -307,17 +307,104 @@ } } +.⁂-timeline-gap { + @apply my-4 border-y border-gray-200 dark:border-gray-800; + + --separator-border: var(--color-gray-200); + --separator-background: var(--color-primary-50); + + &:is(.dark *) { + --separator-border: var(--color-gray-800); + --separator-background: var(--color-gray-900); + } + + button { + margin: 0.5rem 0; + } + + &__separator { + margin: 0 -1rem; + height: 4rem; + + // Adapted from https://css-tricks.com/css-borders-using-masks/ + background: + conic-gradient( + from -45deg at bottom, + transparent, + rgb(var(--separator-border)) 1deg 89deg, + transparent 90deg + ) + bottom/2rem calc(50% + 1px) repeat-x, + conic-gradient( + from 135deg at top, + transparent, + rgb(var(--separator-border)) 1deg 89deg, + transparent 90deg + ) + top / 2rem calc(50% + 1px) repeat-x; + background-position-y: + calc(100% + 1px), + -1px; + position: relative; + + span { + position: absolute; + display: flex; + align-items: center; + justify-content: center; + top: 0; + height: 100%; + width: 100%; + text-align: center; + font-size: 0.875rem; + color: rgb(var(--color-primary-600)); + font-style: italic; + background: + conic-gradient( + from -45deg at bottom, + transparent, + rgb(var(--separator-background)) 1deg 89deg, + transparent 90deg + ) + bottom/2rem calc(50% + 1px) repeat-x, + conic-gradient( + from 135deg at top, + transparent, + rgb(var(--separator-background)) 1deg 89deg, + transparent 90deg + ) + top / 2rem calc(50% + 1px) repeat-x; + + &:is(.dark *) { + color: rgb(var(--color-primary-400)); + } + } + } +} + +div:first-child:has(> .⁂-timeline-gap) { + .⁂-timeline-gap { + margin-top: 0; + border-top: none; + + button:first-child { + margin-top: 0; + } + } +} + .⁂-timeline-status { &--connected-bottom .status__content-wrapper { padding-left: 54px; } } -div:has(> .⁂-timeline-status:not(.⁂-timeline-status--connected-bottom)) + div:has(> .⁂-timeline-status) { +div:has(> .⁂-timeline-status:not(.⁂-timeline-status--connected-bottom)) + + div:has(> .⁂-timeline-status) { .⁂-timeline-status { border-top: 1px solid rgb(var(--color-gray-200)); - &:is(.dark) { + &:is(.dark *) { border-top-color: rgb(var(--color-gray-800)); } } From 5731b975e3cfb5f7ed6b14e51d119146640f94cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 20:42:46 +0100 Subject: [PATCH 50/89] nicolium: add removed functionality back MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/columns/timeline.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/nicolium/src/columns/timeline.tsx b/packages/nicolium/src/columns/timeline.tsx index 53375920e..d490e5bd2 100644 --- a/packages/nicolium/src/columns/timeline.tsx +++ b/packages/nicolium/src/columns/timeline.tsx @@ -430,6 +430,7 @@ const Timeline: React.FC = ({ query, contextType = 'public', ...props } From 3da955dd7e659648a4637667e73fa340243d39a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 20:55:36 +0100 Subject: [PATCH 51/89] nicolium: restore pinned posts display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/columns/timeline.tsx | 83 +++++++++++++++++-- .../src/components/statuses/status-list.tsx | 80 ++---------------- .../src/pages/accounts/account-timeline.tsx | 5 +- 3 files changed, 87 insertions(+), 81 deletions(-) diff --git a/packages/nicolium/src/columns/timeline.tsx b/packages/nicolium/src/columns/timeline.tsx index d490e5bd2..0404905f7 100644 --- a/packages/nicolium/src/columns/timeline.tsx +++ b/packages/nicolium/src/columns/timeline.tsx @@ -1,6 +1,6 @@ import { Link } from '@tanstack/react-router'; import clsx from 'clsx'; -import React, { useRef, useState } from 'react'; +import React, { useMemo, useRef, useState } from 'react'; import { defineMessages, FormattedList, FormattedMessage, useIntl } from 'react-intl'; import ScrollTopButton from '@/components/scroll-top-button'; @@ -57,6 +57,18 @@ const messages = defineMessages({ }, }); +const SkipPinned: React.FC> = ({ onClick }) => { + return ( + + ); +}; + const PlaceholderTimelineStatus = () => (
@@ -284,6 +296,7 @@ interface ITimelineStatus { isConnectedBottom?: boolean; onMoveUp?: (id: string) => void | boolean; onMoveDown?: (id: string) => void | boolean; + featured?: boolean; } /** Status with reply-connector in threads. */ @@ -354,14 +367,21 @@ const TimelineStatus: React.FC = (props): React.JSX.Element => type IBaseTimeline = Pick< IScrollableList, 'emptyMessageIcon' | 'emptyMessageText' | 'onTopItemChanged' ->; +> & { + featuredStatusIds?: Array; +}; interface ITimeline extends IBaseTimeline { query: ReturnType; contextType?: FilterContextType; } -const Timeline: React.FC = ({ query, contextType = 'public', ...props }) => { +const Timeline: React.FC = ({ + query, + contextType = 'public', + featuredStatusIds, + ...props +}) => { const node = useRef(null); const { @@ -376,16 +396,36 @@ const Timeline: React.FC = ({ query, contextType = 'public', ...props hasNextPage, } = query; - const handleMoveUp = (index: number) => + const handleMoveUp = (index: number) => { + console.log(index); selectChild(index - 1, node, document.getElementById('status-list') ?? undefined); + }; - const handleMoveDown = (index: number) => + const handleMoveDown = (index: number) => { + console.log(index); selectChild( index + 1, node, document.getElementById('status-list') ?? undefined, entries.length, ); + }; + + const handleSkipPinned = () => { + const skipPinned = () => { + selectChild( + featuredStatusIds?.length ?? 0, + node, + document.getElementById('status-list') ?? undefined, + (featuredStatusIds?.length ?? 0) + entries.length, + 'start', + ); + }; + + skipPinned(); + + setTimeout(() => skipPinned, 0); + }; const renderEntry = (entry: TimelineEntry, index: number) => { if (entry.type === 'status') { @@ -417,6 +457,34 @@ const Timeline: React.FC = ({ query, contextType = 'public', ...props } }; + const renderedEntries = useMemo(() => { + const rendered = []; + + if (featuredStatusIds && featuredStatusIds.length > 0) { + for (const id of featuredStatusIds) { + const index = rendered.length; + rendered.push( + handleMoveUp(index)} + onMoveDown={() => handleMoveDown(index)} + rebloggedBy={[]} + timelineId={timelineId} + featured + />, + ); + } + } + + for (const entry of entries) { + rendered.push(renderEntry(entry, rendered.length)); + } + + return rendered; + }, [entries, contextType, timelineId, featuredStatusIds]); + return ( <> @@ -427,6 +495,9 @@ const Timeline: React.FC = ({ query, contextType = 'public', ...props liveRegionMessage={messages.queueLiveRegion} /> + {featuredStatusIds && featuredStatusIds.length > 3 && entries?.length > 0 && ( + + )} = ({ query, contextType = 'public', ...props onLoadMore={fetchNextPage} {...props} > - {(entries || []).map(renderEntry)} + {renderedEntries} ); diff --git a/packages/nicolium/src/components/statuses/status-list.tsx b/packages/nicolium/src/components/statuses/status-list.tsx index 49a85f1f2..1e82ad3d4 100644 --- a/packages/nicolium/src/components/statuses/status-list.tsx +++ b/packages/nicolium/src/components/statuses/status-list.tsx @@ -11,22 +11,8 @@ import PendingStatus from '@/features/ui/components/pending-status'; import { timelineToFilterContextType } from '@/queries/settings/use-filters'; import { selectChild } from '@/utils/scroll-utils'; -import Icon from '../ui/icon'; - import type { VirtuosoHandle } from 'react-virtuoso'; -const SkipPinned: React.FC> = ({ onClick }) => { - return ( - - ); -}; - interface IStatusList extends Omit { /** Unique key to preserve the scroll position when navigating back. */ scrollKey: string; @@ -34,8 +20,6 @@ interface IStatusList extends Omit { statusIds: Array; /** Last _unfiltered_ status ID (maxId) for pagination. */ lastStatusId?: string; - /** Pinned statuses to show at the top of the feed. */ - featuredStatusIds?: Array; /** Pagination callback when the end of the list is reached. */ onLoadMore?: (lastStatusId: string) => void; /** Whether the data is currently being fetched. */ @@ -54,7 +38,6 @@ interface IStatusList extends Omit { const StatusList: React.FC = ({ statusIds, lastStatusId, - featuredStatusIds, onLoadMore, timelineId, isLoading, @@ -67,23 +50,17 @@ const StatusList: React.FC = ({ const contextType = timelineToFilterContextType(timelineId); - const getFeaturedStatusCount = () => featuredStatusIds?.length ?? 0; - - const getCurrentStatusIndex = (id: string, featured: boolean): number => { - if (featured) { - return featuredStatusIds?.findIndex((key) => key === id) ?? 0; - } else { - return statusIds.findIndex((key) => key === id) + getFeaturedStatusCount(); - } + const getCurrentStatusIndex = (id: string): number => { + return statusIds.findIndex((key) => key === id); }; - const handleMoveUp = (id: string, featured: boolean = false) => { - const elementIndex = getCurrentStatusIndex(id, featured) - 1; + const handleMoveUp = (id: string) => { + const elementIndex = getCurrentStatusIndex(id) - 1; selectChild(elementIndex, node, document.getElementById('status-list') ?? undefined); }; - const handleMoveDown = (id: string, featured: boolean = false) => { - const elementIndex = getCurrentStatusIndex(id, featured) + 1; + const handleMoveDown = (id: string) => { + const elementIndex = getCurrentStatusIndex(id) + 1; selectChild( elementIndex, node, @@ -106,22 +83,6 @@ const StatusList: React.FC = ({ [onLoadMore, lastStatusId, statusIds.at(-1)], ); - const handleSkipPinned = () => { - const skipPinned = () => { - selectChild( - getFeaturedStatusCount(), - node, - document.getElementById('status-list') ?? undefined, - scrollableContent.length, - 'start', - ); - }; - - skipPinned(); - - setTimeout(() => skipPinned, 0); - }; - const renderLoadGap = (index: number) => { const ids = statusIds; const nextId = ids[index + 1]; @@ -155,23 +116,6 @@ const StatusList: React.FC = ({ }; const scrollableContent = useMemo(() => { - const renderFeaturedStatuses = (): React.ReactNode[] => { - if (!featuredStatusIds) return []; - - return featuredStatusIds.map((statusId) => ( - - )); - }; - const renderStatuses = (): React.ReactNode[] => { if (isLoading || statusIds.length > 0) { return statusIds.reduce((acc, statusId, index) => { @@ -193,15 +137,10 @@ const StatusList: React.FC = ({ } }; - const featuredStatuses = renderFeaturedStatuses(); const statuses = renderStatuses(); - if (featuredStatuses && statuses) { - return featuredStatuses.concat(statuses); - } else { - return statuses; - } - }, [featuredStatusIds, statusIds, isLoading, timelineId, showGroup]); + return statuses; + }, [statusIds, isLoading, timelineId, showGroup]); if (isPartial) { return ( @@ -226,9 +165,6 @@ const StatusList: React.FC = ({ return ( <> - {featuredStatusIds && featuredStatusIds.length > 3 && statusIds.length > 0 && ( - - )} { const features = useFeatures(); const { data: account, isPending } = useAccountLookup(username); - - const { data: _featuredStatusIds } = usePinnedStatuses(account?.id || ''); + const { data: featuredStatusIds } = usePinnedStatuses(account?.id || ''); const isBlocked = account?.relationship?.blocked_by && !features.blockersVisible; @@ -51,7 +50,7 @@ const AccountTimelinePage: React.FC = () => { } From 44841b0501563cf62e3a51afbabecef8bf24b0ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 21:10:49 +0100 Subject: [PATCH 52/89] nicolium: do not store marker position when on top MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/columns/timeline.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/nicolium/src/columns/timeline.tsx b/packages/nicolium/src/columns/timeline.tsx index 0404905f7..2593cd59f 100644 --- a/packages/nicolium/src/columns/timeline.tsx +++ b/packages/nicolium/src/columns/timeline.tsx @@ -526,6 +526,7 @@ const HomeTimelineColumn: React.FC = (props) => { const handleTopItemChanged = (index: number) => { const entry = timelineQuery.entries[index]; + if (index === 0) return localStorage.removeItem(marker); if (!entry || entry.type !== 'status') return; localStorage.setItem(marker, entry.originalId); }; From 411fb82df9535d603d0b2b52dbcce3cc17660a45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 21:57:20 +0100 Subject: [PATCH 53/89] nicolium: do not restore timeline from index===0 until 15 minutes passed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/columns/timeline.tsx | 42 ++++++++++++++++++---- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/packages/nicolium/src/columns/timeline.tsx b/packages/nicolium/src/columns/timeline.tsx index 2593cd59f..9490f61cd 100644 --- a/packages/nicolium/src/columns/timeline.tsx +++ b/packages/nicolium/src/columns/timeline.tsx @@ -517,18 +517,48 @@ const Timeline: React.FC = ({ ); }; +const savePosition = (me: string, entry: TimelineEntry, index: number) => { + if (entry.type !== 'status') return; + return localStorage.setItem( + `nicolium:${me}:homeTimelinePosition`, + `${entry.originalId}|${index}|${Date.now()}`, + ); +}; + +const getRestoredPosition = (me: string) => { + const markerKey = `nicolium:${me}:homeTimelinePosition`; + const marker = localStorage.getItem(markerKey); + if (!marker) return null; + + return marker.split('|'); +}; + const HomeTimelineColumn: React.FC = (props) => { const me = useAppSelector((state) => state.me); - const marker = `nicolium:${me}:homeTimelinePosition`; - const restoredPosition = useRef(localStorage.getItem(marker)); - const timelineQuery = useHomeTimeline(undefined, restoredPosition.current || undefined); + const maxId = useMemo(() => { + if (!me) return undefined; + + const position = getRestoredPosition(me); + + if (!position) return undefined; + + if (position[1] === '0') { + const timeDifference = Date.now() - parseInt(position[2], 10); + + if (timeDifference > 15 * 60 * 1000) { + return position[0]; + } + return undefined; + } + return position[0]; + }, []); + + const timelineQuery = useHomeTimeline(undefined, maxId); const handleTopItemChanged = (index: number) => { const entry = timelineQuery.entries[index]; - if (index === 0) return localStorage.removeItem(marker); - if (!entry || entry.type !== 'status') return; - localStorage.setItem(marker, entry.originalId); + if (me) savePosition(me, entry, index); }; return ( From af276da1be28665343ffda3c5ffe9ccead95db7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 6 Mar 2026 08:42:23 +0100 Subject: [PATCH 54/89] nicolium: i18n cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/components/site-logo.tsx | 2 +- .../features/ui/components/panels/account-note-panel.tsx | 2 +- packages/nicolium/src/locales/ar.json | 4 ++-- packages/nicolium/src/locales/ast.json | 4 ++-- packages/nicolium/src/locales/bg.json | 4 ++-- packages/nicolium/src/locales/bn.json | 4 ++-- packages/nicolium/src/locales/br.json | 4 ++-- packages/nicolium/src/locales/bs.json | 4 ++-- packages/nicolium/src/locales/ca.json | 4 ++-- packages/nicolium/src/locales/co.json | 4 ++-- packages/nicolium/src/locales/cs.json | 4 ++-- packages/nicolium/src/locales/cy.json | 4 ++-- packages/nicolium/src/locales/da.json | 4 ++-- packages/nicolium/src/locales/de.json | 4 ++-- packages/nicolium/src/locales/el.json | 4 ++-- packages/nicolium/src/locales/en-Shaw.json | 4 ++-- packages/nicolium/src/locales/en.json | 6 +++--- packages/nicolium/src/locales/eo.json | 4 ++-- packages/nicolium/src/locales/es-AR.json | 4 ++-- packages/nicolium/src/locales/es.json | 4 ++-- packages/nicolium/src/locales/et.json | 4 ++-- packages/nicolium/src/locales/eu.json | 4 ++-- packages/nicolium/src/locales/fa.json | 4 ++-- packages/nicolium/src/locales/fi.json | 4 ++-- packages/nicolium/src/locales/fr.json | 6 +++--- packages/nicolium/src/locales/ga.json | 4 ++-- packages/nicolium/src/locales/gl.json | 4 ++-- packages/nicolium/src/locales/he.json | 4 ++-- packages/nicolium/src/locales/hi.json | 4 ++-- packages/nicolium/src/locales/hr.json | 4 ++-- packages/nicolium/src/locales/hu.json | 4 ++-- packages/nicolium/src/locales/hy.json | 4 ++-- packages/nicolium/src/locales/id.json | 4 ++-- packages/nicolium/src/locales/io.json | 4 ++-- packages/nicolium/src/locales/is.json | 4 ++-- packages/nicolium/src/locales/it.json | 6 +++--- packages/nicolium/src/locales/ja.json | 4 ++-- packages/nicolium/src/locales/jv.json | 4 ++-- packages/nicolium/src/locales/ka.json | 4 ++-- packages/nicolium/src/locales/kk.json | 4 ++-- packages/nicolium/src/locales/ko.json | 4 ++-- packages/nicolium/src/locales/lt.json | 4 ++-- packages/nicolium/src/locales/lv.json | 4 ++-- packages/nicolium/src/locales/mk.json | 4 ++-- packages/nicolium/src/locales/ms.json | 4 ++-- packages/nicolium/src/locales/nl.json | 4 ++-- packages/nicolium/src/locales/nn.json | 4 ++-- packages/nicolium/src/locales/no.json | 4 ++-- packages/nicolium/src/locales/oc.json | 4 ++-- packages/nicolium/src/locales/pl.json | 6 +++--- packages/nicolium/src/locales/pt-BR.json | 4 ++-- packages/nicolium/src/locales/pt.json | 4 ++-- packages/nicolium/src/locales/ro.json | 4 ++-- packages/nicolium/src/locales/ru.json | 4 ++-- packages/nicolium/src/locales/sk.json | 4 ++-- packages/nicolium/src/locales/sl.json | 4 ++-- packages/nicolium/src/locales/sq.json | 4 ++-- packages/nicolium/src/locales/sr-Latn.json | 4 ++-- packages/nicolium/src/locales/sr.json | 4 ++-- packages/nicolium/src/locales/sv.json | 4 ++-- packages/nicolium/src/locales/ta.json | 6 +++--- packages/nicolium/src/locales/te.json | 4 ++-- packages/nicolium/src/locales/th.json | 4 ++-- packages/nicolium/src/locales/tr.json | 4 ++-- packages/nicolium/src/locales/uk.json | 4 ++-- packages/nicolium/src/locales/zh-CN.json | 6 +++--- packages/nicolium/src/locales/zh-HK.json | 4 ++-- packages/nicolium/src/locales/zh-TW.json | 4 ++-- packages/nicolium/src/pages/dashboard/account.tsx | 2 +- 69 files changed, 141 insertions(+), 141 deletions(-) diff --git a/packages/nicolium/src/components/site-logo.tsx b/packages/nicolium/src/components/site-logo.tsx index 57d25eade..2ef246dd0 100644 --- a/packages/nicolium/src/components/site-logo.tsx +++ b/packages/nicolium/src/components/site-logo.tsx @@ -5,7 +5,7 @@ import { defineMessages, useIntl } from 'react-intl'; import { useLogo } from '@/hooks/use-logo'; const messages = defineMessages({ - logo: { id: 'generic.logo', defaultMessage: 'Logo' }, + logo: { id: 'common.logo', defaultMessage: 'Logo' }, }); interface ISiteLogo extends React.ComponentProps<'img'> { diff --git a/packages/nicolium/src/features/ui/components/panels/account-note-panel.tsx b/packages/nicolium/src/features/ui/components/panels/account-note-panel.tsx index 86b735e43..ce013443d 100644 --- a/packages/nicolium/src/features/ui/components/panels/account-note-panel.tsx +++ b/packages/nicolium/src/features/ui/components/panels/account-note-panel.tsx @@ -60,7 +60,7 @@ const AccountNotePanel: React.FC = ({ account }) => { {saved && ( - + )}
diff --git a/packages/nicolium/src/locales/ar.json b/packages/nicolium/src/locales/ar.json index 67aad9b1b..8c3ef74c9 100644 --- a/packages/nicolium/src/locales/ar.json +++ b/packages/nicolium/src/locales/ar.json @@ -866,7 +866,7 @@ "gdpr.learn_more": "معرفة المزيد", "gdpr.message": "يستخدم {siteTitle} ملفات الكوكيز لدعم الجلسات وهي تعتبر حيوية لكي يعمل الموقع بشكل صحيح.", "gdpr.title": "موقع {siteTitle} يستخدم الكوكيز", - "generic.saved": "حُفظت", + "common.saved": "حُفظت", "getting_started.open_source_notice": "{code_name} هو برنامَج مفتوح المصدر. يمكنك المساهمة أو الإبلاغ عن الأخطاء على {code_link} (الإصدار {code_version}).", "group.banned.message": "أنت محظور من {group}", "group.cancel_request": "إلغاء الطلب", @@ -1489,7 +1489,7 @@ "reset_password.header": "تحديث كلمة المرور", "reset_password.password.label": "كلمة مرور", "reset_password.password.placeholder": "أُكتب هنا", - "save": "حفظ", + "common.save": "حفظ", "schedule.post_time": "تاريخ وساعة النشر", "schedule.remove": "الغاء الجدولة", "schedule_button.add_schedule": "جدولة النشر لوقت لاحق", diff --git a/packages/nicolium/src/locales/ast.json b/packages/nicolium/src/locales/ast.json index 69166e5d1..6a67e0d37 100644 --- a/packages/nicolium/src/locales/ast.json +++ b/packages/nicolium/src/locales/ast.json @@ -821,7 +821,7 @@ "gdpr.learn_more": "", "gdpr.message": "", "gdpr.title": "", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} ye software de códigu abiertu. Pues collaborar o informar de fallos en {code_link} (v{code_version}).", "group.cancel_request": "", "group.delete.success": "", @@ -1354,7 +1354,7 @@ "report.submit": "Submit", "report.target": "Report {target}", "reset_password.password.label": "Password", - "save": "", + "common.save": "", "schedule.post_time": "", "schedule.remove": "", "schedule_button.add_schedule": "", diff --git a/packages/nicolium/src/locales/bg.json b/packages/nicolium/src/locales/bg.json index 1d523bc26..065ef1491 100644 --- a/packages/nicolium/src/locales/bg.json +++ b/packages/nicolium/src/locales/bg.json @@ -891,7 +891,7 @@ "gdpr.learn_more": "", "gdpr.message": "", "gdpr.title": "", - "generic.saved": "Запазено", + "common.saved": "Запазено", "getting_started.open_source_notice": "{code_name} е софтуер с отворен код. Можете да помогнете или да докладвате за проблеми в GitHub: {code_link} (v{code_version}).", "group.cancel_request": "", "group.delete.success": "", @@ -1476,7 +1476,7 @@ "report.submit": "Подаване", "report.target": "", "reset_password.password.label": "Парола", - "save": "Запазване", + "common.save": "Запазване", "schedule.post_time": "", "schedule.remove": "", "schedule_button.add_schedule": "", diff --git a/packages/nicolium/src/locales/bn.json b/packages/nicolium/src/locales/bn.json index 9ac777e72..e08951508 100644 --- a/packages/nicolium/src/locales/bn.json +++ b/packages/nicolium/src/locales/bn.json @@ -821,7 +821,7 @@ "gdpr.learn_more": "", "gdpr.message": "", "gdpr.title": "", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} একটি মুক্ত সফটওয়্যার। তৈরিতে সাহায্য করতে বা কোনো সমস্যা সম্পর্কে জানাতে আমাদের গিটহাবে যেতে পারেন {code_link} (v{code_version})।", "group.cancel_request": "", "group.delete.success": "", @@ -1354,7 +1354,7 @@ "report.submit": "জমা দিন", "report.target": "{target} রিপোর্ট করুন", "reset_password.password.label": "Password", - "save": "", + "common.save": "", "schedule.post_time": "", "schedule.remove": "", "schedule_button.add_schedule": "", diff --git a/packages/nicolium/src/locales/br.json b/packages/nicolium/src/locales/br.json index cfc2c2abc..77c8e4a1e 100644 --- a/packages/nicolium/src/locales/br.json +++ b/packages/nicolium/src/locales/br.json @@ -819,7 +819,7 @@ "gdpr.learn_more": "", "gdpr.message": "", "gdpr.title": "", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "", "group.cancel_request": "", "group.delete.success": "", @@ -1351,7 +1351,7 @@ "report.submit": "Submit", "report.target": "Report {target}", "reset_password.password.label": "Password", - "save": "", + "common.save": "", "schedule.post_time": "", "schedule.remove": "", "schedule_button.add_schedule": "", diff --git a/packages/nicolium/src/locales/bs.json b/packages/nicolium/src/locales/bs.json index 95a81a0c1..69deca9bd 100644 --- a/packages/nicolium/src/locales/bs.json +++ b/packages/nicolium/src/locales/bs.json @@ -848,7 +848,7 @@ "gdpr.learn_more": "", "gdpr.message": "", "gdpr.title": "", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "", "group.cancel_request": "", "group.delete.success": "", @@ -1373,7 +1373,7 @@ "report.reason.title": "", "report.submit": "", "report.target": "", - "save": "", + "common.save": "", "schedule.post_time": "", "schedule.remove": "", "schedule_button.add_schedule": "", diff --git a/packages/nicolium/src/locales/ca.json b/packages/nicolium/src/locales/ca.json index 42ab72307..d0b976d2b 100644 --- a/packages/nicolium/src/locales/ca.json +++ b/packages/nicolium/src/locales/ca.json @@ -826,7 +826,7 @@ "gdpr.learn_more": "", "gdpr.message": "", "gdpr.title": "", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} és un programari de codi obert. Pots contribuir o informar de problemes a {code_link} (v{code_version}).", "group.cancel_request": "", "group.delete.success": "", @@ -1361,7 +1361,7 @@ "report.submit": "Enviar", "report.target": "Denuncies {target}", "reset_password.password.label": "Password", - "save": "", + "common.save": "", "schedule.post_time": "", "schedule.remove": "", "schedule_button.add_schedule": "", diff --git a/packages/nicolium/src/locales/co.json b/packages/nicolium/src/locales/co.json index eed6cba96..b285e241b 100644 --- a/packages/nicolium/src/locales/co.json +++ b/packages/nicolium/src/locales/co.json @@ -821,7 +821,7 @@ "gdpr.learn_more": "", "gdpr.message": "", "gdpr.title": "", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} ghjè un lugiziale liberu. Pudete cuntribuisce à u codice o a traduzione, o palisà un bug, nant'à GitHub: {code_link} (v{code_version}).", "group.cancel_request": "", "group.delete.success": "", @@ -1354,7 +1354,7 @@ "report.submit": "Mandà", "report.target": "Signalamentu", "reset_password.password.label": "Password", - "save": "", + "common.save": "", "schedule.post_time": "", "schedule.remove": "", "schedule_button.add_schedule": "", diff --git a/packages/nicolium/src/locales/cs.json b/packages/nicolium/src/locales/cs.json index 6c08f74f6..06ea23885 100644 --- a/packages/nicolium/src/locales/cs.json +++ b/packages/nicolium/src/locales/cs.json @@ -828,7 +828,7 @@ "gdpr.learn_more": "", "gdpr.message": "", "gdpr.title": "", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} je otevřený software. Na GitHubu k němu můžete přispět nebo nahlásit chyby: {code_link} (v{code_version}).", "group.cancel_request": "", "group.delete.success": "", @@ -1362,7 +1362,7 @@ "report.submit": "Odeslat", "report.target": "Nahlášení uživatele {target}", "reset_password.password.label": "Password", - "save": "Uložit", + "common.save": "Uložit", "schedule.post_time": "", "schedule.remove": "", "schedule_button.add_schedule": "Naplánovat na později", diff --git a/packages/nicolium/src/locales/cy.json b/packages/nicolium/src/locales/cy.json index 4b840d17e..a05e0fe89 100644 --- a/packages/nicolium/src/locales/cy.json +++ b/packages/nicolium/src/locales/cy.json @@ -829,7 +829,7 @@ "gdpr.learn_more": "", "gdpr.message": "", "gdpr.title": "", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "Mae {code_name} yn feddalwedd côd agored. Mae modd cyfrannu neu adrodd materion ar GitHub ar {code_link} (v{code_version}).", "group.cancel_request": "", "group.delete.success": "", @@ -1370,7 +1370,7 @@ "report.submit": "Cyflwyno", "report.target": "Cwyno am {target}", "reset_password.password.label": "Password", - "save": "", + "common.save": "", "schedule.post_time": "", "schedule.remove": "", "schedule_button.add_schedule": "", diff --git a/packages/nicolium/src/locales/da.json b/packages/nicolium/src/locales/da.json index 59548090d..c9c1b8300 100644 --- a/packages/nicolium/src/locales/da.json +++ b/packages/nicolium/src/locales/da.json @@ -821,7 +821,7 @@ "gdpr.learn_more": "", "gdpr.message": "", "gdpr.title": "", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} er et open source software. Du kan bidrage eller rapporterer fejl på GitHub {code_link} (v{code_version}).", "group.cancel_request": "", "group.delete.success": "", @@ -1354,7 +1354,7 @@ "report.submit": "Indsend", "report.target": "Anmelder {target}", "reset_password.password.label": "Password", - "save": "", + "common.save": "", "schedule.post_time": "", "schedule.remove": "", "schedule_button.add_schedule": "", diff --git a/packages/nicolium/src/locales/de.json b/packages/nicolium/src/locales/de.json index c696c9af6..e2f4b37a6 100644 --- a/packages/nicolium/src/locales/de.json +++ b/packages/nicolium/src/locales/de.json @@ -858,7 +858,7 @@ "gdpr.learn_more": "Mehr erfahren", "gdpr.message": "{siteTitle} verwendet Sitzungscookies, die für das Funktionieren der Website unerlässlich sind.", "gdpr.title": "{siteTitle} verwendet Cookies", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} ist quelloffene Software. Du kannst auf GitHub unter {code_link} (v{code_version}) mitarbeiten oder Probleme melden.", "group.cancel_request": "Anfrage zurückziehen", "group.delete.success": "", @@ -1415,7 +1415,7 @@ "reset_password.header": "Neues Passwort festlegen", "reset_password.password.label": "Password", "reset_password.password.placeholder": "Platzhalter", - "save": "Speichern", + "common.save": "Speichern", "schedule.post_time": "Datum/Uhrzeit zum Absenden", "schedule.remove": "Ohne Verzögerung absenden", "schedule_button.add_schedule": "Für späteres Absenden planen", diff --git a/packages/nicolium/src/locales/el.json b/packages/nicolium/src/locales/el.json index ec208ab45..91ae1eafe 100644 --- a/packages/nicolium/src/locales/el.json +++ b/packages/nicolium/src/locales/el.json @@ -834,7 +834,7 @@ "gdpr.learn_more": "Learn more", "gdpr.message": "{siteTitle} uses session cookies, which are essential to the website's functioning.", "gdpr.title": "{siteTitle} uses cookies", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "Το {code_name} είναι ελεύθερο λογισμικό. Μπορείς να συνεισφέρεις ή να αναφέρεις ζητήματα στο GitHub στο {code_link} (v{code_version}).", "group.cancel_request": "", "group.delete.success": "", @@ -1383,7 +1383,7 @@ "reset_password.header": "Set New Password", "reset_password.password.label": "Password", "reset_password.password.placeholder": "Placeholder", - "save": "Save", + "common.save": "Save", "schedule.post_time": "Post Date/Time", "schedule.remove": "Remove schedule", "schedule_button.add_schedule": "Schedule post for later", diff --git a/packages/nicolium/src/locales/en-Shaw.json b/packages/nicolium/src/locales/en-Shaw.json index a4b13eb49..7a64da79f 100644 --- a/packages/nicolium/src/locales/en-Shaw.json +++ b/packages/nicolium/src/locales/en-Shaw.json @@ -835,7 +835,7 @@ "gdpr.learn_more": "Learn more", "gdpr.message": "{siteTitle} uses session cookies, which are essential to the website's functioning.", "gdpr.title": "{siteTitle} uses cookies", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} 𐑦𐑟 𐑴𐑐𐑩𐑯 𐑕𐑹𐑕 𐑕𐑪𐑓𐑑𐑢𐑺. 𐑿 𐑒𐑨𐑯 𐑒𐑩𐑯𐑑𐑮𐑦𐑚𐑿𐑑 𐑹 𐑮𐑦𐑐𐑹𐑑 𐑦𐑖𐑵𐑟 𐑨𐑑 {code_link} (v{code_version}).", "group.cancel_request": "", "group.delete.success": "", @@ -1387,7 +1387,7 @@ "reset_password.header": "Set New Password", "reset_password.password.label": "Password", "reset_password.password.placeholder": "Placeholder", - "save": "Save", + "common.save": "Save", "schedule.post_time": "𐑐𐑴𐑕𐑑 𐑛𐑱𐑑/𐑑𐑲𐑥", "schedule.remove": "𐑮𐑦𐑥𐑵𐑝 𐑖𐑧𐑡𐑵𐑤", "schedule_button.add_schedule": "𐑖𐑧𐑡𐑵𐑤 𐑐𐑴𐑕𐑑 𐑓 𐑤𐑱𐑑𐑼", diff --git a/packages/nicolium/src/locales/en.json b/packages/nicolium/src/locales/en.json index a7be21615..9e5f29d3b 100644 --- a/packages/nicolium/src/locales/en.json +++ b/packages/nicolium/src/locales/en.json @@ -571,6 +571,9 @@ "column_forbidden.title": "Forbidden", "common.add": "Add", "common.cancel": "Cancel", + "common.logo": "Logo", + "common.save": "Save", + "common.saved": "Saved", "compare_history_modal.header": "Edit history", "compose.character_counter.title": "Used {chars} out of {maxChars} {maxChars, plural, one {character} other {characters}}", "compose.clear_link_suggestion.body": "The link {url} likely includes tracking elements used to mark your online activity. They are not required for the URL to work. Do you want to remove them?", @@ -1135,8 +1138,6 @@ "frontend_config.sentry_dsn_label": "Sentry DSN", "frontend_config.tile_server_attribution_label": "Map tiles attribution", "frontend_config.tile_server_label": "Map tile server", - "generic.logo": "Logo", - "generic.saved": "Saved", "getting_started.open_source_notice": "{code_name} is open source software. You can contribute or report issues at {code_link} (v{code_version}).", "group.cancel_request": "Cancel request", "group.delete.success": "Group successfully deleted", @@ -1746,7 +1747,6 @@ "rss_feed_subscriptions.new.create_title": "Subscribe", "rss_feed_subscriptions.new.heading": "Subscribe to a new RSS feed", "rss_feed_subscriptions.new.title_placeholder": "RSS feed URL", - "save": "Save", "schedule.post_time": "Post date/time", "schedule.remove": "Remove schedule", "schedule_button.add_schedule": "Schedule post for later", diff --git a/packages/nicolium/src/locales/eo.json b/packages/nicolium/src/locales/eo.json index 1495c3b6c..77b685632 100644 --- a/packages/nicolium/src/locales/eo.json +++ b/packages/nicolium/src/locales/eo.json @@ -821,7 +821,7 @@ "gdpr.learn_more": "", "gdpr.message": "", "gdpr.title": "", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} estas malfermitkoda programo. Vi povas kontribui aŭ raporti problemojn en GitHub je {code_link} (v{code_version}).", "group.cancel_request": "", "group.delete.success": "", @@ -1354,7 +1354,7 @@ "report.submit": "Sendi", "report.target": "Signali {target}", "reset_password.password.label": "Password", - "save": "", + "common.save": "", "schedule.post_time": "", "schedule.remove": "", "schedule_button.add_schedule": "", diff --git a/packages/nicolium/src/locales/es-AR.json b/packages/nicolium/src/locales/es-AR.json index a36ec4b53..3114f6848 100644 --- a/packages/nicolium/src/locales/es-AR.json +++ b/packages/nicolium/src/locales/es-AR.json @@ -846,7 +846,7 @@ "gdpr.learn_more": "", "gdpr.message": "", "gdpr.title": "", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} es software libre. Podés contribuir o informar errores en {code_link} (v{code_version}).", "group.cancel_request": "", "group.delete.success": "", @@ -1380,7 +1380,7 @@ "report.submit": "Enviar", "report.target": "Denunciando a {target}", "reset_password.password.label": "Password", - "save": "", + "common.save": "", "schedule.post_time": "", "schedule.remove": "", "schedule_button.add_schedule": "", diff --git a/packages/nicolium/src/locales/es.json b/packages/nicolium/src/locales/es.json index 7425ab7cb..33678989e 100644 --- a/packages/nicolium/src/locales/es.json +++ b/packages/nicolium/src/locales/es.json @@ -866,7 +866,7 @@ "gdpr.learn_more": "Learn more", "gdpr.message": "{siteTitle} uses session cookies, which are essential to the website's functioning.", "gdpr.title": "{siteTitle} uses cookies", - "generic.saved": "Guardado", + "common.saved": "Guardado", "getting_started.open_source_notice": "{code_name} es software libre. Puedes contribuir o reportar errores en {code_link} (v{code_version}).", "group.banned.message": "Estás baneado del {group}", "group.cancel_request": "Cancelar solicitud", @@ -1491,7 +1491,7 @@ "reset_password.header": "Set New Password", "reset_password.password.label": "Password", "reset_password.password.placeholder": "Placeholder", - "save": "Save", + "common.save": "Save", "schedule.post_time": "Fecha y hora de publicación", "schedule.remove": "Eliminar programación", "schedule_button.add_schedule": "Programar publicación para más tarde", diff --git a/packages/nicolium/src/locales/et.json b/packages/nicolium/src/locales/et.json index 281f8d75f..032909646 100644 --- a/packages/nicolium/src/locales/et.json +++ b/packages/nicolium/src/locales/et.json @@ -821,7 +821,7 @@ "gdpr.learn_more": "", "gdpr.message": "", "gdpr.title": "", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} on avatud lähtekoodiga tarkvara. Saad panustada või teatada probleemidest GitHubis {code_link} (v{code_version}).", "group.cancel_request": "", "group.delete.success": "", @@ -1354,7 +1354,7 @@ "report.submit": "Saada", "report.target": "Teatamine {target} kohta", "reset_password.password.label": "Password", - "save": "", + "common.save": "", "schedule.post_time": "", "schedule.remove": "", "schedule_button.add_schedule": "", diff --git a/packages/nicolium/src/locales/eu.json b/packages/nicolium/src/locales/eu.json index 9ddd05eea..d73ec1894 100644 --- a/packages/nicolium/src/locales/eu.json +++ b/packages/nicolium/src/locales/eu.json @@ -821,7 +821,7 @@ "gdpr.learn_more": "", "gdpr.message": "", "gdpr.title": "", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} software librea da. Ekarpenak egin ditzakezu edo akatsen berri eman GitHub bidez: {code_link} (v{code_version}).", "group.cancel_request": "", "group.delete.success": "", @@ -1354,7 +1354,7 @@ "report.submit": "Bidali", "report.target": "{target} salatzen", "reset_password.password.label": "Password", - "save": "", + "common.save": "", "schedule.post_time": "", "schedule.remove": "", "schedule_button.add_schedule": "", diff --git a/packages/nicolium/src/locales/fa.json b/packages/nicolium/src/locales/fa.json index a8e2f0894..3e8606128 100644 --- a/packages/nicolium/src/locales/fa.json +++ b/packages/nicolium/src/locales/fa.json @@ -834,7 +834,7 @@ "gdpr.learn_more": "Learn more", "gdpr.message": "{siteTitle} uses session cookies, which are essential to the website's functioning.", "gdpr.title": "{siteTitle} uses cookies", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "ماستدون یک نرم‌افزار آزاد است. می‌توانید در ساخت آن مشارکت کنید یا مشکلاتش را در {code_link} (v{code_version}) گزارش دهید.", "group.cancel_request": "", "group.delete.success": "", @@ -1383,7 +1383,7 @@ "reset_password.header": "Set New Password", "reset_password.password.label": "Password", "reset_password.password.placeholder": "Placeholder", - "save": "Save", + "common.save": "Save", "schedule.post_time": "Post Date/Time", "schedule.remove": "Remove schedule", "schedule_button.add_schedule": "Schedule post for later", diff --git a/packages/nicolium/src/locales/fi.json b/packages/nicolium/src/locales/fi.json index fafd273a9..650e74318 100644 --- a/packages/nicolium/src/locales/fi.json +++ b/packages/nicolium/src/locales/fi.json @@ -821,7 +821,7 @@ "gdpr.learn_more": "", "gdpr.message": "", "gdpr.title": "", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} on avoimen lähdekoodin ohjelma. Voit avustaa tai raportoida ongelmia GitHubissa: {code_link} (v{code_version}).", "group.cancel_request": "", "group.delete.success": "", @@ -1354,7 +1354,7 @@ "report.submit": "Lähetä", "report.target": "Raportoidaan {target}", "reset_password.password.label": "Password", - "save": "", + "common.save": "", "schedule.post_time": "", "schedule.remove": "", "schedule_button.add_schedule": "", diff --git a/packages/nicolium/src/locales/fr.json b/packages/nicolium/src/locales/fr.json index 8329e8272..cd83efe7c 100644 --- a/packages/nicolium/src/locales/fr.json +++ b/packages/nicolium/src/locales/fr.json @@ -992,8 +992,8 @@ "gdpr.learn_more": "En savoir plus", "gdpr.message": "{siteTitle} utilise des cookies de session, qui sont essentiels au fonctionnement du site web.", "gdpr.title": "{siteTitle} utilise des cookies", - "generic.logo": "Logo", - "generic.saved": "Enregistré", + "common.logo": "Logo", + "common.saved": "Enregistré", "getting_started.footer_notice": "Fièrement conçu en Pologne. {emoji}", "getting_started.open_source_notice": "{code_name} est un logiciel libre. Vous pouvez contribuer et envoyer vos commentaires et rapports de bogues via {code_link} (v{code_version}).", "group.cancel_request": "Annuler la demande", @@ -1645,7 +1645,7 @@ "reset_password.header": "Set New Password", "reset_password.password.label": "Password", "reset_password.password.placeholder": "Placeholder", - "save": "Enregistrer", + "common.save": "Enregistrer", "schedule.post_time": "Date/Heure de la publication", "schedule.remove": "Annuler la programmation", "schedule_button.add_schedule": "Programmer pour plus tard", diff --git a/packages/nicolium/src/locales/ga.json b/packages/nicolium/src/locales/ga.json index 6e72ae2d7..f53039bfd 100644 --- a/packages/nicolium/src/locales/ga.json +++ b/packages/nicolium/src/locales/ga.json @@ -834,7 +834,7 @@ "gdpr.learn_more": "Learn more", "gdpr.message": "{siteTitle} uses session cookies, which are essential to the website's functioning.", "gdpr.title": "{siteTitle} uses cookies", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} is open source software. You can contribute or report issues at {code_link} (v{code_version}).", "group.cancel_request": "", "group.delete.success": "", @@ -1383,7 +1383,7 @@ "reset_password.header": "Set New Password", "reset_password.password.label": "Password", "reset_password.password.placeholder": "Placeholder", - "save": "Save", + "common.save": "Save", "schedule.post_time": "Post Date/Time", "schedule.remove": "Remove schedule", "schedule_button.add_schedule": "Schedule post for later", diff --git a/packages/nicolium/src/locales/gl.json b/packages/nicolium/src/locales/gl.json index d0cb33659..10aff2ac1 100644 --- a/packages/nicolium/src/locales/gl.json +++ b/packages/nicolium/src/locales/gl.json @@ -821,7 +821,7 @@ "gdpr.learn_more": "", "gdpr.message": "", "gdpr.title": "", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} é software de código aberto. Pode contribuír ou informar de fallos en {code_link} (v{code_version}).", "group.cancel_request": "", "group.delete.success": "", @@ -1355,7 +1355,7 @@ "report.submit": "Enviar", "report.target": "Informar {target}", "reset_password.password.label": "Password", - "save": "", + "common.save": "", "schedule.post_time": "", "schedule.remove": "", "schedule_button.add_schedule": "", diff --git a/packages/nicolium/src/locales/he.json b/packages/nicolium/src/locales/he.json index 50485d3f0..b91a8093c 100644 --- a/packages/nicolium/src/locales/he.json +++ b/packages/nicolium/src/locales/he.json @@ -859,7 +859,7 @@ "gdpr.learn_more": "Learn more", "gdpr.message": "{siteTitle} uses session cookies, which are essential to the website's functioning.", "gdpr.title": "{siteTitle} uses cookies", - "generic.saved": "נשמר", + "common.saved": "נשמר", "getting_started.open_source_notice": "סבוניה היא תוכנה חופשית (בקוד פתוח). ניתן לתרום או לדווח על בעיות בגיטהאב: {code_link} (v{code_version}).", "group.cancel_request": "בטל בקשה", "group.delete.success": "הקבוצה נמחקה בהצלחה", @@ -1446,7 +1446,7 @@ "reset_password.header": "Set New Password", "reset_password.password.label": "Password", "reset_password.password.placeholder": "Placeholder", - "save": "Save", + "common.save": "Save", "schedule.post_time": "פרסום תאריך/שעה", "schedule.remove": "הסר תזמון", "schedule_button.add_schedule": "תזמן פוסט מאוחר יותר", diff --git a/packages/nicolium/src/locales/hi.json b/packages/nicolium/src/locales/hi.json index c01059ade..615c95c63 100644 --- a/packages/nicolium/src/locales/hi.json +++ b/packages/nicolium/src/locales/hi.json @@ -834,7 +834,7 @@ "gdpr.learn_more": "Learn more", "gdpr.message": "{siteTitle} uses session cookies, which are essential to the website's functioning.", "gdpr.title": "{siteTitle} uses cookies", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} is open source software. You can contribute or report issues at {code_link} (v{code_version}).", "group.cancel_request": "", "group.delete.success": "", @@ -1383,7 +1383,7 @@ "reset_password.header": "Set New Password", "reset_password.password.label": "Password", "reset_password.password.placeholder": "Placeholder", - "save": "Save", + "common.save": "Save", "schedule.post_time": "Post Date/Time", "schedule.remove": "Remove schedule", "schedule_button.add_schedule": "Schedule post for later", diff --git a/packages/nicolium/src/locales/hr.json b/packages/nicolium/src/locales/hr.json index be2b09a03..e38bd9ffe 100644 --- a/packages/nicolium/src/locales/hr.json +++ b/packages/nicolium/src/locales/hr.json @@ -857,7 +857,7 @@ "gdpr.learn_more": "Learn more", "gdpr.message": "{siteTitle} uses session cookies, which are essential to the website's functioning.", "gdpr.title": "{siteTitle} uses cookies", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} je softver otvorenog koda. Možeš doprinijeti ili prijaviti probleme na {code_link} (v{code_version}).", "group.cancel_request": "", "group.delete.success": "", @@ -1405,7 +1405,7 @@ "reset_password.header": "Postavite novu lozinku", "reset_password.password.label": "Lozinka", "reset_password.password.placeholder": "Placeholder", - "save": "Save", + "common.save": "Save", "schedule.post_time": "Post Date/Time", "schedule.remove": "Remove schedule", "schedule_button.add_schedule": "Schedule post for later", diff --git a/packages/nicolium/src/locales/hu.json b/packages/nicolium/src/locales/hu.json index 632e05490..642da317b 100644 --- a/packages/nicolium/src/locales/hu.json +++ b/packages/nicolium/src/locales/hu.json @@ -834,7 +834,7 @@ "gdpr.learn_more": "Learn more", "gdpr.message": "{siteTitle} uses session cookies, which are essential to the website's functioning.", "gdpr.title": "{siteTitle} uses cookies", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "A {code_name} nyílt forráskódú szoftver. Csatlakozhatsz a fejlesztéshez vagy jelenthetsz problémákat GitHub-on {code_link} (v{code_version}).", "group.cancel_request": "", "group.delete.success": "", @@ -1383,7 +1383,7 @@ "reset_password.header": "Set New Password", "reset_password.password.label": "Password", "reset_password.password.placeholder": "Placeholder", - "save": "Save", + "common.save": "Save", "schedule.post_time": "Post Date/Time", "schedule.remove": "Remove schedule", "schedule_button.add_schedule": "Schedule post for later", diff --git a/packages/nicolium/src/locales/hy.json b/packages/nicolium/src/locales/hy.json index 7e206bfc9..2f3fa2641 100644 --- a/packages/nicolium/src/locales/hy.json +++ b/packages/nicolium/src/locales/hy.json @@ -819,7 +819,7 @@ "gdpr.learn_more": "", "gdpr.message": "", "gdpr.title": "", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "Մաստոդոնը բաց ելատեքստով ծրագրակազմ է։ Կարող ես ներդրում անել կամ վրեպներ զեկուցել ԳիթՀաբում՝ {code_link} (v{code_version})։", "group.cancel_request": "", "group.delete.success": "", @@ -1351,7 +1351,7 @@ "report.submit": "Ուղարկել", "report.target": "Բողոքել {target}֊ի մասին", "reset_password.password.label": "Password", - "save": "", + "common.save": "", "schedule.post_time": "", "schedule.remove": "", "schedule_button.add_schedule": "", diff --git a/packages/nicolium/src/locales/id.json b/packages/nicolium/src/locales/id.json index 79445f2f5..fc67476a5 100644 --- a/packages/nicolium/src/locales/id.json +++ b/packages/nicolium/src/locales/id.json @@ -864,7 +864,7 @@ "gdpr.learn_more": "Pelajari selengkapnya", "gdpr.message": "{siteTitle} menggunakan cookie sesi, yang penting untuk fungsi situs web.", "gdpr.title": "{siteTitle} menggunakan cookies", - "generic.saved": "Disimpan", + "common.saved": "Disimpan", "getting_started.open_source_notice": "{code_name} adalah perangkat lunak yang bersifat terbuka. Anda dapat berkontribusi atau melaporkan permasalahan/bug di Gitlab {code_link} (v{code_version}).", "group.banned.message": "Anda dicekal dari {group}", "group.cancel_request": "Batalkan permintaan", @@ -1478,7 +1478,7 @@ "reset_password.header": "Tetapkan Kata Sandi Baru", "reset_password.password.label": "Password", "reset_password.password.placeholder": "Penyangga", - "save": "Simpang", + "common.save": "Simpang", "schedule.post_time": "Tanggal/Waktu Posting", "schedule.remove": "Hapus jadwal", "schedule_button.add_schedule": "Jadwalkan posting untuk nanti", diff --git a/packages/nicolium/src/locales/io.json b/packages/nicolium/src/locales/io.json index c3cd2692f..888d823ab 100644 --- a/packages/nicolium/src/locales/io.json +++ b/packages/nicolium/src/locales/io.json @@ -834,7 +834,7 @@ "gdpr.learn_more": "Learn more", "gdpr.message": "{siteTitle} uses session cookies, which are essential to the website's functioning.", "gdpr.title": "{siteTitle} uses cookies", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} esas programaro kun apertita kodexo. Tu povas kontributar o signalar problemi en GitHub ye {code_link} (v{code_version}).", "group.cancel_request": "", "group.delete.success": "", @@ -1383,7 +1383,7 @@ "reset_password.header": "Set New Password", "reset_password.password.label": "Password", "reset_password.password.placeholder": "Placeholder", - "save": "Save", + "common.save": "Save", "schedule.post_time": "Post Date/Time", "schedule.remove": "Remove schedule", "schedule_button.add_schedule": "Schedule post for later", diff --git a/packages/nicolium/src/locales/is.json b/packages/nicolium/src/locales/is.json index d3731f46a..f6e8a8138 100644 --- a/packages/nicolium/src/locales/is.json +++ b/packages/nicolium/src/locales/is.json @@ -834,7 +834,7 @@ "gdpr.learn_more": "Learn more", "gdpr.message": "{siteTitle} uses session cookies, which are essential to the website's functioning.", "gdpr.title": "{siteTitle} uses cookies", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} er opinn og frjáls hugbúnaður. Þú getur lagt þitt af mörkum eða tilkynnt um vandamál á {code_link} (v{code_version}).", "group.cancel_request": "", "group.delete.success": "", @@ -1383,7 +1383,7 @@ "reset_password.header": "Stilla nýtt lykilorð", "reset_password.password.label": "Password", "reset_password.password.placeholder": "Placeholder", - "save": "Save", + "common.save": "Save", "schedule.post_time": "Færslu dagsetning/tími", "schedule.remove": "Eyða tímasetningu", "schedule_button.add_schedule": "Tímasetja færslu síðar", diff --git a/packages/nicolium/src/locales/it.json b/packages/nicolium/src/locales/it.json index dfbb88f74..08bab731a 100644 --- a/packages/nicolium/src/locales/it.json +++ b/packages/nicolium/src/locales/it.json @@ -901,8 +901,8 @@ "gdpr.learn_more": "Maggiori informazioni", "gdpr.message": "{siteTitle} usa i cookie tecnici, quelli essenziali al funzionamento.", "gdpr.title": "{siteTitle} usa i cookie", - "generic.logo": "Logo", - "generic.saved": "Salvata", + "common.logo": "Logo", + "common.saved": "Salvata", "getting_started.open_source_notice": "{code_name} è un software open source. Puoi contribuire o segnalare errori su GitHub all'indirizzo {code_link} (v{code_version}).", "group.banned.message": "Hai ricevuto il ban da {group}", "group.cancel_request": "Cancella richiesta", @@ -1527,7 +1527,7 @@ "reset_password.header": "Ripristino password", "reset_password.password.label": "Password", "reset_password.password.placeholder": "Segnaposto", - "save": "Salva", + "common.save": "Salva", "schedule.post_time": "Orario e data di pubblicazione", "schedule.remove": "Rimuovi pianificazione", "schedule_button.add_schedule": "Pianifica la pubblicazione in futuro", diff --git a/packages/nicolium/src/locales/ja.json b/packages/nicolium/src/locales/ja.json index 88aa3df06..808ac2742 100644 --- a/packages/nicolium/src/locales/ja.json +++ b/packages/nicolium/src/locales/ja.json @@ -860,7 +860,7 @@ "gdpr.learn_more": "もっと知る", "gdpr.message": "{siteTitle} はセッションクッキーを使用します。これはウェブサイトが機能するために必須です。", "gdpr.title": "{siteTitle} はクッキーを使用します", - "generic.saved": "保存しました", + "common.saved": "保存しました", "getting_started.open_source_notice": "{code_name}はオープンソースソフトウェアです。誰でもGitHub ( {code_link} (v{code_version}) ) から開発に参加したり、問題を報告したりできます。", "group.banned.message": "あなたは {group} から追放されました", "group.cancel_request": "申請をキャンセル", @@ -1462,7 +1462,7 @@ "reset_password.header": "新パスワードを設定", "reset_password.password.label": "Password", "reset_password.password.placeholder": "プレースホルダー", - "save": "保存", + "common.save": "保存", "schedule.post_time": "投票日付・時刻", "schedule.remove": "", "schedule_button.add_schedule": "", diff --git a/packages/nicolium/src/locales/jv.json b/packages/nicolium/src/locales/jv.json index bdd7c50a6..96c50dffb 100644 --- a/packages/nicolium/src/locales/jv.json +++ b/packages/nicolium/src/locales/jv.json @@ -823,7 +823,7 @@ "gdpr.learn_more": "", "gdpr.message": "", "gdpr.title": "", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "", "group.cancel_request": "", "group.delete.success": "", @@ -1348,7 +1348,7 @@ "report.reason.title": "", "report.submit": "", "report.target": "", - "save": "", + "common.save": "", "schedule.post_time": "", "schedule.remove": "", "schedule_button.add_schedule": "", diff --git a/packages/nicolium/src/locales/ka.json b/packages/nicolium/src/locales/ka.json index 8648f2bc4..08df151a6 100644 --- a/packages/nicolium/src/locales/ka.json +++ b/packages/nicolium/src/locales/ka.json @@ -821,7 +821,7 @@ "gdpr.learn_more": "", "gdpr.message": "", "gdpr.title": "", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "მასტოდონი ღია პროგრამაა. შეგიძლიათ შეუწყოთ ხელი ან შექმნათ პრობემის რეპორტი {code_link} (v{code_version})-ზე.", "group.cancel_request": "", "group.delete.success": "", @@ -1354,7 +1354,7 @@ "report.submit": "დასრულება", "report.target": "არეპორტებთ {target}", "reset_password.password.label": "Password", - "save": "", + "common.save": "", "schedule.post_time": "", "schedule.remove": "", "schedule_button.add_schedule": "", diff --git a/packages/nicolium/src/locales/kk.json b/packages/nicolium/src/locales/kk.json index 5a03ca34c..3f4d38480 100644 --- a/packages/nicolium/src/locales/kk.json +++ b/packages/nicolium/src/locales/kk.json @@ -834,7 +834,7 @@ "gdpr.learn_more": "Learn more", "gdpr.message": "{siteTitle} uses session cookies, which are essential to the website's functioning.", "gdpr.title": "{siteTitle} uses cookies", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} - ашық кодты құрылым. Түзету енгізу немесе ұсыныстарды GitHub арқылы жасаңыз {code_link} (v{code_version}).", "group.cancel_request": "", "group.delete.success": "", @@ -1383,7 +1383,7 @@ "reset_password.header": "Set New Password", "reset_password.password.label": "Password", "reset_password.password.placeholder": "Placeholder", - "save": "Save", + "common.save": "Save", "schedule.post_time": "Post Date/Time", "schedule.remove": "Remove schedule", "schedule_button.add_schedule": "Schedule post for later", diff --git a/packages/nicolium/src/locales/ko.json b/packages/nicolium/src/locales/ko.json index af44783b2..95db10651 100644 --- a/packages/nicolium/src/locales/ko.json +++ b/packages/nicolium/src/locales/ko.json @@ -834,7 +834,7 @@ "gdpr.learn_more": "Learn more", "gdpr.message": "{siteTitle} uses session cookies, which are essential to the website's functioning.", "gdpr.title": "{siteTitle} uses cookies", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name}은 오픈 소스 소프트웨어입니다. 누구나 GitHub({code_link} (v{code_version}))에서 개발에 참여하거나, 문제를 보고할 수 있습니다.", "group.cancel_request": "", "group.delete.success": "", @@ -1383,7 +1383,7 @@ "reset_password.header": "Set New Password", "reset_password.password.label": "Password", "reset_password.password.placeholder": "Placeholder", - "save": "Save", + "common.save": "Save", "schedule.post_time": "Post Date/Time", "schedule.remove": "Remove schedule", "schedule_button.add_schedule": "Schedule post for later", diff --git a/packages/nicolium/src/locales/lt.json b/packages/nicolium/src/locales/lt.json index 29afe73d4..9f6b11246 100644 --- a/packages/nicolium/src/locales/lt.json +++ b/packages/nicolium/src/locales/lt.json @@ -834,7 +834,7 @@ "gdpr.learn_more": "Learn more", "gdpr.message": "{siteTitle} uses session cookies, which are essential to the website's functioning.", "gdpr.title": "{siteTitle} uses cookies", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} is open source software. You can contribute or report issues at {code_link} (v{code_version}).", "group.cancel_request": "", "group.delete.success": "", @@ -1383,7 +1383,7 @@ "reset_password.header": "Set New Password", "reset_password.password.label": "Password", "reset_password.password.placeholder": "Placeholder", - "save": "Save", + "common.save": "Save", "schedule.post_time": "Post Date/Time", "schedule.remove": "Remove schedule", "schedule_button.add_schedule": "Schedule post for later", diff --git a/packages/nicolium/src/locales/lv.json b/packages/nicolium/src/locales/lv.json index a61f098ca..29476e39b 100644 --- a/packages/nicolium/src/locales/lv.json +++ b/packages/nicolium/src/locales/lv.json @@ -834,7 +834,7 @@ "gdpr.learn_more": "Learn more", "gdpr.message": "{siteTitle} uses session cookies, which are essential to the website's functioning.", "gdpr.title": "{siteTitle} uses cookies", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} is open source software. You can contribute or report issues at {code_link} (v{code_version}).", "group.cancel_request": "", "group.delete.success": "", @@ -1383,7 +1383,7 @@ "reset_password.header": "Set New Password", "reset_password.password.label": "Password", "reset_password.password.placeholder": "Placeholder", - "save": "Save", + "common.save": "Save", "schedule.post_time": "Post Date/Time", "schedule.remove": "Remove schedule", "schedule_button.add_schedule": "Schedule post for later", diff --git a/packages/nicolium/src/locales/mk.json b/packages/nicolium/src/locales/mk.json index fa88e7288..85c0c2524 100644 --- a/packages/nicolium/src/locales/mk.json +++ b/packages/nicolium/src/locales/mk.json @@ -834,7 +834,7 @@ "gdpr.learn_more": "Learn more", "gdpr.message": "{siteTitle} uses session cookies, which are essential to the website's functioning.", "gdpr.title": "{siteTitle} uses cookies", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} is open source software. You can contribute or report issues at {code_link} (v{code_version}).", "group.cancel_request": "", "group.delete.success": "", @@ -1383,7 +1383,7 @@ "reset_password.header": "Set New Password", "reset_password.password.label": "Password", "reset_password.password.placeholder": "Placeholder", - "save": "Save", + "common.save": "Save", "schedule.post_time": "Post Date/Time", "schedule.remove": "Remove schedule", "schedule_button.add_schedule": "Schedule post for later", diff --git a/packages/nicolium/src/locales/ms.json b/packages/nicolium/src/locales/ms.json index 3c9235735..28326f549 100644 --- a/packages/nicolium/src/locales/ms.json +++ b/packages/nicolium/src/locales/ms.json @@ -834,7 +834,7 @@ "gdpr.learn_more": "Learn more", "gdpr.message": "{siteTitle} uses session cookies, which are essential to the website's functioning.", "gdpr.title": "{siteTitle} uses cookies", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} is open source software. You can contribute or report issues at {code_link} (v{code_version}).", "group.cancel_request": "", "group.delete.success": "", @@ -1383,7 +1383,7 @@ "reset_password.header": "Set New Password", "reset_password.password.label": "Password", "reset_password.password.placeholder": "Placeholder", - "save": "Save", + "common.save": "Save", "schedule.post_time": "Post Date/Time", "schedule.remove": "Remove schedule", "schedule_button.add_schedule": "Schedule post for later", diff --git a/packages/nicolium/src/locales/nl.json b/packages/nicolium/src/locales/nl.json index 5ecdba1d1..d12e0010d 100644 --- a/packages/nicolium/src/locales/nl.json +++ b/packages/nicolium/src/locales/nl.json @@ -823,7 +823,7 @@ "gdpr.learn_more": "", "gdpr.message": "", "gdpr.title": "", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} is vrije software. Je kunt bijdragen of problemen melden op GitHub via {code_link} (v{code_version}).", "group.cancel_request": "", "group.delete.success": "", @@ -1356,7 +1356,7 @@ "report.submit": "Verzenden", "report.target": "Rapporteer {target}", "reset_password.password.label": "Password", - "save": "", + "common.save": "", "schedule.post_time": "", "schedule.remove": "", "schedule_button.add_schedule": "", diff --git a/packages/nicolium/src/locales/nn.json b/packages/nicolium/src/locales/nn.json index 23cab5e0c..7872d2f60 100644 --- a/packages/nicolium/src/locales/nn.json +++ b/packages/nicolium/src/locales/nn.json @@ -834,7 +834,7 @@ "gdpr.learn_more": "Learn more", "gdpr.message": "{siteTitle} uses session cookies, which are essential to the website's functioning.", "gdpr.title": "{siteTitle} uses cookies", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} har åpen kilde kode. Du kan hjelpe til med problemar på GitHub gjennom {code_link} (v{code_version}).", "group.cancel_request": "", "group.delete.success": "", @@ -1383,7 +1383,7 @@ "reset_password.header": "Set New Password", "reset_password.password.label": "Password", "reset_password.password.placeholder": "Placeholder", - "save": "Save", + "common.save": "Save", "schedule.post_time": "Post Date/Time", "schedule.remove": "Remove schedule", "schedule_button.add_schedule": "Schedule post for later", diff --git a/packages/nicolium/src/locales/no.json b/packages/nicolium/src/locales/no.json index 24ecb63ef..be00cb3d1 100644 --- a/packages/nicolium/src/locales/no.json +++ b/packages/nicolium/src/locales/no.json @@ -859,7 +859,7 @@ "gdpr.learn_more": "Lær mer", "gdpr.message": "{siteTitle} bruker øktinformasjonskapsler, som er avgjørende for at nettstedet skal fungere.", "gdpr.title": "{siteTitle} bruker informasjonskapsler", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} er fri programvare. Du kan bidra eller rapportere problemer på GitHub på {code_link} (v{code_version}).", "group.cancel_request": "Avbryt forespørsel", "group.delete.success": "Gruppe slettet", @@ -1442,7 +1442,7 @@ "reset_password.header": "Angi nytt passord", "reset_password.password.label": "Password", "reset_password.password.placeholder": "Plassholder", - "save": "Lagre", + "common.save": "Lagre", "schedule.post_time": "Innlegg Dato/tid", "schedule.remove": "Fjern tidsplan", "schedule_button.add_schedule": "Planlegg innlegg til senere", diff --git a/packages/nicolium/src/locales/oc.json b/packages/nicolium/src/locales/oc.json index fdb5d9fb3..02d58280f 100644 --- a/packages/nicolium/src/locales/oc.json +++ b/packages/nicolium/src/locales/oc.json @@ -834,7 +834,7 @@ "gdpr.learn_more": "Learn more", "gdpr.message": "{siteTitle} uses session cookies, which are essential to the website's functioning.", "gdpr.title": "{siteTitle} uses cookies", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} es un logicial liure. Podètz contribuir e mandar vòstres comentaris e rapòrt de bug via {code_link} (v{code_version}) sus GitHub.", "group.cancel_request": "", "group.delete.success": "", @@ -1383,7 +1383,7 @@ "reset_password.header": "Set New Password", "reset_password.password.label": "Password", "reset_password.password.placeholder": "Placeholder", - "save": "Save", + "common.save": "Save", "schedule.post_time": "Post Date/Time", "schedule.remove": "Remove schedule", "schedule_button.add_schedule": "Schedule post for later", diff --git a/packages/nicolium/src/locales/pl.json b/packages/nicolium/src/locales/pl.json index 6066a0aef..83135a681 100644 --- a/packages/nicolium/src/locales/pl.json +++ b/packages/nicolium/src/locales/pl.json @@ -1008,8 +1008,8 @@ "gdpr.learn_more": "Dowiedz się więcej", "gdpr.message": "{siteTitle} korzysta z ciasteczek sesji, które są niezbędne dla działania strony.", "gdpr.title": "{siteTitle} korzysta z ciasteczek", - "generic.logo": "Logo", - "generic.saved": "Zapisano", + "common.logo": "Logo", + "common.saved": "Zapisano", "getting_started.open_source_notice": "{code_name} jest oprogramowaniem o otwartym źródle. Możesz pomóc w rozwoju lub zgłaszać błędy na Codebergu tutaj: {code_link} (v{code_version}).", "group.cancel_request": "Zaniechaj zgłoszenie", "group.delete.success": "Pomyślnie usunięto grupę", @@ -1574,7 +1574,7 @@ "report.reason.title": "Powód zgłoszenia", "report.submit": "Wyślij", "report.target": "Zgłaszanie {target}", - "save": "Zapisz", + "common.save": "Zapisz", "schedule.post_time": "Data/godzina publikacji", "schedule.remove": "Usuń zaplanowany wpis", "schedule_button.add_schedule": "Zaplanuj wpis na później", diff --git a/packages/nicolium/src/locales/pt-BR.json b/packages/nicolium/src/locales/pt-BR.json index d7c325db9..ac298baf7 100644 --- a/packages/nicolium/src/locales/pt-BR.json +++ b/packages/nicolium/src/locales/pt-BR.json @@ -834,7 +834,7 @@ "gdpr.learn_more": "Learn more", "gdpr.message": "{siteTitle} uses session cookies, which are essential to the website's functioning.", "gdpr.title": "{siteTitle} uses cookies", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} é um software de código aberto. Você pode contribuir ou reportar problemas na página do GitHub do projeto: {code_link} (v{code_version}).", "group.cancel_request": "", "group.delete.success": "", @@ -1383,7 +1383,7 @@ "reset_password.header": "Set New Password", "reset_password.password.label": "Password", "reset_password.password.placeholder": "Placeholder", - "save": "Save", + "common.save": "Save", "schedule.post_time": "Post Date/Time", "schedule.remove": "Remove schedule", "schedule_button.add_schedule": "Schedule post for later", diff --git a/packages/nicolium/src/locales/pt.json b/packages/nicolium/src/locales/pt.json index 42e69e1fb..6a14c07e4 100644 --- a/packages/nicolium/src/locales/pt.json +++ b/packages/nicolium/src/locales/pt.json @@ -856,7 +856,7 @@ "gdpr.learn_more": "Learn more", "gdpr.message": "{siteTitle} uses session cookies, which are essential to the website's functioning.", "gdpr.title": "{siteTitle} uses cookies", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} é software de código aberto (open source). Podes contribuir ou reportar problemas no GitHub do projecto: {code_link} (v{code_version}).", "group.cancel_request": "", "group.delete.success": "", @@ -1405,7 +1405,7 @@ "reset_password.header": "Set New Password", "reset_password.password.label": "Password", "reset_password.password.placeholder": "Placeholder", - "save": "Guardar", + "common.save": "Guardar", "schedule.post_time": "Post Date/Time", "schedule.remove": "Remove schedule", "schedule_button.add_schedule": "Schedule post for later", diff --git a/packages/nicolium/src/locales/ro.json b/packages/nicolium/src/locales/ro.json index ae5a65797..a478c2b77 100644 --- a/packages/nicolium/src/locales/ro.json +++ b/packages/nicolium/src/locales/ro.json @@ -834,7 +834,7 @@ "gdpr.learn_more": "Learn more", "gdpr.message": "{siteTitle} uses session cookies, which are essential to the website's functioning.", "gdpr.title": "{siteTitle} uses cookies", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} este o rețea de socializare de tip open source. Puteți contribuii la dezvoltarea ei sau să semnalați erorile pe GitHub la {code_link} (v{code_version}).", "group.cancel_request": "", "group.delete.success": "", @@ -1383,7 +1383,7 @@ "reset_password.header": "Set New Password", "reset_password.password.label": "Password", "reset_password.password.placeholder": "Placeholder", - "save": "Save", + "common.save": "Save", "schedule.post_time": "Post Date/Time", "schedule.remove": "Remove schedule", "schedule_button.add_schedule": "Schedule post for later", diff --git a/packages/nicolium/src/locales/ru.json b/packages/nicolium/src/locales/ru.json index 704eb9a86..2af06a1e6 100644 --- a/packages/nicolium/src/locales/ru.json +++ b/packages/nicolium/src/locales/ru.json @@ -896,7 +896,7 @@ "gdpr.learn_more": "Learn more", "gdpr.message": "{siteTitle} uses session cookies, which are essential to the website's functioning.", "gdpr.title": "{siteTitle} uses cookies", - "generic.saved": "Сохранено", + "common.saved": "Сохранено", "getting_started.open_source_notice": "{code_name} — сервис с открытым исходным кодом. Вы можете внести вклад или сообщить о проблемах на GitHub: {code_link} (v{code_version}).", "group.banned.message": "Вас забанили в {group}", "group.cancel_request": "Отменить запрос", @@ -1510,7 +1510,7 @@ "reset_password.header": "Set New Password", "reset_password.password.label": "Password", "reset_password.password.placeholder": "Placeholder", - "save": "Save", + "common.save": "Save", "schedule.post_time": "Post Date/Time", "schedule.remove": "Remove schedule", "schedule_button.add_schedule": "Schedule post for later", diff --git a/packages/nicolium/src/locales/sk.json b/packages/nicolium/src/locales/sk.json index 5943cdf88..5ce613e1a 100644 --- a/packages/nicolium/src/locales/sk.json +++ b/packages/nicolium/src/locales/sk.json @@ -834,7 +834,7 @@ "gdpr.learn_more": "Learn more", "gdpr.message": "{siteTitle} uses session cookies, which are essential to the website's functioning.", "gdpr.title": "{siteTitle} uses cookies", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} je softvér s otvoreným kódom. Nahlásiť chyby, alebo prispievať môžeš na GitHube v {code_link} (v{code_version}).", "group.cancel_request": "", "group.delete.success": "", @@ -1383,7 +1383,7 @@ "reset_password.header": "Set New Password", "reset_password.password.label": "Password", "reset_password.password.placeholder": "Placeholder", - "save": "Save", + "common.save": "Save", "schedule.post_time": "Post Date/Time", "schedule.remove": "Remove schedule", "schedule_button.add_schedule": "Schedule post for later", diff --git a/packages/nicolium/src/locales/sl.json b/packages/nicolium/src/locales/sl.json index 4578c0891..62ec0f34a 100644 --- a/packages/nicolium/src/locales/sl.json +++ b/packages/nicolium/src/locales/sl.json @@ -825,7 +825,7 @@ "gdpr.learn_more": "", "gdpr.message": "", "gdpr.title": "", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} je odprtokodna programska oprema. Na GitHubu na {code_link} (v{code_version}) lahko prispevate ali poročate o napakah.", "group.cancel_request": "", "group.delete.success": "", @@ -1359,7 +1359,7 @@ "report.submit": "Pošlji", "report.target": "Prijavi {target}", "reset_password.password.label": "Password", - "save": "", + "common.save": "", "schedule.post_time": "", "schedule.remove": "", "schedule_button.add_schedule": "", diff --git a/packages/nicolium/src/locales/sq.json b/packages/nicolium/src/locales/sq.json index 984df4414..24a7064e7 100644 --- a/packages/nicolium/src/locales/sq.json +++ b/packages/nicolium/src/locales/sq.json @@ -821,7 +821,7 @@ "gdpr.learn_more": "", "gdpr.message": "", "gdpr.title": "", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name}-i është software me burim të hapur. Mund të jepni ndihmesë ose të njoftoni probleme në GitHub, te {code_link} (v{code_version}).", "group.cancel_request": "", "group.delete.success": "", @@ -1354,7 +1354,7 @@ "report.submit": "Parashtroje", "report.target": "Raportim i {target}", "reset_password.password.label": "Password", - "save": "", + "common.save": "", "schedule.post_time": "", "schedule.remove": "", "schedule_button.add_schedule": "", diff --git a/packages/nicolium/src/locales/sr-Latn.json b/packages/nicolium/src/locales/sr-Latn.json index 8465e2f10..945d1d679 100644 --- a/packages/nicolium/src/locales/sr-Latn.json +++ b/packages/nicolium/src/locales/sr-Latn.json @@ -834,7 +834,7 @@ "gdpr.learn_more": "Learn more", "gdpr.message": "{siteTitle} uses session cookies, which are essential to the website's functioning.", "gdpr.title": "{siteTitle} uses cookies", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} je softver otvorenog koda. Možete mu doprineti ili prijaviti probleme preko GitHub-a na {code_link} (v{code_version}).", "group.cancel_request": "", "group.delete.success": "", @@ -1383,7 +1383,7 @@ "reset_password.header": "Set New Password", "reset_password.password.label": "Password", "reset_password.password.placeholder": "Placeholder", - "save": "Save", + "common.save": "Save", "schedule.post_time": "Post Date/Time", "schedule.remove": "Remove schedule", "schedule_button.add_schedule": "Schedule post for later", diff --git a/packages/nicolium/src/locales/sr.json b/packages/nicolium/src/locales/sr.json index a602e74f8..a1c485c3e 100644 --- a/packages/nicolium/src/locales/sr.json +++ b/packages/nicolium/src/locales/sr.json @@ -834,7 +834,7 @@ "gdpr.learn_more": "Learn more", "gdpr.message": "{siteTitle} uses session cookies, which are essential to the website's functioning.", "gdpr.title": "{siteTitle} uses cookies", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} је софтвер отвореног кода. Можете му допринети или пријавити проблеме преко ГитХаба на {code_link} (v{code_version}).", "group.cancel_request": "", "group.delete.success": "", @@ -1383,7 +1383,7 @@ "reset_password.header": "Set New Password", "reset_password.password.label": "Password", "reset_password.password.placeholder": "Placeholder", - "save": "Save", + "common.save": "Save", "schedule.post_time": "Post Date/Time", "schedule.remove": "Remove schedule", "schedule_button.add_schedule": "Schedule post for later", diff --git a/packages/nicolium/src/locales/sv.json b/packages/nicolium/src/locales/sv.json index 2ea15a2fe..d793bd1f6 100644 --- a/packages/nicolium/src/locales/sv.json +++ b/packages/nicolium/src/locales/sv.json @@ -825,7 +825,7 @@ "gdpr.learn_more": "", "gdpr.message": "", "gdpr.title": "", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} är programvara med öppen källkod. Du kan bidra eller rapportera problem via GitHub på {code_link} (v{code_version}).", "group.cancel_request": "", "group.delete.success": "", @@ -1359,7 +1359,7 @@ "report.submit": "Skicka", "report.target": "Rapporterar {target}", "reset_password.password.label": "Password", - "save": "", + "common.save": "", "schedule.post_time": "", "schedule.remove": "", "schedule_button.add_schedule": "", diff --git a/packages/nicolium/src/locales/ta.json b/packages/nicolium/src/locales/ta.json index 3d1f67fce..438be8e64 100644 --- a/packages/nicolium/src/locales/ta.json +++ b/packages/nicolium/src/locales/ta.json @@ -910,8 +910,8 @@ "gdpr.learn_more": "மேலும் அறிக", "gdpr.message": "{siteTitle} அமர்வு குக்கீகளைப் பயன்படுத்துகிறது, அவை வலைத்தளத்தின் செயல்பாட்டிற்கு இன்றியமையாதவை.", "gdpr.title": "{siteTitle} குக்கீகளைப் பயன்படுத்துகிறது", - "generic.logo": "சின்னம்", - "generic.saved": "சேமிக்கப்பட்டது", + "common.logo": "சின்னம்", + "common.saved": "சேமிக்கப்பட்டது", "getting_started.footer_notice": "போலந்தில் பெருமையுடன் தயாரிக்கப்பட்டது. {emoji}", "getting_started.open_source_notice": "{code_name} திறந்த மூல மென்பொருள். GitHub இல் நீங்கள் பங்களிக்கவோ அல்லது புகார் அளிக்கவோ முடியும் {code_link} (v{code_version}).", "group.cancel_request": "கோரிக்கையை ரத்துசெய்", @@ -1525,7 +1525,7 @@ "report.submit": "சமர்ப்பிக்கவும்", "report.target": "Report {target}", "reset_password.password.label": "Password", - "save": "சேமி", + "common.save": "சேமி", "schedule.post_time": "இடுகை தேதி/நேரம்", "schedule.remove": "அட்டவணையை அகற்று", "schedule_button.add_schedule": "பின்னர் இடுகையிடவும்", diff --git a/packages/nicolium/src/locales/te.json b/packages/nicolium/src/locales/te.json index 93687efb5..c2b057532 100644 --- a/packages/nicolium/src/locales/te.json +++ b/packages/nicolium/src/locales/te.json @@ -825,7 +825,7 @@ "gdpr.learn_more": "", "gdpr.message": "", "gdpr.title": "", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "మాస్టొడొన్ ఓపెన్ సోర్స్ సాఫ్ట్వేర్. మీరు {code_link} (v{code_version}) వద్ద GitHub పై సమస్యలను నివేదించవచ్చు లేదా తోడ్పడచ్చు.", "group.cancel_request": "", "group.delete.success": "", @@ -1359,7 +1359,7 @@ "report.submit": "సమర్పించండి", "report.target": "{target}పై ఫిర్యాదు చేయండి", "reset_password.password.label": "Password", - "save": "", + "common.save": "", "schedule.post_time": "", "schedule.remove": "", "schedule_button.add_schedule": "", diff --git a/packages/nicolium/src/locales/th.json b/packages/nicolium/src/locales/th.json index 211bc7680..03bd9fc92 100644 --- a/packages/nicolium/src/locales/th.json +++ b/packages/nicolium/src/locales/th.json @@ -825,7 +825,7 @@ "gdpr.learn_more": "", "gdpr.message": "", "gdpr.title": "", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} เป็นซอฟต์แวร์เปิดต้นฉบับ คุณสามารถมีส่วนร่วมหรือรายงานปัญหาที่ GitHub ที่ {code_link} (v{code_version})", "group.cancel_request": "", "group.delete.success": "", @@ -1359,7 +1359,7 @@ "report.submit": "ส่ง", "report.target": "กำลังรายงาน {target}", "reset_password.password.label": "Password", - "save": "", + "common.save": "", "schedule.post_time": "", "schedule.remove": "", "schedule_button.add_schedule": "", diff --git a/packages/nicolium/src/locales/tr.json b/packages/nicolium/src/locales/tr.json index 06384afe3..e6ce27cad 100644 --- a/packages/nicolium/src/locales/tr.json +++ b/packages/nicolium/src/locales/tr.json @@ -863,7 +863,7 @@ "gdpr.learn_more": "Daha fazla bilgi edinin", "gdpr.message": "{siteTitle}, web sitesinin çalışması için gerekli olan oturum tanımlama bilgilerini kullanır.", "gdpr.title": "{siteTitle} çerezleri kullanır", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} açık kaynaklı bir yazılımdır. Gitlab {code_link} (v{code_version}).", "group.banned.message": "{group} gurubu sizi banladı", "group.cancel_request": "İsteği iptal et", @@ -1477,7 +1477,7 @@ "reset_password.header": "Yeni Şifre Belirle", "reset_password.password.label": "Şifre", "reset_password.password.placeholder": "Yer tutucu", - "save": "Kaydet", + "common.save": "Kaydet", "schedule.post_time": "Gönderme Tarihi/Saati", "schedule.remove": "Zamanlamayı kaldır", "schedule_button.add_schedule": "Gönderiyi daha sonrası için zamanla", diff --git a/packages/nicolium/src/locales/uk.json b/packages/nicolium/src/locales/uk.json index 55f9acfb5..9c216178d 100644 --- a/packages/nicolium/src/locales/uk.json +++ b/packages/nicolium/src/locales/uk.json @@ -825,7 +825,7 @@ "gdpr.learn_more": "", "gdpr.message": "", "gdpr.title": "", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} — програмне забезпечення з відкритим кодом. Ви можете допомогти проєкту, або повідомити про проблеми на GitHub за адресою {code_link} (v{code_version}).", "group.cancel_request": "", "group.delete.success": "", @@ -1359,7 +1359,7 @@ "report.submit": "Відправити", "report.target": "Скаржимося на {target}", "reset_password.password.label": "Password", - "save": "", + "common.save": "", "schedule.post_time": "", "schedule.remove": "", "schedule_button.add_schedule": "", diff --git a/packages/nicolium/src/locales/zh-CN.json b/packages/nicolium/src/locales/zh-CN.json index 1100c4171..34ca6b3d4 100644 --- a/packages/nicolium/src/locales/zh-CN.json +++ b/packages/nicolium/src/locales/zh-CN.json @@ -1087,8 +1087,8 @@ "gdpr.learn_more": "了解更多", "gdpr.message": "{siteTitle} 使用会话cookie,这对网站的运作至关重要。", "gdpr.title": "{siteTitle} 使用cookies", - "generic.logo": "标志", - "generic.saved": "已保存", + "common.logo": "标志", + "common.saved": "已保存", "getting_started.footer_notice": "自豪地在波兰制造。{emoji}", "getting_started.open_source_notice": "{code_name} 是开源软件。欢迎前往 GitHub({code_link} (v{code_version}))贡献代码或反馈问题。", "group.banned.message": "您已被 {group} 封禁", @@ -1823,7 +1823,7 @@ "reset_password.header": "设置新的密码", "reset_password.password.label": "密码", "reset_password.password.placeholder": "占位符", - "save": "保存", + "common.save": "保存", "schedule.post_time": "发布日期/时间", "schedule.remove": "取消发布", "schedule_button.add_schedule": "定时发布", diff --git a/packages/nicolium/src/locales/zh-HK.json b/packages/nicolium/src/locales/zh-HK.json index 6ef0c3f6a..18dc2c258 100644 --- a/packages/nicolium/src/locales/zh-HK.json +++ b/packages/nicolium/src/locales/zh-HK.json @@ -833,7 +833,7 @@ "gdpr.learn_more": "", "gdpr.message": "", "gdpr.title": "", - "generic.saved": "", + "common.saved": "", "getting_started.open_source_notice": "{code_name} 是開源軟體。你可以在 GitHub {code_link} (v{code_version}) 上貢獻或是回報問題。", "group.cancel_request": "", "group.delete.success": "", @@ -1381,7 +1381,7 @@ "reset_password.fail": "令牌已過期,請重試", "reset_password.header": "設定新的密碼", "reset_password.password.label": "Password", - "save": "", + "common.save": "", "schedule.post_time": "發佈時間", "schedule.remove": "取消發佈", "schedule_button.add_schedule": "定時發佈", diff --git a/packages/nicolium/src/locales/zh-TW.json b/packages/nicolium/src/locales/zh-TW.json index 0c78e5451..782d5ba36 100644 --- a/packages/nicolium/src/locales/zh-TW.json +++ b/packages/nicolium/src/locales/zh-TW.json @@ -864,7 +864,7 @@ "gdpr.learn_more": "了解更多", "gdpr.message": "{siteTitle} 使用 cookie,這對於網站的運作至關重要。", "gdpr.title": "{siteTitle} 使用 cookie", - "generic.saved": "已儲存", + "common.saved": "已儲存", "getting_started.open_source_notice": "{code_name} 是開源軟體。您可以在 {code_link} (v{code_version}) 上貢獻或回報問題。", "group.banned.message": "您已被禁止加入 {group}", "group.cancel_request": "取消請求", @@ -1478,7 +1478,7 @@ "reset_password.header": "設定新的密碼", "reset_password.password.label": "密碼", "reset_password.password.placeholder": "佔位符", - "save": "儲存", + "common.save": "儲存", "schedule.post_time": "發布時間", "schedule.remove": "取消排程", "schedule_button.add_schedule": "安排稍後發布", diff --git a/packages/nicolium/src/pages/dashboard/account.tsx b/packages/nicolium/src/pages/dashboard/account.tsx index 229ebf0cc..cb328502c 100644 --- a/packages/nicolium/src/pages/dashboard/account.tsx +++ b/packages/nicolium/src/pages/dashboard/account.tsx @@ -303,7 +303,7 @@ const AdminAccountPage: React.FC = () => { From 2e737331f9172f3897d5e48c7efe6dc6853fdd08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 6 Mar 2026 12:04:18 +0100 Subject: [PATCH 55/89] nicolium: fix hotkeys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/features/ui/components/hotkeys.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nicolium/src/features/ui/components/hotkeys.tsx b/packages/nicolium/src/features/ui/components/hotkeys.tsx index ec70798cd..d1029ef1a 100644 --- a/packages/nicolium/src/features/ui/components/hotkeys.tsx +++ b/packages/nicolium/src/features/ui/components/hotkeys.tsx @@ -216,9 +216,9 @@ function useHotkeys(handlers: HandlerMap) { }); // Sort all matches by priority - matchCandidates.toSorted((a, b) => b.priority - a.priority); + const sortedCandidates = matchCandidates.toSorted((a, b) => b.priority - a.priority); - const bestMatchingHandler = matchCandidates.at(0)?.handler; + const bestMatchingHandler = sortedCandidates.at(0)?.handler; if (bestMatchingHandler) { const result = bestMatchingHandler(event); if (result !== false) { From 73c22ccf3fceb46f47da953f2286bac3fa605575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 6 Mar 2026 12:12:13 +0100 Subject: [PATCH 56/89] update oxlint config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/.oxlintrc.json | 3 ++- packages/pl-api/.oxlintrc.json | 3 ++- packages/pl-hooks/.oxlintrc.json | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/nicolium/.oxlintrc.json b/packages/nicolium/.oxlintrc.json index 971ee1fc3..945c21fb5 100644 --- a/packages/nicolium/.oxlintrc.json +++ b/packages/nicolium/.oxlintrc.json @@ -68,7 +68,8 @@ "typescript/consistent-type-exports": [ "error", { "fixMixedExportsWithInlineTypeSpecifier": true } - ] + ], + "typescript/no-import-type-side-effects": "error" }, "settings": { "jsx-a11y": { diff --git a/packages/pl-api/.oxlintrc.json b/packages/pl-api/.oxlintrc.json index f59b767bf..2b274b7cc 100644 --- a/packages/pl-api/.oxlintrc.json +++ b/packages/pl-api/.oxlintrc.json @@ -32,7 +32,8 @@ "typescript/consistent-type-exports": [ "error", { "fixMixedExportsWithInlineTypeSpecifier": true } - ] + ], + "typescript/no-import-type-side-effects": "error" }, "settings": { "jsx-a11y": { diff --git a/packages/pl-hooks/.oxlintrc.json b/packages/pl-hooks/.oxlintrc.json index 005767932..bac854602 100644 --- a/packages/pl-hooks/.oxlintrc.json +++ b/packages/pl-hooks/.oxlintrc.json @@ -51,7 +51,8 @@ "typescript/consistent-type-exports": [ "error", { "fixMixedExportsWithInlineTypeSpecifier": true } - ] + ], + "typescript/no-import-type-side-effects": "error" }, "settings": { "jsx-a11y": { From 032df3cab571c7cc95e06d13db982f9cd4488fef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 6 Mar 2026 14:11:19 +0100 Subject: [PATCH 57/89] nicolium: cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../nicolium/src/actions/frontend-config.ts | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/packages/nicolium/src/actions/frontend-config.ts b/packages/nicolium/src/actions/frontend-config.ts index 2e0bd7b2d..952cc8417 100644 --- a/packages/nicolium/src/actions/frontend-config.ts +++ b/packages/nicolium/src/actions/frontend-config.ts @@ -28,26 +28,22 @@ const fetchFrontendConfigurations = () => (dispatch: AppDispatch, getState: () = /** Conditionally fetches Nicolium config depending on backend features */ const fetchFrontendConfig = - (host: string | null) => (dispatch: AppDispatch, getState: () => RootState) => { + (host: string | null) => async (dispatch: AppDispatch, getState: () => RootState) => { const features = getState().auth.client.features; if (features.frontendConfigurations) { - return dispatch(fetchFrontendConfigurations()).then((data) => { - const legacyKey = 'pl_fe'; - const key = 'nicolium'; + const data = await dispatch(fetchFrontendConfigurations()); + const legacyKey = 'pl_fe'; + const key = 'nicolium'; - const foundData = data[key] || data[legacyKey]; + const foundData = data[key] || data[legacyKey]; - if (foundData) { - dispatch(importFrontendConfig(foundData, host)); - return foundData; - } else { - return dispatch(fetchFrontendConfigJson(host)); - } - }); - } else { - return dispatch(fetchFrontendConfigJson(host)); + if (foundData) { + dispatch(importFrontendConfig(foundData, host)); + return foundData; + } } + return dispatch(fetchFrontendConfigJson(host)); }; /** Tries to remember the config from browser storage before fetching it */ From 8fab2cb6e80e5e69c630f979fee408ff0e65442c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 6 Mar 2026 14:18:11 +0100 Subject: [PATCH 58/89] nicolium: fix streaming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/queries/timelines/use-timelines.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nicolium/src/queries/timelines/use-timelines.ts b/packages/nicolium/src/queries/timelines/use-timelines.ts index 070ae7fe7..73c255599 100644 --- a/packages/nicolium/src/queries/timelines/use-timelines.ts +++ b/packages/nicolium/src/queries/timelines/use-timelines.ts @@ -22,7 +22,7 @@ const useHomeTimeline = ( maxId?: string, ) => { const client = useClient(); - const stream = 'home'; + const stream = 'user'; return useTimeline( 'home', From 28397b8a0965d6c8c467dacd42c9d4c7fe021133 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 6 Mar 2026 14:39:09 +0100 Subject: [PATCH 59/89] nicolium: store user settings in self account notes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/actions/frontend-config.ts | 1 - packages/nicolium/src/actions/settings.ts | 13 ++++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/nicolium/src/actions/frontend-config.ts b/packages/nicolium/src/actions/frontend-config.ts index 952cc8417..acd1af99d 100644 --- a/packages/nicolium/src/actions/frontend-config.ts +++ b/packages/nicolium/src/actions/frontend-config.ts @@ -54,7 +54,6 @@ const loadFrontendConfig = () => async (dispatch: AppDispatch, getState: () => R if (result) { dispatch(fetchFrontendConfig(host)); - return; } else { return dispatch(fetchFrontendConfig(host)); } diff --git a/packages/nicolium/src/actions/settings.ts b/packages/nicolium/src/actions/settings.ts index f0db1562c..2abcc0bb6 100644 --- a/packages/nicolium/src/actions/settings.ts +++ b/packages/nicolium/src/actions/settings.ts @@ -73,7 +73,7 @@ const updateAuthAccount = async (url: string, settings: any) => { }; const updateSettingsStore = - (settings: any) => (dispatch: AppDispatch, getState: () => RootState) => { + (settings: any) => async (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); const client = getClient(state); @@ -85,6 +85,17 @@ const updateSettingsStore = }, }), ); + } else if (client.features.notes) { + const note = (await client.accounts.getRelationships([state.me as string]))[0]?.note; + const settingsNote = `${encodeURIComponent(JSON.stringify(settings))}`; + + if (/(.*)<\/nicolium-config>/.test(note || '')) { + const newNote = note!.replace(/(.*)<\/nicolium-config>/, settingsNote); + return client.accounts.updateAccountNote(state.me as string, newNote); + } else { + const newNote = `${note || ''}\n\n${settingsNote}`; + return client.accounts.updateAccountNote(state.me as string, newNote); + } } else { const accountUrl = selectOwnAccount(state)!.url; From c28ddc438dc90d3e9636d6936182c02439d5a610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 6 Mar 2026 14:39:59 +0100 Subject: [PATCH 60/89] nicolium: cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/actions/auth.ts | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/packages/nicolium/src/actions/auth.ts b/packages/nicolium/src/actions/auth.ts index 1af872aeb..294c3df60 100644 --- a/packages/nicolium/src/actions/auth.ts +++ b/packages/nicolium/src/actions/auth.ts @@ -7,7 +7,6 @@ * @see module:@/actions/security */ import { - credentialAccountSchema, PlApiClient, type Account, type CreateAccountParams, @@ -16,7 +15,6 @@ import { type Token, } from 'pl-api'; import { defineMessages } from 'react-intl'; -import * as v from 'valibot'; import { createAccount } from '@/actions/accounts'; import { createApp } from '@/actions/apps'; @@ -188,23 +186,9 @@ const verifyCredentials = return account; }) .catch((error) => { - if (error?.response?.status === 403 && error?.response?.json?.id) { - // The user is waitlisted - const account = error.response.json; - const parsedAccount = v.parse(credentialAccountSchema, error.response.json); - queryClient.setQueryData(queryKeys.accounts.show(parsedAccount.id), parsedAccount); - dispatch({ - type: VERIFY_CREDENTIALS_SUCCESS, - token, - account: parsedAccount, - }); - if (account.id === getState().me) dispatch(fetchMeSuccess(parsedAccount)); - return parsedAccount; - } else { - if (getState().me === null) dispatch(fetchMeFail(error)); - dispatch({ type: VERIFY_CREDENTIALS_FAIL, token, error }); - throw error; - } + if (getState().me === null) dispatch(fetchMeFail(error)); + dispatch({ type: VERIFY_CREDENTIALS_FAIL, token, error }); + throw error; }); }; From cf534e2927476cc83955b257223f442c46e7b2f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 6 Mar 2026 14:49:45 +0100 Subject: [PATCH 61/89] nicolium: load settings from own account notes on supported platforms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/actions/me.ts | 53 +++++++++++++++++------ packages/nicolium/src/actions/settings.ts | 2 + 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/packages/nicolium/src/actions/me.ts b/packages/nicolium/src/actions/me.ts index b86b0ab3d..5b27aa032 100644 --- a/packages/nicolium/src/actions/me.ts +++ b/packages/nicolium/src/actions/me.ts @@ -86,21 +86,48 @@ const patchMe = dispatch(patchMeSuccess(response)); }); -const fetchMeSuccess = (account: CredentialAccount) => { - setSentryAccount(account); +interface MeFetchSuccessAction { + type: typeof ME_FETCH_SUCCESS; + me: CredentialAccount; +} - useSettingsStore - .getState() - .actions.loadUserSettings( - account.settings_store?.[FE_NAME] || account.settings_store?.[LEGACY_FE_NAME], - ); - useComposeStore.getState().actions.importDefaultSettings(account); +const fetchMeSuccess = + (account: CredentialAccount) => async (dispatch: AppDispatch, getState: () => RootState) => { + const client = getClient(getState); - return { - type: ME_FETCH_SUCCESS, - me: account, + setSentryAccount(account); + + const settings = account.settings_store?.[FE_NAME] || account.settings_store?.[LEGACY_FE_NAME]; + + if (client.features.frontendConfigurations) { + useSettingsStore.getState().actions.loadUserSettings(settings); + } else if (client.features.notes) { + const note = await getClient(getState) + .accounts.getRelationships([account.id]) + .then((relationships) => relationships[0]?.note); + + if (note) { + const match = note.match(/(.*)<\/nicolium-config>/); + + if (match) { + try { + const frontendConfig = JSON.parse(decodeURIComponent(match[1])); + console.log(frontendConfig); + useSettingsStore.getState().actions.loadUserSettings(frontendConfig); + return frontendConfig; + } catch (error) { + console.error('Failed to parse frontend config from account note', error); + } + } + } + } + useComposeStore.getState().actions.importDefaultSettings(account); + + return dispatch({ + type: ME_FETCH_SUCCESS, + me: account, + }); }; -}; const fetchMeFail = (error: unknown) => ({ type: ME_FETCH_FAIL, @@ -123,7 +150,7 @@ const patchMeSuccess = (me: CredentialAccount) => (dispatch: AppDispatch) => { }; type MeAction = - | ReturnType + | MeFetchSuccessAction | ReturnType | MeFetchSkipAction | MePatchSuccessAction; diff --git a/packages/nicolium/src/actions/settings.ts b/packages/nicolium/src/actions/settings.ts index 2abcc0bb6..a835becb8 100644 --- a/packages/nicolium/src/actions/settings.ts +++ b/packages/nicolium/src/actions/settings.ts @@ -86,6 +86,8 @@ const updateSettingsStore = }), ); } else if (client.features.notes) { + // Inspired by Phanpy and designed for compatibility with other software doing this + // https://github.com/cheeaun/phanpy/commit/a8b5c8cd64d456d30aab09dc56da7e4e20100e67 const note = (await client.accounts.getRelationships([state.me as string]))[0]?.note; const settingsNote = `${encodeURIComponent(JSON.stringify(settings))}`; From 78c924776e0113ebef0ead92bc4862adc88bd64f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 6 Mar 2026 14:54:23 +0100 Subject: [PATCH 62/89] pl-api: reorder features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-api/lib/features.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/pl-api/lib/features.ts b/packages/pl-api/lib/features.ts index ac19a20e5..4c9142181 100644 --- a/packages/pl-api/lib/features.ts +++ b/packages/pl-api/lib/features.ts @@ -305,9 +305,9 @@ const getFeatures = (instance: Instance) => { */ accountNotifies: any([ v.software === AKKOMA, + v.software === GOTOSOCIAL, v.software === MASTODON, v.software === PLEROMA && gte(v.version, '2.5.0'), - v.software === GOTOSOCIAL, ]), /** @@ -1281,10 +1281,10 @@ const getFeatures = (instance: Instance) => { */ notes: any([ v.software === AKKOMA, + v.software === GOTOSOCIAL, v.software === ICESHRIMP_NET, v.software === MASTODON, v.software === PLEROMA && gte(v.version, '2.5.0'), - v.software === GOTOSOCIAL, ]), /** @@ -1314,13 +1314,13 @@ const getFeatures = (instance: Instance) => { notificationsIncludeTypes: any([ v.software === AKKOMA, v.software === FIREFISH, + v.software === GOTOSOCIAL, v.software === ICESHRIMP, v.software === ICESHRIMP_NET, v.software === MASTODON, v.software === NEODB, v.software === PLEROMA && gte(v.version, '2.5.0'), v.software === TAKAHE && gte(v.version, '0.6.2'), - v.software === GOTOSOCIAL, ]), /** @@ -1342,8 +1342,8 @@ const getFeatures = (instance: Instance) => { */ outgoingFollowRequests: any([ v.software === GOTOSOCIAL && gte(v.version, '0.20.0'), - v.software === MITRA && gte(v.version, '4.19.0'), v.software === ICESHRIMP_NET, + v.software === MITRA && gte(v.version, '4.19.0'), instance.api_versions['outgoing_follow_requests.pleroma.pl-api'] >= 1, ]), @@ -1536,10 +1536,10 @@ const getFeatures = (instance: Instance) => { */ richText: any([ v.software === AKKOMA, + v.software === GOTOSOCIAL, v.software === MASTODON && v.build === GLITCH, v.software === PLEROMA, v.software === MITRA, - v.software === GOTOSOCIAL, instance.pleroma.metadata.post_formats.filter((format) => format !== 'text/plain').length > 0, ]), @@ -1548,9 +1548,9 @@ const getFeatures = (instance: Instance) => { */ rssFeeds: any([ v.software === AKKOMA, + v.software === GOTOSOCIAL, v.software === MASTODON, v.software === PLEROMA, - v.software === GOTOSOCIAL, ]), /** @@ -1616,10 +1616,10 @@ const getFeatures = (instance: Instance) => { */ sessions: any([ v.software === AKKOMA, + v.software === GOTOSOCIAL && gte(v.version, '0.18.2'), v.software === ICESHRIMP_NET, v.software === MITRA && gt(v.version, '4.13.1'), v.software === PLEROMA, - v.software === GOTOSOCIAL && gte(v.version, '0.18.2'), ]), shoutbox: instance.api_versions['shout.pleroma.pl-api'] >= 1, From 38d0a43498af46f25abbb73813f0639b869cf2df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 6 Mar 2026 15:44:16 +0100 Subject: [PATCH 63/89] pl-api: remove unused code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/actions/accounts.ts | 22 ++-------------- .../src/queries/accounts/use-relationship.ts | 25 ------------------- 2 files changed, 2 insertions(+), 45 deletions(-) diff --git a/packages/nicolium/src/actions/accounts.ts b/packages/nicolium/src/actions/accounts.ts index 00c7aa1de..0b3f5adc4 100644 --- a/packages/nicolium/src/actions/accounts.ts +++ b/packages/nicolium/src/actions/accounts.ts @@ -1,11 +1,7 @@ import { getClient } from '@/api'; import type { AppDispatch, RootState } from '@/store'; -import type { CreateAccountParams, Relationship } from 'pl-api'; - -const ACCOUNT_BLOCK_SUCCESS = 'ACCOUNT_BLOCK_SUCCESS' as const; -const ACCOUNT_MUTE_SUCCESS = 'ACCOUNT_MUTE_SUCCESS' as const; -const ACCOUNT_UNFOLLOW_SUCCESS = 'ACCOUNT_UNFOLLOW_SUCCESS' as const; +import type { CreateAccountParams } from 'pl-api'; const createAccount = (params: CreateAccountParams) => (_dispatch: AppDispatch, getState: () => RootState) => @@ -13,18 +9,4 @@ const createAccount = .settings.createAccount(params) .then((response) => ({ params, response })); -type AccountsAction = { - type: - | typeof ACCOUNT_BLOCK_SUCCESS - | typeof ACCOUNT_MUTE_SUCCESS - | typeof ACCOUNT_UNFOLLOW_SUCCESS; - relationship: Relationship; -}; - -export { - ACCOUNT_BLOCK_SUCCESS, - ACCOUNT_MUTE_SUCCESS, - ACCOUNT_UNFOLLOW_SUCCESS, - createAccount, - type AccountsAction, -}; +export { createAccount }; diff --git a/packages/nicolium/src/queries/accounts/use-relationship.ts b/packages/nicolium/src/queries/accounts/use-relationship.ts index 946384340..dbca83b6f 100644 --- a/packages/nicolium/src/queries/accounts/use-relationship.ts +++ b/packages/nicolium/src/queries/accounts/use-relationship.ts @@ -1,14 +1,7 @@ import { useMutation, useQueries, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMemo } from 'react'; -import { - ACCOUNT_BLOCK_SUCCESS, - ACCOUNT_MUTE_SUCCESS, - ACCOUNT_UNFOLLOW_SUCCESS, - type AccountsAction, -} from '@/actions/accounts'; import { batcher } from '@/api/batcher'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useClient } from '@/hooks/use-client'; import { useLoggedIn } from '@/hooks/use-logged-in'; import { useOwnAccount } from '@/hooks/use-own-account'; @@ -123,7 +116,6 @@ const useFollowAccountMutation = (accountId: string) => { const useUnfollowAccountMutation = (accountId: string) => { const client = useClient(); - const dispatch = useAppDispatch(); const queryClient = useQueryClient(); return useMutation({ @@ -145,11 +137,6 @@ const useUnfollowAccountMutation = (accountId: string) => { }, onSuccess: (data) => { queryClient.setQueryData(queryKeys.accountRelationships.show(accountId), data); - - dispatch({ - type: ACCOUNT_UNFOLLOW_SUCCESS, - relationship: data, - }); }, }); }; @@ -157,7 +144,6 @@ const useUnfollowAccountMutation = (accountId: string) => { const useBlockAccountMutation = (accountId: string) => { const client = useClient(); const queryClient = useQueryClient(); - const dispatch = useAppDispatch(); const { filterContexts } = useContextsActions(); return useMutation({ @@ -194,11 +180,6 @@ const useBlockAccountMutation = (accountId: string) => { // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers filterContexts(data); useTimelinesStore.getState().actions.filterTimelines(data.id); - - return dispatch({ - type: ACCOUNT_BLOCK_SUCCESS, - relationship: data, - }); }, }); }; @@ -230,7 +211,6 @@ const useUnblockAccountMutation = (accountId: string) => { const useMuteAccountMutation = (accountId: string) => { const client = useClient(); const queryClient = useQueryClient(); - const dispatch = useAppDispatch(); const { filterContexts } = useContextsActions(); return useMutation({ @@ -263,11 +243,6 @@ const useMuteAccountMutation = (accountId: string) => { // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers filterContexts(data); useTimelinesStore.getState().actions.filterTimelines(data.id); - - return dispatch({ - type: ACCOUNT_MUTE_SUCCESS, - relationship: data, - }); }, }); }; From 279f1cb483e0bbeb081a478d7f4c511962248d4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 6 Mar 2026 15:54:27 +0100 Subject: [PATCH 64/89] nicolium: announcement emojis: respect reduced motion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/components/announcements/emoji.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/nicolium/src/components/announcements/emoji.tsx b/packages/nicolium/src/components/announcements/emoji.tsx index 3ce569d63..bb2705087 100644 --- a/packages/nicolium/src/components/announcements/emoji.tsx +++ b/packages/nicolium/src/components/announcements/emoji.tsx @@ -13,7 +13,7 @@ interface IEmoji { } const Emoji: React.FC = ({ emoji, emojiMap, hovered }) => { - const { autoPlayGif, systemEmojiFont } = useSettings(); + const { autoPlayGif, reduceMotion, systemEmojiFont } = useSettings(); if (unicodeMapping[emoji]) { if (systemEmojiFont) return <>{emoji}; @@ -31,7 +31,8 @@ const Emoji: React.FC = ({ emoji, emojiMap, hovered }) => { /> ); } else if (emojiMap[emoji]) { - const filename = autoPlayGif || hovered ? emojiMap[emoji].url : emojiMap[emoji].static_url; + const filename = + (autoPlayGif && !reduceMotion) || hovered ? emojiMap[emoji].url : emojiMap[emoji].static_url; const shortCode = `:${emoji}:`; return ( From 97ffcd88e2661397af33617c373c7e73d40b83b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 6 Mar 2026 16:03:34 +0100 Subject: [PATCH 65/89] nicolium: announcement reactions bar UI consistency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../src/components/animated-number.tsx | 2 +- .../src/components/announcements/reaction.tsx | 33 ++-- .../announcements/reactions-bar.tsx | 21 +- .../statuses/status-reactions-bar.tsx | 1 - .../status/components/detailed-status.tsx | 186 +++++++++--------- .../src/features/status/components/thread.tsx | 2 +- .../nicolium/src/styles/new/statuses.scss | 9 +- 7 files changed, 141 insertions(+), 113 deletions(-) diff --git a/packages/nicolium/src/components/animated-number.tsx b/packages/nicolium/src/components/animated-number.tsx index 99d786f4b..9ad13f274 100644 --- a/packages/nicolium/src/components/animated-number.tsx +++ b/packages/nicolium/src/components/animated-number.tsx @@ -85,7 +85,7 @@ const AnimatedNumber: React.FC = ({ value, obfuscate, short, ma }); if (reduceMotion) { - return <>{formattedValue}; + return formattedValue; } return ( diff --git a/packages/nicolium/src/components/announcements/reaction.tsx b/packages/nicolium/src/components/announcements/reaction.tsx index f01b6f8d5..0362617c9 100644 --- a/packages/nicolium/src/components/announcements/reaction.tsx +++ b/packages/nicolium/src/components/announcements/reaction.tsx @@ -1,6 +1,7 @@ import { animated, type AnimatedProps } from '@react-spring/web'; import clsx from 'clsx'; import React, { useState } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; import AnimatedNumber from '@/components/animated-number'; import unicodeMapping from '@/features/emoji/mapping'; @@ -10,6 +11,13 @@ import Emoji from './emoji'; import type { AnnouncementReaction, CustomEmoji } from 'pl-api'; +const messages = defineMessages({ + emojiCount: { + id: 'status.reactions.label', + defaultMessage: '{count} {count, plural, one {person} other {people}} reacted with {emoji}', + }, +}); + interface IReaction { announcementId: string; reaction: AnnouncementReaction; @@ -18,6 +26,7 @@ interface IReaction { } const Reaction: React.FC = ({ announcementId, reaction, emojiMap, style }) => { + const intl = useIntl(); const [hovered, setHovered] = useState(false); const { addReaction, removeReaction } = useAnnouncements(); @@ -46,25 +55,23 @@ const Reaction: React.FC = ({ announcementId, reaction, emojiMap, sty return ( - - - - + + +

- +

); }; diff --git a/packages/nicolium/src/components/announcements/reactions-bar.tsx b/packages/nicolium/src/components/announcements/reactions-bar.tsx index 463b1924c..479183782 100644 --- a/packages/nicolium/src/components/announcements/reactions-bar.tsx +++ b/packages/nicolium/src/components/announcements/reactions-bar.tsx @@ -1,15 +1,22 @@ import { useTransition } from '@react-spring/web'; import React from 'react'; +import { defineMessages, useIntl } from 'react-intl'; import EmojiPickerDropdown from '@/features/emoji/containers/emoji-picker-dropdown-container'; import { useAnnouncements } from '@/queries/announcements/use-announcements'; import { useSettings } from '@/stores/settings'; +import Icon from '../ui/icon'; + import Reaction from './reaction'; import type { Emoji, NativeEmoji } from '@/features/emoji'; import type { AnnouncementReaction, CustomEmoji } from 'pl-api'; +const messages = defineMessages({ + addEmoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' }, +}); + interface IReactionsBar { announcementId: string; reactions: Array; @@ -17,6 +24,7 @@ interface IReactionsBar { } const ReactionsBar: React.FC = ({ announcementId, reactions, emojiMap }) => { + const intl = useIntl(); const { reduceMotion } = useSettings(); const { addReaction } = useAnnouncements(); @@ -41,7 +49,7 @@ const ReactionsBar: React.FC = ({ announcementId, reactions, emoj }); return ( -
+
{transitions(({ scale }, reaction) => ( = ({ announcementId, reactions, emoj /> ))} - {visibleReactions.length < 8 && } + {visibleReactions.length < 8 && ( + + + + )}
); }; diff --git a/packages/nicolium/src/components/statuses/status-reactions-bar.tsx b/packages/nicolium/src/components/statuses/status-reactions-bar.tsx index 30e4e2395..30357438d 100644 --- a/packages/nicolium/src/components/statuses/status-reactions-bar.tsx +++ b/packages/nicolium/src/components/statuses/status-reactions-bar.tsx @@ -90,7 +90,6 @@ const StatusReaction: React.FC = ({ className={clsx('⁂-status-reactions-bar__button', { '⁂-status-reactions-bar__button--active': reaction.me, })} - key={reaction.name} onClick={handleClick} title={intl.formatMessage(messages.emojiCount, { emoji: `:${shortCode}:`, diff --git a/packages/nicolium/src/features/status/components/detailed-status.tsx b/packages/nicolium/src/features/status/components/detailed-status.tsx index 1fb5b7529..cc305d82c 100644 --- a/packages/nicolium/src/features/status/components/detailed-status.tsx +++ b/packages/nicolium/src/features/status/components/detailed-status.tsx @@ -98,112 +98,110 @@ const DetailedStatus: React.FC = ({ if (!account) return null; return ( -
-
- {renderStatusInfo()} +
+ {renderStatusInfo()} - {actualStatus.rss_feed ? ( - - ) : ( -
- -
- )} + {actualStatus.rss_feed ? ( + + ) : ( +
+ +
+ )} - + - - - - + + + + - {!status.rss_feed && ( - <> - + {!status.rss_feed && ( + <> + - - + + - - - - - - - + + + + + + + - {actualStatus.application && ( - <> - - - {actualStatus.application.name} - - - )} + {actualStatus.application && ( + <> + + + {actualStatus.application.name} + + + )} - {actualStatus.edited_at && ( - <> - - - - )} - - - + {actualStatus.edited_at && ( + <> + + + + )} + + + - + - - + - - )} -
+ + + )}
); }; diff --git a/packages/nicolium/src/features/status/components/thread.tsx b/packages/nicolium/src/features/status/components/thread.tsx index bb33a02a4..f54731846 100644 --- a/packages/nicolium/src/features/status/components/thread.tsx +++ b/packages/nicolium/src/features/status/components/thread.tsx @@ -303,7 +303,7 @@ const Thread = ({ }); setTimeout(() => { - (node.current?.querySelector('.detailed-actualStatus') as HTMLDivElement)?.focus(); + (node.current?.querySelector('.⁂-detailed-status') as HTMLDivElement)?.focus(); }, 100); }, 0); }, [status.id, statusIndex]); diff --git a/packages/nicolium/src/styles/new/statuses.scss b/packages/nicolium/src/styles/new/statuses.scss index 333b8abb5..d64c3294c 100644 --- a/packages/nicolium/src/styles/new/statuses.scss +++ b/packages/nicolium/src/styles/new/statuses.scss @@ -20,8 +20,15 @@ @apply mb-1 block text-sm text-gray-700 dark:text-gray-600; } +.⁂-status, +.⁂-detailed-status { + .⁂-status-reactions-bar { + padding-top: 0.5rem; + } +} + .⁂-status-reactions-bar { - @apply flex gap-2 flex-wrap pt-2; + @apply flex gap-2 flex-wrap; &__button { @apply flex cursor-pointer items-center gap-2 overflow-hidden rounded-md border border-gray-400 px-1.5 py-1 transition-all bg-transparent dark:border-primary-700 dark:bg-primary-700 black:border-primary-800 black:bg-primary-800; From 0e24dcdc41c61f3bf4c30cb2153991e60b89a9b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 6 Mar 2026 16:04:39 +0100 Subject: [PATCH 66/89] nicolium: fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/columns/timeline.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nicolium/src/columns/timeline.tsx b/packages/nicolium/src/columns/timeline.tsx index 9490f61cd..ecf93f12f 100644 --- a/packages/nicolium/src/columns/timeline.tsx +++ b/packages/nicolium/src/columns/timeline.tsx @@ -518,7 +518,7 @@ const Timeline: React.FC = ({ }; const savePosition = (me: string, entry: TimelineEntry, index: number) => { - if (entry.type !== 'status') return; + if (!entry || entry.type !== 'status') return; return localStorage.setItem( `nicolium:${me}:homeTimelinePosition`, `${entry.originalId}|${index}|${Date.now()}`, From 02ae4b7706f4530b0e14ba4f7f3ea504a63b2bbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 6 Mar 2026 16:07:18 +0100 Subject: [PATCH 67/89] nicolium: we can use dompurify directly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/package.json | 2 +- .../nicolium/src/components/preview-card.tsx | 4 +-- .../components/statuses/parsed-content.tsx | 4 +-- pnpm-lock.yaml | 26 +++++-------------- 4 files changed, 12 insertions(+), 24 deletions(-) diff --git a/packages/nicolium/package.json b/packages/nicolium/package.json index 34d2d1bc1..30b70c7ea 100644 --- a/packages/nicolium/package.json +++ b/packages/nicolium/package.json @@ -93,7 +93,7 @@ "intersection-observer": "^0.12.2", "intl-messageformat": "^11.1.2", "intl-pluralrules": "^2.0.1", - "isomorphic-dompurify": "^3.0.0", + "dompurify": "^3.3.2", "leaflet": "^1.9.4", "lexical": "^0.41.0", "line-awesome": "^1.3.0", diff --git a/packages/nicolium/src/components/preview-card.tsx b/packages/nicolium/src/components/preview-card.tsx index b27babad1..1067be89a 100644 --- a/packages/nicolium/src/components/preview-card.tsx +++ b/packages/nicolium/src/components/preview-card.tsx @@ -1,6 +1,6 @@ import { Link } from '@tanstack/react-router'; import clsx from 'clsx'; -import { sanitize } from 'isomorphic-dompurify'; +import DOMPurify from 'dompurify'; import { type MediaAttachment, type PreviewCard as CardEntity, @@ -71,7 +71,7 @@ interface IPreviewCardVideo { const PreviewCardVideo: React.FC = React.memo( React.forwardRef(({ card }, ref) => { - const html = sanitize(handleIframeUrl(card.html, card.url, card.provider_name), { + const html = DOMPurify.sanitize(handleIframeUrl(card.html, card.url, card.provider_name), { ADD_TAGS: ['iframe'], ADD_ATTR: ['allow', 'allowfullscreen', 'referrerpolicy'], }); diff --git a/packages/nicolium/src/components/statuses/parsed-content.tsx b/packages/nicolium/src/components/statuses/parsed-content.tsx index 438c8c5dc..d0a29eddc 100644 --- a/packages/nicolium/src/components/statuses/parsed-content.tsx +++ b/packages/nicolium/src/components/statuses/parsed-content.tsx @@ -1,10 +1,10 @@ +import DOMPurify from 'dompurify'; import parse, { Element, type HTMLReactParserOptions, domToReact, type DOMNode, } from 'html-react-parser'; -import { sanitize } from 'isomorphic-dompurify'; import groupBy from 'lodash/groupBy'; import minBy from 'lodash/minBy'; import React from 'react'; @@ -345,7 +345,7 @@ function parseContent( }; let content = parse( - sanitize(html, { ADD_ATTR: ['target'], USE_PROFILES: { html: true } }), + DOMPurify.sanitize(html, { ADD_ATTR: ['target'], USE_PROFILES: { html: true } }), options, ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d309428a2..b2031b502 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -163,6 +163,9 @@ importers: detect-passive-events: specifier: ^2.0.3 version: 2.0.3 + dompurify: + specifier: ^3.3.2 + version: 3.3.2 emoji-datasource: specifier: 15.0.1 version: 15.0.1 @@ -196,9 +199,6 @@ importers: intl-pluralrules: specifier: ^2.0.1 version: 2.0.1 - isomorphic-dompurify: - specifier: ^3.0.0 - version: 3.0.0 leaflet: specifier: ^1.9.4 version: 1.9.4 @@ -3585,8 +3585,9 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} - dompurify@3.3.1: - resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==} + dompurify@3.3.2: + resolution: {integrity: sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==} + engines: {node: '>=20'} domutils@2.8.0: resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} @@ -4310,10 +4311,6 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - isomorphic-dompurify@3.0.0: - resolution: {integrity: sha512-5K+MYP7Nrg74+Bi+QmQGzQ/FgEOyVHWsN8MuJy5wYQxxBRxPnWsD25Tjjt5FWYhan3OQ+vNLubyNJH9dfG03lQ==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - isomorphic.js@0.2.5: resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==} @@ -9707,7 +9704,7 @@ snapshots: dependencies: domelementtype: 2.3.0 - dompurify@3.3.1: + dompurify@3.3.2: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -10545,15 +10542,6 @@ snapshots: isexe@2.0.0: {} - isomorphic-dompurify@3.0.0: - dependencies: - dompurify: 3.3.1 - jsdom: 28.1.0 - transitivePeerDependencies: - - '@noble/hashes' - - canvas - - supports-color - isomorphic.js@0.2.5: {} isows@1.0.7(ws@8.19.0): From 19d4cfc0041ed5ee30db3152460438605e8364fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 6 Mar 2026 16:18:05 +0100 Subject: [PATCH 68/89] nicolium: more deps updates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/package.json | 4 +- pnpm-lock.yaml | 116 ++++++++++++++++----------------- 2 files changed, 60 insertions(+), 60 deletions(-) diff --git a/packages/nicolium/package.json b/packages/nicolium/package.json index 30b70c7ea..06dcfcf2e 100644 --- a/packages/nicolium/package.json +++ b/packages/nicolium/package.json @@ -36,7 +36,7 @@ }, "dependencies": { "@emoji-mart/data": "^1.2.1", - "@floating-ui/react": "^0.27.18", + "@floating-ui/react": "^0.27.19", "@fontsource/inter": "^5.2.8", "@fontsource/noto-sans-javanese": "^5.2.8", "@fontsource/roboto-mono": "^5.2.8", @@ -150,7 +150,7 @@ "@types/react-dom": "^19.2.3", "@types/react-sparklines": "^1.7.5", "@types/react-swipeable-views": "^0.13.6", - "@typescript/native-preview": "7.0.0-dev.20260301.1", + "@typescript/native-preview": "7.0.0-dev.20260306.1", "@vitejs/plugin-react": "^5.1.4", "@vitest/coverage-v8": "4.0.18", "eslint-plugin-formatjs": "^6.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b2031b502..76dd203fd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,8 +26,8 @@ importers: specifier: ^1.2.1 version: 1.2.1 '@floating-ui/react': - specifier: ^0.27.18 - version: 0.27.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: ^0.27.19 + version: 0.27.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@fontsource/inter': specifier: ^5.2.8 version: 5.2.8 @@ -363,8 +363,8 @@ importers: specifier: ^0.13.6 version: 0.13.6 '@typescript/native-preview': - specifier: 7.0.0-dev.20260301.1 - version: 7.0.0-dev.20260301.1 + specifier: 7.0.0-dev.20260306.1 + version: 7.0.0-dev.20260306.1 '@vitejs/plugin-react': specifier: ^5.1.4 version: 5.1.4(vite@7.3.1(@types/node@25.3.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)) @@ -1519,26 +1519,26 @@ packages: '@noble/hashes': optional: true - '@floating-ui/core@1.7.4': - resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==} + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} - '@floating-ui/dom@1.7.5': - resolution: {integrity: sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==} + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} - '@floating-ui/react-dom@2.1.7': - resolution: {integrity: sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==} + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' - '@floating-ui/react@0.27.18': - resolution: {integrity: sha512-xJWJxvmy3a05j643gQt+pRbht5XnTlGpsEsAPnMi5F5YTOEEJymA90uZKBD8OvIv5XvZ1qi4GcccSlqT3Bq44Q==} + '@floating-ui/react@0.27.19': + resolution: {integrity: sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog==} peerDependencies: react: '>=17.0.0' react-dom: '>=17.0.0' - '@floating-ui/utils@0.2.10': - resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} '@fontsource/inter@5.2.8': resolution: {integrity: sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg==} @@ -2805,43 +2805,43 @@ packages: '@types/use-sync-external-store@0.0.6': resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260301.1': - resolution: {integrity: sha512-z8Efrjf04XjwX3QsLJARUMNl0/Bhe2z3iBbLI1hPAvqvkRK9C6T0Fywup3rEqBpUXCWsVjOyCxJjmuDA/9vZ5g==} + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260306.1': + resolution: {integrity: sha512-4vuh4VlPydMS/nymDzjJIKDk3dntnEEB5UzyJV9mM4kxF5+geFgJih1DTtZS3qVafhHLB3e4l8omtvGftMnb8g==} cpu: [arm64] os: [darwin] - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260301.1': - resolution: {integrity: sha512-qKySo/Tsya2zO3kIecrvP3WfEzS2GYy0qJwPmQ+LTqgONnuQJDohjyC3461cTKYBYL/kvkqfBrUGmjrg9fMyEA==} + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260306.1': + resolution: {integrity: sha512-qxYfv0aM4KCZPEe584KIjT5sO4uR+xdyuQXX5tXbnH1UoksIz7bvJ9KUgRloS/q/ww0f8UjPS2+27LnRA4y7ig==} cpu: [x64] os: [darwin] - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260301.1': - resolution: {integrity: sha512-VNSRYpHbqnsJ18nO0buY85ZGloPoEi0W3rys93UzyZQGdxxqCKK5NxI+FV1siHNedFY2GRLr/7h1gZ8fcdeMvQ==} + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260306.1': + resolution: {integrity: sha512-8G0BKvTkE+eKX1tSnyKeDaf3bWPWY7OI77SMipagCAyYi06v4gxx+IVE3Px7W7kLX2Wqp1MjWDXu2N76wfJtXQ==} cpu: [arm64] os: [linux] - '@typescript/native-preview-linux-arm@7.0.0-dev.20260301.1': - resolution: {integrity: sha512-os9ohNd3XSO3+jKgMo3Ac1L6vzqg2GY9gcBsjp6Z5NrnZtnbq6e+uHkqavsE73NP1VIAsjIwZThjw4zY9GY7bg==} + '@typescript/native-preview-linux-arm@7.0.0-dev.20260306.1': + resolution: {integrity: sha512-8gRAFx0ExDWHOmphl8mzBrSoGWnLWDU4VpxkPRsWqaJpHVbjr9Yk2QkuJNIaDmF6q44eJmW/huSiObmHTbZ1UQ==} cpu: [arm] os: [linux] - '@typescript/native-preview-linux-x64@7.0.0-dev.20260301.1': - resolution: {integrity: sha512-w2iRqNEjvJbzqOYuRckpRBOJpJio2lOFTei7INQ0QED/TOO3XqJvAkyOzDrIgCO9YGWjDUIbuXZ/+4fldGIs3Q==} + '@typescript/native-preview-linux-x64@7.0.0-dev.20260306.1': + resolution: {integrity: sha512-rsJV3Z9J/zYCEtcqvm+WfLAml3i1OAyMEUn0hja7i8C0kzE+tXKXzsJ0+I1TrSU5O7hHvqlLTvueBoCoM4aL4g==} cpu: [x64] os: [linux] - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260301.1': - resolution: {integrity: sha512-w6uu75HQek25Agu5+CcpzPS9PN3NTEyHSNMp9oypR8dj7zPRsudM8M4vhFTMDVCZ/lX/mWXkgG8dHmI+myWWvw==} + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260306.1': + resolution: {integrity: sha512-US1WsIu9IukaFzM+w8wt0fIAkmk2WtxeVuk8nkbrnH9S3ax39r0J4ikMNZSXEJE0VMxhXJoymzfWxhj3s9yW/Q==} cpu: [arm64] os: [win32] - '@typescript/native-preview-win32-x64@7.0.0-dev.20260301.1': - resolution: {integrity: sha512-r2T4W5oYhOHAOVE0U/L1aFCsNDhv0BIRtyk9pL3eqGPLoYH4vtR96/CIpsVt04JDuh0fxOBHcbVjWaZdeZaTCQ==} + '@typescript/native-preview-win32-x64@7.0.0-dev.20260306.1': + resolution: {integrity: sha512-MlneT0RWS9Zdb8XoWvHsUgmnMJu6K3S0BXRu5ZgUYjcbQKlkz+Z87aUB8eX8qnDFd9csJcMp3+ZrgQ/LKVGP1g==} cpu: [x64] os: [win32] - '@typescript/native-preview@7.0.0-dev.20260301.1': - resolution: {integrity: sha512-hmQSkgiIDAzdjyk4P8/dU8lLch1sR8spamGZ/ypPkz3rmraiLaeDj6rqlrgyZNOcSpk0R3kXw3y5qJ9121gjNQ==} + '@typescript/native-preview@7.0.0-dev.20260306.1': + resolution: {integrity: sha512-4m7cOjtKu+iLazWW5MuJuI2ZZMkQkS42+GxN6FVdja1nL0t47l1wpaTnzUa1Ny9Xa0opIJ7psPAMBKYAPKbCKA==} hasBin: true '@uidotdev/usehooks@2.4.1': @@ -7573,30 +7573,30 @@ snapshots: '@exodus/bytes@1.14.1': {} - '@floating-ui/core@1.7.4': + '@floating-ui/core@1.7.5': dependencies: - '@floating-ui/utils': 0.2.10 + '@floating-ui/utils': 0.2.11 - '@floating-ui/dom@1.7.5': + '@floating-ui/dom@1.7.6': dependencies: - '@floating-ui/core': 1.7.4 - '@floating-ui/utils': 0.2.10 + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 - '@floating-ui/react-dom@2.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@floating-ui/react-dom@2.1.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@floating-ui/dom': 1.7.5 + '@floating-ui/dom': 1.7.6 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@floating-ui/react@0.27.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@floating-ui/react@0.27.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@floating-ui/react-dom': 2.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@floating-ui/utils': 0.2.10 + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@floating-ui/utils': 0.2.11 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) tabbable: 6.4.0 - '@floating-ui/utils@0.2.10': {} + '@floating-ui/utils@0.2.11': {} '@fontsource/inter@5.2.8': {} @@ -7817,7 +7817,7 @@ snapshots: '@lexical/react@0.41.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(yjs@13.6.27)': dependencies: - '@floating-ui/react': 0.27.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@floating-ui/react': 0.27.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@lexical/devtools-core': 0.41.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@lexical/dragon': 0.41.0 '@lexical/extension': 0.41.0 @@ -8829,36 +8829,36 @@ snapshots: '@types/use-sync-external-store@0.0.6': {} - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260301.1': + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260306.1': optional: true - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260301.1': + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260306.1': optional: true - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260301.1': + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260306.1': optional: true - '@typescript/native-preview-linux-arm@7.0.0-dev.20260301.1': + '@typescript/native-preview-linux-arm@7.0.0-dev.20260306.1': optional: true - '@typescript/native-preview-linux-x64@7.0.0-dev.20260301.1': + '@typescript/native-preview-linux-x64@7.0.0-dev.20260306.1': optional: true - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260301.1': + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260306.1': optional: true - '@typescript/native-preview-win32-x64@7.0.0-dev.20260301.1': + '@typescript/native-preview-win32-x64@7.0.0-dev.20260306.1': optional: true - '@typescript/native-preview@7.0.0-dev.20260301.1': + '@typescript/native-preview@7.0.0-dev.20260306.1': optionalDependencies: - '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260301.1 - '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260301.1 - '@typescript/native-preview-linux-arm': 7.0.0-dev.20260301.1 - '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260301.1 - '@typescript/native-preview-linux-x64': 7.0.0-dev.20260301.1 - '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260301.1 - '@typescript/native-preview-win32-x64': 7.0.0-dev.20260301.1 + '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260306.1 + '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260306.1 + '@typescript/native-preview-linux-arm': 7.0.0-dev.20260306.1 + '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260306.1 + '@typescript/native-preview-linux-x64': 7.0.0-dev.20260306.1 + '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260306.1 + '@typescript/native-preview-win32-x64': 7.0.0-dev.20260306.1 '@uidotdev/usehooks@2.4.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: @@ -11435,7 +11435,7 @@ snapshots: react-datepicker@9.1.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - '@floating-ui/react': 0.27.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@floating-ui/react': 0.27.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4) clsx: 2.1.1 date-fns: 4.1.0 react: 19.2.4 From 85a21f2bd3f36e611dc4b3b80b18978b77ac576c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 6 Mar 2026 16:19:45 +0100 Subject: [PATCH 69/89] nicolium: use react-helmet in less places MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../src/features/status/components/thread.tsx | 5 +- packages/nicolium/src/init/nicolium-head.tsx | 2 +- .../nicolium/src/layouts/event-layout.tsx | 83 +++++++++---------- .../nicolium/src/layouts/profile-layout.tsx | 7 +- 4 files changed, 45 insertions(+), 52 deletions(-) diff --git a/packages/nicolium/src/features/status/components/thread.tsx b/packages/nicolium/src/features/status/components/thread.tsx index f54731846..defedd0fe 100644 --- a/packages/nicolium/src/features/status/components/thread.tsx +++ b/packages/nicolium/src/features/status/components/thread.tsx @@ -1,7 +1,6 @@ import { useNavigate } from '@tanstack/react-router'; import clsx from 'clsx'; import React, { useCallback, useEffect, useMemo, useRef } from 'react'; -import { Helmet } from 'react-helmet-async'; import { useIntl } from 'react-intl'; import ScrollableList from '@/components/scrollable-list'; @@ -346,7 +345,7 @@ const Thread = ({ const firstAttachment = status.media_attachments && status.media_attachments[0]; return ( - + <> {status.spoiler_text && } {(firstAttachment?.type === 'image' || firstAttachment?.type === 'gifv') && ( <> @@ -377,7 +376,7 @@ const Thread = ({ {status.edited_at && } {status.account.local === false && } - + ); }, [status]); diff --git a/packages/nicolium/src/init/nicolium-head.tsx b/packages/nicolium/src/init/nicolium-head.tsx index ee91afb23..3121f6774 100644 --- a/packages/nicolium/src/init/nicolium-head.tsx +++ b/packages/nicolium/src/init/nicolium-head.tsx @@ -90,8 +90,8 @@ const NicoliumHead = () => { })} /> - + {`:root { ${themeCss} }`} {['dark', 'black'].includes(theme) && ( {':root { color-scheme: dark; }'} diff --git a/packages/nicolium/src/layouts/event-layout.tsx b/packages/nicolium/src/layouts/event-layout.tsx index e0c7ad742..967804233 100644 --- a/packages/nicolium/src/layouts/event-layout.tsx +++ b/packages/nicolium/src/layouts/event-layout.tsx @@ -1,6 +1,5 @@ import { Outlet, useLocation, useNavigate } from '@tanstack/react-router'; import React, { useMemo } from 'react'; -import { Helmet } from 'react-helmet-async'; import { FormattedMessage } from 'react-intl'; import Column from '@/components/ui/column'; @@ -32,6 +31,47 @@ const EventLayout = () => { const event = status?.event; + const meta = useMemo(() => { + if (!status) return null; + + const firstAttachment = status.media_attachments && status.media_attachments[0]; + + return ( + <> + {status.spoiler_text && } + {(firstAttachment?.type === 'image' || firstAttachment?.type === 'gifv') && ( + <> + + + {firstAttachment.mime_type && ( + + )} + {firstAttachment.meta.original && ( + + )} + {firstAttachment.meta.original && ( + + )} + + )} + + + + + + {status.edited_at && } + + {status.account.local === false && } + + ); + }, [status]); + if (status && !event) { navigate({ to: '/@{$username}/posts/$statusId', @@ -64,47 +104,6 @@ const EventLayout = () => { pathname.endsWith(path), ); - const meta = useMemo(() => { - if (!status) return null; - - const firstAttachment = status.media_attachments && status.media_attachments[0]; - - return ( - - {status.spoiler_text && } - {(firstAttachment?.type === 'image' || firstAttachment?.type === 'gifv') && ( - <> - - - {firstAttachment.mime_type && ( - - )} - {firstAttachment.meta.original && ( - - )} - {firstAttachment.meta.original && ( - - )} - - )} - - - - - - {status.edited_at && } - - {status.account.local === false && } - - ); - }, [status]); - return ( <> {meta} diff --git a/packages/nicolium/src/layouts/profile-layout.tsx b/packages/nicolium/src/layouts/profile-layout.tsx index d4ec6970d..394bcb5fd 100644 --- a/packages/nicolium/src/layouts/profile-layout.tsx +++ b/packages/nicolium/src/layouts/profile-layout.tsx @@ -1,6 +1,5 @@ import { Navigate, Outlet, useLocation } from '@tanstack/react-router'; import React from 'react'; -import { Helmet } from 'react-helmet-async'; import { FormattedMessage } from 'react-intl'; import Column from '@/components/ui/column'; @@ -95,11 +94,7 @@ const ProfileLayout: React.FC = () => { return ( <> - {account?.local === false && ( - - - - )} + {account?.local === false && }
From 6e735461d54de45091ef12d4b96cfbb45206afd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 6 Mar 2026 16:26:56 +0100 Subject: [PATCH 70/89] nicolium: deps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/package.json | 12 +- pnpm-lock.yaml | 384 ++++++++++++++++----------------- 2 files changed, 198 insertions(+), 198 deletions(-) diff --git a/packages/nicolium/package.json b/packages/nicolium/package.json index 06dcfcf2e..1c583a058 100644 --- a/packages/nicolium/package.json +++ b/packages/nicolium/package.json @@ -57,15 +57,15 @@ "@reach/tabs": "^0.18.0", "@react-spring/web": "^10.0.3", "@reduxjs/toolkit": "^2.11.2", - "@sentry/browser": "^10.40.0", - "@sentry/core": "^10.40.0", - "@sentry/react": "^10.40.0", + "@sentry/browser": "^10.42.0", + "@sentry/core": "^10.42.0", + "@sentry/react": "^10.42.0", "@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/forms": "^0.5.11", "@tailwindcss/typography": "^0.5.19", "@tanstack/react-pacer": "^0.20.0", "@tanstack/react-query": "^5.90.21", - "@tanstack/react-router": "^1.163.3", + "@tanstack/react-router": "^1.166.2", "@transfem-org/sfm-js": "^0.26.1", "@twemoji/svg": "^15.0.0", "@uidotdev/usehooks": "^2.4.1", @@ -104,7 +104,7 @@ "object-to-formdata": "^4.5.1", "path-browserify": "^1.0.1", "pl-api": "workspace:*", - "postcss": "^8.5.6", + "postcss": "^8.5.8", "process": "^0.11.10", "punycode": "^2.3.1", "qrcode.react": "^4.2.0", @@ -113,7 +113,7 @@ "react-color": "^2.19.3", "react-datepicker": "^9.1.0", "react-dom": "^19.2.4", - "react-helmet-async": "^2.0.5", + "react-helmet-async": "^3.0.0", "react-hot-toast": "^2.6.0", "react-inlinesvg": "^4.2.0", "react-intl": "^8.1.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 76dd203fd..5ce275482 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -89,14 +89,14 @@ importers: specifier: ^2.11.2 version: 2.11.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1))(react@19.2.4) '@sentry/browser': - specifier: ^10.40.0 - version: 10.40.0 + specifier: ^10.42.0 + version: 10.42.0 '@sentry/core': - specifier: ^10.40.0 - version: 10.40.0 + specifier: ^10.42.0 + version: 10.42.0 '@sentry/react': - specifier: ^10.40.0 - version: 10.40.0(react@19.2.4) + specifier: ^10.42.0 + version: 10.42.0(react@19.2.4) '@tailwindcss/aspect-ratio': specifier: ^0.4.2 version: 0.4.2(tailwindcss@3.4.19(yaml@2.8.2)) @@ -113,8 +113,8 @@ importers: specifier: ^5.90.21 version: 5.90.21(react@19.2.4) '@tanstack/react-router': - specifier: ^1.163.3 - version: 1.163.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: ^1.166.2 + version: 1.166.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@transfem-org/sfm-js': specifier: ^0.26.1 version: 0.26.1 @@ -135,7 +135,7 @@ importers: version: 1.7.8 autoprefixer: specifier: ^10.4.27 - version: 10.4.27(postcss@8.5.6) + version: 10.4.27(postcss@8.5.8) blurhash: specifier: ^2.0.5 version: 2.0.5 @@ -159,7 +159,7 @@ importers: version: 0.18.1 cssnano: specifier: ^7.1.2 - version: 7.1.2(postcss@8.5.6) + version: 7.1.2(postcss@8.5.8) detect-passive-events: specifier: ^2.0.3 version: 2.0.3 @@ -230,8 +230,8 @@ importers: specifier: workspace:* version: link:../pl-api postcss: - specifier: ^8.5.6 - version: 8.5.6 + specifier: ^8.5.8 + version: 8.5.8 process: specifier: ^0.11.10 version: 0.11.10 @@ -257,8 +257,8 @@ importers: specifier: ^19.2.4 version: 19.2.4(react@19.2.4) react-helmet-async: - specifier: ^2.0.5 - version: 2.0.5(react@19.2.4) + specifier: ^3.0.0 + version: 3.0.0(react@19.2.4) react-hot-toast: specifier: ^2.6.0 version: 2.6.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -397,7 +397,7 @@ importers: version: 17.4.0(typescript@5.9.3) stylelint-config-standard-scss: specifier: ^17.0.0 - version: 17.0.0(postcss@8.5.6)(stylelint@17.4.0(typescript@5.9.3)) + version: 17.0.0(postcss@8.5.8)(stylelint@17.4.0(typescript@5.9.3)) tailwindcss: specifier: ^3.4.19 version: 3.4.19(yaml@2.8.2) @@ -2518,32 +2518,32 @@ packages: '@rushstack/ts-command-line@5.0.2': resolution: {integrity: sha512-+AkJDbu1GFMPIU8Sb7TLVXDv/Q7Mkvx+wAjEl8XiXVVq+p1FmWW6M3LYpJMmoHNckSofeMecgWg5lfMwNAAsEQ==} - '@sentry-internal/browser-utils@10.40.0': - resolution: {integrity: sha512-3CDeVNBXYOIvBVdT0SOdMZx5LzYDLuhGK/z7A14sYZz4Cd2+f4mSeFDaEOoH/g2SaY2CKR5KGkAADy8IyjZ21w==} + '@sentry-internal/browser-utils@10.42.0': + resolution: {integrity: sha512-HCEICKvepxN4/6NYfnMMMlppcSwIEwtS66X6d1/mwaHdi2ivw0uGl52p7Nfhda/lIJArbrkWprxl0WcjZajhQA==} engines: {node: '>=18'} - '@sentry-internal/feedback@10.40.0': - resolution: {integrity: sha512-V/ixkcdCNMo04KgsCEeNEu966xUUTD6czKT2LOAO5siZACqFjT/Rp9VR1n7QQrVo3sL7P3QNiTHtX0jaeWbwzg==} + '@sentry-internal/feedback@10.42.0': + resolution: {integrity: sha512-lpPcHsog10MVYFTWE0Pf8vQRqQWwZHJpkVl2FEb9/HDdHFyTBUhCVoWo1KyKaG7GJl9AVKMAg7bp9SSNArhFNQ==} engines: {node: '>=18'} - '@sentry-internal/replay-canvas@10.40.0': - resolution: {integrity: sha512-wzQwilFHO2baeCt0dTMf0eW+rgK8O+mkisf9sQzPXzG3Krr/iVtFg1T5T1Th3YsCsEdn6yQ3hcBPLEXjMSvccg==} + '@sentry-internal/replay-canvas@10.42.0': + resolution: {integrity: sha512-am3m1Fj8ihoPfoYo41Qq4KeCAAICn4bySso8Oepu9dMNe9Lcnsf+reMRS2qxTPg3pZDc4JEMOcLyNCcgnAfrHw==} engines: {node: '>=18'} - '@sentry-internal/replay@10.40.0': - resolution: {integrity: sha512-vsH2Ut0KIIQIHNdS3zzEGLJ2C9btbpvJIWAVk7l7oft66JzlUNC89qNaQ5SAypjLQx4Ln2V/ZTqfEoNzXOAsoQ==} + '@sentry-internal/replay@10.42.0': + resolution: {integrity: sha512-Zh3EoaH39x2lqVY1YyVB2vJEyCIrT+YLUQxYl1yvP0MJgLxaR6akVjkgxbSUJahan4cX5DxpZiEHfzdlWnYPyQ==} engines: {node: '>=18'} - '@sentry/browser@10.40.0': - resolution: {integrity: sha512-nCt3FKUMFad0C6xl5wCK0Jz+qT4Vev4fv6HJRn0YoNRRDQCfsUVxAz7pNyyiPNGM/WCDp9wJpGJsRvbBRd2anw==} + '@sentry/browser@10.42.0': + resolution: {integrity: sha512-iXxYjXNEBwY1MH4lDSDZZUNjzPJDK7/YLwVIJq/3iBYpIQVIhaJsoJnf3clx9+NfJ8QFKyKfcvgae61zm+hgTA==} engines: {node: '>=18'} - '@sentry/core@10.40.0': - resolution: {integrity: sha512-/wrcHPp9Avmgl6WBimPjS4gj810a1wU5oX9fF1bzJfeIIbF3jTsAbv0oMbgDp0cSDnkwv2+NvcPnn3+c5J6pBA==} + '@sentry/core@10.42.0': + resolution: {integrity: sha512-L4rMrXMqUKBanpjpMT+TuAVk6xAijz6AWM6RiEYpohAr7SGcCEc1/T0+Ep1eLV8+pwWacfU27OvELIyNeOnGzA==} engines: {node: '>=18'} - '@sentry/react@10.40.0': - resolution: {integrity: sha512-3T5W/e3QJMimXRIOx8xMEZbxeIuFiKlXvHLcMTLGygGBYnxQGeb8Oz/8heov+3zF1JoCIxeVQNFW0woySApfyA==} + '@sentry/react@10.42.0': + resolution: {integrity: sha512-uigyz6E3yPjjqIZpkGzRChww6gzMmqdCpK30M5aBYoaen29DDmSECHYA16sfgXeSwzQhnXyX7GxgOB+eKIr9dw==} engines: {node: '>=18'} peerDependencies: react: ^16.14.0 || 17.x || 18.x || 19.x @@ -2618,8 +2618,8 @@ packages: peerDependencies: react: ^18 || ^19 - '@tanstack/react-router@1.163.3': - resolution: {integrity: sha512-hheBbFVb+PbxtrWp8iy6+TTRTbhx3Pn6hKo8Tv/sWlG89ZMcD1xpQWzx8ukHN9K8YWbh5rdzt4kv6u8X4kB28Q==} + '@tanstack/react-router@1.166.2': + resolution: {integrity: sha512-pKhUtrvVLlhjWhsHkJSuIzh1J4LcP+8ErbIqRLORX9Js8dUFMKoT0+8oFpi+P8QRpuhm/7rzjYiWfcyTsqQZtA==} engines: {node: '>=20.19'} peerDependencies: react: '>=18.0.0 || >=19.0.0' @@ -2637,8 +2637,8 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/router-core@1.163.3': - resolution: {integrity: sha512-jPptiGq/w3nuPzcMC7RNa79aU+b6OjaDzWJnBcV2UAwL4ThJamRS4h42TdhJE+oF5yH9IEnCOGQdfnbw45LbfA==} + '@tanstack/router-core@1.166.2': + resolution: {integrity: sha512-zn3NhENOAX9ToQiX077UV2OH3aJKOvV2ZMNZZxZ3gDG3i3WqL8NfWfEgetEAfMN37/Mnt90PpotYgf7IyuoKqQ==} engines: {node: '>=20.19'} '@tanstack/store@0.8.1': @@ -5173,8 +5173,8 @@ packages: postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.5.6: - resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: @@ -5272,10 +5272,10 @@ packages: peerDependencies: react: 16.8 - 19 - react-helmet-async@2.0.5: - resolution: {integrity: sha512-rYUYHeus+i27MvFE+Jaa4WsyBKGkL6qVgbJvSBoX8mbsWoABJXdEO0bZyi0F6i+4f0NuIb8AvqPMj3iXFHkMwg==} + react-helmet-async@3.0.0: + resolution: {integrity: sha512-nA3IEZfXiclgrz4KLxAhqJqIfFDuvzQwlKwpdmzZIuC1KNSghDEIXmyU0TKtbM+NafnkICcwx8CECFrZ/sL/1w==} peerDependencies: - react: ^16.6.0 || ^17.0.0 || ^18.0.0 + react: ^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-hot-toast@2.6.0: resolution: {integrity: sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==} @@ -8519,38 +8519,38 @@ snapshots: transitivePeerDependencies: - '@types/node' - '@sentry-internal/browser-utils@10.40.0': + '@sentry-internal/browser-utils@10.42.0': dependencies: - '@sentry/core': 10.40.0 + '@sentry/core': 10.42.0 - '@sentry-internal/feedback@10.40.0': + '@sentry-internal/feedback@10.42.0': dependencies: - '@sentry/core': 10.40.0 + '@sentry/core': 10.42.0 - '@sentry-internal/replay-canvas@10.40.0': + '@sentry-internal/replay-canvas@10.42.0': dependencies: - '@sentry-internal/replay': 10.40.0 - '@sentry/core': 10.40.0 + '@sentry-internal/replay': 10.42.0 + '@sentry/core': 10.42.0 - '@sentry-internal/replay@10.40.0': + '@sentry-internal/replay@10.42.0': dependencies: - '@sentry-internal/browser-utils': 10.40.0 - '@sentry/core': 10.40.0 + '@sentry-internal/browser-utils': 10.42.0 + '@sentry/core': 10.42.0 - '@sentry/browser@10.40.0': + '@sentry/browser@10.42.0': dependencies: - '@sentry-internal/browser-utils': 10.40.0 - '@sentry-internal/feedback': 10.40.0 - '@sentry-internal/replay': 10.40.0 - '@sentry-internal/replay-canvas': 10.40.0 - '@sentry/core': 10.40.0 + '@sentry-internal/browser-utils': 10.42.0 + '@sentry-internal/feedback': 10.42.0 + '@sentry-internal/replay': 10.42.0 + '@sentry-internal/replay-canvas': 10.42.0 + '@sentry/core': 10.42.0 - '@sentry/core@10.40.0': {} + '@sentry/core@10.42.0': {} - '@sentry/react@10.40.0(react@19.2.4)': + '@sentry/react@10.42.0(react@19.2.4)': dependencies: - '@sentry/browser': 10.40.0 - '@sentry/core': 10.40.0 + '@sentry/browser': 10.42.0 + '@sentry/core': 10.42.0 react: 19.2.4 '@shikijs/engine-oniguruma@3.22.0': @@ -8623,11 +8623,11 @@ snapshots: '@tanstack/query-core': 5.90.20 react: 19.2.4 - '@tanstack/react-router@1.163.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@tanstack/react-router@1.166.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@tanstack/history': 1.161.4 '@tanstack/react-store': 0.9.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@tanstack/router-core': 1.163.3 + '@tanstack/router-core': 1.166.2 isbot: 5.1.35 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) @@ -8648,7 +8648,7 @@ snapshots: react-dom: 19.2.4(react@19.2.4) use-sync-external-store: 1.6.0(react@19.2.4) - '@tanstack/router-core@1.163.3': + '@tanstack/router-core@1.166.2': dependencies: '@tanstack/history': 1.161.4 '@tanstack/store': 0.9.1 @@ -9010,7 +9010,7 @@ snapshots: '@vue/shared': 3.5.29 estree-walker: 2.0.2 magic-string: 0.30.21 - postcss: 8.5.6 + postcss: 8.5.8 source-map-js: 1.2.1 '@vue/compiler-ssr@3.5.29': @@ -9257,13 +9257,13 @@ snapshots: at-least-node@1.0.0: {} - autoprefixer@10.4.27(postcss@8.5.6): + autoprefixer@10.4.27(postcss@8.5.8): dependencies: browserslist: 4.28.1 caniuse-lite: 1.0.30001775 fraction.js: 5.3.4 picocolors: 1.1.1 - postcss: 8.5.6 + postcss: 8.5.8 postcss-value-parser: 4.2.0 available-typed-arrays@1.0.7: @@ -9505,9 +9505,9 @@ snapshots: cryptocurrency-icons@0.18.1: {} - css-declaration-sorter@7.3.1(postcss@8.5.6): + css-declaration-sorter@7.3.1(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 css-functions-list@3.3.3: {} @@ -9543,49 +9543,49 @@ snapshots: cssesc@3.0.0: {} - cssnano-preset-default@7.0.10(postcss@8.5.6): + cssnano-preset-default@7.0.10(postcss@8.5.8): dependencies: browserslist: 4.28.1 - css-declaration-sorter: 7.3.1(postcss@8.5.6) - cssnano-utils: 5.0.1(postcss@8.5.6) - postcss: 8.5.6 - postcss-calc: 10.1.1(postcss@8.5.6) - postcss-colormin: 7.0.5(postcss@8.5.6) - postcss-convert-values: 7.0.8(postcss@8.5.6) - postcss-discard-comments: 7.0.5(postcss@8.5.6) - postcss-discard-duplicates: 7.0.2(postcss@8.5.6) - postcss-discard-empty: 7.0.1(postcss@8.5.6) - postcss-discard-overridden: 7.0.1(postcss@8.5.6) - postcss-merge-longhand: 7.0.5(postcss@8.5.6) - postcss-merge-rules: 7.0.7(postcss@8.5.6) - postcss-minify-font-values: 7.0.1(postcss@8.5.6) - postcss-minify-gradients: 7.0.1(postcss@8.5.6) - postcss-minify-params: 7.0.5(postcss@8.5.6) - postcss-minify-selectors: 7.0.5(postcss@8.5.6) - postcss-normalize-charset: 7.0.1(postcss@8.5.6) - postcss-normalize-display-values: 7.0.1(postcss@8.5.6) - postcss-normalize-positions: 7.0.1(postcss@8.5.6) - postcss-normalize-repeat-style: 7.0.1(postcss@8.5.6) - postcss-normalize-string: 7.0.1(postcss@8.5.6) - postcss-normalize-timing-functions: 7.0.1(postcss@8.5.6) - postcss-normalize-unicode: 7.0.5(postcss@8.5.6) - postcss-normalize-url: 7.0.1(postcss@8.5.6) - postcss-normalize-whitespace: 7.0.1(postcss@8.5.6) - postcss-ordered-values: 7.0.2(postcss@8.5.6) - postcss-reduce-initial: 7.0.5(postcss@8.5.6) - postcss-reduce-transforms: 7.0.1(postcss@8.5.6) - postcss-svgo: 7.1.0(postcss@8.5.6) - postcss-unique-selectors: 7.0.4(postcss@8.5.6) + css-declaration-sorter: 7.3.1(postcss@8.5.8) + cssnano-utils: 5.0.1(postcss@8.5.8) + postcss: 8.5.8 + postcss-calc: 10.1.1(postcss@8.5.8) + postcss-colormin: 7.0.5(postcss@8.5.8) + postcss-convert-values: 7.0.8(postcss@8.5.8) + postcss-discard-comments: 7.0.5(postcss@8.5.8) + postcss-discard-duplicates: 7.0.2(postcss@8.5.8) + postcss-discard-empty: 7.0.1(postcss@8.5.8) + postcss-discard-overridden: 7.0.1(postcss@8.5.8) + postcss-merge-longhand: 7.0.5(postcss@8.5.8) + postcss-merge-rules: 7.0.7(postcss@8.5.8) + postcss-minify-font-values: 7.0.1(postcss@8.5.8) + postcss-minify-gradients: 7.0.1(postcss@8.5.8) + postcss-minify-params: 7.0.5(postcss@8.5.8) + postcss-minify-selectors: 7.0.5(postcss@8.5.8) + postcss-normalize-charset: 7.0.1(postcss@8.5.8) + postcss-normalize-display-values: 7.0.1(postcss@8.5.8) + postcss-normalize-positions: 7.0.1(postcss@8.5.8) + postcss-normalize-repeat-style: 7.0.1(postcss@8.5.8) + postcss-normalize-string: 7.0.1(postcss@8.5.8) + postcss-normalize-timing-functions: 7.0.1(postcss@8.5.8) + postcss-normalize-unicode: 7.0.5(postcss@8.5.8) + postcss-normalize-url: 7.0.1(postcss@8.5.8) + postcss-normalize-whitespace: 7.0.1(postcss@8.5.8) + postcss-ordered-values: 7.0.2(postcss@8.5.8) + postcss-reduce-initial: 7.0.5(postcss@8.5.8) + postcss-reduce-transforms: 7.0.1(postcss@8.5.8) + postcss-svgo: 7.1.0(postcss@8.5.8) + postcss-unique-selectors: 7.0.4(postcss@8.5.8) - cssnano-utils@5.0.1(postcss@8.5.6): + cssnano-utils@5.0.1(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 - cssnano@7.1.2(postcss@8.5.6): + cssnano@7.1.2(postcss@8.5.8): dependencies: - cssnano-preset-default: 7.0.10(postcss@8.5.6) + cssnano-preset-default: 7.0.10(postcss@8.5.8) lilconfig: 3.1.3 - postcss: 8.5.6 + postcss: 8.5.8 csso@5.0.5: dependencies: @@ -11165,180 +11165,180 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-calc@10.1.1(postcss@8.5.6): + postcss-calc@10.1.1(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 postcss-selector-parser: 7.1.1 postcss-value-parser: 4.2.0 - postcss-colormin@7.0.5(postcss@8.5.6): + postcss-colormin@7.0.5(postcss@8.5.8): dependencies: browserslist: 4.28.1 caniuse-api: 3.0.0 colord: 2.9.3 - postcss: 8.5.6 + postcss: 8.5.8 postcss-value-parser: 4.2.0 - postcss-convert-values@7.0.8(postcss@8.5.6): + postcss-convert-values@7.0.8(postcss@8.5.8): dependencies: browserslist: 4.28.1 - postcss: 8.5.6 + postcss: 8.5.8 postcss-value-parser: 4.2.0 - postcss-discard-comments@7.0.5(postcss@8.5.6): + postcss-discard-comments@7.0.5(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 postcss-selector-parser: 7.1.1 - postcss-discard-duplicates@7.0.2(postcss@8.5.6): + postcss-discard-duplicates@7.0.2(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 - postcss-discard-empty@7.0.1(postcss@8.5.6): + postcss-discard-empty@7.0.1(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 - postcss-discard-overridden@7.0.1(postcss@8.5.6): + postcss-discard-overridden@7.0.1(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 - postcss-import@15.1.0(postcss@8.5.6): + postcss-import@15.1.0(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.11 - postcss-js@4.1.0(postcss@8.5.6): + postcss-js@4.1.0(postcss@8.5.8): dependencies: camelcase-css: 2.0.1 - postcss: 8.5.6 + postcss: 8.5.8 - postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6)(yaml@2.8.2): + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.8)(yaml@2.8.2): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 1.21.7 - postcss: 8.5.6 + postcss: 8.5.8 yaml: 2.8.2 postcss-media-query-parser@0.2.3: {} - postcss-merge-longhand@7.0.5(postcss@8.5.6): + postcss-merge-longhand@7.0.5(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 postcss-value-parser: 4.2.0 - stylehacks: 7.0.7(postcss@8.5.6) + stylehacks: 7.0.7(postcss@8.5.8) - postcss-merge-rules@7.0.7(postcss@8.5.6): + postcss-merge-rules@7.0.7(postcss@8.5.8): dependencies: browserslist: 4.28.1 caniuse-api: 3.0.0 - cssnano-utils: 5.0.1(postcss@8.5.6) - postcss: 8.5.6 + cssnano-utils: 5.0.1(postcss@8.5.8) + postcss: 8.5.8 postcss-selector-parser: 7.1.1 - postcss-minify-font-values@7.0.1(postcss@8.5.6): + postcss-minify-font-values@7.0.1(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 postcss-value-parser: 4.2.0 - postcss-minify-gradients@7.0.1(postcss@8.5.6): + postcss-minify-gradients@7.0.1(postcss@8.5.8): dependencies: colord: 2.9.3 - cssnano-utils: 5.0.1(postcss@8.5.6) - postcss: 8.5.6 + cssnano-utils: 5.0.1(postcss@8.5.8) + postcss: 8.5.8 postcss-value-parser: 4.2.0 - postcss-minify-params@7.0.5(postcss@8.5.6): + postcss-minify-params@7.0.5(postcss@8.5.8): dependencies: browserslist: 4.28.1 - cssnano-utils: 5.0.1(postcss@8.5.6) - postcss: 8.5.6 + cssnano-utils: 5.0.1(postcss@8.5.8) + postcss: 8.5.8 postcss-value-parser: 4.2.0 - postcss-minify-selectors@7.0.5(postcss@8.5.6): + postcss-minify-selectors@7.0.5(postcss@8.5.8): dependencies: cssesc: 3.0.0 - postcss: 8.5.6 + postcss: 8.5.8 postcss-selector-parser: 7.1.1 - postcss-nested@6.2.0(postcss@8.5.6): + postcss-nested@6.2.0(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 postcss-selector-parser: 6.1.2 - postcss-normalize-charset@7.0.1(postcss@8.5.6): + postcss-normalize-charset@7.0.1(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 - postcss-normalize-display-values@7.0.1(postcss@8.5.6): + postcss-normalize-display-values@7.0.1(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 postcss-value-parser: 4.2.0 - postcss-normalize-positions@7.0.1(postcss@8.5.6): + postcss-normalize-positions@7.0.1(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 postcss-value-parser: 4.2.0 - postcss-normalize-repeat-style@7.0.1(postcss@8.5.6): + postcss-normalize-repeat-style@7.0.1(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 postcss-value-parser: 4.2.0 - postcss-normalize-string@7.0.1(postcss@8.5.6): + postcss-normalize-string@7.0.1(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 postcss-value-parser: 4.2.0 - postcss-normalize-timing-functions@7.0.1(postcss@8.5.6): + postcss-normalize-timing-functions@7.0.1(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 postcss-value-parser: 4.2.0 - postcss-normalize-unicode@7.0.5(postcss@8.5.6): + postcss-normalize-unicode@7.0.5(postcss@8.5.8): dependencies: browserslist: 4.28.1 - postcss: 8.5.6 + postcss: 8.5.8 postcss-value-parser: 4.2.0 - postcss-normalize-url@7.0.1(postcss@8.5.6): + postcss-normalize-url@7.0.1(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 postcss-value-parser: 4.2.0 - postcss-normalize-whitespace@7.0.1(postcss@8.5.6): + postcss-normalize-whitespace@7.0.1(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 postcss-value-parser: 4.2.0 - postcss-ordered-values@7.0.2(postcss@8.5.6): + postcss-ordered-values@7.0.2(postcss@8.5.8): dependencies: - cssnano-utils: 5.0.1(postcss@8.5.6) - postcss: 8.5.6 + cssnano-utils: 5.0.1(postcss@8.5.8) + postcss: 8.5.8 postcss-value-parser: 4.2.0 - postcss-reduce-initial@7.0.5(postcss@8.5.6): + postcss-reduce-initial@7.0.5(postcss@8.5.8): dependencies: browserslist: 4.28.1 caniuse-api: 3.0.0 - postcss: 8.5.6 + postcss: 8.5.8 - postcss-reduce-transforms@7.0.1(postcss@8.5.6): + postcss-reduce-transforms@7.0.1(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 postcss-value-parser: 4.2.0 postcss-resolve-nested-selector@0.1.6: {} - postcss-safe-parser@7.0.1(postcss@8.5.6): + postcss-safe-parser@7.0.1(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 - postcss-scss@4.0.9(postcss@8.5.6): + postcss-scss@4.0.9(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 postcss-selector-parser@6.0.10: dependencies: @@ -11355,20 +11355,20 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-svgo@7.1.0(postcss@8.5.6): + postcss-svgo@7.1.0(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 postcss-value-parser: 4.2.0 svgo: 4.0.0 - postcss-unique-selectors@7.0.4(postcss@8.5.6): + postcss-unique-selectors@7.0.4(postcss@8.5.8): dependencies: - postcss: 8.5.6 + postcss: 8.5.8 postcss-selector-parser: 7.1.1 postcss-value-parser@4.2.0: {} - postcss@8.5.6: + postcss@8.5.8: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 @@ -11463,7 +11463,7 @@ snapshots: dependencies: react: 19.2.4 - react-helmet-async@2.0.5(react@19.2.4): + react-helmet-async@3.0.0(react@19.2.4): dependencies: invariant: 2.2.4 react: 19.2.4 @@ -12112,32 +12112,32 @@ snapshots: dependencies: inline-style-parser: 0.2.7 - stylehacks@7.0.7(postcss@8.5.6): + stylehacks@7.0.7(postcss@8.5.8): dependencies: browserslist: 4.28.1 - postcss: 8.5.6 + postcss: 8.5.8 postcss-selector-parser: 7.1.1 - stylelint-config-recommended-scss@17.0.0(postcss@8.5.6)(stylelint@17.4.0(typescript@5.9.3)): + stylelint-config-recommended-scss@17.0.0(postcss@8.5.8)(stylelint@17.4.0(typescript@5.9.3)): dependencies: - postcss-scss: 4.0.9(postcss@8.5.6) + postcss-scss: 4.0.9(postcss@8.5.8) stylelint: 17.4.0(typescript@5.9.3) stylelint-config-recommended: 18.0.0(stylelint@17.4.0(typescript@5.9.3)) stylelint-scss: 7.0.0(stylelint@17.4.0(typescript@5.9.3)) optionalDependencies: - postcss: 8.5.6 + postcss: 8.5.8 stylelint-config-recommended@18.0.0(stylelint@17.4.0(typescript@5.9.3)): dependencies: stylelint: 17.4.0(typescript@5.9.3) - stylelint-config-standard-scss@17.0.0(postcss@8.5.6)(stylelint@17.4.0(typescript@5.9.3)): + stylelint-config-standard-scss@17.0.0(postcss@8.5.8)(stylelint@17.4.0(typescript@5.9.3)): dependencies: stylelint: 17.4.0(typescript@5.9.3) - stylelint-config-recommended-scss: 17.0.0(postcss@8.5.6)(stylelint@17.4.0(typescript@5.9.3)) + stylelint-config-recommended-scss: 17.0.0(postcss@8.5.8)(stylelint@17.4.0(typescript@5.9.3)) stylelint-config-standard: 40.0.0(stylelint@17.4.0(typescript@5.9.3)) optionalDependencies: - postcss: 8.5.6 + postcss: 8.5.8 stylelint-config-standard@40.0.0(stylelint@17.4.0(typescript@5.9.3)): dependencies: @@ -12186,8 +12186,8 @@ snapshots: micromatch: 4.0.8 normalize-path: 3.0.0 picocolors: 1.1.1 - postcss: 8.5.6 - postcss-safe-parser: 7.0.1(postcss@8.5.6) + postcss: 8.5.8 + postcss-safe-parser: 7.0.1(postcss@8.5.8) postcss-selector-parser: 7.1.1 postcss-value-parser: 4.2.0 string-width: 8.2.0 @@ -12278,11 +12278,11 @@ snapshots: normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.1.1 - postcss: 8.5.6 - postcss-import: 15.1.0(postcss@8.5.6) - postcss-js: 4.1.0(postcss@8.5.6) - postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)(yaml@2.8.2) - postcss-nested: 6.2.0(postcss@8.5.6) + postcss: 8.5.8 + postcss-import: 15.1.0(postcss@8.5.8) + postcss-js: 4.1.0(postcss@8.5.8) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.8)(yaml@2.8.2) + postcss-nested: 6.2.0(postcss@8.5.8) postcss-selector-parser: 6.1.2 resolve: 1.22.11 sucrase: 3.35.1 @@ -12633,7 +12633,7 @@ snapshots: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 - postcss: 8.5.6 + postcss: 8.5.8 rollup: 4.59.0 tinyglobby: 0.2.15 optionalDependencies: @@ -12651,7 +12651,7 @@ snapshots: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 - postcss: 8.5.6 + postcss: 8.5.8 rollup: 4.59.0 tinyglobby: 0.2.15 optionalDependencies: From db53daff9910e0ea8427f734c3ad91eaa7baffa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 6 Mar 2026 16:27:33 +0100 Subject: [PATCH 71/89] nicolium: update comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/polyfills.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/nicolium/src/polyfills.ts b/packages/nicolium/src/polyfills.ts index 035b6394f..6803c717f 100644 --- a/packages/nicolium/src/polyfills.ts +++ b/packages/nicolium/src/polyfills.ts @@ -1,8 +1,9 @@ +// TOOD Verify if any of the lesser-known browsers actually need those. import 'abortcontroller-polyfill/dist/polyfill-patch-fetch'; import 'intersection-observer'; import { install as installResizeObserver } from 'resize-observer'; -// Needed by @tanstack/virtual, I guess +// Needed by Virtuoso. if (!window.ResizeObserver) { installResizeObserver(); } From 0d88895fd4ef7aa38ea72f613cb06ae67979f078 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 6 Mar 2026 16:31:05 +0100 Subject: [PATCH 72/89] nicolium: this shouldn't land there MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/package.json | 1 - packages/nicolium/src/actions/me.ts | 1 - packages/nicolium/src/columns/timeline.tsx | 3 --- packages/nicolium/src/polyfills.ts | 1 - pnpm-lock.yaml | 8 -------- 5 files changed, 14 deletions(-) diff --git a/packages/nicolium/package.json b/packages/nicolium/package.json index 1c583a058..9e9c49213 100644 --- a/packages/nicolium/package.json +++ b/packages/nicolium/package.json @@ -71,7 +71,6 @@ "@uidotdev/usehooks": "^2.4.1", "@use-gesture/react": "^10.3.1", "@yornaath/batshit": "^0.14.0", - "abortcontroller-polyfill": "^1.7.8", "autoprefixer": "^10.4.27", "blurhash": "^2.0.5", "bowser": "^2.14.1", diff --git a/packages/nicolium/src/actions/me.ts b/packages/nicolium/src/actions/me.ts index 5b27aa032..221b10b44 100644 --- a/packages/nicolium/src/actions/me.ts +++ b/packages/nicolium/src/actions/me.ts @@ -112,7 +112,6 @@ const fetchMeSuccess = if (match) { try { const frontendConfig = JSON.parse(decodeURIComponent(match[1])); - console.log(frontendConfig); useSettingsStore.getState().actions.loadUserSettings(frontendConfig); return frontendConfig; } catch (error) { diff --git a/packages/nicolium/src/columns/timeline.tsx b/packages/nicolium/src/columns/timeline.tsx index ecf93f12f..a2937e029 100644 --- a/packages/nicolium/src/columns/timeline.tsx +++ b/packages/nicolium/src/columns/timeline.tsx @@ -110,7 +110,6 @@ const TimelineGap: React.FC = ({ gap, onFillGap, firstEntry }) => }; const renderTimeDistance = () => { - console.log(gap); if (!gap.minDate) return null; const maxDate = gap.maxIdDate ? new Date(gap.maxIdDate) : new Date(); @@ -397,12 +396,10 @@ const Timeline: React.FC = ({ } = query; const handleMoveUp = (index: number) => { - console.log(index); selectChild(index - 1, node, document.getElementById('status-list') ?? undefined); }; const handleMoveDown = (index: number) => { - console.log(index); selectChild( index + 1, node, diff --git a/packages/nicolium/src/polyfills.ts b/packages/nicolium/src/polyfills.ts index 6803c717f..13a3db400 100644 --- a/packages/nicolium/src/polyfills.ts +++ b/packages/nicolium/src/polyfills.ts @@ -1,5 +1,4 @@ // TOOD Verify if any of the lesser-known browsers actually need those. -import 'abortcontroller-polyfill/dist/polyfill-patch-fetch'; import 'intersection-observer'; import { install as installResizeObserver } from 'resize-observer'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5ce275482..fc04b9dfb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -130,9 +130,6 @@ importers: '@yornaath/batshit': specifier: ^0.14.0 version: 0.14.0 - abortcontroller-polyfill: - specifier: ^1.7.8 - version: 1.7.8 autoprefixer: specifier: ^10.4.27 version: 10.4.27(postcss@8.5.8) @@ -3010,9 +3007,6 @@ packages: '@yornaath/batshit@0.14.0': resolution: {integrity: sha512-0I+xMi5JoRs3+qVXXhk2AmsEl43MwrG+L+VW+nqw/qQqMFtgRPszLaxhJCfsBKnjfJ0gJzTI1Q9Q9+y903HyHQ==} - abortcontroller-polyfill@1.7.8: - resolution: {integrity: sha512-9f1iZ2uWh92VcrU9Y8x+LdM4DLj75VE0MJB8zuF1iUnroEptStw+DQ8EQPMUdfe5k+PkB1uUfDQfWbhstH8LrQ==} - acorn-import-phases@1.0.4: resolution: {integrity: sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==} engines: {node: '>=10.13.0'} @@ -9126,8 +9120,6 @@ snapshots: dependencies: '@yornaath/batshit-devtools': 1.7.1 - abortcontroller-polyfill@1.7.8: {} - acorn-import-phases@1.0.4(acorn@8.16.0): dependencies: acorn: 8.16.0 From 7a6ec96e9fee32e64273a591a175a9fa6e18801e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 6 Mar 2026 16:46:47 +0100 Subject: [PATCH 73/89] nicolium: remove polyfills because they're no longer needed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/package.json | 2 -- packages/nicolium/src/main.tsx | 1 - packages/nicolium/src/polyfills.ts | 8 -------- pnpm-lock.yaml | 17 ----------------- 4 files changed, 28 deletions(-) delete mode 100644 packages/nicolium/src/polyfills.ts diff --git a/packages/nicolium/package.json b/packages/nicolium/package.json index 9e9c49213..4081a6a08 100644 --- a/packages/nicolium/package.json +++ b/packages/nicolium/package.json @@ -89,7 +89,6 @@ "fuzzysort": "^3.1.0", "graphemesplit": "^2.6.0", "html-react-parser": "^5.2.17", - "intersection-observer": "^0.12.2", "intl-messageformat": "^11.1.2", "intl-pluralrules": "^2.0.1", "dompurify": "^3.3.2", @@ -124,7 +123,6 @@ "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.1", - "resize-observer": "^1.0.4", "sass-embedded": "^1.97.3", "stringz": "^2.1.0", "tabbable": "^6.4.0", diff --git a/packages/nicolium/src/main.tsx b/packages/nicolium/src/main.tsx index c05eb7814..9ab9cd6fa 100644 --- a/packages/nicolium/src/main.tsx +++ b/packages/nicolium/src/main.tsx @@ -10,7 +10,6 @@ declare global { } } -import './polyfills'; import '@/storage/migrate-legacy-data'; import React from 'react'; import { createRoot } from 'react-dom/client'; diff --git a/packages/nicolium/src/polyfills.ts b/packages/nicolium/src/polyfills.ts deleted file mode 100644 index 13a3db400..000000000 --- a/packages/nicolium/src/polyfills.ts +++ /dev/null @@ -1,8 +0,0 @@ -// TOOD Verify if any of the lesser-known browsers actually need those. -import 'intersection-observer'; -import { install as installResizeObserver } from 'resize-observer'; - -// Needed by Virtuoso. -if (!window.ResizeObserver) { - installResizeObserver(); -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fc04b9dfb..7e36840fe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -187,9 +187,6 @@ importers: html-react-parser: specifier: ^5.2.17 version: 5.2.17(@types/react@19.2.14)(react@19.2.4) - intersection-observer: - specifier: ^0.12.2 - version: 0.12.2 intl-messageformat: specifier: ^11.1.2 version: 11.1.2 @@ -289,9 +286,6 @@ importers: reselect: specifier: ^5.1.1 version: 5.1.1 - resize-observer: - specifier: ^1.0.4 - version: 1.0.4 sass-embedded: specifier: ^1.97.3 version: 1.97.3 @@ -4133,10 +4127,6 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} - intersection-observer@0.12.2: - resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==} - deprecated: The Intersection Observer polyfill is no longer needed and can safely be removed. Intersection Observer has been Baseline since 2019. - intl-messageformat@11.1.2: resolution: {integrity: sha512-ucSrQmZGAxfiBHfBRXW/k7UC8MaGFlEj4Ry1tKiDcmgwQm1y3EDl40u+4VNHYomxJQMJi9NEI3riDRlth96jKg==} @@ -5425,9 +5415,6 @@ packages: reselect@5.1.1: resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} - resize-observer@1.0.4: - resolution: {integrity: sha512-AQ2MdkWTng9d6JtjHvljiQR949qdae91pjSNugGGeOFzKIuLHvoZIYhUTjePla5hCFDwQHrnkciAIzjzdsTZew==} - resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -10372,8 +10359,6 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 - intersection-observer@0.12.2: {} - intl-messageformat@11.1.2: dependencies: '@formatjs/ecma402-abstract': 3.1.1 @@ -11623,8 +11608,6 @@ snapshots: reselect@5.1.1: {} - resize-observer@1.0.4: {} - resolve-from@4.0.0: {} resolve@1.22.10: From e3dfa04094432a3828af3e9eea361f778fff8cc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 6 Mar 2026 17:16:56 +0100 Subject: [PATCH 74/89] nicolium: migrate tokens page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../src/pages/settings/auth-token-list.tsx | 188 ++++++++---------- packages/nicolium/src/styles/new/index.scss | 1 + .../nicolium/src/styles/new/settings.scss | 46 +++++ 3 files changed, 135 insertions(+), 100 deletions(-) create mode 100644 packages/nicolium/src/styles/new/settings.scss diff --git a/packages/nicolium/src/pages/settings/auth-token-list.tsx b/packages/nicolium/src/pages/settings/auth-token-list.tsx index 2f108769e..92c6469b3 100644 --- a/packages/nicolium/src/pages/settings/auth-token-list.tsx +++ b/packages/nicolium/src/pages/settings/auth-token-list.tsx @@ -1,16 +1,13 @@ import { useInfiniteQuery, useMutation } from '@tanstack/react-query'; +import clsx from 'clsx'; import React from 'react'; import { defineMessages, FormattedDate, FormattedMessage, useIntl } from 'react-intl'; import Badge from '@/components/badge'; -import Button from '@/components/ui/button'; import Card, { CardBody, CardHeader, CardTitle } from '@/components/ui/card'; import Column from '@/components/ui/column'; -import HStack from '@/components/ui/hstack'; import Icon from '@/components/ui/icon'; import Spinner from '@/components/ui/spinner'; -import Stack from '@/components/ui/stack'; -import Text from '@/components/ui/text'; import { useAppSelector } from '@/hooks/use-app-selector'; import { oauthTokensQueryOptions, @@ -62,102 +59,93 @@ const AuthToken: React.FC = ({ token, isCurrent }) => { }; return ( -
- - - - - {token.app_name} - {token.app_website && ( - - +
+

+ {token.app_name} + {token.app_website && ( + + + + )} +

+ {token.scopes?.length > 0 && ( +
+

+ +

+ {token.scopes.map((scope) => ( + + ))} +
+ )} + {token.created_at && ( +

+ - - )} - - - {token.scopes?.length > 0 && ( - - - - - {token.scopes.map((scope) => ( - - ))} - - )} - {token.created_at && ( - - - ), - }} - /> - - )} - {token.last_used && ( - - - ), - }} - /> - - )} - {token.valid_until && ( - - - ), - }} - /> - - )} - - - - - + ), + }} + /> +

+ )} + {token.last_used && ( +

+ + ), + }} + /> +

+ )} + {token.valid_until && ( +

+ + ), + }} + /> +

+ )} +
+
+ +
); }; @@ -176,7 +164,7 @@ const AuthTokenListPage: React.FC = () => { }); const body = tokens ? ( -
+
{tokens.map((token) => ( Date: Fri, 6 Mar 2026 17:18:24 +0100 Subject: [PATCH 75/89] nicolium: add missing border to chat widget in black mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/styles/new/chats.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/nicolium/src/styles/new/chats.scss b/packages/nicolium/src/styles/new/chats.scss index 447714090..73e7b083e 100644 --- a/packages/nicolium/src/styles/new/chats.scss +++ b/packages/nicolium/src/styles/new/chats.scss @@ -20,6 +20,7 @@ &:is(.black *) { background: black; + border: 1px solid rgb(var(--color-gray-800)); } &--open { From 0f1e6e5c419bf6e62bde197f73b0a2fe559843bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 6 Mar 2026 17:19:21 +0100 Subject: [PATCH 76/89] nicolium: fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/pages/settings/auth-token-list.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nicolium/src/pages/settings/auth-token-list.tsx b/packages/nicolium/src/pages/settings/auth-token-list.tsx index 92c6469b3..e7ba3d4d0 100644 --- a/packages/nicolium/src/pages/settings/auth-token-list.tsx +++ b/packages/nicolium/src/pages/settings/auth-token-list.tsx @@ -60,7 +60,7 @@ const AuthToken: React.FC = ({ token, isCurrent }) => { return (
-
+

{token.app_name} {token.app_website && ( From a960411b821f59044d1965f7bee5943b6b7a10a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 6 Mar 2026 20:42:23 +0100 Subject: [PATCH 77/89] nicolium: only count public/unlisted interactions for circles, include quotes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/actions/circle.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/nicolium/src/actions/circle.ts b/packages/nicolium/src/actions/circle.ts index 47c94d9b6..018605756 100644 --- a/packages/nicolium/src/actions/circle.ts +++ b/packages/nicolium/src/actions/circle.ts @@ -49,6 +49,8 @@ const processCircle = const response = await (next?.() ?? client.accounts.getAccountStatuses(me, { limit: 40 })); response.items.forEach((status) => { + if (!['public', 'unlisted'].includes(status.visibility)) return; + if (status.reblog) { if (status.reblog.account.id === me) return; @@ -66,6 +68,18 @@ const processCircle = const interaction = interactions[status.in_reply_to_account_id]; interaction.replies += 1; + } else if (status.quote && 'quoted_status' in status.quote && status.quote.quoted_status) { + if (status.quote.quoted_status.account.id === me) return; + + initInteraction(status.quote.quoted_status.account.id); + const interaction = interactions[status.quote.quoted_status.account.id]; + interaction.acct = status.quote.quoted_status.account.acct; + interaction.avatar = + status.quote.quoted_status.account.avatar_static || + status.quote.quoted_status.account.avatar; + interaction.avatar_description = status.quote.quoted_status.account.avatar_description; + + interaction.reblogs += 1; } }); @@ -78,6 +92,7 @@ const processCircle = response.items.forEach((status) => { if (status.account.id === me) return; + if (!['public', 'unlisted'].includes(status.visibility)) return; initInteraction(status.account.id); const interaction = interactions[status.account.id]; From 730e0de2247adf84d4ff4fc165e860c560da181a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 6 Mar 2026 21:58:40 +0100 Subject: [PATCH 78/89] nicolium: update logo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/favicon.ico | Bin 67646 -> 67646 bytes packages/nicolium/favicon.svg | 9 +++++++++ .../nicolium/src/instance/images/logo.png | Bin 4362 -> 8936 bytes packages/nicolium/vite.config.ts | 4 ++++ 4 files changed, 13 insertions(+) create mode 100644 packages/nicolium/favicon.svg diff --git a/packages/nicolium/favicon.ico b/packages/nicolium/favicon.ico index 460204573a2174f1e9d352d393179e3fc494d883..92a091d5601a1a1ffd4ded409f90041f95d07132 100644 GIT binary patch literal 67646 zcmeI5TZ|-C8OLiNVNfoE7f=E?sA$j!UN$T{UA4?&5byWkgE1z?Xu=DxJWeOP@T5`U zVFJ4hLtiHIAgmaa2r>wSM_)8iL1|rKcL}h)?5&5XKIQ+b?wYQuuBxu8uBz^y`uAqK z&;5Mo_kHI(RbA)wYT6wBRjV4_Z`5wSbWXcQ)3lrMha^d~(bz?mzm1a##m+|7*A2Jz-tFS+AJiEC-45jneJ+ zJ$euD1&}oqLN)IY!g>`>&<2z%mA+uM?51IK z?-^J3p8C3ay>`(2!fa6}x3mqtdpG9q+a5g*`FQX50a0ZgUvOA|`|@&=ot@FI-Foh0 z@ZV2F={0d4eZlM)Kwr|P-u)5m;8bE=hxiNI7vMXkU)}WTB{K)3qyj&F@Z85?15ajU z19+D7Tve;$R~z}7B?A5Hf_KKU16`e6t8_j!OXH@j0|Md;@GS6XdK>8ebkG_@vq~V1 zFTfh|EdBnhYJ6Dk!}|GaaXt6jc)mcLt}k8q#BeK2nRNo<3vf;MZj>D~D&3#V`Z!0) z->jHl1&e#9eXm@tVt%n{nBN;Uo+SCUFVNLfK{f!t|Hm|bf5R}Bn@c-ON#vMR*|kIM z1bEcFe??B{W%Gz}=A%*NljMnxVgEnFn(&@u2k4`n#_zB0 zd-qDT^B>uFhBj7ZA5s!oiY^P?uRCM5*&6CRSll;lzMx^KHMgALdlJw4zMCblwF8|+ zne#mk-5;lEyP>PYZaHBKuho}!4yI`e&Y!7Zyh3=S_nW^8{ZZyDmWUpLIJxn*;Q zA!p{kpsAbRh$EN3OmBS(dj5<(sMv@6^8?2&_1BqHyl%9wf^GiK&C(x6{<%r}0?+ys z^4%@8aw+ZDW}snbX#-e8uf{?FE(!5zR9UuInc>;r3uTJuibqCdsIIr7B)oP;lkTA#w@Rwr7FF`@^()%d+0;zBfNGZN5Oxj#;ce8nV-iGbUuyozu=auu6KXn z9`7`U<=iaf&eRuRjrskk>z%Bx_ubs%gmLYX`I#SiekS&hw$H(~k4~e$gJhC(RFL;U zVG@U3tTNbR5q7`3UO9U;`a2n`9hWbMoxUYplh%tYpTj=?nz8*kIg=2?dYaRkl^p|f z_n(oxT>6Mp4mS0)e+`5lH{;axmuXhI-xq$EW4WC9;rYMbZ>Y#U0_Itk*9E7X>jL?n z-mljuOZNJwlW1?%!upINL= zd5&`uZOgt$@ry}W?v}?h`XYX_-F@a~x9@#_Jv{I? zyXXJn+QK;ZC3$k6rndXn1@V0W&LmDk{=P4GHoorjU7sewAkRMKoH^!L$i7qge^W7<_oeudpr$eE>ecxnJM*#o3EVl zE>eb3eeL=JU7c~u3A>oc`ZP-3Sbl`l+QF0AOR8}0TLhq5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq z5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq5+DH* zAOR8}0TLhq5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq5+DH*Ac5&7 zVDU901fKWF`3qls-WykQzPRR%o7%D`p`$JO;@$?^+Qpvymga2`@fCN0w&rcHqiGB7 ze5Lss^vAa}uK+D?eXHY(o8JB`%NgJ+((M_`sxF(h0Qbv#%d)msI?EEQ%3Ev2YFnK} zge-d;TZ`7V)fNeq-ELKcZwUfr_gj@5$V}m$DB(Nt5{2wsK;0^!) literal 67646 zcmeI5ZHOJ!9mZ#qH5-kM)M!jwN}AS+6yG(W_CvRpmX=m&p$V<14T6GSd_hrA(Y@wV z3ze3XRIsH*LSLc%QfP~|Zmgw`9H#;V#j}kq6U;?`Uj8|p{eN~p{M~Rnf?J}MQCdJ zM<{AQNv3}QSrM9={t=2AP?G5%KvsmNrhkN@29#v_2apw^sp%h~r~xIJ{sCk~XlnXL zC~81Srhfog5t^F*5sDg6lIb5nR)nUee}tk2lw|q`kQJe+=^vq}0VSFK0c1sJYWhbg zYCuV*e*jq#nwtI*iW*Ro=^sE=gr=r{grWwNWcmk?6``r=AEBrLC7J#KWJPFd`bQ{g zKuM;509g^5n*I@r8c>qyA3#=wrlx;{q6U;?`Uj8|p{eN~p{M~Rnf?J}MQB=;{=^*v z>#C-d9P}G_6IJI?ReWk6`R_jx^LVfq{1L3Js&>UC{^!9#a1p37l}ch$^T_lUzhFD~ zFVJtwEr9DlRkeSZ*iQoG@-FxuIJS~xDhf)yBhjBY$ADYG+dzCfyw3U=qb{GAI{*AU zlZVUaUu-)Qj5?)C;!$U*`in>KN$?|&qZyG$!kHS=cZ8% z2x^=Ly*S8k3;z92{H_7t1+RF-(A+zUlBNB}!ApH|(z)qRsqIMesv-#Wm7+g>H-X;( z@t7Y=z1lkeUQ{**#8?ad?3t7D)HvA*%1*5k|I}5M{-4J8Ea;{6a@(H9{=~9~z_*V5 z>w)i>ba~!GpD6$B1L`VEe{p^F09@Z!7T5UIb=@1l>FTrmKlU5T8k1G~rM6P^S6%ql z`Tq~Vhi7M_YqIZvp;CjtW5K_Hi{rDL!~VjdCbG($QQx#DC&AZ$Diz;bz+ulAPLr3h z-#RR+@TEMyADDx3xl?^({!m}p`iuLE!8~yK`Yi9qeklLm41T@{bbaX)*V#ObedW)m z%N~2wIPJ;HK*zuG$G@Jn?DLG{_}4Ysji3+(^gj_i1Ds!pkqh1ia|RZX7%STz{XXu1~)Xykf*+M7<-` zU%V0XRPY+`(Z$)!V1H6S-m#JYn*x0+#*aai&-hm2jGC*^Um9HrLeI^=>ZcL=&jq?} zbmPWn`2_aIuf$WV`>1>5`io2A>e=A$fR8@TM&spj&{Gb!9|s->&faIK>(T8!9X4Pi zwNHEUHI(yzmQvzw2G4oMcUtOQ^0urWt+P${dUprQCE(Tx+I$v?9@X;%4a|?YsQn5S<918UD zD)yghlGeRyg@YE*dAJ|$#io?!BKX|^{sXf5_FI1o`}2XWBYqv&SJx4{K`%;dTLlcD zg|w8W{dCXn*O<8mgx*K|mOiD7sU8>>7IUONMqn;{M3<#M&&Gxfc`$?GvEfG_cFf6 z_;PmoKJm}MH-Y}%)6V~^XjDm$RF`kC)ZbY=1*F+>dKTHP^T2~Z_nGGaeNSK&u@QeN z;ydO2ES8#={|cl_5nTuAt1+f$0h$|j0-bMcZYW_0idV1+d<*;(JPXP?#s?V->ZA9- ze+RdL3xJ+Ql)y&(F2!5Q-+3(ce)RwdeYQKOj>G7ud(Xq*KA^uv+6-)NSYhajpOmL> zv-}0ncPUrfF+S+n(BFam6HI~6f^}d8Hsar<`cocuu)G8O1Bj!0;zw6V-CrAl{^Ivhpt0tAJ*jJv{XpMYE&KfR2r(LW{?}-r?v3DeFwD8X=k*ma zJ_3Bd-?U_@2g`TexV`%zk z$3j|z=7vF@PwRWfd3TL!Q@`HVJ_+)3fX2ijFqHQz_$f{QtYE~^m>vf{@27TW`!;bs zU z5%%ie*YvNyhD9nc{fn?y|GuVw{WUC7f$3j_z54ex{p+t`kqS)zBJ9<_ujyZZ4U1G@ z`WIoZ{(VjV`fFIE0@J?;d-d;Y`qy8>A{C(jdZ6EF*yREHt*0VnTVG?q7%&Em0b{@z zFb0ePW55_N28;n?z!)$Fi~(c77#JA??N$F-NVYD?(b~pGc0g1y9^O#`Q%AB;{8?5P zCcEt!R_9OXwp&x}_S{6bolMdmTWrs(J)11H=QQMJlBv!H{Ldx$nvoG7+Vcs%4s`c- z+O2N?g=B5J-Rk;pC#(x;f30_HT${cfAFhq>C2ex)u3P+pN%DSZqTr?e-y84@ + + + + + diff --git a/packages/nicolium/src/instance/images/logo.png b/packages/nicolium/src/instance/images/logo.png index fad9d09c7f80e4a0d48b7bc6912bf2276b56557c..1d8154e069f33918f0043ad59e49f7924c9fcfd8 100644 GIT binary patch literal 8936 zcmeHMc{r49+aIMoo`*b+_j;F;=NkfETEHemYNr=&t zEy})Rof;&&v5wj9@1pm6zvVdIKfZt7zdpw?=DN@2cb?aIp7(WK=QUAgCi?upi~SCR z!T1d>=$XS{++fHJ`)w!qy6|!M4fx{nG1ouGHTdZ`c8le5_TpI>?9G!uSXXy}=Y!WT zm|uj!0*}C8VUJ)i1|Ws~gu(nyz+f|1U@)y0FxY{+Y1J4cVDR3(VCw^e30&R!aFI~N z+knX9Yj6?GGqZE|KK0)oE97RwU^{4L#+K+UAXr!d%&PuO*k{$0K6u#h;b-5@WR7dZ z|4>c&sYIALgPTL7&#UJwsHQBP$y`F_(lpYR)v{Jp^Hz`Y=IUCv;8wkRNW-eVLldeOcBXj9mnRJcp&C_|C$gE97&ZcJGrUqdXnZrQjF|-H_ zjU0w{9s`lh(8^_K5*gar3}h}7L1b#?Fg0?SNFo!F!_>-SY7&{q93~=WNJD~nsm+dluFRsNgB|MLG|SlqJmiv@i9r^RhQx9Gnd{zd;!Sm4h;RJJMq z%KvQ{2uXq<$mMH`J1F{1Uvm=+7}t)S+`D%3{KotHAAJ0K1peGBD6~&_{{azEF>wh= zDd~fUWMmJ^9g$Z!3O{ySQAzoP%1PBzYU-yoG_?>&Z5`B^v*&d6(C76p7#JEEUo}7*nV+3II|Ns7d5xDXuiStVI=~H24 zjiEnV%jS1}=d(B{$~5_{gik&E*>Sbt^S^l(x%`=PwKKB#&%Yn<#>9VF;2SMg;q8uu zi1s|{X}9*t{Y1Gj@J*uar}$<(tE^e~g8Zy{ZVl(%d-UXeMV*(~JzYG5ULTHW>hfoi znV&R1#<7hb=~2m(H-v7t9jS~62>x(^R2Gtvx>0XZ%3YpCLDD>VGuLS-m%5$eN+)r3 z&5-+5uG6+VNQJfd%S-j)7N2uVXi)pPCegZECwsf=ca4*Wd(~p|l-29hj`t3Wzu;FZ zAD};${(AS#;Pd^^sK;CGr-rP`G*@J8MOV?)E>3qaDMcH9>GNM9Uj)tUfW!BMWMy#l z#dUGn87me=V*lcPBbZugvyhR->+h#@XV5Ij%V3-j>ulWH81}s^>HZZb`<}GGTOVKk z{j9bo|13rT5kLLmFMb;_%iA={ytNT5zX99k^0|PgzfNnllOnP|z4GHnZSB`dVNx3I zcBajuW22wn{k+fWn4|qw!F^nlQ}TQ5-IsD$`1S!S9)S zIV(1LZX8;6{H>!wEx*PO2#L zD5zgBbHufdPDYkrXY4Ua=i7?R;AzF55+g zHs+--Ee<6gfJSe+U4OdI>ll2PFf#LzPCoUVs$&F0k3_ewD^$8RLcPyG+OEFPD<+k5 zm)~9@nWc&jr^;{f-zrpUt@jBwi!M}hn>7@X+)RH=T0hhe_YUdVE9Z@NwqI7N)cS#Y z)_j}yU>3dO{fH|rAuCvb|E|eVg0NkM`l`*3nW}#+~&;`lC(++ z6KlX$swBZ$%f5fHlK%RtwJ84nn`N(Xtgowq9B0LDhqahyCBrM+wnpP!!yS@`lM;Vr z1X;v6?dmJJFc}ZDb5PxJj?tQFyVCabGs-__j?BD1K!{WQJl02(13nN&;MZ7{Nny?s zSE5P6xcSYRn@@bmyk5^>Tzpww=M z|NVDw$N7Y8?v8AgM)>>Iq8V{W5#>9!6^{p#*25^vS+DTooe}}j?}TxOUV0bi&Pqa~ z85P`SzN8$PQ=qnLP>&c0${YE|^Uye%_p1tS9vW5^5^?$2En<9zC)f)r{d3{om)yr3ZU212C<7{is`}8 z?e{Fz+<3pP#_LhBNA>Z99%0-?`yCAaARCq}XPmxK9p{rcocLmxNyVcYp~OSZYR4)K zyL#-uf|HsL&L@yCS4VGJgvTkv(ir%)8+EEy852Z#6A z?huTEn(y6P{si9lPzhyD$v!A&A0!aY_gVY6HH)0lfNYAwn$M72hkcGUiA<#?%d5m{ zQt>ib(XA(0dMxZj3Og;>I~pu|ckfTqu(o1Yxs%>cvrQ9>(ayFRCHHwX<||H7u~>P$ zOnP+dGON7RtJv$d7$kYhJ3t>VlM>x(E&%NvY=#!42xF4f+I;3YNWI0n_SDE9NZCQzyEXv#%mYU<= z957*Iz|wKj?*Am86<=`c!1X=}Dk@fLZrd->E|Py!Kts&!;5NxApnqUtLg$H3Zh{Y( zjulA24LDr(W$(bO*L^V>YWluBet`~k4;x5)eKV!U*`+*V&w`L~bf^y=uXEeV(sA$R z05Vr#969&+6cyqI0eutQ>L*iq+teYuvPZ%(hGFM2EK#dbA4;kI)NsWLBs(qT*tpww@vq zQhQmzYw}rhRfomx1}NV|XWGWHN$D%o*n$c|yIm6l1g(N_~r|Ge}3~3UH%Y% zAqjbC9Z%?Bwh3q;;^<@(R?p|?R*D{{>&NDuRmtYVD(#gGD75Y|xJ}G^?)MdA@-$Si zwct2wG|)sR4$M$zost`}`p4qZYXEn#i1c1#S>XPU2pUQ?%FB^lmuT8G5#8E%1RdYI z))NI)My&a)9!+(?1b_)HH-#F>3qq$Ov3A2y2`EK;NiUHyhEBk?`!wb4g`5m!Ipn2E zk6RU~j+iP#F>Li-CdyJ>K``K%f79EATaQnYNXULaJY}_vk-H+pW*b%I;XV-v-p|ER zC5Y2TvK&Aikx5)TJRduMxzF&11tuWY9*U|_H(D?lirD?` z)npq8-nk8(KDy^-(yU$eD>&RHw5!jvDrn{Ko++^vwVKag#~v$ca#TUs+=|$veMSiyyH^0;*bCwEqQVNhoDz%=wB_sa>6(HzNca~F+*`YwOl#cqdTq9NM3 zioY-7W*zrPjk92cPYc1(NBTOig8o}!9JvL-Nj7Ao8J;yY&MlP$@7&nrFy>oB>fJq# zBVWP>bNc1dvk~*)g z&$_3;G1}buLSB(8rhNXPRzcDnlfMQUUsos%OkAi@3LK(QrlVV(o7JWn_tMF3H%+_! z=ckI5PjRIGgtW_S9wvpkthX&13=xlWO*df`;c)Ws3%azXLi2%X@?u?w=SvEYq?NYQ z4k^zItVR6PmIF1km7mytK4Dz#8y66(D%oV^lOYMA)ZdlZM7Ka9eKB{Xn+ypFC@hC0 zE<|{+!At2DXoeXamiwkc!!H8}o_BQVMo1D4XI@FRHK0H)0-sq1LoSM3itO;(m*-gn z#o05I#CS-!6#HyFI2qdcazx74a?OkP{;5$qswHMMDeMuIoFR-ubO=ILL)p{=(|8A! z4JMi9P9;lD9S(r-h<8he-P^DeQqUqdIBs70fuT7aX34NeK9 zQ6URRrJO()fWwEE(tD{`v}t$f5cWqPbh;Xib{0(vRmi8KdSg~sLS&_sICtg}7^f{4 zHB{4?C|4k=w3Pm0T=4bxFL1b#k3W0>>bst(k!FOK+~}}UYB|AyNQBacoiCfj3$Cty z(~F;qOMos-M2^(K{0#^ru0JYy4xj8eoD`NEZ#?ycC^Oa5RyeWFf42^7v}}tVG_vct zsg<^bh?O#+G2`TcGel2D7@Zk)GSBcmXNQ>K2McVrH*+^BsLD!47$-gaBZP)u6+EF) z=@}*s&sXR}^=ooWjTeJCeiNc>nHsroM}={@N?dCprXj1YM>*)ewFKhxQ9(1%QE-Fa zvR}<%#}WMVADbgf^Y45D<$wYcOl)m~O1AtcV>**u6~fh9Is)&adn$6Nxmmpk_O6~P zqb$k@%r=bFak*adQ1jS$G*x9apA^w{8M~Gg?gXUB)EAwP72Vl%tZqM>sa@fX<5JXh}7F?{VxGebE)q?_?$|oJ~ zJAFq8(iQX4fn=7yo_DU~M&@M~^vZoJwgdaq>pY3hJF=z=6I(zS54ATcY(D$`tGw?5 z)0@&dz8cl%%q4~SG-C7Xx@cZ?ny(C|Ryt%&7YTr>Z)KTO&Fx$5%ue-7^*d7%i6mQq z$o{-rWd)ZL)_&Uqs?_ig=v8oH=ID{-rO;>wEQDYJkplWv&w7d$2x`dSw#3Qgk&= z8;^g2Y=yqe|B)#>Y26{&{WV~ntj*EDRysZu`01Pw5nu;GfM_?z*yhDiqNE^pDu{2O z_JW4z=lMSl25whC$of66<^VQ>AB89KD0iREA91zpnH9-a;_^I=;MgniJLY&AZ%fWA za?!>r$X6fowNE`gI+k-x%-lmwD<(O4yOXQ>hGv|9ZeJUCS!yNL(!+0jca=n^Q3JLs z%)78_kmb`RGHKltvC;7(xL9W}W)(d%X`L9~aVrCBv8k~*bey;n@PKYwJL2kVU7~2| z;DNJXRmK+krh#?I+s3P^FJ~nBfch(6-giMOBke9YqccJoIf^qt&I7b~Y%9N>VC71B zi9heHro?hT$E}k5jx0P@*{TeVy%tt1<;7*28txk+5ZZ6j+O`Q>*>}WBr9FyDCcPbT zUH(+nzM@O<@`Goks5 z)iNpGh2fT#`_~jI1))*v00Y*6+t2MUir;)8O+RNa{@#gIDRGR`qfl8LC|P#g@EDMi zX8JA3gkYH7g{K>_0BW&Pz4^&2)-(CrG1bZT4&YSab+e`?P3>CP(NO9O_2y01p_`Z7 zt8jGX*5j38HHmxM$8CUzK2=si$KWxbu%e({$zca-FE|yIL_vfjlh#k>+=Bnma4VL- zJgpZTf`M#EtoDYVfeiKeBonxS0r4~i3Yq%pJ!e{S7eG71R;((N0R%ZH`cK5sU z-U>!L_j(svQlZxZ&~;YPnrkh$vDSyZbGaqll2+*E)*``tl7ug(89Ewzal(o5v}!D; z(2^`3^--fJN?}PwVINEr6p)%6BqP|C8Y8ZL)J$9jD?srM)Q{5020f+ zPjhDy9&C|;&+R8IFoQ`Qr;c$p(oaAtk!d@4I@V-DB*i?4csWkY^0v)1P&0LZZo1I( zPftg;C)sK_CZaz?hAjmiBW zuO87X$^)J+!ce;VyV^TGx^R$4!(4(lp16MvLBBSEpOAOVQP#TTv6cb$zP=+wCrlNI z?#cEuo0Gfp03;S$wE#&VpgFaeh1|#@eojt%FHQBy)3O7S&@GgwvIZVqI zxNOe4S0e{jO4DRu^XsH_ijC#lDT3uVwuGk5kp9TgT7I%7>)9>rMX5Ldx!X=oZfX_m zaWmPvmAQzh=Dz>zEeW`ScE()$Wl^VV94qF3XF~4Eq9&CbY3-4lT@aG}tdJyecRj7s z2P?iwWPXr0?T&_`lVLQc=a;sEx~P$7QGHdu0D zfs=k|2hzaa^0J2?IA=Y({K8DXZ_o8}B_VRl9dvD)lcl4F+N>MR2{;0FwVmM|b01@V z^vEi6wZ+#svTSBzm+IfUa;3S27%_+FSv$x7~(4}^TmTZ!oobB+ryl! zy5v-o4O8@?9*~baD%fk>(?bgaS=vsjx-F{U$}9|fV$bcN^$(qOL5f^YZ?HOkbEa2g z&IQt-GYZYI?tDVau~0usvW)wT`P+bl&wq;V{(TypePT~8VqbiRq$6&)1-d8y)5s8n7W>3$JnngwI7E$UQ0oT6;Ea{%d$Z>K7x0`3Z*>!Qs z{({xlaxhm3Xz`IN`{5y7hjFOitRf`|)^dtd6ZeMu48OatQ6zQvJbwUK8k_oUei@PGQaB*$Dz2+qmV136LW{H?%O^$8Uowa{aX;09s?cIjbOhN;L;L? zO71a$au8i4Q>gzvvbX-e5o|&fQk-Ha91LVR?RO>IA(`%!X@>4(sW&N4t%ODPzdP0Y zDL`lMc696Obc7;>q2x!^0o{VI1S-~*gkRjeeDJ}C>~oTS^HbS>wMsG#NyMl&=G{!Y z!dSkFwVl!emNPGGW`ZEy0gleWUogMx$dX#Slp|7^2?D%Z84 z%;1OE^uN~xe3X}V!EzDHxrY~^l?VnY)9u(3V=5z}>tTqs*;`phFeV44J^8)$ZFfr* zRr;jguSQ;4_=Y%C$at0M7E$~3JPZFV`#~wc7Ak@6^u=YD^RVYR!2#@Ylnz`$o1^a= zg>C8GYQPP2;4SJLn+@BugW)>q4QqJs(k#=Ir3^BZAOCB(_D3L)iyCb!R!%6n6l$DIwsc z+P67yfuKbb$Q|}F794;SX#$h>Ua2TnL@>V)^t2?DT*zjVLqnv6EN_gSo(*ABr20D| zxN)22MT-zWRT}P>PLBY6+7RbreVjPmH`5i1>6k^akJAM9Ubb&$VyABEh(umZXDD)k z4Sl2Pj0D!Pl8H^GN8B&|6W~`+NL_4lkdO3ldTTBfRh-2rY~;dn>If9{cFFcko>3S* zqMaVhzW~+RGbA!qm1cuk%99phdqhuGe58?a9Wbq_RTD>OqY>rXpPOC-ZA4*zf)gs5 zrCX;noWjIH{pSVL?i%nEL$AsjQN4f9BxX~fHwaNgZv*d}acl!jH)2Rca*czM;T#%} zyV4)&%qMZuVTaVAoP(;4G0>HiROTGI4wcL>p|8c^e0~+J zZNi}Qy2-cZcNw<<(MI`1T<6J+s2kwIz|xAf=<&`hs5u(j^0nmHJJ2_su(|MeYpybv z!&D6RLZgZtXinBuEiN>d7{);f4~_Sk+wE{XuENv7}jeN-C ziL&RU)HOlT;XElyTpiNaygRv}qnrL3XJ%6Hgdt1!Axq!07%FTQ6*=`O zlUJJzW%Il|aH_4dIHL)oh3|@6!3K#yg<`^|?(*KR<{7|t^X}diSKc8(6WC;QJ)=T; zstuUFm%h^oz03e98-1iTXnVjtGQzxM{gYDq1Z1m zOf?%X9TaHOsXN0GP!Vov%ve$4vM^N>Y-tIgN+0Kvexd$Z7 z2k112ZWT9j{R5k9&C<>A&yWKah$Zg3|9wC9{TnTr*+bIu4^Lbdc;BY6W!3;~qE~$G H%7gy^{iUGx literal 4362 zcmeHKcTkhtw*S5)1f)jbAiYRAC{=n55s@YWho(R%0?MJ|14Mch6yYc!c<2@cQ9ybV zfh1Vy5a}gAf*?T%y+bfBzB_a8+`0F?_s@H8=FQwad(YnMxAt1=w|}$0nf)bMTbgjN z3bFzKz+q-;WCH*Y#u5UsKpE4=VyRBXbRyW)F@)hC`#C^E*}U6~9W~Y#b{84WaoF8| z^?xbw|3U#eoo+sVCggj5Q0EPeE*hJdnwej+xNK$pn~klV z{gtbZPG}cbw`(4rH@tuM@%8f$2n-6o`EMbiVYk9>M?^+N$Hc~A?%ch1Kk-3Q@*gRw zX^+!0o@8a``=vDEX(z5c(s+!um`i91)_sy+XT-%3_pFX#Dbar+3_Vp9K z3=9qpkBp8_OcJN2XGrAPuXFR?7AT9~mzJq3t842Un_JsEyLj%rF1EiCZkzXd7Wfm zUWMf1I^NTXh%oCqHHi|7GmSMq#>R?7<-BQRoGFF+;caaQmb6DXZi*&QHufj5q!8p9 zMZ5nXpMjZkGs^x$!<1$Z&s3RJi~_^YG~P~+a03t&zyg5c01N{l0LcHgVK-CNw!`MW zuDtvsbI5IJX}r}Eq>j%VO!QDScRCj8ILa;n7m5k*<6QRgru?T#hk^SairMIs zA|g@jR6wSIwB22-8F91fPtyEHXII+2EA_2@?r>$VL+ZhD$>i%#!Hn$aHLG!JZ*{Hm(9_Gh8en1aNoluGF(>=9<^7l#0WU_=BbzOK(nU? z>Ra~09d+9#gVvjnnI6}?kFrNO+>MC&2Qe*uXGUY=g^pYwX)g5T&Lo2t-xnk6*BYDJ zCinQa(R8^@JMW`$2Ud44Vt&VAT%L6N+t|i$ZPaf1laVO`+@K18K9MpHZro4ADyr@fcj!L0@ z{BHGtu^6f8-FGXmKCR{F`T6&+>c;h7ii3T*r2dmU#i4_VV!md5L8fJ|LD zQiyg2?R}i_*$2M7K=2WAas}dF6LK4MdOGM2-C$g%wZ5UMW>g?jBh2dundEi^U~}5P zblynM2K{G-A5SpVbCI8^rN?S!7_q&GvhC=pMlO-YVvZZ5R3E(|^hx~&8IQCzaVYB; z)x!{q9tT9Jn(YFS6O70c>RjYWHRjeJnJzxO1|$9MP%d+oY@dlzo|y{lOKXXzqovs5 z*T_mb)+eQ5(t?{GWss+%lIO4tV4p4c_4G+efZMnd$=WSn5&YU9O<}4or)UBYBr$EPd2GDR%o8REMsP6=sgD;y$hCPB_bw6m< zY#9wdCi(3|QEiC;_Qpn3bbSNZTfeU#Ydf|hR{h~?&^u@Knqp8mU`6xL?lSb<;9yYb z^~=!x*RM#6Ns$$FFFD-A6DN3Z!#xtUr=FdBiw!riQqTV4g+Zgk%m)^4vAVYq4Yb`6 zAF61`%IWZyb-#1P+2dCRG7^-_S-<>p0ILJVD+9(xXoIOS@UOt*j`&mdL&xOqXAU}N zr~_rmG2ba+oSXO>+fRu~WratxBW9xw6RJDR$t~Uz?54kQy1&7!LgjDO=3Y|~YqM#% zky2FU^cGCyuoFLp=g>zgeH%0!g*=jkkAb3PTP6t(A>`7fC+HUGUK^?aZ~N}>8EXr7n7 zd?Ov*LA2fIlhO#2+L^W(T(sj(HxH0;6!P2{BMuLg?mpOzYTIl@ckvyIB7B7VjxV5l zGDEMueGXv1IW@e57OtFjj;Sh}h%}~2&M1MYsiFGL{Uhh%IA}@B6TT_Qk+!#wt1Ev0 zWBNog&3b^1$7LvU@K2qNlK%dO^w$_w^vLMBhzQO+VE3kr^Jj8Y$iAohp@}BU`Sa%k zDttdhjwo16*)}@Jy?`b@f z*IAJ6#~Jpj%qJ8t{vizHdaN+jUtdwQ48`Uv9lPQU-ZQM1Ja=PXvK=;UcwK3ZltGFG zcV60C3ys2!(r{5XuZ%VO!lwn+|BQAZ;Al2@gO(nSwo>h1Y1qKd>A6~$jmv=n;}H?D z{I*vT=+}RpUo(;0Yupx6kL`QE*}}A+B_BuRIAGhnz{y%_17tD1rPl}1xB4-0KVtmc zo&re0VBXHV3W1b|oZ}Z8R_FL|XTgUND$AUNw3s3*ZPE8?dhZ>1O;$ zr7`fF<7x|Y<6+_I7iNg-Ja3QNJ>XW7K0G1eEo0Fp15ZGGc6Vr2cvRM-@PmH>3KxgTL97%HbkHO#Bd zFfe4?us(8oj?waE9$ z%rNUv;3Hj>!6@67!T<0&RL`^pI8zahUj_s zdW$9&)|q;WV2ME80TXC%Vq@o&;?5&+?kwV&0^0N%(a=9;LqXnO@S>G!IK7@+!F}Wz zvdmmfE6mygxHwfsZvit#IK;ig=ga%BT}n1k8971J7qOLaQ`T``>mD%U9y6jYfYMo% z-DfPxloka@KqZv8A_~P`!7Ut=F@>ze_d<2E`&YqvA=Kl%y?9aWY*`E%$`TPxnfjus ztEBE;;{oP7E<&Tvr;0q2z|bHp0xUV-PR?j?iL!}10Oz3+s`{VeJbdw9_aN`y?R)A4 zs=3$bfC<9$wOVr_x}Nj^yqEpYpr+LaMGv8!?JVMnfFaz_HS?HN*B zvL(Fh&ajyvvTG=kj~sjUTNEH+68W5mVoNh;IKG4V_FoHroG)b2fgw~eX?M~#+9MO# zB}@7+U4{@ALF&-6CweKSz`)OH5iz}lP45ygLmG}yL8z<-;|Ay|?lmpIUAUpB=-!ZC zXgQeiiV2~HP}!yU;Jti6ySZPLp)x(J;-TVR{&0&-9@IXnbDTX}UK$fp8X`7|>EecD zI~CwQ@hdw<0~2t=_8WMwSrF||KFvQSsO;zu*b`ILyBS=dok)a=22_TPL;S;EQYd5i zp%R581VfrCp!5Kp31(bmLiqhhAwFJI-JP^*0cA%EAW=`ihXI1#cJexCppVX8<>D>q z%|I@5Wm5$y-k29|P^;*3j~FLvSb$q7>W@_3DF}yzV^zBug>rRD0%ulNVfX0@?$#a4 z=NJ@6_Z|O~^X9OGrCm3`*Ch_JKqWl=Lh??7;w@+iJ z&3Cz6(r$>d2sleZWu7BXo52usZE4j{Czu(@=$NjG4N>-_-QdM8CHp|vO9r-nnNKsN zAP@F#?<j50V)Dek&3cl{b^(D!n{)B?5J z#2}jy7iURc*IquW-WVnXl^F|9F^8oQ9NSD3D~9J6T$wEtwc8!r4xaL1j|oHa4C>3* z@$==nQjpFN57VO_0l1+tPU77Trt6!C*=bC)H+&_+>OMou0Co4xJFPt6JW?a@ORv4p zIzyZWVdbXCU4-WxtAI!|{o^bnTT~Q&evVl>2}l+Y)%Vy^N+3M>Avv}HW~d*1tE6sh zm=S9W7~60%l90I0`c*_E2?GH@j5Q-c^ECR&!M7I@FZje_L zr4r$*uGlu*l>~JE&pnhe08AVJSx^83@n0?h@c-&%{ ({ src: './favicon.ico', dest: '.', }, + { + src: './favicon.svg', + dest: '.', + }, { src: './src/instance', dest: '.', From 50233495453f2b7035802a2f56d511d02cb49836 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 6 Mar 2026 22:08:10 +0100 Subject: [PATCH 79/89] nicolium: update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/favicon.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nicolium/favicon.svg b/packages/nicolium/favicon.svg index d42be4866..f086c3800 100644 --- a/packages/nicolium/favicon.svg +++ b/packages/nicolium/favicon.svg @@ -1,4 +1,4 @@ - +

+
+

+ +

- return ( -
-
-

- {intl.formatMessage(messages.title)} -

- -

{intl.formatMessage(messages.body)}

-
- - +

+ +

- ); -}; + + +
+); export { Blankslate as default }; diff --git a/packages/nicolium/src/features/chats/components/chats-page/components/blankslate-empty.tsx b/packages/nicolium/src/features/chats/components/chats-page/components/blankslate-empty.tsx index 5ab5f6b51..91a1cf91e 100644 --- a/packages/nicolium/src/features/chats/components/chats-page/components/blankslate-empty.tsx +++ b/packages/nicolium/src/features/chats/components/chats-page/components/blankslate-empty.tsx @@ -1,4 +1,3 @@ -import { useNavigate } from '@tanstack/react-router'; import React from 'react'; import { FormattedMessage } from 'react-intl'; @@ -7,33 +6,25 @@ import Stack from '@/components/ui/stack'; import Text from '@/components/ui/text'; /** To display on the chats main page when no message is selected. */ -const BlankslateEmpty: React.FC = () => { - const navigate = useNavigate(); +const BlankslateEmpty: React.FC = () => ( + + + + + - const handleNewChat = () => { - navigate({ to: '/chats/new' }); - }; - - return ( - - - - - - - - - - - - + + + - ); -}; + + + +); export { BlankslateEmpty as default }; diff --git a/packages/nicolium/src/features/chats/components/chats-page/components/chats-page-empty.tsx b/packages/nicolium/src/features/chats/components/chats-page/components/chats-page-empty.tsx index c743cba80..482b79fee 100644 --- a/packages/nicolium/src/features/chats/components/chats-page/components/chats-page-empty.tsx +++ b/packages/nicolium/src/features/chats/components/chats-page/components/chats-page-empty.tsx @@ -1,11 +1,13 @@ import React from 'react'; import { useChats } from '@/queries/chats'; +import { useShoutboxIsLoading } from '@/stores/shoutbox'; import BlankslateEmpty from './blankslate-empty'; import BlankslateWithChats from './blankslate-with-chats'; const ChatsPageEmpty = () => { + const showShoutbox = !useShoutboxIsLoading(); const { chatsQuery: { data: chats, isLoading }, } = useChats(); @@ -14,7 +16,7 @@ const ChatsPageEmpty = () => { return null; } - if (chats && chats.length > 0) { + if ((chats && chats.length > 0) || showShoutbox) { return ; } From a6dff620347e5e49c8bf71ff10d52bed3196b6ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Sat, 7 Mar 2026 00:40:36 +0100 Subject: [PATCH 81/89] nicolium: cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/package.json | 2 +- .../chats/components/chat-message-list.tsx | 34 +++++++------ .../components/chat-search/blankslate.tsx | 34 ++++++------- .../chat-search/empty-results-blankslate.tsx | 40 +++++++-------- .../components/polls/duration-selector.tsx | 32 ++++++------ .../compose/components/polls/poll-form.tsx | 31 +++++------- packages/nicolium/src/locales/en.json | 2 - .../report-modal/steps/confirmation-step.tsx | 20 ++------ .../report-modal/steps/other-actions-step.tsx | 49 +++++++++---------- .../modals/report-modal/steps/reason-step.tsx | 5 +- .../src/pages/settings/delete-account.tsx | 15 +++--- .../src/pages/settings/edit-email.tsx | 22 +++++---- .../src/pages/settings/edit-password.tsx | 47 ++++++++++-------- .../src/pages/settings/edit-profile.tsx | 3 +- .../nicolium/src/pages/settings/filters.tsx | 6 +-- .../nicolium/src/styles/new/settings.scss | 2 +- .../nicolium/src/styles/new/statuses.scss | 2 +- 17 files changed, 160 insertions(+), 186 deletions(-) diff --git a/packages/nicolium/package.json b/packages/nicolium/package.json index 4081a6a08..f2339cfea 100644 --- a/packages/nicolium/package.json +++ b/packages/nicolium/package.json @@ -81,6 +81,7 @@ "cryptocurrency-icons": "^0.18.1", "cssnano": "^7.1.2", "detect-passive-events": "^2.0.3", + "dompurify": "^3.3.2", "emoji-datasource": "15.0.1", "emoji-mart": "^5.6.0", "exifr": "^7.1.3", @@ -91,7 +92,6 @@ "html-react-parser": "^5.2.17", "intl-messageformat": "^11.1.2", "intl-pluralrules": "^2.0.1", - "dompurify": "^3.3.2", "leaflet": "^1.9.4", "lexical": "^0.41.0", "line-awesome": "^1.3.0", diff --git a/packages/nicolium/src/features/chats/components/chat-message-list.tsx b/packages/nicolium/src/features/chats/components/chat-message-list.tsx index 0e7947af5..519bb9d0c 100644 --- a/packages/nicolium/src/features/chats/components/chat-message-list.tsx +++ b/packages/nicolium/src/features/chats/components/chat-message-list.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'; -import { useIntl, defineMessages } from 'react-intl'; +import { useIntl, defineMessages, FormattedMessage } from 'react-intl'; import { type Components, Virtuoso, type VirtuosoHandle } from 'react-virtuoso'; import Avatar from '@/components/ui/avatar'; @@ -22,16 +22,6 @@ import type { Chat } from 'pl-api'; const messages = defineMessages({ today: { id: 'chats.dividers.today', defaultMessage: 'Today' }, - blockedBy: { id: 'chat_message_list.blocked_by', defaultMessage: 'You are blocked by' }, - networkFailureTitle: { id: 'chat_message_list.network_failure.title', defaultMessage: 'Whoops!' }, - networkFailureSubtitle: { - id: 'chat_message_list.network_failure.subtitle', - defaultMessage: 'We encountered a network failure.', - }, - networkFailureAction: { - id: 'chat_message_list.network_failure.action', - defaultMessage: 'Try again', - }, }); type TimeFormat = 'today' | 'date'; @@ -219,7 +209,12 @@ const ChatMessageList: React.FC = React.memo(({ chat }) => { /> <> - {intl.formatMessage(messages.blockedBy)}{' '} + + + {' '} @{chat.account.acct} @@ -236,16 +231,25 @@ const ChatMessageList: React.FC = React.memo(({ chat }) => { - {intl.formatMessage(messages.networkFailureTitle)} + - {intl.formatMessage(messages.networkFailureSubtitle)} +
diff --git a/packages/nicolium/src/features/chats/components/chat-search/blankslate.tsx b/packages/nicolium/src/features/chats/components/chat-search/blankslate.tsx index c715ec8a6..8b2f61087 100644 --- a/packages/nicolium/src/features/chats/components/chat-search/blankslate.tsx +++ b/packages/nicolium/src/features/chats/components/chat-search/blankslate.tsx @@ -1,27 +1,21 @@ import React from 'react'; -import { defineMessages, useIntl } from 'react-intl'; +import { FormattedMessage } from 'react-intl'; import Stack from '@/components/ui/stack'; import Text from '@/components/ui/text'; -const messages = defineMessages({ - title: { id: 'chat_search.blankslate.title', defaultMessage: 'Start a chat' }, - body: { id: 'chat_search.blankslate.body', defaultMessage: 'Search for someone to chat with.' }, -}); - -const Blankslate = () => { - const intl = useIntl(); - - return ( - - - {intl.formatMessage(messages.title)} - - - {intl.formatMessage(messages.body)} - - - ); -}; +const Blankslate = () => ( + + + + + + + + +); export { Blankslate as default }; diff --git a/packages/nicolium/src/features/chats/components/chat-search/empty-results-blankslate.tsx b/packages/nicolium/src/features/chats/components/chat-search/empty-results-blankslate.tsx index 2dad5fe91..95fcbcf6e 100644 --- a/packages/nicolium/src/features/chats/components/chat-search/empty-results-blankslate.tsx +++ b/packages/nicolium/src/features/chats/components/chat-search/empty-results-blankslate.tsx @@ -1,31 +1,25 @@ import React from 'react'; -import { defineMessages, useIntl } from 'react-intl'; +import { FormattedMessage } from 'react-intl'; import Stack from '@/components/ui/stack'; import Text from '@/components/ui/text'; -const messages = defineMessages({ - title: { id: 'chat_search.empty_results_blankslate.title', defaultMessage: 'No matches found' }, - body: { - id: 'chat_search.empty_results_blankslate.body', - defaultMessage: 'Try searching for another name.', - }, -}); +const EmptyResultsBlankslate = () => ( + + + + -const EmptyResultsBlankslate = () => { - const intl = useIntl(); - - return ( - - - {intl.formatMessage(messages.title)} - - - - {intl.formatMessage(messages.body)} - - - ); -}; + + + + +); export { EmptyResultsBlankslate as default }; diff --git a/packages/nicolium/src/features/compose/components/polls/duration-selector.tsx b/packages/nicolium/src/features/compose/components/polls/duration-selector.tsx index 64c9cf8b9..81dbc9574 100644 --- a/packages/nicolium/src/features/compose/components/polls/duration-selector.tsx +++ b/packages/nicolium/src/features/compose/components/polls/duration-selector.tsx @@ -1,21 +1,9 @@ import React, { useEffect, useState } from 'react'; -import { defineMessages, useIntl } from 'react-intl'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import Select from '@/components/ui/select'; const messages = defineMessages({ - days: { - id: 'intervals.full.days', - defaultMessage: '{number, plural, one {# day} other {# days}}', - }, - hours: { - id: 'intervals.full.hours', - defaultMessage: '{number, plural, one {# hour} other {# hours}}', - }, - minutes: { - id: 'intervals.full.minutes', - defaultMessage: '{number, plural, one {# minute} other {# minutes}}', - }, daysTitle: { id: 'compose_form.poll.duration.days', defaultMessage: 'Days' }, hoursTitle: { id: 'compose_form.poll.duration.hours', defaultMessage: 'Hours' }, minutesTitle: { id: 'compose_form.poll.duration.minutes', defaultMessage: 'Minutes' }, @@ -59,7 +47,11 @@ const DurationSelector = ({ onDurationChange, value }: IDurationSelector) => { > {[...Array(8).fill(undefined)].map((_, number) => ( ))} @@ -77,7 +69,11 @@ const DurationSelector = ({ onDurationChange, value }: IDurationSelector) => { > {[...Array(24).fill(undefined)].map((_, number) => ( ))} @@ -95,7 +91,11 @@ const DurationSelector = ({ onDurationChange, value }: IDurationSelector) => { > {[0, 15, 30, 45].map((number) => ( ))} diff --git a/packages/nicolium/src/features/compose/components/polls/poll-form.tsx b/packages/nicolium/src/features/compose/components/polls/poll-form.tsx index 6adb12fea..7371cfcd3 100644 --- a/packages/nicolium/src/features/compose/components/polls/poll-form.tsx +++ b/packages/nicolium/src/features/compose/components/polls/poll-form.tsx @@ -21,21 +21,6 @@ const messages = defineMessages({ id: 'compose_form.poll.option_placeholder', defaultMessage: 'Answer #{number}', }, - pollDuration: { id: 'compose_form.poll.duration', defaultMessage: 'Poll duration' }, - removePoll: { id: 'compose_form.poll.remove', defaultMessage: 'Remove poll' }, - switchToMultiple: { - id: 'compose_form.poll.switch_to_multiple', - defaultMessage: 'Change poll to allow multiple answers', - }, - switchToSingle: { - id: 'compose_form.poll.switch_to_single', - defaultMessage: 'Change poll to allow for a single answer', - }, - multiSelect: { id: 'compose_form.poll.multiselect', defaultMessage: 'Multi-select' }, - multiSelectDetail: { - id: 'compose_form.poll.multiselect_detail', - defaultMessage: 'Allow users to select multiple answers', - }, }); interface IOption { @@ -137,7 +122,6 @@ interface IPollForm { const PollForm: React.FC = ({ composeId }) => { const { updateCompose } = useComposeActions(); - const intl = useIntl(); const { configuration } = useInstance(); const { poll, language, modifiedLanguage } = useCompose(composeId); @@ -224,10 +208,15 @@ const PollForm: React.FC = ({ composeId }) => {
diff --git a/packages/nicolium/src/locales/en.json b/packages/nicolium/src/locales/en.json index 9e5f29d3b..8f010d3fd 100644 --- a/packages/nicolium/src/locales/en.json +++ b/packages/nicolium/src/locales/en.json @@ -671,8 +671,6 @@ "compose_form.poll.option_placeholder": "Answer #{number}", "compose_form.poll.remove": "Remove poll", "compose_form.poll.remove_option": "Remove this answer", - "compose_form.poll.switch_to_multiple": "Change poll to allow multiple answers", - "compose_form.poll.switch_to_single": "Change poll to allow for a single answer", "compose_form.poll_placeholder": "Add a poll topic…", "compose_form.preview": "Preview post", "compose_form.preview.close": "Hide preview", diff --git a/packages/nicolium/src/modals/report-modal/steps/confirmation-step.tsx b/packages/nicolium/src/modals/report-modal/steps/confirmation-step.tsx index 8d764ba46..3dc43463b 100644 --- a/packages/nicolium/src/modals/report-modal/steps/confirmation-step.tsx +++ b/packages/nicolium/src/modals/report-modal/steps/confirmation-step.tsx @@ -1,20 +1,10 @@ import React from 'react'; -import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; +import { FormattedMessage } from 'react-intl'; import Stack from '@/components/ui/stack'; import Text from '@/components/ui/text'; import { useAppSelector } from '@/hooks/use-app-selector'; -const messages = defineMessages({ - accountEntity: { id: 'report.confirmation.entity.account', defaultMessage: 'account' }, - title: { id: 'report.confirmation.title', defaultMessage: 'Thanks for submitting your report.' }, - content: { - id: 'report.confirmation.content', - defaultMessage: - 'If we find that this {entity} is violating the {link} we will take further action on the matter.', - }, -}); - const termsOfServiceText = ; const renderTermsOfServiceLink = (href: string) => ( @@ -28,11 +18,8 @@ const renderTermsOfServiceLink = (href: string) => ( ); const ConfirmationStep: React.FC = () => { - const intl = useIntl(); const links = useAppSelector((state) => state.frontendConfig.links); - const entity = intl.formatMessage(messages.accountEntity); - return ( @@ -40,7 +27,6 @@ const ConfirmationStep: React.FC = () => { id='report.confirmation.title' defaultMessage='Thanks for submitting your report.' /> - {intl.formatMessage(messages.title)} @@ -48,7 +34,9 @@ const ConfirmationStep: React.FC = () => { id='report.confirmation.content' defaultMessage='If we find that this {entity} is violating the {link} we will take further action on the matter.' values={{ - entity, + entity: ( + + ), link: links?.termsOfService ? renderTermsOfServiceLink(links.termsOfService) : termsOfServiceText, diff --git a/packages/nicolium/src/modals/report-modal/steps/other-actions-step.tsx b/packages/nicolium/src/modals/report-modal/steps/other-actions-step.tsx index a8ae3b6b7..9118a3343 100644 --- a/packages/nicolium/src/modals/report-modal/steps/other-actions-step.tsx +++ b/packages/nicolium/src/modals/report-modal/steps/other-actions-step.tsx @@ -1,5 +1,5 @@ import React, { useMemo, useState } from 'react'; -import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; +import { FormattedMessage } from 'react-intl'; import Button from '@/components/ui/button'; import FormGroup from '@/components/ui/form-group'; @@ -14,26 +14,6 @@ import { getDomain } from '@/utils/accounts'; import type { Account } from 'pl-api'; -const messages = defineMessages({ - addAdditionalStatuses: { - id: 'report.other_actions.add_additional', - defaultMessage: 'Would you like to add additional statuses to this report?', - }, - addMore: { id: 'report.other_actions.add_more', defaultMessage: 'Add more' }, - furtherActions: { - id: 'report.other_actions.further_actions', - defaultMessage: 'Further actions:', - }, - hideAdditionalStatuses: { - id: 'report.other_actions.hide_additional', - defaultMessage: 'Hide additional statuses', - }, - otherStatuses: { - id: 'report.other_actions.other_statuses', - defaultMessage: 'Include other statuses?', - }, -}); - interface IOtherActionsStep { account: Pick; selectedStatusIds: string[]; @@ -56,7 +36,6 @@ const OtherActionsStep = ({ isSubmitting, }: IOtherActionsStep) => { const features = useFeatures(); - const intl = useIntl(); const { entries } = useAccountTimeline(account.id, { exclude_replies: false }); @@ -100,10 +79,20 @@ const OtherActionsStep = ({ - {intl.formatMessage(messages.otherStatuses)} + - + + } + > {showAdditionalStatuses ? (
@@ -126,7 +115,10 @@ const OtherActionsStep = ({ setShowAdditionalStatuses(false); }} > - {intl.formatMessage(messages.hideAdditionalStatuses)} +
@@ -139,7 +131,7 @@ const OtherActionsStep = ({ setShowAdditionalStatuses(true); }} > - {intl.formatMessage(messages.addMore)} + )}
@@ -147,7 +139,10 @@ const OtherActionsStep = ({ - {intl.formatMessage(messages.furtherActions)} + = ({ comment, setComment, ruleIds, setRu {shouldRequireRule && ( - {intl.formatMessage(messages.reasonForReporting)} +
diff --git a/packages/nicolium/src/pages/settings/delete-account.tsx b/packages/nicolium/src/pages/settings/delete-account.tsx index b9cc213ed..2a13e226f 100644 --- a/packages/nicolium/src/pages/settings/delete-account.tsx +++ b/packages/nicolium/src/pages/settings/delete-account.tsx @@ -15,9 +15,6 @@ import { useFeatures } from '@/hooks/use-features'; import toast from '@/toast'; const messages = defineMessages({ - passwordFieldLabel: { id: 'security.fields.password.label', defaultMessage: 'Password' }, - deleteHeader: { id: 'column.delete_account', defaultMessage: 'Delete account' }, - deleteSubmit: { id: 'security.submit.delete', defaultMessage: 'Delete account' }, deleteAccountSuccess: { id: 'security.delete_account.success', defaultMessage: 'Account successfully deleted.', @@ -61,7 +58,9 @@ const DeleteAccountPage = () => { return ( - + } + /> @@ -94,7 +93,11 @@ const DeleteAccountPage = () => {
{!features.deleteAccountWithoutPassword && ( - + + } + > { diff --git a/packages/nicolium/src/pages/settings/edit-email.tsx b/packages/nicolium/src/pages/settings/edit-email.tsx index 81623c55d..cd1e42fb2 100644 --- a/packages/nicolium/src/pages/settings/edit-email.tsx +++ b/packages/nicolium/src/pages/settings/edit-email.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { defineMessages, useIntl } from 'react-intl'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { changeEmail } from '@/actions/security'; import Button from '@/components/ui/button'; @@ -18,11 +18,7 @@ const messages = defineMessages({ defaultMessage: 'Email successfully updated.', }, updateEmailFail: { id: 'security.update_email.fail', defaultMessage: 'Update email failed.' }, - emailFieldLabel: { id: 'security.fields.email.label', defaultMessage: 'Email address' }, emailFieldPlaceholder: { id: 'edit_email.placeholder', defaultMessage: 'me@example.com' }, - passwordFieldLabel: { id: 'security.fields.password.label', defaultMessage: 'Password' }, - submit: { id: 'security.submit', defaultMessage: 'Save changes' }, - cancel: { id: 'common.cancel', defaultMessage: 'Cancel' }, }); const initialState = { email: '', password: '' }; @@ -64,7 +60,11 @@ const EditEmailPage = () => { return (
- + + } + > { /> - + + } + > diff --git a/packages/nicolium/src/pages/settings/edit-password.tsx b/packages/nicolium/src/pages/settings/edit-password.tsx index 5633c3e62..3d66ab94c 100644 --- a/packages/nicolium/src/pages/settings/edit-password.tsx +++ b/packages/nicolium/src/pages/settings/edit-password.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { defineMessages, useIntl } from 'react-intl'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { changePassword } from '@/actions/security'; import Button from '@/components/ui/button'; @@ -24,21 +24,7 @@ const messages = defineMessages({ id: 'security.update_password.password_confirmation_no_match', defaultMessage: 'Passwords do not match.', }, - oldPasswordFieldLabel: { - id: 'security.fields.old_password.label', - defaultMessage: 'Current password', - }, - newPasswordFieldLabel: { - id: 'security.fields.new_password.label', - defaultMessage: 'New password', - }, - confirmationFieldLabel: { - id: 'security.fields.password_confirmation.label', - defaultMessage: 'New password (again)', - }, header: { id: 'edit_password.header', defaultMessage: 'Change password' }, - submit: { id: 'security.submit', defaultMessage: 'Save changes' }, - cancel: { id: 'common.cancel', defaultMessage: 'Cancel' }, }); const initialState = { currentPassword: '', newPassword: '', newPasswordConfirmation: '' }; @@ -89,7 +75,14 @@ const EditPasswordPage = () => { return (
- + + } + > { /> - + + } + > { /> - + + } + > { diff --git a/packages/nicolium/src/pages/settings/edit-profile.tsx b/packages/nicolium/src/pages/settings/edit-profile.tsx index 67d778394..fc87f79f1 100644 --- a/packages/nicolium/src/pages/settings/edit-profile.tsx +++ b/packages/nicolium/src/pages/settings/edit-profile.tsx @@ -74,7 +74,6 @@ const messages = defineMessages({ id: 'edit_profile.fields.location_placeholder', defaultMessage: 'Location', }, - cancel: { id: 'common.cancel', defaultMessage: 'Cancel' }, mentionPolicyNone: { id: 'edit_profile.fields.mention_policy.none', defaultMessage: 'Everybody' }, mentionPolicyOnlyKnown: { id: 'edit_profile.fields.mention_policy.only_known', @@ -825,7 +824,7 @@ const EditProfilePage: React.FC = () => { diff --git a/packages/nicolium/src/styles/new/settings.scss b/packages/nicolium/src/styles/new/settings.scss index cc238c542..40e1e4376 100644 --- a/packages/nicolium/src/styles/new/settings.scss +++ b/packages/nicolium/src/styles/new/settings.scss @@ -43,4 +43,4 @@ &--current button { @apply border-transparent bg-danger-100 dark:bg-danger-900 text-danger-600 dark:text-danger-200 hover:bg-danger-600 hover:text-gray-100 dark:hover:text-gray-100 dark:hover:bg-danger-500 focus:ring-danger-500; } -} \ No newline at end of file +} diff --git a/packages/nicolium/src/styles/new/statuses.scss b/packages/nicolium/src/styles/new/statuses.scss index d64c3294c..6c4f662bb 100644 --- a/packages/nicolium/src/styles/new/statuses.scss +++ b/packages/nicolium/src/styles/new/statuses.scss @@ -24,7 +24,7 @@ .⁂-detailed-status { .⁂-status-reactions-bar { padding-top: 0.5rem; - } + } } .⁂-status-reactions-bar { From 405e99ed086cf8d374a94c08713b642e6af2e55f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Sat, 7 Mar 2026 09:55:26 +0100 Subject: [PATCH 82/89] nicolium: attempt at fixing duplicated statuses in timeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/stores/timelines.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/nicolium/src/stores/timelines.ts b/packages/nicolium/src/stores/timelines.ts index 4a26fe9c0..f2e46cf3f 100644 --- a/packages/nicolium/src/stores/timelines.ts +++ b/packages/nicolium/src/stores/timelines.ts @@ -182,7 +182,10 @@ const useTimelinesStore = create()( const timeline = state.timelines[timelineId]; if (!timeline) return; - if (timeline.entries.some((entry) => entry.type === 'status' && entry.id === status.id)) + if ( + timeline.entries.some((entry) => entry.type === 'status' && entry.id === status.id) || + timeline.queuedEntries.some((s) => s.id === status.id) + ) return; timeline.queuedEntries.unshift(status); @@ -303,6 +306,13 @@ const useTimelinesStore = create()( (e) => e.type === 'pending-status' && e.id === idempotencyKey, ); if (idx !== -1) { + if ( + timeline.entries.some((entry) => entry.type === 'status' && entry.id === newId) || + timeline.queuedEntries.some((s) => s.id === newId) + ) { + timeline.entries.splice(idx, 1); + return; + } timeline.entries[idx] = { type: 'status', id: newId, From f41474f50be4260982977f182f28190910712512 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Sat, 7 Mar 2026 10:13:01 +0100 Subject: [PATCH 83/89] nicolium: for now, store settings in both note and indexeddb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/actions/settings.ts | 28 ++++++++++--------- .../src/features/preferences/index.tsx | 24 ++++++++++++++++ .../nicolium/src/schemas/frontend-settings.ts | 1 + 3 files changed, 40 insertions(+), 13 deletions(-) diff --git a/packages/nicolium/src/actions/settings.ts b/packages/nicolium/src/actions/settings.ts index a835becb8..cd99b66c2 100644 --- a/packages/nicolium/src/actions/settings.ts +++ b/packages/nicolium/src/actions/settings.ts @@ -85,20 +85,22 @@ const updateSettingsStore = }, }), ); - } else if (client.features.notes) { - // Inspired by Phanpy and designed for compatibility with other software doing this - // https://github.com/cheeaun/phanpy/commit/a8b5c8cd64d456d30aab09dc56da7e4e20100e67 - const note = (await client.accounts.getRelationships([state.me as string]))[0]?.note; - const settingsNote = `${encodeURIComponent(JSON.stringify(settings))}`; - - if (/(.*)<\/nicolium-config>/.test(note || '')) { - const newNote = note!.replace(/(.*)<\/nicolium-config>/, settingsNote); - return client.accounts.updateAccountNote(state.me as string, newNote); - } else { - const newNote = `${note || ''}\n\n${settingsNote}`; - return client.accounts.updateAccountNote(state.me as string, newNote); - } } else { + if (client.features.notes) { + // Inspired by Phanpy and designed for compatibility with other software doing this + // https://github.com/cheeaun/phanpy/commit/a8b5c8cd64d456d30aab09dc56da7e4e20100e67 + const note = (await client.accounts.getRelationships([state.me as string]))[0]?.note; + const settingsNote = `${encodeURIComponent(JSON.stringify(settings))}`; + + let newNote; + if (/(.*)<\/nicolium-config>/.test(note || '')) { + newNote = note!.replace(/(.*)<\/nicolium-config>/, settingsNote); + } else { + newNote = `${note || ''}\n\n${settingsNote}`; + } + client.accounts.updateAccountNote(state.me as string, newNote); + } + const accountUrl = selectOwnAccount(state)!.url; return updateAuthAccount(accountUrl, settings); diff --git a/packages/nicolium/src/features/preferences/index.tsx b/packages/nicolium/src/features/preferences/index.tsx index 5244aab20..0d504ff25 100644 --- a/packages/nicolium/src/features/preferences/index.tsx +++ b/packages/nicolium/src/features/preferences/index.tsx @@ -298,6 +298,30 @@ const Preferences = () => { onChange={onToggleChange} /> + + + + {/* + } + hint={ + + } + > + + */} Date: Sat, 7 Mar 2026 10:30:33 +0100 Subject: [PATCH 84/89] nicolium: make settings synchronization in notes opt-in MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/actions/me.ts | 9 ++- packages/nicolium/src/actions/settings.ts | 58 +++++++++++-------- .../src/features/preferences/index.tsx | 50 ++++++++-------- packages/nicolium/src/locales/en.json | 2 + 4 files changed, 68 insertions(+), 51 deletions(-) diff --git a/packages/nicolium/src/actions/me.ts b/packages/nicolium/src/actions/me.ts index 221b10b44..d63210fad 100644 --- a/packages/nicolium/src/actions/me.ts +++ b/packages/nicolium/src/actions/me.ts @@ -99,9 +99,11 @@ const fetchMeSuccess = const settings = account.settings_store?.[FE_NAME] || account.settings_store?.[LEGACY_FE_NAME]; - if (client.features.frontendConfigurations) { + if (settings) { useSettingsStore.getState().actions.loadUserSettings(settings); - } else if (client.features.notes) { + } + + if (!client.features.frontendConfigurations && client.features.notes) { const note = await getClient(getState) .accounts.getRelationships([account.id]) .then((relationships) => relationships[0]?.note); @@ -112,6 +114,9 @@ const fetchMeSuccess = if (match) { try { const frontendConfig = JSON.parse(decodeURIComponent(match[1])); + if (typeof frontendConfig === 'object' && frontendConfig !== null) { + frontendConfig.storeSettingsInNotes = true; + } useSettingsStore.getState().actions.loadUserSettings(frontendConfig); return frontendConfig; } catch (error) { diff --git a/packages/nicolium/src/actions/settings.ts b/packages/nicolium/src/actions/settings.ts index cd99b66c2..fce73e8e2 100644 --- a/packages/nicolium/src/actions/settings.ts +++ b/packages/nicolium/src/actions/settings.ts @@ -10,6 +10,7 @@ import { useSettingsStore } from '@/stores/settings'; import toast from '@/toast'; import { isLoggedIn } from '@/utils/auth'; +import type { Settings } from '@/schemas/frontend-settings'; import type { AppDispatch, RootState } from '@/store'; const LEGACY_FE_NAME = NODE_ENV === 'production' ? 'pl_fe' : 'pl_fe_dev'; @@ -30,33 +31,35 @@ const saveSuccessMessage = defineMessage({ const changeSetting = (path: string[], value: any, opts?: SettingOpts) => { useSettingsStore.getState().actions.changeSetting(path, value); - if (opts?.save !== false) return saveSettings(opts); + if (opts?.save !== false) return saveSettings(opts, path[0] === 'storeSettingsInNotes'); return () => {}; }; -const saveSettings = (opts?: SettingOpts) => (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; +const saveSettings = + (opts?: SettingOpts, isNotesChange?: boolean) => + (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return; - const { - userSettings, - actions: { userSettingsSaving }, - } = useSettingsStore.getState(); - if (userSettings.saved) return; + const { + userSettings, + actions: { userSettingsSaving }, + } = useSettingsStore.getState(); + if (userSettings.saved) return; - const { saved, ...data } = userSettings; + const { saved, ...data } = userSettings; - dispatch(updateSettingsStore(data)) - .then(() => { - userSettingsSaving(); + dispatch(updateSettingsStore(data, isNotesChange)) + .then(() => { + userSettingsSaving(); - if (opts?.showAlert) { - toast.success(saveSuccessMessage); - } - }) - .catch((error) => { - toast.showAlertForError(error); - }); -}; + if (opts?.showAlert) { + toast.success(saveSuccessMessage); + } + }) + .catch((error) => { + toast.showAlertForError(error); + }); + }; /** Update settings store for Mastodon, etc. */ const updateAuthAccount = async (url: string, settings: any) => { @@ -73,7 +76,8 @@ const updateAuthAccount = async (url: string, settings: any) => { }; const updateSettingsStore = - (settings: any) => async (dispatch: AppDispatch, getState: () => RootState) => { + (settings: Partial, isNotesChange?: boolean) => + async (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); const client = getClient(state); @@ -86,17 +90,21 @@ const updateSettingsStore = }), ); } else { - if (client.features.notes) { + if (client.features.notes && (settings.storeSettingsInNotes || isNotesChange)) { // Inspired by Phanpy and designed for compatibility with other software doing this // https://github.com/cheeaun/phanpy/commit/a8b5c8cd64d456d30aab09dc56da7e4e20100e67 const note = (await client.accounts.getRelationships([state.me as string]))[0]?.note; const settingsNote = `${encodeURIComponent(JSON.stringify(settings))}`; let newNote; - if (/(.*)<\/nicolium-config>/.test(note || '')) { - newNote = note!.replace(/(.*)<\/nicolium-config>/, settingsNote); + if (settings.storeSettingsInNotes) { + if (/(.*)<\/nicolium-config>/.test(note || '')) { + newNote = note!.replace(/(.*)<\/nicolium-config>/, settingsNote); + } else { + newNote = `${note || ''}\n\n${settingsNote}`; + } } else { - newNote = `${note || ''}\n\n${settingsNote}`; + newNote = note ? note.replace(/(.*)<\/nicolium-config>/, '') : ''; } client.accounts.updateAccountNote(state.me as string, newNote); } diff --git a/packages/nicolium/src/features/preferences/index.tsx b/packages/nicolium/src/features/preferences/index.tsx index 0d504ff25..299a3a104 100644 --- a/packages/nicolium/src/features/preferences/index.tsx +++ b/packages/nicolium/src/features/preferences/index.tsx @@ -282,6 +282,32 @@ const Preferences = () => { return (
+ {!features.frontendConfigurations && features.notes && ( + + + } + hint={ + + } + > + + + + )} + { onChange={onToggleChange} /> - - - - {/* - } - hint={ - - } - > - - */} Date: Sat, 7 Mar 2026 10:56:53 +0100 Subject: [PATCH 85/89] nicolium: prefer FormattedMessage for i18n MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../src/components/ui/multiselect.tsx | 7 +- .../compose/components/language-dropdown.tsx | 9 +- .../compose/editor/nodes/image-component.tsx | 7 +- .../list-editor-modal/components/search.tsx | 6 +- .../src/modals/report-modal/index.tsx | 34 ++--- .../src/pages/groups/manage-group.tsx | 61 +++++--- .../nicolium/src/pages/settings/aliases.tsx | 5 +- .../src/pages/settings/auth-token-list.tsx | 21 +-- .../nicolium/src/pages/settings/settings.tsx | 143 +++++++++++------- 9 files changed, 178 insertions(+), 115 deletions(-) diff --git a/packages/nicolium/src/components/ui/multiselect.tsx b/packages/nicolium/src/components/ui/multiselect.tsx index 29f37652d..1ad963c91 100644 --- a/packages/nicolium/src/components/ui/multiselect.tsx +++ b/packages/nicolium/src/components/ui/multiselect.tsx @@ -25,13 +25,12 @@ THE SOFTWARE. import clsx from 'clsx'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { defineMessages, useIntl } from 'react-intl'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import Icon from './icon'; const messages = defineMessages({ placeholder: { id: 'select.placeholder', defaultMessage: 'Select' }, - noOptions: { id: 'select.no_options', defaultMessage: 'No options available' }, removeItem: { id: 'select.remove_item', defaultMessage: 'Remove item' }, }); @@ -261,7 +260,9 @@ const Multiselect: React.FC = ({ >
    {visibleOptions.length === 0 && ( - {intl.formatMessage(messages.noOptions)} + + + )} {visibleOptions.map((option, i) => (