index.tsx raw
1 import Emoji from '@/components/Emoji'
2 import EmojiPickerDialog from '@/components/EmojiPickerDialog'
3 import { Button } from '@/components/ui/button'
4 import { Label } from '@/components/ui/label'
5 import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select'
6 import { Switch } from '@/components/ui/switch'
7 import { MEDIA_AUTO_LOAD_POLICY, NSFW_DISPLAY_POLICY } from '@/constants'
8 import { LocalizedLanguageNames, TLanguage } from '@/i18n'
9 import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
10 import { cn, isSupportCheckConnectionType } from '@/lib/utils'
11 import { useContentPolicy } from '@/providers/ContentPolicyProvider'
12 import { useUserPreferences } from '@/providers/UserPreferencesProvider'
13 import { dispatchSettingsChanged } from '@/services/local-storage.service'
14 import { TMediaAutoLoadPolicy, TNsfwDisplayPolicy } from '@/types'
15 import { SelectValue } from '@radix-ui/react-select'
16 import { RotateCcw } from 'lucide-react'
17 import { forwardRef, HTMLProps, useState } from 'react'
18 import { useTranslation } from 'react-i18next'
19
20 const GeneralSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
21 const { t, i18n } = useTranslation()
22 const [language, setLanguage] = useState<TLanguage>(i18n.language as TLanguage)
23 const {
24 nsfwDisplayPolicy,
25 setNsfwDisplayPolicy,
26 hideContentMentioningMutedUsers,
27 setHideContentMentioningMutedUsers,
28 mediaAutoLoadPolicy,
29 setMediaAutoLoadPolicy,
30 verboseLogging,
31 setVerboseLogging,
32 enableMarkdown,
33 setEnableMarkdown
34 } = useContentPolicy()
35 const { quickReaction, updateQuickReaction, quickReactionEmoji, updateQuickReactionEmoji } =
36 useUserPreferences()
37
38 const handleLanguageChange = (value: TLanguage) => {
39 i18n.changeLanguage(value)
40 setLanguage(value)
41 dispatchSettingsChanged()
42 }
43
44 return (
45 <SecondaryPageLayout ref={ref} index={index} title={t('General')}>
46 <div className="space-y-4 mt-3">
47 <SettingItem>
48 <Label htmlFor="languages" className="text-base font-normal">
49 {t('Languages')}
50 </Label>
51 <Select defaultValue="en" value={language} onValueChange={handleLanguageChange}>
52 <SelectTrigger id="languages" className="w-48">
53 <SelectValue />
54 </SelectTrigger>
55 <SelectContent>
56 {Object.entries(LocalizedLanguageNames).map(([key, value]) => (
57 <SelectItem key={key} value={key}>
58 {value}
59 </SelectItem>
60 ))}
61 </SelectContent>
62 </Select>
63 </SettingItem>
64 <SettingItem>
65 <Label htmlFor="media-auto-load-policy" className="text-base font-normal">
66 {t('Auto-load media')}
67 </Label>
68 <Select
69 defaultValue="wifi-only"
70 value={mediaAutoLoadPolicy}
71 onValueChange={(value: TMediaAutoLoadPolicy) =>
72 setMediaAutoLoadPolicy(value as TMediaAutoLoadPolicy)
73 }
74 >
75 <SelectTrigger id="media-auto-load-policy" className="w-48">
76 <SelectValue />
77 </SelectTrigger>
78 <SelectContent>
79 <SelectItem value={MEDIA_AUTO_LOAD_POLICY.ALWAYS}>{t('Always')}</SelectItem>
80 {isSupportCheckConnectionType() && (
81 <SelectItem value={MEDIA_AUTO_LOAD_POLICY.WIFI_ONLY}>{t('Wi-Fi only')}</SelectItem>
82 )}
83 <SelectItem value={MEDIA_AUTO_LOAD_POLICY.NEVER}>{t('Never')}</SelectItem>
84 </SelectContent>
85 </Select>
86 </SettingItem>
87 <SettingItem>
88 <Label htmlFor="enable-markdown" className="text-base font-normal">
89 <div>{t('Render Markdown')}</div>
90 <div className="text-muted-foreground">{t('Parse and render markdown formatting in notes')}</div>
91 </Label>
92 <Switch id="enable-markdown" checked={enableMarkdown} onCheckedChange={setEnableMarkdown} />
93 </SettingItem>
94 <SettingItem>
95 <Label htmlFor="hide-content-mentioning-muted-users" className="text-base font-normal">
96 {t('Hide content mentioning muted users')}
97 </Label>
98 <Switch
99 id="hide-content-mentioning-muted-users"
100 checked={hideContentMentioningMutedUsers}
101 onCheckedChange={setHideContentMentioningMutedUsers}
102 />
103 </SettingItem>
104 <SettingItem>
105 <Label htmlFor="nsfw-display-policy" className="text-base font-normal">
106 {t('NSFW content display')}
107 </Label>
108 <Select
109 value={nsfwDisplayPolicy}
110 onValueChange={(value: TNsfwDisplayPolicy) => setNsfwDisplayPolicy(value)}
111 >
112 <SelectTrigger id="nsfw-display-policy" className="w-48">
113 <SelectValue />
114 </SelectTrigger>
115 <SelectContent>
116 <SelectItem value={NSFW_DISPLAY_POLICY.HIDE}>{t('Hide completely')}</SelectItem>
117 <SelectItem value={NSFW_DISPLAY_POLICY.HIDE_CONTENT}>
118 {t('Show but hide content')}
119 </SelectItem>
120 <SelectItem value={NSFW_DISPLAY_POLICY.SHOW}>{t('Show directly')}</SelectItem>
121 </SelectContent>
122 </Select>
123 </SettingItem>
124 <SettingItem>
125 <Label htmlFor="quick-reaction" className="text-base font-normal">
126 <div>{t('Quick reaction')}</div>
127 <div className="text-muted-foreground">
128 {t('If enabled, you can react with a single click. Click and hold for more options')}
129 </div>
130 </Label>
131 <Switch
132 id="quick-reaction"
133 checked={quickReaction}
134 onCheckedChange={updateQuickReaction}
135 />
136 </SettingItem>
137 {quickReaction && (
138 <SettingItem>
139 <Label htmlFor="quick-reaction-emoji" className="text-base font-normal">
140 {t('Quick reaction emoji')}
141 </Label>
142 <div className="flex items-center gap-2">
143 <Button
144 variant="ghost"
145 size="icon"
146 onClick={() => updateQuickReactionEmoji('+')}
147 className="text-muted-foreground hover:text-foreground"
148 >
149 <RotateCcw />
150 </Button>
151 <EmojiPickerDialog
152 onEmojiClick={(emoji) => {
153 if (!emoji) return
154 updateQuickReactionEmoji(emoji)
155 }}
156 >
157 <Button variant="ghost" size="icon" className="border">
158 <Emoji emoji={quickReactionEmoji} />
159 </Button>
160 </EmojiPickerDialog>
161 </div>
162 </SettingItem>
163 )}
164 <SettingItem>
165 <Label htmlFor="verbose-logging" className="text-base font-normal">
166 <div>{t('Verbose logging')}</div>
167 <div className="text-muted-foreground">
168 {t('Enable detailed logging for debugging (check browser console)')}
169 </div>
170 </Label>
171 <Switch
172 id="verbose-logging"
173 checked={verboseLogging}
174 onCheckedChange={setVerboseLogging}
175 />
176 </SettingItem>
177 </div>
178 </SecondaryPageLayout>
179 )
180 })
181 GeneralSettingsPage.displayName = 'GeneralSettingsPage'
182 export default GeneralSettingsPage
183
184 const SettingItem = forwardRef<HTMLDivElement, HTMLProps<HTMLDivElement>>(
185 ({ children, className, ...props }, ref) => {
186 return (
187 <div
188 className={cn(
189 'flex justify-between select-none items-center px-4 min-h-9 [&_svg]:size-4 [&_svg]:shrink-0',
190 className
191 )}
192 {...props}
193 ref={ref}
194 >
195 {children}
196 </div>
197 )
198 }
199 )
200 SettingItem.displayName = 'SettingItem'
201