diff --git a/package.json b/package.json index b4fcb1219..26d362024 100644 --- a/package.json +++ b/package.json @@ -133,7 +133,7 @@ "multiselect-react-dropdown": "^2.0.25", "object-to-formdata": "^4.5.1", "path-browserify": "^1.0.1", - "pl-api": "^0.0.11", + "pl-api": "^0.0.12", "postcss": "^8.4.29", "process": "^0.11.10", "punycode": "^2.1.1", diff --git a/src/actions/search.ts b/src/actions/search.ts index 4960cf49c..c461cfa66 100644 --- a/src/actions/search.ts +++ b/src/actions/search.ts @@ -115,6 +115,7 @@ const setFilter = (filterType: SearchFilter) => }; const expandSearch = (type: SearchFilter) => (dispatch: AppDispatch, getState: () => RootState) => { + if (type === 'links') return; const value = getState().search.value; const offset = getState().search.results[type].size; const accountId = getState().search.accountId; diff --git a/src/api/hooks/index.ts b/src/api/hooks/index.ts index c86eff78e..2b4a12338 100644 --- a/src/api/hooks/index.ts +++ b/src/api/hooks/index.ts @@ -44,3 +44,6 @@ export { useHashtagStream } from './streaming/useHashtagStream'; export { useListStream } from './streaming/useListStream'; export { useGroupStream } from './streaming/useGroupStream'; export { useRemoteStream } from './streaming/useRemoteStream'; + +// Trends +export { useTrendingLinks } from './trends/useTrendingLinks'; diff --git a/src/api/hooks/trends/useTrendingLinks.ts b/src/api/hooks/trends/useTrendingLinks.ts new file mode 100644 index 000000000..e1078ffbb --- /dev/null +++ b/src/api/hooks/trends/useTrendingLinks.ts @@ -0,0 +1,20 @@ +import { trendsLinkSchema } from 'pl-api'; + +import { Entities } from 'soapbox/entity-store/entities'; +import { useEntities } from 'soapbox/entity-store/hooks'; +import { useClient, useFeatures } from 'soapbox/hooks'; + +const useTrendingLinks = () => { + const client = useClient(); + const features = useFeatures(); + + const { entities, ...rest } = useEntities( + [Entities.TRENDS_LINKS], + () => client.trends.getTrendingLinks(), + { schema: trendsLinkSchema, enabled: features.trendingLinks }, + ); + + return { trendingLinks: entities, ...rest }; +}; + +export { useTrendingLinks }; \ No newline at end of file diff --git a/src/components/hashtag.tsx b/src/components/hashtag.tsx index 4b18f42c8..0680524d6 100644 --- a/src/components/hashtag.tsx +++ b/src/components/hashtag.tsx @@ -9,6 +9,19 @@ import { HStack, Stack, Text } from './ui'; import type { Tag } from 'pl-api'; +const accountsCountRenderer = (count: number) => !!count && ( + + {shortNumberFormat(count)}, + }} + /> + +); + interface IHashtag { hashtag: Tag; } @@ -23,18 +36,7 @@ const Hashtag: React.FC = ({ hashtag }) => { #{hashtag.name} - {Boolean(count) && ( - - {shortNumberFormat(count)}, - }} - /> - - )} + {accountsCountRenderer(count)} {hashtag.history && ( @@ -52,4 +54,4 @@ const Hashtag: React.FC = ({ hashtag }) => { ); }; -export { Hashtag as default }; +export { Hashtag as default, accountsCountRenderer }; diff --git a/src/components/preview-card.tsx b/src/components/preview-card.tsx index 9a53ec7f1..59f64130e 100644 --- a/src/components/preview-card.tsx +++ b/src/components/preview-card.tsx @@ -159,6 +159,7 @@ const PreviewCard: React.FC = ({ height: horizontal ? height : undefined, }} className='status-card__image-image' + title={card.image_description || undefined} /> ); diff --git a/src/components/trending-link.tsx b/src/components/trending-link.tsx new file mode 100644 index 000000000..8209c8093 --- /dev/null +++ b/src/components/trending-link.tsx @@ -0,0 +1,63 @@ +import { TrendsLink } from 'pl-api'; +import React from 'react'; + +import { getTextDirection } from 'soapbox/utils/rtl'; + +import Blurhash from './blurhash'; +import { accountsCountRenderer } from './hashtag'; +import { HStack, Icon, Stack, Text } from './ui'; + +interface ITrendingLink { + trendingLink: TrendsLink; +} + +const TrendingLink: React.FC = ({ trendingLink }) => { + const count = Number(trendingLink.history?.[0]?.accounts); + + const direction = getTextDirection(trendingLink.title + trendingLink.description); + + let media; + + if (trendingLink.image) { + media = ( + + {trendingLink.blurhash && ( + + )} + + + ); + } + + return ( + + {media} + + {trendingLink.title} + {trendingLink.description && {trendingLink.description}} + + + + + + + {trendingLink.provider_name} + + + + {!!count && accountsCountRenderer(count)} + + + + ); +}; + +export default TrendingLink; \ No newline at end of file diff --git a/src/entity-store/entities.ts b/src/entity-store/entities.ts index 23373bd63..62dd40aad 100644 --- a/src/entity-store/entities.ts +++ b/src/entity-store/entities.ts @@ -1,4 +1,4 @@ -import type { BookmarkFolder, GroupMember, GroupRelationship, Relationship } from 'pl-api'; +import type { BookmarkFolder, GroupMember, GroupRelationship, Relationship, TrendsLink } from 'pl-api'; import type { Account, Group, Status } from 'soapbox/normalizers'; import type * as Schemas from 'soapbox/schemas'; @@ -14,6 +14,7 @@ enum Entities { RELAYS = 'Relays', RULES = 'Rules', STATUSES = 'Statuses', + TRENDS_LINKS = 'TrendsLinks', } interface EntityTypes { @@ -27,6 +28,7 @@ interface EntityTypes { [Entities.RELAYS]: Schemas.Relay; [Entities.RULES]: Schemas.AdminRule; [Entities.STATUSES]: Status; + [Entities.TRENDS_LINKS]: TrendsLink; } export { Entities, type EntityTypes }; \ No newline at end of file diff --git a/src/features/compose/components/search-results.tsx b/src/features/compose/components/search-results.tsx index 5c9be6cc4..db0c4985a 100644 --- a/src/features/compose/components/search-results.tsx +++ b/src/features/compose/components/search-results.tsx @@ -1,22 +1,23 @@ import clsx from 'clsx'; -import React, { useEffect, useRef } from 'react'; +import { List as ImmutableList, type OrderedSet as ImmutableOrderedSet } from 'immutable'; +import React, { useEffect, useRef, useState } from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; import { expandSearch, setFilter, setSearchAccount } from 'soapbox/actions/search'; import { fetchTrendingStatuses } from 'soapbox/actions/trending-statuses'; -import { useAccount } from 'soapbox/api/hooks'; +import { useAccount, useTrendingLinks } from 'soapbox/api/hooks'; import Hashtag from 'soapbox/components/hashtag'; import IconButton from 'soapbox/components/icon-button'; import ScrollableList from 'soapbox/components/scrollable-list'; +import TrendingLink from 'soapbox/components/trending-link'; import { HStack, Spinner, Tabs, Text } from 'soapbox/components/ui'; import AccountContainer from 'soapbox/containers/account-container'; import StatusContainer from 'soapbox/containers/status-container'; import PlaceholderAccount from 'soapbox/features/placeholder/components/placeholder-account'; import PlaceholderHashtag from 'soapbox/features/placeholder/components/placeholder-hashtag'; import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder-status'; -import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; +import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks'; -import type { OrderedSet as ImmutableOrderedSet } from 'immutable'; import type { VirtuosoHandle } from 'react-virtuoso'; import type { SearchFilter } from 'soapbox/reducers/search'; @@ -24,6 +25,7 @@ const messages = defineMessages({ accounts: { id: 'search_results.accounts', defaultMessage: 'People' }, statuses: { id: 'search_results.statuses', defaultMessage: 'Posts' }, hashtags: { id: 'search_results.hashtags', defaultMessage: 'Hashtags' }, + links: { id: 'search_results.links', defaultMessage: 'News' }, }); const SearchResults = () => { @@ -31,6 +33,9 @@ const SearchResults = () => { const intl = useIntl(); const dispatch = useAppDispatch(); + const features = useFeatures(); + + const [tabKey, setTabKey] = useState(1); const value = useAppSelector((state) => state.search.submittedValue); const results = useAppSelector((state) => state.search.results); @@ -40,6 +45,7 @@ const SearchResults = () => { const submitted = useAppSelector((state) => state.search.submitted); const selectedFilter = useAppSelector((state) => state.search.filter); const filterByAccount = useAppSelector((state) => state.search.accountId || undefined); + const { trendingLinks } = useTrendingLinks(); const { account } = useAccount(filterByAccount); const handleLoadMore = () => dispatch(expandSearch(selectedFilter)); @@ -61,9 +67,6 @@ const SearchResults = () => { action: () => selectFilter('statuses'), name: 'statuses', }, - ); - - items.push( { text: intl.formatMessage(messages.hashtags), action: () => selectFilter('hashtags'), @@ -71,7 +74,13 @@ const SearchResults = () => { }, ); - return ; + if (!submitted && features.trendingLinks) items.push({ + text: intl.formatMessage(messages.links), + action: () => selectFilter('links'), + name: 'links', + }); + + return ; }; const getCurrentIndex = (id: string): number => resultsIds?.keySeq().findIndex(key => key === id); @@ -197,6 +206,15 @@ const SearchResults = () => { } } + if (selectedFilter === 'links') { + if (submitted) { + selectFilter('accounts'); + setTabKey(key => ++key); + } else if (!submitted && trendingLinks) { + searchResults = ImmutableList(trendingLinks.map(trendingLink => )); + } + } + return ( <> {filterByAccount ? ( @@ -228,7 +246,7 @@ const SearchResults = () => { 'divide-gray-200 dark:divide-gray-800 divide-solid divide-y': selectedFilter === 'statuses', })} itemClassName={clsx({ - 'pb-4': selectedFilter === 'accounts', + 'pb-4': selectedFilter === 'accounts' || selectedFilter === 'links', 'pb-3': selectedFilter === 'hashtags', })} > diff --git a/src/locales/en.json b/src/locales/en.json index e3f90244f..69c6579a1 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1305,6 +1305,7 @@ "search_results.accounts": "People", "search_results.filter_message": "You are searching for posts from @{acct}.", "search_results.hashtags": "Hashtags", + "search_results.links": "News", "search_results.statuses": "Posts", "security.codes.fail": "Failed to fetch backup codes", "security.confirm.fail": "Incorrect code or password. Try again.", diff --git a/src/locales/pl.json b/src/locales/pl.json index 643dea0cd..fb69455dc 100644 --- a/src/locales/pl.json +++ b/src/locales/pl.json @@ -1305,6 +1305,7 @@ "search_results.accounts": "Ludzie", "search_results.filter_message": "Szukasz wpisów autorstwa @{acct}.", "search_results.hashtags": "Hashtagi", + "search_results.links": "Aktualności", "search_results.statuses": "Wpisy", "security.codes.fail": "Nie udało się uzyskać zapasowych kodów", "security.confirm.fail": "Nieprawidłowy kod lub hasło. Spróbuj ponownie.", diff --git a/src/reducers/search.ts b/src/reducers/search.ts index 32c08580e..b668384af 100644 --- a/src/reducers/search.ts +++ b/src/reducers/search.ts @@ -51,7 +51,7 @@ const ReducerRecord = ImmutableRecord({ type State = ReturnType; type APIEntities = Array; -type SearchFilter = 'accounts' | 'statuses' | 'groups' | 'hashtags'; +type SearchFilter = 'accounts' | 'statuses' | 'groups' | 'hashtags' | 'links'; const toIds = (items: APIEntities = []) => ImmutableOrderedSet(items.map(item => item.id)); diff --git a/yarn.lock b/yarn.lock index 584ee2c3e..2485014e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8390,10 +8390,10 @@ pkg-types@^1.0.3: mlly "^1.2.0" pathe "^1.1.0" -pl-api@^0.0.11: - version "0.0.11" - resolved "https://registry.yarnpkg.com/pl-api/-/pl-api-0.0.11.tgz#3adbfac0c1ba16e2696e00f0ef1f792880e6d0a5" - integrity sha512-tFFETaG4lFkLNXH1hj4JZ4qTiF3Zxy0K/3gV/KHhGwn9V/IuNTVw1y5cZWajGiGKU+hOnj31oX4ZZwwEggHh9Q== +pl-api@^0.0.12: + version "0.0.12" + resolved "https://registry.yarnpkg.com/pl-api/-/pl-api-0.0.12.tgz#3bd12c2e1799bc4252420b61fb3893039cb25e78" + integrity sha512-S4KouCH5ZL2GOIeQPczgY3NXX8yhtRhbhdcaywUxrjybHY2idVv9SnW3sgldUe83INY0AwnBHBuxRlMY2VuNiA== dependencies: blurhash "^2.0.5" http-link-header "^1.1.3"