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
|
profile: bsky.profile.AnyProfileView
|
||||||
moderationOpts: ModerationOpts
|
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 moderation = moderateProfile(profile, moderationOpts)
|
||||||
const name = sanitizeDisplayName(
|
const name = sanitizeDisplayName(
|
||||||
profile.displayName || sanitizeHandle(profile.handle),
|
profile.displayName || sanitizeHandle(profile.handle),
|
||||||
moderation.ui('displayName'),
|
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, '@')
|
const handle = sanitizeHandle(profile.handle, '@')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.flex_1]}>
|
<Text
|
||||||
<Text
|
emoji
|
||||||
emoji
|
style={[a.leading_snug, t.atoms.text_contrast_medium]}
|
||||||
style={[a.text_md, a.font_bold, a.leading_snug, a.self_start]}
|
numberOfLines={1}>
|
||||||
numberOfLines={1}>
|
{handle}
|
||||||
{name}
|
</Text>
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
emoji
|
|
||||||
style={[a.leading_snug, t.atoms.text_contrast_medium]}
|
|
||||||
numberOfLines={1}>
|
|
||||||
{handle}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+108
-86
@@ -1,4 +1,5 @@
|
|||||||
import React, {
|
import {
|
||||||
|
Fragment,
|
||||||
useCallback,
|
useCallback,
|
||||||
useLayoutEffect,
|
useLayoutEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
@@ -6,7 +7,7 @@ import React, {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import {TextInput, View} from 'react-native'
|
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 {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
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 {useListConvosQuery} from '#/state/queries/messages/list-conversations'
|
||||||
import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
|
import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {ListMethods} from '#/view/com/util/List'
|
import {type ListMethods} from '#/view/com/util/List'
|
||||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
import {android, atoms as a, native, useTheme, web} from '#/alf'
|
||||||
import {atoms as a, native, useTheme, web} from '#/alf'
|
|
||||||
import {Button, ButtonIcon} from '#/components/Button'
|
import {Button, ButtonIcon} from '#/components/Button'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import {canBeMessaged} from '#/components/dms/util'
|
import {canBeMessaged} from '#/components/dms/util'
|
||||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||||
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
|
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
|
||||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||||
|
import * as ProfileCard from '#/components/ProfileCard'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import * as bsky from '#/types/bsky'
|
import type * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
type Item =
|
export type ProfileItem = {
|
||||||
| {
|
type: 'profile'
|
||||||
type: 'profile'
|
key: string
|
||||||
key: string
|
profile: bsky.profile.AnyProfileView
|
||||||
enabled: boolean
|
}
|
||||||
profile: bsky.profile.AnyProfileView
|
|
||||||
}
|
type EmptyItem = {
|
||||||
| {
|
type: 'empty'
|
||||||
type: 'empty'
|
key: string
|
||||||
key: string
|
message: string
|
||||||
message: string
|
}
|
||||||
}
|
|
||||||
| {
|
type PlaceholderItem = {
|
||||||
type: 'placeholder'
|
type: 'placeholder'
|
||||||
key: string
|
key: string
|
||||||
}
|
}
|
||||||
| {
|
|
||||||
type: 'error'
|
type ErrorItem = {
|
||||||
key: string
|
type: 'error'
|
||||||
}
|
key: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Item = ProfileItem | EmptyItem | PlaceholderItem | ErrorItem
|
||||||
|
|
||||||
export function SearchablePeopleList({
|
export function SearchablePeopleList({
|
||||||
title,
|
title,
|
||||||
onSelectChat,
|
|
||||||
showRecentConvos,
|
showRecentConvos,
|
||||||
|
sortByMessageDeclaration,
|
||||||
|
onSelectChat,
|
||||||
|
renderProfileCard,
|
||||||
}: {
|
}: {
|
||||||
title: string
|
title: string
|
||||||
onSelectChat: (did: string) => void
|
|
||||||
showRecentConvos?: boolean
|
showRecentConvos?: boolean
|
||||||
}) {
|
sortByMessageDeclaration?: boolean
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
renderProfileCard: (item: ProfileItem) => React.ReactNode
|
||||||
|
onSelectChat?: undefined
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
onSelectChat: (did: string) => void
|
||||||
|
renderProfileCard?: undefined
|
||||||
|
}
|
||||||
|
)) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const moderationOpts = useModerationOpts()
|
const moderationOpts = useModerationOpts()
|
||||||
@@ -98,15 +113,17 @@ export function SearchablePeopleList({
|
|||||||
_items.push({
|
_items.push({
|
||||||
type: 'profile',
|
type: 'profile',
|
||||||
key: profile.did,
|
key: profile.did,
|
||||||
enabled: canBeMessaged(profile),
|
|
||||||
profile,
|
profile,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
_items = _items.sort(item => {
|
if (sortByMessageDeclaration) {
|
||||||
// @ts-ignore
|
_items = _items.sort(item => {
|
||||||
return item.enabled ? -1 : 1
|
return item.type === 'profile' && canBeMessaged(item.profile)
|
||||||
})
|
? -1
|
||||||
|
: 1
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const placeholders: Item[] = Array(10)
|
const placeholders: Item[] = Array(10)
|
||||||
@@ -134,14 +151,13 @@ export function SearchablePeopleList({
|
|||||||
_items.push({
|
_items.push({
|
||||||
type: 'profile',
|
type: 'profile',
|
||||||
key: profile.did,
|
key: profile.did,
|
||||||
enabled: true,
|
|
||||||
profile,
|
profile,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let followsItems: typeof _items = []
|
let followsItems: ProfileItem[] = []
|
||||||
|
|
||||||
for (const page of follows.pages) {
|
for (const page of follows.pages) {
|
||||||
for (const profile of page.follows) {
|
for (const profile of page.follows) {
|
||||||
@@ -150,17 +166,17 @@ export function SearchablePeopleList({
|
|||||||
followsItems.push({
|
followsItems.push({
|
||||||
type: 'profile',
|
type: 'profile',
|
||||||
key: profile.did,
|
key: profile.did,
|
||||||
enabled: canBeMessaged(profile),
|
|
||||||
profile,
|
profile,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// only sort follows
|
if (sortByMessageDeclaration) {
|
||||||
followsItems = followsItems.sort(item => {
|
// only sort follows
|
||||||
// @ts-ignore
|
followsItems = followsItems.sort(item => {
|
||||||
return item.enabled ? -1 : 1
|
return canBeMessaged(item.profile) ? -1 : 1
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// then append
|
// then append
|
||||||
_items.push(...followsItems)
|
_items.push(...followsItems)
|
||||||
@@ -173,16 +189,18 @@ export function SearchablePeopleList({
|
|||||||
_items.push({
|
_items.push({
|
||||||
type: 'profile',
|
type: 'profile',
|
||||||
key: profile.did,
|
key: profile.did,
|
||||||
enabled: canBeMessaged(profile),
|
|
||||||
profile,
|
profile,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_items = _items.sort(item => {
|
if (sortByMessageDeclaration) {
|
||||||
// @ts-ignore
|
_items = _items.sort(item => {
|
||||||
return item.enabled ? -1 : 1
|
return item.type === 'profile' && canBeMessaged(item.profile)
|
||||||
})
|
? -1
|
||||||
|
: 1
|
||||||
|
})
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
_items.push(...placeholders)
|
_items.push(...placeholders)
|
||||||
}
|
}
|
||||||
@@ -198,6 +216,7 @@ export function SearchablePeopleList({
|
|||||||
follows,
|
follows,
|
||||||
convos,
|
convos,
|
||||||
showRecentConvos,
|
showRecentConvos,
|
||||||
|
sortByMessageDeclaration,
|
||||||
])
|
])
|
||||||
|
|
||||||
if (searchText && !isFetching && !items.length && !isError) {
|
if (searchText && !isFetching && !items.length && !isError) {
|
||||||
@@ -208,15 +227,18 @@ export function SearchablePeopleList({
|
|||||||
({item}: {item: Item}) => {
|
({item}: {item: Item}) => {
|
||||||
switch (item.type) {
|
switch (item.type) {
|
||||||
case 'profile': {
|
case 'profile': {
|
||||||
return (
|
if (renderProfileCard) {
|
||||||
<ProfileCard
|
return <Fragment key={item.key}>{renderProfileCard(item)}</Fragment>
|
||||||
key={item.key}
|
} else {
|
||||||
enabled={item.enabled}
|
return (
|
||||||
profile={item.profile}
|
<DefaultProfileCard
|
||||||
moderationOpts={moderationOpts!}
|
key={item.key}
|
||||||
onPress={onSelectChat}
|
profile={item.profile}
|
||||||
/>
|
moderationOpts={moderationOpts!}
|
||||||
)
|
onPress={onSelectChat}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case 'placeholder': {
|
case 'placeholder': {
|
||||||
return <ProfileCardSkeleton key={item.key} />
|
return <ProfileCardSkeleton key={item.key} />
|
||||||
@@ -228,7 +250,7 @@ export function SearchablePeopleList({
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[moderationOpts, onSelectChat],
|
[moderationOpts, onSelectChat, renderProfileCard],
|
||||||
)
|
)
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
@@ -247,6 +269,10 @@ export function SearchablePeopleList({
|
|||||||
a.relative,
|
a.relative,
|
||||||
web(a.pt_lg),
|
web(a.pt_lg),
|
||||||
native(a.pt_4xl),
|
native(a.pt_4xl),
|
||||||
|
android({
|
||||||
|
borderTopLeftRadius: a.rounded_md.borderRadius,
|
||||||
|
borderTopRightRadius: a.rounded_md.borderRadius,
|
||||||
|
}),
|
||||||
a.pb_xs,
|
a.pb_xs,
|
||||||
a.px_lg,
|
a.px_lg,
|
||||||
a.border_b,
|
a.border_b,
|
||||||
@@ -327,19 +353,18 @@ export function SearchablePeopleList({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProfileCard({
|
function DefaultProfileCard({
|
||||||
enabled,
|
|
||||||
profile,
|
profile,
|
||||||
moderationOpts,
|
moderationOpts,
|
||||||
onPress,
|
onPress,
|
||||||
}: {
|
}: {
|
||||||
enabled: boolean
|
|
||||||
profile: bsky.profile.AnyProfileView
|
profile: bsky.profile.AnyProfileView
|
||||||
moderationOpts: ModerationOpts
|
moderationOpts: ModerationOpts
|
||||||
onPress: (did: string) => void
|
onPress: (did: string) => void
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const enabled = canBeMessaged(profile)
|
||||||
const moderation = moderateProfile(profile, moderationOpts)
|
const moderation = moderateProfile(profile, moderationOpts)
|
||||||
const handle = sanitizeHandle(profile.handle, '@')
|
const handle = sanitizeHandle(profile.handle, '@')
|
||||||
const displayName = sanitizeDisplayName(
|
const displayName = sanitizeDisplayName(
|
||||||
@@ -360,38 +385,35 @@ function ProfileCard({
|
|||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.flex_1,
|
a.flex_1,
|
||||||
a.py_md,
|
a.py_sm,
|
||||||
a.px_lg,
|
a.px_lg,
|
||||||
a.gap_md,
|
|
||||||
a.align_center,
|
|
||||||
a.flex_row,
|
|
||||||
!enabled
|
!enabled
|
||||||
? {opacity: 0.5}
|
? {opacity: 0.5}
|
||||||
: pressed || focused
|
: pressed || focused || hovered
|
||||||
? t.atoms.bg_contrast_25
|
? t.atoms.bg_contrast_25
|
||||||
: hovered
|
|
||||||
? t.atoms.bg_contrast_50
|
|
||||||
: t.atoms.bg,
|
: t.atoms.bg,
|
||||||
]}>
|
]}>
|
||||||
<UserAvatar
|
<ProfileCard.Header>
|
||||||
size={42}
|
<ProfileCard.Avatar
|
||||||
avatar={profile.avatar}
|
profile={profile}
|
||||||
moderation={moderation.ui('avatar')}
|
moderationOpts={moderationOpts}
|
||||||
type={profile.associated?.labeler ? 'labeler' : 'user'}
|
/>
|
||||||
/>
|
<View style={[a.flex_1]}>
|
||||||
<View style={[a.flex_1, a.gap_2xs]}>
|
<ProfileCard.Name
|
||||||
<Text
|
profile={profile}
|
||||||
style={[t.atoms.text, a.font_bold, a.leading_tight, a.self_start]}
|
moderationOpts={moderationOpts}
|
||||||
numberOfLines={1}
|
/>
|
||||||
emoji>
|
{enabled ? (
|
||||||
{displayName}
|
<ProfileCard.Handle profile={profile} />
|
||||||
</Text>
|
) : (
|
||||||
<Text
|
<Text
|
||||||
style={[a.leading_tight, t.atoms.text_contrast_high]}
|
style={[a.leading_snug, t.atoms.text_contrast_high]}
|
||||||
numberOfLines={2}>
|
numberOfLines={2}>
|
||||||
{!enabled ? <Trans>{handle} can't be messaged</Trans> : handle}
|
<Trans>{handle} can't be messaged</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
)}
|
||||||
|
</View>
|
||||||
|
</ProfileCard.Header>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</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 {useTheme} from '#/alf'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import {useDialogControl} from '#/components/Dialog'
|
import {useDialogControl} from '#/components/Dialog'
|
||||||
|
import {SearchablePeopleList} from '#/components/dialogs/SearchablePeopleList'
|
||||||
import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog'
|
import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog'
|
||||||
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||||
import {SearchablePeopleList} from './SearchablePeopleList'
|
|
||||||
|
|
||||||
export function NewChat({
|
export function NewChat({
|
||||||
control,
|
control,
|
||||||
@@ -71,6 +71,7 @@ export function NewChat({
|
|||||||
<SearchablePeopleList
|
<SearchablePeopleList
|
||||||
title={_(msg`Start a new chat`)}
|
title={_(msg`Start a new chat`)}
|
||||||
onSelectChat={onCreateChat}
|
onSelectChat={onCreateChat}
|
||||||
|
sortByMessageDeclaration
|
||||||
/>
|
/>
|
||||||
</Dialog.Outer>
|
</Dialog.Outer>
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {logger} from '#/logger'
|
|||||||
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
|
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import {SearchablePeopleList} from './SearchablePeopleList'
|
import {SearchablePeopleList} from '#/components/dialogs/SearchablePeopleList'
|
||||||
|
|
||||||
export function SendViaChatDialog({
|
export function SendViaChatDialog({
|
||||||
control,
|
control,
|
||||||
@@ -62,6 +62,7 @@ function SendViaChatDialogInner({
|
|||||||
title={_(msg`Send post to...`)}
|
title={_(msg`Send post to...`)}
|
||||||
onSelectChat={onCreateChat}
|
onSelectChat={onCreateChat}
|
||||||
showRecentConvos
|
showRecentConvos
|
||||||
|
sortByMessageDeclaration
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
import {type Image as RNImage} from 'react-native-image-crop-picker'
|
||||||
import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api'
|
import {type AppBskyActorDefs, type AppBskyGraphDefs} from '@atproto/api'
|
||||||
|
|
||||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||||
|
|
||||||
@@ -26,15 +26,6 @@ export interface UserAddRemoveListsModal {
|
|||||||
onRemove?: (listUri: string) => void
|
onRemove?: (listUri: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ListAddRemoveUsersModal {
|
|
||||||
name: 'list-add-remove-users'
|
|
||||||
list: AppBskyGraphDefs.ListView
|
|
||||||
onChange?: (
|
|
||||||
type: 'add' | 'remove',
|
|
||||||
profile: AppBskyActorDefs.ProfileViewBasic,
|
|
||||||
) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CropImageModal {
|
export interface CropImageModal {
|
||||||
name: 'crop-image'
|
name: 'crop-image'
|
||||||
uri: string
|
uri: string
|
||||||
@@ -107,7 +98,6 @@ export type Modal =
|
|||||||
// Lists
|
// Lists
|
||||||
| CreateOrEditListModal
|
| CreateOrEditListModal
|
||||||
| UserAddRemoveListsModal
|
| UserAddRemoveListsModal
|
||||||
| ListAddRemoveUsersModal
|
|
||||||
|
|
||||||
// Posts
|
// Posts
|
||||||
| CropImageModal
|
| CropImageModal
|
||||||
|
|||||||
@@ -90,7 +90,13 @@ export function getMembership(
|
|||||||
return membership ? membership.membershipUri : false
|
return membership ? membership.membershipUri : false
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useListMembershipAddMutation() {
|
export function useListMembershipAddMutation({
|
||||||
|
onSuccess,
|
||||||
|
onError,
|
||||||
|
}: {
|
||||||
|
onSuccess?: (data: {uri: string; cid: string}) => void
|
||||||
|
onError?: (error: Error) => void
|
||||||
|
} = {}) {
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
@@ -117,7 +123,7 @@ export function useListMembershipAddMutation() {
|
|||||||
// -prf
|
// -prf
|
||||||
return res
|
return res
|
||||||
},
|
},
|
||||||
onSuccess(data, variables) {
|
onSuccess: (data, variables) => {
|
||||||
// manually update the cache; a refetch is too expensive
|
// manually update the cache; a refetch is too expensive
|
||||||
let memberships = queryClient.getQueryData<ListMembersip[]>(RQKEY())
|
let memberships = queryClient.getQueryData<ListMembersip[]>(RQKEY())
|
||||||
if (memberships) {
|
if (memberships) {
|
||||||
@@ -145,11 +151,19 @@ export function useListMembershipAddMutation() {
|
|||||||
queryKey: LIST_MEMBERS_RQKEY(variables.listUri),
|
queryKey: LIST_MEMBERS_RQKEY(variables.listUri),
|
||||||
})
|
})
|
||||||
}, 1e3)
|
}, 1e3)
|
||||||
|
onSuccess?.(data)
|
||||||
},
|
},
|
||||||
|
onError,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useListMembershipRemoveMutation() {
|
export function useListMembershipRemoveMutation({
|
||||||
|
onSuccess,
|
||||||
|
onError,
|
||||||
|
}: {
|
||||||
|
onSuccess?: (data: void) => void
|
||||||
|
onError?: (error: Error) => void
|
||||||
|
} = {}) {
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
@@ -172,7 +186,7 @@ export function useListMembershipRemoveMutation() {
|
|||||||
// query for that, so we use a timeout below
|
// query for that, so we use a timeout below
|
||||||
// -prf
|
// -prf
|
||||||
},
|
},
|
||||||
onSuccess(data, variables) {
|
onSuccess: (data, variables) => {
|
||||||
// manually update the cache; a refetch is too expensive
|
// manually update the cache; a refetch is too expensive
|
||||||
let memberships = queryClient.getQueryData<ListMembersip[]>(RQKEY())
|
let memberships = queryClient.getQueryData<ListMembersip[]>(RQKEY())
|
||||||
if (memberships) {
|
if (memberships) {
|
||||||
@@ -192,6 +206,8 @@ export function useListMembershipRemoveMutation() {
|
|||||||
queryKey: LIST_MEMBERS_RQKEY(variables.listUri),
|
queryKey: LIST_MEMBERS_RQKEY(variables.listUri),
|
||||||
})
|
})
|
||||||
}, 1e3)
|
}, 1e3)
|
||||||
|
onSuccess?.(data)
|
||||||
},
|
},
|
||||||
|
onError,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,316 +0,0 @@
|
|||||||
import React, {useCallback, useState} from 'react'
|
|
||||||
import {
|
|
||||||
ActivityIndicator,
|
|
||||||
Pressable,
|
|
||||||
SafeAreaView,
|
|
||||||
StyleSheet,
|
|
||||||
View,
|
|
||||||
} from 'react-native'
|
|
||||||
import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api'
|
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
|
||||||
import {msg, Trans} from '@lingui/macro'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
|
|
||||||
import {HITSLOP_20} from '#/lib/constants'
|
|
||||||
import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible'
|
|
||||||
import {usePalette} from '#/lib/hooks/usePalette'
|
|
||||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
|
||||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
|
||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
|
||||||
import {colors, s} from '#/lib/styles'
|
|
||||||
import {isWeb} from '#/platform/detection'
|
|
||||||
import {useModalControls} from '#/state/modals'
|
|
||||||
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
|
|
||||||
import {
|
|
||||||
getMembership,
|
|
||||||
ListMembersip,
|
|
||||||
useDangerousListMembershipsQuery,
|
|
||||||
useListMembershipAddMutation,
|
|
||||||
useListMembershipRemoveMutation,
|
|
||||||
} from '#/state/queries/list-memberships'
|
|
||||||
import {Button} from '../util/forms/Button'
|
|
||||||
import {Text} from '../util/text/Text'
|
|
||||||
import * as Toast from '../util/Toast'
|
|
||||||
import {UserAvatar} from '../util/UserAvatar'
|
|
||||||
import {ScrollView, TextInput} from './util'
|
|
||||||
|
|
||||||
export const snapPoints = ['90%']
|
|
||||||
|
|
||||||
export function Component({
|
|
||||||
list,
|
|
||||||
onChange,
|
|
||||||
}: {
|
|
||||||
list: AppBskyGraphDefs.ListView
|
|
||||||
onChange?: (
|
|
||||||
type: 'add' | 'remove',
|
|
||||||
profile: AppBskyActorDefs.ProfileViewBasic,
|
|
||||||
) => void
|
|
||||||
}) {
|
|
||||||
const pal = usePalette('default')
|
|
||||||
const {_} = useLingui()
|
|
||||||
const {closeModal} = useModalControls()
|
|
||||||
const {isMobile} = useWebMediaQueries()
|
|
||||||
const [query, setQuery] = useState('')
|
|
||||||
const autocomplete = useActorAutocompleteQuery(query)
|
|
||||||
const {data: memberships} = useDangerousListMembershipsQuery()
|
|
||||||
const [isKeyboardVisible] = useIsKeyboardVisible()
|
|
||||||
|
|
||||||
const onPressCancelSearch = useCallback(() => setQuery(''), [setQuery])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SafeAreaView
|
|
||||||
testID="listAddUserModal"
|
|
||||||
style={[pal.view, isWeb ? styles.fixedHeight : s.flex1]}>
|
|
||||||
<View style={[s.flex1, isMobile && {paddingHorizontal: 18}]}>
|
|
||||||
<View style={[styles.searchContainer, pal.border]}>
|
|
||||||
<FontAwesomeIcon icon="search" size={16} />
|
|
||||||
<TextInput
|
|
||||||
testID="searchInput"
|
|
||||||
style={[styles.searchInput, pal.border, pal.text]}
|
|
||||||
placeholder={_(msg`Search for users`)}
|
|
||||||
placeholderTextColor={pal.colors.textLight}
|
|
||||||
value={query}
|
|
||||||
onChangeText={setQuery}
|
|
||||||
accessible={true}
|
|
||||||
accessibilityLabel={_(msg`Search`)}
|
|
||||||
accessibilityHint=""
|
|
||||||
autoFocus
|
|
||||||
autoCapitalize="none"
|
|
||||||
autoComplete="off"
|
|
||||||
autoCorrect={false}
|
|
||||||
selectTextOnFocus
|
|
||||||
/>
|
|
||||||
{query ? (
|
|
||||||
<Pressable
|
|
||||||
onPress={onPressCancelSearch}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={_(msg`Cancel search`)}
|
|
||||||
accessibilityHint={_(msg`Exits inputting search query`)}
|
|
||||||
onAccessibilityEscape={onPressCancelSearch}
|
|
||||||
hitSlop={HITSLOP_20}>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon="xmark"
|
|
||||||
size={16}
|
|
||||||
color={pal.colors.textLight}
|
|
||||||
/>
|
|
||||||
</Pressable>
|
|
||||||
) : undefined}
|
|
||||||
</View>
|
|
||||||
<ScrollView
|
|
||||||
style={[s.flex1]}
|
|
||||||
keyboardDismissMode="none"
|
|
||||||
keyboardShouldPersistTaps="always">
|
|
||||||
{autocomplete.isLoading ? (
|
|
||||||
<View style={{marginVertical: 20}}>
|
|
||||||
<ActivityIndicator />
|
|
||||||
</View>
|
|
||||||
) : autocomplete.data?.length ? (
|
|
||||||
<>
|
|
||||||
{autocomplete.data.slice(0, 40).map((item, i) => (
|
|
||||||
<UserResult
|
|
||||||
key={item.did}
|
|
||||||
list={list}
|
|
||||||
profile={item}
|
|
||||||
memberships={memberships}
|
|
||||||
noBorder={i === 0}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<Text
|
|
||||||
type="xl"
|
|
||||||
style={[
|
|
||||||
pal.textLight,
|
|
||||||
{paddingHorizontal: 12, paddingVertical: 16},
|
|
||||||
]}>
|
|
||||||
<Trans>No results found for {query}</Trans>
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</ScrollView>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.btnContainer,
|
|
||||||
{paddingBottom: isKeyboardVisible ? 10 : 20},
|
|
||||||
]}>
|
|
||||||
<Button
|
|
||||||
testID="doneBtn"
|
|
||||||
type="default"
|
|
||||||
onPress={() => {
|
|
||||||
closeModal()
|
|
||||||
}}
|
|
||||||
accessibilityLabel={_(msg`Done`)}
|
|
||||||
accessibilityHint=""
|
|
||||||
label={_(msg({message: 'Done', context: 'action'}))}
|
|
||||||
labelContainerStyle={{justifyContent: 'center', padding: 4}}
|
|
||||||
labelStyle={[s.f18]}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</SafeAreaView>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function UserResult({
|
|
||||||
profile,
|
|
||||||
list,
|
|
||||||
memberships,
|
|
||||||
noBorder,
|
|
||||||
onChange,
|
|
||||||
}: {
|
|
||||||
profile: AppBskyActorDefs.ProfileViewBasic
|
|
||||||
list: AppBskyGraphDefs.ListView
|
|
||||||
memberships: ListMembersip[] | undefined
|
|
||||||
noBorder: boolean
|
|
||||||
onChange?: (
|
|
||||||
type: 'add' | 'remove',
|
|
||||||
profile: AppBskyActorDefs.ProfileViewBasic,
|
|
||||||
) => void | undefined
|
|
||||||
}) {
|
|
||||||
const pal = usePalette('default')
|
|
||||||
const {_} = useLingui()
|
|
||||||
const [isProcessing, setIsProcessing] = useState(false)
|
|
||||||
const membership = React.useMemo(
|
|
||||||
() => getMembership(memberships, list.uri, profile.did),
|
|
||||||
[memberships, list.uri, profile.did],
|
|
||||||
)
|
|
||||||
const listMembershipAddMutation = useListMembershipAddMutation()
|
|
||||||
const listMembershipRemoveMutation = useListMembershipRemoveMutation()
|
|
||||||
|
|
||||||
const onToggleMembership = useCallback(async () => {
|
|
||||||
if (typeof membership === 'undefined') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setIsProcessing(true)
|
|
||||||
try {
|
|
||||||
if (membership === false) {
|
|
||||||
await listMembershipAddMutation.mutateAsync({
|
|
||||||
listUri: list.uri,
|
|
||||||
actorDid: profile.did,
|
|
||||||
})
|
|
||||||
Toast.show(_(msg`Added to list`))
|
|
||||||
onChange?.('add', profile)
|
|
||||||
} else {
|
|
||||||
await listMembershipRemoveMutation.mutateAsync({
|
|
||||||
listUri: list.uri,
|
|
||||||
actorDid: profile.did,
|
|
||||||
membershipUri: membership,
|
|
||||||
})
|
|
||||||
Toast.show(_(msg`Removed from list`))
|
|
||||||
onChange?.('remove', profile)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
Toast.show(cleanError(e), 'xmark')
|
|
||||||
} finally {
|
|
||||||
setIsProcessing(false)
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
_,
|
|
||||||
list,
|
|
||||||
profile,
|
|
||||||
membership,
|
|
||||||
setIsProcessing,
|
|
||||||
onChange,
|
|
||||||
listMembershipAddMutation,
|
|
||||||
listMembershipRemoveMutation,
|
|
||||||
])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
pal.border,
|
|
||||||
{
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
borderTopWidth: noBorder ? 0 : 1,
|
|
||||||
paddingHorizontal: 8,
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
width: 54,
|
|
||||||
paddingLeft: 4,
|
|
||||||
}}>
|
|
||||||
<UserAvatar
|
|
||||||
size={40}
|
|
||||||
avatar={profile.avatar}
|
|
||||||
type={profile.associated?.labeler ? 'labeler' : 'user'}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
paddingRight: 10,
|
|
||||||
paddingTop: 10,
|
|
||||||
paddingBottom: 10,
|
|
||||||
}}>
|
|
||||||
<Text
|
|
||||||
type="lg"
|
|
||||||
style={[s.bold, pal.text]}
|
|
||||||
numberOfLines={1}
|
|
||||||
lineHeight={1.2}>
|
|
||||||
{sanitizeDisplayName(
|
|
||||||
profile.displayName || sanitizeHandle(profile.handle),
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
<Text type="md" style={[pal.textLight]} numberOfLines={1}>
|
|
||||||
{sanitizeHandle(profile.handle, '@')}
|
|
||||||
</Text>
|
|
||||||
{!!profile.viewer?.followedBy && <View style={s.flexRow} />}
|
|
||||||
</View>
|
|
||||||
<View>
|
|
||||||
{isProcessing || typeof membership === 'undefined' ? (
|
|
||||||
<ActivityIndicator />
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
testID={`user-${profile.handle}-addBtn`}
|
|
||||||
type="default"
|
|
||||||
label={membership === false ? _(msg`Add`) : _(msg`Remove`)}
|
|
||||||
onPress={onToggleMembership}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
fixedHeight: {
|
|
||||||
// @ts-ignore web only -prf
|
|
||||||
height: '80vh',
|
|
||||||
},
|
|
||||||
titleSection: {
|
|
||||||
paddingTop: isWeb ? 0 : 4,
|
|
||||||
paddingBottom: isWeb ? 14 : 10,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
textAlign: 'center',
|
|
||||||
fontWeight: '600',
|
|
||||||
marginBottom: 5,
|
|
||||||
},
|
|
||||||
searchContainer: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 8,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderRadius: 24,
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
paddingVertical: 10,
|
|
||||||
},
|
|
||||||
searchInput: {
|
|
||||||
fontSize: 16,
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
btn: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
borderRadius: 32,
|
|
||||||
padding: 14,
|
|
||||||
backgroundColor: colors.blue3,
|
|
||||||
},
|
|
||||||
btnContainer: {
|
|
||||||
paddingTop: 10,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -17,7 +17,6 @@ import * as InviteCodesModal from './InviteCodes'
|
|||||||
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
|
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
|
||||||
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
|
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
|
||||||
import * as LinkWarningModal from './LinkWarning'
|
import * as LinkWarningModal from './LinkWarning'
|
||||||
import * as ListAddUserModal from './ListAddRemoveUsers'
|
|
||||||
import * as UserAddRemoveListsModal from './UserAddRemoveLists'
|
import * as UserAddRemoveListsModal from './UserAddRemoveLists'
|
||||||
import * as VerifyEmailModal from './VerifyEmail'
|
import * as VerifyEmailModal from './VerifyEmail'
|
||||||
|
|
||||||
@@ -61,9 +60,6 @@ export function ModalsContainer() {
|
|||||||
} else if (activeModal?.name === 'user-add-remove-lists') {
|
} else if (activeModal?.name === 'user-add-remove-lists') {
|
||||||
snapPoints = UserAddRemoveListsModal.snapPoints
|
snapPoints = UserAddRemoveListsModal.snapPoints
|
||||||
element = <UserAddRemoveListsModal.Component {...activeModal} />
|
element = <UserAddRemoveListsModal.Component {...activeModal} />
|
||||||
} else if (activeModal?.name === 'list-add-remove-users') {
|
|
||||||
snapPoints = ListAddUserModal.snapPoints
|
|
||||||
element = <ListAddUserModal.Component {...activeModal} />
|
|
||||||
} else if (activeModal?.name === 'delete-account') {
|
} else if (activeModal?.name === 'delete-account') {
|
||||||
snapPoints = DeleteAccountModal.snapPoints
|
snapPoints = DeleteAccountModal.snapPoints
|
||||||
element = <DeleteAccountModal.Component />
|
element = <DeleteAccountModal.Component />
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {RemoveScrollBar} from 'react-remove-scroll-bar'
|
|||||||
|
|
||||||
import {usePalette} from '#/lib/hooks/usePalette'
|
import {usePalette} from '#/lib/hooks/usePalette'
|
||||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||||
import type {Modal as ModalIface} from '#/state/modals'
|
import {type Modal as ModalIface} from '#/state/modals'
|
||||||
import {useModalControls, useModals} from '#/state/modals'
|
import {useModalControls, useModals} from '#/state/modals'
|
||||||
import * as ChangeEmailModal from './ChangeEmail'
|
import * as ChangeEmailModal from './ChangeEmail'
|
||||||
import * as ChangePasswordModal from './ChangePassword'
|
import * as ChangePasswordModal from './ChangePassword'
|
||||||
@@ -16,7 +16,6 @@ import * as InviteCodesModal from './InviteCodes'
|
|||||||
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
|
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
|
||||||
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
|
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
|
||||||
import * as LinkWarningModal from './LinkWarning'
|
import * as LinkWarningModal from './LinkWarning'
|
||||||
import * as ListAddUserModal from './ListAddRemoveUsers'
|
|
||||||
import * as UserAddRemoveLists from './UserAddRemoveLists'
|
import * as UserAddRemoveLists from './UserAddRemoveLists'
|
||||||
import * as VerifyEmailModal from './VerifyEmail'
|
import * as VerifyEmailModal from './VerifyEmail'
|
||||||
|
|
||||||
@@ -65,8 +64,6 @@ function Modal({modal}: {modal: ModalIface}) {
|
|||||||
element = <CreateOrEditListModal.Component {...modal} />
|
element = <CreateOrEditListModal.Component {...modal} />
|
||||||
} else if (modal.name === 'user-add-remove-lists') {
|
} else if (modal.name === 'user-add-remove-lists') {
|
||||||
element = <UserAddRemoveLists.Component {...modal} />
|
element = <UserAddRemoveLists.Component {...modal} />
|
||||||
} else if (modal.name === 'list-add-remove-users') {
|
|
||||||
element = <ListAddUserModal.Component {...modal} />
|
|
||||||
} else if (modal.name === 'crop-image') {
|
} else if (modal.name === 'crop-image') {
|
||||||
element = <CropImageModal.Component {...modal} />
|
element = <CropImageModal.Component {...modal} />
|
||||||
} else if (modal.name === 'delete-account') {
|
} else if (modal.name === 'delete-account') {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
AppBskyGraphDefs,
|
AppBskyGraphDefs,
|
||||||
AtUri,
|
AtUri,
|
||||||
moderateUserList,
|
moderateUserList,
|
||||||
ModerationOpts,
|
type ModerationOpts,
|
||||||
RichText as RichTextAPI,
|
RichText as RichTextAPI,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
@@ -21,8 +21,11 @@ import {useSetTitle} from '#/lib/hooks/useSetTitle'
|
|||||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||||
import {ComposeIcon2} from '#/lib/icons'
|
import {ComposeIcon2} from '#/lib/icons'
|
||||||
import {makeListLink} from '#/lib/routes/links'
|
import {makeListLink} from '#/lib/routes/links'
|
||||||
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
import {
|
||||||
import {NavigationProp} from '#/lib/routes/types'
|
type CommonNavigatorParams,
|
||||||
|
type NativeStackScreenProps,
|
||||||
|
} from '#/lib/routes/types'
|
||||||
|
import {type NavigationProp} from '#/lib/routes/types'
|
||||||
import {shareUrl} from '#/lib/sharing'
|
import {shareUrl} from '#/lib/sharing'
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
import {toShareUrl} from '#/lib/strings/url-helpers'
|
import {toShareUrl} from '#/lib/strings/url-helpers'
|
||||||
@@ -38,12 +41,12 @@ import {
|
|||||||
useListMuteMutation,
|
useListMuteMutation,
|
||||||
useListQuery,
|
useListQuery,
|
||||||
} from '#/state/queries/list'
|
} from '#/state/queries/list'
|
||||||
import {FeedDescriptor} from '#/state/queries/post-feed'
|
import {type FeedDescriptor} from '#/state/queries/post-feed'
|
||||||
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
|
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
|
||||||
import {
|
import {
|
||||||
useAddSavedFeedsMutation,
|
useAddSavedFeedsMutation,
|
||||||
usePreferencesQuery,
|
usePreferencesQuery,
|
||||||
UsePreferencesQueryResponse,
|
type UsePreferencesQueryResponse,
|
||||||
useRemoveFeedMutation,
|
useRemoveFeedMutation,
|
||||||
useUpdateSavedFeedsMutation,
|
useUpdateSavedFeedsMutation,
|
||||||
} from '#/state/queries/preferences'
|
} from '#/state/queries/preferences'
|
||||||
@@ -60,10 +63,10 @@ import {EmptyState} from '#/view/com/util/EmptyState'
|
|||||||
import {FAB} from '#/view/com/util/fab/FAB'
|
import {FAB} from '#/view/com/util/fab/FAB'
|
||||||
import {Button} from '#/view/com/util/forms/Button'
|
import {Button} from '#/view/com/util/forms/Button'
|
||||||
import {
|
import {
|
||||||
DropdownItem,
|
type DropdownItem,
|
||||||
NativeDropdown,
|
NativeDropdown,
|
||||||
} from '#/view/com/util/forms/NativeDropdown'
|
} from '#/view/com/util/forms/NativeDropdown'
|
||||||
import {ListRef} from '#/view/com/util/List'
|
import {type ListRef} from '#/view/com/util/List'
|
||||||
import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
|
import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
|
||||||
import {LoadingScreen} from '#/view/com/util/LoadingScreen'
|
import {LoadingScreen} from '#/view/com/util/LoadingScreen'
|
||||||
import {Text} from '#/view/com/util/text/Text'
|
import {Text} from '#/view/com/util/text/Text'
|
||||||
@@ -72,6 +75,7 @@ import {ListHiddenScreen} from '#/screens/List/ListHiddenScreen'
|
|||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
import {Button as NewButton, ButtonIcon, ButtonText} from '#/components/Button'
|
import {Button as NewButton, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
import {useDialogControl} from '#/components/Dialog'
|
import {useDialogControl} from '#/components/Dialog'
|
||||||
|
import {ListAddRemoveUsersDialog} from '#/components/dialogs/lists/ListAddRemoveUsersDialog'
|
||||||
import {PersonPlus_Stroke2_Corner0_Rounded as PersonPlusIcon} from '#/components/icons/Person'
|
import {PersonPlus_Stroke2_Corner0_Rounded as PersonPlusIcon} from '#/components/icons/Person'
|
||||||
import * as Layout from '#/components/Layout'
|
import * as Layout from '#/components/Layout'
|
||||||
import * as Hider from '#/components/moderation/Hider'
|
import * as Hider from '#/components/moderation/Hider'
|
||||||
@@ -157,12 +161,12 @@ function ProfileListScreenLoaded({
|
|||||||
const {rkey} = route.params
|
const {rkey} = route.params
|
||||||
const feedSectionRef = React.useRef<SectionRef>(null)
|
const feedSectionRef = React.useRef<SectionRef>(null)
|
||||||
const aboutSectionRef = React.useRef<SectionRef>(null)
|
const aboutSectionRef = React.useRef<SectionRef>(null)
|
||||||
const {openModal} = useModalControls()
|
|
||||||
const isCurateList = list.purpose === AppBskyGraphDefs.CURATELIST
|
const isCurateList = list.purpose === AppBskyGraphDefs.CURATELIST
|
||||||
const isScreenFocused = useIsFocused()
|
const isScreenFocused = useIsFocused()
|
||||||
const isHidden = list.labels?.findIndex(l => l.val === '!hide') !== -1
|
const isHidden = list.labels?.findIndex(l => l.val === '!hide') !== -1
|
||||||
const isOwner = currentAccount?.did === list.creator.did
|
const isOwner = currentAccount?.did === list.creator.did
|
||||||
const scrollElRef = useAnimatedRef()
|
const scrollElRef = useAnimatedRef()
|
||||||
|
const addUserDialogControl = useDialogControl()
|
||||||
const sectionTitlesCurate = [_(msg`Posts`), _(msg`People`)]
|
const sectionTitlesCurate = [_(msg`Posts`), _(msg`People`)]
|
||||||
|
|
||||||
const moderation = React.useMemo(() => {
|
const moderation = React.useMemo(() => {
|
||||||
@@ -177,17 +181,11 @@ function ProfileListScreenLoaded({
|
|||||||
}, [setMinimalShellMode]),
|
}, [setMinimalShellMode]),
|
||||||
)
|
)
|
||||||
|
|
||||||
const onPressAddUser = useCallback(() => {
|
const onChangeMembers = useCallback(() => {
|
||||||
openModal({
|
if (isCurateList) {
|
||||||
name: 'list-add-remove-users',
|
truncateAndInvalidate(queryClient, FEED_RQKEY(`list|${list.uri}`))
|
||||||
list,
|
}
|
||||||
onChange() {
|
}, [list.uri, isCurateList, queryClient])
|
||||||
if (isCurateList) {
|
|
||||||
truncateAndInvalidate(queryClient, FEED_RQKEY(`list|${list.uri}`))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}, [openModal, list, isCurateList, queryClient])
|
|
||||||
|
|
||||||
const onCurrentPageSelected = React.useCallback(
|
const onCurrentPageSelected = React.useCallback(
|
||||||
(index: number) => {
|
(index: number) => {
|
||||||
@@ -225,7 +223,7 @@ function ProfileListScreenLoaded({
|
|||||||
headerHeight={headerHeight}
|
headerHeight={headerHeight}
|
||||||
isFocused={isScreenFocused && isFocused}
|
isFocused={isScreenFocused && isFocused}
|
||||||
isOwner={isOwner}
|
isOwner={isOwner}
|
||||||
onPressAddUser={onPressAddUser}
|
onPressAddUser={addUserDialogControl.open}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{({headerHeight, scrollElRef}) => (
|
{({headerHeight, scrollElRef}) => (
|
||||||
@@ -233,7 +231,7 @@ function ProfileListScreenLoaded({
|
|||||||
ref={aboutSectionRef}
|
ref={aboutSectionRef}
|
||||||
scrollElRef={scrollElRef as ListRef}
|
scrollElRef={scrollElRef as ListRef}
|
||||||
list={list}
|
list={list}
|
||||||
onPressAddUser={onPressAddUser}
|
onPressAddUser={addUserDialogControl.open}
|
||||||
headerHeight={headerHeight}
|
headerHeight={headerHeight}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -253,6 +251,11 @@ function ProfileListScreenLoaded({
|
|||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
<ListAddRemoveUsersDialog
|
||||||
|
control={addUserDialogControl}
|
||||||
|
list={list}
|
||||||
|
onChange={onChangeMembers}
|
||||||
|
/>
|
||||||
</Hider.Content>
|
</Hider.Content>
|
||||||
</Hider.Outer>
|
</Hider.Outer>
|
||||||
)
|
)
|
||||||
@@ -268,7 +271,7 @@ function ProfileListScreenLoaded({
|
|||||||
<AboutSection
|
<AboutSection
|
||||||
list={list}
|
list={list}
|
||||||
scrollElRef={scrollElRef as ListRef}
|
scrollElRef={scrollElRef as ListRef}
|
||||||
onPressAddUser={onPressAddUser}
|
onPressAddUser={addUserDialogControl.open}
|
||||||
headerHeight={0}
|
headerHeight={0}
|
||||||
/>
|
/>
|
||||||
<FAB
|
<FAB
|
||||||
@@ -286,6 +289,11 @@ function ProfileListScreenLoaded({
|
|||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
<ListAddRemoveUsersDialog
|
||||||
|
control={addUserDialogControl}
|
||||||
|
list={list}
|
||||||
|
onChange={onChangeMembers}
|
||||||
|
/>
|
||||||
</Hider.Content>
|
</Hider.Content>
|
||||||
</Hider.Outer>
|
</Hider.Outer>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user