feat: quick reaction
This commit is contained in:
parent
77d56265ad
commit
33fa1ec441
23 changed files with 305 additions and 47 deletions
|
|
@ -1,25 +1,22 @@
|
||||||
import { Drawer, DrawerContent, DrawerOverlay } from '@/components/ui/drawer'
|
import { Drawer, DrawerContent, DrawerOverlay } from '@/components/ui/drawer'
|
||||||
import {
|
import { Popover, PopoverAnchor, PopoverContent } from '@/components/ui/popover'
|
||||||
DropdownMenu,
|
import { BIG_RELAY_URLS, LONG_PRESS_THRESHOLD } from '@/constants'
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuTrigger
|
|
||||||
} from '@/components/ui/dropdown-menu'
|
|
||||||
import { BIG_RELAY_URLS } from '@/constants'
|
|
||||||
import { useStuffStatsById } from '@/hooks/useStuffStatsById'
|
|
||||||
import { useStuff } from '@/hooks/useStuff'
|
import { useStuff } from '@/hooks/useStuff'
|
||||||
|
import { useStuffStatsById } from '@/hooks/useStuffStatsById'
|
||||||
import {
|
import {
|
||||||
createExternalContentReactionDraftEvent,
|
createExternalContentReactionDraftEvent,
|
||||||
createReactionDraftEvent
|
createReactionDraftEvent
|
||||||
} from '@/lib/draft-event'
|
} from '@/lib/draft-event'
|
||||||
import { useNostr } from '@/providers/NostrProvider'
|
import { useNostr } from '@/providers/NostrProvider'
|
||||||
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||||
|
import { useUserPreferences } from '@/providers/UserPreferencesProvider'
|
||||||
import { useUserTrust } from '@/providers/UserTrustProvider'
|
import { useUserTrust } from '@/providers/UserTrustProvider'
|
||||||
import client from '@/services/client.service'
|
import client from '@/services/client.service'
|
||||||
import stuffStatsService from '@/services/stuff-stats.service'
|
import stuffStatsService from '@/services/stuff-stats.service'
|
||||||
import { TEmoji } from '@/types'
|
import { TEmoji } from '@/types'
|
||||||
import { Loader, SmilePlus } from 'lucide-react'
|
import { Loader, SmilePlus } from 'lucide-react'
|
||||||
import { Event } from 'nostr-tools'
|
import { Event } from 'nostr-tools'
|
||||||
import { useMemo, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Emoji from '../Emoji'
|
import Emoji from '../Emoji'
|
||||||
import EmojiPicker from '../EmojiPicker'
|
import EmojiPicker from '../EmojiPicker'
|
||||||
|
|
@ -31,10 +28,13 @@ export default function LikeButton({ stuff }: { stuff: Event | string }) {
|
||||||
const { isSmallScreen } = useScreenSize()
|
const { isSmallScreen } = useScreenSize()
|
||||||
const { pubkey, publish, checkLogin } = useNostr()
|
const { pubkey, publish, checkLogin } = useNostr()
|
||||||
const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
|
const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
|
||||||
|
const { quickReaction, quickReactionEmoji } = useUserPreferences()
|
||||||
const { event, externalContent, stuffKey } = useStuff(stuff)
|
const { event, externalContent, stuffKey } = useStuff(stuff)
|
||||||
const [liking, setLiking] = useState(false)
|
const [liking, setLiking] = useState(false)
|
||||||
const [isEmojiReactionsOpen, setIsEmojiReactionsOpen] = useState(false)
|
const [isEmojiReactionsOpen, setIsEmojiReactionsOpen] = useState(false)
|
||||||
const [isPickerOpen, setIsPickerOpen] = useState(false)
|
const [isPickerOpen, setIsPickerOpen] = useState(false)
|
||||||
|
const longPressTimerRef = useRef<NodeJS.Timeout | null>(null)
|
||||||
|
const isLongPressRef = useRef(false)
|
||||||
const noteStats = useStuffStatsById(stuffKey)
|
const noteStats = useStuffStatsById(stuffKey)
|
||||||
const { myLastEmoji, likeCount } = useMemo(() => {
|
const { myLastEmoji, likeCount } = useMemo(() => {
|
||||||
const stats = noteStats || {}
|
const stats = noteStats || {}
|
||||||
|
|
@ -45,6 +45,10 @@ export default function LikeButton({ stuff }: { stuff: Event | string }) {
|
||||||
return { myLastEmoji: myLike?.emoji, likeCount: likes?.length }
|
return { myLastEmoji: myLike?.emoji, likeCount: likes?.length }
|
||||||
}, [noteStats, pubkey, hideUntrustedInteractions])
|
}, [noteStats, pubkey, hideUntrustedInteractions])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeout(() => setIsPickerOpen(false), 100)
|
||||||
|
}, [isEmojiReactionsOpen])
|
||||||
|
|
||||||
const like = async (emoji: string | TEmoji) => {
|
const like = async (emoji: string | TEmoji) => {
|
||||||
checkLogin(async () => {
|
checkLogin(async () => {
|
||||||
if (liking || !pubkey) return
|
if (liking || !pubkey) return
|
||||||
|
|
@ -72,16 +76,50 @@ export default function LikeButton({ stuff }: { stuff: Event | string }) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleLongPressStart = () => {
|
||||||
|
if (!quickReaction) return
|
||||||
|
isLongPressRef.current = false
|
||||||
|
longPressTimerRef.current = setTimeout(() => {
|
||||||
|
isLongPressRef.current = true
|
||||||
|
setIsEmojiReactionsOpen(true)
|
||||||
|
}, LONG_PRESS_THRESHOLD)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLongPressEnd = () => {
|
||||||
|
if (longPressTimerRef.current) {
|
||||||
|
clearTimeout(longPressTimerRef.current)
|
||||||
|
longPressTimerRef.current = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClick = (e: React.MouseEvent | React.TouchEvent) => {
|
||||||
|
if (quickReaction) {
|
||||||
|
// If it was a long press, don't trigger the click action
|
||||||
|
if (isLongPressRef.current) {
|
||||||
|
isLongPressRef.current = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Quick reaction mode: click to react with default emoji
|
||||||
|
// Prevent dropdown from opening
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
like(quickReactionEmoji)
|
||||||
|
} else {
|
||||||
|
setIsEmojiReactionsOpen(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const trigger = (
|
const trigger = (
|
||||||
<button
|
<button
|
||||||
className="flex items-center enabled:hover:text-primary gap-1 px-3 h-full text-muted-foreground"
|
className="flex items-center enabled:hover:text-primary gap-1 px-3 h-full text-muted-foreground"
|
||||||
title={t('Like')}
|
title={t('Like')}
|
||||||
disabled={liking}
|
disabled={liking}
|
||||||
onClick={() => {
|
onClick={handleClick}
|
||||||
if (isSmallScreen) {
|
onMouseDown={handleLongPressStart}
|
||||||
setIsEmojiReactionsOpen(true)
|
onMouseUp={handleLongPressEnd}
|
||||||
}
|
onMouseLeave={handleLongPressEnd}
|
||||||
}}
|
onTouchStart={handleLongPressStart}
|
||||||
|
onTouchEnd={handleLongPressEnd}
|
||||||
>
|
>
|
||||||
{liking ? (
|
{liking ? (
|
||||||
<Loader className="animate-spin" />
|
<Loader className="animate-spin" />
|
||||||
|
|
@ -121,17 +159,9 @@ export default function LikeButton({ stuff }: { stuff: Event | string }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu
|
<Popover open={isEmojiReactionsOpen} onOpenChange={(open) => setIsEmojiReactionsOpen(open)}>
|
||||||
open={isEmojiReactionsOpen}
|
<PopoverAnchor asChild>{trigger}</PopoverAnchor>
|
||||||
onOpenChange={(open) => {
|
<PopoverContent side="top" className="p-0 w-fit border-0 shadow-lg">
|
||||||
setIsEmojiReactionsOpen(open)
|
|
||||||
if (open) {
|
|
||||||
setIsPickerOpen(false)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent side="top" className="p-0 w-fit">
|
|
||||||
{isPickerOpen ? (
|
{isPickerOpen ? (
|
||||||
<EmojiPicker
|
<EmojiPicker
|
||||||
onEmojiClick={(emoji, e) => {
|
onEmojiClick={(emoji, e) => {
|
||||||
|
|
@ -153,7 +183,7 @@ export default function LikeButton({ stuff }: { stuff: Event | string }) {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</DropdownMenuContent>
|
</PopoverContent>
|
||||||
</DropdownMenu>
|
</Popover>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,47 @@ import * as React from 'react'
|
||||||
import * as PopoverPrimitive from '@radix-ui/react-popover'
|
import * as PopoverPrimitive from '@radix-ui/react-popover'
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
import { createPortal } from 'react-dom'
|
||||||
|
|
||||||
const Popover = PopoverPrimitive.Root
|
const Popover = ({
|
||||||
|
open: controlledOpen,
|
||||||
|
onOpenChange: controlledOnOpenChange,
|
||||||
|
...props
|
||||||
|
}: React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Root>) => {
|
||||||
|
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false)
|
||||||
|
const isControlled = controlledOpen !== undefined
|
||||||
|
const open = isControlled ? controlledOpen : uncontrolledOpen
|
||||||
|
const backdropRef = React.useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const handleOpenChange = React.useCallback(
|
||||||
|
(newOpen: boolean) => {
|
||||||
|
if (!isControlled) {
|
||||||
|
setUncontrolledOpen(newOpen)
|
||||||
|
}
|
||||||
|
controlledOnOpenChange?.(newOpen)
|
||||||
|
},
|
||||||
|
[isControlled, controlledOnOpenChange]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{open &&
|
||||||
|
createPortal(
|
||||||
|
<div
|
||||||
|
ref={backdropRef}
|
||||||
|
className="fixed inset-0 z-40 pointer-events-auto"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
handleOpenChange(false)
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
|
<PopoverPrimitive.Root {...props} open={open} onOpenChange={handleOpenChange} modal={false} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Popover.displayName = 'Popover'
|
||||||
|
|
||||||
const PopoverTrigger = PopoverPrimitive.Trigger
|
const PopoverTrigger = PopoverPrimitive.Trigger
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,8 @@ export const StorageKey = {
|
||||||
ENABLE_SINGLE_COLUMN_LAYOUT: 'enableSingleColumnLayout',
|
ENABLE_SINGLE_COLUMN_LAYOUT: 'enableSingleColumnLayout',
|
||||||
FAVICON_URL_TEMPLATE: 'faviconUrlTemplate',
|
FAVICON_URL_TEMPLATE: 'faviconUrlTemplate',
|
||||||
FILTER_OUT_ONION_RELAYS: 'filterOutOnionRelays',
|
FILTER_OUT_ONION_RELAYS: 'filterOutOnionRelays',
|
||||||
|
QUICK_REACTION: 'quickReaction',
|
||||||
|
QUICK_REACTION_EMOJI: 'quickReactionEmoji',
|
||||||
PINNED_PUBKEYS: 'pinnedPubkeys', // deprecated
|
PINNED_PUBKEYS: 'pinnedPubkeys', // deprecated
|
||||||
MEDIA_UPLOAD_SERVICE: 'mediaUploadService', // deprecated
|
MEDIA_UPLOAD_SERVICE: 'mediaUploadService', // deprecated
|
||||||
HIDE_UNTRUSTED_EVENTS: 'hideUntrustedEvents', // deprecated
|
HIDE_UNTRUSTED_EVENTS: 'hideUntrustedEvents', // deprecated
|
||||||
|
|
@ -444,4 +446,4 @@ export const PRIMARY_COLORS = {
|
||||||
} as const
|
} as const
|
||||||
export type TPrimaryColor = keyof typeof PRIMARY_COLORS
|
export type TPrimaryColor = keyof typeof PRIMARY_COLORS
|
||||||
|
|
||||||
export const LONG_PRESS_THRESHOLD = 500
|
export const LONG_PRESS_THRESHOLD = 400
|
||||||
|
|
|
||||||
|
|
@ -566,6 +566,11 @@ export default {
|
||||||
'Load earlier': 'تحميل سابق',
|
'Load earlier': 'تحميل سابق',
|
||||||
'Last 24 hours': 'آخر 24 ساعة',
|
'Last 24 hours': 'آخر 24 ساعة',
|
||||||
'Last {{count}} days': 'آخر {{count}} أيام',
|
'Last {{count}} days': 'آخر {{count}} أيام',
|
||||||
notes: 'ملاحظات'
|
notes: 'ملاحظات',
|
||||||
|
'Quick reaction': 'رد فعل سريع',
|
||||||
|
'If enabled, you can react with a single click. Click and hold for more options':
|
||||||
|
'إذا تم التمكين، يمكنك التفاعل بنقرة واحدة. اضغط مع الاستمرار للمزيد من الخيارات',
|
||||||
|
'Quick reaction emoji': 'رمز تعبيري للرد السريع',
|
||||||
|
'Select emoji': 'اختر رمز تعبيري'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -582,6 +582,11 @@ export default {
|
||||||
'Load earlier': 'Früher laden',
|
'Load earlier': 'Früher laden',
|
||||||
'Last 24 hours': 'Letzte 24 Stunden',
|
'Last 24 hours': 'Letzte 24 Stunden',
|
||||||
'Last {{count}} days': 'Letzte {{count}} Tage',
|
'Last {{count}} days': 'Letzte {{count}} Tage',
|
||||||
notes: 'Notizen'
|
notes: 'Notizen',
|
||||||
|
'Quick reaction': 'Schnellreaktion',
|
||||||
|
'If enabled, you can react with a single click. Click and hold for more options':
|
||||||
|
'Wenn aktiviert, können Sie mit einem Klick reagieren. Klicken und halten Sie für weitere Optionen',
|
||||||
|
'Quick reaction emoji': 'Schnellreaktions-Emoji',
|
||||||
|
'Select emoji': 'Emoji auswählen'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -569,6 +569,11 @@ export default {
|
||||||
'Load earlier': 'Load earlier',
|
'Load earlier': 'Load earlier',
|
||||||
'Last 24 hours': 'Last 24 hours',
|
'Last 24 hours': 'Last 24 hours',
|
||||||
'Last {{count}} days': 'Last {{count}} days',
|
'Last {{count}} days': 'Last {{count}} days',
|
||||||
notes: 'notes'
|
notes: 'notes',
|
||||||
|
'Quick reaction': 'Quick reaction',
|
||||||
|
'If enabled, you can react with a single click. Click and hold for more options':
|
||||||
|
'If enabled, you can react with a single click. Click and hold for more options',
|
||||||
|
'Quick reaction emoji': 'Quick reaction emoji',
|
||||||
|
'Select emoji': 'Select emoji'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -578,6 +578,11 @@ export default {
|
||||||
'Load earlier': 'Cargar anterior',
|
'Load earlier': 'Cargar anterior',
|
||||||
'Last 24 hours': 'Últimas 24 horas',
|
'Last 24 hours': 'Últimas 24 horas',
|
||||||
'Last {{count}} days': 'Últimos {{count}} días',
|
'Last {{count}} days': 'Últimos {{count}} días',
|
||||||
notes: 'notas'
|
notes: 'notas',
|
||||||
|
'Quick reaction': 'Reacción rápida',
|
||||||
|
'If enabled, you can react with a single click. Click and hold for more options':
|
||||||
|
'Si está habilitado, puedes reaccionar con un solo clic. Mantén presionado para más opciones',
|
||||||
|
'Quick reaction emoji': 'Emoji de reacción rápida',
|
||||||
|
'Select emoji': 'Seleccionar emoji'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -572,6 +572,11 @@ export default {
|
||||||
'Load earlier': 'بارگذاری قدیمیتر',
|
'Load earlier': 'بارگذاری قدیمیتر',
|
||||||
'Last 24 hours': '24 ساعت گذشته',
|
'Last 24 hours': '24 ساعت گذشته',
|
||||||
'Last {{count}} days': '{{count}} روز گذشته',
|
'Last {{count}} days': '{{count}} روز گذشته',
|
||||||
notes: 'یادداشتها'
|
notes: 'یادداشتها',
|
||||||
|
'Quick reaction': 'واکنش سریع',
|
||||||
|
'If enabled, you can react with a single click. Click and hold for more options':
|
||||||
|
'اگر فعال باشد، میتوانید با یک کلیک واکنش نشان دهید. برای گزینههای بیشتر کلیک کنید و نگه دارید',
|
||||||
|
'Quick reaction emoji': 'ایموجی واکنش سریع',
|
||||||
|
'Select emoji': 'انتخاب ایموجی'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -581,6 +581,11 @@ export default {
|
||||||
'Load earlier': 'Charger plus tôt',
|
'Load earlier': 'Charger plus tôt',
|
||||||
'Last 24 hours': 'Dernières 24 heures',
|
'Last 24 hours': 'Dernières 24 heures',
|
||||||
'Last {{count}} days': 'Derniers {{count}} jours',
|
'Last {{count}} days': 'Derniers {{count}} jours',
|
||||||
notes: 'notes'
|
notes: 'notes',
|
||||||
|
'Quick reaction': 'Réaction rapide',
|
||||||
|
'If enabled, you can react with a single click. Click and hold for more options':
|
||||||
|
'Si activé, vous pouvez réagir en un seul clic. Maintenez enfoncé pour plus d\'options',
|
||||||
|
'Quick reaction emoji': 'Emoji de réaction rapide',
|
||||||
|
'Select emoji': 'Sélectionner un emoji'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -573,6 +573,11 @@ export default {
|
||||||
'Load earlier': 'पहले लोड करें',
|
'Load earlier': 'पहले लोड करें',
|
||||||
'Last 24 hours': 'पिछले 24 घंटे',
|
'Last 24 hours': 'पिछले 24 घंटे',
|
||||||
'Last {{count}} days': 'पिछले {{count}} दिन',
|
'Last {{count}} days': 'पिछले {{count}} दिन',
|
||||||
notes: 'नोट्स'
|
notes: 'नोट्स',
|
||||||
|
'Quick reaction': 'त्वरित प्रतिक्रिया',
|
||||||
|
'If enabled, you can react with a single click. Click and hold for more options':
|
||||||
|
'यदि सक्षम है, तो आप एक क्लिक से प्रतिक्रिया दे सकते हैं। अधिक विकल्पों के लिए क्लिक करें और रोकें',
|
||||||
|
'Quick reaction emoji': 'त्वरित प्रतिक्रिया इमोजी',
|
||||||
|
'Select emoji': 'इमोजी चुनें'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -567,6 +567,11 @@ export default {
|
||||||
'Load earlier': 'Korábbi betöltése',
|
'Load earlier': 'Korábbi betöltése',
|
||||||
'Last 24 hours': 'Utolsó 24 óra',
|
'Last 24 hours': 'Utolsó 24 óra',
|
||||||
'Last {{count}} days': 'Utolsó {{count}} nap',
|
'Last {{count}} days': 'Utolsó {{count}} nap',
|
||||||
notes: 'jegyzetek'
|
notes: 'jegyzetek',
|
||||||
|
'Quick reaction': 'Gyors reakció',
|
||||||
|
'If enabled, you can react with a single click. Click and hold for more options':
|
||||||
|
'Ha engedélyezve van, egy kattintással reagálhat. Tartsa lenyomva további lehetőségekért',
|
||||||
|
'Quick reaction emoji': 'Gyors reakció emoji',
|
||||||
|
'Select emoji': 'Emoji kiválasztása'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -577,6 +577,11 @@ export default {
|
||||||
'Load earlier': 'Carica precedente',
|
'Load earlier': 'Carica precedente',
|
||||||
'Last 24 hours': 'Ultime 24 ore',
|
'Last 24 hours': 'Ultime 24 ore',
|
||||||
'Last {{count}} days': 'Ultimi {{count}} giorni',
|
'Last {{count}} days': 'Ultimi {{count}} giorni',
|
||||||
notes: 'note'
|
notes: 'note',
|
||||||
|
'Quick reaction': 'Reazione rapida',
|
||||||
|
'If enabled, you can react with a single click. Click and hold for more options':
|
||||||
|
'Se abilitato, puoi reagire con un solo clic. Fai clic e tieni premuto per altre opzioni',
|
||||||
|
'Quick reaction emoji': 'Emoji reazione rapida',
|
||||||
|
'Select emoji': 'Seleziona emoji'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -572,6 +572,11 @@ export default {
|
||||||
'Load earlier': '以前を読み込む',
|
'Load earlier': '以前を読み込む',
|
||||||
'Last 24 hours': '過去24時間',
|
'Last 24 hours': '過去24時間',
|
||||||
'Last {{count}} days': '過去{{count}}日間',
|
'Last {{count}} days': '過去{{count}}日間',
|
||||||
notes: 'ノート'
|
notes: 'ノート',
|
||||||
|
'Quick reaction': 'クイックリアクション',
|
||||||
|
'If enabled, you can react with a single click. Click and hold for more options':
|
||||||
|
'有効にすると、ワンクリックでリアクションできます。長押しで他のオプションを表示',
|
||||||
|
'Quick reaction emoji': 'クイックリアクション絵文字',
|
||||||
|
'Select emoji': '絵文字を選択'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -571,6 +571,11 @@ export default {
|
||||||
'Load earlier': '이전 데이터 로드',
|
'Load earlier': '이전 데이터 로드',
|
||||||
'Last 24 hours': '최근 24시간',
|
'Last 24 hours': '최근 24시간',
|
||||||
'Last {{count}} days': '최근 {{count}}일',
|
'Last {{count}} days': '최근 {{count}}일',
|
||||||
notes: '노트'
|
notes: '노트',
|
||||||
|
'Quick reaction': '빠른 반응',
|
||||||
|
'If enabled, you can react with a single click. Click and hold for more options':
|
||||||
|
'활성화하면 한 번의 클릭으로 반응할 수 있습니다. 더 많은 옵션을 보려면 길게 누르세요',
|
||||||
|
'Quick reaction emoji': '빠른 반응 이모지',
|
||||||
|
'Select emoji': '이모지 선택'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -578,6 +578,11 @@ export default {
|
||||||
'Load earlier': 'Załaduj wcześniejsze',
|
'Load earlier': 'Załaduj wcześniejsze',
|
||||||
'Last 24 hours': 'Ostatnie 24 godziny',
|
'Last 24 hours': 'Ostatnie 24 godziny',
|
||||||
'Last {{count}} days': 'Ostatnie {{count}} dni',
|
'Last {{count}} days': 'Ostatnie {{count}} dni',
|
||||||
notes: 'notatki'
|
notes: 'notatki',
|
||||||
|
'Quick reaction': 'Szybka reakcja',
|
||||||
|
'If enabled, you can react with a single click. Click and hold for more options':
|
||||||
|
'Jeśli włączone, możesz zareagować jednym kliknięciem. Kliknij i przytrzymaj, aby uzyskać więcej opcji',
|
||||||
|
'Quick reaction emoji': 'Emoji szybkiej reakcji',
|
||||||
|
'Select emoji': 'Wybierz emoji'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -573,6 +573,11 @@ export default {
|
||||||
'Load earlier': 'Carregar anterior',
|
'Load earlier': 'Carregar anterior',
|
||||||
'Last 24 hours': 'Últimas 24 horas',
|
'Last 24 hours': 'Últimas 24 horas',
|
||||||
'Last {{count}} days': 'Últimos {{count}} dias',
|
'Last {{count}} days': 'Últimos {{count}} dias',
|
||||||
notes: 'notas'
|
notes: 'notas',
|
||||||
|
'Quick reaction': 'Reação rápida',
|
||||||
|
'If enabled, you can react with a single click. Click and hold for more options':
|
||||||
|
'Se ativado, você pode reagir com um único clique. Clique e segure para mais opções',
|
||||||
|
'Quick reaction emoji': 'Emoji de reação rápida',
|
||||||
|
'Select emoji': 'Selecionar emoji'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -576,6 +576,11 @@ export default {
|
||||||
'Load earlier': 'Carregar anterior',
|
'Load earlier': 'Carregar anterior',
|
||||||
'Last 24 hours': 'Últimas 24 horas',
|
'Last 24 hours': 'Últimas 24 horas',
|
||||||
'Last {{count}} days': 'Últimos {{count}} dias',
|
'Last {{count}} days': 'Últimos {{count}} dias',
|
||||||
notes: 'notas'
|
notes: 'notas',
|
||||||
|
'Quick reaction': 'Reação rápida',
|
||||||
|
'If enabled, you can react with a single click. Click and hold for more options':
|
||||||
|
'Se ativado, pode reagir com um único clique. Clique e mantenha premido para mais opções',
|
||||||
|
'Quick reaction emoji': 'Emoji de reação rápida',
|
||||||
|
'Select emoji': 'Selecionar emoji'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -578,6 +578,11 @@ export default {
|
||||||
'Load earlier': 'Загрузить ранее',
|
'Load earlier': 'Загрузить ранее',
|
||||||
'Last 24 hours': 'Последние 24 часа',
|
'Last 24 hours': 'Последние 24 часа',
|
||||||
'Last {{count}} days': 'Последние {{count}} дней',
|
'Last {{count}} days': 'Последние {{count}} дней',
|
||||||
notes: 'заметки'
|
notes: 'заметки',
|
||||||
|
'Quick reaction': 'Быстрая реакция',
|
||||||
|
'If enabled, you can react with a single click. Click and hold for more options':
|
||||||
|
'Если включено, вы можете реагировать одним щелчком. Нажмите и удерживайте для дополнительных параметров',
|
||||||
|
'Quick reaction emoji': 'Эмодзи быстрой реакции',
|
||||||
|
'Select emoji': 'Выбрать эмодзи'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -565,6 +565,11 @@ export default {
|
||||||
'Load earlier': 'โหลดข้อมูลก่อนหน้า',
|
'Load earlier': 'โหลดข้อมูลก่อนหน้า',
|
||||||
'Last 24 hours': '24 ชั่วโมงที่แล้ว',
|
'Last 24 hours': '24 ชั่วโมงที่แล้ว',
|
||||||
'Last {{count}} days': '{{count}} วันที่แล้ว',
|
'Last {{count}} days': '{{count}} วันที่แล้ว',
|
||||||
notes: 'โน้ต'
|
notes: 'โน้ต',
|
||||||
|
'Quick reaction': 'รีแอคชั่นด่วน',
|
||||||
|
'If enabled, you can react with a single click. Click and hold for more options':
|
||||||
|
'หากเปิดใช้งาน คุณสามารถรีแอคได้ด้วยคลิกเดียว คลิกค้างไว้สำหรับตัวเลือกเพิ่มเติม',
|
||||||
|
'Quick reaction emoji': 'อีโมจิรีแอคชั่นด่วน',
|
||||||
|
'Select emoji': 'เลือกอีโมจิ'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -558,6 +558,11 @@ export default {
|
||||||
'Load earlier': '加载更早',
|
'Load earlier': '加载更早',
|
||||||
'Last 24 hours': '最近 24 小时',
|
'Last 24 hours': '最近 24 小时',
|
||||||
'Last {{count}} days': '最近 {{count}} 天',
|
'Last {{count}} days': '最近 {{count}} 天',
|
||||||
notes: '笔记'
|
notes: '笔记',
|
||||||
|
'Quick reaction': '快速点赞',
|
||||||
|
'If enabled, you can react with a single click. Click and hold for more options':
|
||||||
|
'启用后,您可以通过单击进行点赞。长按以获取更多选项',
|
||||||
|
'Quick reaction emoji': '快速点赞表情',
|
||||||
|
'Select emoji': '选择表情'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
import Emoji from '@/components/Emoji'
|
||||||
|
import EmojiPickerDialog from '@/components/EmojiPickerDialog'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select'
|
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select'
|
||||||
import { Switch } from '@/components/ui/switch'
|
import { Switch } from '@/components/ui/switch'
|
||||||
|
|
@ -6,9 +9,11 @@ import { LocalizedLanguageNames, TLanguage } from '@/i18n'
|
||||||
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
|
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
|
||||||
import { cn, isSupportCheckConnectionType } from '@/lib/utils'
|
import { cn, isSupportCheckConnectionType } from '@/lib/utils'
|
||||||
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
||||||
|
import { useUserPreferences } from '@/providers/UserPreferencesProvider'
|
||||||
import { useUserTrust } from '@/providers/UserTrustProvider'
|
import { useUserTrust } from '@/providers/UserTrustProvider'
|
||||||
import { TMediaAutoLoadPolicy } from '@/types'
|
import { TMediaAutoLoadPolicy } from '@/types'
|
||||||
import { SelectValue } from '@radix-ui/react-select'
|
import { SelectValue } from '@radix-ui/react-select'
|
||||||
|
import { RotateCcw } from 'lucide-react'
|
||||||
import { forwardRef, HTMLProps, useState } from 'react'
|
import { forwardRef, HTMLProps, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
|
@ -26,6 +31,8 @@ const GeneralSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
|
||||||
setMediaAutoLoadPolicy
|
setMediaAutoLoadPolicy
|
||||||
} = useContentPolicy()
|
} = useContentPolicy()
|
||||||
const { hideUntrustedNotes, updateHideUntrustedNotes } = useUserTrust()
|
const { hideUntrustedNotes, updateHideUntrustedNotes } = useUserTrust()
|
||||||
|
const { quickReaction, updateQuickReaction, quickReactionEmoji, updateQuickReactionEmoji } =
|
||||||
|
useUserPreferences()
|
||||||
|
|
||||||
const handleLanguageChange = (value: TLanguage) => {
|
const handleLanguageChange = (value: TLanguage) => {
|
||||||
i18n.changeLanguage(value)
|
i18n.changeLanguage(value)
|
||||||
|
|
@ -108,6 +115,46 @@ const GeneralSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
|
||||||
</Label>
|
</Label>
|
||||||
<Switch id="show-nsfw" checked={defaultShowNsfw} onCheckedChange={setDefaultShowNsfw} />
|
<Switch id="show-nsfw" checked={defaultShowNsfw} onCheckedChange={setDefaultShowNsfw} />
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
<SettingItem>
|
||||||
|
<Label htmlFor="quick-reaction" className="text-base font-normal">
|
||||||
|
<div>{t('Quick reaction')}</div>
|
||||||
|
<div className="text-muted-foreground">
|
||||||
|
{t('If enabled, you can react with a single click. Click and hold for more options')}
|
||||||
|
</div>
|
||||||
|
</Label>
|
||||||
|
<Switch
|
||||||
|
id="quick-reaction"
|
||||||
|
checked={quickReaction}
|
||||||
|
onCheckedChange={updateQuickReaction}
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
{quickReaction && (
|
||||||
|
<SettingItem>
|
||||||
|
<Label htmlFor="quick-reaction-emoji" className="text-base font-normal">
|
||||||
|
{t('Quick reaction emoji')}
|
||||||
|
</Label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => updateQuickReactionEmoji('+')}
|
||||||
|
className="text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
<RotateCcw />
|
||||||
|
</Button>
|
||||||
|
<EmojiPickerDialog
|
||||||
|
onEmojiClick={(emoji) => {
|
||||||
|
if (!emoji) return
|
||||||
|
updateQuickReactionEmoji(emoji)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button variant="ghost" size="icon" className="border">
|
||||||
|
<Emoji emoji={quickReactionEmoji} />
|
||||||
|
</Button>
|
||||||
|
</EmojiPickerDialog>
|
||||||
|
</div>
|
||||||
|
</SettingItem>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</SecondaryPageLayout>
|
</SecondaryPageLayout>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import storage from '@/services/local-storage.service'
|
import storage from '@/services/local-storage.service'
|
||||||
import { TNotificationStyle } from '@/types'
|
import { TEmoji, TNotificationStyle } from '@/types'
|
||||||
import { createContext, useContext, useEffect, useState } from 'react'
|
import { createContext, useContext, useEffect, useState } from 'react'
|
||||||
import { useScreenSize } from './ScreenSizeProvider'
|
import { useScreenSize } from './ScreenSizeProvider'
|
||||||
|
|
||||||
|
|
@ -15,6 +15,12 @@ type TUserPreferencesContext = {
|
||||||
|
|
||||||
enableSingleColumnLayout: boolean
|
enableSingleColumnLayout: boolean
|
||||||
updateEnableSingleColumnLayout: (enable: boolean) => void
|
updateEnableSingleColumnLayout: (enable: boolean) => void
|
||||||
|
|
||||||
|
quickReaction: boolean
|
||||||
|
updateQuickReaction: (enable: boolean) => void
|
||||||
|
|
||||||
|
quickReactionEmoji: string | TEmoji
|
||||||
|
updateQuickReactionEmoji: (emoji: string | TEmoji) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserPreferencesContext = createContext<TUserPreferencesContext | undefined>(undefined)
|
const UserPreferencesContext = createContext<TUserPreferencesContext | undefined>(undefined)
|
||||||
|
|
@ -37,6 +43,8 @@ export function UserPreferencesProvider({ children }: { children: React.ReactNod
|
||||||
const [enableSingleColumnLayout, setEnableSingleColumnLayout] = useState(
|
const [enableSingleColumnLayout, setEnableSingleColumnLayout] = useState(
|
||||||
storage.getEnableSingleColumnLayout()
|
storage.getEnableSingleColumnLayout()
|
||||||
)
|
)
|
||||||
|
const [quickReaction, setQuickReaction] = useState(storage.getQuickReaction())
|
||||||
|
const [quickReactionEmoji, setQuickReactionEmoji] = useState(storage.getQuickReactionEmoji())
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isSmallScreen && enableSingleColumnLayout) {
|
if (!isSmallScreen && enableSingleColumnLayout) {
|
||||||
|
|
@ -61,6 +69,16 @@ export function UserPreferencesProvider({ children }: { children: React.ReactNod
|
||||||
storage.setEnableSingleColumnLayout(enable)
|
storage.setEnableSingleColumnLayout(enable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateQuickReaction = (enable: boolean) => {
|
||||||
|
setQuickReaction(enable)
|
||||||
|
storage.setQuickReaction(enable)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateQuickReactionEmoji = (emoji: string | TEmoji) => {
|
||||||
|
setQuickReactionEmoji(emoji)
|
||||||
|
storage.setQuickReactionEmoji(emoji)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserPreferencesContext.Provider
|
<UserPreferencesContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
|
@ -71,7 +89,11 @@ export function UserPreferencesProvider({ children }: { children: React.ReactNod
|
||||||
sidebarCollapse,
|
sidebarCollapse,
|
||||||
updateSidebarCollapse,
|
updateSidebarCollapse,
|
||||||
enableSingleColumnLayout: isSmallScreen ? true : enableSingleColumnLayout,
|
enableSingleColumnLayout: isSmallScreen ? true : enableSingleColumnLayout,
|
||||||
updateEnableSingleColumnLayout
|
updateEnableSingleColumnLayout,
|
||||||
|
quickReaction,
|
||||||
|
updateQuickReaction,
|
||||||
|
quickReactionEmoji,
|
||||||
|
updateQuickReactionEmoji
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import { isTorBrowser } from '@/lib/utils'
|
||||||
import {
|
import {
|
||||||
TAccount,
|
TAccount,
|
||||||
TAccountPointer,
|
TAccountPointer,
|
||||||
|
TEmoji,
|
||||||
TFeedInfo,
|
TFeedInfo,
|
||||||
TMediaAutoLoadPolicy,
|
TMediaAutoLoadPolicy,
|
||||||
TMediaUploadServiceConfig,
|
TMediaUploadServiceConfig,
|
||||||
|
|
@ -57,6 +58,8 @@ class LocalStorageService {
|
||||||
private enableSingleColumnLayout: boolean = true
|
private enableSingleColumnLayout: boolean = true
|
||||||
private faviconUrlTemplate: string = DEFAULT_FAVICON_URL_TEMPLATE
|
private faviconUrlTemplate: string = DEFAULT_FAVICON_URL_TEMPLATE
|
||||||
private filterOutOnionRelays: boolean = !isTorBrowser()
|
private filterOutOnionRelays: boolean = !isTorBrowser()
|
||||||
|
private quickReaction: boolean = false
|
||||||
|
private quickReactionEmoji: string | TEmoji = '+'
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
if (!LocalStorageService.instance) {
|
if (!LocalStorageService.instance) {
|
||||||
|
|
@ -230,6 +233,15 @@ class LocalStorageService {
|
||||||
this.filterOutOnionRelays = filterOutOnionRelaysStr !== 'false'
|
this.filterOutOnionRelays = filterOutOnionRelaysStr !== 'false'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.quickReaction = window.localStorage.getItem(StorageKey.QUICK_REACTION) === 'true'
|
||||||
|
const quickReactionEmojiStr =
|
||||||
|
window.localStorage.getItem(StorageKey.QUICK_REACTION_EMOJI) ?? '+'
|
||||||
|
if (quickReactionEmojiStr.startsWith('{')) {
|
||||||
|
this.quickReactionEmoji = JSON.parse(quickReactionEmojiStr) as TEmoji
|
||||||
|
} else {
|
||||||
|
this.quickReactionEmoji = quickReactionEmojiStr
|
||||||
|
}
|
||||||
|
|
||||||
// Clean up deprecated data
|
// Clean up deprecated data
|
||||||
window.localStorage.removeItem(StorageKey.PINNED_PUBKEYS)
|
window.localStorage.removeItem(StorageKey.PINNED_PUBKEYS)
|
||||||
window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP)
|
window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP)
|
||||||
|
|
@ -559,6 +571,27 @@ class LocalStorageService {
|
||||||
this.filterOutOnionRelays = filterOut
|
this.filterOutOnionRelays = filterOut
|
||||||
window.localStorage.setItem(StorageKey.FILTER_OUT_ONION_RELAYS, filterOut.toString())
|
window.localStorage.setItem(StorageKey.FILTER_OUT_ONION_RELAYS, filterOut.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getQuickReaction() {
|
||||||
|
return this.quickReaction
|
||||||
|
}
|
||||||
|
|
||||||
|
setQuickReaction(quickReaction: boolean) {
|
||||||
|
this.quickReaction = quickReaction
|
||||||
|
window.localStorage.setItem(StorageKey.QUICK_REACTION, quickReaction.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
getQuickReactionEmoji() {
|
||||||
|
return this.quickReactionEmoji
|
||||||
|
}
|
||||||
|
|
||||||
|
setQuickReactionEmoji(emoji: string | TEmoji) {
|
||||||
|
this.quickReactionEmoji = emoji
|
||||||
|
window.localStorage.setItem(
|
||||||
|
StorageKey.QUICK_REACTION_EMOJI,
|
||||||
|
typeof emoji === 'string' ? emoji : JSON.stringify(emoji)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const instance = new LocalStorageService()
|
const instance = new LocalStorageService()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue