diff --git a/src/assets/images/bridge-image.png b/src/assets/images/bridge-image.png new file mode 100644 index 000000000..20d319554 Binary files /dev/null and b/src/assets/images/bridge-image.png differ diff --git a/src/assets/images/nostr-image.png b/src/assets/images/nostr-image.png new file mode 100644 index 000000000..3c707cd9b Binary files /dev/null and b/src/assets/images/nostr-image.png differ diff --git a/src/features/compose/components/search-results.tsx b/src/features/compose/components/search-results.tsx index da3c8e6c6..03a7509c3 100644 --- a/src/features/compose/components/search-results.tsx +++ b/src/features/compose/components/search-results.tsx @@ -1,18 +1,22 @@ -import xIcon from '@tabler/icons/outline/x.svg'; import clsx from 'clsx'; import { useEffect, useRef } from 'react'; -import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; +import { + FormattedMessage, + // defineMessages, + // useIntl +} from 'react-intl'; -import { expandSearch, setFilter, setSearchAccount } from 'soapbox/actions/search.ts'; +import { + expandSearch, + // setFilter, + // setSearchAccount +} from 'soapbox/actions/search.ts'; import { expandTrendingStatuses, fetchTrendingStatuses } from 'soapbox/actions/trending-statuses.ts'; -import { useAccount } from 'soapbox/api/hooks/index.ts'; +// import { useAccount } from 'soapbox/api/hooks/index.ts'; import Hashtag from 'soapbox/components/hashtag.tsx'; -import IconButton from 'soapbox/components/icon-button.tsx'; import ScrollableList from 'soapbox/components/scrollable-list.tsx'; -import HStack from 'soapbox/components/ui/hstack.tsx'; import Spinner from 'soapbox/components/ui/spinner.tsx'; -import Tabs from 'soapbox/components/ui/tabs.tsx'; -import Text from 'soapbox/components/ui/text.tsx'; +// import Tabs from 'soapbox/components/ui/tabs.tsx'; import AccountContainer from 'soapbox/containers/account-container.tsx'; import StatusContainer from 'soapbox/containers/status-container.tsx'; import PlaceholderAccount from 'soapbox/features/placeholder/components/placeholder-account.tsx'; @@ -24,18 +28,18 @@ import { useSuggestions } from 'soapbox/queries/suggestions.ts'; import type { OrderedSet as ImmutableOrderedSet } from 'immutable'; import type { VirtuosoHandle } from 'react-virtuoso'; -import type { SearchFilter } from 'soapbox/reducers/search.ts'; +// import type { SearchFilter } from 'soapbox/reducers/search.ts'; -const messages = defineMessages({ - accounts: { id: 'search_results.accounts', defaultMessage: 'People' }, - statuses: { id: 'search_results.statuses', defaultMessage: 'Posts' }, - hashtags: { id: 'search_results.hashtags', defaultMessage: 'Hashtags' }, -}); +// const messages = defineMessages({ +// accounts: { id: 'search_results.accounts', defaultMessage: 'People' }, +// statuses: { id: 'search_results.statuses', defaultMessage: 'Posts' }, +// hashtags: { id: 'search_results.hashtags', defaultMessage: 'Hashtags' }, +// }); const SearchResults = () => { const node = useRef(null); - const intl = useIntl(); + // const intl = useIntl(); const dispatch = useAppDispatch(); const { data: suggestions } = useSuggestions(); @@ -47,8 +51,8 @@ const SearchResults = () => { const trends = useAppSelector((state) => state.trends.items); const submitted = useAppSelector((state) => state.search.submitted); const selectedFilter = useAppSelector((state) => state.search.filter); - const filterByAccount = useAppSelector((state) => state.search.accountId || undefined); - const { account } = useAccount(filterByAccount); + // const filterByAccount = useAppSelector((state) => state.search.accountId || undefined); + // const { account } = useAccount(filterByAccount); const handleLoadMore = () => { if (results.accounts.size || results.statuses.size || results.hashtags.size) { @@ -58,35 +62,35 @@ const SearchResults = () => { } }; - const handleUnsetAccount = () => dispatch(setSearchAccount(null)); + // const handleUnsetAccount = () => dispatch(setSearchAccount(null)); - const selectFilter = (newActiveFilter: SearchFilter) => dispatch(setFilter(newActiveFilter)); + // const selectFilter = (newActiveFilter: SearchFilter) => dispatch(setFilter(newActiveFilter)); - const renderFilterBar = () => { - const items = []; - items.push( - { - text: intl.formatMessage(messages.statuses), - action: () => selectFilter('statuses'), - name: 'statuses', - }, - { - text: intl.formatMessage(messages.accounts), - action: () => selectFilter('accounts'), - name: 'accounts', - }, - ); + // const renderFilterBar = () => { + // const items = []; + // items.push( + // { + // text: intl.formatMessage(messages.statuses), + // action: () => selectFilter('statuses'), + // name: 'statuses', + // }, + // { + // text: intl.formatMessage(messages.accounts), + // action: () => selectFilter('accounts'), + // name: 'accounts', + // }, + // ); - items.push( - { - text: intl.formatMessage(messages.hashtags), - action: () => selectFilter('hashtags'), - name: 'hashtags', - }, - ); + // items.push( + // { + // text: intl.formatMessage(messages.hashtags), + // action: () => selectFilter('hashtags'), + // name: 'hashtags', + // }, + // ); - return ; - }; + // return ; + // }; const getCurrentIndex = (id: string): number => { return resultsIds?.keySeq().findIndex(key => key === id); @@ -215,7 +219,7 @@ const SearchResults = () => { return ( <> - {filterByAccount ? ( + {/* {filterByAccount ? ( @@ -228,7 +232,7 @@ const SearchResults = () => { ) : (
{renderFilterBar()}
- )} + )} */} {noResultsMessage || ( decentralized social media, dive into Nostr or cross the bridge to other networks, and connect with a global community. All in one place.' }, + nostrTitle: { id: 'column.explorer.nostr_card.title', defaultMessage: 'Nostr' }, + nostrText: { id: 'column.explorer.nostr_card.text', defaultMessage: 'Wondering about Nostr? Find Out More' }, + bridgeTitle: { id: 'column.explorer.bridge_card.title', defaultMessage: 'Bridge' }, + bridgeText: { id: 'column.explorer.bridge_card.text', defaultMessage: 'Curious about Bridges? Click here' }, +}); + +const ExplorerCards = () => { + const [isOpen, setIsOpen] = useState(true); + const intl = useIntl(); + return ( + + + + + +

+ {intl.formatMessage(messages.welcomeTitle)} +

+
+ setIsOpen(!isOpen)} + className={`transition-transform duration-300${ + isOpen ? 'rotate-0' : 'rotate-180' + }`} + /> +
+ + + {intl.formatMessage(messages.welcomeText, { + span: (node) => {node}, + })} + +
+ + + {/* Nostr */} + + + {/* Title */} + +

+ {intl.formatMessage(messages.nostrTitle)} +

+ + {intl.formatMessage(messages.nostrText)} + +
+ +
+ +
+
+
+ + {/* Bridge */} + + + {/* Title */} + +

{intl.formatMessage(messages.bridgeTitle)}

+ + {intl.formatMessage(messages.bridgeText)} + +
+ +
+ +
+
+
+
+
+ ); +}; + + +export default ExplorerCards; \ No newline at end of file diff --git a/src/features/search/components/explorerFilter.tsx b/src/features/search/components/explorerFilter.tsx new file mode 100644 index 000000000..c49c69392 --- /dev/null +++ b/src/features/search/components/explorerFilter.tsx @@ -0,0 +1,358 @@ +import arrowIcon from '@tabler/icons/outline/chevron-down.svg'; +import searchIcon from '@tabler/icons/outline/search.svg'; +import xIcon from '@tabler/icons/outline/x.svg'; +import clsx from 'clsx'; +import { useState } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +import Button from 'soapbox/components/ui/button.tsx'; +import Checkbox from 'soapbox/components/ui/checkbox.tsx'; +import Divider from 'soapbox/components/ui/divider.tsx'; +import HStack from 'soapbox/components/ui/hstack.tsx'; +import IconButton from 'soapbox/components/ui/icon-button.tsx'; +import Input from 'soapbox/components/ui/input.tsx'; +import Stack from 'soapbox/components/ui/stack.tsx'; +import SvgIcon from 'soapbox/components/ui/svg-icon.tsx'; +import Text from 'soapbox/components/ui/text.tsx'; +import Toggle from 'soapbox/components/ui/toggle.tsx'; +import { SelectDropdown } from 'soapbox/features/forms/index.tsx'; + +const messages = defineMessages({ + filters: { id: 'column.explorer.filters', defaultMessage: 'Filters:' }, + showReplies: { id: 'column.explorer.filters.show_replies', defaultMessage: 'Show replies:' }, + language: { id: 'column.explorer.filters.language', defaultMessage: 'Language:' }, + platforms: { id: 'column.explorer.filters.platforms', defaultMessage: 'Platforms:' }, + createYourFilter: { id: 'column.explorer.filters.create_your_filter', defaultMessage: 'Create your filter' }, + filterByWords: { id: 'column.explorer.filters.filter_by_words', defaultMessage: 'Filter by this/these words' }, + include: { id: 'column.explorer.filters.include', defaultMessage: 'Include' }, + exclude: { id: 'column.explorer.filters.exclude', defaultMessage: 'Exclude' }, + nostr: { id: 'column.explorer.filters.nostr', defaultMessage: 'Nostr' }, + bluesky: { id: 'column.explorer.filters.bluesky', defaultMessage: 'Bluesky' }, + fediverse: { id: 'column.explorer.filters.fediverse', defaultMessage: 'Fediverse' }, + cancel: { id: 'column.explorer.filters.cancel', defaultMessage: 'Cancel' }, + applyFilter: { id: 'column.explorer.filters.apply_filter', defaultMessage: 'Apply Filter' }, +}); + +const languages = { + en: 'English', + ar: 'العربية', + ast: 'Asturianu', + bg: 'Български', + bn: 'বাংলা', + ca: 'Català', + co: 'Corsu', + cs: 'Čeština', + cy: 'Cymraeg', + da: 'Dansk', + de: 'Deutsch', + el: 'Ελληνικά', + 'en-Shaw': '𐑖𐑱𐑝𐑾𐑯', + eo: 'Esperanto', + es: 'Español', + eu: 'Euskara', + fa: 'فارسی', + fi: 'Suomi', + fr: 'Français', + ga: 'Gaeilge', + gl: 'Galego', + he: 'עברית', + hi: 'हिन्दी', + hr: 'Hrvatski', + hu: 'Magyar', + hy: 'Հայերեն', + id: 'Bahasa Indonesia', + io: 'Ido', + is: 'íslenska', + it: 'Italiano', + ja: '日本語', + jv: 'ꦧꦱꦗꦮ', + ka: 'ქართული', + kk: 'Қазақша', + ko: '한국어', + lt: 'Lietuvių', + lv: 'Latviešu', + ml: 'മലയാളം', + ms: 'Bahasa Melayu', + nl: 'Nederlands', + no: 'Norsk', + oc: 'Occitan', + pl: 'Polski', + pt: 'Português', + 'pt-BR': 'Português do Brasil', + ro: 'Română', + ru: 'Русский', + sk: 'Slovenčina', + sl: 'Slovenščina', + sq: 'Shqip', + sr: 'Српски', + 'sr-Latn': 'Srpski (latinica)', + sv: 'Svenska', + ta: 'தமிழ்', + te: 'తెలుగు', + th: 'ไทย', + tr: 'Türkçe', + uk: 'Українська', + zh: '中文', + 'zh-CN': '简体中文', + 'zh-HK': '繁體中文(香港)', + 'zh-TW': '繁體中文(臺灣)', +}; + +interface IGenerateFilter { + name: string; + status: boolean | null; +} + +const ExplorerFilter = () => { + const [showReplies, setShowReplies] = useState(false); + const [inputValue, setInputValue] = useState(''); + const [include, setInclude] = useState(true); + const [isOpen, setIsOpen] = useState(true); + + const hasValue = inputValue.length > 0; + + const intl = useIntl(); + + const [testList, setTestList] = useState([ + { 'name': 'EN', 'status': true }, + { 'name': 'Nostr', 'status': null }, + { 'name': 'Bluesky', 'status': null }, + { 'name': 'Fediverse', 'status': null }, + { 'name': 'Bitcoin', 'status': false }, + { 'name': 'NostrInArgentina', 'status': true }, + { 'name': 'ElonForPresident', 'status': false }, + ]); + + const generateFilter = ({ name, status }: IGenerateFilter) => { + let borderColor = ''; + let textColor = ''; + switch (name.toLowerCase()) { + case 'nostr': + borderColor = 'border-purple-500'; + textColor = 'text-purple-500'; + break; + case 'bluesky': + borderColor = 'border-blue-500'; + textColor = 'text-blue-500'; + break; + case 'fediverse': + borderColor = 'border-indigo-500'; + textColor = 'text-indigo-500'; + break; + default: + borderColor = status ? 'border-green-500' : 'border-red-500'; + textColor = status ? 'text-green-500' : 'text-red-500'; + } + + return ( +
+ {name} + setTestList((prevValue) => { + return prevValue.filter((x) => x.name !== name); + })} + /> +
+ ); + }; + + return ( + + + {/* Filters */} + + + + {intl.formatMessage(messages.filters)} + + + {testList.map(generateFilter)} + + + setIsOpen(!isOpen)} + /> + + + + + {/* Show Reply toggle */} + + + {intl.formatMessage(messages.showReplies)} + + + setShowReplies(!showReplies)} + /> + + + + {/* Language */} + + + {intl.formatMessage(messages.language)} + + + + + + {/* Platforms */} + + + {intl.formatMessage(messages.platforms)} + + + {/* Nostr */} + + tag.name.toLowerCase() === 'nostr')} + onChange={() => setTestList((prevValue) => { + const exists = prevValue.some((x) => x.name.toLowerCase() === 'nostr'); + if (exists) { + return prevValue.filter((x) => x.name.toLowerCase() !== 'nostr'); + } else { + return [...prevValue, { name: 'Nostr', status: null }]; + } + })} + // checked={params.get('agreement', false)} + // required + /> + + {intl.formatMessage(messages.nostr)} + + + + {/* Bluesky */} + + tag.name.toLowerCase() === 'bluesky')} + onChange={() => setTestList((prevValue) => { + const exists = prevValue.some((x) => x.name.toLowerCase() === 'bluesky'); + if (exists) { + return prevValue.filter((x) => x.name.toLowerCase() !== 'bluesky'); + } else { + return [...prevValue, { name: 'Bluesky', status: null }]; + } + })} + /> + + {intl.formatMessage(messages.bluesky)} + + + + {/* Fediverse */} + + tag.name.toLowerCase() === 'fediverse')} + onChange={() => setTestList((prevValue) => { + const exists = prevValue.some((x) => x.name.toLowerCase() === 'fediverse'); + if (exists) { + return prevValue.filter((x) => x.name.toLowerCase() !== 'fediverse'); + } else { + return [...prevValue, { name: 'Fediverse', status: null }]; + } + })} + /> + + {intl.formatMessage(messages.fediverse)} + + + + + + + + {/* Create your filter */} + + + {intl.formatMessage(messages.createYourFilter)} + + + + + {intl.formatMessage(messages.filterByWords)} + + + + + +
+ setInputValue(e.target.value)} /> +
+ + + +
+ +
+ + {/* Include */} + + setInclude(!include)} + /> + + {intl.formatMessage(messages.include)} + + + + {/* Exclude */} + + setInclude(!include)} + /> + + {intl.formatMessage(messages.exclude)} + + +
+
+ + + + + + + +
+
+ +
+ ); +}; + + +export default ExplorerFilter; \ No newline at end of file diff --git a/src/features/search/index.tsx b/src/features/search/index.tsx index 57852d502..d1ad532c5 100644 --- a/src/features/search/index.tsx +++ b/src/features/search/index.tsx @@ -1,12 +1,15 @@ import { defineMessages, useIntl } from 'react-intl'; import { Column } from 'soapbox/components/ui/column.tsx'; +import Divider from 'soapbox/components/ui/divider.tsx'; import Stack from 'soapbox/components/ui/stack.tsx'; import SearchResults from 'soapbox/features/compose/components/search-results.tsx'; import Search from 'soapbox/features/compose/components/search.tsx'; +import ExplorerCards from 'soapbox/features/search/components/explorerCards.tsx'; +import ExplorerFilter from 'soapbox/features/search/components/explorerFilter.tsx'; const messages = defineMessages({ - heading: { id: 'column.search', defaultMessage: 'Discover' }, + heading: { id: 'column.search', defaultMessage: 'Explorer' }, }); const SearchPage = () => { @@ -14,10 +17,20 @@ const SearchPage = () => { return ( + + + + + + + + +
+
diff --git a/src/locales/en.json b/src/locales/en.json index 3a531ab37..ff581101b 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -430,13 +430,33 @@ "column.reblogs": "Reposts", "column.registration": "Sign Up", "column.scheduled_statuses": "Scheduled Posts", - "column.search": "Discover", + "column.search": "Explorer", "column.settings_store": "Settings store", "column.soapbox_config": "Soapbox config", "column.test": "Test timeline", "column.zaps": "Zaps", "column_forbidden.body": "You do not have permission to access this page.", "column_forbidden.title": "Forbidden", + "column.explorer": "Explorer", + "column.explorer.welcome_card.title": "Welcome to Explorer", + "column.explorer.welcome_card.text": "Explore the world of decentralized social media, dive into Nostr or cross the bridge to other networks, and connect with a global community. All in one place.", + "column.explorer.nostr_card.title": "Nostr", + "column.explorer.nostr_card.text": "Wondering about Nostr? Find Out More", + "column.explorer.bridge_card.title": "Bridge", + "column.explorer.bridge_card.text": "Curious about Bridges? Click Here", + "column.explorer.filters.show_replies": "Show replies:", + "column.explorer.filters": "Fiters:", + "column.explorer.filters.language": "Language:", + "column.explorer.filters.platforms": "Platforms:", + "column.explorer.filters.create_filter": "Create your filter", + "column.explorer.filters.filter_words": "Filter by this/these words", + "column.explorer.filters.checkbox.include": "Include", + "column.explorer.filters.checkbox.exclude": "Exclude", + "column.explorer.filters.checkbox.nostr": "Nostr", + "column.explorer.filters.checkbox.bluesky": "Bluesky", + "column.explorer.filters.checkbox.fediverse": "Fediverse", + "column.explorer.filters.cancel": "Cancel", + "column.explorer.filters.apply_filter": "Apply Filter", "common.cancel": "Cancel", "compare_history_modal.header": "Edit history", "compose.character_counter.title": "Used {chars} out of {maxChars} {maxChars, plural, one {character} other {characters}}",