refs #4653 Add emoji picker for emoji reactions

This commit is contained in:
AkiraFukushima 2023-12-12 00:00:44 +09:00
parent 7ac9a17b7d
commit 22d2858c1c
No known key found for this signature in database
GPG Key ID: B6E51BAC4DE1A957
11 changed files with 112 additions and 13 deletions

View File

@ -36,7 +36,7 @@ const customTheme: CustomFlowbiteTheme = {
content: 'focus:outline-none',
floating: {
item: {
base: ''
base: 'hidden'
}
}
}
@ -162,6 +162,8 @@ 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()
}
return (
@ -197,8 +199,9 @@ export default function Compose(props: Props) {
</span>
)}
>
<Picker data={data} onEmojiSelect={onEmojiSelect} previewPosition="none" set="native" perLine="7" theme="light" />
<Dropdown.Item>
<Picker data={data} onEmojiSelect={onEmojiSelect} previewPosition="none" set="native" perLine="7" theme="light" />
<span id="dummy-emoji-picker" />
</Dropdown.Item>
</Dropdown>
</Flowbite>

View File

@ -5,9 +5,11 @@ import Thread from './Thread'
import { Entity, MegalodonInterface } from 'megalodon'
import Reply from './Reply'
import Profile from './Profile'
import { Account } from '@/db'
type Props = {
client: MegalodonInterface
account: Account
openMedia: (media: Entity.Attachment) => void
} & HTMLAttributes<HTMLElement>
@ -43,11 +45,25 @@ export default function Detail(props: Props) {
<FaChevronLeft onClick={back} className="cursor-pointer text-lg" />
<FaX onClick={close} className="cursor-pointer text-lg" />
</div>
{target === 'status' && <Thread client={props.client} status_id={router.query.status_id as string} openMedia={props.openMedia} />}
{target === 'reply' && (
<Reply client={props.client} status_id={router.query.reply_target_id as string} openMedia={props.openMedia} />
{target === 'status' && (
<Thread
client={props.client}
account={props.account}
status_id={router.query.status_id as string}
openMedia={props.openMedia}
/>
)}
{target === 'reply' && (
<Reply
client={props.client}
account={props.account}
status_id={router.query.reply_target_id as string}
openMedia={props.openMedia}
/>
)}
{target === 'profile' && (
<Profile client={props.client} account={props.account} user_id={router.query.user_id as string} openMedia={props.openMedia} />
)}
{target === 'profile' && <Profile client={props.client} user_id={router.query.user_id as string} openMedia={props.openMedia} />}
</div>
)}
</>

View File

@ -8,9 +8,11 @@ import Timeline from './profile/Timeline'
import Followings from './profile/Followings'
import Followers from './profile/Followers'
import { findLink } from '@/utils/statusParser'
import { Account } from '@/db'
type Props = {
client: MegalodonInterface
account: Account
user_id: string
openMedia: (media: Entity.Attachment) => void
}
@ -122,7 +124,7 @@ export default function Profile(props: Props) {
<div>
<Tabs.Group aria-label="Tabs with icons" style="underline">
<Tabs.Item active title={formatMessage({ id: 'profile.timeline' })}>
<Timeline client={props.client} user_id={props.user_id} openMedia={props.openMedia} />
<Timeline client={props.client} account={props.account} user_id={props.user_id} openMedia={props.openMedia} />
</Tabs.Item>
<Tabs.Item title={formatMessage({ id: 'profile.followings' })}>
<Followings client={props.client} user_id={props.user_id} />

View File

@ -3,9 +3,11 @@ import { useEffect, useRef, useState } from 'react'
import { Virtuoso } from 'react-virtuoso'
import Status from '../timelines/status/Status'
import Compose from '../compose/Compose'
import { Account } from '@/db'
type Props = {
client: MegalodonInterface
account: Account
status_id: string
openMedia: (media: Entity.Attachment) => void
}
@ -49,7 +51,14 @@ export default function Reply(props: Props) {
style={{ height: `calc(100% - ${composeHeight}px)` }}
data={[...ancestors, status].filter(s => s !== null)}
itemContent={(_, status) => (
<Status client={props.client} status={status} key={status.id} onRefresh={() => {}} openMedia={props.openMedia} />
<Status
client={props.client}
account={props.account}
status={status}
key={status.id}
onRefresh={() => {}}
openMedia={props.openMedia}
/>
)}
/>
<div ref={composeRef}>

View File

@ -2,9 +2,11 @@ import { Entity, MegalodonInterface } from 'megalodon'
import { useEffect, useState } from 'react'
import { Virtuoso } from 'react-virtuoso'
import Status from '../timelines/status/Status'
import { Account } from '@/db'
type Props = {
client: MegalodonInterface
account: Account
status_id: string
openMedia: (media: Entity.Attachment) => void
}
@ -33,7 +35,14 @@ export default function Thread(props: Props) {
style={{ height: 'calc(100% - 50px)' }}
data={[...ancestors, status, ...descendants].filter(s => s !== null)}
itemContent={(_, status) => (
<Status client={props.client} status={status} key={status.id} onRefresh={() => {}} openMedia={props.openMedia} />
<Status
client={props.client}
account={props.account}
status={status}
key={status.id}
onRefresh={() => {}}
openMedia={props.openMedia}
/>
)}
/>
</>

View File

@ -1,9 +1,11 @@
import Status from '@/components/timelines/status/Status'
import { Account } from '@/db'
import { Entity, MegalodonInterface } from 'megalodon'
import { useEffect, useState } from 'react'
type Props = {
client: MegalodonInterface
account: Account
user_id: string
openMedia: (media: Entity.Attachment) => void
}
@ -43,6 +45,7 @@ export default function Timeline(props: Props) {
{statuses.map((status, index) => (
<Status
client={props.client}
account={props.account}
status={status}
key={index}
onRefresh={status => setStatuses(current => updateStatus(current, status))}

View File

@ -125,6 +125,7 @@ export default function Notifications(props: Props) {
itemContent={(_, notification) => (
<Notification
client={props.client}
account={props.account}
notification={notification}
onRefresh={updateStatus}
key={notification.id}

View File

@ -182,6 +182,7 @@ export default function Timeline(props: Props) {
itemContent={(_, status) => (
<Status
client={props.client}
account={props.account}
status={status}
key={status.id}
onRefresh={status => setStatuses(current => updateStatus(current, status))}
@ -194,7 +195,7 @@ export default function Timeline(props: Props) {
</div>
</div>
</section>
<Detail client={props.client} className="detail" openMedia={media => props.setAttachment(media)} />
<Detail client={props.client} account={props.account} className="detail" openMedia={media => props.setAttachment(media)} />
</div>
)
}

View File

@ -2,9 +2,11 @@ import { Entity, MegalodonInterface } from 'megalodon'
import Status from '../status/Status'
import Reaction from './Reaction'
import Follow from './Follow'
import { Account } from '@/db'
type Props = {
notification: Entity.Notification
account: Account
client: MegalodonInterface
onRefresh: (status: Entity.Status) => void
openMedia: (media: Entity.Attachment) => void
@ -14,7 +16,15 @@ export default function Notification(props: Props) {
switch (props.notification.type) {
case 'mention': {
if (props.notification.status) {
return <Status client={props.client} status={props.notification.status} onRefresh={props.onRefresh} openMedia={props.openMedia} />
return (
<Status
account={props.account}
client={props.client}
status={props.notification.status}
onRefresh={props.onRefresh}
openMedia={props.openMedia}
/>
)
} else {
return null
}

View File

@ -1,13 +1,29 @@
import { CustomFlowbiteTheme, Dropdown, Flowbite } from 'flowbite-react'
import { Entity, MegalodonInterface } from 'megalodon'
import { useRouter } from 'next/router'
import { FaBookmark, FaEllipsis, FaReply, FaRetweet, FaStar } from 'react-icons/fa6'
import { FaBookmark, FaEllipsis, FaFaceLaughBeam, FaReply, FaRetweet, FaStar } from 'react-icons/fa6'
import Picker from '@emoji-mart/react'
import { data } from '@/utils/emojiData'
import { Account } from '@/db'
type Props = {
status: Entity.Status
account: Account
client: MegalodonInterface
onRefresh: () => void
}
const customTheme: CustomFlowbiteTheme = {
dropdown: {
content: 'focus:outline-none',
floating: {
item: {
base: 'hidden'
}
}
}
}
export default function Actions(props: Props) {
const router = useRouter()
@ -42,12 +58,39 @@ export default function Actions(props: Props) {
props.onRefresh()
}
const onEmojiSelect = async emoji => {
await props.client.createEmojiReaction(props.status.id, emoji.native)
const dummy = document.getElementById('dummy-emoji-picker')
dummy.click()
props.onRefresh()
}
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} />
{props.account.sns !== 'mastodon' && (
<Flowbite theme={{ theme: customTheme }}>
<Dropdown
disabled
label=""
dismissOnClick
renderTrigger={() => (
<span className="text-gray-400 hover:text-gray-600 cursor-pointer">
<FaFaceLaughBeam />
</span>
)}
>
<Picker data={data} onEmojiSelect={onEmojiSelect} previewPosition="none" set="native" perLine="7" theme="light" />
<Dropdown.Item>
<span id="dummy-emoji-picker" />
</Dropdown.Item>
</Dropdown>
</Flowbite>
)}
<FaEllipsis className="w-4 text-gray-400 cursor-pointer hover:text-gray-600" />
</div>
)

View File

@ -11,9 +11,11 @@ import Actions from './Actions'
import { useRouter } from 'next/router'
import { MouseEventHandler, useState } from 'react'
import { findLink } from '@/utils/statusParser'
import { Account } from '@/db'
type Props = {
status: Entity.Status
account: Account
client: MegalodonInterface
onRefresh: (status: Entity.Status) => void
openMedia: (media: Entity.Attachment) => void
@ -83,7 +85,7 @@ export default function Status(props: Props) {
</>
)}
<Actions status={status} client={props.client} onRefresh={onRefresh} />
<Actions status={status} client={props.client} account={props.account} onRefresh={onRefresh} />
</div>
</div>
</div>