kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Merge branch 'main' into create-wallet
commit
33612d77c3
|
|
@ -100,7 +100,7 @@ const dequeueTimeline = (timelineId: string, expandFunc?: (lastStatusId: string)
|
|||
} else {
|
||||
if (timelineId === 'home') {
|
||||
dispatch(clearTimeline(timelineId));
|
||||
dispatch(expandHomeTimeline(optionalExpandArgs));
|
||||
dispatch(expandFollowsTimeline(optionalExpandArgs));
|
||||
} else if (timelineId === 'community') {
|
||||
dispatch(clearTimeline(timelineId));
|
||||
dispatch(expandCommunityTimeline(optionalExpandArgs));
|
||||
|
|
@ -194,20 +194,20 @@ const expandTimeline = (timelineId: string, path: string, params: Record<string,
|
|||
});
|
||||
};
|
||||
|
||||
interface ExpandHomeTimelineOpts {
|
||||
interface ExpandFollowsTimelineOpts {
|
||||
maxId?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
interface HomeTimelineParams {
|
||||
interface FollowsTimelineParams {
|
||||
max_id?: string;
|
||||
exclude_replies?: boolean;
|
||||
with_muted?: boolean;
|
||||
}
|
||||
|
||||
const expandHomeTimeline = ({ url, maxId }: ExpandHomeTimelineOpts = {}, done = noOp) => {
|
||||
const expandFollowsTimeline = ({ url, maxId }: ExpandFollowsTimelineOpts = {}, done = noOp) => {
|
||||
const endpoint = url || '/api/v1/timelines/home';
|
||||
const params: HomeTimelineParams = {};
|
||||
const params: FollowsTimelineParams = {};
|
||||
|
||||
if (!url && maxId) {
|
||||
params.max_id = maxId;
|
||||
|
|
@ -337,7 +337,7 @@ export {
|
|||
deleteFromTimelines,
|
||||
clearTimeline,
|
||||
expandTimeline,
|
||||
expandHomeTimeline,
|
||||
expandFollowsTimeline,
|
||||
expandPublicTimeline,
|
||||
expandRemoteTimeline,
|
||||
expandCommunityTimeline,
|
||||
|
|
|
|||
|
|
@ -6,15 +6,14 @@ import { closeModal } from 'soapbox/actions/modals.ts';
|
|||
import { HTTPError } from 'soapbox/api/HTTPError.ts';
|
||||
import { useApi } from 'soapbox/hooks/useApi.ts';
|
||||
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
||||
import { useInstance } from 'soapbox/hooks/useInstance.ts';
|
||||
import { captchaSchema, type CaptchaData } from 'soapbox/schemas/captcha.ts';
|
||||
import toast from 'soapbox/toast.tsx';
|
||||
|
||||
const messages = defineMessages({
|
||||
sucessMessage: { id: 'nostr_signup.captcha_message.sucess', defaultMessage: 'Incredible! You\'ve successfully completed the captcha.' },
|
||||
wrongMessage: { id: 'nostr_signup.captcha_message.wrong', defaultMessage: 'Oops! It looks like your captcha response was incorrect. Please try again.' },
|
||||
errorMessage: { id: 'nostr_signup.captcha_message.error', defaultMessage: 'It seems an error has occurred. Please try again. If the problem persists, please contact us.' },
|
||||
misbehavingMessage: { id: 'nostr_signup.captcha_message.misbehaving', defaultMessage: 'It looks like we\'re experiencing issues with the {instance}. Please try again. If the error persists, try again later.' },
|
||||
success: { id: 'nostr_signup.captcha_message.sucess', defaultMessage: 'Incredible! You\'ve successfully completed the captcha.' },
|
||||
wrong: { id: 'nostr_signup.captcha_message.wrong', defaultMessage: 'Oops! It looks like your captcha response was incorrect. Please try again.' },
|
||||
expired: { id: 'nostr_signup.captcha_message.expired', defaultMessage: 'The captcha has expired. Please start over.' },
|
||||
error: { id: 'nostr_signup.captcha_message.error', defaultMessage: 'It seems an error has occurred. Please try again. If the problem persists, please contact us.' },
|
||||
});
|
||||
|
||||
function getRandomNumber(min: number, max: number): number {
|
||||
|
|
@ -23,7 +22,6 @@ function getRandomNumber(min: number, max: number): number {
|
|||
|
||||
const useCaptcha = () => {
|
||||
const api = useApi();
|
||||
const { instance } = useInstance();
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
const [captcha, setCaptcha] = useState<CaptchaData>();
|
||||
|
|
@ -68,7 +66,7 @@ const useCaptcha = () => {
|
|||
await api.post(`/api/v1/ditto/captcha/${captcha.id}/verify`, result);
|
||||
dispatch(closeModal('CAPTCHA'));
|
||||
await dispatch(fetchMe()); // refetch account so `captcha_solved` changes.
|
||||
toast.success(messages.sucessMessage);
|
||||
toast.success(messages.success);
|
||||
} catch (error) {
|
||||
setTryAgain(true);
|
||||
|
||||
|
|
@ -77,13 +75,13 @@ const useCaptcha = () => {
|
|||
|
||||
switch (status) {
|
||||
case 400:
|
||||
message = intl.formatMessage(messages.wrongMessage);
|
||||
message = intl.formatMessage(messages.wrong);
|
||||
break;
|
||||
case 422:
|
||||
message = intl.formatMessage(messages.misbehavingMessage, { instance: instance.title });
|
||||
case 410:
|
||||
message = intl.formatMessage(messages.expired);
|
||||
break;
|
||||
default:
|
||||
message = intl.formatMessage(messages.errorMessage);
|
||||
message = intl.formatMessage(messages.error);
|
||||
console.error(error);
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ export const ProfileHoverCard: React.FC<IProfileHoverCard> = ({ visible = true }
|
|||
<Avatar src={account.avatar} size={80} className='size-20 overflow-hidden bg-gray-50 ring-2 ring-white' />
|
||||
</Link>
|
||||
|
||||
<div className='mt-2'>
|
||||
<div className='relative z-50 mt-2'>
|
||||
<ActionButton account={account} small />
|
||||
</div>
|
||||
</HStack>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import atIcon from '@tabler/icons/outline/at.svg';
|
||||
import banIcon from '@tabler/icons/outline/ban.svg';
|
||||
import bookmarkIcon from '@tabler/icons/outline/bookmark.svg';
|
||||
import calendarEventIcon from '@tabler/icons/outline/calendar-event.svg';
|
||||
|
|
@ -36,7 +35,6 @@ import ProfileStats from 'soapbox/features/ui/components/profile-stats.tsx';
|
|||
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
||||
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
||||
import { useFeatures } from 'soapbox/hooks/useFeatures.ts';
|
||||
import { useInstance } from 'soapbox/hooks/useInstance.ts';
|
||||
import { useSettingsNotifications } from 'soapbox/hooks/useSettingsNotifications.ts';
|
||||
import { makeGetOtherAccounts } from 'soapbox/selectors/index.ts';
|
||||
|
||||
|
|
@ -114,7 +112,6 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
|
|||
const sidebarOpen = useAppSelector((state) => state.sidebar.sidebarOpen);
|
||||
const settings = useAppSelector((state) => getSettings(state));
|
||||
const followRequestsCount = useAppSelector((state) => state.user_lists.follow_requests.items.count());
|
||||
const { instance } = useInstance();
|
||||
const settingsNotifications = useSettingsNotifications();
|
||||
|
||||
const closeButtonRef = useRef(null);
|
||||
|
|
@ -271,25 +268,14 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
|
|||
/>
|
||||
)}
|
||||
|
||||
{features.publicTimeline && <>
|
||||
<Divider />
|
||||
|
||||
{features.publicTimeline && features.federating && (
|
||||
<SidebarLink
|
||||
to='/timeline/local'
|
||||
icon={features.federating ? atIcon : worldIcon}
|
||||
text={features.federating ? instance.domain : <FormattedMessage id='tabs_bar.all' defaultMessage='All' />}
|
||||
to='/timeline/global'
|
||||
icon={worldIcon}
|
||||
text={<FormattedMessage id='tabs_bar.global' defaultMessage='Global' />}
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{features.federating && (
|
||||
<SidebarLink
|
||||
to='/timeline/global'
|
||||
icon={worldIcon}
|
||||
text={<FormattedMessage id='tabs_bar.global' defaultMessage='Global' />}
|
||||
onClick={onClose}
|
||||
/>
|
||||
)}
|
||||
</>}
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import circlesFilledIcon from '@tabler/icons/filled/circles.svg';
|
|||
import homeFilledIcon from '@tabler/icons/filled/home.svg';
|
||||
import settingsFilledIcon from '@tabler/icons/filled/settings.svg';
|
||||
import userFilledIcon from '@tabler/icons/filled/user.svg';
|
||||
import atIcon from '@tabler/icons/outline/at.svg';
|
||||
import bellIcon from '@tabler/icons/outline/bell.svg';
|
||||
import bookmarkIcon from '@tabler/icons/outline/bookmark.svg';
|
||||
import calendarEventIcon from '@tabler/icons/outline/calendar-event.svg';
|
||||
|
|
@ -162,6 +161,16 @@ const SidebarNavigation = () => {
|
|||
text={<FormattedMessage id='tabs_bar.home' defaultMessage='Home' />}
|
||||
/>
|
||||
|
||||
{account && (
|
||||
<SidebarNavigationLink
|
||||
to='/notifications'
|
||||
icon={bellIcon}
|
||||
activeIcon={bellFilledIcon}
|
||||
count={notificationCount}
|
||||
text={<FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' />}
|
||||
/>
|
||||
)}
|
||||
|
||||
<SidebarNavigationLink
|
||||
to='/search'
|
||||
icon={searchIcon}
|
||||
|
|
@ -170,13 +179,6 @@ const SidebarNavigation = () => {
|
|||
|
||||
{account && (
|
||||
<>
|
||||
<SidebarNavigationLink
|
||||
to='/notifications'
|
||||
icon={bellIcon}
|
||||
activeIcon={bellFilledIcon}
|
||||
count={notificationCount}
|
||||
text={<FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' />}
|
||||
/>
|
||||
|
||||
{renderMessagesLink()}
|
||||
|
||||
|
|
@ -188,6 +190,10 @@ const SidebarNavigation = () => {
|
|||
text={<FormattedMessage id='tabs_bar.groups' defaultMessage='Groups' />}
|
||||
/>
|
||||
)}
|
||||
</>)}
|
||||
|
||||
{account && (
|
||||
<>
|
||||
|
||||
<SidebarNavigationLink
|
||||
to={`/@${account.acct}`}
|
||||
|
|
@ -223,23 +229,13 @@ const SidebarNavigation = () => {
|
|||
)}
|
||||
|
||||
{(features.publicTimeline) && (
|
||||
<>
|
||||
{(account || !restrictUnauth.timelines.local) && (
|
||||
<SidebarNavigationLink
|
||||
to='/timeline/local'
|
||||
icon={features.federating ? atIcon : worldIcon}
|
||||
text={features.federating ? instance.domain : <FormattedMessage id='tabs_bar.global' defaultMessage='Global' />}
|
||||
/>
|
||||
)}
|
||||
features.federating && (account || !restrictUnauth.timelines.federated)) && (
|
||||
<SidebarNavigationLink
|
||||
to='/timeline/global'
|
||||
icon={worldIcon}
|
||||
text={<FormattedMessage id='tabs_bar.global' defaultMessage='Global' />}
|
||||
/>
|
||||
|
||||
{(features.federating && (account || !restrictUnauth.timelines.federated)) && (
|
||||
<SidebarNavigationLink
|
||||
to='/timeline/global'
|
||||
icon={worldIcon}
|
||||
text={<FormattedMessage id='tabs_bar.global' defaultMessage='Global' />}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{menu.length > 0 && (
|
||||
|
|
|
|||
|
|
@ -251,8 +251,8 @@ const Upload: React.FC<IUpload> = ({
|
|||
|
||||
<div className='absolute inset-0 z-[-1] size-full'>
|
||||
{mediaType === 'video' && (
|
||||
<video className='size-full object-cover' autoPlay playsInline muted loop>
|
||||
<source src={media.preview_url} />
|
||||
<video className='size-full object-cover' poster={media.preview_url} autoPlay playsInline muted loop>
|
||||
<source src={media.url} />
|
||||
</video>
|
||||
)}
|
||||
{uploadIcon}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
|||
|
||||
import { patchMe } from 'soapbox/actions/me.ts';
|
||||
import { changeSetting } from 'soapbox/actions/settings.ts';
|
||||
import { HTTPError } from 'soapbox/api/HTTPError.ts';
|
||||
import List, { ListItem } from 'soapbox/components/list.tsx';
|
||||
import Button from 'soapbox/components/ui/button.tsx';
|
||||
import { CardHeader, CardTitle } from 'soapbox/components/ui/card.tsx';
|
||||
|
|
@ -90,6 +91,11 @@ const EditIdentity: React.FC<IEditIdentity> = () => {
|
|||
setReason('');
|
||||
setSubmitted(true);
|
||||
},
|
||||
onError(error) {
|
||||
if (error instanceof HTTPError) {
|
||||
toast.showAlertForError(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -185,6 +191,10 @@ export const UsernameInput: React.FC<React.ComponentProps<typeof Input>> = (prop
|
|||
return (
|
||||
<Input
|
||||
placeholder={intl.formatMessage(messages.username)}
|
||||
autoComplete='off'
|
||||
autoCorrect='off'
|
||||
autoCapitalize='off'
|
||||
pattern='^[\w.]+$'
|
||||
append={(
|
||||
<HStack alignItems='center' space={1} className='rounded p-1 text-sm backdrop-blur'>
|
||||
<Icon className='size-4' src={atIcon} />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,114 @@
|
|||
import { useEffect, useRef } from 'react';
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { expandFollowsTimeline } from 'soapbox/actions/timelines.ts';
|
||||
import PullToRefresh from 'soapbox/components/pull-to-refresh.tsx';
|
||||
import { Column } from 'soapbox/components/ui/column.tsx';
|
||||
import Stack from 'soapbox/components/ui/stack.tsx';
|
||||
import Text from 'soapbox/components/ui/text.tsx';
|
||||
import Timeline from 'soapbox/features/ui/components/timeline.tsx';
|
||||
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
||||
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
||||
import { useFeatures } from 'soapbox/hooks/useFeatures.ts';
|
||||
import { useInstance } from 'soapbox/hooks/useInstance.ts';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.home', defaultMessage: 'Home' },
|
||||
});
|
||||
|
||||
const FollowsTimeline: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const features = useFeatures();
|
||||
const { instance } = useInstance();
|
||||
|
||||
const polling = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const isPartial = useAppSelector(state => state.timelines.get('home')?.isPartial === true);
|
||||
const next = useAppSelector(state => state.timelines.get('home')?.next);
|
||||
|
||||
const handleLoadMore = (maxId: string) => {
|
||||
dispatch(expandFollowsTimeline({ url: next, maxId }));
|
||||
};
|
||||
|
||||
// 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 = () => {
|
||||
if (isPartial) {
|
||||
polling.current = setInterval(() => {
|
||||
dispatch(expandFollowsTimeline());
|
||||
}, 3000);
|
||||
} else {
|
||||
stopPolling();
|
||||
}
|
||||
};
|
||||
|
||||
const stopPolling = () => {
|
||||
if (polling.current) {
|
||||
clearInterval(polling.current);
|
||||
polling.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
return dispatch(expandFollowsTimeline());
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
checkIfReloadNeeded();
|
||||
|
||||
return () => {
|
||||
stopPolling();
|
||||
};
|
||||
}, [isPartial]);
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.title)} withHeader={false} slim>
|
||||
<PullToRefresh onRefresh={handleRefresh}>
|
||||
<Timeline
|
||||
scrollKey='home_timeline'
|
||||
onLoadMore={handleLoadMore}
|
||||
timelineId='home'
|
||||
emptyMessage={
|
||||
<Stack space={1}>
|
||||
<Text size='xl' weight='medium' align='center'>
|
||||
<FormattedMessage
|
||||
id='empty_column.home.title'
|
||||
defaultMessage="You're not following anyone yet"
|
||||
/>
|
||||
</Text>
|
||||
|
||||
<Text theme='muted' align='center'>
|
||||
<FormattedMessage
|
||||
id='empty_column.home.subtitle'
|
||||
defaultMessage='{siteTitle} gets more interesting once you follow other users.'
|
||||
values={{ siteTitle: instance.title }}
|
||||
/>
|
||||
</Text>
|
||||
|
||||
{features.federating && (
|
||||
<Text theme='muted' align='center'>
|
||||
<FormattedMessage
|
||||
id='empty_column.home'
|
||||
defaultMessage='Or you can visit {public} to get started and meet other users.'
|
||||
values={{
|
||||
public: (
|
||||
<Link to='/timeline/local' className='text-primary-600 hover:underline dark:text-primary-400'>
|
||||
<FormattedMessage id='empty_column.home.local_tab' defaultMessage='the {site_title} tab' values={{ site_title: instance.title }} />
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
</PullToRefresh>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default FollowsTimeline;
|
||||
|
|
@ -1,112 +1,47 @@
|
|||
import { useEffect, useRef } from 'react';
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Suspense } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { Route, Switch, useRouteMatch } from 'react-router-dom';
|
||||
|
||||
import { expandHomeTimeline } from 'soapbox/actions/timelines.ts';
|
||||
import PullToRefresh from 'soapbox/components/pull-to-refresh.tsx';
|
||||
import { Column } from 'soapbox/components/ui/column.tsx';
|
||||
import Stack from 'soapbox/components/ui/stack.tsx';
|
||||
import Text from 'soapbox/components/ui/text.tsx';
|
||||
import Timeline from 'soapbox/features/ui/components/timeline.tsx';
|
||||
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
||||
import Tabs from 'soapbox/components/ui/tabs.tsx';
|
||||
import { CommunityTimeline, FollowsTimeline } from 'soapbox/features/ui/util/async-components.ts';
|
||||
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
||||
import { useFeatures } from 'soapbox/hooks/useFeatures.ts';
|
||||
import { useInstance } from 'soapbox/hooks/useInstance.ts';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.home', defaultMessage: 'Home' },
|
||||
});
|
||||
|
||||
const HomeTimeline: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const features = useFeatures();
|
||||
const HomeTimeline = () => {
|
||||
const { instance } = useInstance();
|
||||
|
||||
const polling = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const isPartial = useAppSelector(state => state.timelines.get('home')?.isPartial === true);
|
||||
const next = useAppSelector(state => state.timelines.get('home')?.next);
|
||||
|
||||
const handleLoadMore = (maxId: string) => {
|
||||
dispatch(expandHomeTimeline({ url: next, maxId }));
|
||||
};
|
||||
|
||||
// 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 = () => {
|
||||
if (isPartial) {
|
||||
polling.current = setInterval(() => {
|
||||
dispatch(expandHomeTimeline());
|
||||
}, 3000);
|
||||
} else {
|
||||
stopPolling();
|
||||
}
|
||||
};
|
||||
|
||||
const stopPolling = () => {
|
||||
if (polling.current) {
|
||||
clearInterval(polling.current);
|
||||
polling.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
return dispatch(expandHomeTimeline());
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
checkIfReloadNeeded();
|
||||
|
||||
return () => {
|
||||
stopPolling();
|
||||
};
|
||||
}, [isPartial]);
|
||||
const match = useRouteMatch();
|
||||
const notifications = useAppSelector((state) => state.notificationsTab);
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.title)} withHeader={false} slim>
|
||||
<PullToRefresh onRefresh={handleRefresh}>
|
||||
<Timeline
|
||||
scrollKey='home_timeline'
|
||||
onLoadMore={handleLoadMore}
|
||||
timelineId='home'
|
||||
emptyMessage={
|
||||
<Stack space={1}>
|
||||
<Text size='xl' weight='medium' align='center'>
|
||||
<FormattedMessage
|
||||
id='empty_column.home.title'
|
||||
defaultMessage="You're not following anyone yet"
|
||||
/>
|
||||
</Text>
|
||||
|
||||
<Text theme='muted' align='center'>
|
||||
<FormattedMessage
|
||||
id='empty_column.home.subtitle'
|
||||
defaultMessage='{siteTitle} gets more interesting once you follow other users.'
|
||||
values={{ siteTitle: instance.title }}
|
||||
/>
|
||||
</Text>
|
||||
|
||||
{features.federating && (
|
||||
<Text theme='muted' align='center'>
|
||||
<FormattedMessage
|
||||
id='empty_column.home'
|
||||
defaultMessage='Or you can visit {public} to get started and meet other users.'
|
||||
values={{
|
||||
public: (
|
||||
<Link to='/timeline/local' className='text-primary-600 hover:underline dark:text-primary-400'>
|
||||
<FormattedMessage id='empty_column.home.local_tab' defaultMessage='the {site_title} tab' values={{ site_title: instance.title }} />
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
}
|
||||
<>
|
||||
<div className='sticky top-11 z-50 bg-white black:bg-black dark:bg-primary-900 lg:top-0'>
|
||||
<Tabs
|
||||
items={[
|
||||
{
|
||||
to: '/',
|
||||
name: '/',
|
||||
text: <FormattedMessage id='tabs_bar.follows' defaultMessage='Follows' />,
|
||||
notification: notifications.home,
|
||||
},
|
||||
{
|
||||
to: '/timeline/local',
|
||||
name: '/timeline/local',
|
||||
text: <div className='block max-w-xs truncate'>{instance.title}</div>,
|
||||
notification: notifications.instance,
|
||||
},
|
||||
]}
|
||||
activeItem={match.path}
|
||||
/>
|
||||
</PullToRefresh>
|
||||
</Column>
|
||||
</div>
|
||||
|
||||
<Suspense fallback={<div className='p-4 text-center'><FormattedMessage id='loading_indicator.label' defaultMessage='Loading…' /></div>}>
|
||||
<Switch>
|
||||
<Route path='/' exact component={FollowsTimeline} />
|
||||
<Route path='/timeline/local' exact component={CommunityTimeline} />
|
||||
</Switch>
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -128,7 +128,8 @@ const SoapboxConfig: React.FC = () => {
|
|||
if (file) {
|
||||
data.append('file', file);
|
||||
|
||||
dispatch(uploadMedia(data)).then(({ data }: any) => {
|
||||
dispatch(uploadMedia(data)).then(async (response) => {
|
||||
const data = await response.json();
|
||||
handleChange(path, () => data.url)(e);
|
||||
}).catch(console.error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
|||
|
||||
import { revokeName, setBadges as saveBadges } from 'soapbox/actions/admin.ts';
|
||||
import { deactivateUserModal, deleteUserModal } from 'soapbox/actions/moderation.tsx';
|
||||
import { HTTPError } from 'soapbox/api/HTTPError.ts';
|
||||
import { useSuggest, useVerify } from 'soapbox/api/hooks/admin/index.ts';
|
||||
import { useAccount } from 'soapbox/api/hooks/index.ts';
|
||||
import Account from 'soapbox/components/account.tsx';
|
||||
|
|
@ -100,7 +101,11 @@ const AccountModerationModal: React.FC<IAccountModerationModal> = ({ onClose, ac
|
|||
const handleRevokeName = () => {
|
||||
dispatch(revokeName(account.id))
|
||||
.then(() => toast.success(intl.formatMessage(messages.revokedName)))
|
||||
.catch(() => {});
|
||||
.catch((error) => {
|
||||
if (error instanceof HTTPError) {
|
||||
toast.showAlertForError(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
|
|
|
|||
|
|
@ -34,13 +34,13 @@ const StreakModal: React.FC<IStreakModal> = ({ onClose }) => {
|
|||
<Modal
|
||||
title={
|
||||
<HStack alignItems='center' justifyContent='center' space={1} className='my-6 -mr-8'>
|
||||
<Text weight='bold' size='2xl' className='text-black'>
|
||||
<FormattedMessage id='streak_modal.title' defaultMessage="You've unlocked a" />
|
||||
<Text weight='bold' size='xl' className='text-black'>
|
||||
<FormattedMessage id='streak_modal.title' defaultMessage='You unlocked a' />
|
||||
</Text>
|
||||
<Text theme='primary'>
|
||||
<Icon src={flameIcon} className='size-6' />
|
||||
</Text>
|
||||
<Text weight='bold' size='2xl' className='text-black'>
|
||||
<Text weight='bold' size='xl' className='text-black'>
|
||||
<FormattedMessage id='streak_modal.sub' defaultMessage='streak!' />
|
||||
</Text>
|
||||
</HStack>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { FormattedMessage } from 'react-intl';
|
|||
|
||||
import { openModal } from 'soapbox/actions/modals.ts';
|
||||
import Button from 'soapbox/components/ui/button.tsx';
|
||||
import HStack from 'soapbox/components/ui/hstack.tsx';
|
||||
import Stack from 'soapbox/components/ui/stack.tsx';
|
||||
import Text from 'soapbox/components/ui/text.tsx';
|
||||
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
||||
|
|
@ -26,19 +27,32 @@ const SignUpPanel = () => {
|
|||
<FormattedMessage id='signup_panel.title' defaultMessage='New to {site_title}?' values={{ site_title: instance.title }} />
|
||||
</Text>
|
||||
|
||||
<Text theme='muted' size='sm'>
|
||||
<Text size='sm' theme='muted'>
|
||||
<FormattedMessage id='signup_panel.subtitle' defaultMessage="Sign up now to discuss what's happening." />
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Button
|
||||
theme='primary'
|
||||
onClick={nostrSignup ? () => dispatch(openModal('NOSTR_SIGNUP')) : undefined}
|
||||
to={nostrSignup ? undefined : '/signup'}
|
||||
block
|
||||
>
|
||||
<FormattedMessage id='account.register' defaultMessage='Sign up' />
|
||||
</Button>
|
||||
<HStack space={2}>
|
||||
<Button
|
||||
theme='tertiary'
|
||||
onClick={nostrSignup ? () => dispatch(openModal('NOSTR_LOGIN')) : undefined}
|
||||
to={nostrSignup ? undefined : '/login'}
|
||||
block
|
||||
>
|
||||
<FormattedMessage id='account.login' defaultMessage='Log in' />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
theme='primary'
|
||||
onClick={nostrSignup ? () => dispatch(openModal('NOSTR_SIGNUP')) : undefined}
|
||||
to={nostrSignup ? undefined : '/signup'}
|
||||
block
|
||||
>
|
||||
<FormattedMessage id='account.register' defaultMessage='Sign up' />
|
||||
</Button>
|
||||
|
||||
</HStack>
|
||||
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { expandNotifications } from 'soapbox/actions/notifications.ts';
|
|||
import { registerPushNotifications } from 'soapbox/actions/push-notifications/registerer.ts';
|
||||
import { fetchScheduledStatuses } from 'soapbox/actions/scheduled-statuses.ts';
|
||||
import { fetchSuggestionsForTimeline } from 'soapbox/actions/suggestions.ts';
|
||||
import { expandHomeTimeline } from 'soapbox/actions/timelines.ts';
|
||||
import { expandFollowsTimeline } from 'soapbox/actions/timelines.ts';
|
||||
import { useUserStream } from 'soapbox/api/hooks/index.ts';
|
||||
import { useCustomEmojis } from 'soapbox/api/hooks/useCustomEmojis.ts';
|
||||
import SidebarNavigation from 'soapbox/components/sidebar-navigation.tsx';
|
||||
|
|
@ -47,7 +47,6 @@ import FloatingActionButton from './components/floating-action-button.tsx';
|
|||
import Navbar from './components/navbar.tsx';
|
||||
import {
|
||||
Status,
|
||||
CommunityTimeline,
|
||||
PublicTimeline,
|
||||
RemoteTimeline,
|
||||
AccountTimeline,
|
||||
|
|
@ -196,7 +195,7 @@ const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = ({ children }) =>
|
|||
NOTE: we cannot nest routes in a fragment
|
||||
https://stackoverflow.com/a/68637108
|
||||
*/}
|
||||
{features.federating && <WrappedRoute path='/timeline/local' exact page={HomePage} component={CommunityTimeline} content={children} publicRoute />}
|
||||
{features.federating && <WrappedRoute path='/timeline/local' exact page={HomePage} component={HomeTimeline} content={children} publicRoute />}
|
||||
{features.federating && <WrappedRoute path='/timeline/global' exact page={HomePage} component={PublicTimeline} content={children} publicRoute />}
|
||||
{features.federating && <WrappedRoute path='/timeline/:instance' exact page={RemoteInstancePage} component={RemoteTimeline} content={children} publicRoute />}
|
||||
|
||||
|
|
@ -426,7 +425,7 @@ const UI: React.FC<IUI> = ({ children }) => {
|
|||
const loadAccountData = () => {
|
||||
if (!account) return;
|
||||
|
||||
dispatch(expandHomeTimeline({}, () => {
|
||||
dispatch(expandFollowsTimeline({}, () => {
|
||||
dispatch(fetchSuggestionsForTimeline());
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ export const LandingTimeline = lazy(() => import('soapbox/features/landing-timel
|
|||
export const HomeTimeline = lazy(() => import('soapbox/features/home-timeline/index.tsx'));
|
||||
export const PublicTimeline = lazy(() => import('soapbox/features/public-timeline/index.tsx'));
|
||||
export const RemoteTimeline = lazy(() => import('soapbox/features/remote-timeline/index.tsx'));
|
||||
export const CommunityTimeline = lazy(() => import('soapbox/features/community-timeline/index.tsx'));
|
||||
export const HashtagTimeline = lazy(() => import('soapbox/features/hashtag-timeline/index.tsx'));
|
||||
export const DirectTimeline = lazy(() => import('soapbox/features/direct-timeline/index.tsx'));
|
||||
export const Conversations = lazy(() => import('soapbox/features/conversations/index.tsx'));
|
||||
|
|
@ -185,3 +184,5 @@ export const MyWallet = lazy(() => import('soapbox/features/my-wallet/index.tsx'
|
|||
export const MyWalletRelays = lazy(() => import('soapbox/features/my-wallet/components/wallet-relays.tsx'));
|
||||
export const MyWalletMints = lazy(() => import('soapbox/features/my-wallet/components/wallet-mints.tsx'));
|
||||
export const StreakModal = lazy(() => import('soapbox/features/ui/components/modals/streak-modal.tsx'));
|
||||
export const FollowsTimeline = lazy(() => import('soapbox/features/home-timeline/follows-timeline.tsx'));
|
||||
export const CommunityTimeline = lazy(() => import('soapbox/features/home-timeline/community-timeline.tsx'));
|
||||
|
|
@ -133,6 +133,7 @@ const Video: React.FC<IVideo> = ({
|
|||
aspectRatio = 16 / 9,
|
||||
link,
|
||||
blurhash,
|
||||
preview,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const isMobile = useIsMobile();
|
||||
|
|
@ -571,6 +572,7 @@ const Video: React.FC<IVideo> = ({
|
|||
onProgress={handleProgress}
|
||||
onVolumeChange={handleVolumeChange}
|
||||
muted={muted}
|
||||
poster={preview}
|
||||
/>
|
||||
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -1462,6 +1462,7 @@
|
|||
"sw.url": "رابط الإسكربت",
|
||||
"tabs_bar.all": "الكل",
|
||||
"tabs_bar.dashboard": "لوحة التحكم",
|
||||
"tabs_bar.global": "عالمي",
|
||||
"tabs_bar.groups": "المجموعات",
|
||||
"tabs_bar.home": "الرئيسية",
|
||||
"tabs_bar.more": "المزيد",
|
||||
|
|
|
|||
|
|
@ -1190,7 +1190,7 @@
|
|||
"nostr_signup.captcha_check_button.checking": "Checking…",
|
||||
"nostr_signup.captcha_instruction": "Complete the puzzle by dragging the puzzle piece to the correct position.",
|
||||
"nostr_signup.captcha_message.error": "It seems an error has occurred. Please try again. If the problem persists, please contact us.",
|
||||
"nostr_signup.captcha_message.misbehaving": "It looks like we're experiencing issues with the {instance}. Please try again. If the error persists, try again later.",
|
||||
"nostr_signup.captcha_message.expired": "The captcha has expired. Please start over.",
|
||||
"nostr_signup.captcha_message.sucess": "Incredible! You've successfully completed the captcha.",
|
||||
"nostr_signup.captcha_message.wrong": "Oops! It looks like your captcha response was incorrect. Please try again.",
|
||||
"nostr_signup.captcha_reset_button": "Reset puzzle",
|
||||
|
|
@ -1614,7 +1614,7 @@
|
|||
"streak_modal.message": "Post every day to keep it going.",
|
||||
"streak_modal.streak_count": "1",
|
||||
"streak_modal.sub": "streak!",
|
||||
"streak_modal.title": "You've unlocked a",
|
||||
"streak_modal.title": "You unlocked a",
|
||||
"streamfield.add": "Add",
|
||||
"streamfield.remove": "Remove",
|
||||
"suggestions.dismiss": "Dismiss suggestion",
|
||||
|
|
@ -1626,8 +1626,8 @@
|
|||
"sw.state.waiting": "Waiting",
|
||||
"sw.status": "Status",
|
||||
"sw.url": "Script URL",
|
||||
"tabs_bar.all": "All",
|
||||
"tabs_bar.dashboard": "Dashboard",
|
||||
"tabs_bar.follows": "Follows",
|
||||
"tabs_bar.global": "Global",
|
||||
"tabs_bar.groups": "Groups",
|
||||
"tabs_bar.home": "Home",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import clsx from 'clsx';
|
||||
import { useRef } from 'react';
|
||||
import { FormattedMessage, useIntl } from 'react-intl';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
|
||||
import { uploadCompose } from 'soapbox/actions/compose.ts';
|
||||
|
|
@ -9,7 +8,6 @@ import Avatar from 'soapbox/components/ui/avatar.tsx';
|
|||
import { Card, CardBody } from 'soapbox/components/ui/card.tsx';
|
||||
import HStack from 'soapbox/components/ui/hstack.tsx';
|
||||
import Layout from 'soapbox/components/ui/layout.tsx';
|
||||
import Tabs from 'soapbox/components/ui/tabs.tsx';
|
||||
import LinkFooter from 'soapbox/features/ui/components/link-footer.tsx';
|
||||
import {
|
||||
WhoToFollowPanel,
|
||||
|
|
@ -27,11 +25,9 @@ import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
|||
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
||||
import { useDraggedFiles } from 'soapbox/hooks/useDraggedFiles.ts';
|
||||
import { useFeatures } from 'soapbox/hooks/useFeatures.ts';
|
||||
import { useInstance } from 'soapbox/hooks/useInstance.ts';
|
||||
import { useIsMobile } from 'soapbox/hooks/useIsMobile.ts';
|
||||
import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts';
|
||||
import { useSoapboxConfig } from 'soapbox/hooks/useSoapboxConfig.ts';
|
||||
import { RootState } from 'soapbox/store.ts';
|
||||
|
||||
import ComposeForm from '../features/compose/components/compose-form.tsx';
|
||||
|
||||
|
|
@ -43,17 +39,16 @@ const HomePage: React.FC<IHomePage> = ({ children }) => {
|
|||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const { pathname } = useLocation();
|
||||
const notifications = useSelector((state: RootState) => state.notificationsTab);
|
||||
|
||||
const me = useAppSelector(state => state.me);
|
||||
const { account } = useOwnAccount();
|
||||
const features = useFeatures();
|
||||
const soapboxConfig = useSoapboxConfig();
|
||||
const { instance } = useInstance();
|
||||
|
||||
const composeId = 'home';
|
||||
const composeBlock = useRef<HTMLDivElement>(null);
|
||||
const isMobile = useIsMobile();
|
||||
const isGlobalPage = pathname === '/timeline/global';
|
||||
|
||||
const hasPatron = soapboxConfig.extensions.getIn(['patron', 'enabled']) === true;
|
||||
const hasCrypto = typeof soapboxConfig.cryptoAddresses.getIn([0, 'ticker']) === 'string';
|
||||
|
|
@ -67,7 +62,7 @@ const HomePage: React.FC<IHomePage> = ({ children }) => {
|
|||
const avatar = account ? account.avatar : '';
|
||||
|
||||
const renderSuggestions = () => {
|
||||
if (features.suggestionsLocal && pathname !== '/timeline/global') {
|
||||
if (features.suggestionsLocal && !isGlobalPage) {
|
||||
return <LatestAccountsPanel limit={3} />;
|
||||
} else if (features.suggestions) {
|
||||
return <WhoToFollowPanel limit={3} />;
|
||||
|
|
@ -105,15 +100,6 @@ const HomePage: React.FC<IHomePage> = ({ children }) => {
|
|||
</Card>
|
||||
)}
|
||||
|
||||
<div className='sticky top-12 z-20 bg-white/90 backdrop-blur black:bg-black/90 dark:bg-primary-900/90 lg:top-0'>
|
||||
<Tabs
|
||||
items={[
|
||||
{ name: 'home', text: <FormattedMessage id='tabs_bar.home' defaultMessage='Home' />, to: '/', notification: notifications.home },
|
||||
{ name: 'local', text: <div className='block max-w-xs truncate'>{instance.domain}</div>, to: '/timeline/local', notification: notifications.instance },
|
||||
]}
|
||||
activeItem={pathname === '/timeline/local' ? 'local' : 'home'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{children}
|
||||
|
||||
|
|
|
|||
Ładowanie…
Reference in New Issue