このコミットが含まれているのは:
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useRef, useState } from 'react'
|
||||
|
||||
import { useUserSettings } from '@/components/users/UserSettingsProvider'
|
||||
import TagSearchBox from '@/components/TagSearchBox'
|
||||
import FormField from '@/components/common/FormField'
|
||||
import TextArea from '@/components/common/TextArea'
|
||||
@@ -39,6 +40,7 @@ type Props = Omit<ComponentPropsWithoutRef<'textarea'>, 'value' | 'onChange' | '
|
||||
|
||||
const PostFormTagsArea: FC<Props> = ({ tags, setTags, errors, ...rest }) => {
|
||||
const ref = useRef<HTMLTextAreaElement> (null)
|
||||
const { settings } = useUserSettings ()
|
||||
|
||||
const [bounds, setBounds] = useState<{ start: number; end: number }> ({ start: 0, end: 0 })
|
||||
const [focused, setFocused] = useState (false)
|
||||
@@ -68,7 +70,9 @@ const PostFormTagsArea: FC<Props> = ({ tags, setTags, errors, ...rest }) => {
|
||||
|
||||
setBounds ({ start, end })
|
||||
|
||||
const data = await apiGet<Tag[]> ('/tags/autocomplete', { params: { q: token, nico: '0' } })
|
||||
const data = await apiGet<Tag[]> ('/tags/autocomplete', { params: {
|
||||
q: token,
|
||||
nico: settings.tagAutocompleteNico ? '1' : '0' } })
|
||||
setSuggestions (data.filter (t => t.postCount > 0))
|
||||
setSuggestionsVsbl (suggestions.length > 0)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { motion } from 'framer-motion'
|
||||
import { useRef } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
import { useUserSettings } from '@/components/users/UserSettingsProvider'
|
||||
import PrefetchLink from '@/components/PrefetchLink'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useSharedTransitionStore } from '@/stores/sharedTransitionStore'
|
||||
@@ -16,14 +17,19 @@ type Props = { posts: Post[]
|
||||
|
||||
const PostList: FC<Props> = ({ posts, onClick }) => {
|
||||
const location = useLocation ()
|
||||
const { settings } = useUserSettings ()
|
||||
|
||||
const setForLocationKey = useSharedTransitionStore (s => s.setForLocationKey)
|
||||
|
||||
const cardRef = useRef<HTMLDivElement> (null)
|
||||
const visiblePosts =
|
||||
settings.viewedPostDisplay === 'hide'
|
||||
? posts.filter (post => !(post.viewed))
|
||||
: posts
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-6 p-4">
|
||||
{posts.map ((post, i) => {
|
||||
{visiblePosts.map ((post, i) => {
|
||||
const sharedId = `page-${ post.id }`
|
||||
const layoutId = sharedId
|
||||
|
||||
@@ -42,6 +48,9 @@ const PostList: FC<Props> = ({ posts, onClick }) => {
|
||||
layoutId={layoutId}
|
||||
className={cn ('w-full h-full overflow-hidden rounded-xl shadow',
|
||||
'transform-gpu will-change-transform',
|
||||
settings.viewedPostDisplay === 'dim'
|
||||
&& post.viewed
|
||||
&& 'opacity-40 saturate-50',
|
||||
(post.childPosts ?? []).length > 0 && 'ring-4 ring-green-500',
|
||||
(post.parentPosts ?? []).length > 0 && 'ring-4 ring-yellow-500')}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
@@ -72,4 +81,4 @@ const PostList: FC<Props> = ({ posts, onClick }) => {
|
||||
</div>)
|
||||
}
|
||||
|
||||
export default PostList
|
||||
export default PostList
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
import { useUserSettings } from '@/components/users/UserSettingsProvider'
|
||||
import TagSearchBox from '@/components/TagSearchBox'
|
||||
import { apiGet } from '@/lib/api'
|
||||
import { inputClass } from '@/lib/utils'
|
||||
@@ -20,6 +21,7 @@ const TagInput: FC<Props> = ({ describedBy, invalid, value, setValue }) => {
|
||||
const [activeIndex, setActiveIndex] = useState (-1)
|
||||
const [suggestions, setSuggestions] = useState<Tag[]> ([])
|
||||
const [suggestionsVsbl, setSuggestionsVsbl] = useState (false)
|
||||
const { settings } = useUserSettings ()
|
||||
|
||||
// TODO: TagSearch からのコピペのため,共通化を考へる.
|
||||
const whenChanged = async (ev: ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -32,7 +34,9 @@ const TagInput: FC<Props> = ({ describedBy, invalid, value, setValue }) => {
|
||||
return
|
||||
}
|
||||
|
||||
const data = await apiGet<Tag[]> ('/tags/autocomplete', { params: { q } })
|
||||
const data = await apiGet<Tag[]> ('/tags/autocomplete', { params: {
|
||||
q,
|
||||
nico: settings.tagAutocompleteNico ? '1' : '0' } })
|
||||
setSuggestions (data.filter (t => t.postCount > 0))
|
||||
if (suggestions.length > 0)
|
||||
setSuggestionsVsbl (true)
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { DEFAULT_USER_SETTINGS, fetchUserSettings } from '@/lib/settings'
|
||||
|
||||
import type { Dispatch, FC, ReactNode, SetStateAction } from 'react'
|
||||
|
||||
import type { User } from '@/types'
|
||||
import type { UserSettings } from '@/lib/settings'
|
||||
|
||||
type ContextValue = {
|
||||
loaded: boolean
|
||||
settings: UserSettings
|
||||
setSettings: Dispatch<SetStateAction<UserSettings>> }
|
||||
|
||||
const UserSettingsContext = createContext<ContextValue | null> (null)
|
||||
|
||||
|
||||
const applyTheme = (theme: UserSettings['theme']) => {
|
||||
const root = document.documentElement
|
||||
const media = window.matchMedia ('(prefers-color-scheme: dark)')
|
||||
|
||||
const sync = () => {
|
||||
const dark = theme === 'dark' || (theme === 'system' && media.matches)
|
||||
root.classList.toggle ('dark', dark)
|
||||
root.style.colorScheme = dark ? 'dark' : 'light'
|
||||
}
|
||||
|
||||
sync ()
|
||||
|
||||
if (theme !== 'system')
|
||||
return () => { }
|
||||
|
||||
const onChange = () => sync ()
|
||||
media.addEventListener ('change', onChange)
|
||||
return () => {
|
||||
media.removeEventListener ('change', onChange)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const UserSettingsProvider: FC<{
|
||||
children: ReactNode
|
||||
user: User | null }> = ({ children, user }) => {
|
||||
const [loaded, setLoaded] = useState (false)
|
||||
const [settings, setSettings] = useState<UserSettings> (DEFAULT_USER_SETTINGS)
|
||||
|
||||
useEffect (() => {
|
||||
let cancelled = false
|
||||
|
||||
if (!(user))
|
||||
{
|
||||
setSettings (DEFAULT_USER_SETTINGS)
|
||||
setLoaded (true)
|
||||
return
|
||||
}
|
||||
|
||||
setLoaded (false)
|
||||
|
||||
void (async () => {
|
||||
try
|
||||
{
|
||||
const next = await fetchUserSettings ()
|
||||
if (!(cancelled))
|
||||
{
|
||||
setSettings (next)
|
||||
setLoaded (true)
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (!(cancelled))
|
||||
{
|
||||
setSettings (DEFAULT_USER_SETTINGS)
|
||||
setLoaded (true)
|
||||
}
|
||||
}
|
||||
}) ()
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [user])
|
||||
|
||||
useEffect (() => applyTheme (settings.theme), [settings.theme])
|
||||
|
||||
useEffect (() => {
|
||||
document.documentElement.dataset.displayDensity = settings.displayDensity
|
||||
document.documentElement.dataset.fontSize = settings.fontSize
|
||||
}, [settings.displayDensity, settings.fontSize])
|
||||
|
||||
const value = useMemo<ContextValue> (() => ({
|
||||
loaded,
|
||||
settings,
|
||||
setSettings,
|
||||
}), [loaded, settings])
|
||||
|
||||
return (
|
||||
<UserSettingsContext.Provider value={value}>
|
||||
{children}
|
||||
</UserSettingsContext.Provider>)
|
||||
}
|
||||
|
||||
|
||||
export const useUserSettings = (): ContextValue => {
|
||||
const value = useContext (UserSettingsContext)
|
||||
|
||||
if (value == null)
|
||||
throw new Error ('UserSettingsProvider is missing')
|
||||
|
||||
return value
|
||||
}
|
||||
新しい課題から参照
ユーザをブロックする