Merge branch 'main' into update-notification-icon
This commit is contained in:
commit
15d96e8fdf
|
@ -80,7 +80,7 @@ const fetchSuggestions = (params: Record<string, any> = { limit: 50 }) =>
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchSuggestionsForTimeline = () => (dispatch: AppDispatch, _getState: () => RootState) => {
|
const fetchSuggestionsForTimeline = () => (dispatch: AppDispatch, _getState: () => RootState) => {
|
||||||
dispatch(fetchSuggestions({ limit: 20 }))?.then(() => dispatch(insertSuggestionsIntoTimeline()));
|
dispatch(insertSuggestionsIntoTimeline());
|
||||||
};
|
};
|
||||||
|
|
||||||
const dismissSuggestion = (accountId: string) =>
|
const dismissSuggestion = (accountId: string) =>
|
||||||
|
|
|
@ -20,6 +20,7 @@ import PlaceholderHashtag from 'soapbox/features/placeholder/components/placehol
|
||||||
import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder-status.tsx';
|
import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder-status.tsx';
|
||||||
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
||||||
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
||||||
|
import { useSuggestions } from 'soapbox/queries/suggestions.ts';
|
||||||
|
|
||||||
import type { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
import type { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||||
import type { VirtuosoHandle } from 'react-virtuoso';
|
import type { VirtuosoHandle } from 'react-virtuoso';
|
||||||
|
@ -37,9 +38,10 @@ const SearchResults = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const { data: suggestions } = useSuggestions();
|
||||||
|
|
||||||
const value = useAppSelector((state) => state.search.submittedValue);
|
const value = useAppSelector((state) => state.search.submittedValue);
|
||||||
const results = useAppSelector((state) => state.search.results);
|
const results = useAppSelector((state) => state.search.results);
|
||||||
const suggestions = useAppSelector((state) => state.suggestions.items);
|
|
||||||
const trendingStatuses = useAppSelector((state) => state.trending_statuses.items);
|
const trendingStatuses = useAppSelector((state) => state.trending_statuses.items);
|
||||||
const nextTrendingStatuses = useAppSelector((state) => state.trending_statuses.next);
|
const nextTrendingStatuses = useAppSelector((state) => state.trending_statuses.next);
|
||||||
const trends = useAppSelector((state) => state.trends.items);
|
const trends = useAppSelector((state) => state.trends.items);
|
||||||
|
@ -133,7 +135,7 @@ const SearchResults = () => {
|
||||||
|
|
||||||
if (results.accounts && results.accounts.size > 0) {
|
if (results.accounts && results.accounts.size > 0) {
|
||||||
searchResults = results.accounts.map(accountId => <AccountContainer key={accountId} id={accountId} />);
|
searchResults = results.accounts.map(accountId => <AccountContainer key={accountId} id={accountId} />);
|
||||||
} else if (!submitted && suggestions && !suggestions.isEmpty()) {
|
} else if (!submitted && suggestions.length) {
|
||||||
searchResults = suggestions.map(suggestion => <AccountContainer key={suggestion.account} id={suggestion.account} />);
|
searchResults = suggestions.map(suggestion => <AccountContainer key={suggestion.account} id={suggestion.account} />);
|
||||||
} else if (loaded) {
|
} else if (loaded) {
|
||||||
noResultsMessage = (
|
noResultsMessage = (
|
||||||
|
@ -196,7 +198,7 @@ const SearchResults = () => {
|
||||||
|
|
||||||
if (results.hashtags && results.hashtags.size > 0) {
|
if (results.hashtags && results.hashtags.size > 0) {
|
||||||
searchResults = results.hashtags.map(hashtag => <Hashtag key={hashtag.name} hashtag={hashtag} />);
|
searchResults = results.hashtags.map(hashtag => <Hashtag key={hashtag.name} hashtag={hashtag} />);
|
||||||
} else if (!submitted && suggestions && !suggestions.isEmpty()) {
|
} else if (!submitted && !trends.isEmpty()) {
|
||||||
searchResults = trends.map(hashtag => <Hashtag key={hashtag.name} hashtag={hashtag} />);
|
searchResults = trends.map(hashtag => <Hashtag key={hashtag.name} hashtag={hashtag} />);
|
||||||
} else if (loaded) {
|
} else if (loaded) {
|
||||||
noResultsMessage = (
|
noResultsMessage = (
|
||||||
|
@ -235,7 +237,7 @@ const SearchResults = () => {
|
||||||
key={selectedFilter}
|
key={selectedFilter}
|
||||||
scrollKey={`${selectedFilter}:${value}`}
|
scrollKey={`${selectedFilter}:${value}`}
|
||||||
isLoading={submitted && !loaded}
|
isLoading={submitted && !loaded}
|
||||||
showLoading={submitted && !loaded && searchResults?.isEmpty()}
|
showLoading={submitted && !loaded && (Array.isArray(searchResults) ? !searchResults.length : searchResults?.isEmpty())}
|
||||||
hasMore={hasMore}
|
hasMore={hasMore}
|
||||||
onLoadMore={handleLoadMore}
|
onLoadMore={handleLoadMore}
|
||||||
placeholderComponent={placeholderComponent}
|
placeholderComponent={placeholderComponent}
|
||||||
|
|
|
@ -52,15 +52,18 @@ const EditIdentity: React.FC<IEditIdentity> = () => {
|
||||||
|
|
||||||
const [username, setUsername] = useState<string>('');
|
const [username, setUsername] = useState<string>('');
|
||||||
const [reason, setReason] = useState<string>('');
|
const [reason, setReason] = useState<string>('');
|
||||||
|
const [submitted, setSubmitted] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!submitted) return;
|
||||||
|
|
||||||
const dismissed = new Set(dismissedSettingsNotifications);
|
const dismissed = new Set(dismissedSettingsNotifications);
|
||||||
|
|
||||||
if (!dismissed.has('needsNip05')) {
|
if (!dismissed.has('needsNip05')) {
|
||||||
dismissed.add('needsNip05');
|
dismissed.add('needsNip05');
|
||||||
dispatch(changeSetting(['dismissedSettingsNotifications'], [...dismissed]));
|
dispatch(changeSetting(['dismissedSettingsNotifications'], [...dismissed]));
|
||||||
}
|
}
|
||||||
}, []);
|
}, [submitted]);
|
||||||
|
|
||||||
if (!account) return null;
|
if (!account) return null;
|
||||||
|
|
||||||
|
@ -85,6 +88,7 @@ const EditIdentity: React.FC<IEditIdentity> = () => {
|
||||||
});
|
});
|
||||||
setUsername('');
|
setUsername('');
|
||||||
setReason('');
|
setReason('');
|
||||||
|
setSubmitted(true);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,7 +7,8 @@ import HStack from 'soapbox/components/ui/hstack.tsx';
|
||||||
import Stack from 'soapbox/components/ui/stack.tsx';
|
import Stack from 'soapbox/components/ui/stack.tsx';
|
||||||
import Text from 'soapbox/components/ui/text.tsx';
|
import Text from 'soapbox/components/ui/text.tsx';
|
||||||
import VerificationBadge from 'soapbox/components/verification-badge.tsx';
|
import VerificationBadge from 'soapbox/components/verification-badge.tsx';
|
||||||
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
import { useFeatures } from 'soapbox/hooks/useFeatures.ts';
|
||||||
|
import { useSuggestions } from 'soapbox/queries/suggestions.ts';
|
||||||
import { emojifyText } from 'soapbox/utils/emojify.tsx';
|
import { emojifyText } from 'soapbox/utils/emojify.tsx';
|
||||||
|
|
||||||
import ActionButton from '../ui/components/action-button.tsx';
|
import ActionButton from '../ui/components/action-button.tsx';
|
||||||
|
@ -66,18 +67,19 @@ const SuggestionItem: React.FC<ISuggestionItem> = ({ accountId }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IFeedSuggesetions {
|
interface IFeedSuggestions {
|
||||||
statusId: string;
|
statusId: string;
|
||||||
onMoveUp?: (statusId: string, featured?: boolean) => void;
|
onMoveUp?: (statusId: string, featured?: boolean) => void;
|
||||||
onMoveDown?: (statusId: string, featured?: boolean) => void;
|
onMoveDown?: (statusId: string, featured?: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FeedSuggestions: React.FC<IFeedSuggesetions> = ({ statusId, onMoveUp, onMoveDown }) => {
|
const FeedSuggestions: React.FC<IFeedSuggestions> = ({ statusId, onMoveUp, onMoveDown }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const suggestedProfiles = useAppSelector((state) => state.suggestions.items);
|
const features = useFeatures();
|
||||||
const isLoading = useAppSelector((state) => state.suggestions.isLoading);
|
|
||||||
|
|
||||||
if (!isLoading && suggestedProfiles.size === 0) return null;
|
const { data: suggestedProfiles, isLoading } = useSuggestions({ local: features.suggestionsLocal });
|
||||||
|
|
||||||
|
if (!isLoading && suggestedProfiles.length === 0) return null;
|
||||||
|
|
||||||
const handleHotkeyMoveUp = (e?: KeyboardEvent): void => {
|
const handleHotkeyMoveUp = (e?: KeyboardEvent): void => {
|
||||||
if (onMoveUp) {
|
if (onMoveUp) {
|
||||||
|
|
|
@ -1,41 +1,33 @@
|
||||||
import { debounce } from 'es-toolkit';
|
import { debounce } from 'es-toolkit';
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { fetchSuggestions } from 'soapbox/actions/suggestions.ts';
|
|
||||||
import ScrollableList from 'soapbox/components/scrollable-list.tsx';
|
import ScrollableList from 'soapbox/components/scrollable-list.tsx';
|
||||||
import { Column } from 'soapbox/components/ui/column.tsx';
|
import { Column } from 'soapbox/components/ui/column.tsx';
|
||||||
import Stack from 'soapbox/components/ui/stack.tsx';
|
import Stack from 'soapbox/components/ui/stack.tsx';
|
||||||
import Text from 'soapbox/components/ui/text.tsx';
|
import Text from 'soapbox/components/ui/text.tsx';
|
||||||
import AccountContainer from 'soapbox/containers/account-container.tsx';
|
import AccountContainer from 'soapbox/containers/account-container.tsx';
|
||||||
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
import { useSuggestions } from 'soapbox/queries/suggestions.ts';
|
||||||
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
heading: { id: 'follow_recommendations.heading', defaultMessage: 'Suggested Profiles' },
|
heading: { id: 'follow_recommendations.heading', defaultMessage: 'Suggested Profiles' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const FollowRecommendations: React.FC = () => {
|
interface IFollowRecommendations {
|
||||||
const dispatch = useAppDispatch();
|
local?: boolean;
|
||||||
const intl = useIntl();
|
|
||||||
|
|
||||||
const suggestions = useAppSelector((state) => state.suggestions.items);
|
|
||||||
const hasMore = useAppSelector((state) => !!state.suggestions.next);
|
|
||||||
const isLoading = useAppSelector((state) => state.suggestions.isLoading);
|
|
||||||
|
|
||||||
const handleLoadMore = debounce(() => {
|
|
||||||
if (isLoading) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return dispatch(fetchSuggestions({ limit: 20 }));
|
const FollowRecommendations: React.FC<IFollowRecommendations> = ({ local = false }) => {
|
||||||
}, 300);
|
const intl = useIntl();
|
||||||
|
|
||||||
useEffect(() => {
|
const { data: suggestions, fetchNextPage, hasNextPage, isLoading, isFetchingNextPage } = useSuggestions({ local });
|
||||||
dispatch(fetchSuggestions({ limit: 20 }));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (suggestions.size === 0 && !isLoading) {
|
const handleLoadMore = debounce(() => {
|
||||||
|
if (hasNextPage && !isFetchingNextPage) {
|
||||||
|
fetchNextPage();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
if (suggestions.length === 0 && !isLoading) {
|
||||||
return (
|
return (
|
||||||
<Column label={intl.formatMessage(messages.heading)}>
|
<Column label={intl.formatMessage(messages.heading)}>
|
||||||
<Text align='center'>
|
<Text align='center'>
|
||||||
|
@ -52,7 +44,7 @@ const FollowRecommendations: React.FC = () => {
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
scrollKey='suggestions'
|
scrollKey='suggestions'
|
||||||
onLoadMore={handleLoadMore}
|
onLoadMore={handleLoadMore}
|
||||||
hasMore={hasMore}
|
hasMore={hasNextPage}
|
||||||
itemClassName='pb-4'
|
itemClassName='pb-4'
|
||||||
>
|
>
|
||||||
{suggestions.map((suggestion) => (
|
{suggestions.map((suggestion) => (
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
import xIcon from '@tabler/icons/outline/x.svg';
|
||||||
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import Text from 'soapbox/components/ui/text.tsx';
|
||||||
|
import Widget from 'soapbox/components/ui/widget.tsx';
|
||||||
|
import AccountContainer from 'soapbox/containers/account-container.tsx';
|
||||||
|
import PlaceholderSidebarSuggestions from 'soapbox/features/placeholder/components/placeholder-sidebar-suggestions.tsx';
|
||||||
|
import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts';
|
||||||
|
import { useDismissSuggestion, useSuggestions } from 'soapbox/queries/suggestions.ts';
|
||||||
|
|
||||||
|
import type { Account as AccountEntity } from 'soapbox/types/entities.ts';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
dismissSuggestion: { id: 'suggestions.dismiss', defaultMessage: 'Dismiss suggestion' },
|
||||||
|
});
|
||||||
|
|
||||||
|
interface ILatestAccountsPanel {
|
||||||
|
limit: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LatestAccountsPanel: React.FC<ILatestAccountsPanel> = ({ limit }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const { account } = useOwnAccount();
|
||||||
|
const { data: suggestions, isFetching } = useSuggestions({ local: true });
|
||||||
|
const dismissSuggestion = useDismissSuggestion();
|
||||||
|
|
||||||
|
const suggestionsToRender = suggestions.slice(0, limit);
|
||||||
|
|
||||||
|
const handleDismiss = (account: AccountEntity) => {
|
||||||
|
dismissSuggestion.mutate(account.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isFetching && !suggestions.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Widget
|
||||||
|
title={<FormattedMessage id='latest_accounts.title' defaultMessage='Latest Accounts' />}
|
||||||
|
action={
|
||||||
|
<Link className='text-right' to='/suggestions/local'>
|
||||||
|
<Text tag='span' theme='primary' size='sm' className='hover:underline'>
|
||||||
|
<FormattedMessage id='feed_suggestions.view_all' defaultMessage='View all' />
|
||||||
|
</Text>
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isFetching ? (
|
||||||
|
<PlaceholderSidebarSuggestions limit={limit} />
|
||||||
|
) : (
|
||||||
|
suggestionsToRender.map((suggestion: any) => (
|
||||||
|
<AccountContainer
|
||||||
|
key={suggestion.account}
|
||||||
|
// @ts-ignore: TS thinks `id` is passed to <Account>, but it isn't
|
||||||
|
id={suggestion.account}
|
||||||
|
actionIcon={xIcon}
|
||||||
|
actionTitle={intl.formatMessage(messages.dismissSuggestion)}
|
||||||
|
onActionClick={account ? handleDismiss : undefined}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</Widget>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LatestAccountsPanel;
|
|
@ -262,8 +262,9 @@ const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = ({ children }) =>
|
||||||
<WrappedRoute path='/notifications' page={DefaultPage} component={Notifications} content={children} />
|
<WrappedRoute path='/notifications' page={DefaultPage} component={Notifications} content={children} />
|
||||||
|
|
||||||
<WrappedRoute path='/search' page={SearchPage} component={Search} content={children} publicRoute />
|
<WrappedRoute path='/search' page={SearchPage} component={Search} content={children} publicRoute />
|
||||||
{features.suggestions && <WrappedRoute path='/suggestions' publicRoute page={DefaultPage} component={FollowRecommendations} content={children} />}
|
{features.suggestionsLocal && <WrappedRoute path='/suggestions/local' publicRoute page={DefaultPage} component={FollowRecommendations} content={children} componentParams={{ local: true }} />}
|
||||||
{features.profileDirectory && <WrappedRoute path='/directory' publicRoute page={DefaultPage} component={Directory} content={children} />}
|
{features.suggestions && <WrappedRoute path='/suggestions' exact publicRoute page={DefaultPage} component={FollowRecommendations} content={children} />}
|
||||||
|
{features.profileDirectory && <WrappedRoute path='/directory' exact publicRoute page={DefaultPage} component={Directory} content={children} />}
|
||||||
{features.events && <WrappedRoute path='/events' page={EventsPage} component={Events} content={children} />}
|
{features.events && <WrappedRoute path='/events' page={EventsPage} component={Events} content={children} />}
|
||||||
|
|
||||||
{features.chats && <WrappedRoute path='/chats' exact page={ChatsPage} component={ChatIndex} content={children} />}
|
{features.chats && <WrappedRoute path='/chats' exact page={ChatsPage} component={ChatIndex} content={children} />}
|
||||||
|
|
|
@ -93,7 +93,7 @@ export const ProfileFieldsPanel = lazy(() => import('soapbox/features/ui/compone
|
||||||
export const PinnedAccountsPanel = lazy(() => import('soapbox/features/ui/components/pinned-accounts-panel.tsx'));
|
export const PinnedAccountsPanel = lazy(() => import('soapbox/features/ui/components/pinned-accounts-panel.tsx'));
|
||||||
export const InstanceInfoPanel = lazy(() => import('soapbox/features/ui/components/instance-info-panel.tsx'));
|
export const InstanceInfoPanel = lazy(() => import('soapbox/features/ui/components/instance-info-panel.tsx'));
|
||||||
export const InstanceModerationPanel = lazy(() => import('soapbox/features/ui/components/instance-moderation-panel.tsx'));
|
export const InstanceModerationPanel = lazy(() => import('soapbox/features/ui/components/instance-moderation-panel.tsx'));
|
||||||
export const LatestAccountsPanel = lazy(() => import('soapbox/features/admin/components/latest-accounts-panel.tsx'));
|
export const LatestAdminAccountsPanel = lazy(() => import('soapbox/features/admin/components/latest-accounts-panel.tsx'));
|
||||||
export const SidebarMenu = lazy(() => import('soapbox/components/sidebar-menu.tsx'));
|
export const SidebarMenu = lazy(() => import('soapbox/components/sidebar-menu.tsx'));
|
||||||
export const ModalContainer = lazy(() => import('soapbox/features/ui/containers/modal-container.ts'));
|
export const ModalContainer = lazy(() => import('soapbox/features/ui/containers/modal-container.ts'));
|
||||||
export const ProfileHoverCard = lazy(() => import('soapbox/components/profile-hover-card.tsx'));
|
export const ProfileHoverCard = lazy(() => import('soapbox/components/profile-hover-card.tsx'));
|
||||||
|
@ -109,6 +109,7 @@ export const FederationRestrictions = lazy(() => import('soapbox/features/federa
|
||||||
export const Aliases = lazy(() => import('soapbox/features/aliases/index.tsx'));
|
export const Aliases = lazy(() => import('soapbox/features/aliases/index.tsx'));
|
||||||
export const Migration = lazy(() => import('soapbox/features/migration/index.tsx'));
|
export const Migration = lazy(() => import('soapbox/features/migration/index.tsx'));
|
||||||
export const WhoToFollowPanel = lazy(() => import('soapbox/features/ui/components/who-to-follow-panel.tsx'));
|
export const WhoToFollowPanel = lazy(() => import('soapbox/features/ui/components/who-to-follow-panel.tsx'));
|
||||||
|
export const LatestAccountsPanel = lazy(() => import('soapbox/features/ui/components/latest-accounts-panel.tsx'));
|
||||||
export const FollowRecommendations = lazy(() => import('soapbox/features/follow-recommendations/index.tsx'));
|
export const FollowRecommendations = lazy(() => import('soapbox/features/follow-recommendations/index.tsx'));
|
||||||
export const Directory = lazy(() => import('soapbox/features/directory/index.tsx'));
|
export const Directory = lazy(() => import('soapbox/features/directory/index.tsx'));
|
||||||
export const RegisterInvite = lazy(() => import('soapbox/features/register-invite/index.tsx'));
|
export const RegisterInvite = lazy(() => import('soapbox/features/register-invite/index.tsx'));
|
||||||
|
|
|
@ -191,10 +191,12 @@ const Video: React.FC<IVideo> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTimeUpdate = () => {
|
const handleTimeUpdate = () => {
|
||||||
if (video.current) {
|
if (!video.current) return;
|
||||||
setCurrentTime(Math.floor(video.current.currentTime));
|
|
||||||
setDuration(Math.floor(video.current.duration));
|
const { duration, currentTime } = video.current;
|
||||||
}
|
|
||||||
|
setCurrentTime(Math.floor(currentTime));
|
||||||
|
setDuration(Number.isNaN(duration) || (duration === Infinity) ? 0 : Math.floor(duration));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleVolumeMouseDown: React.MouseEventHandler = e => {
|
const handleVolumeMouseDown: React.MouseEventHandler = e => {
|
||||||
|
@ -480,7 +482,11 @@ const Video: React.FC<IVideo> = ({
|
||||||
const playerStyle: React.CSSProperties = {};
|
const playerStyle: React.CSSProperties = {};
|
||||||
|
|
||||||
const startTimeout = () => {
|
const startTimeout = () => {
|
||||||
timeoutRef.current = setTimeout(() => setHovered(false), 1000);
|
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||||
|
timeoutRef.current = setTimeout(() => {
|
||||||
|
setHovered(false);
|
||||||
|
timeoutRef.current = null;
|
||||||
|
}, 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (inline && containerWidth) {
|
if (inline && containerWidth) {
|
||||||
|
@ -649,9 +655,13 @@ const Video: React.FC<IVideo> = ({
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
<span className='text-sm font-medium text-white/75'>{formatTime(currentTime)}</span>
|
<span className='text-sm font-medium text-white/75'>{formatTime(currentTime)}</span>
|
||||||
|
{duration > 0 && (
|
||||||
|
<>
|
||||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||||
<span className='mx-1.5 inline-block text-sm font-medium text-white/75'>/</span>
|
<span className='mx-1.5 inline-block text-sm font-medium text-white/75'>/</span>
|
||||||
<span className='text-sm font-medium text-white/75'>{formatTime(duration)}</span>
|
<span className='text-sm font-medium text-white/75'>{formatTime(duration)}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{link && (
|
{link && (
|
||||||
|
@ -660,7 +670,6 @@ const Video: React.FC<IVideo> = ({
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex min-w-[30px] flex-auto items-center truncate text-[16px]'>
|
<div className='flex min-w-[30px] flex-auto items-center truncate text-[16px]'>
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
|
|
|
@ -990,6 +990,7 @@
|
||||||
"keyboard_shortcuts.up": "to move up in the list",
|
"keyboard_shortcuts.up": "to move up in the list",
|
||||||
"landing_page_modal.download": "Download",
|
"landing_page_modal.download": "Download",
|
||||||
"landing_page_modal.help_center": "Help Center",
|
"landing_page_modal.help_center": "Help Center",
|
||||||
|
"latest_accounts.title": "Latest Accounts",
|
||||||
"lightbox.close": "Close",
|
"lightbox.close": "Close",
|
||||||
"lightbox.expand": "Expand",
|
"lightbox.expand": "Expand",
|
||||||
"lightbox.minimize": "Minimize",
|
"lightbox.minimize": "Minimize",
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import Layout from 'soapbox/components/ui/layout.tsx';
|
import Layout from 'soapbox/components/ui/layout.tsx';
|
||||||
import {
|
import { LatestAdminAccountsPanel } from 'soapbox/features/ui/util/async-components.ts';
|
||||||
LatestAccountsPanel,
|
|
||||||
} from 'soapbox/features/ui/util/async-components.ts';
|
|
||||||
|
|
||||||
import LinkFooter from '../features/ui/components/link-footer.tsx';
|
import LinkFooter from '../features/ui/components/link-footer.tsx';
|
||||||
|
|
||||||
|
@ -17,7 +15,7 @@ const AdminPage: React.FC<IAdminPage> = ({ children }) => {
|
||||||
</Layout.Main>
|
</Layout.Main>
|
||||||
|
|
||||||
<Layout.Aside>
|
<Layout.Aside>
|
||||||
<LatestAccountsPanel limit={5} />
|
<LatestAdminAccountsPanel limit={5} />
|
||||||
<LinkFooter />
|
<LinkFooter />
|
||||||
</Layout.Aside>
|
</Layout.Aside>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -20,6 +20,7 @@ import {
|
||||||
BirthdayPanel,
|
BirthdayPanel,
|
||||||
CtaBanner,
|
CtaBanner,
|
||||||
AnnouncementsPanel,
|
AnnouncementsPanel,
|
||||||
|
LatestAccountsPanel,
|
||||||
} from 'soapbox/features/ui/util/async-components.ts';
|
} from 'soapbox/features/ui/util/async-components.ts';
|
||||||
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
||||||
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
||||||
|
@ -62,6 +63,14 @@ const HomePage: React.FC<IHomePage> = ({ children }) => {
|
||||||
const acct = account ? account.acct : '';
|
const acct = account ? account.acct : '';
|
||||||
const avatar = account ? account.avatar : '';
|
const avatar = account ? account.avatar : '';
|
||||||
|
|
||||||
|
const renderSuggestions = () => {
|
||||||
|
if (features.suggestionsLocal && pathname !== '/timeline/global') {
|
||||||
|
return <LatestAccountsPanel limit={3} />;
|
||||||
|
} else if (features.suggestions) {
|
||||||
|
return <WhoToFollowPanel limit={3} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Layout.Main className={clsx('space-y-0 dark:divide-gray-800')}>
|
<Layout.Main className={clsx('space-y-0 dark:divide-gray-800')}>
|
||||||
|
@ -120,9 +129,7 @@ const HomePage: React.FC<IHomePage> = ({ children }) => {
|
||||||
{features.trends && (
|
{features.trends && (
|
||||||
<TrendsPanel limit={5} />
|
<TrendsPanel limit={5} />
|
||||||
)}
|
)}
|
||||||
{features.suggestions && (
|
{renderSuggestions()}
|
||||||
<WhoToFollowPanel limit={3} />
|
|
||||||
)}
|
|
||||||
{features.birthdays && (
|
{features.birthdays && (
|
||||||
<BirthdayPanel limit={10} />
|
<BirthdayPanel limit={10} />
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -7,14 +7,15 @@ import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
||||||
|
|
||||||
import { PaginatedResult, removePageItem } from '../utils/queries.ts';
|
import { PaginatedResult, removePageItem } from '../utils/queries.ts';
|
||||||
|
|
||||||
import type { IAccount } from './accounts.ts';
|
import type { Account } from 'soapbox/schemas/account.ts';
|
||||||
|
|
||||||
type Suggestion = {
|
type Suggestion = {
|
||||||
source: 'staff';
|
source: string;
|
||||||
account: IAccount;
|
account: Account;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Result = {
|
type Result = {
|
||||||
|
source: string;
|
||||||
account: string;
|
account: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,47 +25,51 @@ type PageParam = {
|
||||||
|
|
||||||
const SuggestionKeys = {
|
const SuggestionKeys = {
|
||||||
suggestions: ['suggestions'] as const,
|
suggestions: ['suggestions'] as const,
|
||||||
|
localSuggestions: ['suggestions', 'local'] as const,
|
||||||
};
|
};
|
||||||
|
|
||||||
const useSuggestions = () => {
|
interface UseSuggestionsOpts {
|
||||||
|
local?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useSuggestions = (opts?: UseSuggestionsOpts) => {
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const local = opts?.local ?? false;
|
||||||
|
|
||||||
const getV2Suggestions = async (pageParam: PageParam): Promise<PaginatedResult<Result>> => {
|
const getV2Suggestions = async (pageParam?: PageParam): Promise<PaginatedResult<Result>> => {
|
||||||
const endpoint = pageParam?.link || '/api/v2/suggestions';
|
const endpoint = pageParam?.link || (local ? '/api/v2/ditto/suggestions/local' : '/api/v2/suggestions');
|
||||||
const response = await api.get(endpoint);
|
const response = await api.get(endpoint);
|
||||||
const next = response.next();
|
const next = response.next();
|
||||||
const hasMore = !!next;
|
|
||||||
|
|
||||||
const data: Suggestion[] = await response.json();
|
const data: Suggestion[] = await response.json();
|
||||||
const accounts = data.map(({ account }) => account);
|
const accounts = data.map(({ account }) => account);
|
||||||
const accountIds = accounts.map((account) => account.id);
|
const accountIds = accounts.map((account) => account.id);
|
||||||
|
|
||||||
dispatch(importFetchedAccounts(accounts));
|
dispatch(importFetchedAccounts(accounts));
|
||||||
dispatch(fetchRelationships(accountIds));
|
dispatch(fetchRelationships(accountIds));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
result: data.map(x => ({ ...x, account: x.account.id })),
|
result: data.map(x => ({ ...x, account: x.account.id })),
|
||||||
link: next ?? undefined,
|
link: next ?? undefined,
|
||||||
hasMore,
|
hasMore: !!next,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = useInfiniteQuery({
|
const result = useInfiniteQuery({
|
||||||
queryKey: SuggestionKeys.suggestions,
|
queryKey: local ? SuggestionKeys.localSuggestions : SuggestionKeys.suggestions,
|
||||||
queryFn: ({ pageParam }: any) => getV2Suggestions(pageParam),
|
queryFn: ({ pageParam }) => getV2Suggestions(pageParam),
|
||||||
placeholderData: keepPreviousData,
|
placeholderData: keepPreviousData,
|
||||||
initialPageParam: { nextLink: undefined },
|
initialPageParam: undefined as PageParam | undefined,
|
||||||
getNextPageParam: (config) => {
|
getNextPageParam: (config): PageParam | undefined => {
|
||||||
if (config?.hasMore) {
|
if (config?.hasMore) {
|
||||||
return { nextLink: config?.link };
|
return { link: config?.link };
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const data: any = result.data?.pages.reduce<Suggestion[]>(
|
const data = result.data?.pages.reduce<Result[]>(
|
||||||
(prev: any, curr: any) => [...prev, ...curr.result],
|
(prev, curr) => [...prev, ...curr.result],
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -133,4 +138,5 @@ function useOnboardingSuggestions() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export { useOnboardingSuggestions, useSuggestions, useDismissSuggestion };
|
export { useOnboardingSuggestions, useSuggestions, useDismissSuggestion };
|
|
@ -1027,6 +1027,8 @@ const getInstanceFeatures = (instance: InstanceV1 | InstanceV2) => {
|
||||||
features.includes('v2_suggestions'),
|
features.includes('v2_suggestions'),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
|
suggestionsLocal: v.software === DITTO,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Supports V2 suggested accounts.
|
* Supports V2 suggested accounts.
|
||||||
* @see GET /api/v2/suggestions
|
* @see GET /api/v2/suggestions
|
||||||
|
|
Loading…
Reference in New Issue