Support group chats across other surfaces (#10266)
This commit is contained in:
@@ -18,7 +18,7 @@ import type * as bsky from '#/types/bsky'
|
|||||||
type Props = {
|
type Props = {
|
||||||
animate?: boolean
|
animate?: boolean
|
||||||
profiles: bsky.profile.AnyProfileView[]
|
profiles: bsky.profile.AnyProfileView[]
|
||||||
size?: 'small' | 'medium' | 'large'
|
size?: 'small' | 'medium' | 'large' | number
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AvatarBubbles({
|
export function AvatarBubbles({
|
||||||
@@ -28,8 +28,22 @@ export function AvatarBubbles({
|
|||||||
}: Props) {
|
}: Props) {
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const profiles = allProfiles.filter(p => p.did !== currentAccount?.did)
|
const profiles = allProfiles.filter(p => p.did !== currentAccount?.did)
|
||||||
const containerSize = size === 'small' ? 40 : size === 'medium' ? 56 : 120
|
const containerSize =
|
||||||
const scale = size === 'small' ? 40 / 120 : size === 'medium' ? 56 / 120 : 1
|
typeof size === 'number'
|
||||||
|
? size
|
||||||
|
: size === 'small'
|
||||||
|
? 40
|
||||||
|
: size === 'medium'
|
||||||
|
? 56
|
||||||
|
: 120
|
||||||
|
const scale =
|
||||||
|
typeof size === 'number'
|
||||||
|
? size / 120
|
||||||
|
: size === 'small'
|
||||||
|
? 40 / 120
|
||||||
|
: size === 'medium'
|
||||||
|
? 56 / 120
|
||||||
|
: 1
|
||||||
const marginOffset = size === 'small' || size === 'medium' ? -2 : 0
|
const marginOffset = size === 'small' || size === 'medium' ? -2 : 0
|
||||||
|
|
||||||
const initialValue = animate ? 0 : 1
|
const initialValue = animate ? 0 : 1
|
||||||
|
|||||||
@@ -6,21 +6,21 @@ import {Trans} from '@lingui/react/macro'
|
|||||||
import {useNavigation} from '@react-navigation/native'
|
import {useNavigation} from '@react-navigation/native'
|
||||||
|
|
||||||
import {isBlockedOrBlocking, isMuted} from '#/lib/moderation/blocked-and-muted'
|
import {isBlockedOrBlocking, isMuted} from '#/lib/moderation/blocked-and-muted'
|
||||||
|
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
|
||||||
import {type NavigationProp} from '#/lib/routes/types'
|
import {type NavigationProp} from '#/lib/routes/types'
|
||||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
|
||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
|
||||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
import {useListConvosQuery} from '#/state/queries/messages/list-conversations'
|
import {useListConvosQuery} from '#/state/queries/messages/list-conversations'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {atoms as a, tokens, useTheme} from '#/alf'
|
import {atoms as a, tokens, useTheme} from '#/alf'
|
||||||
|
import {AvatarBubbles} from '#/components/AvatarBubbles'
|
||||||
import {Button} from '#/components/Button'
|
import {Button} from '#/components/Button'
|
||||||
import {useDialogContext} from '#/components/Dialog'
|
import {useDialogContext} from '#/components/Dialog'
|
||||||
|
import {type ConvoWithDetails, parseConvoView} from '#/components/dms/util'
|
||||||
import {ProfileBadges} from '#/components/ProfileBadges'
|
import {ProfileBadges} from '#/components/ProfileBadges'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {useAnalytics} from '#/analytics'
|
import {useAnalytics} from '#/analytics'
|
||||||
import type * as bsky from '#/types/bsky'
|
|
||||||
|
|
||||||
export function RecentChats({
|
export function RecentChats({
|
||||||
postUri,
|
postUri,
|
||||||
@@ -60,23 +60,24 @@ export function RecentChats({
|
|||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
nestedScrollEnabled>
|
nestedScrollEnabled>
|
||||||
{convos && convos.length > 0 ? (
|
{convos && convos.length > 0 ? (
|
||||||
convos.map(convo => {
|
convos.map(c => {
|
||||||
const otherMember = convo.members.find(
|
const convo = parseConvoView(c, currentAccount?.did)
|
||||||
member => member.did !== currentAccount?.did,
|
|
||||||
)
|
if (!convo) return null
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!otherMember ||
|
(convo.kind === 'direct' &&
|
||||||
otherMember.handle === 'missing.invalid' ||
|
convo.primaryMember.handle === 'missing.invalid') ||
|
||||||
convo.muted
|
convo.view.muted
|
||||||
)
|
) {
|
||||||
return null
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RecentChatItem
|
<RecentChatItem
|
||||||
key={convo.id}
|
key={convo.view.id}
|
||||||
profile={otherMember}
|
convo={convo}
|
||||||
onPress={() => onSelectChat(convo.id)}
|
onPress={() => onSelectChat(convo.view.id)}
|
||||||
moderationOpts={moderationOpts}
|
moderationOpts={moderationOpts}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -99,26 +100,33 @@ export function RecentChats({
|
|||||||
const WIDTH = 80
|
const WIDTH = 80
|
||||||
|
|
||||||
function RecentChatItem({
|
function RecentChatItem({
|
||||||
profile: profileUnshadowed,
|
|
||||||
onPress,
|
onPress,
|
||||||
moderationOpts,
|
moderationOpts,
|
||||||
|
convo,
|
||||||
}: {
|
}: {
|
||||||
profile: bsky.profile.AnyProfileView
|
|
||||||
onPress: () => void
|
onPress: () => void
|
||||||
moderationOpts: ModerationOpts
|
moderationOpts: ModerationOpts
|
||||||
|
convo: ConvoWithDetails
|
||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
|
||||||
const profile = useProfileShadow(profileUnshadowed)
|
const primaryProfile = useProfileShadow(convo.primaryMember)
|
||||||
|
|
||||||
const moderation = moderateProfile(profile, moderationOpts)
|
const moderation = moderateProfile(primaryProfile, moderationOpts)
|
||||||
const name = sanitizeDisplayName(
|
const name =
|
||||||
profile.displayName || sanitizeHandle(profile.handle),
|
convo.kind === 'group'
|
||||||
moderation.ui('displayName'),
|
? convo.details.name
|
||||||
)
|
: createSanitizedDisplayName(
|
||||||
|
primaryProfile,
|
||||||
|
true,
|
||||||
|
moderation.ui('displayName'),
|
||||||
|
)
|
||||||
|
|
||||||
if (isBlockedOrBlocking(profile) || isMuted(profile)) {
|
if (
|
||||||
|
convo.kind === 'direct' &&
|
||||||
|
(isBlockedOrBlocking(primaryProfile) || isMuted(primaryProfile))
|
||||||
|
) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,12 +141,16 @@ function RecentChatItem({
|
|||||||
a.justify_start,
|
a.justify_start,
|
||||||
a.align_center,
|
a.align_center,
|
||||||
]}>
|
]}>
|
||||||
<UserAvatar
|
{convo.kind === 'group' ? (
|
||||||
avatar={profile.avatar}
|
<AvatarBubbles profiles={convo.members} size={WIDTH - 8} />
|
||||||
size={WIDTH - 8}
|
) : (
|
||||||
type={profile.associated?.labeler ? 'labeler' : 'user'}
|
<UserAvatar
|
||||||
moderation={moderation.ui('avatar')}
|
avatar={primaryProfile.avatar}
|
||||||
/>
|
size={WIDTH - 8}
|
||||||
|
type={primaryProfile.associated?.labeler ? 'labeler' : 'user'}
|
||||||
|
moderation={moderation.ui('avatar')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<View style={[a.flex_row, a.align_center, a.justify_center, a.w_full]}>
|
<View style={[a.flex_row, a.align_center, a.justify_center, a.w_full]}>
|
||||||
<Text
|
<Text
|
||||||
emoji
|
emoji
|
||||||
@@ -146,7 +158,13 @@ function RecentChatItem({
|
|||||||
numberOfLines={1}>
|
numberOfLines={1}>
|
||||||
{name}
|
{name}
|
||||||
</Text>
|
</Text>
|
||||||
<ProfileBadges profile={profile} size="xs" style={[a.pl_2xs]} />
|
{convo.kind === 'direct' && (
|
||||||
|
<ProfileBadges
|
||||||
|
profile={primaryProfile}
|
||||||
|
size="xs"
|
||||||
|
style={[a.pl_2xs]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,11 +8,9 @@ import {
|
|||||||
} from 'react'
|
} from 'react'
|
||||||
import {TextInput, View} from 'react-native'
|
import {TextInput, View} from 'react-native'
|
||||||
import {moderateProfile, type ModerationOpts} from '@atproto/api'
|
import {moderateProfile, type ModerationOpts} from '@atproto/api'
|
||||||
import {msg} from '@lingui/core/macro'
|
import {Plural, Trans, useLingui} from '@lingui/react/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
import {Trans} from '@lingui/react/macro'
|
|
||||||
|
|
||||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
|
||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
|
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
|
||||||
@@ -23,7 +21,11 @@ import {type ListMethods} from '#/view/com/util/List'
|
|||||||
import {android, atoms as a, native, useTheme, web} from '#/alf'
|
import {android, 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,
|
||||||
|
type ConvoWithDetails,
|
||||||
|
parseConvoView,
|
||||||
|
} from '#/components/dms/util'
|
||||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||||
import {MagnifyingGlass_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass'
|
import {MagnifyingGlass_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass'
|
||||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||||
@@ -31,6 +33,9 @@ import * as ProfileCard from '#/components/ProfileCard'
|
|||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {IS_WEB} from '#/env'
|
import {IS_WEB} from '#/env'
|
||||||
import type * as bsky from '#/types/bsky'
|
import type * as bsky from '#/types/bsky'
|
||||||
|
import {AvatarBubbles} from '../AvatarBubbles'
|
||||||
|
import {Error} from '../Error'
|
||||||
|
import {ProfileBadges} from '../ProfileBadges'
|
||||||
|
|
||||||
export type ProfileItem = {
|
export type ProfileItem = {
|
||||||
type: 'profile'
|
type: 'profile'
|
||||||
@@ -38,6 +43,12 @@ export type ProfileItem = {
|
|||||||
profile: bsky.profile.AnyProfileView
|
profile: bsky.profile.AnyProfileView
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ExistingChatItem = {
|
||||||
|
type: 'existingChat'
|
||||||
|
key: string
|
||||||
|
convo: ConvoWithDetails
|
||||||
|
}
|
||||||
|
|
||||||
type EmptyItem = {
|
type EmptyItem = {
|
||||||
type: 'empty'
|
type: 'empty'
|
||||||
key: string
|
key: string
|
||||||
@@ -54,7 +65,12 @@ type ErrorItem = {
|
|||||||
key: string
|
key: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Item = ProfileItem | EmptyItem | PlaceholderItem | ErrorItem
|
type Item =
|
||||||
|
| ProfileItem
|
||||||
|
| ExistingChatItem
|
||||||
|
| EmptyItem
|
||||||
|
| PlaceholderItem
|
||||||
|
| ErrorItem
|
||||||
|
|
||||||
export function SearchablePeopleList({
|
export function SearchablePeopleList({
|
||||||
title,
|
title,
|
||||||
@@ -72,12 +88,14 @@ export function SearchablePeopleList({
|
|||||||
onSelectChat?: undefined
|
onSelectChat?: undefined
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
onSelectChat: (did: string) => void
|
onSelectChat: (
|
||||||
|
chat: {kind: 'user'; did: string} | {kind: 'convo'; id: string},
|
||||||
|
) => void
|
||||||
renderProfileCard?: undefined
|
renderProfileCard?: undefined
|
||||||
}
|
}
|
||||||
)) {
|
)) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const moderationOpts = useModerationOpts()
|
const moderationOpts = useModerationOpts()
|
||||||
const control = Dialog.useDialogContext()
|
const control = Dialog.useDialogContext()
|
||||||
const [headerHeight, setHeaderHeight] = useState(0)
|
const [headerHeight, setHeaderHeight] = useState(0)
|
||||||
@@ -105,7 +123,7 @@ export function SearchablePeopleList({
|
|||||||
_items.push({
|
_items.push({
|
||||||
type: 'empty',
|
type: 'empty',
|
||||||
key: 'empty',
|
key: 'empty',
|
||||||
message: _(msg`We're having network issues, try again`),
|
message: l`We're having network issues, try again`,
|
||||||
})
|
})
|
||||||
} else if (searchText.length) {
|
} else if (searchText.length) {
|
||||||
if (results?.length) {
|
if (results?.length) {
|
||||||
@@ -139,20 +157,27 @@ export function SearchablePeopleList({
|
|||||||
const usedDids = new Set()
|
const usedDids = new Set()
|
||||||
|
|
||||||
for (const page of convos.pages) {
|
for (const page of convos.pages) {
|
||||||
for (const convo of page.convos) {
|
for (const convoView of page.convos) {
|
||||||
const profiles = convo.members.filter(
|
const convo = parseConvoView(convoView, currentAccount?.did)
|
||||||
m => m.did !== currentAccount?.did,
|
|
||||||
)
|
|
||||||
|
|
||||||
for (const profile of profiles) {
|
if (!convo) continue
|
||||||
if (usedDids.has(profile.did)) continue
|
|
||||||
|
|
||||||
usedDids.add(profile.did)
|
if (convo.kind === 'group') {
|
||||||
|
_items.push({
|
||||||
|
type: 'existingChat',
|
||||||
|
key: convo.view.id,
|
||||||
|
convo,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
if (convo.primaryMember.handle === 'missing.invalid') continue
|
||||||
|
if (usedDids.has(convo.primaryMember.did)) continue
|
||||||
|
|
||||||
|
usedDids.add(convo.primaryMember.did)
|
||||||
|
|
||||||
_items.push({
|
_items.push({
|
||||||
type: 'profile',
|
type: 'existingChat',
|
||||||
key: profile.did,
|
key: convo.view.id,
|
||||||
profile,
|
convo: convo,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -209,7 +234,7 @@ export function SearchablePeopleList({
|
|||||||
|
|
||||||
return _items
|
return _items
|
||||||
}, [
|
}, [
|
||||||
_,
|
l,
|
||||||
searchText,
|
searchText,
|
||||||
results,
|
results,
|
||||||
isError,
|
isError,
|
||||||
@@ -221,12 +246,27 @@ export function SearchablePeopleList({
|
|||||||
])
|
])
|
||||||
|
|
||||||
if (searchText && !isFetching && !items.length && !isError) {
|
if (searchText && !isFetching && !items.length && !isError) {
|
||||||
items.push({type: 'empty', key: 'empty', message: _(msg`No results`)})
|
items.push({type: 'empty', key: 'empty', message: l`No results`})
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderItems = useCallback(
|
const renderItems = useCallback(
|
||||||
({item}: {item: Item}) => {
|
({item}: {item: Item}) => {
|
||||||
switch (item.type) {
|
switch (item.type) {
|
||||||
|
case 'existingChat': {
|
||||||
|
if (renderProfileCard) {
|
||||||
|
// should be unreachable
|
||||||
|
return null
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<ExistingChatCard
|
||||||
|
key={item.key}
|
||||||
|
convo={item.convo}
|
||||||
|
moderationOpts={moderationOpts!}
|
||||||
|
onPress={id => onSelectChat({kind: 'convo', id})}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
case 'profile': {
|
case 'profile': {
|
||||||
if (renderProfileCard) {
|
if (renderProfileCard) {
|
||||||
return <Fragment key={item.key}>{renderProfileCard(item)}</Fragment>
|
return <Fragment key={item.key}>{renderProfileCard(item)}</Fragment>
|
||||||
@@ -236,7 +276,7 @@ export function SearchablePeopleList({
|
|||||||
key={item.key}
|
key={item.key}
|
||||||
profile={item.profile}
|
profile={item.profile}
|
||||||
moderationOpts={moderationOpts!}
|
moderationOpts={moderationOpts!}
|
||||||
onPress={onSelectChat}
|
onPress={did => onSelectChat({kind: 'user', did})}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -247,11 +287,14 @@ export function SearchablePeopleList({
|
|||||||
case 'empty': {
|
case 'empty': {
|
||||||
return <Empty key={item.key} message={item.message} />
|
return <Empty key={item.key} message={item.message} />
|
||||||
}
|
}
|
||||||
|
case 'error': {
|
||||||
|
return <Error key={item.key} message={l`Failed to load profiles`} />
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[moderationOpts, onSelectChat, renderProfileCard],
|
[moderationOpts, onSelectChat, renderProfileCard, l],
|
||||||
)
|
)
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
@@ -293,7 +336,7 @@ export function SearchablePeopleList({
|
|||||||
</Text>
|
</Text>
|
||||||
{IS_WEB ? (
|
{IS_WEB ? (
|
||||||
<Button
|
<Button
|
||||||
label={_(msg`Close`)}
|
label={l`Close`}
|
||||||
size="small"
|
size="small"
|
||||||
shape="round"
|
shape="round"
|
||||||
variant={IS_WEB ? 'ghost' : 'solid'}
|
variant={IS_WEB ? 'ghost' : 'solid'}
|
||||||
@@ -328,7 +371,7 @@ export function SearchablePeopleList({
|
|||||||
t.atoms.border_contrast_low,
|
t.atoms.border_contrast_low,
|
||||||
t.atoms.bg,
|
t.atoms.bg,
|
||||||
t.atoms.text_contrast_high,
|
t.atoms.text_contrast_high,
|
||||||
_,
|
l,
|
||||||
title,
|
title,
|
||||||
searchText,
|
searchText,
|
||||||
control,
|
control,
|
||||||
@@ -364,12 +407,13 @@ function DefaultProfileCard({
|
|||||||
onPress: (did: string) => void
|
onPress: (did: string) => void
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const enabled = canBeMessaged(profile)
|
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 = createSanitizedDisplayName(
|
||||||
profile.displayName || sanitizeHandle(profile.handle),
|
profile,
|
||||||
|
true,
|
||||||
moderation.ui('displayName'),
|
moderation.ui('displayName'),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -380,7 +424,7 @@ function DefaultProfileCard({
|
|||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
disabled={!enabled}
|
disabled={!enabled}
|
||||||
label={_(msg`Start chat with ${displayName}`)}
|
label={l`Start chat with ${displayName}`}
|
||||||
onPress={handleOnPress}>
|
onPress={handleOnPress}>
|
||||||
{({hovered, pressed, focused}) => (
|
{({hovered, pressed, focused}) => (
|
||||||
<View
|
<View
|
||||||
@@ -422,6 +466,113 @@ function DefaultProfileCard({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ExistingChatCard({
|
||||||
|
convo,
|
||||||
|
moderationOpts,
|
||||||
|
onPress,
|
||||||
|
}: {
|
||||||
|
convo: ConvoWithDetails
|
||||||
|
moderationOpts: ModerationOpts
|
||||||
|
onPress: (convoId: string) => void
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const {t: l} = useLingui()
|
||||||
|
const enabled =
|
||||||
|
convo.kind === 'group' ? convo.details.lockStatus === 'unlocked' : true
|
||||||
|
const moderation = moderateProfile(convo.primaryMember, moderationOpts)
|
||||||
|
const name =
|
||||||
|
convo.kind === 'group'
|
||||||
|
? convo.details.name
|
||||||
|
: createSanitizedDisplayName(
|
||||||
|
convo.primaryMember,
|
||||||
|
true,
|
||||||
|
moderation.ui('displayName'),
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleOnPress = useCallback(() => {
|
||||||
|
onPress(convo.view.id)
|
||||||
|
}, [onPress, convo.view.id])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
disabled={!enabled}
|
||||||
|
label={l`Select chat "${name}"`}
|
||||||
|
onPress={handleOnPress}>
|
||||||
|
{({hovered, pressed, focused}) => (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.py_sm,
|
||||||
|
a.px_lg,
|
||||||
|
!enabled
|
||||||
|
? {opacity: 0.5}
|
||||||
|
: pressed || focused || hovered
|
||||||
|
? t.atoms.bg_contrast_25
|
||||||
|
: t.atoms.bg,
|
||||||
|
]}>
|
||||||
|
<ProfileCard.Header>
|
||||||
|
{convo.kind === 'group' ? (
|
||||||
|
<AvatarBubbles profiles={convo.members} size="small" />
|
||||||
|
) : (
|
||||||
|
<ProfileCard.Avatar
|
||||||
|
profile={convo.primaryMember}
|
||||||
|
moderationOpts={moderationOpts}
|
||||||
|
disabledPreview
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<View style={[a.flex_1]}>
|
||||||
|
<View style={[a.flex_row, a.align_center, a.max_w_full]}>
|
||||||
|
<Text
|
||||||
|
emoji
|
||||||
|
style={[
|
||||||
|
a.text_md,
|
||||||
|
a.font_semi_bold,
|
||||||
|
a.leading_snug,
|
||||||
|
a.self_start,
|
||||||
|
a.flex_shrink,
|
||||||
|
]}
|
||||||
|
numberOfLines={1}>
|
||||||
|
{name}
|
||||||
|
</Text>
|
||||||
|
{convo.kind === 'direct' && (
|
||||||
|
<ProfileBadges
|
||||||
|
profile={convo.primaryMember}
|
||||||
|
size="md"
|
||||||
|
style={[a.pl_xs]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
{convo.kind === 'direct' ? (
|
||||||
|
<ProfileCard.Handle profile={convo.primaryMember} />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{enabled ? (
|
||||||
|
<Text
|
||||||
|
style={[a.leading_snug, t.atoms.text_contrast_medium]}
|
||||||
|
numberOfLines={2}>
|
||||||
|
<Plural
|
||||||
|
value={convo.members.length}
|
||||||
|
one="# member"
|
||||||
|
other="# members"
|
||||||
|
/>
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Text
|
||||||
|
style={[a.leading_snug, t.atoms.text_contrast_high]}
|
||||||
|
numberOfLines={2}>
|
||||||
|
<Trans>Group is locked</Trans>
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</ProfileCard.Header>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function ProfileCardSkeleton() {
|
function ProfileCardSkeleton() {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
|
||||||
@@ -488,7 +639,7 @@ function SearchInput({
|
|||||||
inputRef: React.RefObject<TextInput | null>
|
inputRef: React.RefObject<TextInput | null>
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const {
|
const {
|
||||||
state: hovered,
|
state: hovered,
|
||||||
onIn: onMouseEnter,
|
onIn: onMouseEnter,
|
||||||
@@ -512,7 +663,7 @@ function SearchInput({
|
|||||||
<TextInput
|
<TextInput
|
||||||
// @ts-ignore bottom sheet input types issue — esb
|
// @ts-ignore bottom sheet input types issue — esb
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
placeholder={_(msg`Search`)}
|
placeholder={l`Search`}
|
||||||
value={value}
|
value={value}
|
||||||
onChangeText={onChangeText}
|
onChangeText={onChangeText}
|
||||||
onFocus={onFocus}
|
onFocus={onFocus}
|
||||||
@@ -532,8 +683,8 @@ function SearchInput({
|
|||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoFocus
|
autoFocus
|
||||||
accessibilityLabel={_(msg`Search profiles`)}
|
accessibilityLabel={l`Search profiles`}
|
||||||
accessibilityHint={_(msg`Searches for profiles`)}
|
accessibilityHint={l`Searches for profiles`}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -77,6 +77,15 @@ export function NewChat({
|
|||||||
[control, createGroupChat],
|
[control, createGroupChat],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const onSelectExistingChat = useCallback(
|
||||||
|
(chatId: string) => {
|
||||||
|
control.close(() => {
|
||||||
|
onNewChat(chatId)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[control, onNewChat],
|
||||||
|
)
|
||||||
|
|
||||||
const onPress = useCallback(() => {
|
const onPress = useCallback(() => {
|
||||||
control.open()
|
control.open()
|
||||||
}, [control])
|
}, [control])
|
||||||
@@ -112,7 +121,13 @@ export function NewChat({
|
|||||||
) : (
|
) : (
|
||||||
<SearchablePeopleList
|
<SearchablePeopleList
|
||||||
title={l`Start a new chat`}
|
title={l`Start a new chat`}
|
||||||
onSelectChat={onCreateChat}
|
onSelectChat={chat => {
|
||||||
|
if (chat.kind === 'user') {
|
||||||
|
onCreateChat(chat.did)
|
||||||
|
} else {
|
||||||
|
onSelectExistingChat(chat.id)
|
||||||
|
}
|
||||||
|
}}
|
||||||
sortByMessageDeclaration
|
sortByMessageDeclaration
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -53,6 +53,13 @@ function SendViaChatDialogInner({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const onSelectExistingChat = useCallback(
|
||||||
|
(chatId: string) => {
|
||||||
|
control.close(() => onSelectChat(chatId))
|
||||||
|
},
|
||||||
|
[control, onSelectChat],
|
||||||
|
)
|
||||||
|
|
||||||
const onCreateChat = useCallback(
|
const onCreateChat = useCallback(
|
||||||
(did: string) => {
|
(did: string) => {
|
||||||
control.close(() => createChat([did]))
|
control.close(() => createChat([did]))
|
||||||
@@ -63,7 +70,13 @@ function SendViaChatDialogInner({
|
|||||||
return (
|
return (
|
||||||
<SearchablePeopleList
|
<SearchablePeopleList
|
||||||
title={_(msg`Send post to...`)}
|
title={_(msg`Send post to...`)}
|
||||||
onSelectChat={onCreateChat}
|
onSelectChat={chat => {
|
||||||
|
if (chat.kind === 'user') {
|
||||||
|
onCreateChat(chat.did)
|
||||||
|
} else {
|
||||||
|
onSelectExistingChat(chat.id)
|
||||||
|
}
|
||||||
|
}}
|
||||||
showRecentConvos
|
showRecentConvos
|
||||||
sortByMessageDeclaration
|
sortByMessageDeclaration
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export {BottomSheetTextInput as TextInput} from '@discord/bottom-sheet/src'
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export {TextInput} from 'react-native'
|
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import {type ChatBskyConvoDefs} from '@atproto/api'
|
import {type $Typed, ChatBskyActorDefs, ChatBskyConvoDefs} from '@atproto/api'
|
||||||
|
|
||||||
import {EMOJI_REACTION_LIMIT} from '#/lib/constants'
|
import {EMOJI_REACTION_LIMIT} from '#/lib/constants'
|
||||||
import type * as bsky from '#/types/bsky'
|
import {logger} from '#/logger'
|
||||||
|
import * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
export function canBeMessaged(profile: bsky.profile.AnyProfileView) {
|
export function canBeMessaged(profile: bsky.profile.AnyProfileView) {
|
||||||
switch (profile.associated?.chat?.allowIncoming) {
|
switch (profile.associated?.chat?.allowIncoming) {
|
||||||
@@ -54,3 +55,99 @@ export function hasReachedReactionLimit(
|
|||||||
)
|
)
|
||||||
return myReactions.length >= EMOJI_REACTION_LIMIT
|
return myReactions.length >= EMOJI_REACTION_LIMIT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GroupConvoMember = ChatBskyActorDefs.ProfileViewBasic & {
|
||||||
|
// can be missing if account deleted
|
||||||
|
kind?: $Typed<ChatBskyActorDefs.GroupConvoMember>
|
||||||
|
}
|
||||||
|
|
||||||
|
type DirectConvoMember = ChatBskyActorDefs.ProfileViewBasic & {
|
||||||
|
kind: $Typed<ChatBskyActorDefs.DirectConvoMember>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ConvoWithDetails = {view: ChatBskyConvoDefs.ConvoView} & (
|
||||||
|
| {
|
||||||
|
kind: 'group'
|
||||||
|
details: ChatBskyConvoDefs.GroupConvo
|
||||||
|
primaryMember: GroupConvoMember // the owner
|
||||||
|
members: Array<GroupConvoMember>
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
kind: 'direct'
|
||||||
|
details: ChatBskyConvoDefs.DirectConvo
|
||||||
|
primaryMember: DirectConvoMember // the other user
|
||||||
|
members: Array<DirectConvoMember>
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a raw convoView into something easier to use (i.e. extracts chat owner)
|
||||||
|
* and enforces the correct type for convo members.
|
||||||
|
*/
|
||||||
|
export function parseConvoView(
|
||||||
|
convoView: ChatBskyConvoDefs.ConvoView,
|
||||||
|
ownDid: string | undefined,
|
||||||
|
): ConvoWithDetails | null {
|
||||||
|
if (
|
||||||
|
bsky.dangerousIsType<ChatBskyConvoDefs.GroupConvo>(
|
||||||
|
convoView.kind,
|
||||||
|
ChatBskyConvoDefs.isGroupConvo,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
let owner: GroupConvoMember | undefined = undefined
|
||||||
|
|
||||||
|
for (const member of convoView.members) {
|
||||||
|
if (
|
||||||
|
bsky.dangerousIsType<ChatBskyActorDefs.GroupConvoMember>(
|
||||||
|
member.kind,
|
||||||
|
ChatBskyActorDefs.isGroupConvoMember,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (member.kind.role === 'owner') {
|
||||||
|
// have to do a type assertion here
|
||||||
|
// this works: {...member, kind: member.kind}
|
||||||
|
// however that's creating a new object for no good reason
|
||||||
|
owner = member as GroupConvoMember
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
'Expected a GroupConvoMember, got an unknown kind of member',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!owner) {
|
||||||
|
throw new Error('No owner found in group convo')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
view: convoView,
|
||||||
|
kind: 'group',
|
||||||
|
details: convoView.kind,
|
||||||
|
primaryMember: owner,
|
||||||
|
members: convoView.members as Array<GroupConvoMember>,
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
bsky.dangerousIsType<ChatBskyConvoDefs.DirectConvo>(
|
||||||
|
convoView.kind,
|
||||||
|
ChatBskyConvoDefs.isDirectConvo,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
const otherUser = convoView.members.find(m => m.did !== ownDid)
|
||||||
|
|
||||||
|
if (!otherUser) {
|
||||||
|
throw new Error('No other user found in direct convo')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
view: convoView,
|
||||||
|
kind: 'direct',
|
||||||
|
details: convoView.kind,
|
||||||
|
primaryMember: otherUser as DirectConvoMember,
|
||||||
|
members: convoView.members as Array<DirectConvoMember>,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn('Unknown convo kind: ' + JSON.stringify(convoView.kind))
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import {useCallback, useMemo, useState} from 'react'
|
|||||||
import {type GestureResponderEvent, View} from 'react-native'
|
import {type GestureResponderEvent, View} from 'react-native'
|
||||||
import {
|
import {
|
||||||
AppBskyEmbedRecord,
|
AppBskyEmbedRecord,
|
||||||
ChatBskyActorDefs,
|
|
||||||
ChatBskyConvoDefs,
|
ChatBskyConvoDefs,
|
||||||
moderateProfile,
|
moderateProfile,
|
||||||
type ModerationDecision,
|
type ModerationDecision,
|
||||||
@@ -38,6 +37,7 @@ import {AvatarBubbles} from '#/components/AvatarBubbles'
|
|||||||
import {useDialogControl} from '#/components/Dialog'
|
import {useDialogControl} from '#/components/Dialog'
|
||||||
import {ConvoMenu} from '#/components/dms/ConvoMenu'
|
import {ConvoMenu} from '#/components/dms/ConvoMenu'
|
||||||
import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt'
|
import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt'
|
||||||
|
import {type ConvoWithDetails, parseConvoView} from '#/components/dms/util'
|
||||||
import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/Bell2'
|
import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/Bell2'
|
||||||
import {Envelope_Open_Stroke2_Corner0_Rounded as EnvelopeOpen} from '#/components/icons/EnveopeOpen'
|
import {Envelope_Open_Stroke2_Corner0_Rounded as EnvelopeOpen} from '#/components/icons/EnveopeOpen'
|
||||||
import {Trash_Stroke2_Corner0_Rounded} from '#/components/icons/Trash'
|
import {Trash_Stroke2_Corner0_Rounded} from '#/components/icons/Trash'
|
||||||
@@ -49,7 +49,7 @@ import {ProfileBadges} from '#/components/ProfileBadges'
|
|||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {useAnalytics} from '#/analytics'
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_NATIVE} from '#/env'
|
import {IS_NATIVE} from '#/env'
|
||||||
import * as bsky from '#/types/bsky'
|
import type * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
export const ChatListItemPortal = createPortalGroup()
|
export const ChatListItemPortal = createPortalGroup()
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ export const ChatListItemPortal = createPortalGroup()
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export function ChatListItem({
|
export function ChatListItem({
|
||||||
convo,
|
convo: convoView,
|
||||||
showMenu = true,
|
showMenu = true,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
@@ -75,83 +75,47 @@ export function ChatListItem({
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
const convo = parseConvoView(convoView, currentAccount?.did)
|
||||||
bsky.dangerousIsType<ChatBskyConvoDefs.GroupConvo>(
|
|
||||||
convo.kind,
|
switch (convo?.kind) {
|
||||||
ChatBskyConvoDefs.isGroupConvo,
|
case 'direct': {
|
||||||
)
|
return (
|
||||||
) {
|
<DirectChatItem
|
||||||
const owner = convo.members.find(r => {
|
convo={convo}
|
||||||
if (
|
moderationOpts={moderationOpts}
|
||||||
bsky.dangerousIsType<ChatBskyActorDefs.GroupConvoMember>(
|
showMenu={showMenu}>
|
||||||
r.kind,
|
{children}
|
||||||
ChatBskyActorDefs.isGroupConvoMember,
|
</DirectChatItem>
|
||||||
)
|
)
|
||||||
) {
|
|
||||||
return r.kind.role === 'owner'
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
'Expected a GroupConvoMember, got an unknown kind of member',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (!owner) {
|
|
||||||
// TODO: Determine if this is the right thing to do here. Throwing here so that
|
|
||||||
// if it turns out to be wrong it'll be very visible
|
|
||||||
throw new Error('Could not find the group owner in the group members')
|
|
||||||
}
|
}
|
||||||
|
case 'group': {
|
||||||
return (
|
return (
|
||||||
<GroupChatItem
|
<GroupChatItem
|
||||||
convo={convo}
|
convo={convo}
|
||||||
groupOwner={owner}
|
moderationOpts={moderationOpts}
|
||||||
groupInfo={convo.kind}
|
showMenu={showMenu}
|
||||||
moderationOpts={moderationOpts}
|
/>
|
||||||
showMenu={showMenu}
|
)
|
||||||
/>
|
}
|
||||||
)
|
default: {
|
||||||
} else if (
|
|
||||||
bsky.dangerousIsType<ChatBskyConvoDefs.DirectConvo>(
|
|
||||||
convo.kind,
|
|
||||||
ChatBskyConvoDefs.isDirectConvo,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
const otherMember = convo.members.find(
|
|
||||||
member => member.did !== currentAccount?.did,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!otherMember) {
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return (
|
|
||||||
<DirectChatItem
|
|
||||||
convo={convo}
|
|
||||||
profile={otherMember}
|
|
||||||
moderationOpts={moderationOpts}
|
|
||||||
showMenu={showMenu}>
|
|
||||||
{children}
|
|
||||||
</DirectChatItem>
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function DirectChatItem({
|
function DirectChatItem({
|
||||||
convo,
|
convo,
|
||||||
profile: profileUnshadowed,
|
|
||||||
moderationOpts,
|
moderationOpts,
|
||||||
showMenu,
|
showMenu,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
convo: ChatBskyConvoDefs.ConvoView
|
convo: Extract<ConvoWithDetails, {kind: 'direct'}>
|
||||||
profile: bsky.profile.AnyProfileView
|
|
||||||
moderationOpts: ModerationOpts
|
moderationOpts: ModerationOpts
|
||||||
showMenu?: boolean
|
showMenu?: boolean
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const profile = useProfileShadow(profileUnshadowed)
|
const profile = useProfileShadow(convo.primaryMember)
|
||||||
|
|
||||||
const moderation = useMemo(
|
const moderation = useMemo(
|
||||||
() => moderateProfile(profile, moderationOpts),
|
() => moderateProfile(profile, moderationOpts),
|
||||||
@@ -165,7 +129,7 @@ function DirectChatItem({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseChatItem
|
<BaseChatItem
|
||||||
convo={convo}
|
convo={convo.view}
|
||||||
avatar={
|
avatar={
|
||||||
<PreviewableUserAvatar
|
<PreviewableUserAvatar
|
||||||
profile={profile}
|
profile={profile}
|
||||||
@@ -176,7 +140,9 @@ function DirectChatItem({
|
|||||||
primaryProfile={profile}
|
primaryProfile={profile}
|
||||||
primaryProfileModeration={moderation}
|
primaryProfileModeration={moderation}
|
||||||
title={displayName}
|
title={displayName}
|
||||||
subtitle={isDeletedAccount ? undefined : sanitizeHandle(profile.handle)}
|
subtitle={
|
||||||
|
isDeletedAccount ? undefined : sanitizeHandle(profile.handle, '@')
|
||||||
|
}
|
||||||
accessibilityHint={
|
accessibilityHint={
|
||||||
!isDeletedAccount
|
!isDeletedAccount
|
||||||
? l`Go to conversation with ${profile.handle}`
|
? l`Go to conversation with ${profile.handle}`
|
||||||
@@ -200,32 +166,28 @@ function DirectChatItem({
|
|||||||
|
|
||||||
function GroupChatItem({
|
function GroupChatItem({
|
||||||
convo,
|
convo,
|
||||||
groupOwner: groupOwnerUnshadowed,
|
|
||||||
groupInfo,
|
|
||||||
moderationOpts,
|
moderationOpts,
|
||||||
showMenu,
|
showMenu,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
convo: ChatBskyConvoDefs.ConvoView
|
convo: Extract<ConvoWithDetails, {kind: 'group'}>
|
||||||
groupOwner: bsky.profile.AnyProfileView
|
|
||||||
groupInfo: ChatBskyConvoDefs.GroupConvo
|
|
||||||
moderationOpts: ModerationOpts
|
moderationOpts: ModerationOpts
|
||||||
showMenu?: boolean
|
showMenu?: boolean
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const groupOwner = useProfileShadow(groupOwnerUnshadowed)
|
const groupOwner = useProfileShadow(convo.primaryMember)
|
||||||
|
|
||||||
const moderation = useMemo(
|
const moderation = useMemo(
|
||||||
() => moderateProfile(groupOwner, moderationOpts),
|
() => moderateProfile(groupOwner, moderationOpts),
|
||||||
[groupOwner, moderationOpts],
|
[groupOwner, moderationOpts],
|
||||||
)
|
)
|
||||||
|
|
||||||
const chatName = groupInfo.name ?? l`${groupOwner.handle}'s group chat`
|
const chatName = convo.details.name
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseChatItem
|
<BaseChatItem
|
||||||
convo={convo}
|
convo={convo.view}
|
||||||
avatar={<AvatarBubbles profiles={convo.members} size="medium" />}
|
avatar={<AvatarBubbles profiles={convo.members} size="medium" />}
|
||||||
title={chatName}
|
title={chatName}
|
||||||
accessibilityHint={l`Go to the group chat named "${chatName}"`}
|
accessibilityHint={l`Go to the group chat named "${chatName}"`}
|
||||||
@@ -507,7 +469,7 @@ function BaseChatItem({
|
|||||||
label={title}
|
label={title}
|
||||||
accessibilityHint={accessibilityHint}
|
accessibilityHint={accessibilityHint}
|
||||||
accessibilityActions={
|
accessibilityActions={
|
||||||
IS_NATIVE
|
showMenu && IS_NATIVE
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
name: 'magicTap',
|
name: 'magicTap',
|
||||||
@@ -521,8 +483,8 @@ function BaseChatItem({
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
onLongPress={IS_NATIVE ? onLongPress : undefined}
|
onLongPress={showMenu && IS_NATIVE ? onLongPress : undefined}
|
||||||
onAccessibilityAction={onLongPress}>
|
onAccessibilityAction={showMenu ? onLongPress : undefined}>
|
||||||
{({hovered, pressed, focused}) => (
|
{({hovered, pressed, focused}) => (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
|
|||||||
@@ -5,31 +5,34 @@ import {Trans} from '@lingui/react/macro'
|
|||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {atoms as a, tokens} from '#/alf'
|
import {atoms as a, tokens} from '#/alf'
|
||||||
|
import {parseConvoView} from '#/components/dms/util'
|
||||||
import {KnownFollowers} from '#/components/KnownFollowers'
|
import {KnownFollowers} from '#/components/KnownFollowers'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {ChatListItem, ChatListItemPortal} from './ChatListItem'
|
import {ChatListItem, ChatListItemPortal} from './ChatListItem'
|
||||||
import {AcceptChatButton, DeleteChatButton, RejectMenu} from './RequestButtons'
|
import {AcceptChatButton, DeleteChatButton, RejectMenu} from './RequestButtons'
|
||||||
|
|
||||||
export function RequestListItem({convo}: {convo: ChatBskyConvoDefs.ConvoView}) {
|
export function RequestListItem({
|
||||||
|
convo: convoView,
|
||||||
|
}: {
|
||||||
|
convo: ChatBskyConvoDefs.ConvoView
|
||||||
|
}) {
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const moderationOpts = useModerationOpts()
|
const moderationOpts = useModerationOpts()
|
||||||
|
|
||||||
const otherUser = convo.members.find(
|
const convo = parseConvoView(convoView, currentAccount?.did)
|
||||||
member => member.did !== currentAccount?.did,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!otherUser || !moderationOpts) {
|
if (!convo || !moderationOpts) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDeletedAccount = otherUser.handle === 'missing.invalid'
|
const isDeletedAccount = convo.primaryMember.handle === 'missing.invalid'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.relative, a.flex_1]}>
|
<View style={[a.relative, a.flex_1]}>
|
||||||
<ChatListItem convo={convo} showMenu={false}>
|
<ChatListItem convo={convo.view} showMenu={false}>
|
||||||
<View style={[a.pt_xs, a.pb_2xs]}>
|
<View style={[a.pt_xs, a.pb_2xs]}>
|
||||||
<KnownFollowers
|
<KnownFollowers
|
||||||
profile={otherUser}
|
profile={convo.primaryMember}
|
||||||
moderationOpts={moderationOpts}
|
moderationOpts={moderationOpts}
|
||||||
minimal
|
minimal
|
||||||
showIfEmpty
|
showIfEmpty
|
||||||
@@ -59,17 +62,17 @@ export function RequestListItem({convo}: {convo: ChatBskyConvoDefs.ConvoView}) {
|
|||||||
]}>
|
]}>
|
||||||
{!isDeletedAccount ? (
|
{!isDeletedAccount ? (
|
||||||
<>
|
<>
|
||||||
<AcceptChatButton convo={convo} currentScreen="list" />
|
<AcceptChatButton convo={convo.view} currentScreen="list" />
|
||||||
<RejectMenu
|
<RejectMenu
|
||||||
convo={convo}
|
convo={convo.view}
|
||||||
profile={otherUser}
|
profile={convo.primaryMember}
|
||||||
showDeleteConvo
|
showDeleteConvo
|
||||||
currentScreen="list"
|
currentScreen="list"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<DeleteChatButton convo={convo} currentScreen="list" />
|
<DeleteChatButton convo={convo.view} currentScreen="list" />
|
||||||
<View style={a.flex_1} />
|
<View style={a.flex_1} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -24,17 +24,20 @@ export const RQKEY_ROOT = 'convo-list'
|
|||||||
export const RQKEY = (
|
export const RQKEY = (
|
||||||
status: 'accepted' | 'request' | 'all',
|
status: 'accepted' | 'request' | 'all',
|
||||||
readState: 'all' | 'unread' = 'all',
|
readState: 'all' | 'unread' = 'all',
|
||||||
) => [RQKEY_ROOT, status, readState]
|
kind: 'all' | 'group' | 'direct' = 'all',
|
||||||
|
) => [RQKEY_ROOT, status, readState, kind]
|
||||||
type RQPageParam = string | undefined
|
type RQPageParam = string | undefined
|
||||||
|
|
||||||
export function useListConvosQuery({
|
export function useListConvosQuery({
|
||||||
enabled,
|
enabled,
|
||||||
status,
|
status,
|
||||||
readState = 'all',
|
readState = 'all',
|
||||||
|
kind = 'all',
|
||||||
}: {
|
}: {
|
||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
status?: 'request' | 'accepted'
|
status?: 'request' | 'accepted'
|
||||||
readState?: 'all' | 'unread'
|
readState?: 'all' | 'unread'
|
||||||
|
kind?: 'all' | 'group' | 'direct'
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
|
|
||||||
@@ -47,6 +50,7 @@ export function useListConvosQuery({
|
|||||||
limit: 20,
|
limit: 20,
|
||||||
cursor: pageParam,
|
cursor: pageParam,
|
||||||
readState: readState === 'unread' ? 'unread' : undefined,
|
readState: readState === 'unread' ? 'unread' : undefined,
|
||||||
|
kind: kind === 'all' ? undefined : kind,
|
||||||
status,
|
status,
|
||||||
},
|
},
|
||||||
{headers: DM_SERVICE_HEADERS},
|
{headers: DM_SERVICE_HEADERS},
|
||||||
|
|||||||
Reference in New Issue
Block a user