Merge branch 'main' into starter-pack-stuck

This commit is contained in:
vineyardbovines
2026-04-20 10:46:40 -04:00
30 changed files with 1991 additions and 628 deletions
+2 -2
View File
@@ -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'
+1 -1
View File
@@ -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})
+59 -116
View File
@@ -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>
}
/>
)
+1 -1
View File
@@ -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}
+2 -2
View File
@@ -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>
}
-1
View File
@@ -257,7 +257,6 @@ export function Gallery({
horizontal
pagingEnabled={false}
showsHorizontalScrollIndicator={false}
decelerationRate={0.993}
directionalLockEnabled
nestedScrollEnabled
alwaysBounceVertical={false}
+11 -4
View File
@@ -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,
}),
[],
)
}
File diff suppressed because it is too large Load Diff
+43 -46
View File
@@ -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
}
+53 -55
View File
@@ -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}>
+4 -5
View File
@@ -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,
+12 -9
View File
@@ -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'
+1 -1
View File
@@ -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'