diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx
index 660cdfcf6c..656817b5ff 100644
--- a/src/view/com/util/post-ctrls/PostCtrls.tsx
+++ b/src/view/com/util/post-ctrls/PostCtrls.tsx
@@ -38,6 +38,7 @@ import {
useProgressGuideControls,
} from '#/state/shell/progress-guide'
import {atoms as a, useTheme} from '#/alf'
+import * as Dialog from '#/components/Dialog'
import {useDialogControl} from '#/components/Dialog'
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as ArrowOutOfBox} from '#/components/icons/ArrowOutOfBox'
import {Bubble_Stroke2_Corner2_Rounded as Bubble} from '#/components/icons/Bubble'
@@ -47,6 +48,7 @@ import {formatCount} from '../numeric/format'
import {Text} from '../text/Text'
import * as Toast from '../Toast'
import {RepostButton} from './RepostButton'
+import {FeedSelectDialog} from './SubmitButton'
let PostCtrls = ({
big,
@@ -85,6 +87,7 @@ let PostCtrls = ({
const {sendInteraction} = useFeedFeedbackContext()
const {captureAction} = useProgressGuideControls()
const playHaptic = useHaptics()
+ const submitControl = Dialog.useDialogControl()
const isBlocked = Boolean(
post.author.viewer?.blocking ||
post.author.viewer?.blockedBy ||
@@ -224,6 +227,10 @@ let PostCtrls = ({
isBlocked,
])
+ const onSubmit = () => {
+ submitControl.open()
+ }
+
const onShare = useCallback(() => {
const urip = new AtUri(post.uri)
const href = makeProfileLink(post.author, 'post', urip.rkey)
@@ -293,7 +300,7 @@ let PostCtrls = ({
repostCount={(post.repostCount ?? 0) + (post.quoteCount ?? 0)}
onRepost={onRepost}
onQuote={onQuote}
- onSubmit={onShare /* TODO */}
+ onSubmit={onSubmit}
big={big}
embeddingDisabled={Boolean(post.viewer?.embeddingDisabled)}
/>
@@ -400,6 +407,15 @@ let PostCtrls = ({
)}
+
+
+ {}}
+ profiles={[]}
+ />
+
)
}
diff --git a/src/view/com/util/post-ctrls/SubmitButton.tsx b/src/view/com/util/post-ctrls/SubmitButton.tsx
new file mode 100644
index 0000000000..df641b5373
--- /dev/null
+++ b/src/view/com/util/post-ctrls/SubmitButton.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 FeedSelectDialog({
+ 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`)}
+ />
+
+ )
+}