Use SearchablePeopleList for add user to list dialog, replace old modal (#8212)
* move to dialogs dir * make searchable people list more generic * new list-add-remove-users dialog * update header text * fix header on android * delete old modal * reduce spacing on items
This commit is contained in:
@@ -166,29 +166,47 @@ export function NameAndHandle({
|
||||
profile: bsky.profile.AnyProfileView
|
||||
moderationOpts: ModerationOpts
|
||||
}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<View style={[a.flex_1]}>
|
||||
<Name profile={profile} moderationOpts={moderationOpts} />
|
||||
<Handle profile={profile} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function Name({
|
||||
profile,
|
||||
moderationOpts,
|
||||
}: {
|
||||
profile: bsky.profile.AnyProfileView
|
||||
moderationOpts: ModerationOpts
|
||||
}) {
|
||||
const moderation = moderateProfile(profile, moderationOpts)
|
||||
const name = sanitizeDisplayName(
|
||||
profile.displayName || sanitizeHandle(profile.handle),
|
||||
moderation.ui('displayName'),
|
||||
)
|
||||
return (
|
||||
<Text
|
||||
emoji
|
||||
style={[a.text_md, a.font_bold, a.leading_snug, a.self_start]}
|
||||
numberOfLines={1}>
|
||||
{name}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
export function Handle({profile}: {profile: bsky.profile.AnyProfileView}) {
|
||||
const t = useTheme()
|
||||
const handle = sanitizeHandle(profile.handle, '@')
|
||||
|
||||
return (
|
||||
<View style={[a.flex_1]}>
|
||||
<Text
|
||||
emoji
|
||||
style={[a.text_md, a.font_bold, a.leading_snug, a.self_start]}
|
||||
numberOfLines={1}>
|
||||
{name}
|
||||
</Text>
|
||||
<Text
|
||||
emoji
|
||||
style={[a.leading_snug, t.atoms.text_contrast_medium]}
|
||||
numberOfLines={1}>
|
||||
{handle}
|
||||
</Text>
|
||||
</View>
|
||||
<Text
|
||||
emoji
|
||||
style={[a.leading_snug, t.atoms.text_contrast_medium]}
|
||||
numberOfLines={1}>
|
||||
{handle}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+108
-86
@@ -1,4 +1,5 @@
|
||||
import React, {
|
||||
import {
|
||||
Fragment,
|
||||
useCallback,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
@@ -6,7 +7,7 @@ import React, {
|
||||
useState,
|
||||
} from 'react'
|
||||
import {TextInput, View} from 'react-native'
|
||||
import {moderateProfile, ModerationOpts} from '@atproto/api'
|
||||
import {moderateProfile, type ModerationOpts} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
@@ -18,48 +19,62 @@ import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
|
||||
import {useListConvosQuery} from '#/state/queries/messages/list-conversations'
|
||||
import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
|
||||
import {useSession} from '#/state/session'
|
||||
import {ListMethods} from '#/view/com/util/List'
|
||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a, native, useTheme, web} from '#/alf'
|
||||
import {type ListMethods} from '#/view/com/util/List'
|
||||
import {android, atoms as a, native, useTheme, web} from '#/alf'
|
||||
import {Button, ButtonIcon} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {canBeMessaged} from '#/components/dms/util'
|
||||
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 * as ProfileCard from '#/components/ProfileCard'
|
||||
import {Text} from '#/components/Typography'
|
||||
import * as bsky from '#/types/bsky'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
|
||||
type Item =
|
||||
| {
|
||||
type: 'profile'
|
||||
key: string
|
||||
enabled: boolean
|
||||
profile: bsky.profile.AnyProfileView
|
||||
}
|
||||
| {
|
||||
type: 'empty'
|
||||
key: string
|
||||
message: string
|
||||
}
|
||||
| {
|
||||
type: 'placeholder'
|
||||
key: string
|
||||
}
|
||||
| {
|
||||
type: 'error'
|
||||
key: string
|
||||
}
|
||||
export type ProfileItem = {
|
||||
type: 'profile'
|
||||
key: string
|
||||
profile: bsky.profile.AnyProfileView
|
||||
}
|
||||
|
||||
type EmptyItem = {
|
||||
type: 'empty'
|
||||
key: string
|
||||
message: string
|
||||
}
|
||||
|
||||
type PlaceholderItem = {
|
||||
type: 'placeholder'
|
||||
key: string
|
||||
}
|
||||
|
||||
type ErrorItem = {
|
||||
type: 'error'
|
||||
key: string
|
||||
}
|
||||
|
||||
type Item = ProfileItem | EmptyItem | PlaceholderItem | ErrorItem
|
||||
|
||||
export function SearchablePeopleList({
|
||||
title,
|
||||
onSelectChat,
|
||||
showRecentConvos,
|
||||
sortByMessageDeclaration,
|
||||
onSelectChat,
|
||||
renderProfileCard,
|
||||
}: {
|
||||
title: string
|
||||
onSelectChat: (did: string) => void
|
||||
showRecentConvos?: boolean
|
||||
}) {
|
||||
sortByMessageDeclaration?: boolean
|
||||
} & (
|
||||
| {
|
||||
renderProfileCard: (item: ProfileItem) => React.ReactNode
|
||||
onSelectChat?: undefined
|
||||
}
|
||||
| {
|
||||
onSelectChat: (did: string) => void
|
||||
renderProfileCard?: undefined
|
||||
}
|
||||
)) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const moderationOpts = useModerationOpts()
|
||||
@@ -98,15 +113,17 @@ export function SearchablePeopleList({
|
||||
_items.push({
|
||||
type: 'profile',
|
||||
key: profile.did,
|
||||
enabled: canBeMessaged(profile),
|
||||
profile,
|
||||
})
|
||||
}
|
||||
|
||||
_items = _items.sort(item => {
|
||||
// @ts-ignore
|
||||
return item.enabled ? -1 : 1
|
||||
})
|
||||
if (sortByMessageDeclaration) {
|
||||
_items = _items.sort(item => {
|
||||
return item.type === 'profile' && canBeMessaged(item.profile)
|
||||
? -1
|
||||
: 1
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const placeholders: Item[] = Array(10)
|
||||
@@ -134,14 +151,13 @@ export function SearchablePeopleList({
|
||||
_items.push({
|
||||
type: 'profile',
|
||||
key: profile.did,
|
||||
enabled: true,
|
||||
profile,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let followsItems: typeof _items = []
|
||||
let followsItems: ProfileItem[] = []
|
||||
|
||||
for (const page of follows.pages) {
|
||||
for (const profile of page.follows) {
|
||||
@@ -150,17 +166,17 @@ export function SearchablePeopleList({
|
||||
followsItems.push({
|
||||
type: 'profile',
|
||||
key: profile.did,
|
||||
enabled: canBeMessaged(profile),
|
||||
profile,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// only sort follows
|
||||
followsItems = followsItems.sort(item => {
|
||||
// @ts-ignore
|
||||
return item.enabled ? -1 : 1
|
||||
})
|
||||
if (sortByMessageDeclaration) {
|
||||
// only sort follows
|
||||
followsItems = followsItems.sort(item => {
|
||||
return canBeMessaged(item.profile) ? -1 : 1
|
||||
})
|
||||
}
|
||||
|
||||
// then append
|
||||
_items.push(...followsItems)
|
||||
@@ -173,16 +189,18 @@ export function SearchablePeopleList({
|
||||
_items.push({
|
||||
type: 'profile',
|
||||
key: profile.did,
|
||||
enabled: canBeMessaged(profile),
|
||||
profile,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
_items = _items.sort(item => {
|
||||
// @ts-ignore
|
||||
return item.enabled ? -1 : 1
|
||||
})
|
||||
if (sortByMessageDeclaration) {
|
||||
_items = _items.sort(item => {
|
||||
return item.type === 'profile' && canBeMessaged(item.profile)
|
||||
? -1
|
||||
: 1
|
||||
})
|
||||
}
|
||||
} else {
|
||||
_items.push(...placeholders)
|
||||
}
|
||||
@@ -198,6 +216,7 @@ export function SearchablePeopleList({
|
||||
follows,
|
||||
convos,
|
||||
showRecentConvos,
|
||||
sortByMessageDeclaration,
|
||||
])
|
||||
|
||||
if (searchText && !isFetching && !items.length && !isError) {
|
||||
@@ -208,15 +227,18 @@ export function SearchablePeopleList({
|
||||
({item}: {item: Item}) => {
|
||||
switch (item.type) {
|
||||
case 'profile': {
|
||||
return (
|
||||
<ProfileCard
|
||||
key={item.key}
|
||||
enabled={item.enabled}
|
||||
profile={item.profile}
|
||||
moderationOpts={moderationOpts!}
|
||||
onPress={onSelectChat}
|
||||
/>
|
||||
)
|
||||
if (renderProfileCard) {
|
||||
return <Fragment key={item.key}>{renderProfileCard(item)}</Fragment>
|
||||
} else {
|
||||
return (
|
||||
<DefaultProfileCard
|
||||
key={item.key}
|
||||
profile={item.profile}
|
||||
moderationOpts={moderationOpts!}
|
||||
onPress={onSelectChat}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
case 'placeholder': {
|
||||
return <ProfileCardSkeleton key={item.key} />
|
||||
@@ -228,7 +250,7 @@ export function SearchablePeopleList({
|
||||
return null
|
||||
}
|
||||
},
|
||||
[moderationOpts, onSelectChat],
|
||||
[moderationOpts, onSelectChat, renderProfileCard],
|
||||
)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
@@ -247,6 +269,10 @@ export function SearchablePeopleList({
|
||||
a.relative,
|
||||
web(a.pt_lg),
|
||||
native(a.pt_4xl),
|
||||
android({
|
||||
borderTopLeftRadius: a.rounded_md.borderRadius,
|
||||
borderTopRightRadius: a.rounded_md.borderRadius,
|
||||
}),
|
||||
a.pb_xs,
|
||||
a.px_lg,
|
||||
a.border_b,
|
||||
@@ -327,19 +353,18 @@ export function SearchablePeopleList({
|
||||
)
|
||||
}
|
||||
|
||||
function ProfileCard({
|
||||
enabled,
|
||||
function DefaultProfileCard({
|
||||
profile,
|
||||
moderationOpts,
|
||||
onPress,
|
||||
}: {
|
||||
enabled: boolean
|
||||
profile: bsky.profile.AnyProfileView
|
||||
moderationOpts: ModerationOpts
|
||||
onPress: (did: string) => void
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const enabled = canBeMessaged(profile)
|
||||
const moderation = moderateProfile(profile, moderationOpts)
|
||||
const handle = sanitizeHandle(profile.handle, '@')
|
||||
const displayName = sanitizeDisplayName(
|
||||
@@ -360,38 +385,35 @@ function ProfileCard({
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.py_md,
|
||||
a.py_sm,
|
||||
a.px_lg,
|
||||
a.gap_md,
|
||||
a.align_center,
|
||||
a.flex_row,
|
||||
!enabled
|
||||
? {opacity: 0.5}
|
||||
: pressed || focused
|
||||
: pressed || focused || hovered
|
||||
? 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={[a.leading_tight, t.atoms.text_contrast_high]}
|
||||
numberOfLines={2}>
|
||||
{!enabled ? <Trans>{handle} can't be messaged</Trans> : handle}
|
||||
</Text>
|
||||
</View>
|
||||
<ProfileCard.Header>
|
||||
<ProfileCard.Avatar
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
/>
|
||||
<View style={[a.flex_1]}>
|
||||
<ProfileCard.Name
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
/>
|
||||
{enabled ? (
|
||||
<ProfileCard.Handle profile={profile} />
|
||||
) : (
|
||||
<Text
|
||||
style={[a.leading_snug, t.atoms.text_contrast_high]}
|
||||
numberOfLines={2}>
|
||||
<Trans>{handle} can't be messaged</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</ProfileCard.Header>
|
||||
</View>
|
||||
)}
|
||||
</Button>
|
||||
@@ -0,0 +1,180 @@
|
||||
import {useCallback, useMemo} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {type AppBskyGraphDefs, type ModerationOpts} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {
|
||||
getMembership,
|
||||
type ListMembersip,
|
||||
useDangerousListMembershipsQuery,
|
||||
useListMembershipAddMutation,
|
||||
useListMembershipRemoveMutation,
|
||||
} from '#/state/queries/list-memberships'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {
|
||||
type ProfileItem,
|
||||
SearchablePeopleList,
|
||||
} from '#/components/dialogs/SearchablePeopleList'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import * as ProfileCard from '#/components/ProfileCard'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
|
||||
export function ListAddRemoveUsersDialog({
|
||||
control,
|
||||
list,
|
||||
onChange,
|
||||
}: {
|
||||
control: Dialog.DialogControlProps
|
||||
list: AppBskyGraphDefs.ListView
|
||||
onChange?: (
|
||||
type: 'add' | 'remove',
|
||||
profile: bsky.profile.AnyProfileView,
|
||||
) => void | undefined
|
||||
}) {
|
||||
return (
|
||||
<Dialog.Outer control={control} testID="listAddRemoveUsersDialog">
|
||||
<Dialog.Handle />
|
||||
<DialogInner list={list} onChange={onChange} />
|
||||
</Dialog.Outer>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogInner({
|
||||
list,
|
||||
onChange,
|
||||
}: {
|
||||
list: AppBskyGraphDefs.ListView
|
||||
onChange?: (
|
||||
type: 'add' | 'remove',
|
||||
profile: bsky.profile.AnyProfileView,
|
||||
) => void | undefined
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const moderationOpts = useModerationOpts()
|
||||
const {data: memberships} = useDangerousListMembershipsQuery()
|
||||
|
||||
const renderProfileCard = useCallback(
|
||||
(item: ProfileItem) => {
|
||||
return (
|
||||
<UserResult
|
||||
profile={item.profile}
|
||||
onChange={onChange}
|
||||
memberships={memberships}
|
||||
list={list}
|
||||
moderationOpts={moderationOpts}
|
||||
/>
|
||||
)
|
||||
},
|
||||
[onChange, memberships, list, moderationOpts],
|
||||
)
|
||||
|
||||
return (
|
||||
<SearchablePeopleList
|
||||
title={_(msg`Add people to list`)}
|
||||
renderProfileCard={renderProfileCard}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function UserResult({
|
||||
profile,
|
||||
list,
|
||||
memberships,
|
||||
onChange,
|
||||
moderationOpts,
|
||||
}: {
|
||||
profile: bsky.profile.AnyProfileView
|
||||
list: AppBskyGraphDefs.ListView
|
||||
memberships: ListMembersip[] | undefined
|
||||
onChange?: (
|
||||
type: 'add' | 'remove',
|
||||
profile: bsky.profile.AnyProfileView,
|
||||
) => void | undefined
|
||||
moderationOpts?: ModerationOpts
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const membership = useMemo(
|
||||
() => getMembership(memberships, list.uri, profile.did),
|
||||
[memberships, list.uri, profile.did],
|
||||
)
|
||||
const {mutate: listMembershipAdd, isPending: isAddingPending} =
|
||||
useListMembershipAddMutation({
|
||||
onSuccess: () => {
|
||||
Toast.show(_(msg`Added to list`))
|
||||
onChange?.('add', profile)
|
||||
},
|
||||
onError: e => Toast.show(cleanError(e), 'xmark'),
|
||||
})
|
||||
const {mutate: listMembershipRemove, isPending: isRemovingPending} =
|
||||
useListMembershipRemoveMutation({
|
||||
onSuccess: () => {
|
||||
Toast.show(_(msg`Removed from list`))
|
||||
onChange?.('remove', profile)
|
||||
},
|
||||
onError: e => Toast.show(cleanError(e), 'xmark'),
|
||||
})
|
||||
const isMutating = isAddingPending || isRemovingPending
|
||||
|
||||
const onToggleMembership = useCallback(() => {
|
||||
if (typeof membership === 'undefined') {
|
||||
return
|
||||
}
|
||||
if (membership === false) {
|
||||
listMembershipAdd({
|
||||
listUri: list.uri,
|
||||
actorDid: profile.did,
|
||||
})
|
||||
} else {
|
||||
listMembershipRemove({
|
||||
listUri: list.uri,
|
||||
actorDid: profile.did,
|
||||
membershipUri: membership,
|
||||
})
|
||||
}
|
||||
}, [list, profile, membership, listMembershipAdd, listMembershipRemove])
|
||||
|
||||
if (!moderationOpts) return null
|
||||
|
||||
return (
|
||||
<View style={[a.flex_1, a.py_sm, a.px_lg]}>
|
||||
<ProfileCard.Header>
|
||||
<ProfileCard.Avatar profile={profile} moderationOpts={moderationOpts} />
|
||||
<View style={[a.flex_1]}>
|
||||
<ProfileCard.Name profile={profile} moderationOpts={moderationOpts} />
|
||||
<ProfileCard.Handle profile={profile} />
|
||||
</View>
|
||||
{membership !== undefined && (
|
||||
<Button
|
||||
label={
|
||||
membership === false
|
||||
? _(msg`Add user to list`)
|
||||
: _(msg`Remove user from list`)
|
||||
}
|
||||
onPress={onToggleMembership}
|
||||
disabled={isMutating}
|
||||
size="small"
|
||||
variant="solid"
|
||||
color="secondary">
|
||||
{isMutating ? (
|
||||
<ButtonIcon icon={Loader} />
|
||||
) : (
|
||||
<ButtonText>
|
||||
{membership === false ? (
|
||||
<Trans>Add</Trans>
|
||||
) : (
|
||||
<Trans>Remove</Trans>
|
||||
)}
|
||||
</ButtonText>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</ProfileCard.Header>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -11,9 +11,9 @@ import * as Toast from '#/view/com/util/Toast'
|
||||
import {useTheme} from '#/alf'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
import {SearchablePeopleList} from '#/components/dialogs/SearchablePeopleList'
|
||||
import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog'
|
||||
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||
import {SearchablePeopleList} from './SearchablePeopleList'
|
||||
|
||||
export function NewChat({
|
||||
control,
|
||||
@@ -71,6 +71,7 @@ export function NewChat({
|
||||
<SearchablePeopleList
|
||||
title={_(msg`Start a new chat`)}
|
||||
onSelectChat={onCreateChat}
|
||||
sortByMessageDeclaration
|
||||
/>
|
||||
</Dialog.Outer>
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import {logger} from '#/logger'
|
||||
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {SearchablePeopleList} from './SearchablePeopleList'
|
||||
import {SearchablePeopleList} from '#/components/dialogs/SearchablePeopleList'
|
||||
|
||||
export function SendViaChatDialog({
|
||||
control,
|
||||
@@ -62,6 +62,7 @@ function SendViaChatDialogInner({
|
||||
title={_(msg`Send post to...`)}
|
||||
onSelectChat={onCreateChat}
|
||||
showRecentConvos
|
||||
sortByMessageDeclaration
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user