Change component library to material-tailwind

This commit is contained in:
AkiraFukushima 2024-01-09 21:30:05 +09:00
parent 7c9708906e
commit b49c1e01a9
No known key found for this signature in database
GPG Key ID: B6E51BAC4DE1A957
27 changed files with 864 additions and 4371 deletions

View File

@ -91,7 +91,7 @@
},
"nsfw": "Sensitive",
"poll": {
"add": "Add a choice",
"add": "Add",
"5min": "5 minutes",
"30min": "30 minutes",
"1h": "1 hour",

View File

@ -16,14 +16,13 @@
},
"dependencies": {
"@emoji-mart/react": "^1.1.1",
"@material-tailwind/react": "^2.1.8",
"blurhash": "^2.0.5",
"dayjs": "^1.11.10",
"dexie": "^3.2.4",
"electron-serve": "^1.1.0",
"electron-store": "^8.1.0",
"emoji-mart": "^5.5.2",
"flowbite": "^2.0.0",
"flowbite-react": "^0.7.0",
"megalodon": "^9.1.1",
"react-blurhash": "^0.3.0",
"react-hotkeys-hook": "^4.4.1",
@ -36,7 +35,7 @@
"@babel/runtime-corejs3": "^7.23.2",
"@electron/notarize": "^2.1.0",
"@types/node": "^20.0.0",
"@types/react": "^18.0.26",
"@types/react": "18.2.19",
"@types/sanitize-html": "^2.9.4",
"@typescript-eslint/eslint-plugin": "^6.13.1",
"@typescript-eslint/parser": "^6.13.1",

View File

@ -1,4 +1,4 @@
import { Modal } from 'flowbite-react'
import { Dialog, DialogBody } from '@material-tailwind/react'
import { Entity } from 'megalodon'
type Props = {
@ -9,9 +9,8 @@ type Props = {
export default function Media(props: Props) {
return (
<Modal show={props.open} onClose={props.close} size="6xl">
<Modal.Header />
<Modal.Body className="max-h-full max-w-full">
<Dialog open={props.open} handler={props.close} size="lg">
<DialogBody className="max-h-full max-w-full">
{props.attachment && (
<img
src={props.attachment.url}
@ -20,7 +19,8 @@ export default function Media(props: Props) {
className="object-contain max-h-full max-w-full m-auto"
/>
)}
</Modal.Body>
</Modal>
<></>
</DialogBody>
</Dialog>
)
}

View File

@ -1,5 +1,5 @@
import { localeType } from '@/utils/i18n'
import { Label, Modal, Select, TextInput } from 'flowbite-react'
import { Dialog, DialogBody, DialogHeader, Input, Option, Select, Typography } from '@material-tailwind/react'
import { ChangeEvent, useEffect, useState } from 'react'
import { FormattedMessage } from 'react-intl'
@ -31,10 +31,9 @@ export default function Settings(props: Props) {
}
}, [])
const languageChanged = (e: ChangeEvent<HTMLSelectElement>) => {
setLanguage(e.target.value as localeType)
const languageChanged = (e: string) => {
if (typeof localStorage !== 'undefined') {
localStorage.setItem('language', e.target.value)
localStorage.setItem('language', e)
}
props.reloadSettings()
}
@ -48,40 +47,40 @@ export default function Settings(props: Props) {
}
return (
<Modal show={props.opened} onClose={props.close}>
<Modal.Header>
<Dialog open={props.opened} handler={props.close} size="sm">
<DialogHeader>
<FormattedMessage id="settings.title" />
</Modal.Header>
<Modal.Body>
</DialogHeader>
<DialogBody>
<div className="flex flex-col gap-4">
<div>
<div className="mb-2">
<Label htmlFor="fontsize">
<Typography>
<FormattedMessage id="settings.font_size" />
</Label>
</Typography>
</div>
<div>
<TextInput type="number" value={fontSize} onChange={fontSizeChanged} />
<Input type="number" value={fontSize} onChange={fontSizeChanged} />
</div>
</div>
<div>
<div className="mb-2">
<Label htmlFor="language">
<Typography>
<FormattedMessage id="settings.language" />
</Label>
</Typography>
</div>
<div>
<Select id="language" onChange={languageChanged} defaultValue={language}>
<Select id="language" onChange={languageChanged} value={language}>
{languages.map(lang => (
<option key={lang.value} value={lang.value}>
<Option key={lang.value} value={lang.value}>
{lang.label}
</option>
</Option>
))}
</Select>
</div>
</div>
</div>
</Modal.Body>
</Modal>
</DialogBody>
</Dialog>
)
}

View File

@ -1,8 +1,8 @@
import { Label, Modal, TextInput, Button, Alert, Spinner } from 'flowbite-react'
import generator, { MegalodonInterface, OAuth, detector } from 'megalodon'
import { useState } from 'react'
import { db } from '@/db'
import { FormattedMessage, useIntl } from 'react-intl'
import { Alert, Button, Dialog, DialogBody, DialogHeader, Input, Spinner, Typography } from '@material-tailwind/react'
type NewProps = {
opened: boolean
@ -107,13 +107,13 @@ export default function New(props: NewProps) {
return (
<>
<Modal dismissible={false} show={props.opened} onClose={close} size="lg">
<Modal.Header>
<Dialog open={props.opened} handler={close} size="xs">
<DialogHeader>
<FormattedMessage id="accounts.new.title" />
</Modal.Header>
<Modal.Body>
</DialogHeader>
<DialogBody>
{error && (
<Alert color="failure">
<Alert color="red">
<span>{error}</span>
</Alert>
)}
@ -121,12 +121,12 @@ export default function New(props: NewProps) {
{sns === null ? (
<>
<div className="block">
<Label htmlFor="domain">
<Typography>
<FormattedMessage id="accounts.new.domain" />
</Label>
</Typography>
</div>
<TextInput id="domain" placeholder="mastodon.social" required type="text" />
<Button color="blue" onClick={checkDomain} disabled={loading}>
<Input type="text" id="domain" placeholder="mastodon.social" />
<Button onClick={checkDomain} loading={loading} color="blue">
<FormattedMessage id="accounts.new.sign_in" />
</Button>
</>
@ -143,31 +143,31 @@ export default function New(props: NewProps) {
) : (
<>
<div className="block">
<Label htmlFor="authorization">
<Typography>
<FormattedMessage id="accounts.new.authorization_code" />
</Label>
<p className="text-sm text-gray-600">
</Typography>
<Typography variant="small">
<FormattedMessage id="accounts.new.authorization_helper" />
</p>
</Typography>
</div>
<TextInput id="authorization" required type="text" />
<Input id="authorization" type="text" />
</>
)}
<Button onClick={authorize} disabled={loading}>
<Button onClick={authorize} disabled={loading} color="blue">
<FormattedMessage id="accounts.new.authorize" />
</Button>
</>
) : (
<div className="text-center">
<Spinner aria-label="Loading" />
<Spinner />
</div>
)}
</>
)}
</form>
</Modal.Body>
</Modal>
</DialogBody>
</Dialog>
</>
)
}

View File

@ -1,17 +1,3 @@
import {
Button,
Checkbox,
CustomFlowbiteTheme,
Dropdown,
Flowbite,
Label,
Radio,
Select,
Spinner,
TextInput,
Textarea,
ToggleSwitch
} from 'flowbite-react'
import { ChangeEvent, Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from 'react'
import { FormattedMessage, useIntl } from 'react-intl'
import {
@ -31,6 +17,22 @@ import { useToast } from '@/utils/toast'
import Picker from '@emoji-mart/react'
import { data } from '@/utils/emojiData'
import EditMedia from './EditMedia'
import {
Button,
Checkbox,
IconButton,
Input,
List,
ListItem,
Option,
Popover,
PopoverContent,
PopoverHandler,
Radio,
Select,
Switch,
Textarea
} from '@material-tailwind/react'
type Props = {
client: MegalodonInterface
@ -43,17 +45,6 @@ type Poll = {
multiple: boolean
}
const customTheme: CustomFlowbiteTheme = {
dropdown: {
content: 'focus:outline-none',
floating: {
item: {
base: 'hidden'
}
}
}
}
export default function Compose(props: Props) {
const [body, setBody] = useState('')
const [visibility, setVisibility] = useState<'public' | 'unlisted' | 'private' | 'direct'>('public')
@ -65,11 +56,13 @@ export default function Compose(props: Props) {
const [editMedia, setEditMedia] = useState<Entity.Attachment>()
const [maxCharacters, setMaxCharacters] = useState<number | null>(null)
const [remaining, setRemaining] = useState<number | null>(null)
const [popoverVisibility, setPopoverVisibility] = useState(false)
const [popoverEmoji, setPopoverEmoji] = useState(false)
const { formatMessage } = useIntl()
const uploaderRef = useRef(null)
const showToast = useToast()
const textareaRef = useRef<HTMLTextAreaElement>(null)
const textareaRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (!props.client) return
@ -228,19 +221,18 @@ export default function Compose(props: Props) {
} else if (emoji.shortcodes) {
setBody(current => `${current.slice(0, cursor)}${emoji.shortcodes} ${current.slice(cursor)}`)
}
const dummy = document.getElementById('dummy-emoji-picker')
dummy.click()
setPopoverEmoji(false)
}
return (
<div className="px-4 pb-4">
<form id="form">
{cw && (
<TextInput
<Input
id="spoiler"
type="text"
sizing="sm"
className="mb-2"
color="blue"
containerProps={{ className: 'mb-2' }}
value={spoiler}
onChange={ev => setSpoiler(ev.target.value)}
placeholder={formatMessage({ id: 'compose.spoiler.placeholder' })}
@ -249,6 +241,7 @@ export default function Compose(props: Props) {
<div className="relative">
<Textarea
id="body"
color="blue"
className="resize-none focus:ring-0"
placeholder={formatMessage({ id: 'compose.placeholder' })}
rows={3}
@ -256,22 +249,16 @@ export default function Compose(props: Props) {
onChange={ev => setBody(ev.target.value)}
ref={textareaRef}
/>
<Flowbite theme={{ theme: customTheme }}>
<Dropdown
label=""
dismissOnClick
renderTrigger={() => (
<span className="absolute top-1 right-1 text-gray-600 cursor-pointer">
<FaFaceLaughBeam />
</span>
)}
>
<Popover open={popoverEmoji} handler={setPopoverEmoji}>
<PopoverHandler>
<span className="absolute top-1 right-1 text-gray-600 cursor-pointer">
<FaFaceLaughBeam />
</span>
</PopoverHandler>
<PopoverContent>
<Picker data={data} onEmojiSelect={onEmojiSelect} previewPosition="none" set="native" perLine="7" theme="light" />
<Dropdown.Item>
<span id="dummy-emoji-picker" />
</Dropdown.Item>
</Dropdown>
</Flowbite>
</PopoverContent>
</Popover>
</div>
</form>
{poll && <PollForm poll={poll} setPoll={setPoll} />}
@ -291,48 +278,84 @@ export default function Compose(props: Props) {
{attachments.length > 0 && (
<div>
<Checkbox id="sensitive" className="focus:ring-0" />
<Label htmlFor="sensitive" className="pl-2 text-gray-600">
<FormattedMessage id="compose.nsfw" />
</Label>
<Checkbox id="sensitive" label={formatMessage({ id: 'compose.nsfw' })} />
</div>
)}
<div className="w-full flex justify-between mt-1 items-center h-5">
<div className="ml-1 flex gap-3">
<input type="file" id="file" className="hidden" ref={uploaderRef} onChange={fileChanged} />
<FaPaperclip className="text-gray-400 hover:text-gray-600 cursor-pointer" onClick={selectFile} />
<FaListCheck className="text-gray-400 hover:text-gray-600 cursor-pointer" onClick={togglePoll} />
<Dropdown label="" dismissOnClick={true} placement="top" renderTrigger={() => visibilityIcon(visibility)}>
<Dropdown.Item onClick={() => setVisibility('public')}>
<FormattedMessage id="compose.visibility.public" />
</Dropdown.Item>
<Dropdown.Item onClick={() => setVisibility('unlisted')}>
<FormattedMessage id="compose.visibility.unlisted" />
</Dropdown.Item>
<Dropdown.Item onClick={() => setVisibility('private')}>
<FormattedMessage id="compose.visibility.private" />
</Dropdown.Item>
<Dropdown.Item onClick={() => setVisibility('direct')}>
<FormattedMessage id="compose.visibility.direct" />
</Dropdown.Item>
</Dropdown>
<IconButton variant="text" size="sm" onClick={selectFile} className="text-gray-400 hover:text-gray-600 text-base">
<FaPaperclip />
</IconButton>
<IconButton variant="text" size="sm" onClick={togglePoll} className="text-gray-400 hover:text-gray-600 text-base">
<FaListCheck />
</IconButton>
<Popover open={popoverVisibility} handler={setPopoverVisibility}>
<PopoverHandler>{visibilityIcon(visibility)}</PopoverHandler>
<PopoverContent>
<List>
<ListItem
onClick={() => {
setVisibility('public')
setPopoverVisibility(false)
}}
>
<FormattedMessage id="compose.visibility.public" />
</ListItem>
<ListItem
onClick={() => {
setVisibility('unlisted')
setPopoverVisibility(false)
}}
>
<FormattedMessage id="compose.visibility.unlisted" />
</ListItem>
<ListItem
onClick={() => {
setVisibility('private')
setPopoverVisibility(false)
}}
>
<FormattedMessage id="compose.visibility.private" />
</ListItem>
<ListItem
onClick={() => {
setVisibility('direct')
setPopoverVisibility(false)
}}
>
<FormattedMessage id="compose.visibility.direct" />
</ListItem>
</List>
</PopoverContent>
</Popover>
{cw ? (
<span className="text-blue-400 hover:text-blue-600 leading-4 cursor-pointer" onClick={() => setCW(false)}>
<IconButton
variant="text"
size="sm"
className="text-blue-400 hover:text-blue-600 leading-4 text-base"
onClick={() => setCW(false)}
>
CW
</span>
</IconButton>
) : (
<span className="text-gray-400 hover:text-gray-600 leading-4 cursor-pointer" onClick={() => setCW(true)}>
<IconButton
variant="text"
size="sm"
className="text-gray-400 hover:text-gray-600 leading-4 text-base"
onClick={() => setCW(true)}
>
CW
</span>
</IconButton>
)}
</div>
<div className="mr-1">
<div className="mr-1 flex items-center gap-2">
<span className="text-gray-400">{remaining}</span>
<button className="ml-2 text-gray-400 hover:text-gray-600" disabled={loading} onClick={post}>
{loading ? <Spinner size="sm" /> : <FaPaperPlane />}
</button>
<IconButton disabled={loading} onClick={post} variant="text" size="sm">
<FaPaperPlane className="text-base text-gray-600" />
</IconButton>
</div>
</div>
<EditMedia media={editMedia} close={closeDescription} client={props.client} />
@ -344,27 +367,27 @@ const visibilityIcon = (visibility: 'public' | 'unlisted' | 'private' | 'direct'
switch (visibility) {
case 'public':
return (
<span>
<FaGlobe className="text-gray-400 hover:text-gray-600 cursor-pointer" />
</span>
<IconButton variant="text" size="sm" className="text-gray-400 hover:text-gray-600 text-base">
<FaGlobe />
</IconButton>
)
case 'unlisted':
return (
<span>
<FaLockOpen className="text-gray-400 hover:text-gray-600 cursor-pointer" />
</span>
<IconButton variant="text" size="sm" className="text-gray-400 hover:text-gray-600 text-base">
<FaLockOpen />
</IconButton>
)
case 'private':
return (
<span>
<FaLock className="text-gray-400 hover:text-gray-600 cursor-pointer" />
</span>
<IconButton variant="text" size="sm" className="text-gray-400 hover:text-gray-600 text-base">
<FaLock />
</IconButton>
)
case 'direct':
return (
<span>
<FaEnvelope className="text-gray-400 hover:text-gray-600 cursor-pointer" />
</span>
<IconButton variant="text" size="sm" className="text-gray-400 hover:text-gray-600 text-base">
<FaEnvelope />
</IconButton>
)
}
}
@ -417,10 +440,11 @@ const PollForm = (props: PollProps) => {
)
}
const changeMultiple = (value: boolean) => {
const changeMultiple = (ev: ChangeEvent<HTMLInputElement>) => {
console.log(ev)
props.setPoll(current =>
Object.assign({}, current, {
multiple: value
multiple: ev.target.checked
})
)
}
@ -437,29 +461,42 @@ const PollForm = (props: PollProps) => {
<div className="pt-1">
{props.poll.options.map((option, index) => (
<div className="flex items-center gap-3 py-1" key={index}>
{props.poll.multiple ? <Checkbox disabled /> : <Radio disabled />}
<TextInput sizing="sm" value={option} onChange={ev => updateOption(index, ev.target.value)} />
{props.poll.multiple ? (
<Checkbox disabled containerProps={{ className: 'p-1' }} />
) : (
<Radio disabled containerProps={{ className: 'p-1' }} />
)}
<Input
type="text"
color="blue"
value={option}
onChange={ev => updateOption(index, ev.target.value)}
containerProps={{ className: 'h-8' }}
/>
<FaXmark className="text-gray-400 cursor-pointer" onClick={() => removeOption(index)} />
</div>
))}
<div className="flex gap-3 pt-2">
<Button onClick={addOption} color="light">
<Button onClick={addOption} size="sm" color="indigo" variant="outlined">
<FormattedMessage id="compose.poll.add" />
</Button>
<Select id="expires" onChange={e => changeExpire(parseInt(e.target.value))}>
<Select
id="expires"
color="blue"
value={`${props.poll.expires_in}`}
onChange={e => changeExpire(parseInt(e))}
containerProps={{ className: 'h-8' }}
>
{expiresList.map((expire, index) => (
<option value={expire.value} key={index}>
<Option value={`${expire.value}`} key={index}>
{expire.label}
</option>
</Option>
))}
</Select>
</div>
<ToggleSwitch
checked={props.poll.multiple}
onChange={v => changeMultiple(v)}
className="mt-2"
label={formatMessage({ id: 'compose.poll.multiple' })}
/>
<div className="mt-2">
<Switch checked={props.poll.multiple} onChange={v => changeMultiple(v)} label={formatMessage({ id: 'compose.poll.multiple' })} />
</div>
</div>
)
}

View File

@ -1,4 +1,4 @@
import { Label, Modal, Textarea, Button } from 'flowbite-react'
import { Button, Dialog, DialogBody, DialogHeader, Textarea, Typography } from '@material-tailwind/react'
import { Entity, MegalodonInterface } from 'megalodon'
import { useEffect, useState } from 'react'
import { FormattedMessage } from 'react-intl'
@ -37,20 +37,20 @@ export default function EditMedia(props: Props) {
}
return (
<Modal show={props.media !== undefined} onClose={onClose} size="2xl">
<Dialog open={props.media !== undefined} handler={onClose} size="xl">
{props.media && (
<>
<Modal.Header>
<DialogHeader>
<FormattedMessage id="compose.edit_media.title" />
</Modal.Header>
<Modal.Body className="max-h-full max-w-full">
</DialogHeader>
<DialogBody className="max-h-full max-w-full">
<div className="flex gap-2">
<div className="w-1/4">
<Label htmlFor="description" className="mb-2 block">
<Typography>
<FormattedMessage id="compose.edit_media.label" />
</Label>
</Typography>
<Textarea id="description" rows={4} value={description} onChange={ev => setDescription(ev.target.value)} />
<Button color="blue" className="mt-2" onClick={submit}>
<Button className="mt-2" onClick={submit}>
<FormattedMessage id="compose.edit_media.submit" />
</Button>
</div>
@ -58,9 +58,10 @@ export default function EditMedia(props: Props) {
<img src={props.media.preview_url} className="object-cover m-auto" />
</div>
</div>
</Modal.Body>
</DialogBody>
</>
)}
</Modal>
<></>
</Dialog>
)
}

View File

@ -1,14 +1,28 @@
import emojify from '@/utils/emojify'
import { Avatar, Button, CustomFlowbiteTheme, Dropdown, Flowbite, Tabs } from 'flowbite-react'
import { Entity, MegalodonInterface } from 'megalodon'
import { MouseEventHandler, useEffect, useState } from 'react'
import { FaEllipsisVertical } from 'react-icons/fa6'
import { FormattedMessage, useIntl } from 'react-intl'
import { FormattedMessage } from 'react-intl'
import Timeline from './profile/Timeline'
import Followings from './profile/Followings'
import Followers from './profile/Followers'
import { findLink } from '@/utils/statusParser'
import { Account } from '@/db'
import {
Avatar,
Button,
IconButton,
List,
ListItem,
Popover,
PopoverContent,
PopoverHandler,
Tab,
TabPanel,
Tabs,
TabsBody,
TabsHeader
} from '@material-tailwind/react'
type Props = {
client: MegalodonInterface
@ -17,20 +31,10 @@ type Props = {
openMedia: (media: Entity.Attachment) => void
}
const customTheme: CustomFlowbiteTheme = {
tabs: {
tablist: {
tabitem: {
base: 'flex items-center justify-center p-4 rounded-t-lg text-sm font-medium first:ml-0 disabled:cursor-not-allowed disabled:text-gray-400 disabled:dark:text-gray-500'
}
}
}
}
export default function Profile(props: Props) {
const [user, setUser] = useState<Entity.Account | null>(null)
const [relationship, setRelationship] = useState<Entity.Relationship | null>(null)
const { formatMessage } = useIntl()
const [popoverDetail, setPopoverDetail] = useState(false)
useEffect(() => {
const f = async () => {
@ -69,74 +73,92 @@ export default function Profile(props: Props) {
return (
<div style={{ height: 'calc(100% - 50px)' }} className="overflow-y-auto timeline-scrollable">
<Flowbite theme={{ theme: customTheme }}>
{user && relationship && (
<>
<div className="header-image w-full bg-gray-100">
<img src={user.header} alt="header image" className="w-full object-cover h-40" />
</div>
<div className="p-5">
<div className="flex items-end justify-between" style={{ marginTop: '-50px' }}>
<Avatar img={user.avatar} size="lg" stacked />
<div className="flex gap-2">
{relationship.following ? (
<Button color="failure" onClick={() => unfollow(user.id)}>
<FormattedMessage id="profile.unfollow" />
</Button>
) : (
<Button color="blue" onClick={() => follow(user.id)}>
<FormattedMessage id="profile.follow" />
</Button>
)}
<Dropdown
label=""
renderTrigger={() => (
<Button color="gray">
<FaEllipsisVertical />
</Button>
)}
>
<Dropdown.Item onClick={() => openOriginal(user.url)}>
<FormattedMessage id="profile.open_original" />
</Dropdown.Item>
</Dropdown>
</div>
</div>
<div className="pt-4">
<div className="font-bold" dangerouslySetInnerHTML={{ __html: emojify(user.display_name, user.emojis) }} />
<div className="text-gray-500">@{user.acct}</div>
<div className="mt-4 raw-html profile" onClick={profileClicked}>
<span
dangerouslySetInnerHTML={{ __html: emojify(user.note, user.emojis) }}
className="overflow-hidden break-all text-gray-800"
/>
</div>
<div className="bg-gray-100 overflow-hidden break-all raw-html mt-2 profile" onClick={profileClicked}>
{user.fields.map((data, index) => (
<dl key={index} className="px-4 py-2 border-gray-200 border-b">
<dt className="text-gray-500">{data.name}</dt>
<dd className="text-gray-700" dangerouslySetInnerHTML={{ __html: emojify(data.value, user.emojis) }} />
</dl>
))}
</div>
{user && relationship && (
<>
<div className="header-image w-full bg-gray-100">
<img src={user.header} alt="header image" className="w-full object-cover h-40" />
</div>
<div className="p-5">
<div className="flex items-end justify-between" style={{ marginTop: '-50px' }}>
<Avatar src={user.avatar} size="xl" variant="rounded" />
<div className="flex gap-2">
{relationship.following ? (
<Button color="red" onClick={() => unfollow(user.id)}>
<FormattedMessage id="profile.unfollow" />
</Button>
) : (
<Button color="blue" onClick={() => follow(user.id)}>
<FormattedMessage id="profile.follow" />
</Button>
)}
<Popover open={popoverDetail} handler={setPopoverDetail}>
<PopoverHandler>
<IconButton variant="outlined">
<FaEllipsisVertical />
</IconButton>
</PopoverHandler>
<PopoverContent>
<List>
<ListItem
onClick={() => {
openOriginal(user.url)
setPopoverDetail(false)
}}
>
<FormattedMessage id="profile.open_original" />
</ListItem>
</List>
</PopoverContent>
</Popover>
</div>
</div>
<div>
<Tabs aria-label="Tabs with icons" style="underline">
<Tabs.Item active title={formatMessage({ id: 'profile.timeline' })}>
<div className="pt-4">
<div className="font-bold" dangerouslySetInnerHTML={{ __html: emojify(user.display_name, user.emojis) }} />
<div className="text-gray-500">@{user.acct}</div>
<div className="mt-4 raw-html profile" onClick={profileClicked}>
<span
dangerouslySetInnerHTML={{ __html: emojify(user.note, user.emojis) }}
className="overflow-hidden break-all text-gray-800"
/>
</div>
<div className="bg-gray-100 overflow-hidden break-all raw-html mt-2 profile" onClick={profileClicked}>
{user.fields.map((data, index) => (
<dl key={index} className="px-4 py-2 border-gray-200 border-b">
<dt className="text-gray-500">{data.name}</dt>
<dd className="text-gray-700" dangerouslySetInnerHTML={{ __html: emojify(data.value, user.emojis) }} />
</dl>
))}
</div>
</div>
</div>
<div>
<Tabs value="timeline">
<TabsHeader>
<Tab value="timeline">
<FormattedMessage id="profile.timeline" />
</Tab>
<Tab value="followings">
<FormattedMessage id="profile.followings" />
</Tab>
<Tab value="followers">
<FormattedMessage id="profile.followers" />
</Tab>
</TabsHeader>
<TabsBody>
<TabPanel value="timeline">
<Timeline client={props.client} account={props.account} user_id={props.user_id} openMedia={props.openMedia} />
</Tabs.Item>
<Tabs.Item title={formatMessage({ id: 'profile.followings' })}>
</TabPanel>
<TabPanel value="followings">
<Followings client={props.client} user_id={props.user_id} />
</Tabs.Item>
<Tabs.Item title={formatMessage({ id: 'profile.followers' })} className="focus:ring-0">
</TabPanel>
<TabPanel value="followers">
<Followers client={props.client} user_id={props.user_id} />
</Tabs.Item>
</Tabs>
</div>
</>
)}
</Flowbite>
</TabPanel>
</TabsBody>
</Tabs>
</div>
</>
)}
</div>
)
}

View File

@ -1,5 +1,5 @@
import emojify from '@/utils/emojify'
import { Avatar } from 'flowbite-react'
import { Avatar } from '@material-tailwind/react'
import { Entity } from 'megalodon'
import { useRouter } from 'next/router'
@ -18,7 +18,7 @@ export default function User(props: Props) {
<div className="border-b mr-2 py-1">
<div className="flex" onClick={() => openUser(props.user.id)}>
<div className="p2 cursor-pointer" style={{ width: '56px' }}>
<Avatar img={props.user.avatar} />
<Avatar src={props.user.avatar} />
</div>
<div>
<p className="text-gray-800" dangerouslySetInnerHTML={{ __html: emojify(props.user.display_name, props.user.emojis) }} />

View File

@ -1,15 +1,15 @@
import { CSSProperties, useContext, useEffect, useRef, useState } from 'react'
import { FaGear, FaPlus } from 'react-icons/fa6'
import { FaGear, FaPlus, FaTrash } from 'react-icons/fa6'
import { Account, db } from '@/db'
import NewAccount from '@/components/accounts/New'
import Settings from '@/components/Settings'
import { Avatar, Dropdown } from 'flowbite-react'
import { useRouter } from 'next/router'
import { FormattedMessage, useIntl } from 'react-intl'
import generateNotification from '@/utils/notification'
import generator, { Entity, WebSocketInterface } from 'megalodon'
import { Context } from '@/utils/i18n'
import { useHotkeys } from 'react-hotkeys-hook'
import { Avatar, IconButton, List, ListItem, ListItemPrefix, Popover, PopoverContent, PopoverHandler } from '@material-tailwind/react'
type LayoutProps = {
children: React.ReactNode
@ -20,6 +20,8 @@ export default function Layout({ children }: LayoutProps) {
const [openNewModal, setOpenNewModal] = useState(false)
const [openSettings, setOpenSettings] = useState(false)
const [style, setStyle] = useState<CSSProperties>({})
const [openPopover, setOpenPopover] = useState(false)
const { switchLang } = useContext(Context)
const router = useRouter()
const { formatMessage } = useIntl()
@ -91,8 +93,6 @@ export default function Layout({ children }: LayoutProps) {
document.getElementById(`${id}`).click()
}
const dropdownTrigger = (accountId: number) => <span id={`${accountId}`} className="" />
const removeAccount = async (id: number) => {
await db.accounts.delete(id)
const acct = await db.accounts.toArray()
@ -105,9 +105,9 @@ export default function Layout({ children }: LayoutProps) {
const selectedClassName = (id: number) => {
if (id === parseInt(router.query.id as string)) {
return 'bg-blue-950 cursor-pointer'
return 'bg-blue-950 cursor-pointer text-center'
} else {
return 'cursor-pointer'
return 'cursor-pointer text-center'
}
}
@ -131,44 +131,61 @@ export default function Layout({ children }: LayoutProps) {
<div>
{accounts.map(account => (
<div key={account.id} className={selectedClassName(account.id)}>
<Popover>
<PopoverHandler>
<span id={`${account.id}`} />
</PopoverHandler>
<PopoverContent>
<List className="py-2 px-0">
<ListItem onClick={() => removeAccount(account.id)} className="py-2 px-4 rounded-none">
<ListItemPrefix>
<FaTrash />
</ListItemPrefix>
<FormattedMessage id="accounts.remove" />
</ListItem>
</List>
</PopoverContent>
</Popover>
<Avatar
alt={account.domain}
img={account.avatar}
rounded
key={account.id}
className="py-2"
src={account.avatar}
className="p-1"
onClick={() => openAccount(account.id)}
onContextMenu={() => openContextMenu(account.id)}
/>
<Dropdown label="" dismissOnClick={true} renderTrigger={() => dropdownTrigger(account.id)}>
<Dropdown.Item onClick={() => removeAccount(account.id)}>
<FormattedMessage id="accounts.remove" />
</Dropdown.Item>
</Dropdown>
</div>
))}
<button className="py-4 px-6 items-center" onClick={() => setOpenNewModal(true)}>
<FaPlus className="text-gray-400" />
</button>
<div className="flex flex-col items-center">
<IconButton variant="text" size="lg" onClick={() => setOpenNewModal(true)}>
<FaPlus className="text-gray-400 text-xl" />
</IconButton>
</div>
<NewAccount opened={openNewModal} close={closeNewModal} />
</div>
<div className="settings text-gray-400 py-4 px-6 items-center">
<div className="relative cursor-pointer">
<Dropdown
label=""
dismissOnClick
renderTrigger={() => (
<span>
<FaGear />
</span>
)}
placement="right-start"
>
<Dropdown.Item onClick={() => setOpenSettings(true)}>
<FormattedMessage id="settings.title" />{' '}
</Dropdown.Item>
</Dropdown>
</div>
<div className="settings text-gray-400 flex flex-col items-center mb-2">
<Popover open={openPopover} handler={setOpenPopover}>
<PopoverHandler>
<IconButton variant="text" size="lg">
<FaGear className="text-gray-400 text-xl" />
</IconButton>
</PopoverHandler>
<PopoverContent>
<List className="py-2 px-0">
<ListItem
onClick={() => {
setOpenSettings(true)
setOpenPopover(false)
}}
className="py-2 px-4 rounded-none"
>
<ListItemPrefix>
<FaGear />
</ListItemPrefix>
<FormattedMessage id="settings.title" />
</ListItem>
</List>
</PopoverContent>
</Popover>
</div>
</aside>
{children}

View File

@ -1,5 +1,5 @@
import { Account, db } from '@/db'
import { CustomFlowbiteTheme, Flowbite, Sidebar } from 'flowbite-react'
import { Card, List, ListItem, ListItemPrefix } from '@material-tailwind/react'
import generator, { Entity } from 'megalodon'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
@ -10,22 +10,6 @@ type LayoutProps = {
children: React.ReactNode
}
const customTheme: CustomFlowbiteTheme = {
sidebar: {
root: {
inner: 'h-full overflow-y-auto overflow-x-hidden bg-blue-950 py-4 px-3 dark:bg-blue-950'
},
item: {
base: 'flex items-center justify-center rounded-lg p-2 text-base font-normal text-blue-200 hover:bg-blue-900 dark:text-blue-200 dark:hover:bg-blue-900 cursor-pointer',
active: 'bg-blue-400 text-gray-800 hover:bg-blue-300',
icon: {
base: 'h-5 w-5 flex-shrink-0 text-gray-500 transition duration-75 text-blue-200 group-hover:text-blue-900 dark:text-blue-200 dark:group-hover:text-blue-900',
active: 'text-gray-800'
}
}
}
}
export default function Layout({ children }: LayoutProps) {
const router = useRouter()
const { formatMessage } = useIntl()
@ -58,65 +42,63 @@ export default function Layout({ children }: LayoutProps) {
{
id: 'home',
title: formatMessage({ id: 'timeline.home' }),
icon: FaHouse,
icon: <FaHouse />,
path: `/accounts/${router.query.id}/home`
},
{
id: 'notifications',
title: formatMessage({ id: 'timeline.notifications' }),
icon: FaBell,
icon: <FaBell />,
path: `/accounts/${router.query.id}/notifications`
},
{
id: 'local',
title: formatMessage({ id: 'timeline.local' }),
icon: FaUsers,
icon: <FaUsers />,
path: `/accounts/${router.query.id}/local`
},
{
id: 'public',
title: formatMessage({ id: 'timeline.public' }),
icon: FaGlobe,
icon: <FaGlobe />,
path: `/accounts/${router.query.id}/public`
}
]
return (
<section className="flex h-screen w-full overflow-hidden">
<Flowbite theme={{ theme: customTheme }}>
<Sidebar className="text-blue-200 sidebar">
<div className="max-w-full pl-4 mt-2 mb-4 my-profile">
<p>{account?.username}</p>
<p>@{account?.domain}</p>
</div>
<Sidebar.Items>
<Sidebar.ItemGroup>
{pages.map(page => (
<Sidebar.Item
key={page.id}
active={router.asPath.includes(page.path)}
onClick={() => router.push(page.path)}
icon={page.icon}
className="sidebar-menu-item"
>
<span className="sidebar-menu">{page.title}</span>
</Sidebar.Item>
))}
{lists.map(list => (
<Sidebar.Item
key={list.id}
active={router.asPath.includes(`list_${list.id}`)}
onClick={() => router.push({ pathname: `/accounts/${router.query.id}/list_${list.id}` })}
icon={FaList}
className="sidebar-menu-item"
>
<span className="sidebar-menu">{list.title}</span>
</Sidebar.Item>
))}
</Sidebar.ItemGroup>
</Sidebar.Items>
</Sidebar>
</Flowbite>
<Card className="text-blue-100 sidebar w-64 bg-blue-950 rounded-none">
<div className="max-w-full pl-4 mt-2 mb-4 my-profile">
<p>{account?.username}</p>
<p>@{account?.domain}</p>
</div>
<List className="min-w-[64px]">
{pages.map(page => (
<ListItem
key={page.id}
selected={router.asPath.includes(page.path)}
onClick={() => router.push(page.path)}
className="sidebar-menu-item text-blue-100"
>
<ListItemPrefix>{page.icon}</ListItemPrefix>
<span className="sidebar-menu">{page.title}</span>
</ListItem>
))}
{lists.map(list => (
<ListItem
key={list.id}
selected={router.asPath.includes(`list_${list.id}`)}
onClick={() => router.push({ pathname: `/accounts/${router.query.id}/list_${list.id}` })}
className="sidebar-menu-item text-blue-100"
>
<ListItemPrefix>
<FaList />
</ListItemPrefix>
<span className="sidebar-menu">{list.title}</span>
</ListItem>
))}
</List>
</Card>
{children}
</section>
)

View File

@ -1,4 +1,4 @@
import { Button, Label, Modal, Textarea } from 'flowbite-react'
import { Button, Dialog, DialogBody, DialogHeader, Textarea, Typography } from '@material-tailwind/react'
import { Entity, MegalodonInterface } from 'megalodon'
import { FormattedMessage } from 'react-intl'
@ -21,23 +21,24 @@ export default function Report(props: Props) {
}
return (
<Modal show={props.open} onClose={props.close} size="xl">
<Modal.Header>
<Dialog open={props.open} handler={props.close} size="md">
<DialogHeader>
<FormattedMessage id="report.title" values={{ user: `@${props.status.account.username}` }} />
</Modal.Header>
<Modal.Body className="max-h-full max-w-full">
</DialogHeader>
<DialogBody className="max-h-full max-w-full">
<form>
<div className="block">
<Label htmlFor="comment">
<Typography>
<FormattedMessage id="report.detail" />
</Label>
</Typography>
<Textarea id="comment" />
<Textarea id="comment" rows={4} />
</div>
<Button color="blue" className="mt-2" onClick={submit}>
<Button className="mt-2" onClick={submit}>
<FormattedMessage id="report.submit" />
</Button>
</form>
</Modal.Body>
</Modal>
</DialogBody>
</Dialog>
)
}

View File

@ -1,10 +1,10 @@
import { Account } from '@/db'
import { TextInput } from 'flowbite-react'
import generator, { Entity, MegalodonInterface, WebSocketInterface } from 'megalodon'
import { useEffect, useState, useCallback, useRef, Dispatch, SetStateAction } from 'react'
import { FormattedMessage, useIntl } from 'react-intl'
import { Virtuoso } from 'react-virtuoso'
import Notification from './notification/Notification'
import { Input, Spinner } from '@material-tailwind/react'
const TIMELINE_STATUSES_COUNT = 30
const TIMELINE_MAX_STATUSES = 2147483647
@ -107,32 +107,38 @@ export default function Notifications(props: Props) {
</div>
<div className="w-64 text-xs">
<form>
<TextInput type="text" placeholder={formatMessage({ id: 'timeline.search' })} disabled sizing="sm" />
<Input type="text" placeholder={formatMessage({ id: 'timeline.search' })} disabled />
</form>
</div>
</div>
<div className="timeline overflow-y-auto w-full overflow-x-hidden" style={{ height: 'calc(100% - 50px)' }}>
<Virtuoso
style={{ height: '100%' }}
scrollerRef={ref => {
scrollerRef.current = ref as HTMLElement
}}
firstItemIndex={firstItemIndex}
atTopStateChange={prependUnreads}
className="timeline-scrollable"
data={notifications}
endReached={loadMore}
itemContent={(_, notification) => (
<Notification
client={props.client}
account={props.account}
notification={notification}
onRefresh={updateStatus}
key={notification.id}
openMedia={media => props.setAttachment(media)}
/>
)}
/>
{notifications.length > 0 ? (
<Virtuoso
style={{ height: '100%' }}
scrollerRef={ref => {
scrollerRef.current = ref as HTMLElement
}}
firstItemIndex={firstItemIndex}
atTopStateChange={prependUnreads}
className="timeline-scrollable"
data={notifications}
endReached={loadMore}
itemContent={(_, notification) => (
<Notification
client={props.client}
account={props.account}
notification={notification}
onRefresh={updateStatus}
key={notification.id}
openMedia={media => props.setAttachment(media)}
/>
)}
/>
) : (
<div className="w-full" style={{ height: '100%' }}>
<Spinner className="m-auto mt-6" />
</div>
)}
</div>
</section>
)

View File

@ -1,5 +1,4 @@
import { Account } from '@/db'
import { TextInput } from 'flowbite-react'
import generator, { Entity, MegalodonInterface, WebSocketInterface } from 'megalodon'
import { useEffect, useState, useCallback, useRef, Dispatch, SetStateAction } from 'react'
import { Virtuoso } from 'react-virtuoso'
@ -9,6 +8,7 @@ import Detail from '../detail/Detail'
import { useRouter } from 'next/router'
import Compose from '../compose/Compose'
import { useHotkeys } from 'react-hotkeys-hook'
import { Input, Spinner } from '@material-tailwind/react'
const TIMELINE_STATUSES_COUNT = 30
const TIMELINE_MAX_STATUSES = 2147483647
@ -189,32 +189,46 @@ export default function Timeline(props: Props) {
</div>
<div className="w-64 text-xs">
<form>
<TextInput type="text" placeholder={formatMessage({ id: 'timeline.search' })} disabled sizing="sm" />
<Input
type="text"
label={formatMessage({ id: 'timeline.search' })}
disabled
containerProps={{ className: 'h-7' }}
className="!py-1 !px-2 !text-xs"
labelProps={{ className: '!leading-10' }}
/>
</form>
</div>
</div>
<div className="overflow-x-hidden" style={{ height: 'calc(100% - 50px)' }}>
<Virtuoso
style={{ height: `calc(100% - ${composeHeight}px)` }}
scrollerRef={ref => {
scrollerRef.current = ref as HTMLElement
}}
className="timeline-scrollable"
firstItemIndex={firstItemIndex}
atTopStateChange={prependUnreads}
data={statuses}
endReached={loadMore}
itemContent={(_, status) => (
<Status
client={props.client}
account={props.account}
status={status}
key={status.id}
onRefresh={status => setStatuses(current => updateStatus(current, status))}
openMedia={media => props.setAttachment(media)}
/>
)}
/>
{statuses.length > 0 ? (
<Virtuoso
style={{ height: `calc(100% - ${composeHeight}px)` }}
scrollerRef={ref => {
scrollerRef.current = ref as HTMLElement
}}
className="timeline-scrollable"
firstItemIndex={firstItemIndex}
atTopStateChange={prependUnreads}
data={statuses}
endReached={loadMore}
itemContent={(_, status) => (
<Status
client={props.client}
account={props.account}
status={status}
key={status.id}
onRefresh={status => setStatuses(current => updateStatus(current, status))}
openMedia={media => props.setAttachment(media)}
/>
)}
/>
) : (
<div className="w-full" style={{ height: `calc(100% - ${composeHeight}px)` }}>
<Spinner className="m-auto mt-6" />
</div>
)}
<div ref={composeRef}>
<Compose client={props.client} />
</div>

View File

@ -1,8 +1,8 @@
import { Avatar } from 'flowbite-react'
import { Entity } from 'megalodon'
import { FaUserPlus } from 'react-icons/fa6'
import { FormattedMessage, useIntl } from 'react-intl'
import emojify from '@/utils/emojify'
import { Avatar } from '@material-tailwind/react'
type Props = {
notification: Entity.Notification
@ -35,7 +35,7 @@ export default function Follow(props: Props) {
</div>
<div className="flex">
<div className="p-2" style={{ width: '56px' }}>
<Avatar img={props.notification.account.avatar} />
<Avatar src={props.notification.account.avatar} />
</div>
<div style={{ width: 'calc(100% - 56px)' }}>
<div className="flex">

View File

@ -1,4 +1,3 @@
import { Avatar } from 'flowbite-react'
import { Entity, MegalodonInterface } from 'megalodon'
import dayjs from 'dayjs'
import emojify from '@/utils/emojify'
@ -9,6 +8,7 @@ import Media from '../status/Media'
import { FaBarsProgress, FaHouse, FaPenToSquare, FaRetweet, FaStar } from 'react-icons/fa6'
import { useIntl } from 'react-intl'
import { useState } from 'react'
import { Avatar } from '@material-tailwind/react'
type Props = {
notification: Entity.Notification
@ -49,7 +49,7 @@ export default function Reaction(props: Props) {
</div>
<div className="flex">
<div className="p-2" style={{ width: '56px' }}>
<Avatar img={status.account.avatar} />
<Avatar src={status.account.avatar} />
</div>
<div className="text-gray-600 break-all overflow-hidden" style={{ width: 'calc(100% - 56px)' }}>
<div className="flex justify-between">

View File

@ -1,4 +1,3 @@
import { CustomFlowbiteTheme, Dropdown, Flowbite } from 'flowbite-react'
import { Entity, MegalodonInterface } from 'megalodon'
import { useRouter } from 'next/router'
import { FaBookmark, FaEllipsis, FaFaceLaughBeam, FaReply, FaRetweet, FaStar } from 'react-icons/fa6'
@ -6,6 +5,8 @@ import Picker from '@emoji-mart/react'
import { data } from '@/utils/emojiData'
import { Account } from '@/db'
import { FormattedMessage } from 'react-intl'
import { IconButton, List, ListItem, Popover, PopoverContent, PopoverHandler } from '@material-tailwind/react'
import { useState } from 'react'
type Props = {
status: Entity.Status
@ -14,18 +15,9 @@ type Props = {
onRefresh: () => void
}
const customTheme: CustomFlowbiteTheme = {
dropdown: {
content: 'focus:outline-none',
floating: {
item: {
base: 'hidden'
}
}
}
}
export default function Actions(props: Props) {
const [popoverDetail, setPopoverDetail] = useState(false)
const [popoverEmoji, setPopoverEmoji] = useState(false)
const router = useRouter()
const reply = async () => {
@ -61,53 +53,56 @@ export default function Actions(props: Props) {
const onEmojiSelect = async emoji => {
await props.client.createEmojiReaction(props.status.id, emoji.native)
const dummy = document.getElementById('dummy-emoji-picker')
dummy.click()
setPopoverDetail(false)
props.onRefresh()
}
const report = () => {
setPopoverDetail(false)
router.push({ query: { id: router.query.id, timeline: router.query.timeline, report_target_id: props.status.id, modal: true } })
}
return (
<div className="flex gap-6">
<FaReply className={`w-4 text-gray-400 cursor-pointer hover:text-gray-600`} onClick={reply} />
<FaRetweet className={`${retweetColor(props.status)} w-4 cursor-pointer hover:text-gray-600`} onClick={reblog} />
<FaStar className={`${favouriteColor(props.status)} w-4 cursor-pointer hover:text-gray-600`} onClick={favourite} />
<FaBookmark className={`${bookmarkColor(props.status)} w-4 cursor-pointer hover:text-gray-600`} onClick={bookmark} />
<div className="flex gap-2">
<IconButton variant="text" size="sm" onClick={reply} className="text-gray-400 text-base hover:text-gray-600">
<FaReply className="w-4" />
</IconButton>
<IconButton variant="text" size="sm" onClick={reblog} className={`${retweetColor(props.status)} text-base hover:text-gray-600`}>
<FaRetweet className="w-4" />
</IconButton>
<IconButton variant="text" size="sm" onClick={favourite} className={`${favouriteColor(props.status)} text-base hover:text-gray-600`}>
<FaStar className="w-4" />
</IconButton>
<IconButton variant="text" size="sm" onClick={bookmark} className={`${bookmarkColor(props.status)} text-base hover:text-gray-600`}>
<FaBookmark className="w-4" />
</IconButton>
{props.account.sns !== 'mastodon' && (
<Flowbite theme={{ theme: customTheme }}>
<Dropdown
label=""
dismissOnClick
renderTrigger={() => (
<span className="text-gray-400 hover:text-gray-600 cursor-pointer">
<FaFaceLaughBeam />
</span>
)}
>
<Popover open={popoverEmoji} handler={setPopoverEmoji}>
<PopoverHandler>
<IconButton variant="text" size="sm" className="text-gray-400 hover:text-gray-600 text-base">
<FaFaceLaughBeam />
</IconButton>
</PopoverHandler>
<PopoverContent className="z-10">
<Picker data={data} onEmojiSelect={onEmojiSelect} previewPosition="none" set="native" perLine="7" theme="light" />
<Dropdown.Item>
<span id="dummy-emoji-picker" />
</Dropdown.Item>
</Dropdown>
</Flowbite>
</PopoverContent>
</Popover>
)}
<Dropdown
label=""
dismissOnClick
renderTrigger={() => (
<span className="text-gray-400 hover:text-gray-600 cursor-pointer">
<Popover open={popoverDetail} handler={setPopoverDetail}>
<PopoverHandler>
<IconButton variant="text" size="sm" className="text-gray-400 hover:text-gray-600 text-base">
<FaEllipsis className="w-4" />
</span>
)}
>
<Dropdown.Item onClick={report}>
<FormattedMessage id="timeline.status.report" values={{ user: `@${props.status.account.acct}` }} />
</Dropdown.Item>
</Dropdown>
</IconButton>
</PopoverHandler>
<PopoverContent className="z-10">
<List className="py-2 px-0">
<ListItem onClick={report} className="rounded-none">
<FormattedMessage id="timeline.status.report" values={{ user: `@${props.status.account.acct}` }} />
</ListItem>
</List>
</PopoverContent>
</Popover>
</div>
)
}
@ -130,7 +125,7 @@ const favouriteColor = (status: Entity.Status) => {
const bookmarkColor = (status: Entity.Status) => {
if (status.bookmarked) {
return 'text-rose-500'
return 'text-red-500'
} else {
return 'text-gray-400'
}

View File

@ -1,8 +1,8 @@
import { Entity } from 'megalodon'
import { Dispatch, HTMLAttributes, SetStateAction } from 'react'
import emojify from '@/utils/emojify'
import { Button } from 'flowbite-react'
import { FormattedMessage } from 'react-intl'
import { Button } from '@material-tailwind/react'
type Props = {
status: Entity.Status
@ -24,7 +24,7 @@ export default function Body(props: Props) {
dangerouslySetInnerHTML={{ __html: emojify(props.status.spoiler_text, props.status.emojis) }}
onClick={props.onClick}
/>
<Button size="xs" color="gray" className="focus:ring-0 my-1" onClick={() => setSpoilered(current => !current)}>
<Button size="sm" onClick={() => setSpoilered(current => !current)} variant="outlined" color="blue-gray">
{spoilered ? <FormattedMessage id="timeline.status.show_more" /> : <FormattedMessage id="timeline.status.show_less" />}
</Button>
</div>

View File

@ -1,4 +1,4 @@
import { Button } from 'flowbite-react'
import { Button } from '@material-tailwind/react'
import { Entity } from 'megalodon'
import { useState } from 'react'
import { FaEyeSlash } from 'react-icons/fa6'
@ -16,7 +16,7 @@ export default function Media(props: Props) {
return (
<div className="relative">
{sensitive ? (
<Button size="xs" color="gray" className="focus:ring-0 my-1" onClick={() => setSensitive(false)}>
<Button size="sm" onClick={() => setSensitive(false)} variant="outlined" className="my-1" color="blue-gray">
<FormattedMessage id="timeline.status.cw" />
</Button>
) : (

View File

@ -1,5 +1,5 @@
import { Button, Checkbox, Progress, Radio } from '@material-tailwind/react'
import dayjs from 'dayjs'
import { Progress, Button, Radio, Label, Checkbox } from 'flowbite-react'
import { Entity, MegalodonInterface } from 'megalodon'
import { HTMLAttributes } from 'react'
import { FormattedMessage } from 'react-intl'
@ -38,12 +38,11 @@ function SimplePoll(props: Props) {
<div className={props.className + ' my-2'}>
{props.poll.options.map((option, index) => (
<div key={index} className="flex items-center gap-2 my-2 pl-1">
<Radio id={option.title} name={props.poll.id} value={option.title} />
<Label htmlFor={option.title}>{option.title}</Label>
<Radio id={option.title} name={props.poll.id} value={option.title} label={option.title} />
</div>
))}
<div className="flex gap-2 items-center mt-2">
<Button color="blue" outline={true} size="xs" onClick={vote}>
<Button size="sm" onClick={vote} variant="outlined">
<FormattedMessage id="timeline.status.poll.vote" />
</Button>
<div>
@ -76,12 +75,11 @@ function MultiplePoll(props: Props) {
<div className={props.className + ' my-2'}>
{props.poll.options.map((option, index) => (
<div key={index} className="flex items-center gap-2 my-2 pl-1">
<Checkbox id={option.title} />
<Label htmlFor={option.title}>{option.title}</Label>
<Checkbox id={option.title} label={option.title} />
</div>
))}
<div className="flex gap-2 items-center mt-2">
<Button color="blue" outline={true} size="xs" onClick={vote}>
<Button size="sm" onClick={vote} variant="outlined">
<FormattedMessage id="timeline.status.poll.vote" />
</Button>
<div>
@ -102,11 +100,11 @@ function PollResult(props: Props) {
<div key={index}>
<span className="pr-2">{percent(option.votes_count ?? 0, props.poll.votes_count)}%</span>
<span>{option.title}</span>
<Progress progress={percent(option.votes_count ?? 0, props.poll.votes_count)} />
<Progress value={percent(option.votes_count ?? 0, props.poll.votes_count)} color="blue" />
</div>
))}
<div className="flex gap-2 items-center mt-2">
<Button color="gray" outline={true} size="xs" onClick={props.onRefresh}>
<Button size="sm" onClick={props.onRefresh} variant="outlined">
<FormattedMessage id="timeline.status.poll.refresh" />
</Button>
<div>

View File

@ -1,4 +1,3 @@
import { Avatar } from 'flowbite-react'
import { Entity, MegalodonInterface } from 'megalodon'
import dayjs from 'dayjs'
import Body from './Body'
@ -12,6 +11,7 @@ import { useRouter } from 'next/router'
import { MouseEventHandler, useState } from 'react'
import { findAccount, findLink, ParsedAccount, accountMatch, findTag } from '@/utils/statusParser'
import { Account } from '@/db'
import { Avatar } from '@material-tailwind/react'
type Props = {
status: Entity.Status
@ -75,7 +75,12 @@ export default function Status(props: Props) {
{rebloggedHeader(props.status)}
<div className="flex">
<div className="p-2 cursor-pointer" style={{ width: '56px' }}>
<Avatar img={status.account.avatar} onClick={() => openUser(status.account.id)} />
<Avatar
src={status.account.avatar}
onClick={() => openUser(status.account.id)}
variant="rounded"
style={{ width: '40px', height: '40px' }}
/>
</div>
<div className="text-gray-950 break-all overflow-hidden" style={{ width: 'calc(100% - 56px)' }}>
<div className="flex justify-between">
@ -129,7 +134,7 @@ const rebloggedHeader = (status: Entity.Status) => {
return (
<div className="flex text-gray-600">
<div className="grid justify-items-end pr-2" style={{ width: '56px' }}>
<Avatar img={status.account.avatar} size="xs" />
<Avatar src={status.account.avatar} size="xs" variant="rounded" />
</div>
<div style={{ width: 'calc(100% - 56px)' }}>
<FormattedMessage id="timeline.status.boosted" values={{ user: status.account.username }} />

View File

@ -3,18 +3,111 @@ import '../app.css'
import AccountLayout from '@/components/layouts/account'
import TimelineLayout from '@/components/layouts/timelines'
import { IntlProviderWrapper } from '@/utils/i18n'
import { ToastProvider } from '@/utils/toast'
import { ThemeProvider } from '@material-tailwind/react'
export default function MyApp({ Component, pageProps }: AppProps) {
const customTheme = {
popover: {
styles: {
base: {
p: 'p-0'
}
}
},
input: {
styles: {
variants: {
outlined: {
base: {
input: {
floated: {
borderWidth: 'border focus:border'
}
},
label: {
before: {
floated: {
bt: 'before:border-t peer-focus:before:border-t-1',
bl: 'before:border-l peer-focus:before:border-l-1'
}
},
after: {
floated: {
bt: 'after:border-t peer-focus:after:border-t-1',
br: 'after:border-r peer-focus:after:border-r-1'
}
}
}
}
}
}
}
},
textarea: {
styles: {
variants: {
outlined: {
base: {
textarea: {
floated: {
borderWidth: 'border focus:border'
}
},
label: {
before: {
floated: {
bt: 'before:border-t peer-focus:before:border-t-1',
bl: 'before:border-l peer-focus:before:border-l-1'
}
},
after: {
floated: {
bt: 'after:border-t peer-focus:after:border-t-1',
br: 'after:border-r peer-focus:after:border-r-1'
}
}
}
}
}
}
}
},
select: {
styles: {
variants: {
outlined: {
states: {
open: {
select: {
borderWidth: 'border'
},
label: {
before: {
bt: 'before:border-t',
bl: 'before:border-l'
},
after: {
bt: 'after:border-t',
br: 'after:border-r'
}
}
}
}
}
}
}
}
}
return (
<IntlProviderWrapper>
<ToastProvider>
<ThemeProvider value={customTheme}>
<IntlProviderWrapper>
<AccountLayout>
<TimelineLayout>
<Component {...pageProps} />
</TimelineLayout>
</AccountLayout>
</ToastProvider>
</IntlProviderWrapper>
</IntlProviderWrapper>
</ThemeProvider>
)
}

View File

@ -1,5 +1,5 @@
import { Spinner } from '@material-tailwind/react'
import { useRouter } from 'next/router'
import { Spinner } from 'flowbite-react'
export default function Account() {
const router = useRouter()
@ -14,7 +14,7 @@ export default function Account() {
return (
<div className="h-screen w-full flex justify-center items-center">
<Spinner color="info" />
<Spinner />
</div>
)
}

View File

@ -1,7 +1,7 @@
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import { db } from '@/db'
import { Spinner } from 'flowbite-react'
import { Spinner } from '@material-tailwind/react'
export default function Index() {
const router = useRouter()
@ -27,7 +27,7 @@ export default function Index() {
return (
<div className="h-screen w-full flex justify-center items-center">
<Spinner color="info" />
<Spinner />
</div>
)
}

View File

@ -1,25 +1,15 @@
module.exports = {
content: ['./node_modules/flowbite-react/**/*.js', './renderer/pages/**/*.{js,ts,jsx,tsx}', './renderer/components/**/*.{js,ts,jsx,tsx}'],
plugins: [require('flowbite/plugin')],
darkMode: 'class',
const withMT = require('@material-tailwind/react/utils/withMT')
module.exports = withMT({
content: ['./renderer/pages/**/*.{js,ts,jsx,tsx}', './renderer/components/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {
colors: {
// flowbite-svelte
// Refs: https://github.com/themesberg/flowbite-svelte/blob/main/tailwind.config.cjs
primary: {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a8a'
blue: {
950: '#0B2A68'
}
}
}
}
}
},
plugins: []
})

View File

@ -1,4 +1,4 @@
import { Alert } from 'flowbite-react'
import { Alert } from '@material-tailwind/react'
import React, { useState, createContext, useContext } from 'react'
type ToastTypes = 'info' | 'success' | 'failure' | 'warning'
@ -28,11 +28,24 @@ export const ToastProvider: React.FC<Props> = ({ children }) => {
}, 10000)
}
const color = (toastType: ToastTypes) => {
switch (toastType) {
case 'success':
return 'green'
case 'failure':
return 'red'
case 'warning':
return 'amber'
default:
return 'blue'
}
}
return (
<ToastContext.Provider value={showToast}>
{children}
<div className={`${showable ? 'block' : 'hidden'} fixed top-2 -translate-x-1/2`} style={{ left: '50%' }}>
<Alert color={toastType} className="w96">
<Alert color={color(toastType)} className="w96">
<span>{toastText}</span>
</Alert>
</div>

4107
yarn.lock

File diff suppressed because it is too large Load Diff