Merge branch 'main' into starter-pack-stuck
This commit is contained in:
@@ -3,6 +3,7 @@ import {type StyleProp, View, type ViewStyle} from 'react-native'
|
||||
import Animated, {
|
||||
Easing,
|
||||
interpolate,
|
||||
type SharedValue,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withDelay,
|
||||
@@ -191,7 +192,7 @@ function AvatarBubble({
|
||||
includeProfileBorder,
|
||||
}: {
|
||||
profile?: bsky.profile.AnyProfileView
|
||||
scale: Animated.SharedValue<number>
|
||||
scale: SharedValue<number>
|
||||
size: number
|
||||
style?: StyleProp<ViewStyle>
|
||||
x: number
|
||||
@@ -214,7 +215,6 @@ function AvatarBubble({
|
||||
a.absolute,
|
||||
a.rounded_full,
|
||||
a.flex_grow_0,
|
||||
{transform: [{translateX: x}, {translateY: y}]},
|
||||
includeProfileBorder && {
|
||||
borderColor: t.atoms.text_inverted.color,
|
||||
borderWidth: 2,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {useImperativeHandle, useRef, useState} from 'react'
|
||||
import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native'
|
||||
import {type AppBskyEmbedVideo} from '@atproto/api'
|
||||
import {BlueskyVideoView} from '@haileyok/bluesky-video'
|
||||
import {BlueskyVideoView} from '@bsky.app/video'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
|
||||
@@ -190,7 +190,7 @@ function MenuContent({
|
||||
const isDeletedAccount = profile.handle === 'missing.invalid'
|
||||
|
||||
const convoId = initialConvo.id
|
||||
const {data: convo} = useConvoQuery(initialConvo)
|
||||
const {data: convo} = useConvoQuery({convoId})
|
||||
|
||||
const onNavigateToProfile = useCallback(() => {
|
||||
navigation.navigate('Profile', {name: profile.did})
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {useMemo} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {
|
||||
type AppBskyActorDefs,
|
||||
type ModerationCause,
|
||||
type ModerationDecision,
|
||||
ChatBskyConvoDefs,
|
||||
moderateProfile,
|
||||
type ModerationOpts,
|
||||
} from '@atproto/api'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
@@ -11,14 +11,8 @@ import {useNavigation} from '@react-navigation/native'
|
||||
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
|
||||
import {makeProfileLink} from '#/lib/routes/links'
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {logger} from '#/logger'
|
||||
import {type Shadow} from '#/state/cache/profile-shadow'
|
||||
import {
|
||||
type ActiveConvoStates,
|
||||
isConvoActive,
|
||||
useConvo,
|
||||
} from '#/state/messages/convo'
|
||||
import {type ConvoItem} from '#/state/messages/convo/types'
|
||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useSession} from '#/state/session'
|
||||
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
@@ -32,32 +26,13 @@ import {Link} from '#/components/Link'
|
||||
import {ProfileBadges} from '#/components/ProfileBadges'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {IS_LIQUID_GLASS, IS_WEB} from '#/env'
|
||||
import {type ConvoWithDetails} from './util'
|
||||
|
||||
const PFP_SIZE = IS_WEB ? 40 : Layout.HEADER_SLOT_SIZE
|
||||
|
||||
export function MessagesListHeader({
|
||||
profile,
|
||||
moderation,
|
||||
}: {
|
||||
profile?: Shadow<AppBskyActorDefs.ProfileViewDetailed>
|
||||
moderation?: ModerationDecision | null
|
||||
}) {
|
||||
export function MessagesListHeader({convo}: {convo?: ConvoWithDetails | null}) {
|
||||
const t = useTheme()
|
||||
|
||||
const convoState = useConvo()
|
||||
const isGroupChat = convoState?.isGroup?.()
|
||||
|
||||
const blockInfo = useMemo(() => {
|
||||
if (!moderation) return
|
||||
const modui = moderation.ui('profileView')
|
||||
const blocks = modui.alerts.filter(alert => alert.type === 'blocking')
|
||||
const listBlocks = blocks.filter(alert => alert.source.type === 'list')
|
||||
const userBlock = blocks.find(alert => alert.source.type === 'user')
|
||||
return {
|
||||
listBlocks,
|
||||
userBlock,
|
||||
}
|
||||
}, [moderation])
|
||||
const moderationOpts = useModerationOpts()
|
||||
|
||||
return (
|
||||
<Layout.Header.Outer noBottomBorder={IS_LIQUID_GLASS}>
|
||||
@@ -65,20 +40,11 @@ export function MessagesListHeader({
|
||||
<View style={[{minHeight: PFP_SIZE}, a.justify_center]}>
|
||||
<Layout.Header.BackButton />
|
||||
</View>
|
||||
{isConvoActive(convoState) ? (
|
||||
moderation && blockInfo && profile && !isGroupChat ? (
|
||||
<ProfileHeaderReady
|
||||
convoState={convoState}
|
||||
profile={profile}
|
||||
moderation={moderation}
|
||||
blockInfo={blockInfo}
|
||||
/>
|
||||
{convo && moderationOpts ? (
|
||||
convo.kind === 'direct' ? (
|
||||
<ProfileHeaderReady convo={convo} moderationOpts={moderationOpts} />
|
||||
) : (
|
||||
<GroupHeaderReady
|
||||
convoState={convoState}
|
||||
profile={profile}
|
||||
moderation={moderation}
|
||||
/>
|
||||
<GroupHeaderReady convo={convo} />
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
@@ -111,36 +77,38 @@ export function MessagesListHeader({
|
||||
}
|
||||
|
||||
function ProfileHeaderReady({
|
||||
convoState,
|
||||
profile,
|
||||
moderation,
|
||||
blockInfo,
|
||||
convo,
|
||||
moderationOpts,
|
||||
}: {
|
||||
convoState: ActiveConvoStates
|
||||
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
|
||||
moderation: ModerationDecision
|
||||
blockInfo: {
|
||||
listBlocks: ModerationCause[]
|
||||
userBlock?: ModerationCause
|
||||
}
|
||||
convo: Extract<ConvoWithDetails, {kind: 'direct'}>
|
||||
moderationOpts: ModerationOpts
|
||||
}) {
|
||||
const {t: l} = useLingui()
|
||||
const {currentAccount} = useSession()
|
||||
const profile = useProfileShadow(convo.primaryMember)
|
||||
|
||||
const moderation = moderateProfile(profile, moderationOpts)
|
||||
|
||||
const blockInfo = useMemo(() => {
|
||||
const modui = moderation.ui('profileView')
|
||||
const blocks = modui.alerts.filter(alert => alert.type === 'blocking')
|
||||
const listBlocks = blocks.filter(alert => alert.source.type === 'list')
|
||||
const userBlock = blocks.find(alert => alert.source.type === 'user')
|
||||
return {
|
||||
listBlocks,
|
||||
userBlock,
|
||||
}
|
||||
}, [moderation])
|
||||
|
||||
const isDeletedAccount = profile?.handle === 'missing.invalid'
|
||||
const displayName = isDeletedAccount
|
||||
? l`Deleted Account`
|
||||
: createSanitizedDisplayName(profile, true, moderation.ui('displayName'))
|
||||
|
||||
const latestMessageFromOther = convoState.items.findLast(
|
||||
(item: ConvoItem) =>
|
||||
item.type === 'message' &&
|
||||
item.message.sender.did !== currentAccount?.did,
|
||||
)
|
||||
|
||||
const latestReportableMessage =
|
||||
latestMessageFromOther?.type === 'message'
|
||||
? latestMessageFromOther.message
|
||||
ChatBskyConvoDefs.isMessageView(convo.view.lastMessage) &&
|
||||
convo.view.lastMessage.sender?.did !== currentAccount?.did
|
||||
? convo.view.lastMessage
|
||||
: undefined
|
||||
|
||||
return (
|
||||
@@ -164,82 +132,57 @@ function ProfileHeaderReady({
|
||||
</View>
|
||||
</Link>
|
||||
}
|
||||
muted={convoState.convo?.muted}
|
||||
muted={convo.view.muted}
|
||||
settings={
|
||||
isConvoActive(convoState) ? (
|
||||
<ConvoMenu
|
||||
convo={convoState.convo}
|
||||
profile={profile}
|
||||
currentScreen="conversation"
|
||||
blockInfo={blockInfo}
|
||||
latestReportableMessage={latestReportableMessage}
|
||||
/>
|
||||
) : null
|
||||
<ConvoMenu
|
||||
convo={convo.view}
|
||||
profile={profile}
|
||||
currentScreen="conversation"
|
||||
blockInfo={blockInfo}
|
||||
latestReportableMessage={latestReportableMessage}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function GroupHeaderReady({
|
||||
convoState,
|
||||
profile,
|
||||
moderation,
|
||||
convo,
|
||||
}: {
|
||||
convoState: ActiveConvoStates
|
||||
profile?: Shadow<AppBskyActorDefs.ProfileViewDetailed>
|
||||
moderation?: ModerationDecision | null
|
||||
convo: Extract<ConvoWithDetails, {kind: 'group'}>
|
||||
}) {
|
||||
const {t: l} = useLingui()
|
||||
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
|
||||
const groupInfo = convoState.getGroupInfo?.()
|
||||
|
||||
const isDeletedAccount = profile?.handle === 'missing.invalid'
|
||||
const displayName = isDeletedAccount
|
||||
? l`Deleted Account`
|
||||
: profile
|
||||
? createSanitizedDisplayName(profile, true, moderation?.ui('displayName'))
|
||||
: undefined
|
||||
const groupName =
|
||||
groupInfo?.name ??
|
||||
(displayName ? l`${displayName}’s group chat` : l`Group chat`)
|
||||
|
||||
const handleNavigateToSettings = () => {
|
||||
const convoId = convoState.convo?.id
|
||||
if (convoId) {
|
||||
navigation.navigate('MessagesConversationSettings', {
|
||||
conversation: convoId,
|
||||
})
|
||||
} else {
|
||||
logger.error(`handleNavigateToSettings: missing convo ID`)
|
||||
}
|
||||
navigation.navigate('MessagesConversationSettings', {
|
||||
conversation: convo.view.id,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrapper
|
||||
heading={
|
||||
<>
|
||||
<AvatarBubbles size="small" profiles={convoState.recipients ?? []} />
|
||||
<AvatarBubbles size="small" profiles={convo.members} />
|
||||
<Text style={[a.text_md, a.font_semi_bold]} numberOfLines={1}>
|
||||
{groupName}
|
||||
{convo.details.name}
|
||||
</Text>
|
||||
</>
|
||||
}
|
||||
muted={convoState.convo?.muted}
|
||||
muted={convo.view.muted}
|
||||
settings={
|
||||
isConvoActive(convoState) ? (
|
||||
<Button
|
||||
label={l`Open group chat settings`}
|
||||
size="small"
|
||||
color="secondary"
|
||||
shape="round"
|
||||
variant="ghost"
|
||||
style={[a.bg_transparent]}
|
||||
onPress={handleNavigateToSettings}>
|
||||
<ButtonIcon icon={DotsHorizontalIcon} size="md" />
|
||||
</Button>
|
||||
) : null
|
||||
<Button
|
||||
label={l`Open group chat settings`}
|
||||
size="small"
|
||||
color="secondary"
|
||||
shape="round"
|
||||
variant="ghost"
|
||||
style={[a.bg_transparent]}
|
||||
onPress={handleNavigateToSettings}>
|
||||
<ButtonIcon icon={DotsHorizontalIcon} size="md" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -357,7 +357,7 @@ function ReactionTab({
|
||||
accessibilityRole="button"
|
||||
accessibilityHint={
|
||||
reaction.key === 'all'
|
||||
? l`Tap to show all reactions `
|
||||
? l`Tap to show all reactions`
|
||||
: l`Tap to show ${reaction.value} reactions`
|
||||
}
|
||||
hitSlop={HITSLOP_10}
|
||||
|
||||
@@ -56,12 +56,12 @@ export function hasReachedReactionLimit(
|
||||
return myReactions.length >= EMOJI_REACTION_LIMIT
|
||||
}
|
||||
|
||||
type GroupConvoMember = ChatBskyActorDefs.ProfileViewBasic & {
|
||||
export type GroupConvoMember = ChatBskyActorDefs.ProfileViewBasic & {
|
||||
// can be missing if account deleted
|
||||
kind?: $Typed<ChatBskyActorDefs.GroupConvoMember>
|
||||
}
|
||||
|
||||
type DirectConvoMember = ChatBskyActorDefs.ProfileViewBasic & {
|
||||
export type DirectConvoMember = ChatBskyActorDefs.ProfileViewBasic & {
|
||||
kind: $Typed<ChatBskyActorDefs.DirectConvoMember>
|
||||
}
|
||||
|
||||
|
||||
@@ -257,7 +257,6 @@ export function Gallery({
|
||||
horizontal
|
||||
pagingEnabled={false}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
decelerationRate={0.993}
|
||||
directionalLockEnabled
|
||||
nestedScrollEnabled
|
||||
alwaysBounceVertical={false}
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import {useMemo} from 'react'
|
||||
|
||||
export function Provider({children}: {children: React.ReactNode}) {
|
||||
return children
|
||||
}
|
||||
|
||||
const noop = () => {}
|
||||
|
||||
export function useHotkeysContext() {
|
||||
return {
|
||||
enableScope: () => {},
|
||||
disableScope: () => {},
|
||||
}
|
||||
return useMemo(
|
||||
() => ({
|
||||
enableScope: noop,
|
||||
disableScope: noop,
|
||||
}),
|
||||
[],
|
||||
)
|
||||
}
|
||||
|
||||
+228
-227
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,7 @@
|
||||
import {useCallback, useEffect, useMemo, useState} from 'react'
|
||||
import {type LayoutChangeEvent, View} from 'react-native'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {
|
||||
type AppBskyActorDefs,
|
||||
moderateProfile,
|
||||
type ModerationDecision,
|
||||
} from '@atproto/api'
|
||||
import {moderateProfile} from '@atproto/api'
|
||||
import {
|
||||
ScrollEdgeEffect,
|
||||
ScrollEdgeEffectProvider,
|
||||
@@ -28,13 +24,14 @@ import {
|
||||
type CommonNavigatorParams,
|
||||
type NavigationProp,
|
||||
} from '#/lib/routes/types'
|
||||
import {type Shadow, useMaybeProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {useMaybeProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {useEmail} from '#/state/email-verification'
|
||||
import {ConvoProvider, isConvoActive, useConvo} from '#/state/messages/convo'
|
||||
import {ConvoStatus} from '#/state/messages/convo/types'
|
||||
import {useCurrentConvoId} from '#/state/messages/current-convo-id'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useProfileQuery} from '#/state/queries/profile'
|
||||
import {useConvoQuery} from '#/state/queries/messages/conversation'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {MessagesList} from '#/screens/Messages/components/MessagesList'
|
||||
import {atoms as a, useTheme, web} from '#/alf'
|
||||
@@ -46,10 +43,12 @@ import {
|
||||
} from '#/components/dialogs/EmailDialog'
|
||||
import {MessagesListBlockedFooter} from '#/components/dms/MessagesListBlockedFooter'
|
||||
import {MessagesListHeader} from '#/components/dms/MessagesListHeader'
|
||||
import {type ConvoWithDetails, parseConvoView} from '#/components/dms/util'
|
||||
import {Error} from '#/components/Error'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {IS_LIQUID_GLASS, IS_WEB} from '#/env'
|
||||
import {ChatDisabled} from './components/ChatDisabled'
|
||||
|
||||
type Props = NativeStackScreenProps<
|
||||
CommonNavigatorParams,
|
||||
@@ -93,30 +92,25 @@ export function MessagesConversationScreenInner({route}: Props) {
|
||||
style={web([{minHeight: 0}, a.flex_1])}>
|
||||
<ScrollEdgeEffectProvider>
|
||||
<ConvoProvider key={convoId} convoId={convoId}>
|
||||
<Inner />
|
||||
<Inner convoId={convoId} />
|
||||
</ConvoProvider>
|
||||
</ScrollEdgeEffectProvider>
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
|
||||
function Inner() {
|
||||
function Inner({convoId}: {convoId: string}) {
|
||||
const t = useTheme()
|
||||
const convoState = useConvo()
|
||||
const {_} = useLingui()
|
||||
const {currentAccount} = useSession()
|
||||
const isFocused = useIsFocused()
|
||||
const {top: topInset} = useSafeAreaInsets()
|
||||
const {data: convoData} = useConvoQuery({convoId})
|
||||
|
||||
const moderationOpts = useModerationOpts()
|
||||
const {data: recipientUnshadowed} = useProfileQuery({
|
||||
did: convoState.getPrimaryMember?.()?.did,
|
||||
})
|
||||
const recipient = useMaybeProfileShadow(recipientUnshadowed)
|
||||
|
||||
const moderation = useMemo(() => {
|
||||
if (!recipient || !moderationOpts) return null
|
||||
return moderateProfile(recipient, moderationOpts)
|
||||
}, [recipient, moderationOpts])
|
||||
const convo = convoData
|
||||
? parseConvoView(convoData, currentAccount?.did)
|
||||
: null
|
||||
|
||||
// Because we want to give the list a chance to asynchronously scroll to the end before it is visible to the user,
|
||||
// we use `hasScrolled` to determine when to render. With that said however, there is a chance that the chat will be
|
||||
@@ -143,11 +137,7 @@ function Inner() {
|
||||
<>
|
||||
<Layout.Center
|
||||
style={[a.w_full, IS_LIQUID_GLASS && {paddingTop: topInset}]}>
|
||||
{moderation ? (
|
||||
<MessagesListHeader profile={recipient} moderation={moderation} />
|
||||
) : (
|
||||
<MessagesListHeader />
|
||||
)}
|
||||
<MessagesListHeader convo={convo} />
|
||||
</Layout.Center>
|
||||
<Error
|
||||
title={_(msg`Something went wrong`)}
|
||||
@@ -165,19 +155,17 @@ function Inner() {
|
||||
{isFocused && IS_WEB && <RemoveScrollBar />}
|
||||
{!readyToShow && (
|
||||
<View style={IS_LIQUID_GLASS && {paddingTop: topInset}}>
|
||||
{moderation ? (
|
||||
<MessagesListHeader profile={recipient} moderation={moderation} />
|
||||
) : (
|
||||
<MessagesListHeader />
|
||||
)}
|
||||
<MessagesListHeader convo={convo} />
|
||||
</View>
|
||||
)}
|
||||
<View style={[a.flex_1]}>
|
||||
<InnerReady
|
||||
moderation={moderation}
|
||||
recipient={recipient}
|
||||
convo={convo}
|
||||
hasScrolled={hasScrolled}
|
||||
setHasScrolled={setHasScrolled}
|
||||
isActive={isConvoActive(convoState)}
|
||||
isDisabled={convoState.status === ConvoStatus.Disabled}
|
||||
hasMessages={isConvoActive(convoState) && convoState.items.length > 0}
|
||||
/>
|
||||
{!readyToShow && (
|
||||
<View
|
||||
@@ -201,17 +189,20 @@ function Inner() {
|
||||
}
|
||||
|
||||
function InnerReady({
|
||||
moderation,
|
||||
recipient,
|
||||
hasScrolled,
|
||||
setHasScrolled,
|
||||
convo,
|
||||
isActive,
|
||||
isDisabled,
|
||||
hasMessages,
|
||||
}: {
|
||||
moderation: ModerationDecision | null
|
||||
recipient: Shadow<AppBskyActorDefs.ProfileViewDetailed> | undefined
|
||||
hasScrolled: boolean
|
||||
setHasScrolled: React.Dispatch<React.SetStateAction<boolean>>
|
||||
convo: ConvoWithDetails | null
|
||||
isActive: boolean
|
||||
isDisabled: boolean
|
||||
hasMessages: boolean
|
||||
}) {
|
||||
const convoState = useConvo()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {top: topInset} = useSafeAreaInsets()
|
||||
const [headerHeight, setHeaderHeight] = useState(0)
|
||||
@@ -261,9 +252,14 @@ function InnerReady({
|
||||
maybeBlockForEmailVerification()
|
||||
}, [maybeBlockForEmailVerification])
|
||||
|
||||
const header = (
|
||||
<MessagesListHeader profile={recipient} moderation={moderation} />
|
||||
)
|
||||
const primaryMember = useMaybeProfileShadow(convo?.primaryMember)
|
||||
const moderationOpts = useModerationOpts()
|
||||
const primaryMemberModeration = useMemo(() => {
|
||||
if (!primaryMember || !moderationOpts) return null
|
||||
return moderateProfile(primaryMember, moderationOpts)
|
||||
}, [primaryMember, moderationOpts])
|
||||
|
||||
const header = <MessagesListHeader convo={convo} />
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -277,20 +273,21 @@ function InnerReady({
|
||||
) : (
|
||||
header
|
||||
)}
|
||||
{isConvoActive(convoState) && (
|
||||
{isActive && (
|
||||
<MessagesList
|
||||
hasScrolled={hasScrolled}
|
||||
setHasScrolled={setHasScrolled}
|
||||
blocked={moderation?.blocked}
|
||||
hasAcceptOverride={!!params.accept}
|
||||
transparentHeaderHeight={IS_LIQUID_GLASS ? headerHeight : 0}
|
||||
footer={
|
||||
moderation && recipient ? (
|
||||
isDisabled ? (
|
||||
<ChatDisabled />
|
||||
) : convo && primaryMember && primaryMemberModeration?.blocked ? (
|
||||
<MessagesListBlockedFooter
|
||||
recipient={recipient}
|
||||
convoId={convoState.convo.id}
|
||||
hasMessages={convoState.items.length > 0}
|
||||
moderation={moderation}
|
||||
recipient={primaryMember}
|
||||
convoId={convo.view.id}
|
||||
hasMessages={hasMessages}
|
||||
moderation={primaryMemberModeration}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {useMemo, useState} from 'react'
|
||||
import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native'
|
||||
import {type ChatBskyConvoDefs, moderateProfile} from '@atproto/api'
|
||||
import {moderateProfile} from '@atproto/api'
|
||||
import {plural} from '@lingui/core/macro'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
import {StackActions, useNavigation} from '@react-navigation/native'
|
||||
@@ -34,6 +34,7 @@ import {AvatarBubbles} from '#/components/AvatarBubbles'
|
||||
import {Button, type ButtonColor, ButtonIcon} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {AddMembersFlow} from '#/components/dms/AddMembersFlow'
|
||||
import {type ConvoWithDetails, parseConvoView} from '#/components/dms/util'
|
||||
import {Error} from '#/components/Error'
|
||||
import * as TextField from '#/components/forms/TextField'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
@@ -126,9 +127,14 @@ function SettingsInner() {
|
||||
|
||||
const convoState = useConvo()
|
||||
const {currentAccount} = useSession()
|
||||
const primaryMember = convoState?.getPrimaryMember?.()
|
||||
|
||||
const data: bsky.profile.AnyProfileView[] = convoState.convo?.members ?? []
|
||||
const convo = convoState.convo
|
||||
? parseConvoView(convoState.convo, currentAccount?.did)
|
||||
: null
|
||||
const primaryMember = convo?.primaryMember
|
||||
const isOwner = !!primaryMember && primaryMember.did === currentAccount?.did
|
||||
|
||||
const data: bsky.profile.AnyProfileView[] = convo?.members ?? []
|
||||
const invites: string[] = []
|
||||
|
||||
const items = [
|
||||
@@ -163,11 +169,23 @@ function SettingsInner() {
|
||||
function renderItem({item}: {item: Item}) {
|
||||
switch (item.type) {
|
||||
case 'MEMBERS_AND_REQUESTS':
|
||||
return <MembersAndRequests memberCount={data.length} requestCount={5} />
|
||||
return (
|
||||
<MembersAndRequests
|
||||
memberCount={data.length}
|
||||
requestCount={5}
|
||||
isOwner={isOwner}
|
||||
/>
|
||||
)
|
||||
case 'ADD_MEMBERS_LINK':
|
||||
return <AddMembersLink />
|
||||
return <AddMembersLink isOwner={isOwner} />
|
||||
case 'CHAT_MEMBER':
|
||||
return <Member profile={item.profile} status={item.status} />
|
||||
return (
|
||||
<Member
|
||||
profile={item.profile}
|
||||
status={item.status}
|
||||
isOwner={isOwner}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
@@ -194,8 +212,8 @@ function SettingsInner() {
|
||||
initialNumToRender={initialNumToRender}
|
||||
keyExtractor={keyExtractor}
|
||||
ListHeaderComponent={
|
||||
convoState.convo ? (
|
||||
<SettingsHeader convo={convoState.convo} profiles={data} />
|
||||
convo ? (
|
||||
<SettingsHeader convo={convo} isOwner={isOwner} />
|
||||
) : (
|
||||
<SettingsHeaderPlaceholder />
|
||||
)
|
||||
@@ -211,21 +229,15 @@ function SettingsInner() {
|
||||
function MembersAndRequests({
|
||||
memberCount,
|
||||
requestCount,
|
||||
isOwner,
|
||||
}: {
|
||||
memberCount: number
|
||||
requestCount: number
|
||||
isOwner: boolean
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
|
||||
const convoState = useConvo()
|
||||
const {currentAccount} = useSession()
|
||||
|
||||
const isOwner =
|
||||
currentAccount?.did == null
|
||||
? false
|
||||
: convoState.getPrimaryMember?.()?.did === currentAccount.did
|
||||
|
||||
return (
|
||||
<View style={[a.flex_row, a.justify_between, a.mx_xl, a.mt_lg, a.mb_sm]}>
|
||||
<View style={[a.flex_row, a.align_center]}>
|
||||
@@ -254,20 +266,12 @@ function MembersAndRequests({
|
||||
)
|
||||
}
|
||||
|
||||
function AddMembersLink() {
|
||||
function AddMembersLink({isOwner}: {isOwner: boolean}) {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
|
||||
const convoState = useConvo()
|
||||
const {currentAccount} = useSession()
|
||||
|
||||
const addMembersControl = Dialog.useDialogControl()
|
||||
|
||||
const isOwner =
|
||||
currentAccount?.did == null
|
||||
? false
|
||||
: convoState.getPrimaryMember?.()?.did === currentAccount.did
|
||||
|
||||
if (!isOwner) {
|
||||
return null
|
||||
}
|
||||
@@ -354,9 +358,11 @@ function AddMembersLink() {
|
||||
function Member({
|
||||
profile,
|
||||
status,
|
||||
isOwner,
|
||||
}: {
|
||||
profile: Shadow<bsky.profile.AnyProfileView>
|
||||
status: 'owner' | 'member' | 'invited'
|
||||
isOwner: boolean
|
||||
}) {
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const t = useTheme()
|
||||
@@ -388,7 +394,9 @@ function Member({
|
||||
break
|
||||
}
|
||||
} else {
|
||||
statusBadge = <MemberMenu profile={profile} type={status} />
|
||||
statusBadge = (
|
||||
<MemberMenu profile={profile} type={status} isOwner={isOwner} />
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -496,9 +504,11 @@ function StatusButton({
|
||||
function MemberMenu({
|
||||
profile,
|
||||
type,
|
||||
isOwner,
|
||||
}: {
|
||||
profile: Shadow<bsky.profile.AnyProfileView>
|
||||
type: 'owner' | 'member' | 'invited'
|
||||
isOwner: boolean
|
||||
}) {
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const t = useTheme()
|
||||
@@ -506,16 +516,9 @@ function MemberMenu({
|
||||
const ax = useAnalytics()
|
||||
|
||||
const requireEmailVerification = useRequireEmailVerification()
|
||||
const convoState = useConvo()
|
||||
const {currentAccount} = useSession()
|
||||
|
||||
const blockMemberPrompt = Prompt.usePromptControl()
|
||||
|
||||
const isOwner =
|
||||
currentAccount?.did == null
|
||||
? false
|
||||
: convoState.getPrimaryMember?.()?.did === currentAccount.did
|
||||
|
||||
const {data: convoAvailability} = useGetConvoAvailabilityQuery(profile.did)
|
||||
const {mutate: initiateConvo} = useGetConvoForMembers({
|
||||
onSuccess: ({convo}) => {
|
||||
@@ -652,7 +655,7 @@ function MemberMenu({
|
||||
label={l`Message ${displayName}`}
|
||||
onPress={handleMessageMember}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Message</Trans>
|
||||
<Trans context="action">Message</Trans>
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={MessageIcon} />
|
||||
</Menu.Item>
|
||||
@@ -706,29 +709,22 @@ function MemberMenu({
|
||||
|
||||
function SettingsHeader({
|
||||
convo,
|
||||
profiles,
|
||||
isOwner,
|
||||
}: {
|
||||
convo: ChatBskyConvoDefs.ConvoView
|
||||
profiles: bsky.profile.AnyProfileView[]
|
||||
convo: ConvoWithDetails
|
||||
isOwner: boolean
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const convoState = useConvo()
|
||||
const {currentAccount} = useSession()
|
||||
|
||||
const groupName = convoState.getGroupInfo?.()?.name ?? ''
|
||||
const groupName = convo.kind === 'group' ? convo.details.name : ''
|
||||
const [newGroupName, setNewGroupName] = useState(groupName)
|
||||
|
||||
const [isLocked, setIsLocked] = useState(false)
|
||||
|
||||
const isOwner =
|
||||
currentAccount?.did == null
|
||||
? false
|
||||
: convoState.getPrimaryMember?.()?.did === currentAccount.did
|
||||
|
||||
const {mutate: editGroupName} = useEditGroupName(convo.id, {
|
||||
const {mutate: editGroupName} = useEditGroupName(convo.view.id, {
|
||||
onError: e => {
|
||||
setNewGroupName(groupName)
|
||||
logger.error('Failed to edit group chat name', {message: e})
|
||||
@@ -738,7 +734,7 @@ function SettingsHeader({
|
||||
},
|
||||
})
|
||||
|
||||
const {mutate: muteConvo} = useMuteConvo(convo.id, {
|
||||
const {mutate: muteConvo} = useMuteConvo(convo.view.id, {
|
||||
onSuccess: data => {
|
||||
if (data.convo.muted) {
|
||||
Toast.show(l({message: 'Group chat muted', context: 'toast'}))
|
||||
@@ -754,7 +750,7 @@ function SettingsHeader({
|
||||
},
|
||||
})
|
||||
|
||||
const {mutate: leaveConvo} = useLeaveConvo(convo.id, {
|
||||
const {mutate: leaveConvo} = useLeaveConvo(convo.view.id, {
|
||||
onMutate: () => {
|
||||
navigation.dispatch(StackActions.pop(2))
|
||||
},
|
||||
@@ -772,7 +768,7 @@ function SettingsHeader({
|
||||
const leaveChatPrompt = Prompt.usePromptControl()
|
||||
|
||||
const handleToggleMute = () => {
|
||||
muteConvo({mute: !convo?.muted})
|
||||
muteConvo({mute: !convo.view.muted})
|
||||
}
|
||||
|
||||
const handleLeaveChat = () => {
|
||||
@@ -815,7 +811,7 @@ function SettingsHeader({
|
||||
<View
|
||||
style={[a.px_xl, a.py_4xl, a.border_b, t.atoms.border_contrast_low]}>
|
||||
<View style={[a.align_center, a.justify_center]}>
|
||||
<AvatarBubbles profiles={profiles} />
|
||||
<AvatarBubbles profiles={convo.members} />
|
||||
</View>
|
||||
<Text
|
||||
style={[
|
||||
@@ -846,12 +842,14 @@ function SettingsHeader({
|
||||
a.pt_2xl,
|
||||
]}>
|
||||
<SettingsButton
|
||||
color={convo?.muted ? 'negative_subtle' : 'secondary'}
|
||||
icon={convo?.muted ? BellOffIcon : BellIcon}
|
||||
color={convo.view.muted ? 'negative_subtle' : 'secondary'}
|
||||
icon={convo.view.muted ? BellOffIcon : BellIcon}
|
||||
label={
|
||||
convo?.muted ? l`Unmute this group chat` : l`Mute this group chat`
|
||||
convo.view.muted
|
||||
? l`Unmute this group chat`
|
||||
: l`Mute this group chat`
|
||||
}
|
||||
text={convo?.muted ? l`Muted` : l`Mute`}
|
||||
text={convo.view.muted ? l`Muted` : l`Mute`}
|
||||
onPress={handleToggleMute}
|
||||
/>
|
||||
{isOwner ? (
|
||||
@@ -1086,7 +1084,7 @@ function InviteLinkPrompt({
|
||||
<Prompt.Basic
|
||||
control={control}
|
||||
title={l`Invite link`}
|
||||
description={l`An invite link lets people join this group chat without being added directly. You control who can use the link and whether they need your approval. You can disable the link at any time. Your name, avatar, and the name of the group chat will be visible to everyone`}
|
||||
description={l`An invite link lets people join this group chat without being added directly. You control who can use the link and whether they need your approval. You can disable the link at any time. Your name, avatar, and the name of the group chat will be visible to everyone.`}
|
||||
confirmButtonCta={l`Get started`}
|
||||
cancelButtonCta={l`Cancel`}
|
||||
onConfirm={onConfirm}
|
||||
|
||||
@@ -482,6 +482,7 @@ function BaseChatItem({
|
||||
]
|
||||
: undefined
|
||||
}
|
||||
onPressIn={() => precacheConvoQuery(queryClient, convo)}
|
||||
onPress={onPress}
|
||||
onLongPress={showMenu && IS_NATIVE ? onLongPress : undefined}
|
||||
onAccessibilityAction={showMenu ? onLongPress : undefined}>
|
||||
|
||||
@@ -50,7 +50,6 @@ import {
|
||||
import {useGetPost} from '#/state/queries/post'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {List, type ListMethods} from '#/view/com/util/List'
|
||||
import {ChatDisabled} from '#/screens/Messages/components/ChatDisabled'
|
||||
import {MessageComposer} from '#/screens/Messages/components/MessageComposer'
|
||||
import {MessageInput} from '#/screens/Messages/components/MessageInput'
|
||||
import {MessageListError} from '#/screens/Messages/components/MessageListError'
|
||||
@@ -93,14 +92,12 @@ function onScrollToIndexFailed() {
|
||||
export function MessagesList({
|
||||
hasScrolled,
|
||||
setHasScrolled,
|
||||
blocked,
|
||||
footer,
|
||||
hasAcceptOverride,
|
||||
transparentHeaderHeight,
|
||||
}: {
|
||||
hasScrolled: boolean
|
||||
setHasScrolled: React.Dispatch<React.SetStateAction<boolean>>
|
||||
blocked?: boolean
|
||||
footer?: React.ReactNode
|
||||
hasAcceptOverride?: boolean
|
||||
transparentHeaderHeight?: number
|
||||
@@ -489,11 +486,7 @@ export function MessagesList({
|
||||
}),
|
||||
opened: 0,
|
||||
}}>
|
||||
{convoState.status === ConvoStatus.Disabled ? (
|
||||
<ChatDisabled />
|
||||
) : blocked ? (
|
||||
footer
|
||||
) : (
|
||||
{footer ?? (
|
||||
<ConversationFooter
|
||||
convoState={convoState}
|
||||
hasAcceptOverride={hasAcceptOverride}>
|
||||
|
||||
@@ -19,19 +19,18 @@ import {
|
||||
const RQKEY_ROOT = 'convo'
|
||||
export const RQKEY = (convoId: string) => [RQKEY_ROOT, convoId]
|
||||
|
||||
export function useConvoQuery(convo: ChatBskyConvoDefs.ConvoView) {
|
||||
export function useConvoQuery({convoId}: {convoId: string}) {
|
||||
const agent = useAgent()
|
||||
|
||||
return useQuery({
|
||||
queryKey: RQKEY(convo.id),
|
||||
queryKey: RQKEY(convoId),
|
||||
queryFn: async () => {
|
||||
const {data} = await agent.chat.bsky.convo.getConvo(
|
||||
{convoId: convo.id},
|
||||
{convoId},
|
||||
{headers: DM_SERVICE_HEADERS},
|
||||
)
|
||||
return data.convo
|
||||
},
|
||||
initialData: convo,
|
||||
staleTime: STALE.INFINITY,
|
||||
})
|
||||
}
|
||||
@@ -58,7 +57,7 @@ export function useMarkAsReadMutation() {
|
||||
}) => {
|
||||
if (!convoId) throw new Error('No convoId provided')
|
||||
|
||||
await agent.api.chat.bsky.convo.updateRead(
|
||||
await agent.chat.bsky.convo.updateRead(
|
||||
{
|
||||
convoId,
|
||||
messageId,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {createContext, useContext, useState} from 'react'
|
||||
import {createContext, useCallback, useContext, useState} from 'react'
|
||||
|
||||
import {useHotkeysContext} from '#/lib/hotkeys'
|
||||
|
||||
@@ -14,14 +14,17 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
const [state, setState] = useState(false)
|
||||
const {disableScope, enableScope} = useHotkeysContext()
|
||||
|
||||
const setDrawerOpen = (open: boolean) => {
|
||||
if (open) {
|
||||
disableScope('global')
|
||||
} else {
|
||||
enableScope('global')
|
||||
}
|
||||
setState(open)
|
||||
}
|
||||
const setDrawerOpen = useCallback(
|
||||
(open: boolean) => {
|
||||
if (open) {
|
||||
disableScope('global')
|
||||
} else {
|
||||
enableScope('global')
|
||||
}
|
||||
setState(open)
|
||||
},
|
||||
[disableScope, enableScope],
|
||||
)
|
||||
|
||||
return (
|
||||
<stateContext.Provider value={state}>
|
||||
|
||||
@@ -2,7 +2,7 @@ import {useRef} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {Image} from 'expo-image'
|
||||
import {type ImagePickerAsset} from 'expo-image-picker'
|
||||
import {BlueskyVideoView} from '@haileyok/bluesky-video'
|
||||
import {BlueskyVideoView} from '@bsky.app/video'
|
||||
|
||||
import {type CompressedVideo} from '#/lib/media/video/types'
|
||||
import {clamp} from '#/lib/numbers'
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
useAnimatedScrollHandler,
|
||||
useSharedValue,
|
||||
} from 'react-native-reanimated'
|
||||
import {updateActiveVideoViewAsync} from '@haileyok/bluesky-video'
|
||||
import {updateActiveVideoViewAsync} from '@bsky.app/video'
|
||||
|
||||
import {useDedupe} from '#/lib/hooks/useDedupe'
|
||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||
|
||||
Reference in New Issue
Block a user