Merge pull request #4758 from h3poteto/feat/material
Change component library to material-tailwind
This commit is contained in:
commit
0e9c1f9716
|
@ -91,7 +91,7 @@
|
|||
},
|
||||
"nsfw": "Sensitive",
|
||||
"poll": {
|
||||
"add": "Add a choice",
|
||||
"add": "Add",
|
||||
"5min": "5 minutes",
|
||||
"30min": "30 minutes",
|
||||
"1h": "1 hour",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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) }} />
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
) : (
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 }} />
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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: []
|
||||
})
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue