diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx
index 43cb95b032..2fbf270fb1 100644
--- a/src/components/Dialog/index.web.tsx
+++ b/src/components/Dialog/index.web.tsx
@@ -156,6 +156,7 @@ export function Inner({
accessibilityLabelledBy,
accessibilityDescribedBy,
header,
+ // TODO no need for this I think
contentContainerStyle,
}: DialogInnerProps) {
const t = useTheme()
@@ -181,6 +182,7 @@ export function Inner({
a.rounded_md,
a.w_full,
a.border,
+ gtMobile ? a.p_2xl : a.p_xl,
t.atoms.bg,
{
maxWidth: 600,
@@ -190,6 +192,7 @@ export function Inner({
shadowRadius: 30,
},
flatten(style),
+ contentContainerStyle,
])}>
{header}
-
- {children}
-
+ {children}
diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx
index 0bc4cef48e..873f475a90 100644
--- a/src/components/Layout.tsx
+++ b/src/components/Layout.tsx
@@ -1,5 +1,5 @@
import React from 'react'
-import {View, ViewProps,ViewStyle} from 'react-native'
+import {View, ViewProps, ViewStyle} from 'react-native'
import {StyleProp} from 'react-native'
import {
KeyboardAwareScrollView,
@@ -11,7 +11,7 @@ import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
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 {Divider} from '#/components/Divider'
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
@@ -122,7 +122,8 @@ export function Gutter({
children,
top,
bottom,
-}: {
+ style,
+}: ViewStyleProp & {
children: React.ReactNode
top?: boolean
bottom?: boolean
@@ -135,6 +136,7 @@ export function Gutter({
top && a.pt_lg,
bottom && a.pb_lg,
gtMobile && [a.px_xl, top && a.pt_xl, bottom && a.pb_xl],
+ style,
]}>
{children}
diff --git a/src/screens/Feeds/Creator/UserSelectButton.tsx b/src/screens/Feeds/Creator/UserSelectButton.tsx
new file mode 100644
index 0000000000..c5e94ff4c5
--- /dev/null
+++ b/src/screens/Feeds/Creator/UserSelectButton.tsx
@@ -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 (
+ <>
+
+
+
+
+
+
+ >
+ )
+}
+
+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(null)
+ const inputRef = React.useRef(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 (
+
+ )
+ }
+ case 'placeholder': {
+ return
+ }
+ case 'empty': {
+ return
+ }
+ default:
+ return null
+ }
+ },
+ [moderationOpts, onToggleUser],
+ )
+
+ React.useLayoutEffect(() => {
+ if (isWeb) {
+ setImmediate(() => {
+ inputRef?.current?.focus()
+ })
+ }
+ }, [])
+
+ const listHeader = React.useMemo(() => {
+ return (
+
+
+ {
+ setSearchText(text)
+ listRef.current?.scrollToOffset({offset: 0, animated: false})
+ }}
+ onEscape={control.close}
+ />
+
+
+ {isWeb && (
+
+ )}
+
+ )
+ }, [t.atoms.border_contrast_low, t.atoms.bg, _, searchText, control])
+
+ return (
+ 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 (
+
+ {({hovered, pressed, focused}) => (
+
+
+
+
+ {displayName}
+
+
+ {handle}
+
+
+
+
+ )}
+
+ )
+}
+
+function ProfileCardSkeleton() {
+ const t = useTheme()
+
+ return (
+
+
+
+
+
+
+
+
+ )
+}
+
+function Empty({message}: {message: string}) {
+ const t = useTheme()
+ return (
+
+
+ {message}
+
+
+ (╯°□°)╯︵ ┻━┻
+
+ )
+}
+
+function SearchInput({
+ value,
+ onChangeText,
+ onEscape,
+ inputRef,
+}: {
+ value: string
+ onChangeText: (text: string) => void
+ onEscape: () => void
+ inputRef: React.RefObject
+}) {
+ 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 (
+
+
+
+ {
+ if (nativeEvent.key === 'Escape') {
+ onEscape()
+ }
+ }}
+ autoCorrect={false}
+ autoComplete="off"
+ autoCapitalize="none"
+ autoFocus
+ accessibilityLabel={_(msg`Search profiles`)}
+ accessibilityHint={_(msg`Search profiles`)}
+ />
+
+ )
+}
diff --git a/src/screens/Feeds/Creator/index.tsx b/src/screens/Feeds/Creator/index.tsx
index c8ab3018ce..5f8b33cc91 100644
--- a/src/screens/Feeds/Creator/index.tsx
+++ b/src/screens/Feeds/Creator/index.tsx
@@ -5,9 +5,13 @@ import {useLingui} from '@lingui/react'
import {useProfileQuery} from '#/state/queries/profile'
import {useSession} from '#/state/session'
+import {UserSelectButton} from '#/screens/Feeds/Creator/UserSelectButton'
import {atoms as a, useTheme} from '#/alf'
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 {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import * as Layout from '#/components/Layout'
import {Text} from '#/components/Typography'
@@ -22,6 +26,8 @@ export function Creator() {
const [name, setName] = React.useState('')
const [description, setDescription] = React.useState('')
+ const [_tags, setTags] = React.useState([])
+ const [dids, setDids] = React.useState([])
return (
@@ -31,13 +37,12 @@ export function Creator() {
Create a feed
-
+
@@ -83,7 +88,7 @@ export function Creator() {
value={description}
onChangeText={setDescription}
multiline
- style={{minHeight: 150}}
+ style={{minHeight: 100}}
/>
@@ -93,6 +98,72 @@ export function Creator() {
find your feed!
+
+
+
+ Users
+
+
+
+
+ Only posts by users you add here will appear in your feed.
+
+
+
+
+
+
+
+
+ Tags
+
+
+
+
+ Posts tagged with the following tags will automatically appear
+ in your feed, provided they are from the users you've selected
+ above.
+
+
+
+
+
+
+
+
+ Tags added here also help users find your feed!
+
+
+
+
+
+