diff --git a/src/actions/suggestions.ts b/src/actions/suggestions.ts index 2a836bda4..fbc4e5ae0 100644 --- a/src/actions/suggestions.ts +++ b/src/actions/suggestions.ts @@ -80,7 +80,7 @@ const fetchSuggestions = (params: Record = { limit: 50 }) => }; const fetchSuggestionsForTimeline = () => (dispatch: AppDispatch, _getState: () => RootState) => { - dispatch(fetchSuggestions({ limit: 20 }))?.then(() => dispatch(insertSuggestionsIntoTimeline())); + dispatch(insertSuggestionsIntoTimeline()); }; const dismissSuggestion = (accountId: string) => diff --git a/src/features/compose/components/search-results.tsx b/src/features/compose/components/search-results.tsx index 7e7a7cfab..da3c8e6c6 100644 --- a/src/features/compose/components/search-results.tsx +++ b/src/features/compose/components/search-results.tsx @@ -20,6 +20,7 @@ import PlaceholderHashtag from 'soapbox/features/placeholder/components/placehol import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder-status.tsx'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; +import { useSuggestions } from 'soapbox/queries/suggestions.ts'; import type { OrderedSet as ImmutableOrderedSet } from 'immutable'; import type { VirtuosoHandle } from 'react-virtuoso'; @@ -37,9 +38,10 @@ const SearchResults = () => { const intl = useIntl(); const dispatch = useAppDispatch(); + const { data: suggestions } = useSuggestions(); + const value = useAppSelector((state) => state.search.submittedValue); const results = useAppSelector((state) => state.search.results); - const suggestions = useAppSelector((state) => state.suggestions.items); const trendingStatuses = useAppSelector((state) => state.trending_statuses.items); const nextTrendingStatuses = useAppSelector((state) => state.trending_statuses.next); const trends = useAppSelector((state) => state.trends.items); @@ -133,7 +135,7 @@ const SearchResults = () => { if (results.accounts && results.accounts.size > 0) { searchResults = results.accounts.map(accountId => ); - } else if (!submitted && suggestions && !suggestions.isEmpty()) { + } else if (!submitted && suggestions.length) { searchResults = suggestions.map(suggestion => ); } else if (loaded) { noResultsMessage = ( @@ -196,7 +198,7 @@ const SearchResults = () => { if (results.hashtags && results.hashtags.size > 0) { searchResults = results.hashtags.map(hashtag => ); - } else if (!submitted && suggestions && !suggestions.isEmpty()) { + } else if (!submitted && !trends.isEmpty()) { searchResults = trends.map(hashtag => ); } else if (loaded) { noResultsMessage = ( @@ -235,7 +237,7 @@ const SearchResults = () => { key={selectedFilter} scrollKey={`${selectedFilter}:${value}`} isLoading={submitted && !loaded} - showLoading={submitted && !loaded && searchResults?.isEmpty()} + showLoading={submitted && !loaded && (Array.isArray(searchResults) ? !searchResults.length : searchResults?.isEmpty())} hasMore={hasMore} onLoadMore={handleLoadMore} placeholderComponent={placeholderComponent} diff --git a/src/features/edit-identity/index.tsx b/src/features/edit-identity/index.tsx index 17a4ddbd0..ad5c6b3cc 100644 --- a/src/features/edit-identity/index.tsx +++ b/src/features/edit-identity/index.tsx @@ -52,15 +52,18 @@ const EditIdentity: React.FC = () => { const [username, setUsername] = useState(''); const [reason, setReason] = useState(''); + const [submitted, setSubmitted] = useState(false); useEffect(() => { + if (!submitted) return; + const dismissed = new Set(dismissedSettingsNotifications); if (!dismissed.has('needsNip05')) { dismissed.add('needsNip05'); dispatch(changeSetting(['dismissedSettingsNotifications'], [...dismissed])); } - }, []); + }, [submitted]); if (!account) return null; @@ -85,6 +88,7 @@ const EditIdentity: React.FC = () => { }); setUsername(''); setReason(''); + setSubmitted(true); }, }); }; diff --git a/src/features/feed-suggestions/feed-suggestions.tsx b/src/features/feed-suggestions/feed-suggestions.tsx index d32c98d72..3a90758b2 100644 --- a/src/features/feed-suggestions/feed-suggestions.tsx +++ b/src/features/feed-suggestions/feed-suggestions.tsx @@ -7,7 +7,8 @@ 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 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 ActionButton from '../ui/components/action-button.tsx'; @@ -66,18 +67,19 @@ const SuggestionItem: React.FC = ({ accountId }) => { ); }; -interface IFeedSuggesetions { +interface IFeedSuggestions { statusId: string; onMoveUp?: (statusId: string, featured?: boolean) => void; onMoveDown?: (statusId: string, featured?: boolean) => void; } -const FeedSuggestions: React.FC = ({ statusId, onMoveUp, onMoveDown }) => { +const FeedSuggestions: React.FC = ({ statusId, onMoveUp, onMoveDown }) => { const intl = useIntl(); - const suggestedProfiles = useAppSelector((state) => state.suggestions.items); - const isLoading = useAppSelector((state) => state.suggestions.isLoading); + const features = useFeatures(); - 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 => { if (onMoveUp) { diff --git a/src/features/follow-recommendations/index.tsx b/src/features/follow-recommendations/index.tsx index 20e1992b8..1078edf2c 100644 --- a/src/features/follow-recommendations/index.tsx +++ b/src/features/follow-recommendations/index.tsx @@ -1,41 +1,33 @@ import { debounce } from 'es-toolkit'; -import { useEffect } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { fetchSuggestions } from 'soapbox/actions/suggestions.ts'; import ScrollableList from 'soapbox/components/scrollable-list.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 AccountContainer from 'soapbox/containers/account-container.tsx'; -import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; -import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; +import { useSuggestions } from 'soapbox/queries/suggestions.ts'; const messages = defineMessages({ heading: { id: 'follow_recommendations.heading', defaultMessage: 'Suggested Profiles' }, }); -const FollowRecommendations: React.FC = () => { - const dispatch = useAppDispatch(); +interface IFollowRecommendations { + local?: boolean; +} + +const FollowRecommendations: React.FC = ({ local = false }) => { 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 { data: suggestions, fetchNextPage, hasNextPage, isLoading, isFetchingNextPage } = useSuggestions({ local }); const handleLoadMore = debounce(() => { - if (isLoading) { - return null; + if (hasNextPage && !isFetchingNextPage) { + fetchNextPage(); } + }, 1000); - return dispatch(fetchSuggestions({ limit: 20 })); - }, 300); - - useEffect(() => { - dispatch(fetchSuggestions({ limit: 20 })); - }, []); - - if (suggestions.size === 0 && !isLoading) { + if (suggestions.length === 0 && !isLoading) { return ( @@ -52,7 +44,7 @@ const FollowRecommendations: React.FC = () => { isLoading={isLoading} scrollKey='suggestions' onLoadMore={handleLoadMore} - hasMore={hasMore} + hasMore={hasNextPage} itemClassName='pb-4' > {suggestions.map((suggestion) => ( diff --git a/src/features/ui/components/latest-accounts-panel.tsx b/src/features/ui/components/latest-accounts-panel.tsx new file mode 100644 index 000000000..8f09d3c08 --- /dev/null +++ b/src/features/ui/components/latest-accounts-panel.tsx @@ -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 = ({ 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 ( + } + action={ + + + + + + } + > + {isFetching ? ( + + ) : ( + suggestionsToRender.map((suggestion: any) => ( + , but it isn't + id={suggestion.account} + actionIcon={xIcon} + actionTitle={intl.formatMessage(messages.dismissSuggestion)} + onActionClick={account ? handleDismiss : undefined} + /> + )) + )} + + ); +}; + +export default LatestAccountsPanel; diff --git a/src/features/ui/index.tsx b/src/features/ui/index.tsx index b5e23a0e5..d6422624e 100644 --- a/src/features/ui/index.tsx +++ b/src/features/ui/index.tsx @@ -262,8 +262,9 @@ const SwitchingColumnsArea: React.FC = ({ children }) => - {features.suggestions && } - {features.profileDirectory && } + {features.suggestionsLocal && } + {features.suggestions && } + {features.profileDirectory && } {features.events && } {features.chats && } diff --git a/src/features/ui/util/async-components.ts b/src/features/ui/util/async-components.ts index 5654b246b..ff6b05aca 100644 --- a/src/features/ui/util/async-components.ts +++ b/src/features/ui/util/async-components.ts @@ -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 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 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 ModalContainer = lazy(() => import('soapbox/features/ui/containers/modal-container.ts')); 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 Migration = lazy(() => import('soapbox/features/migration/index.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 Directory = lazy(() => import('soapbox/features/directory/index.tsx')); export const RegisterInvite = lazy(() => import('soapbox/features/register-invite/index.tsx')); diff --git a/src/features/video/index.tsx b/src/features/video/index.tsx index c58ded688..77047c5d5 100644 --- a/src/features/video/index.tsx +++ b/src/features/video/index.tsx @@ -191,10 +191,12 @@ const Video: React.FC = ({ }; const handleTimeUpdate = () => { - if (video.current) { - setCurrentTime(Math.floor(video.current.currentTime)); - setDuration(Math.floor(video.current.duration)); - } + if (!video.current) return; + + const { duration, currentTime } = video.current; + + setCurrentTime(Math.floor(currentTime)); + setDuration(Number.isNaN(duration) || (duration === Infinity) ? 0 : Math.floor(duration)); }; const handleVolumeMouseDown: React.MouseEventHandler = e => { @@ -480,7 +482,11 @@ const Video: React.FC = ({ const playerStyle: React.CSSProperties = {}; 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) { @@ -649,9 +655,13 @@ const Video: React.FC = ({ {formatTime(currentTime)} - {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} - / - {formatTime(duration)} + {duration > 0 && ( + <> + {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} + / + {formatTime(duration)} + + )} {link && ( @@ -660,7 +670,6 @@ const Video: React.FC = ({ )} -