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,113 +1,48 @@
 | 
			
		|||
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>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default HomeTimeline;
 | 
			
		||||
export default HomeTimeline;
 | 
			
		||||
| 
						 | 
				
			
			@ -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