Start integrating group clip clop settings (#10217)
This commit is contained in:
@@ -35,13 +35,11 @@ import {
|
|||||||
PersonCheck_Stroke2_Corner0_Rounded as PersonCheck,
|
PersonCheck_Stroke2_Corner0_Rounded as PersonCheck,
|
||||||
PersonX_Stroke2_Corner0_Rounded as PersonX,
|
PersonX_Stroke2_Corner0_Rounded as PersonX,
|
||||||
} from '#/components/icons/Person'
|
} from '#/components/icons/Person'
|
||||||
import {SettingsGear2_Stroke2_Corner0_Rounded as GearIcon} from '#/components/icons/SettingsGear2'
|
|
||||||
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
|
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
|
||||||
import * as Menu from '#/components/Menu'
|
import * as Menu from '#/components/Menu'
|
||||||
import {ReportDialog} from '#/components/moderation/ReportDialog'
|
import {ReportDialog} from '#/components/moderation/ReportDialog'
|
||||||
import * as Prompt from '#/components/Prompt'
|
import * as Prompt from '#/components/Prompt'
|
||||||
import * as Toast from '#/components/Toast'
|
import * as Toast from '#/components/Toast'
|
||||||
import {useAnalytics} from '#/analytics'
|
|
||||||
import type * as bsky from '#/types/bsky'
|
import type * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
let ConvoMenu = ({
|
let ConvoMenu = ({
|
||||||
@@ -183,7 +181,6 @@ function MenuContent({
|
|||||||
reportControl: Prompt.PromptControlProps
|
reportControl: Prompt.PromptControlProps
|
||||||
blockedByListControl: Prompt.PromptControlProps
|
blockedByListControl: Prompt.PromptControlProps
|
||||||
}) {
|
}) {
|
||||||
const ax = useAnalytics()
|
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {mutate: markAsRead} = useMarkAsReadMutation()
|
const {mutate: markAsRead} = useMarkAsReadMutation()
|
||||||
@@ -195,12 +192,6 @@ function MenuContent({
|
|||||||
const convoId = initialConvo.id
|
const convoId = initialConvo.id
|
||||||
const {data: convo} = useConvoQuery(initialConvo)
|
const {data: convo} = useConvoQuery(initialConvo)
|
||||||
|
|
||||||
const onNavigateToSettings = useCallback(() => {
|
|
||||||
navigation.navigate('MessagesConversationSettings', {
|
|
||||||
conversation: convoId,
|
|
||||||
})
|
|
||||||
}, [convoId, navigation])
|
|
||||||
|
|
||||||
const onNavigateToProfile = useCallback(() => {
|
const onNavigateToProfile = useCallback(() => {
|
||||||
navigation.navigate('Profile', {name: profile.did})
|
navigation.navigate('Profile', {name: profile.did})
|
||||||
}, [navigation, profile.did])
|
}, [navigation, profile.did])
|
||||||
@@ -229,14 +220,12 @@ function MenuContent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (userBlock) {
|
if (userBlock) {
|
||||||
queueUnblock()
|
void queueUnblock()
|
||||||
} else {
|
} else {
|
||||||
queueBlock()
|
void queueBlock()
|
||||||
}
|
}
|
||||||
}, [userBlock, listBlocks, blockedByListControl, queueBlock, queueUnblock])
|
}, [userBlock, listBlocks, blockedByListControl, queueBlock, queueUnblock])
|
||||||
|
|
||||||
const isGroupChatEnabled = ax.features.enabled(ax.features.GroupChatsEnable)
|
|
||||||
|
|
||||||
return isDeletedAccount ? (
|
return isDeletedAccount ? (
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
label={_(msg`Leave conversation`)}
|
label={_(msg`Leave conversation`)}
|
||||||
@@ -248,16 +237,6 @@ function MenuContent({
|
|||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{isGroupChatEnabled ? (
|
|
||||||
<Menu.Group>
|
|
||||||
<Menu.Item label={_(msg`Settings`)} onPress={onNavigateToSettings}>
|
|
||||||
<Menu.ItemText>
|
|
||||||
<Trans>Settings</Trans>
|
|
||||||
</Menu.ItemText>
|
|
||||||
<Menu.ItemIcon icon={GearIcon} />
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu.Group>
|
|
||||||
) : null}
|
|
||||||
<Menu.Group>
|
<Menu.Group>
|
||||||
{showMarkAsRead && (
|
{showMarkAsRead && (
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
|
|||||||
@@ -6,9 +6,12 @@ import {
|
|||||||
type ModerationDecision,
|
type ModerationDecision,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {useLingui} from '@lingui/react/macro'
|
import {useLingui} from '@lingui/react/macro'
|
||||||
|
import {useNavigation} from '@react-navigation/native'
|
||||||
|
|
||||||
import {makeProfileLink} from '#/lib/routes/links'
|
import {makeProfileLink} from '#/lib/routes/links'
|
||||||
|
import {type NavigationProp} from '#/lib/routes/types'
|
||||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||||
|
import {logger} from '#/logger'
|
||||||
import {type Shadow} from '#/state/cache/profile-shadow'
|
import {type Shadow} from '#/state/cache/profile-shadow'
|
||||||
import {isConvoActive, useConvo} from '#/state/messages/convo'
|
import {isConvoActive, useConvo} from '#/state/messages/convo'
|
||||||
import {type ConvoItem} from '#/state/messages/convo/types'
|
import {type ConvoItem} from '#/state/messages/convo/types'
|
||||||
@@ -16,8 +19,10 @@ import {useSession} from '#/state/session'
|
|||||||
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {AvatarBubbles} from '#/components/AvatarBubbles'
|
import {AvatarBubbles} from '#/components/AvatarBubbles'
|
||||||
|
import {Button, ButtonIcon} from '#/components/Button'
|
||||||
import {ConvoMenu} from '#/components/dms/ConvoMenu'
|
import {ConvoMenu} from '#/components/dms/ConvoMenu'
|
||||||
import {Bell2Off_Filled_Corner0_Rounded as BellOffIcon} from '#/components/icons/Bell2'
|
import {Bell2Off_Filled_Corner0_Rounded as BellOffIcon} from '#/components/icons/Bell2'
|
||||||
|
import {DotGrid3x1_Stroke2_Corner0_Rounded as DotsHorizontalIcon} from '#/components/icons/DotGrid'
|
||||||
import * as Layout from '#/components/Layout'
|
import * as Layout from '#/components/Layout'
|
||||||
import {Link} from '#/components/Link'
|
import {Link} from '#/components/Link'
|
||||||
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
||||||
@@ -117,6 +122,8 @@ function HeaderReady({
|
|||||||
const convoState = useConvo()
|
const convoState = useConvo()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
|
|
||||||
|
const navigation = useNavigation<NavigationProp>()
|
||||||
|
|
||||||
const groupInfo = convoState.getGroupInfo?.()
|
const groupInfo = convoState.getGroupInfo?.()
|
||||||
const isGroupChat = groupInfo != null
|
const isGroupChat = groupInfo != null
|
||||||
|
|
||||||
@@ -141,6 +148,17 @@ function HeaderReady({
|
|||||||
? latestMessageFromOther.message
|
? latestMessageFromOther.message
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
|
const handleNavigateToSettings = () => {
|
||||||
|
const convoId = convoState.convo?.id
|
||||||
|
if (convoId) {
|
||||||
|
navigation.navigate('MessagesConversationSettings', {
|
||||||
|
conversation: convoId,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
logger.error(`handleNavigateToSettings: missing convo ID`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.flex_1]}>
|
<View style={[a.flex_1]}>
|
||||||
<View style={[a.w_full, a.flex_row, a.align_center, a.justify_between]}>
|
<View style={[a.w_full, a.flex_row, a.align_center, a.justify_between]}>
|
||||||
@@ -199,15 +217,28 @@ function HeaderReady({
|
|||||||
|
|
||||||
<View style={[{minHeight: PFP_SIZE}, a.justify_center]}>
|
<View style={[{minHeight: PFP_SIZE}, a.justify_center]}>
|
||||||
<Layout.Header.Slot>
|
<Layout.Header.Slot>
|
||||||
{isConvoActive(convoState) && (
|
{isConvoActive(convoState) ? (
|
||||||
<ConvoMenu
|
isGroupChat ? (
|
||||||
convo={convoState.convo}
|
<Button
|
||||||
profile={profile}
|
label={l`Open group chat settings`}
|
||||||
currentScreen="conversation"
|
size="small"
|
||||||
blockInfo={blockInfo}
|
color="secondary"
|
||||||
latestReportableMessage={latestReportableMessage}
|
shape="round"
|
||||||
/>
|
variant="ghost"
|
||||||
)}
|
style={[a.bg_transparent]}
|
||||||
|
onPress={handleNavigateToSettings}>
|
||||||
|
<ButtonIcon icon={DotsHorizontalIcon} size="md" />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<ConvoMenu
|
||||||
|
convo={convoState.convo}
|
||||||
|
profile={profile}
|
||||||
|
currentScreen="conversation"
|
||||||
|
blockInfo={blockInfo}
|
||||||
|
latestReportableMessage={latestReportableMessage}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
) : null}
|
||||||
</Layout.Header.Slot>
|
</Layout.Header.Slot>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {useMemo, useState} from 'react'
|
import {useMemo, useState} from 'react'
|
||||||
import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native'
|
import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native'
|
||||||
import {moderateProfile} from '@atproto/api'
|
import {type ChatBskyConvoDefs, moderateProfile} from '@atproto/api'
|
||||||
import {plural} from '@lingui/core/macro'
|
import {plural} from '@lingui/core/macro'
|
||||||
import {Trans, useLingui} from '@lingui/react/macro'
|
import {Trans, useLingui} from '@lingui/react/macro'
|
||||||
import {useNavigation} from '@react-navigation/native'
|
import {useNavigation} from '@react-navigation/native'
|
||||||
@@ -8,18 +8,30 @@ import {useNavigation} from '@react-navigation/native'
|
|||||||
import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset'
|
import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset'
|
||||||
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
||||||
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
|
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
|
||||||
import {type NavigationProp} from '#/lib/routes/types'
|
import {
|
||||||
|
type CommonNavigatorParams,
|
||||||
|
type NativeStackScreenProps,
|
||||||
|
type NavigationProp,
|
||||||
|
} from '#/lib/routes/types'
|
||||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
|
import {logger} from '#/logger'
|
||||||
|
import {type Shadow} from '#/state/cache/types'
|
||||||
|
import {ConvoProvider, useConvo} from '#/state/messages/convo'
|
||||||
|
import {ConvoStatus} from '#/state/messages/convo/types'
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
import {useGetConvoAvailabilityQuery} from '#/state/queries/messages/get-convo-availability'
|
import {useGetConvoAvailabilityQuery} from '#/state/queries/messages/get-convo-availability'
|
||||||
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
|
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
|
||||||
|
import {useMuteConvo} from '#/state/queries/messages/mute-conversation'
|
||||||
|
import {useProfileBlockMutationQueue} from '#/state/queries/profile'
|
||||||
|
import {useSession} from '#/state/session'
|
||||||
import {List} from '#/view/com/util/List'
|
import {List} from '#/view/com/util/List'
|
||||||
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
||||||
import {AvatarBubbles} from '#/components/AvatarBubbles'
|
import {AvatarBubbles} from '#/components/AvatarBubbles'
|
||||||
import {Button, type ButtonColor, ButtonIcon} from '#/components/Button'
|
import {Button, type ButtonColor, ButtonIcon} from '#/components/Button'
|
||||||
import type * as Dialog from '#/components/Dialog'
|
import type * as Dialog from '#/components/Dialog'
|
||||||
|
import {Error} from '#/components/Error'
|
||||||
import * as TextField from '#/components/forms/TextField'
|
import * as TextField from '#/components/forms/TextField'
|
||||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||||
import {ArrowBoxLeft_Stroke2_Corner0_Rounded as ArrowBoxLeftIcon} from '#/components/icons/ArrowBoxLeft'
|
import {ArrowBoxLeft_Stroke2_Corner0_Rounded as ArrowBoxLeftIcon} from '#/components/icons/ArrowBoxLeft'
|
||||||
@@ -32,6 +44,7 @@ import {ChevronRight_Stroke2_Corner0_Rounded as ChevronIcon} from '#/components/
|
|||||||
import {type Props as SVGIconProps} from '#/components/icons/common'
|
import {type Props as SVGIconProps} from '#/components/icons/common'
|
||||||
import {DotGrid3x1_Stroke2_Corner0_Rounded as EllipsisIcon} from '#/components/icons/DotGrid'
|
import {DotGrid3x1_Stroke2_Corner0_Rounded as EllipsisIcon} from '#/components/icons/DotGrid'
|
||||||
import {EditBig_Stroke2_Corner0_Rounded as EditIcon} from '#/components/icons/EditBig'
|
import {EditBig_Stroke2_Corner0_Rounded as EditIcon} from '#/components/icons/EditBig'
|
||||||
|
import {Flag_Stroke2_Corner0_Rounded as FlagIcon} from '#/components/icons/Flag'
|
||||||
import {Lock_Stroke2_Corner0_Rounded as LockIcon} from '#/components/icons/Lock'
|
import {Lock_Stroke2_Corner0_Rounded as LockIcon} from '#/components/icons/Lock'
|
||||||
import {Message_Stroke2_Corner0_Rounded as MessageIcon} from '#/components/icons/Message'
|
import {Message_Stroke2_Corner0_Rounded as MessageIcon} from '#/components/icons/Message'
|
||||||
import {
|
import {
|
||||||
@@ -63,16 +76,23 @@ type Item =
|
|||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'CHAT_MEMBER'
|
type: 'CHAT_MEMBER'
|
||||||
profile: bsky.profile.AnyProfileView
|
profile: Shadow<bsky.profile.AnyProfileView>
|
||||||
status: 'admin' | 'member' | 'invited'
|
status: 'owner' | 'member' | 'invited'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Props = NativeStackScreenProps<
|
||||||
|
CommonNavigatorParams,
|
||||||
|
'MessagesConversationSettings'
|
||||||
|
>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO This is just layout for now.
|
* TODO This is just layout for now.
|
||||||
*/
|
*/
|
||||||
export function MessagesConversationSettingsScreen() {
|
export function MessagesConversationSettingsScreen({route}: Props) {
|
||||||
const {gtTablet} = useBreakpoints()
|
const {gtTablet} = useBreakpoints()
|
||||||
|
|
||||||
|
const convoId = route.params.conversation
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout.Screen>
|
<Layout.Screen>
|
||||||
<Layout.Header.Outer>
|
<Layout.Header.Outer>
|
||||||
@@ -84,7 +104,9 @@ export function MessagesConversationSettingsScreen() {
|
|||||||
</Layout.Header.Content>
|
</Layout.Header.Content>
|
||||||
<Layout.Header.Slot />
|
<Layout.Header.Slot />
|
||||||
</Layout.Header.Outer>
|
</Layout.Header.Outer>
|
||||||
<SettingsInner />
|
<ConvoProvider key={convoId} convoId={convoId}>
|
||||||
|
<SettingsInner />
|
||||||
|
</ConvoProvider>
|
||||||
</Layout.Screen>
|
</Layout.Screen>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -94,10 +116,16 @@ function keyExtractor(item: Item) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function SettingsInner() {
|
function SettingsInner() {
|
||||||
|
const {t: l} = useLingui()
|
||||||
|
|
||||||
const initialNumToRender = useInitialNumToRender({minItemHeight: 68})
|
const initialNumToRender = useInitialNumToRender({minItemHeight: 68})
|
||||||
const bottomBarOffset = useBottomBarOffset()
|
const bottomBarOffset = useBottomBarOffset()
|
||||||
|
|
||||||
const data: bsky.profile.AnyProfileView[] = []
|
const convoState = useConvo()
|
||||||
|
const {currentAccount} = useSession()
|
||||||
|
const primaryMember = convoState?.getPrimaryMember?.()
|
||||||
|
|
||||||
|
const data: bsky.profile.AnyProfileView[] = convoState.convo?.members ?? []
|
||||||
const invites: string[] = []
|
const invites: string[] = []
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
@@ -107,16 +135,26 @@ function SettingsInner() {
|
|||||||
{
|
{
|
||||||
type: 'ADD_MEMBERS_LINK',
|
type: 'ADD_MEMBERS_LINK',
|
||||||
},
|
},
|
||||||
...data.map((profile, index) => ({
|
...[...data]
|
||||||
type: 'CHAT_MEMBER',
|
.sort((a, b) => {
|
||||||
profile,
|
const aIsAdmin = a.did === primaryMember?.did
|
||||||
status:
|
const bIsAdmin = b.did === primaryMember?.did
|
||||||
index === 0
|
const aIsSelf = a.did === currentAccount?.did
|
||||||
? 'admin'
|
const bIsSelf = b.did === currentAccount?.did
|
||||||
: invites.includes(profile.did)
|
if (aIsAdmin !== bIsAdmin) return aIsAdmin ? -1 : 1
|
||||||
? 'invited'
|
if (aIsSelf !== bIsSelf) return aIsSelf ? -1 : 1
|
||||||
: 'member',
|
return 0
|
||||||
})),
|
})
|
||||||
|
.map(profile => ({
|
||||||
|
type: 'CHAT_MEMBER',
|
||||||
|
profile,
|
||||||
|
status:
|
||||||
|
primaryMember?.did === profile.did
|
||||||
|
? 'owner'
|
||||||
|
: invites.includes(profile.did)
|
||||||
|
? 'invited'
|
||||||
|
: 'member',
|
||||||
|
})),
|
||||||
]
|
]
|
||||||
|
|
||||||
function renderItem({item}: {item: Item}) {
|
function renderItem({item}: {item: Item}) {
|
||||||
@@ -132,6 +170,19 @@ function SettingsInner() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (convoState.status === ConvoStatus.Error) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Error
|
||||||
|
title={l`Something went wrong`}
|
||||||
|
message={l`We couldn’t load this conversation’s settings`}
|
||||||
|
onRetry={() => convoState.error.retry()}
|
||||||
|
sideBorders={false}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List
|
<List
|
||||||
data={items}
|
data={items}
|
||||||
@@ -141,7 +192,13 @@ function SettingsInner() {
|
|||||||
desktopFixedHeight
|
desktopFixedHeight
|
||||||
initialNumToRender={initialNumToRender}
|
initialNumToRender={initialNumToRender}
|
||||||
keyExtractor={keyExtractor}
|
keyExtractor={keyExtractor}
|
||||||
ListHeaderComponent={<SettingsHeader profiles={data} />}
|
ListHeaderComponent={
|
||||||
|
convoState.convo ? (
|
||||||
|
<SettingsHeader convo={convoState.convo} profiles={data} />
|
||||||
|
) : (
|
||||||
|
<SettingsHeaderPlaceholder />
|
||||||
|
)
|
||||||
|
}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
sideBorders={false}
|
sideBorders={false}
|
||||||
windowSize={11}
|
windowSize={11}
|
||||||
@@ -173,15 +230,17 @@ function MembersAndRequests({
|
|||||||
{color: t.palette.contrast_500},
|
{color: t.palette.contrast_500},
|
||||||
]}>{l`${memberCount}/${MEMBER_LIMIT}`}</Text>
|
]}>{l`${memberCount}/${MEMBER_LIMIT}`}</Text>
|
||||||
</View>
|
</View>
|
||||||
<InlineLinkText
|
{requestCount > 0 ? (
|
||||||
label={l`View incoming group chat requests`}
|
<InlineLinkText
|
||||||
style={[a.text_sm, a.text_right, a.font_semi_bold]}
|
label={l`View incoming group chat requests`}
|
||||||
to="#">
|
style={[a.text_sm, a.text_right, a.font_semi_bold]}
|
||||||
{l`${plural(requestCount, {
|
to="#">
|
||||||
one: '# request',
|
{l`${plural(requestCount, {
|
||||||
other: '# requests',
|
one: '# request',
|
||||||
})}`}
|
other: '# requests',
|
||||||
</InlineLinkText>
|
})}`}
|
||||||
|
</InlineLinkText>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -252,13 +311,14 @@ function Member({
|
|||||||
profile,
|
profile,
|
||||||
status,
|
status,
|
||||||
}: {
|
}: {
|
||||||
profile: bsky.profile.AnyProfileView
|
profile: Shadow<bsky.profile.AnyProfileView>
|
||||||
status: 'admin' | 'member' | 'invited'
|
status: 'owner' | 'member' | 'invited'
|
||||||
}) {
|
}) {
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
|
|
||||||
|
const {currentAccount} = useSession()
|
||||||
const moderationOpts = useModerationOpts()
|
const moderationOpts = useModerationOpts()
|
||||||
const moderation = useMemo(
|
const moderation = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -268,8 +328,6 @@ function Member({
|
|||||||
|
|
||||||
if (!moderation) return null
|
if (!moderation) return null
|
||||||
|
|
||||||
const invitedByDisplayName = 'Darrin Loeliger'
|
|
||||||
|
|
||||||
const isDeletedAccount = profile.handle === 'missing.invalid'
|
const isDeletedAccount = profile.handle === 'missing.invalid'
|
||||||
const displayName = isDeletedAccount
|
const displayName = isDeletedAccount
|
||||||
? l`Deleted Account`
|
? l`Deleted Account`
|
||||||
@@ -278,27 +336,15 @@ function Member({
|
|||||||
moderation.ui('displayName'),
|
moderation.ui('displayName'),
|
||||||
)
|
)
|
||||||
|
|
||||||
let invitedBy: React.ReactNode | null = (
|
let statusBadge: React.ReactNode | null = null
|
||||||
<Text style={[a.text_xs, {color: t.palette.contrast_500}]}>
|
if (currentAccount?.did === profile.did) {
|
||||||
{l`Added by ${invitedByDisplayName}`}
|
switch (status) {
|
||||||
</Text>
|
case 'owner':
|
||||||
)
|
statusBadge = <StatusBadge label={l`Admin`} />
|
||||||
let statusBadge: React.ReactNode | null = (
|
break
|
||||||
<MemberMenu profile={profile} type="member" />
|
}
|
||||||
)
|
} else {
|
||||||
switch (status) {
|
statusBadge = <MemberMenu profile={profile} type={status} />
|
||||||
case 'admin':
|
|
||||||
invitedBy = null
|
|
||||||
statusBadge = <StatusBadge label={l`Admin`} />
|
|
||||||
break
|
|
||||||
case 'invited':
|
|
||||||
invitedBy = (
|
|
||||||
<Text style={[a.text_xs, {color: t.palette.contrast_500}]}>
|
|
||||||
{l`Invited by ${invitedByDisplayName}`}
|
|
||||||
</Text>
|
|
||||||
)
|
|
||||||
statusBadge = <MemberMenu profile={profile} type="invited" />
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -334,16 +380,6 @@ function Member({
|
|||||||
]}>
|
]}>
|
||||||
{sanitizeHandle(profile.handle, '@')}
|
{sanitizeHandle(profile.handle, '@')}
|
||||||
</Text>
|
</Text>
|
||||||
{invitedBy ? (
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.text_xs,
|
|
||||||
{color: t.palette.contrast_500},
|
|
||||||
web(a.pt_2xs),
|
|
||||||
]}>
|
|
||||||
{invitedBy}
|
|
||||||
</Text>
|
|
||||||
) : null}
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View>{statusBadge}</View>
|
<View>{statusBadge}</View>
|
||||||
@@ -353,7 +389,13 @@ function Member({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatusBadge({label}: {label: string}) {
|
function StatusBadge({
|
||||||
|
label,
|
||||||
|
style,
|
||||||
|
}: {
|
||||||
|
label: string
|
||||||
|
style?: StyleProp<ViewStyle>
|
||||||
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -367,6 +409,7 @@ function StatusBadge({label}: {label: string}) {
|
|||||||
paddingLeft: 6,
|
paddingLeft: 6,
|
||||||
paddingRight: 6,
|
paddingRight: 6,
|
||||||
},
|
},
|
||||||
|
style,
|
||||||
]}>
|
]}>
|
||||||
<Text style={[a.text_sm, a.font_semi_bold, t.atoms.text_contrast_medium]}>
|
<Text style={[a.text_sm, a.font_semi_bold, t.atoms.text_contrast_medium]}>
|
||||||
{label}
|
{label}
|
||||||
@@ -410,14 +453,24 @@ function MemberMenu({
|
|||||||
profile,
|
profile,
|
||||||
type,
|
type,
|
||||||
}: {
|
}: {
|
||||||
profile: bsky.profile.AnyProfileView
|
profile: Shadow<bsky.profile.AnyProfileView>
|
||||||
type: 'member' | 'invited'
|
type: 'owner' | 'member' | 'invited'
|
||||||
}) {
|
}) {
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const ax = useAnalytics()
|
const ax = useAnalytics()
|
||||||
|
|
||||||
const requireEmailVerification = useRequireEmailVerification()
|
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 {data: convoAvailability} = useGetConvoAvailabilityQuery(profile.did)
|
||||||
const {mutate: initiateConvo} = useGetConvoForMembers({
|
const {mutate: initiateConvo} = useGetConvoForMembers({
|
||||||
@@ -429,8 +482,9 @@ function MemberMenu({
|
|||||||
Toast.show(l`Failed to create conversation`)
|
Toast.show(l`Failed to create conversation`)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
|
||||||
|
|
||||||
const onPress = () => {
|
const messageMember = () => {
|
||||||
if (!convoAvailability?.canChat) {
|
if (!convoAvailability?.canChat) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -446,7 +500,7 @@ function MemberMenu({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const wrappedOnPress = requireEmailVerification(onPress, {
|
const handleMessageMember = requireEmailVerification(messageMember, {
|
||||||
instructions: [
|
instructions: [
|
||||||
<Trans key="message">
|
<Trans key="message">
|
||||||
Before you can message another user, you must first verify your email.
|
Before you can message another user, you must first verify your email.
|
||||||
@@ -454,6 +508,36 @@ function MemberMenu({
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const handleBlockMember = async () => {
|
||||||
|
if (profile.viewer?.blocking) {
|
||||||
|
try {
|
||||||
|
await queueUnblock()
|
||||||
|
Toast.show(l({message: 'Account unblocked', context: 'toast'}))
|
||||||
|
} catch (err) {
|
||||||
|
const e = err as Error
|
||||||
|
if (e?.name !== 'AbortError') {
|
||||||
|
ax.logger.error('Failed to unblock account', {message: e})
|
||||||
|
Toast.show(l`There was an issue! ${e.toString()}`, {
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await queueBlock()
|
||||||
|
Toast.show(l({message: 'Account blocked', context: 'toast'}))
|
||||||
|
} catch (err) {
|
||||||
|
const e = err as Error
|
||||||
|
if (e?.name !== 'AbortError') {
|
||||||
|
ax.logger.error('Failed to block account', {message: e})
|
||||||
|
Toast.show(l`There was an issue! ${e.toString()}`, {
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const moderationOpts = useModerationOpts()
|
const moderationOpts = useModerationOpts()
|
||||||
const moderation = useMemo(
|
const moderation = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -472,67 +556,80 @@ function MemberMenu({
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu.Root>
|
<>
|
||||||
<Menu.Trigger label={l`Open chat member options for ${displayName}`}>
|
<Menu.Root>
|
||||||
{({props, state}) =>
|
<Menu.Trigger label={l`Open chat member options for ${displayName}`}>
|
||||||
type === 'invited' ? (
|
{({props, state, control: menuControl}) =>
|
||||||
<StatusButton
|
type === 'owner' || type === 'invited' ? (
|
||||||
{...props}
|
<StatusButton
|
||||||
label={l`Invited`}
|
{...props}
|
||||||
style={[
|
label={type === 'owner' ? l`Admin` : l`Invited`}
|
||||||
state.hovered
|
style={[
|
||||||
? {
|
state.hovered || state.pressed || menuControl.isOpen
|
||||||
backgroundColor: t.palette.contrast_0,
|
? {
|
||||||
}
|
backgroundColor: t.palette.contrast_0,
|
||||||
: null,
|
}
|
||||||
]}
|
: null,
|
||||||
/>
|
]}
|
||||||
) : (
|
/>
|
||||||
<Pressable
|
) : (
|
||||||
{...props}
|
<Pressable
|
||||||
style={[
|
{...props}
|
||||||
a.rounded_full,
|
style={[
|
||||||
a.p_sm,
|
a.rounded_full,
|
||||||
state.hovered
|
a.p_sm,
|
||||||
? {
|
state.hovered || state.pressed || menuControl.isOpen
|
||||||
backgroundColor: t.palette.contrast_0,
|
? {
|
||||||
}
|
backgroundColor: t.palette.contrast_0,
|
||||||
: null,
|
}
|
||||||
]}>
|
: null,
|
||||||
<EllipsisIcon style={[t.atoms.text_contrast_medium]} size="md" />
|
]}>
|
||||||
</Pressable>
|
<EllipsisIcon
|
||||||
)
|
style={[t.atoms.text_contrast_medium]}
|
||||||
}
|
size="md"
|
||||||
</Menu.Trigger>
|
/>
|
||||||
<Menu.Outer>
|
</Pressable>
|
||||||
<Menu.Group>
|
)
|
||||||
<Menu.Item
|
}
|
||||||
label={l`View ${displayName}’s profile`}
|
</Menu.Trigger>
|
||||||
onPress={() => {
|
<Menu.Outer>
|
||||||
navigation.navigate('Profile', {name: profile.did})
|
<Menu.Group>
|
||||||
}}>
|
<Menu.Item
|
||||||
<Menu.ItemText>
|
label={l`View ${displayName}’s profile`}
|
||||||
<Trans>Go to profile</Trans>
|
onPress={() => {
|
||||||
</Menu.ItemText>
|
navigation.navigate('Profile', {name: profile.did})
|
||||||
<Menu.ItemIcon icon={PersonIcon} />
|
}}>
|
||||||
</Menu.Item>
|
<Menu.ItemText>
|
||||||
<Menu.Item label={l`Message ${displayName}`} onPress={wrappedOnPress}>
|
<Trans>Go to profile</Trans>
|
||||||
<Menu.ItemText>
|
</Menu.ItemText>
|
||||||
<Trans>Message</Trans>
|
<Menu.ItemIcon icon={PersonIcon} />
|
||||||
</Menu.ItemText>
|
</Menu.Item>
|
||||||
<Menu.ItemIcon icon={MessageIcon} />
|
<Menu.Item
|
||||||
</Menu.Item>
|
label={l`Message ${displayName}`}
|
||||||
</Menu.Group>
|
onPress={handleMessageMember}>
|
||||||
<Menu.Divider />
|
<Menu.ItemText>
|
||||||
<Menu.Group>
|
<Trans>Message</Trans>
|
||||||
{type === 'member' ? (
|
</Menu.ItemText>
|
||||||
<>
|
<Menu.ItemIcon icon={MessageIcon} />
|
||||||
<Menu.Item label={l`Block ${displayName}`} onPress={() => {}}>
|
</Menu.Item>
|
||||||
|
</Menu.Group>
|
||||||
|
<Menu.Divider />
|
||||||
|
<Menu.Group>
|
||||||
|
{type === 'owner' || type === 'member' ? (
|
||||||
|
<Menu.Item
|
||||||
|
label={
|
||||||
|
profile.viewer?.blocking
|
||||||
|
? l`Unblock ${displayName}`
|
||||||
|
: l`Block ${displayName}`
|
||||||
|
}
|
||||||
|
onPress={() => blockMemberPrompt.open()}>
|
||||||
<Menu.ItemText>
|
<Menu.ItemText>
|
||||||
<Trans>Block</Trans>
|
<Trans>Block</Trans>
|
||||||
</Menu.ItemText>
|
</Menu.ItemText>
|
||||||
<Menu.ItemIcon icon={PersonXIcon} />
|
<Menu.ItemIcon icon={PersonXIcon} />
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
) : null}
|
||||||
|
{isOwner ? (
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
label={l`Remove ${displayName} from this group chat`}
|
label={l`Remove ${displayName} from this group chat`}
|
||||||
onPress={() => {}}>
|
onPress={() => {}}>
|
||||||
@@ -541,39 +638,80 @@ function MemberMenu({
|
|||||||
</Menu.ItemText>
|
</Menu.ItemText>
|
||||||
<Menu.ItemIcon icon={ArrowBoxLeftIcon} />
|
<Menu.ItemIcon icon={ArrowBoxLeftIcon} />
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</>
|
) : null}
|
||||||
) : null}
|
{isOwner && type === 'invited' ? (
|
||||||
{type === 'invited' ? (
|
<Menu.Item
|
||||||
<Menu.Item
|
label={l`Uninvite ${displayName} from this group chat`}
|
||||||
label={l`Uninvite ${displayName} from this group chat`}
|
onPress={() => {}}>
|
||||||
onPress={() => {}}>
|
<Menu.ItemText>
|
||||||
<Menu.ItemText>
|
<Trans>Uninvite</Trans>
|
||||||
<Trans>Uninvite</Trans>
|
</Menu.ItemText>
|
||||||
</Menu.ItemText>
|
<Menu.ItemIcon icon={ArrowBoxLeftIcon} />
|
||||||
<Menu.ItemIcon icon={ArrowBoxLeftIcon} />
|
</Menu.Item>
|
||||||
</Menu.Item>
|
) : null}
|
||||||
) : null}
|
</Menu.Group>
|
||||||
</Menu.Group>
|
</Menu.Outer>
|
||||||
</Menu.Outer>
|
</Menu.Root>
|
||||||
</Menu.Root>
|
<BlockMemberPrompt
|
||||||
|
control={blockMemberPrompt}
|
||||||
|
onConfirm={() => void handleBlockMember()}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function SettingsHeader({profiles}: {profiles: bsky.profile.AnyProfileView[]}) {
|
function SettingsHeader({
|
||||||
|
convo,
|
||||||
|
profiles,
|
||||||
|
}: {
|
||||||
|
convo: ChatBskyConvoDefs.ConvoView
|
||||||
|
profiles: bsky.profile.AnyProfileView[]
|
||||||
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
|
|
||||||
|
const convoState = useConvo()
|
||||||
|
const {currentAccount} = useSession()
|
||||||
|
|
||||||
|
const isOwner =
|
||||||
|
currentAccount?.did == null
|
||||||
|
? false
|
||||||
|
: convoState.getPrimaryMember?.()?.did === currentAccount.did
|
||||||
|
|
||||||
|
const {mutate: muteConvo} = useMuteConvo(convo.id, {
|
||||||
|
onSuccess: data => {
|
||||||
|
if (data.convo.muted) {
|
||||||
|
Toast.show(l({message: 'Group chat muted', context: 'toast'}))
|
||||||
|
} else {
|
||||||
|
Toast.show(l({message: 'Group chat unmuted', context: 'toast'}))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
Toast.show(l`Could not mute group chat`, {
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const editNamePrompt = Prompt.usePromptControl()
|
const editNamePrompt = Prompt.usePromptControl()
|
||||||
const inviteLinkPrompt = Prompt.usePromptControl()
|
const inviteLinkPrompt = Prompt.usePromptControl()
|
||||||
const lockChatPrompt = Prompt.usePromptControl()
|
const lockChatPrompt = Prompt.usePromptControl()
|
||||||
|
|
||||||
const [groupName, setGroupName] = useState('Work in Progress')
|
const [groupName, setGroupName] = useState(
|
||||||
|
convoState.getGroupInfo?.()?.name ?? '',
|
||||||
|
)
|
||||||
const [newGroupName, setNewGroupName] = useState(groupName)
|
const [newGroupName, setNewGroupName] = useState(groupName)
|
||||||
|
|
||||||
const [isMuted, setIsMuted] = useState(false)
|
|
||||||
const [isLocked, setIsLocked] = useState(false)
|
const [isLocked, setIsLocked] = useState(false)
|
||||||
|
|
||||||
const handleToggleMute = () => {
|
const handleToggleMute = () => {
|
||||||
setIsMuted(prev => !prev)
|
try {
|
||||||
|
muteConvo({mute: !convo?.muted})
|
||||||
|
} catch (err) {
|
||||||
|
const e = err as Error
|
||||||
|
logger.error('Failed to mute group chat', {message: e})
|
||||||
|
Toast.show(l`There was an issue! ${e.toString()}`, {type: 'error'})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePromptName = () => {
|
const handlePromptName = () => {
|
||||||
@@ -641,35 +779,57 @@ function SettingsHeader({profiles}: {profiles: bsky.profile.AnyProfileView[]}) {
|
|||||||
a.pt_2xl,
|
a.pt_2xl,
|
||||||
]}>
|
]}>
|
||||||
<SettingsButton
|
<SettingsButton
|
||||||
color={isMuted ? 'negative_subtle' : 'secondary'}
|
color={convo?.muted ? 'negative_subtle' : 'secondary'}
|
||||||
icon={isMuted ? BellOffIcon : BellIcon}
|
icon={convo?.muted ? BellOffIcon : BellIcon}
|
||||||
label={
|
label={
|
||||||
isMuted ? l`Unmute this group chat` : l`Mute this group chat`
|
convo?.muted ? l`Unmute this group chat` : l`Mute this group chat`
|
||||||
}
|
}
|
||||||
text={isMuted ? l`Muted` : l`Mute`}
|
text={convo?.muted ? l`Muted` : l`Mute`}
|
||||||
onPress={handleToggleMute}
|
onPress={handleToggleMute}
|
||||||
/>
|
/>
|
||||||
<SettingsButton
|
{isOwner ? (
|
||||||
icon={EditIcon}
|
<SettingsButton
|
||||||
label={l`Edit this group chat’s name`}
|
icon={EditIcon}
|
||||||
text={l`Edit name`}
|
label={l`Edit this group chat’s name`}
|
||||||
onPress={handlePromptName}
|
text={l`Edit name`}
|
||||||
/>
|
onPress={handlePromptName}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
<SettingsButton
|
<SettingsButton
|
||||||
icon={ChainLinkIcon}
|
icon={ChainLinkIcon}
|
||||||
label={l`Create an invite link for this group chat`}
|
label={l`Create an invite link for this group chat`}
|
||||||
text={l`Invite link`}
|
text={l`Invite link`}
|
||||||
onPress={handlePromptInviteLink}
|
onPress={handlePromptInviteLink}
|
||||||
/>
|
/>
|
||||||
<SettingsButton
|
{isOwner ? (
|
||||||
color={isLocked ? 'negative_subtle' : 'secondary'}
|
<SettingsButton
|
||||||
icon={LockIcon}
|
color={isLocked ? 'negative_subtle' : 'secondary'}
|
||||||
label={
|
icon={LockIcon}
|
||||||
isLocked ? l`Unlock this group chat` : l`Lock this group chat`
|
label={
|
||||||
}
|
isLocked ? l`Unlock this group chat` : l`Lock this group chat`
|
||||||
text={isLocked ? l`Locked` : l`Lock`}
|
}
|
||||||
onPress={isLocked ? handleUnlock : handlePromptLock}
|
text={isLocked ? l`Locked` : l`Lock`}
|
||||||
/>
|
onPress={isLocked ? handleUnlock : handlePromptLock}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{isOwner ? null : (
|
||||||
|
<SettingsButton
|
||||||
|
color="secondary"
|
||||||
|
icon={FlagIcon}
|
||||||
|
label={l`Report this group chat`}
|
||||||
|
text={l`Report`}
|
||||||
|
onPress={() => {}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isOwner ? null : (
|
||||||
|
<SettingsButton
|
||||||
|
color="secondary"
|
||||||
|
icon={ArrowBoxLeftIcon}
|
||||||
|
label={l`Leave this group chat`}
|
||||||
|
text={l`Leave`}
|
||||||
|
onPress={() => {}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<EditNamePrompt
|
<EditNamePrompt
|
||||||
@@ -687,6 +847,46 @@ function SettingsHeader({profiles}: {profiles: bsky.profile.AnyProfileView[]}) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SettingsHeaderPlaceholder() {
|
||||||
|
const t = useTheme()
|
||||||
|
const {t: l} = useLingui()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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={[]} />
|
||||||
|
</View>
|
||||||
|
<Text
|
||||||
|
style={[a.text_2xl, a.font_bold, a.text_center, a.pt_lg, t.atoms.text]}>
|
||||||
|
{l`…`}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.text_sm,
|
||||||
|
a.text_center,
|
||||||
|
a.pt_xs,
|
||||||
|
a.px_xl,
|
||||||
|
t.atoms.text_contrast_high,
|
||||||
|
]}>
|
||||||
|
<Trans>…</Trans>
|
||||||
|
</Text>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.justify_center,
|
||||||
|
a.gap_2xl,
|
||||||
|
a.pt_2xl,
|
||||||
|
]}>
|
||||||
|
<SettingsButtonPlaceholder />
|
||||||
|
<SettingsButtonPlaceholder />
|
||||||
|
<SettingsButtonPlaceholder />
|
||||||
|
<SettingsButtonPlaceholder />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function SettingsButton({
|
function SettingsButton({
|
||||||
color = 'secondary',
|
color = 'secondary',
|
||||||
icon,
|
icon,
|
||||||
@@ -713,6 +913,7 @@ function SettingsButton({
|
|||||||
<ButtonIcon icon={icon} size="md" />
|
<ButtonIcon icon={icon} size="md" />
|
||||||
</Button>
|
</Button>
|
||||||
<Text
|
<Text
|
||||||
|
numberOfLines={1}
|
||||||
style={[
|
style={[
|
||||||
a.text_2xs,
|
a.text_2xs,
|
||||||
a.font_medium,
|
a.font_medium,
|
||||||
@@ -726,11 +927,28 @@ function SettingsButton({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type EditNamePromptProps = {
|
function SettingsButtonPlaceholder() {
|
||||||
control: Dialog.DialogOuterProps['control']
|
const t = useTheme()
|
||||||
value: string
|
const {t: l} = useLingui()
|
||||||
onChangeText: (value: string) => void
|
|
||||||
onConfirm: () => void
|
return (
|
||||||
|
<View>
|
||||||
|
<Button color="secondary" size="large" shape="round" label={l`Loading…`}>
|
||||||
|
<ButtonIcon icon={EllipsisIcon} size="md" />
|
||||||
|
</Button>
|
||||||
|
<Text
|
||||||
|
numberOfLines={1}
|
||||||
|
style={[
|
||||||
|
a.text_2xs,
|
||||||
|
a.font_medium,
|
||||||
|
a.text_center,
|
||||||
|
a.pt_xs,
|
||||||
|
t.atoms.text,
|
||||||
|
]}>
|
||||||
|
{l`…`}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditNamePrompt({
|
function EditNamePrompt({
|
||||||
@@ -738,7 +956,12 @@ function EditNamePrompt({
|
|||||||
value,
|
value,
|
||||||
onChangeText,
|
onChangeText,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
}: EditNamePromptProps) {
|
}: {
|
||||||
|
control: Dialog.DialogOuterProps['control']
|
||||||
|
value: string
|
||||||
|
onChangeText: (value: string) => void
|
||||||
|
onConfirm: () => void
|
||||||
|
}) {
|
||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -819,6 +1042,27 @@ function LockChatPrompt({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function BlockMemberPrompt({
|
||||||
|
control,
|
||||||
|
onConfirm,
|
||||||
|
}: {
|
||||||
|
control: Dialog.DialogOuterProps['control']
|
||||||
|
onConfirm: () => void
|
||||||
|
}) {
|
||||||
|
const {t: l} = useLingui()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Prompt.Basic
|
||||||
|
control={control}
|
||||||
|
title={l`Block account?`}
|
||||||
|
description={l`Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`}
|
||||||
|
onConfirm={onConfirm}
|
||||||
|
confirmButtonCta={l`Block`}
|
||||||
|
confirmButtonColor="negative"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function SubtleHoverWrapper({children}: React.PropsWithChildren<unknown>) {
|
function SubtleHoverWrapper({children}: React.PropsWithChildren<unknown>) {
|
||||||
const {
|
const {
|
||||||
state: hover,
|
state: hover,
|
||||||
|
|||||||
@@ -365,7 +365,7 @@ export function MessagesList({
|
|||||||
profile={convoState.convo.members.find(
|
profile={convoState.convo.members.find(
|
||||||
member => member.did === item.message.sender.did,
|
member => member.did === item.message.sender.did,
|
||||||
)}
|
)}
|
||||||
isGroupChat={convoState.convo.kind === 'group'}
|
isGroupChat={convoState.getGroupInfo?.() != null}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
} else if (item.type === 'deleted-message') {
|
} else if (item.type === 'deleted-message') {
|
||||||
|
|||||||
@@ -31,13 +31,13 @@ export function useMuteConvo(
|
|||||||
mutationFn: async ({mute}: {mute: boolean}) => {
|
mutationFn: async ({mute}: {mute: boolean}) => {
|
||||||
if (!convoId) throw new Error('No convoId provided')
|
if (!convoId) throw new Error('No convoId provided')
|
||||||
if (mute) {
|
if (mute) {
|
||||||
const {data} = await agent.api.chat.bsky.convo.muteConvo(
|
const {data} = await agent.chat.bsky.convo.muteConvo(
|
||||||
{convoId},
|
{convoId},
|
||||||
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
|
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
|
||||||
)
|
)
|
||||||
return data
|
return data
|
||||||
} else {
|
} else {
|
||||||
const {data} = await agent.api.chat.bsky.convo.unmuteConvo(
|
const {data} = await agent.chat.bsky.convo.unmuteConvo(
|
||||||
{convoId},
|
{convoId},
|
||||||
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
|
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user