diff --git a/locales/en/translation.json b/locales/en/translation.json index ee1850cb..3a6d8ed4 100644 --- a/locales/en/translation.json +++ b/locales/en/translation.json @@ -183,6 +183,9 @@ "submit": "Submit" }, "search": { - "placeholder": "Search timeline" + "placeholder": "Search timeline", + "statuses": "Statuses", + "accounts": "Accounts", + "hashtags": "Hashtags" } } diff --git a/renderer/components/compose/Compose.tsx b/renderer/components/compose/Compose.tsx index 0f5b936c..2ef8d75f 100644 --- a/renderer/components/compose/Compose.tsx +++ b/renderer/components/compose/Compose.tsx @@ -239,6 +239,7 @@ export default function Compose(props: Props) { color="blue-gray" containerProps={{ className: 'mb-2' }} value={spoiler} + className="placeholder:opacity-100" onChange={ev => setSpoiler(ev.target.value)} placeholder={formatMessage({ id: 'compose.spoiler.placeholder' })} /> diff --git a/renderer/components/detail/Profile.tsx b/renderer/components/detail/Profile.tsx index 4a08449d..e4114a27 100644 --- a/renderer/components/detail/Profile.tsx +++ b/renderer/components/detail/Profile.tsx @@ -256,7 +256,12 @@ export default function Profile(props: Props) {
- + diff --git a/renderer/components/timelines/Search.tsx b/renderer/components/timelines/Search.tsx new file mode 100644 index 00000000..968bea95 --- /dev/null +++ b/renderer/components/timelines/Search.tsx @@ -0,0 +1,132 @@ +import { Input, Tab, TabPanel, Tabs, TabsBody, TabsHeader } from '@material-tailwind/react' +import { Entity, MegalodonInterface } from 'megalodon' +import { useRouter } from 'next/router' +import { FormEvent, useEffect, useState } from 'react' +import { FaMagnifyingGlass } from 'react-icons/fa6' +import { FormattedMessage, useIntl } from 'react-intl' +import Statuses from './search/Statuses' +import { Account } from '@/db' +import Accounts from './search/Accounts' +import Hashtags from './search/Hashtags' +import Detail from '../detail/Detail' + +type Props = { + client: MegalodonInterface + account: Account + openMedia: (media: Array, index: number) => void +} + +export default function Search(props: Props) { + const router = useRouter() + const { formatMessage } = useIntl() + + const [query, setQuery] = useState('') + const [activeTab, setActiveTab] = useState('statuses') + const [results, setResults] = useState({ + accounts: [], + hashtags: [], + statuses: [] + }) + const [loading, setLoading] = useState(false) + + useEffect(() => { + if (router.query.q) { + setQuery(router.query.q as string) + search(router.query.q as string) + } + }, [router.query.q]) + + const search = (query: string) => { + setLoading(true) + setResults({ + accounts: [], + hashtags: [], + statuses: [] + }) + props.client + .search(query) + .then(res => { + setResults(res.data) + }) + .finally(() => { + setLoading(false) + }) + } + + const submit = (ev: FormEvent) => { + ev.preventDefault() + const word = ((ev.target as HTMLFormElement).elements[0] as HTMLInputElement).value + if (word.length > 0) { + router.push({ + query: Object.assign({}, router.query, { + timeline: 'search', + q: word + }) + }) + } + } + + const timelineClass = () => { + if (router.query.detail) { + return 'timeline-with-drawer' + } + return 'timeline' + } + + return ( + <> +
+
+
+
submit(ev)}> + setQuery(e.target.value)} + icon={} + /> + +
+ + + setActiveTab('statuses')}> + + + setActiveTab('accounts')}> + + + setActiveTab('hashtags')}> + + + + + + + + + + + + + + + +
+ +
+ + ) +} diff --git a/renderer/components/timelines/Timeline.tsx b/renderer/components/timelines/Timeline.tsx index 65a305b9..b4e5bb04 100644 --- a/renderer/components/timelines/Timeline.tsx +++ b/renderer/components/timelines/Timeline.tsx @@ -1,6 +1,6 @@ import { Account } from '@/db' import { Entity, MegalodonInterface, WebSocketInterface } from 'megalodon' -import { useEffect, useState, useCallback, useRef } from 'react' +import { useEffect, useState, useCallback, useRef, FormEvent } from 'react' import { Virtuoso } from 'react-virtuoso' import Status from './status/Status' import { FormattedMessage, useIntl } from 'react-intl' @@ -244,6 +244,19 @@ export default function Timeline(props: Props) { }) } + const search = (ev: FormEvent) => { + ev.preventDefault() + const word = ((ev.target as HTMLFormElement).elements[0] as HTMLInputElement).value + if (word.length > 0) { + router.push({ + query: Object.assign({}, router.query, { + timeline: 'search', + q: word + }) + }) + } + } + return (
@@ -252,14 +265,13 @@ export default function Timeline(props: Props) { {props.timeline.match(/list_(\d+)/) ? <>{list && list.title} : }
-
+ search(ev)}>
diff --git a/renderer/components/timelines/search/Accounts.tsx b/renderer/components/timelines/search/Accounts.tsx new file mode 100644 index 00000000..073accb1 --- /dev/null +++ b/renderer/components/timelines/search/Accounts.tsx @@ -0,0 +1,63 @@ +import { Avatar, Spinner } from '@material-tailwind/react' +import { Entity } from 'megalodon' +import emojify from '@/utils/emojify' +import { useRouter } from 'next/router' + +type Props = { + users: Array + loading: boolean +} + +export default function Accounts(props: Props) { + const router = useRouter() + + const openUser = (id: string) => { + router.push({ query: { id: router.query.id, timeline: router.query.timeline, user_id: id, detail: true } }) + } + + return ( + <> +
+ {props.users.length > 0 ? ( + <> + {props.users.map((user, index) => ( + + ))} + + ) : ( + <> + {props.loading && ( +
+ +
+ )} + + )} +
+ + ) +} + +type UserProps = { + user: Entity.Account + openUser: (id: string) => void +} + +function User(props: UserProps) { + return ( +
+
props.openUser(props.user.id)}> +
+ +
+
+

+

@{props.user.acct}

+
+
+
+ ) +} diff --git a/renderer/components/timelines/search/Hashtags.tsx b/renderer/components/timelines/search/Hashtags.tsx new file mode 100644 index 00000000..c8e57f6c --- /dev/null +++ b/renderer/components/timelines/search/Hashtags.tsx @@ -0,0 +1,60 @@ +import { Spinner } from '@material-tailwind/react' +import { Entity } from 'megalodon' +import { useRouter } from 'next/router' +import { FaHashtag } from 'react-icons/fa6' + +type Props = { + hashtags: Array + loading: boolean +} + +export default function Hashtags(props: Props) { + const router = useRouter() + + const openTag = (tag: string) => { + router.push({ query: { id: router.query.id, timeline: router.query.timeline, tag: tag, detail: true } }) + } + + return ( + <> +
+ {props.hashtags.length > 0 ? ( + <> + {props.hashtags.map(hashtag => ( + + ))} + + ) : ( + <> + {props.loading && ( +
+ +
+ )} + + )} +
+ + ) +} + +type HashtagProps = { + hashtag: Entity.Tag + openTag: (tag: string) => void +} + +function Hashtag(props: HashtagProps) { + return ( +
+
+
props.openTag(props.hashtag.name)} + > + + {props.hashtag.name} +
+
+
+ ) +} diff --git a/renderer/components/timelines/search/Statuses.tsx b/renderer/components/timelines/search/Statuses.tsx new file mode 100644 index 00000000..4f214dc5 --- /dev/null +++ b/renderer/components/timelines/search/Statuses.tsx @@ -0,0 +1,48 @@ +import { Entity, MegalodonInterface } from 'megalodon' +import { Virtuoso } from 'react-virtuoso' +import Status from '../status/Status' +import { Account } from '@/db' +import { Spinner } from '@material-tailwind/react' + +type Props = { + client: MegalodonInterface + account: Account + statuses: Array + loading: boolean + openMedia: (media: Array, index: number) => void +} + +export default function Statuses(props: Props) { + return ( + <> +
+ {props.statuses.length > 0 ? ( + ( + {}} + filters={[]} + /> + )} + /> + ) : ( + <> + {props.loading && ( +
+ +
+ )} + + )} +
+ + ) +} diff --git a/renderer/pages/_app.tsx b/renderer/pages/_app.tsx index 9579946c..053930a7 100644 --- a/renderer/pages/_app.tsx +++ b/renderer/pages/_app.tsx @@ -340,12 +340,7 @@ export default function MyApp({ Component, pageProps }: AppProps) { }, tabsHeader: { defaultProps: { - className: '' - }, - styles: { - base: { - bg: 'bg-blue-gray-50 dark:bg-blue-gray-800' - } + className: 'rounded-none border-b border-blue-gray-50 dark:border-blue-gray-900 bg-transparent p-0' } }, tab: { @@ -360,9 +355,6 @@ export default function MyApp({ Component, pageProps }: AppProps) { initial: { color: 'text-blue-gray-900 dark:text-blue-gray-200' } - }, - indicator: { - bg: 'bg-white dark:bg-gray-700' } } } diff --git a/renderer/pages/accounts/[id]/[timeline].tsx b/renderer/pages/accounts/[id]/[timeline].tsx index 0f72773d..8b3eaab3 100644 --- a/renderer/pages/accounts/[id]/[timeline].tsx +++ b/renderer/pages/accounts/[id]/[timeline].tsx @@ -4,6 +4,7 @@ import { useEffect, useReducer, useState } from 'react' import { Account, db } from '@/db' import generator, { Entity, MegalodonInterface } from 'megalodon' import Notifications from '@/components/timelines/Notifications' +import Search from '@/components/timelines/Search' import Media from '@/components/Media' import Report from '@/components/report/Report' @@ -69,14 +70,26 @@ export default function Page() { } /> ) : ( - , index: number) => - dispatch({ target: 'media', value: true, object: media, index: index }) - } - /> + <> + {(router.query.timeline as string) === 'search' ? ( + , index: number) => + dispatch({ target: 'media', value: true, object: media, index: index }) + } + /> + ) : ( + , index: number) => + dispatch({ target: 'media', value: true, object: media, index: index }) + } + /> + )} + )}