Tags and users
This commit is contained in:
@@ -156,6 +156,7 @@ export function Inner({
|
|||||||
accessibilityLabelledBy,
|
accessibilityLabelledBy,
|
||||||
accessibilityDescribedBy,
|
accessibilityDescribedBy,
|
||||||
header,
|
header,
|
||||||
|
// TODO no need for this I think
|
||||||
contentContainerStyle,
|
contentContainerStyle,
|
||||||
}: DialogInnerProps) {
|
}: DialogInnerProps) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
@@ -181,6 +182,7 @@ export function Inner({
|
|||||||
a.rounded_md,
|
a.rounded_md,
|
||||||
a.w_full,
|
a.w_full,
|
||||||
a.border,
|
a.border,
|
||||||
|
gtMobile ? a.p_2xl : a.p_xl,
|
||||||
t.atoms.bg,
|
t.atoms.bg,
|
||||||
{
|
{
|
||||||
maxWidth: 600,
|
maxWidth: 600,
|
||||||
@@ -190,6 +192,7 @@ export function Inner({
|
|||||||
shadowRadius: 30,
|
shadowRadius: 30,
|
||||||
},
|
},
|
||||||
flatten(style),
|
flatten(style),
|
||||||
|
contentContainerStyle,
|
||||||
])}>
|
])}>
|
||||||
<DismissableLayer
|
<DismissableLayer
|
||||||
onInteractOutside={preventDefault}
|
onInteractOutside={preventDefault}
|
||||||
@@ -197,9 +200,7 @@ export function Inner({
|
|||||||
onDismiss={close}
|
onDismiss={close}
|
||||||
style={{display: 'flex', flexDirection: 'column'}}>
|
style={{display: 'flex', flexDirection: 'column'}}>
|
||||||
{header}
|
{header}
|
||||||
<View style={[gtMobile ? a.p_2xl : a.p_xl, contentContainerStyle]}>
|
<View>{children}</View>
|
||||||
{children}
|
|
||||||
</View>
|
|
||||||
</DismissableLayer>
|
</DismissableLayer>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</FocusScope>
|
</FocusScope>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View, ViewProps,ViewStyle} from 'react-native'
|
import {View, ViewProps, ViewStyle} from 'react-native'
|
||||||
import {StyleProp} from 'react-native'
|
import {StyleProp} from 'react-native'
|
||||||
import {
|
import {
|
||||||
KeyboardAwareScrollView,
|
KeyboardAwareScrollView,
|
||||||
@@ -11,7 +11,7 @@ import {useLingui} from '@lingui/react'
|
|||||||
import {useNavigation} from '@react-navigation/native'
|
import {useNavigation} from '@react-navigation/native'
|
||||||
|
|
||||||
import {NavigationProp} from '#/lib/routes/types'
|
import {NavigationProp} from '#/lib/routes/types'
|
||||||
import {atoms as a, useBreakpoints,useTheme} from '#/alf'
|
import {atoms as a, useBreakpoints, useTheme, ViewStyleProp} from '#/alf'
|
||||||
import {Button, ButtonIcon} from '#/components/Button'
|
import {Button, ButtonIcon} from '#/components/Button'
|
||||||
import {Divider} from '#/components/Divider'
|
import {Divider} from '#/components/Divider'
|
||||||
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
|
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
|
||||||
@@ -122,7 +122,8 @@ export function Gutter({
|
|||||||
children,
|
children,
|
||||||
top,
|
top,
|
||||||
bottom,
|
bottom,
|
||||||
}: {
|
style,
|
||||||
|
}: ViewStyleProp & {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
top?: boolean
|
top?: boolean
|
||||||
bottom?: boolean
|
bottom?: boolean
|
||||||
@@ -135,6 +136,7 @@ export function Gutter({
|
|||||||
top && a.pt_lg,
|
top && a.pt_lg,
|
||||||
bottom && a.pb_lg,
|
bottom && a.pb_lg,
|
||||||
gtMobile && [a.px_xl, top && a.pt_xl, bottom && a.pb_xl],
|
gtMobile && [a.px_xl, top && a.pt_xl, bottom && a.pb_xl],
|
||||||
|
style,
|
||||||
]}>
|
]}>
|
||||||
{children}
|
{children}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -0,0 +1,629 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {TextInput,View} from 'react-native'
|
||||||
|
import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
|
||||||
|
import {msg, Plural,Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||||
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
|
import {isWeb} from '#/platform/detection'
|
||||||
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
|
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
|
||||||
|
import {useProfilesQuery} from '#/state/queries/profile'
|
||||||
|
import {ListMethods} from '#/view/com/util/List'
|
||||||
|
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
|
import {atoms as a, native,useTheme, web} from '#/alf'
|
||||||
|
import {Button, ButtonIcon} from '#/components/Button'
|
||||||
|
import * as Dialog from '#/components/Dialog'
|
||||||
|
import * as Toggle from '#/components/forms/Toggle'
|
||||||
|
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||||
|
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
|
||||||
|
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
|
const AVI_SIZE = 30
|
||||||
|
const AVI_BORDER = 1
|
||||||
|
|
||||||
|
type Item =
|
||||||
|
| {
|
||||||
|
type: 'profile'
|
||||||
|
key: string
|
||||||
|
checked: boolean
|
||||||
|
profile: AppBskyActorDefs.ProfileView
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'empty'
|
||||||
|
key: string
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'placeholder'
|
||||||
|
key: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'error'
|
||||||
|
key: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UserSelectButton({
|
||||||
|
dids,
|
||||||
|
onChangeDids,
|
||||||
|
}: {
|
||||||
|
dids: string[]
|
||||||
|
onChangeDids: (dids: string[]) => void
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
|
const control = Dialog.useDialogControl()
|
||||||
|
const {data: profiles} = useProfilesQuery({
|
||||||
|
handles: dids,
|
||||||
|
})
|
||||||
|
const moderationOpts = useModerationOpts()
|
||||||
|
const slice =
|
||||||
|
profiles?.profiles?.slice(0, 3).map(f => {
|
||||||
|
if (!moderationOpts) {
|
||||||
|
return {
|
||||||
|
profile: {
|
||||||
|
...f,
|
||||||
|
displayName: f.displayName || f.handle,
|
||||||
|
},
|
||||||
|
moderation: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const moderation = moderateProfile(f, moderationOpts)
|
||||||
|
return {
|
||||||
|
profile: {
|
||||||
|
...f,
|
||||||
|
displayName: sanitizeDisplayName(
|
||||||
|
f.displayName || f.handle,
|
||||||
|
moderation.ui('displayName'),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
moderation,
|
||||||
|
}
|
||||||
|
}) || []
|
||||||
|
const serverCount = dids.length
|
||||||
|
const textStyle = [a.text_sm]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button label={_(msg`Select users`)} onPress={control.open}>
|
||||||
|
{ctx => (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.w_full,
|
||||||
|
a.flex_row,
|
||||||
|
a.align_start,
|
||||||
|
a.p_lg,
|
||||||
|
a.border,
|
||||||
|
a.rounded_sm,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
ctx.hovered || ctx.pressed
|
||||||
|
? [t.atoms.border_contrast_medium]
|
||||||
|
: [],
|
||||||
|
]}>
|
||||||
|
<View style={[a.flex_1, a.gap_xs]}>
|
||||||
|
<Text style={[a.text_md, a.font_bold, a.leading_tight]}>
|
||||||
|
{dids.length ? (
|
||||||
|
<Trans>
|
||||||
|
{dids.length}{' '}
|
||||||
|
<Plural value={dids.length} one="user" other="users" />{' '}
|
||||||
|
selected
|
||||||
|
</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>No users selected</Trans>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
{slice.length ? (
|
||||||
|
<Text style={[textStyle]} numberOfLines={2}>
|
||||||
|
{slice.length >= 2 ? (
|
||||||
|
// 2-n followers, including blocks
|
||||||
|
serverCount > 2 ? (
|
||||||
|
<Trans>
|
||||||
|
Including{' '}
|
||||||
|
<Text
|
||||||
|
emoji
|
||||||
|
key={slice[0].profile.did}
|
||||||
|
style={textStyle}>
|
||||||
|
{slice[0].profile.displayName}
|
||||||
|
</Text>
|
||||||
|
,{' '}
|
||||||
|
<Text
|
||||||
|
emoji
|
||||||
|
key={slice[1].profile.did}
|
||||||
|
style={textStyle}>
|
||||||
|
{slice[1].profile.displayName}
|
||||||
|
</Text>
|
||||||
|
, and{' '}
|
||||||
|
<Plural
|
||||||
|
value={serverCount - 2}
|
||||||
|
one="# other"
|
||||||
|
other="# others"
|
||||||
|
/>
|
||||||
|
</Trans>
|
||||||
|
) : (
|
||||||
|
// only 2
|
||||||
|
<Trans>
|
||||||
|
Including{' '}
|
||||||
|
<Text
|
||||||
|
emoji
|
||||||
|
key={slice[0].profile.did}
|
||||||
|
style={textStyle}>
|
||||||
|
{slice[0].profile.displayName}
|
||||||
|
</Text>{' '}
|
||||||
|
and{' '}
|
||||||
|
<Text
|
||||||
|
emoji
|
||||||
|
key={slice[1].profile.did}
|
||||||
|
style={textStyle}>
|
||||||
|
{slice[1].profile.displayName}
|
||||||
|
</Text>
|
||||||
|
</Trans>
|
||||||
|
)
|
||||||
|
) : serverCount > 1 ? (
|
||||||
|
// 1-n followers, including blocks
|
||||||
|
<Trans>
|
||||||
|
Including{' '}
|
||||||
|
<Text emoji key={slice[0].profile.did} style={textStyle}>
|
||||||
|
{slice[0].profile.displayName}
|
||||||
|
</Text>{' '}
|
||||||
|
and{' '}
|
||||||
|
<Plural
|
||||||
|
value={serverCount - 1}
|
||||||
|
one="# other"
|
||||||
|
other="# others"
|
||||||
|
/>
|
||||||
|
</Trans>
|
||||||
|
) : (
|
||||||
|
// only 1
|
||||||
|
<Trans>
|
||||||
|
Including{' '}
|
||||||
|
<Text emoji key={slice[0].profile.did} style={textStyle}>
|
||||||
|
{slice[0].profile.displayName}
|
||||||
|
</Text>
|
||||||
|
</Trans>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Text style={[a.text_sm, a.leading_snug]}>
|
||||||
|
Your feed will contain posts from all user.
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={[a.flex_row, a.justify_end]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
height: AVI_SIZE,
|
||||||
|
width: AVI_SIZE + (slice.length - 1) * a.gap_md.gap,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
{slice.map(({profile: prof, moderation}, i) => (
|
||||||
|
<View
|
||||||
|
key={prof.did}
|
||||||
|
style={[
|
||||||
|
a.absolute,
|
||||||
|
a.rounded_full,
|
||||||
|
{
|
||||||
|
borderWidth: AVI_BORDER,
|
||||||
|
borderColor: t.atoms.bg.backgroundColor,
|
||||||
|
width: AVI_SIZE + AVI_BORDER * 2,
|
||||||
|
height: AVI_SIZE + AVI_BORDER * 2,
|
||||||
|
left: i * a.gap_md.gap,
|
||||||
|
zIndex: AVI_BORDER - i,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<UserAvatar
|
||||||
|
size={AVI_SIZE}
|
||||||
|
avatar={prof.avatar}
|
||||||
|
moderation={moderation?.ui('avatar')}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Dialog.Outer control={control}>
|
||||||
|
<Dialog.Handle />
|
||||||
|
<UserSelectDialog
|
||||||
|
control={control}
|
||||||
|
dids={dids}
|
||||||
|
onChangeDids={onChangeDids}
|
||||||
|
profiles={profiles?.profiles || []}
|
||||||
|
/>
|
||||||
|
</Dialog.Outer>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UserSelectDialog({
|
||||||
|
control,
|
||||||
|
profiles,
|
||||||
|
dids,
|
||||||
|
onChangeDids,
|
||||||
|
}: {
|
||||||
|
control: Dialog.DialogOuterProps['control']
|
||||||
|
profiles: AppBskyActorDefs.ProfileView[]
|
||||||
|
dids: string[]
|
||||||
|
onChangeDids: (dids: string[]) => void
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
|
const moderationOpts = useModerationOpts()
|
||||||
|
const listRef = React.useRef<ListMethods>(null)
|
||||||
|
const inputRef = React.useRef<TextInput>(null)
|
||||||
|
|
||||||
|
const [searchText, setSearchText] = React.useState('')
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: results,
|
||||||
|
isError,
|
||||||
|
isFetching,
|
||||||
|
} = useActorAutocompleteQuery(searchText, true, 12)
|
||||||
|
|
||||||
|
const onToggleUser = React.useCallback(
|
||||||
|
(did: string, checked: boolean) => {
|
||||||
|
if (checked) {
|
||||||
|
onChangeDids(Array.from(new Set([did, ...dids])))
|
||||||
|
} else {
|
||||||
|
onChangeDids(dids.filter(d => d !== did))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dids, onChangeDids],
|
||||||
|
)
|
||||||
|
|
||||||
|
const items = React.useMemo(() => {
|
||||||
|
let _items: Item[] = []
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
_items.push({
|
||||||
|
type: 'empty',
|
||||||
|
key: 'empty',
|
||||||
|
message: _(msg`We're having network issues, try again`),
|
||||||
|
})
|
||||||
|
} else if (searchText.length) {
|
||||||
|
if (results?.length) {
|
||||||
|
for (const profile of results) {
|
||||||
|
_items.push({
|
||||||
|
type: 'profile',
|
||||||
|
key: profile.did,
|
||||||
|
checked: dids.includes(profile.did),
|
||||||
|
profile,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// _items = _items.sort(item => {
|
||||||
|
// // @ts-ignore
|
||||||
|
// return item.enabled ? -1 : 1
|
||||||
|
// })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (profiles.length) {
|
||||||
|
for (const profile of profiles) {
|
||||||
|
_items.push({
|
||||||
|
type: 'profile',
|
||||||
|
key: profile.did,
|
||||||
|
checked: dids.includes(profile.did),
|
||||||
|
profile,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// _items = _items.sort(item => {
|
||||||
|
// // @ts-ignore
|
||||||
|
// return item.checked ? -1 : 1
|
||||||
|
// })
|
||||||
|
} else {
|
||||||
|
const placeholders: Item[] = Array(10)
|
||||||
|
.fill(0)
|
||||||
|
.map((__, i) => ({
|
||||||
|
type: 'placeholder',
|
||||||
|
key: i + '',
|
||||||
|
}))
|
||||||
|
_items.push(...placeholders)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _items
|
||||||
|
}, [_, dids, profiles, searchText, results, isError])
|
||||||
|
|
||||||
|
if (searchText && !isFetching && !items.length && !isError) {
|
||||||
|
items.push({type: 'empty', key: 'empty', message: _(msg`No results`)})
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderItems = React.useCallback(
|
||||||
|
({item}: {item: Item}) => {
|
||||||
|
switch (item.type) {
|
||||||
|
case 'profile': {
|
||||||
|
return (
|
||||||
|
<ProfileCard
|
||||||
|
key={item.key}
|
||||||
|
checked={item.checked}
|
||||||
|
profile={item.profile}
|
||||||
|
moderationOpts={moderationOpts!}
|
||||||
|
onToggle={onToggleUser}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case 'placeholder': {
|
||||||
|
return <ProfileCardSkeleton key={item.key} />
|
||||||
|
}
|
||||||
|
case 'empty': {
|
||||||
|
return <Empty key={item.key} message={item.message} />
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[moderationOpts, onToggleUser],
|
||||||
|
)
|
||||||
|
|
||||||
|
React.useLayoutEffect(() => {
|
||||||
|
if (isWeb) {
|
||||||
|
setImmediate(() => {
|
||||||
|
inputRef?.current?.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const listHeader = React.useMemo(() => {
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.relative,
|
||||||
|
a.pt_xs,
|
||||||
|
a.pb_xs,
|
||||||
|
a.pl_lg,
|
||||||
|
a.pr_xs,
|
||||||
|
a.border_b,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
t.atoms.bg,
|
||||||
|
]}>
|
||||||
|
<View style={[a.flex_1]}>
|
||||||
|
<SearchInput
|
||||||
|
inputRef={inputRef}
|
||||||
|
value={searchText}
|
||||||
|
onChangeText={text => {
|
||||||
|
setSearchText(text)
|
||||||
|
listRef.current?.scrollToOffset({offset: 0, animated: false})
|
||||||
|
}}
|
||||||
|
onEscape={control.close}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{isWeb && (
|
||||||
|
<Button
|
||||||
|
label={_(msg`Close`)}
|
||||||
|
size="small"
|
||||||
|
shape="round"
|
||||||
|
variant={isWeb ? 'ghost' : 'solid'}
|
||||||
|
color="secondary"
|
||||||
|
style={[a.z_20]}
|
||||||
|
onPress={() => control.close()}>
|
||||||
|
<ButtonIcon icon={X} size="md" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}, [t.atoms.border_contrast_low, t.atoms.bg, _, searchText, control])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog.InnerFlatList
|
||||||
|
ref={listRef}
|
||||||
|
data={items}
|
||||||
|
renderItem={renderItems}
|
||||||
|
ListHeaderComponent={listHeader}
|
||||||
|
stickyHeaderIndices={[0]}
|
||||||
|
keyExtractor={(item: Item) => item.key}
|
||||||
|
style={[
|
||||||
|
web([a.py_0, {height: '100vh', maxHeight: 600}, a.px_0]),
|
||||||
|
native({height: '100%'}),
|
||||||
|
a.p_0,
|
||||||
|
]}
|
||||||
|
webInnerStyle={[a.p_0, {maxWidth: 500, minWidth: 200}]}
|
||||||
|
keyboardDismissMode="on-drag"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProfileCard({
|
||||||
|
checked,
|
||||||
|
profile,
|
||||||
|
moderationOpts,
|
||||||
|
onToggle,
|
||||||
|
}: {
|
||||||
|
checked: boolean
|
||||||
|
profile: AppBskyActorDefs.ProfileView
|
||||||
|
moderationOpts: ModerationOpts
|
||||||
|
onToggle: (did: string, checked: boolean) => void
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
|
const moderation = moderateProfile(profile, moderationOpts)
|
||||||
|
const handle = sanitizeHandle(profile.handle, '@')
|
||||||
|
const displayName = sanitizeDisplayName(
|
||||||
|
profile.displayName || sanitizeHandle(profile.handle),
|
||||||
|
moderation.ui('displayName'),
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleOnPress = React.useCallback(
|
||||||
|
(selected: boolean) => {
|
||||||
|
onToggle(profile.did, selected)
|
||||||
|
},
|
||||||
|
[onToggle, profile.did],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Toggle.Item
|
||||||
|
label={_(msg`Add ${displayName} to feed`)}
|
||||||
|
name={_(msg`Add ${displayName} to feed`)}
|
||||||
|
value={checked}
|
||||||
|
onChange={handleOnPress}>
|
||||||
|
{({hovered, pressed, focused}) => (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.py_md,
|
||||||
|
a.px_lg,
|
||||||
|
a.gap_md,
|
||||||
|
a.align_center,
|
||||||
|
a.flex_row,
|
||||||
|
pressed || focused
|
||||||
|
? t.atoms.bg_contrast_25
|
||||||
|
: hovered
|
||||||
|
? t.atoms.bg_contrast_50
|
||||||
|
: t.atoms.bg,
|
||||||
|
]}>
|
||||||
|
<UserAvatar
|
||||||
|
size={42}
|
||||||
|
avatar={profile.avatar}
|
||||||
|
moderation={moderation.ui('avatar')}
|
||||||
|
type={profile.associated?.labeler ? 'labeler' : 'user'}
|
||||||
|
/>
|
||||||
|
<View style={[a.flex_1, a.gap_2xs]}>
|
||||||
|
<Text
|
||||||
|
style={[t.atoms.text, a.font_bold, a.leading_tight, a.self_start]}
|
||||||
|
numberOfLines={1}
|
||||||
|
emoji>
|
||||||
|
{displayName}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
t.atoms.text,
|
||||||
|
a.leading_tight,
|
||||||
|
a.self_start,
|
||||||
|
t.atoms.text_contrast_medium,
|
||||||
|
]}
|
||||||
|
numberOfLines={1}
|
||||||
|
emoji>
|
||||||
|
{handle}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Toggle.Checkbox />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</Toggle.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProfileCardSkeleton() {
|
||||||
|
const t = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.py_md,
|
||||||
|
a.px_lg,
|
||||||
|
a.gap_md,
|
||||||
|
a.align_center,
|
||||||
|
a.flex_row,
|
||||||
|
]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.rounded_full,
|
||||||
|
{width: 42, height: 42},
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<View style={[a.flex_1, a.gap_sm]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.rounded_xs,
|
||||||
|
{width: 80, height: 14},
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.rounded_xs,
|
||||||
|
{width: 120, height: 10},
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Empty({message}: {message: string}) {
|
||||||
|
const t = useTheme()
|
||||||
|
return (
|
||||||
|
<View style={[a.p_lg, a.py_xl, a.align_center, a.gap_md]}>
|
||||||
|
<Text style={[a.text_sm, a.italic, t.atoms.text_contrast_high]}>
|
||||||
|
{message}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text style={[a.text_xs, t.atoms.text_contrast_low]}>(╯°□°)╯︵ ┻━┻</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SearchInput({
|
||||||
|
value,
|
||||||
|
onChangeText,
|
||||||
|
onEscape,
|
||||||
|
inputRef,
|
||||||
|
}: {
|
||||||
|
value: string
|
||||||
|
onChangeText: (text: string) => void
|
||||||
|
onEscape: () => void
|
||||||
|
inputRef: React.RefObject<TextInput>
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
|
const {
|
||||||
|
state: hovered,
|
||||||
|
onIn: onMouseEnter,
|
||||||
|
onOut: onMouseLeave,
|
||||||
|
} = useInteractionState()
|
||||||
|
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
|
||||||
|
const interacted = hovered || focused
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
{...web({
|
||||||
|
onMouseEnter,
|
||||||
|
onMouseLeave,
|
||||||
|
})}
|
||||||
|
style={[a.flex_row, a.align_center, a.gap_sm]}>
|
||||||
|
<Search
|
||||||
|
size="md"
|
||||||
|
fill={interacted ? t.palette.primary_500 : t.palette.contrast_300}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
// @ts-ignore bottom sheet input types issue — esb
|
||||||
|
ref={inputRef}
|
||||||
|
placeholder={_(msg`Search`)}
|
||||||
|
value={value}
|
||||||
|
onChangeText={onChangeText}
|
||||||
|
onFocus={onFocus}
|
||||||
|
onBlur={onBlur}
|
||||||
|
style={[a.flex_1, a.py_md, a.text_md, t.atoms.text]}
|
||||||
|
placeholderTextColor={t.palette.contrast_500}
|
||||||
|
keyboardAppearance={t.name === 'light' ? 'light' : 'dark'}
|
||||||
|
returnKeyType="search"
|
||||||
|
clearButtonMode="while-editing"
|
||||||
|
maxLength={50}
|
||||||
|
onKeyPress={({nativeEvent}) => {
|
||||||
|
if (nativeEvent.key === 'Escape') {
|
||||||
|
onEscape()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
autoCorrect={false}
|
||||||
|
autoComplete="off"
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoFocus
|
||||||
|
accessibilityLabel={_(msg`Search profiles`)}
|
||||||
|
accessibilityHint={_(msg`Search profiles`)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5,9 +5,13 @@ import {useLingui} from '@lingui/react'
|
|||||||
|
|
||||||
import {useProfileQuery} from '#/state/queries/profile'
|
import {useProfileQuery} from '#/state/queries/profile'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
|
import {UserSelectButton} from '#/screens/Feeds/Creator/UserSelectButton'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Admonition} from '#/components/Admonition'
|
import {Admonition} from '#/components/Admonition'
|
||||||
|
import {Button, ButtonIcon,ButtonText} from '#/components/Button'
|
||||||
|
import {OutlineTags} from '#/components/Composer/OutlineTags'
|
||||||
import * as TextField from '#/components/forms/TextField'
|
import * as TextField from '#/components/forms/TextField'
|
||||||
|
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||||
import * as Layout from '#/components/Layout'
|
import * as Layout from '#/components/Layout'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
@@ -22,6 +26,8 @@ export function Creator() {
|
|||||||
|
|
||||||
const [name, setName] = React.useState('')
|
const [name, setName] = React.useState('')
|
||||||
const [description, setDescription] = React.useState('')
|
const [description, setDescription] = React.useState('')
|
||||||
|
const [_tags, setTags] = React.useState<string[]>([])
|
||||||
|
const [dids, setDids] = React.useState<string[]>([])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout.Screen>
|
<Layout.Screen>
|
||||||
@@ -31,13 +37,12 @@ export function Creator() {
|
|||||||
<Layout.Header.TitleText>Create a feed</Layout.Header.TitleText>
|
<Layout.Header.TitleText>Create a feed</Layout.Header.TitleText>
|
||||||
</Layout.Header>
|
</Layout.Header>
|
||||||
|
|
||||||
<Layout.Gutter top>
|
<Layout.Gutter top style={[a.gap_2xl]}>
|
||||||
<View style={[a.gap_lg]}>
|
<View style={[a.gap_lg]}>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
a.text_md,
|
a.text_md,
|
||||||
a.leading_snug,
|
a.leading_snug,
|
||||||
a.pb_sm,
|
|
||||||
t.atoms.text_contrast_medium,
|
t.atoms.text_contrast_medium,
|
||||||
]}>
|
]}>
|
||||||
<Trans>
|
<Trans>
|
||||||
@@ -83,7 +88,7 @@ export function Creator() {
|
|||||||
value={description}
|
value={description}
|
||||||
onChangeText={setDescription}
|
onChangeText={setDescription}
|
||||||
multiline
|
multiline
|
||||||
style={{minHeight: 150}}
|
style={{minHeight: 100}}
|
||||||
/>
|
/>
|
||||||
</TextField.Root>
|
</TextField.Root>
|
||||||
</View>
|
</View>
|
||||||
@@ -93,6 +98,72 @@ export function Creator() {
|
|||||||
find your feed!
|
find your feed!
|
||||||
</Admonition>
|
</Admonition>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
<View style={[a.gap_md]}>
|
||||||
|
<Text style={[a.text_xl, a.font_heavy, a.leading_tight]}>
|
||||||
|
<Trans>Users</Trans>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.text_md,
|
||||||
|
a.leading_snug,
|
||||||
|
t.atoms.text_contrast_medium,
|
||||||
|
]}>
|
||||||
|
<Trans>
|
||||||
|
Only posts by users you add here will appear in your feed.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<UserSelectButton dids={dids} onChangeDids={setDids} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={[a.gap_md]}>
|
||||||
|
<Text style={[a.text_xl, a.font_heavy, a.leading_tight]}>
|
||||||
|
<Trans>Tags</Trans>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.text_md,
|
||||||
|
a.leading_snug,
|
||||||
|
t.atoms.text_contrast_medium,
|
||||||
|
]}>
|
||||||
|
<Trans>
|
||||||
|
Posts tagged with the following tags will automatically appear
|
||||||
|
in your feed, provided they are from the users you've selected
|
||||||
|
above.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.py_sm,
|
||||||
|
a.px_sm,
|
||||||
|
a.border,
|
||||||
|
a.rounded_sm,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
]}>
|
||||||
|
<OutlineTags onChangeTags={setTags} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Admonition type="tip">
|
||||||
|
Tags added here also help users find your feed!
|
||||||
|
</Admonition>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
label={_(msg`Save feed`)}
|
||||||
|
size="large"
|
||||||
|
variant="solid"
|
||||||
|
color="primary">
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Create feed</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
<ButtonIcon icon={Plus} position="right" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<View style={{height: 500}} />
|
||||||
</Layout.Gutter>
|
</Layout.Gutter>
|
||||||
</Layout.Center>
|
</Layout.Center>
|
||||||
</Layout.ScrollView>
|
</Layout.ScrollView>
|
||||||
|
|||||||
Reference in New Issue
Block a user