diff --git a/locales/en/translation.json b/locales/en/translation.json index bd5e794a..19737a09 100644 --- a/locales/en/translation.json +++ b/locales/en/translation.json @@ -151,6 +151,14 @@ "submit": "Apply" } }, + "search": { + "title": "Search", + "placeholder": "Search or paste URL", + "results": { + "accounts": "People", + "more": "Load more" + } + }, "timeline": { "reload": "Reload", "settings": { diff --git a/src/components/Navigator.tsx b/src/components/Navigator.tsx index ac718a03..62679e3c 100644 --- a/src/components/Navigator.tsx +++ b/src/components/Navigator.tsx @@ -2,7 +2,7 @@ import { invoke } from '@tauri-apps/api/tauri' import { Dispatch, ReactElement, SetStateAction, useEffect, useState } from 'react' import { Icon } from '@rsuite/icons' import { Popover, Dropdown, Sidebar, Sidenav, Whisper, Button, Avatar, Badge, FlexboxGrid, useToaster } from 'rsuite' -import { BsPlus, BsGear, BsPencilSquare } from 'react-icons/bs' +import { BsPlus, BsGear, BsPencilSquare, BsSearch } from 'react-icons/bs' import { Server, ServerSet } from 'src/entities/server' import { Account } from 'src/entities/account' import { Timeline } from 'src/entities/timeline' @@ -23,6 +23,7 @@ type NavigatorProps = { openAuthorize: (server: Server) => void openAnnouncements: (server: Server, account: Account) => void toggleCompose: () => void + toggleSearch: () => void openThirdparty: () => void openSettings: () => void setHighlighted: Dispatch> @@ -132,6 +133,9 @@ const Navigator: React.FC = (props): ReactElement => { + diff --git a/src/components/compose/Compose.tsx b/src/components/compose/Compose.tsx index 4c7ac78f..907fa872 100644 --- a/src/components/compose/Compose.tsx +++ b/src/components/compose/Compose.tsx @@ -12,7 +12,7 @@ import failoverImg from 'src/utils/failoverImg' import Status from './Status' import { FormattedMessage } from 'react-intl' -const renderAccountIcon = (props: any, ref: any, account: [Account, Server] | undefined) => { +export const renderAccountIcon = (props: any, ref: any, account: [Account, Server] | undefined) => { if (account && account.length > 0) { return ( diff --git a/src/components/detail/profile/Followers.tsx b/src/components/detail/profile/Followers.tsx index 2abbf392..7f4b122b 100644 --- a/src/components/detail/profile/Followers.tsx +++ b/src/components/detail/profile/Followers.tsx @@ -119,7 +119,7 @@ const Followers: React.ForwardRefRenderFunction = (props, r ) : ( {followers.map(account => ( - + ))} diff --git a/src/components/detail/profile/Following.tsx b/src/components/detail/profile/Following.tsx index cf3783de..23a924c0 100644 --- a/src/components/detail/profile/Following.tsx +++ b/src/components/detail/profile/Following.tsx @@ -119,7 +119,7 @@ const Following: React.ForwardRefRenderFunction = (props, r ) : ( {following.map(account => ( - + ))} diff --git a/src/components/search/Results.tsx b/src/components/search/Results.tsx new file mode 100644 index 00000000..569eed9d --- /dev/null +++ b/src/components/search/Results.tsx @@ -0,0 +1,101 @@ +import { Icon } from '@rsuite/icons' +import { Entity, MegalodonInterface } from 'megalodon' +import { useRouter } from 'next/router' +import { useCallback, useState } from 'react' +import { BsSearch, BsPeople } from 'react-icons/bs' +import { FormattedMessage, useIntl } from 'react-intl' +import { Input, InputGroup, List, Avatar } from 'rsuite' +import { Server } from 'src/entities/server' +import emojify from 'src/utils/emojify' + +type Props = { + server: Server + client: MegalodonInterface +} + +export default function Results(props: Props) { + const { formatMessage } = useIntl() + const router = useRouter() + + const [word, setWord] = useState('') + const [accounts, setAccounts] = useState>([]) + + const search = async (word: string) => { + const res = await props.client.search(word, { limit: 5 }) + setAccounts(res.data.accounts) + } + + const loadMoreAccount = useCallback(async () => { + const res = await props.client.search(word, { type: 'accounts', limit: 5, offset: accounts.length }) + setAccounts(prev => prev.concat(res.data.accounts)) + }, [word, accounts]) + + const open = (user: Entity.Account) => { + router.push({ query: { user_id: user.id, server_id: props.server.id, account_id: props.server.account_id } }) + } + + return ( + <> +
+ + setWord(value)} /> + search(word)}> + + + +
+ {/* accounts */} + {accounts.length > 0 && ( +
+
+ + +
+ + {accounts.map((account, index) => ( + + + + ))} + loadMoreAccount()} + > + + + +
+ )} + + ) +} + +type UserProps = { + user: Entity.Account + open: (user: Entity.Account) => void +} + +const User: React.FC = props => { + const { user, open } = props + + return ( +
open(user)}> + {/** icon **/} +
+
+ +
+
+ {/** name **/} +
+
+ +
+
+ @{user.acct} +
+
+
+ ) +} diff --git a/src/components/search/Search.tsx b/src/components/search/Search.tsx new file mode 100644 index 00000000..764216c3 --- /dev/null +++ b/src/components/search/Search.tsx @@ -0,0 +1,83 @@ +import { Icon } from '@rsuite/icons' +import { BsX } from 'react-icons/bs' +import { FormattedMessage } from 'react-intl' +import { Button, Container, Content, FlexboxGrid, Header, Dropdown } from 'rsuite' +import { Server, ServerSet } from 'src/entities/server' +import { renderAccountIcon } from '../compose/Compose' +import { useState, useEffect } from 'react' +import { Account } from 'src/entities/account' +import { invoke } from '@tauri-apps/api/tauri' +import generator, { MegalodonInterface } from 'megalodon' +import { USER_AGENT } from 'src/defaults' +import Results from './Results' + +type Props = { + setOpened: (value: boolean) => void + servers: Array +} + +export default function Search(props: Props) { + const [accounts, setAccounts] = useState>([]) + const [fromAccount, setFromAccount] = useState<[Account, Server]>() + const [client, setClient] = useState() + + useEffect(() => { + const f = async () => { + const accounts = await invoke>('list_accounts') + setAccounts(accounts) + + const usual = accounts.find(([a, _]) => a.usual) + if (usual) { + setFromAccount(usual) + } else { + setFromAccount(accounts[0]) + } + } + f() + }, [props.servers]) + + useEffect(() => { + if (!fromAccount || fromAccount.length < 2) { + return + } + const client = generator(fromAccount[1].sns, fromAccount[1].base_url, fromAccount[0].access_token, USER_AGENT) + setClient(client) + }, [fromAccount]) + + const selectAccount = async (eventKey: string) => { + const account = accounts[parseInt(eventKey)] + setFromAccount(account) + await invoke('set_usual_account', { id: account[0].id }) + } + + return ( + +
+ + + + + + + + +
+ + + + renderAccountIcon(props, ref, fromAccount)} onSelect={selectAccount}> + {accounts.map((account, index) => ( + + @{account[0].username}@{account[1].domain} + + ))} + + + + {fromAccount && } + +
+ ) +} diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 3963ded8..919fab36 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -30,6 +30,7 @@ import ListMemberships from 'src/components/listMemberships/ListMemberships' import AddListMember from 'src/components/addListMember/AddListMember' import { useIntl } from 'react-intl' import { Context } from 'src/i18n' +import Search from 'src/components/search/Search' const { scrollLeft } = DOMHelper @@ -40,6 +41,7 @@ function App() { const [timelines, setTimelines] = useState>([]) const [unreads, setUnreads] = useState>([]) const [composeOpened, setComposeOpened] = useState(false) + const [searchOpened, setSearchOpened] = useState(false) const [style, setStyle] = useState({}) const [highlighted, setHighlighted] = useState(null) @@ -149,12 +151,22 @@ function App() { const toggleCompose = () => { if (servers.find(s => s.account !== null)) { + setSearchOpened(false) setComposeOpened(previous => !previous) } else { toaster.push(alert('info', formatMessage({ id: 'alert.need_auth' })), { placement: 'topStart' }) } } + const toggleSearch = () => { + if (servers.find(s => s.account !== null)) { + setComposeOpened(false) + setSearchOpened(previous => !previous) + } else { + toaster.push(alert('info', formatMessage({ id: 'alert.need_auth' })), { placement: 'topStart' }) + } + } + return (
dispatch({ target: 'thirdparty', value: true })} openSettings={() => dispatch({ target: 'settings', value: true })} toggleCompose={toggleCompose} + toggleSearch={toggleSearch} setHighlighted={setHighlighted} setUnreads={setUnreads} /> @@ -239,6 +252,19 @@ function App() {
)} + + {(props, ref) => ( +
+ +
+ )} +
{timelines.map(timeline => (