Merge branch 'develop' of https://codeberg.org/mkljczk/pl-fe into develop
Some checks failed
pl-api CI / Test for pl-api formatting (22.x) (push) Has been cancelled
pl-fe CI / Test and upload artifacts (22.x) (push) Has been cancelled
pl-hooks CI / Test for a successful build (22.x) (push) Has been cancelled
pl-fe CI / deploy (push) Has been cancelled

This commit is contained in:
2026-01-22 23:22:01 +00:00
20 changed files with 293 additions and 96 deletions

View File

@@ -40,10 +40,12 @@ const Blurhash: React.FC<IBlurhash> = React.memo(({
try {
const pixels = decode(hash, width, height);
const ctx = canvas.getContext('2d');
const imageData = new ImageData(new Uint8ClampedArray(pixels), width, height);
const imageData = ctx?.createImageData(width, height);
imageData?.data.set(pixels);
if (!ctx) return;
ctx.putImageData(imageData, 0, 0);
if (imageData) {
ctx?.putImageData(imageData, 0, 0);
}
} catch (err) {
console.error('Blurhash decoding failure', { err, hash });
}

View File

@@ -40,8 +40,15 @@ interface SizeData {
itemsDimensions: Dimensions[];
}
const getAspectRatio = (attachment: MediaAttachment) =>
(attachment.type === 'gifv' || attachment.type === 'image' || attachment.type === 'video') && attachment.meta.original?.aspect || null;
const getAspectRatio = (attachment: MediaAttachment) => {
if ((attachment.type === 'gifv' || attachment.type === 'image' || attachment.type === 'video') && attachment.meta.original) {
if (attachment.meta.original.aspect) {
return attachment.meta.original.aspect;
}
return attachment.meta.original.width / attachment.meta.original.height;
}
return null;
};
const withinLimits = (aspectRatio: number) =>
aspectRatio >= minimumAspectRatio && aspectRatio <= maximumAspectRatio;

View File

@@ -105,7 +105,7 @@ const SensitiveContentOverlay = React.forwardRef<HTMLDivElement, ISensitiveConte
<HStack alignItems='center' justifyContent='center' space={2}>
<Button
type='button'
theme='outline'
theme='outlined'
size='sm'
icon={require('@phosphor-icons/core/regular/eye.svg')}
onClick={toggleVisibility}

View File

@@ -10,7 +10,7 @@ const themes = {
accent: 'border-transparent bg-secondary-500 hover:bg-secondary-400 focus:bg-secondary-500 text-gray-100 focus:ring-secondary-300',
danger: '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',
transparent: 'border-transparent bg-transparent text-primary-600 dark:text-primary-400 dark:bg-transparent hover:bg-gray-200 dark:hover:bg-gray-800/50',
outline: 'border-gray-100 border-2 bg-transparent text-gray-100 hover:bg-white/10',
outlined: 'border-gray-100 border-2 bg-transparent text-gray-100 hover:bg-white/10',
muted: 'border border-solid bg-transparent border-gray-400 dark:border-gray-800 hover:border-primary-300 dark:hover:border-primary-700 focus:border-primary-500 text-gray-800 dark:text-gray-100 focus:ring-primary-500',
};

View File

@@ -1436,6 +1436,13 @@ const router = createRouter({
pathParamsAllowedCharacters: ['@'],
});
router.subscribe('onBeforeNavigate', (event) => {
if (!event.fromLocation || event.hashChanged || event.hrefChanged || event.pathChanged) return;
if (event.fromLocation.state.modalIndex === event.toLocation.state.modalIndex) {
window.scrollTo(0, 0);
}
});
declare module '@tanstack/react-router' {
interface Register {
router: typeof router;

View File

@@ -92,6 +92,8 @@ const ProfileLayout: React.FC = () => {
const showTabs = !['/following', '/followers', '/pins'].some(path => pathname.endsWith(path));
console.log(account);
return (
<>
{account?.local === false && (

View File

@@ -1717,7 +1717,7 @@
"status.sensitive_warning": "Wrażliwa zawartość",
"status.sensitive_warning.subtitle": "Ta treść może nie być odpowiednia dla niektórych odbiorców.",
"status.share": "Udostępnij",
"status.show_filter_reason": "Pokaż mimo wszystko",
"status.show_filter_reason": "Pokaż mimo tego",
"status.show_less_all": "Zwiń wszystkie",
"status.show_more_all": "Rozwiń wszystkie",
"status.show_original": "Pokaż oryginalny wpis",

View File

@@ -289,9 +289,10 @@ const MediaModal: React.FC<MediaModalProps & BaseModalProps> = (props) => {
role='presentation'
>
<Stack
{...bind()}
onClick={handleClickOutside}
className={
clsx('⁂-media-modal__content fixed inset-0 h-full grow transition-all', {
clsx('⁂-media-modal__content fixed inset-0 h-full grow touch-pan-y transition-all', {
'xl:pr-96': !isFullScreen,
'xl:pr-0': isFullScreen,
})
@@ -349,7 +350,6 @@ const MediaModal: React.FC<MediaModalProps & BaseModalProps> = (props) => {
{/* Height based on height of top/bottom bars */}
<div
{...bind()}
className='relative h-[calc(100vh-120px)] w-full grow'
>
{hasMultipleImages && (

View File

@@ -250,7 +250,7 @@ const EditProfilePage: React.FC = () => {
if (fields_attributes?.length === 0) params.fields_attributes = { '0': { name: '', value: '' } };
else if (fields_attributes) params.fields_attributes = Object.fromEntries(
fields_attributes.map((field, i) => [i.toString(), field]),
fields_attributes.map(({ name, value }, i) => [i.toString(), { name, value }]),
);
if (header.file !== undefined) params.header = header.file || '';
if (avatar.file !== undefined) params.avatar = avatar.file || '';

View File

@@ -0,0 +1,134 @@
import { type InfiniteData, useMutation, useQuery } from '@tanstack/react-query';
import { useClient } from 'pl-fe/hooks/use-client';
import { useFeatures } from 'pl-fe/hooks/use-features';
import { queryClient } from '../client';
import { filterById } from '../utils/filter-id';
import { makePaginatedResponseQuery } from '../utils/make-paginated-response-query';
import { minifyAccountList } from '../utils/minify-list';
import type { Antenna, PaginatedResponse, CreateAntennaParams, UpdateAntennaParams } from 'pl-api';
const useAntennas = <T>(
select?: ((data: Array<Antenna>) => T),
) => {
const client = useClient();
const features = useFeatures();
return useQuery({
queryKey: ['antennas'],
queryFn: () => client.antennas.fetchAntennas(),
enabled: features.antennas,
select,
});
};
const useAntenna = (antennaId?: string) => useAntennas((data) => antennaId ? data.find(antenna => antenna.id === antennaId) : undefined);
const useCreateAntenna = () => {
const client = useClient();
return useMutation({
mutationKey: ['antennas', 'create'],
mutationFn: (params: CreateAntennaParams) => client.antennas.createAntenna(params),
onSettled: () => queryClient.invalidateQueries({ queryKey: ['antennas'] }),
});
};
const useDeleteAntenna = () => {
const client = useClient();
return useMutation({
mutationKey: ['antennas', 'delete'],
mutationFn: (antennaId: string) => client.antennas.deleteAntenna(antennaId),
onSuccess: (_, deletedAntennaId) => {
queryClient.setQueryData<Array<Antenna>>(
['antennas'],
(prevData) => prevData?.filter(({ id }) => id !== deletedAntennaId),
);
},
});
};
const useUpdateAntenna = (antennaId: string) => {
const client = useClient();
return useMutation({
mutationKey: ['antennas', 'update', antennaId],
mutationFn: (params: UpdateAntennaParams) => client.antennas.updateAntenna(antennaId, params),
onSettled: () => queryClient.invalidateQueries({ queryKey: ['antennas'] }),
});
};
const useAntennaAccounts = makePaginatedResponseQuery(
(antennaId: string) => ['accountsLists', 'antennas', antennaId],
(client, [antennaId]) => client.antennas.getAntennaAccounts(antennaId).then(minifyAccountList),
);
const useAddAccountsToAntenna = (antennaId: string) => {
const client = useClient();
return useMutation({
mutationKey: ['accountsLists', 'antennas', antennaId, 'add'],
mutationFn: (accountIds: Array<string>) => client.antennas.addAntennaAccounts(antennaId, accountIds),
onSettled: (_, __, accountIds) => {
queryClient.invalidateQueries({ queryKey: ['accountsLists', 'antennas', antennaId] });
},
});
};
const useRemoveAccountsFromAntenna = (antennaId: string) => {
const client = useClient();
return useMutation({
mutationKey: ['accountsLists', 'antennas', antennaId, 'remove'],
mutationFn: (accountIds: Array<string>) => client.antennas.removeAntennaAccounts(antennaId, accountIds),
onSettled: (_, __, accountIds) => {
queryClient.setQueryData<InfiniteData<PaginatedResponse<string>>>(['accountsLists', 'antennas', antennaId], filterById(accountIds));
},
});
};
const useAntennaExcludedAccounts = makePaginatedResponseQuery(
(antennaId: string) => ['accountsLists', 'antennas', antennaId, 'excluded'],
(client, [antennaId]) => client.antennas.getAntennaExcludedAccounts(antennaId).then(minifyAccountList),
);
const useAddExcludedAccountsToAntenna = (antennaId: string) => {
const client = useClient();
return useMutation({
mutationKey: ['accountsLists', 'antennas', antennaId, 'addExcluded'],
mutationFn: (accountIds: Array<string>) => client.antennas.addAntennaExcludedAccounts(antennaId, accountIds),
onSettled: (_, __, accountIds) => {
queryClient.invalidateQueries({ queryKey: ['accountsLists', 'antennas', antennaId] });
},
});
};
const useRemoveExcludedAccountsFromAntenna = (antennaId: string) => {
const client = useClient();
return useMutation({
mutationKey: ['accountsLists', 'antennas', antennaId, 'removeExcluded'],
mutationFn: (accountIds: Array<string>) => client.antennas.removeAntennaExcludedAccounts(antennaId, accountIds),
onSettled: (_, __, accountIds) => {
queryClient.setQueryData<InfiniteData<PaginatedResponse<string>>>(['accountsLists', 'antennas', antennaId], filterById(accountIds));
},
});
};
export {
useAntennas,
useAntenna,
useCreateAntenna,
useDeleteAntenna,
useUpdateAntenna,
useAntennaAccounts,
useAddAccountsToAntenna,
useRemoveAccountsFromAntenna,
useAntennaExcludedAccounts,
useAddExcludedAccountsToAntenna,
useRemoveExcludedAccountsFromAntenna,
};

View File

@@ -1,5 +1,5 @@
.empty-column-indicator {
@apply bg-primary-50 dark:bg-gray-700 text-gray-900 dark:text-gray-300 text-center p-10 flex flex-1 items-center justify-center min-h-[160px] rounded-lg;
@apply bg-primary-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100 text-center p-10 flex flex-1 items-center justify-center min-h-[160px] rounded-lg;
& > span {
@apply max-w-[400px];

View File

@@ -1,15 +1,19 @@
@use 'mixins';
.-edit-event .-content-type-button {
position: absolute;
top: 0.5rem;
right: 0.5rem;
z-index: 11!important;
@include mixins.button($theme: muted, $size: xs);
.-edit-event {
@apply space-y-4;
.-icon svg {
width: 1rem;
height: 1rem;
.-content-type-button {
position: absolute;
top: 0.5rem;
right: 0.5rem;
z-index: 11!important;
@include mixins.button($theme: muted, $size: xs);
.-icon svg {
width: 1rem;
height: 1rem;
}
}
}

View File

@@ -7,6 +7,13 @@ html {
--font-mono: 'Roboto Mono', ui-monospace, monospace;
}
html:has(body.with-modals),
body.with-modals {
touch-action: none;
overscroll-behavior: none;
scrollbar-gutter: stable;
}
body {
@apply bg-white text-base antialiased black:bg-black dark:bg-gray-800;
height: 100%;
@@ -21,7 +28,7 @@ body {
}
&.with-modals {
@apply overflow-hidden;
overflow: hidden;
}
}

View File

@@ -7,18 +7,27 @@
}
.-zoomable-image {
@apply relative flex size-full items-center justify-center;
position: relative;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
scrollbar-width: none;
overflow: hidden;
user-select: none;
&--zoomed-in {
@apply cursor-grab z-[9999];
z-index: 9999;
cursor: grab;
}
&--error img {
@apply hidden;
display: none;
}
&--dragging {
@apply cursor-grabbing;
cursor: grabbing;
}
&__preview {
@@ -30,6 +39,11 @@
}
img {
@apply size-auto max-h-[80%] max-w-full object-contain shadow-2xl;
@apply shadow-2xl;
max-height: 80%;
width: auto;
height: auto;
touch-action: none;
user-select: none;
}
}