Merge branch 'develop' of https://codeberg.org/mkljczk/pl-fe into develop
This commit is contained in:
@ -14,10 +14,9 @@ import Text from 'pl-fe/components/ui/text';
|
||||
import VerificationBadge from 'pl-fe/components/verification-badge';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import ActionButton from 'pl-fe/features/ui/components/action-button';
|
||||
import { useAcct } from 'pl-fe/hooks/use-acct';
|
||||
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
|
||||
import { useSettings } from 'pl-fe/stores/settings';
|
||||
import { getAcct } from 'pl-fe/utils/accounts';
|
||||
import { displayFqn } from 'pl-fe/utils/state';
|
||||
|
||||
import Badge from './badge';
|
||||
import { ParsedContent } from './parsed-content';
|
||||
@ -148,7 +147,7 @@ const Account = ({
|
||||
const [style, setStyle] = useState<React.CSSProperties>({});
|
||||
|
||||
const me = useAppSelector((state) => state.me);
|
||||
const username = useAppSelector((state) => account ? getAcct(account, displayFqn(state)) : null);
|
||||
const username = useAcct(account);
|
||||
const { disableUserProvidedMedia } = useSettings();
|
||||
|
||||
const handleAction = () => {
|
||||
|
||||
@ -37,7 +37,7 @@ const SiteError: ErrorRouteComponent = ({ error, info }) => {
|
||||
|
||||
const sentryEnabled = Boolean(sentryDsn);
|
||||
const isProduction = NODE_ENV === 'production';
|
||||
const errorText = String(error) + info?.componentStack;
|
||||
const errorText = String(error) + (info?.componentStack || '');
|
||||
|
||||
const clearCookies: React.MouseEventHandler = (e) => {
|
||||
localStorage.clear();
|
||||
|
||||
@ -279,10 +279,6 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
||||
unblockDomain(domain);
|
||||
};
|
||||
|
||||
const onProfileExternal = (url: string) => {
|
||||
window.open(url, '_blank');
|
||||
};
|
||||
|
||||
const onAddToList = () => {
|
||||
openModal('LIST_ADDER', {
|
||||
accountId: account.id,
|
||||
@ -380,7 +376,7 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
||||
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.profileExternal, { domain }),
|
||||
action: () => onProfileExternal(account.url),
|
||||
href: account.url,
|
||||
icon: require('@phosphor-icons/core/regular/arrow-square-out.svg'),
|
||||
});
|
||||
}
|
||||
|
||||
@ -191,10 +191,6 @@ const EventHeader: React.FC<IEventHeader> = ({ status }) => {
|
||||
dispatch(initReport(ReportableEntities.STATUS, account, { status }));
|
||||
};
|
||||
|
||||
const handleModerateStatus = () => {
|
||||
window.open(`/pleroma/admin/#/statuses/${status.id}/`, '_blank');
|
||||
};
|
||||
|
||||
const handleToggleStatusSensitivity = () => {
|
||||
dispatch(toggleStatusSensitivityModal(intl, status.id, status.sensitive));
|
||||
};
|
||||
@ -349,7 +345,7 @@ const EventHeader: React.FC<IEventHeader> = ({ status }) => {
|
||||
if (isAdmin && features.pleromaAdminStatuses) {
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.adminStatus),
|
||||
action: handleModerateStatus,
|
||||
href: `/pleroma/admin/#/statuses/${status.id}/`,
|
||||
icon: require('@phosphor-icons/core/regular/pencil-simple.svg'),
|
||||
});
|
||||
}
|
||||
|
||||
@ -67,7 +67,6 @@ const ModalRoot: React.FC = () => {
|
||||
const index = modals.length - 1;
|
||||
|
||||
const onClickClose = (type?: ModalType, all?: boolean) => {
|
||||
console.log('Closing modal:', type, all);
|
||||
switch (type) {
|
||||
case 'COMPOSE':
|
||||
dispatch(cancelReplyCompose());
|
||||
|
||||
@ -12,8 +12,8 @@ import Icon from 'pl-fe/components/ui/icon';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import { useAcct } from 'pl-fe/hooks/use-acct';
|
||||
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
|
||||
import { usePlFeConfig } from 'pl-fe/hooks/use-pl-fe-config';
|
||||
import { accountScrobbleQueryOptions } from 'pl-fe/queries/accounts/account-scrobble';
|
||||
import { capitalize } from 'pl-fe/utils/strings';
|
||||
|
||||
@ -40,7 +40,7 @@ interface IProfileInfoPanel {
|
||||
/** User profile metadata, such as location, birthday, etc. */
|
||||
const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) => {
|
||||
const intl = useIntl();
|
||||
const { displayFqn } = usePlFeConfig();
|
||||
const acct = useAcct(account);
|
||||
const me = useAppSelector(state => state.me);
|
||||
const ownAccount = account?.id === me;
|
||||
|
||||
@ -152,7 +152,7 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) =>
|
||||
|
||||
<HStack alignItems='center' space={0.5}>
|
||||
<Text size='sm' theme='muted' direction='ltr' truncate>
|
||||
@{displayFqn ? account.fqn : account.acct}
|
||||
@{acct}
|
||||
</Text>
|
||||
|
||||
{account.locked && (
|
||||
|
||||
@ -11,11 +11,9 @@ import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import VerificationBadge from 'pl-fe/components/verification-badge';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
|
||||
import { useAcct } from 'pl-fe/hooks/use-acct';
|
||||
import { useSettings } from 'pl-fe/stores/settings';
|
||||
import { getAcct } from 'pl-fe/utils/accounts';
|
||||
import { shortNumberFormat } from 'pl-fe/utils/numbers';
|
||||
import { displayFqn } from 'pl-fe/utils/state';
|
||||
|
||||
const messages = defineMessages({
|
||||
account_locked: { id: 'account.locked_info', defaultMessage: 'This account privacy status is set to locked. The owner manually reviews who can follow them.' },
|
||||
@ -32,7 +30,7 @@ const UserPanel: React.FC<IUserPanel> = ({ accountId, action, badges, domain })
|
||||
const intl = useIntl();
|
||||
const { demetricator, disableUserProvidedMedia } = useSettings();
|
||||
const { account } = useAccount(accountId);
|
||||
const fqn = useAppSelector((state) => displayFqn(state));
|
||||
const displayedAcct = useAcct(account);
|
||||
|
||||
if (!account) return null;
|
||||
const acct = !account.acct.includes('@') && domain ? `${account.acct}@${domain}` : account.acct;
|
||||
@ -96,7 +94,7 @@ const UserPanel: React.FC<IUserPanel> = ({ accountId, action, badges, domain })
|
||||
|
||||
<HStack alignItems='center' space={1}>
|
||||
<Text size='sm' theme='muted' direction='ltr' truncate>
|
||||
@{getAcct(account, fqn)}
|
||||
@{displayedAcct}
|
||||
</Text>
|
||||
|
||||
{account.locked && (
|
||||
|
||||
28
packages/pl-fe/src/hooks/use-acct.ts
Normal file
28
packages/pl-fe/src/hooks/use-acct.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { displayFqn } from 'pl-fe/utils/state';
|
||||
|
||||
import { useAppSelector } from './use-app-selector';
|
||||
import { useInstance } from './use-instance';
|
||||
import { useOwnAccount } from './use-own-account';
|
||||
|
||||
import type { Account } from 'pl-api';
|
||||
|
||||
const useAcct = (account?: Pick<Account, 'fqn' | 'acct' | 'local' | 'url'>): string | undefined => {
|
||||
const fqn = useAppSelector((state) => displayFqn(state));
|
||||
const instance = useInstance();
|
||||
const localUrl = useOwnAccount().account?.url;
|
||||
|
||||
return useMemo(() => {
|
||||
if (!account) return;
|
||||
if (!fqn) return account.acct;
|
||||
const localHost = localUrl ? new URL(localUrl).host : null;
|
||||
const otherHost = new URL(account.url).host;
|
||||
if (account.local === false || (localHost && localHost !== otherHost)) return account.fqn;
|
||||
return `${account.acct}@${instance.domain}`;
|
||||
}, [account?.acct, fqn, instance.domain, localUrl]);
|
||||
};
|
||||
|
||||
export {
|
||||
useAcct,
|
||||
};
|
||||
@ -19,10 +19,9 @@ import {
|
||||
PinnedAccountsPanel,
|
||||
AccountNotePanel,
|
||||
} from 'pl-fe/features/ui/util/async-components';
|
||||
import { useAcct } from 'pl-fe/hooks/use-acct';
|
||||
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
|
||||
import { useFeatures } from 'pl-fe/hooks/use-features';
|
||||
import { usePlFeConfig } from 'pl-fe/hooks/use-pl-fe-config';
|
||||
import { getAcct } from 'pl-fe/utils/accounts';
|
||||
|
||||
/** Layout to display a user's profile. */
|
||||
const ProfileLayout: React.FC = () => {
|
||||
@ -33,7 +32,7 @@ const ProfileLayout: React.FC = () => {
|
||||
|
||||
const me = useAppSelector(state => state.me);
|
||||
const features = useFeatures();
|
||||
const { displayFqn } = usePlFeConfig();
|
||||
const acct = useAcct(account);
|
||||
|
||||
if (isUnauthorized) {
|
||||
localStorage.setItem('plfe:redirect_uri', location.href);
|
||||
@ -101,7 +100,7 @@ const ProfileLayout: React.FC = () => {
|
||||
</Helmet>
|
||||
)}
|
||||
<Layout.Main>
|
||||
<Column size='lg' label={account ? `@${getAcct(account, displayFqn)}` : ''} withHeader={false}>
|
||||
<Column size='lg' label={account ? `@${acct}` : ''} withHeader={false}>
|
||||
<div className='space-y-4'>
|
||||
<Header key={`profile-header-${account?.id}`} account={account} />
|
||||
<ProfileInfoPanel username={username} account={account} />
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { animated, useSpring } from '@react-spring/web';
|
||||
import { Link } from '@tanstack/react-router';
|
||||
import { useDrag } from '@use-gesture/react';
|
||||
import clsx from 'clsx';
|
||||
import React, { type RefCallback, useCallback, useEffect, useState } from 'react';
|
||||
import React, { type RefCallback, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
import ReactSwipeableViews from 'react-swipeable-views';
|
||||
|
||||
import { fetchStatusWithContext } from 'pl-fe/actions/statuses';
|
||||
import ExtendedVideoPlayer from 'pl-fe/components/extended-video-player';
|
||||
@ -25,6 +26,8 @@ import { makeGetStatus } from 'pl-fe/selectors';
|
||||
import type { MediaAttachment } from 'pl-api';
|
||||
import type { BaseModalProps } from 'pl-fe/features/ui/components/modal-root';
|
||||
|
||||
const MIN_SWIPE_DISTANCE = 400;
|
||||
|
||||
const messages = defineMessages({
|
||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||
expand: { id: 'lightbox.expand', defaultMessage: 'Expand' },
|
||||
@ -36,19 +39,6 @@ const messages = defineMessages({
|
||||
download: { id: 'video.download', defaultMessage: 'Download file' },
|
||||
});
|
||||
|
||||
// you can't use 100vh, because the viewport height is taller
|
||||
// than the visible part of the document in some mobile
|
||||
// browsers when its address bar is visible.
|
||||
// https://developers.google.com/web/updates/2016/12/url-bar-resizing
|
||||
const swipeableViewsStyle: React.CSSProperties = {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
};
|
||||
|
||||
const containerStyle: React.CSSProperties = {
|
||||
alignItems: 'center', // center vertically
|
||||
};
|
||||
|
||||
interface MediaModalProps {
|
||||
media?: Array<MediaAttachment>;
|
||||
statusId?: string;
|
||||
@ -77,6 +67,32 @@ const MediaModal: React.FC<MediaModalProps & BaseModalProps> = (props) => {
|
||||
const [navigationHidden, setNavigationHidden] = useState(false);
|
||||
const [isFullScreen, setIsFullScreen] = useState(!status);
|
||||
|
||||
const [wrapperStyles, api] = useSpring(() => ({
|
||||
x: `-${index * 100}%`,
|
||||
}));
|
||||
|
||||
const handleChangeIndex = useCallback(
|
||||
(newIndex: number, animate = false) => {
|
||||
if (newIndex < 0) {
|
||||
newIndex = media.length + newIndex;
|
||||
} else if (newIndex >= media.length) {
|
||||
newIndex = newIndex % media.length;
|
||||
}
|
||||
setIndex(newIndex);
|
||||
setZoomedIn(false);
|
||||
if (animate) {
|
||||
void api.start({ x: `calc(-${newIndex * 100}% + 0px)` });
|
||||
}
|
||||
},
|
||||
[api, media.length],
|
||||
);
|
||||
const handlePrevClick = useCallback(() => {
|
||||
handleChangeIndex(index - 1, true);
|
||||
}, [handleChangeIndex, index]);
|
||||
const handleNextClick = useCallback(() => {
|
||||
handleChangeIndex(index + 1, true);
|
||||
}, [handleChangeIndex, index]);
|
||||
|
||||
const [viewportDimensions, setViewportDimensions] = useState<{
|
||||
width: number;
|
||||
height: number;
|
||||
@ -93,10 +109,6 @@ const MediaModal: React.FC<MediaModalProps & BaseModalProps> = (props) => {
|
||||
|
||||
const hasMultipleImages = media.length > 1;
|
||||
|
||||
const handleSwipe = (index: number) => setIndex(index % media.length);
|
||||
const handleNextClick = () => setIndex((index + 1) % media.length);
|
||||
const handlePrevClick = () => setIndex((media.length + index - 1) % media.length);
|
||||
|
||||
const navigationHiddenClassName = navigationHidden ? 'pointer-events-none opacity-0' : '';
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
@ -114,6 +126,30 @@ const MediaModal: React.FC<MediaModalProps & BaseModalProps> = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const bind = useDrag(
|
||||
({ active, movement: [mx], direction: [xDir], cancel }) => {
|
||||
// Disable swipe when zoomed in.
|
||||
if (zoomedIn) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If dragging and swipe distance is enough, change the index.
|
||||
if (
|
||||
active &&
|
||||
Math.abs(mx) > Math.min(window.innerWidth / 4, MIN_SWIPE_DISTANCE)
|
||||
) {
|
||||
handleChangeIndex(index - xDir);
|
||||
cancel();
|
||||
}
|
||||
// Set the x position via calc to ensure proper centering regardless of screen size.
|
||||
const x = active ? mx : 0;
|
||||
void api.start({
|
||||
x: `calc(-${index * 100}% + ${x}px)`,
|
||||
});
|
||||
},
|
||||
{ pointer: { capture: false } },
|
||||
);
|
||||
|
||||
const handleDownload = () => {
|
||||
const mediaItem = hasMultipleImages ? media[index as number] : media[0];
|
||||
window.open(mediaItem?.url);
|
||||
@ -133,7 +169,7 @@ const MediaModal: React.FC<MediaModalProps & BaseModalProps> = (props) => {
|
||||
setZoomedIn((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
const content = media.map((attachment, i) => {
|
||||
const content = useMemo(() => media.map((attachment, i) => {
|
||||
let width: number | undefined, height: number | undefined;
|
||||
if (attachment.type === 'image' || attachment.type === 'gifv' || attachment.type === 'video') {
|
||||
width = (attachment.meta?.original?.width);
|
||||
@ -209,7 +245,7 @@ const MediaModal: React.FC<MediaModalProps & BaseModalProps> = (props) => {
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}), [media.length, index, zoomedIn, handleZoomClick]);
|
||||
|
||||
// Load data.
|
||||
useEffect(() => {
|
||||
@ -313,6 +349,7 @@ 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 && (
|
||||
@ -328,14 +365,14 @@ const MediaModal: React.FC<MediaModalProps & BaseModalProps> = (props) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ReactSwipeableViews
|
||||
style={swipeableViewsStyle}
|
||||
containerStyle={containerStyle}
|
||||
onChangeIndex={handleSwipe}
|
||||
index={index}
|
||||
<animated.div
|
||||
style={wrapperStyles}
|
||||
className='media-modal__closer'
|
||||
role='presentation'
|
||||
onClick={() => onClose()}
|
||||
>
|
||||
{content}
|
||||
</ReactSwipeableViews>
|
||||
</animated.div>
|
||||
|
||||
{hasMultipleImages && (
|
||||
<div className={clsx('absolute inset-y-0 right-5 z-10 flex items-center transition-opacity', navigationHiddenClassName)}>
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
.media-modal {
|
||||
touch-action: pan-y;
|
||||
|
||||
.audio-player.detailed,
|
||||
.extended-video-player {
|
||||
@apply flex items-center justify-center;
|
||||
@ -15,5 +17,23 @@
|
||||
@apply max-w-full max-h-[80%];
|
||||
}
|
||||
}
|
||||
|
||||
&__closer {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
inset-inline-end: 0;
|
||||
bottom: 0;
|
||||
|
||||
> div {
|
||||
flex-shrink: 0;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -306,7 +306,8 @@ body {
|
||||
z-index: 1;
|
||||
|
||||
&__content {
|
||||
@apply mx-auto w-full max-w-3xl grow black:pt-0 sm:px-6 sm:pt-4 md:gap-4 md:px-8;
|
||||
// FIXME: min-h-[100dvh] is a workaround for some Ladybird bug
|
||||
@apply mx-auto w-full max-w-3xl grow black:pt-0 sm:px-6 sm:pt-4 md:gap-4 md:px-8 min-h-[100dvh];
|
||||
|
||||
&--full-width {
|
||||
@apply flex md:max-w-full;
|
||||
|
||||
@ -22,12 +22,7 @@ const getBaseURL = (account: Pick<Account, 'url'>): string => {
|
||||
}
|
||||
};
|
||||
|
||||
const getAcct = (account: Pick<Account, 'fqn' | 'acct'>, displayFqn: boolean): string => (
|
||||
displayFqn === true ? account.fqn : account.acct
|
||||
);
|
||||
|
||||
export {
|
||||
getDomain,
|
||||
getBaseURL,
|
||||
getAcct,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user