1
0
mirror of https://github.com/h3poteto/whalebird-desktop synced 2024-12-22 06:27:13 +01:00

refs #4792 Implement search for statuses

This commit is contained in:
AkiraFukushima 2024-06-24 01:05:55 +09:00
parent 83c7ed95a7
commit b2737bf8bd
No known key found for this signature in database
GPG Key ID: B6E51BAC4DE1A957
8 changed files with 198 additions and 25 deletions

View File

@ -183,6 +183,9 @@
"submit": "Submit"
},
"search": {
"placeholder": "Search timeline"
"placeholder": "Search timeline",
"statuses": "Statuses",
"accounts": "Accounts",
"hashtags": "Hashtags"
}
}

View File

@ -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' })}
/>

View File

@ -256,7 +256,12 @@ export default function Profile(props: Props) {
</div>
<div>
<Tabs value="timeline">
<TabsHeader>
<TabsHeader
indicatorProps={{
className:
'bg-blue-gray-50 dark:bg-blue-gray-900 border-b-2 border-blue-400 dark:border-blue-600 shadow-none rounded-none'
}}
>
<Tab value="timeline">
<FormattedMessage id="profile.timeline" />
</Tab>

View File

@ -0,0 +1,104 @@
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'
type Props = {
client: MegalodonInterface
account: Account
openMedia: (media: Array<Entity.Attachment>, 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<Entity.Results>({
accounts: [],
hashtags: [],
statuses: []
})
useEffect(() => {
if (router.query.q) {
setQuery(router.query.q as string)
search(router.query.q as string)
}
}, [router.query.q])
const search = (query: string) => {
setResults({
accounts: [],
hashtags: [],
statuses: []
})
props.client.search(query).then(res => {
setResults(res.data)
})
}
const submit = (ev: FormEvent<HTMLFormElement>) => {
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 (
<>
<div className="search w-full h-full">
<section className="h-full w-full">
<div className="w-full theme-text-primary p-2 flex justify-center" style={{ height: '56px' }}>
<form onSubmit={ev => submit(ev)}>
<Input
type="text"
color="blue-gray"
placeholder={formatMessage({ id: 'timeline.search' })}
className="placeholder:opacity-100"
value={query}
onChange={e => setQuery(e.target.value)}
icon={<FaMagnifyingGlass />}
/>
</form>
</div>
<Tabs value={activeTab} style={{ height: 'calc(100% - 56px)' }}>
<TabsHeader
indicatorProps={{
className: 'bg-blue-gray-50 dark:bg-blue-gray-900 border-b-2 border-blue-400 dark:border-blue-600 shadow-none rounded-none'
}}
>
<Tab value="statuses" onClick={() => setActiveTab('statuses')}>
<FormattedMessage id="search.statuses" />
</Tab>
<Tab value="accounts" onClick={() => setActiveTab('accounts')}>
<FormattedMessage id="search.accounts" />
</Tab>
<Tab value="hashtags" onClick={() => setActiveTab('hashtags')}>
<FormattedMessage id="search.hashtags" />
</Tab>
</TabsHeader>
<TabsBody style={{ height: 'calc(100% - 35px)' }}>
<TabPanel value="statuses" className="h-full p-0">
<Statuses statuses={results.statuses} client={props.client} account={props.account} openMedia={props.openMedia} />
</TabPanel>
<TabPanel value="accounts">accounts</TabPanel>
<TabPanel value="hashtags">hashtags</TabPanel>
</TabsBody>
</Tabs>
</section>
</div>
</>
)
}

View File

@ -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<HTMLFormElement>) => {
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 (
<div className="flex timeline-wrapper">
<section className={`h-full ${timelineClass()}`}>
@ -252,14 +265,13 @@ export default function Timeline(props: Props) {
{props.timeline.match(/list_(\d+)/) ? <>{list && list.title}</> : <FormattedMessage id={`timeline.${props.timeline}`} />}
</div>
<div className="w-64 text-xs">
<form>
<form onSubmit={ev => search(ev)}>
<Input
type="text"
label={formatMessage({ id: 'timeline.search' })}
disabled
color="blue-gray"
placeholder={formatMessage({ id: 'timeline.search' })}
containerProps={{ className: 'h-7' }}
className="!py-1 !px-2 !text-xs"
labelProps={{ className: '!leading-10' }}
className="!py-1 !px-2 !text-xs placeholder:opacity-100"
/>
</form>
</div>

View File

@ -0,0 +1,43 @@
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<Entity.Status>
openMedia: (media: Array<Entity.Attachment>, index: number) => void
}
export default function Statuses(props: Props) {
return (
<>
<div className="overflow-x-hidden h-full w-full">
{props.statuses.length > 0 ? (
<Virtuoso
style={{ height: '100%' }}
data={props.statuses}
className="timeline-scrollable"
itemContent={(index, status) => (
<Status
key={index}
client={props.client}
account={props.account}
status={status}
openMedia={props.openMedia}
onRefresh={() => {}}
filters={[]}
/>
)}
/>
) : (
<div className="py-4">
<Spinner className="m-auto" />
</div>
)}
</div>
</>
)
}

View File

@ -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'
}
}
}

View File

@ -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() {
}
/>
) : (
<Timeline
timeline={router.query.timeline as string}
account={account}
client={client}
openMedia={(media: Array<Entity.Attachment>, index: number) =>
dispatch({ target: 'media', value: true, object: media, index: index })
}
/>
<>
{(router.query.timeline as string) === 'search' ? (
<Search
client={client}
account={account}
openMedia={(media: Array<Entity.Attachment>, index: number) =>
dispatch({ target: 'media', value: true, object: media, index: index })
}
/>
) : (
<Timeline
timeline={router.query.timeline as string}
account={account}
client={client}
openMedia={(media: Array<Entity.Attachment>, index: number) =>
dispatch({ target: 'media', value: true, object: media, index: index })
}
/>
)}
</>
)}
<Media
open={modalState.media.opened}