[Chat] Fix assumption that every chat has an owner (#10373)
This commit is contained in:
@@ -86,6 +86,9 @@ stats.json
|
|||||||
# VSCode
|
# VSCode
|
||||||
.vscode
|
.vscode
|
||||||
|
|
||||||
|
# Zed
|
||||||
|
.zed
|
||||||
|
|
||||||
# gitignore and github actions
|
# gitignore and github actions
|
||||||
!.gitignore
|
!.gitignore
|
||||||
!.github
|
!.github
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import {ScrollView, View} from 'react-native'
|
import {ScrollView, View} from 'react-native'
|
||||||
import {moderateProfile, type ModerationOpts} from '@atproto/api'
|
import {
|
||||||
|
type ChatBskyActorDefs,
|
||||||
|
moderateProfile,
|
||||||
|
type ModerationOpts,
|
||||||
|
} from '@atproto/api'
|
||||||
import {msg} from '@lingui/core/macro'
|
import {msg} from '@lingui/core/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {Trans} from '@lingui/react/macro'
|
import {Trans} from '@lingui/react/macro'
|
||||||
@@ -66,8 +70,8 @@ export function RecentChats({
|
|||||||
if (!convo) return null
|
if (!convo) return null
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(convo.kind === 'direct' &&
|
!convo.primaryMember ||
|
||||||
convo.primaryMember.handle === 'missing.invalid') ||
|
convo.primaryMember.handle === 'missing.invalid' ||
|
||||||
convo.view.muted
|
convo.view.muted
|
||||||
) {
|
) {
|
||||||
return null
|
return null
|
||||||
@@ -77,6 +81,7 @@ export function RecentChats({
|
|||||||
<RecentChatItem
|
<RecentChatItem
|
||||||
key={convo.view.id}
|
key={convo.view.id}
|
||||||
convo={convo}
|
convo={convo}
|
||||||
|
primaryMember={convo.primaryMember}
|
||||||
onPress={() => onSelectChat(convo.view.id)}
|
onPress={() => onSelectChat(convo.view.id)}
|
||||||
moderationOpts={moderationOpts}
|
moderationOpts={moderationOpts}
|
||||||
/>
|
/>
|
||||||
@@ -103,15 +108,17 @@ function RecentChatItem({
|
|||||||
onPress,
|
onPress,
|
||||||
moderationOpts,
|
moderationOpts,
|
||||||
convo,
|
convo,
|
||||||
|
primaryMember,
|
||||||
}: {
|
}: {
|
||||||
onPress: () => void
|
onPress: () => void
|
||||||
moderationOpts: ModerationOpts
|
moderationOpts: ModerationOpts
|
||||||
convo: ConvoWithDetails
|
convo: ConvoWithDetails
|
||||||
|
primaryMember: ChatBskyActorDefs.ProfileViewBasic
|
||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
|
||||||
const primaryProfile = useProfileShadow(convo.primaryMember)
|
const primaryProfile = useProfileShadow(primaryMember)
|
||||||
|
|
||||||
const moderation = moderateProfile(primaryProfile, moderationOpts)
|
const moderation = moderateProfile(primaryProfile, moderationOpts)
|
||||||
const name =
|
const name =
|
||||||
|
|||||||
@@ -479,14 +479,15 @@ function ExistingChatCard({
|
|||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const enabled =
|
const enabled =
|
||||||
convo.kind === 'group' ? convo.details.lockStatus === 'unlocked' : true
|
convo.kind === 'group' ? convo.details.lockStatus === 'unlocked' : true
|
||||||
const moderation = moderateProfile(convo.primaryMember, moderationOpts)
|
|
||||||
const name =
|
const name =
|
||||||
convo.kind === 'group'
|
convo.kind === 'group'
|
||||||
? convo.details.name
|
? convo.details.name
|
||||||
: createSanitizedDisplayName(
|
: createSanitizedDisplayName(
|
||||||
convo.primaryMember,
|
convo.primaryMember,
|
||||||
true,
|
true,
|
||||||
moderation.ui('displayName'),
|
moderateProfile(convo.primaryMember, moderationOpts).ui(
|
||||||
|
'displayName',
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleOnPress = useCallback(() => {
|
const handleOnPress = useCallback(() => {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
Unlock_Stroke2_Corner2_Rounded as UnlockIcon,
|
Unlock_Stroke2_Corner2_Rounded as UnlockIcon,
|
||||||
} from '#/components/icons/Lock'
|
} from '#/components/icons/Lock'
|
||||||
import {PencilLine_Stroke2_Corner0_Rounded as PencilIcon} from '#/components/icons/Pencil'
|
import {PencilLine_Stroke2_Corner0_Rounded as PencilIcon} from '#/components/icons/Pencil'
|
||||||
|
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
|
||||||
|
|
||||||
export type SystemMessageInfo = {
|
export type SystemMessageInfo = {
|
||||||
message: MessageDescriptor
|
message: MessageDescriptor
|
||||||
@@ -68,7 +69,13 @@ export function getSystemMessageInfo(
|
|||||||
} else if (ChatBskyConvoDefs.isSystemMessageDataUnlockConvo(data)) {
|
} else if (ChatBskyConvoDefs.isSystemMessageDataUnlockConvo(data)) {
|
||||||
return {Icon: UnlockIcon, message: msg`Chat unlocked`}
|
return {Icon: UnlockIcon, message: msg`Chat unlocked`}
|
||||||
} else if (ChatBskyConvoDefs.isSystemMessageDataLockConvoPermanently(data)) {
|
} else if (ChatBskyConvoDefs.isSystemMessageDataLockConvoPermanently(data)) {
|
||||||
return {Icon: LockIcon, message: msg`Chat locked permanently`}
|
const name = getReferredDisplayName(data.lockedBy, relatedProfiles)
|
||||||
|
return {
|
||||||
|
Icon: XIcon,
|
||||||
|
message: name
|
||||||
|
? msg`${name} ended the group chat`
|
||||||
|
: msg`This group chat was ended`,
|
||||||
|
}
|
||||||
} else if (ChatBskyConvoDefs.isSystemMessageDataEditGroup(data)) {
|
} else if (ChatBskyConvoDefs.isSystemMessageDataEditGroup(data)) {
|
||||||
return {
|
return {
|
||||||
Icon: PencilIcon,
|
Icon: PencilIcon,
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export type ConvoWithDetails = {view: ChatBskyConvoDefs.ConvoView} & (
|
|||||||
| {
|
| {
|
||||||
kind: 'group'
|
kind: 'group'
|
||||||
details: $Typed<ChatBskyConvoDefs.GroupConvo>
|
details: $Typed<ChatBskyConvoDefs.GroupConvo>
|
||||||
primaryMember: GroupConvoMember // the owner
|
primaryMember?: GroupConvoMember // the owner - may have left, thus optional
|
||||||
members: Array<GroupConvoMember>
|
members: Array<GroupConvoMember>
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
@@ -117,11 +117,6 @@ export function parseConvoView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!owner) {
|
|
||||||
logger.warn('No owner found in group convo')
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
view: convoView,
|
view: convoView,
|
||||||
kind: 'group',
|
kind: 'group',
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export function Member({
|
|||||||
const displayName = isDeletedAccount
|
const displayName = isDeletedAccount
|
||||||
? l`Deleted Account`
|
? l`Deleted Account`
|
||||||
: createSanitizedDisplayName(profile, true, moderation.ui('displayName'))
|
: createSanitizedDisplayName(profile, true, moderation.ui('displayName'))
|
||||||
const isProfileOwner = profile.did === convo.primaryMember.did
|
const isProfileOwner = profile.did === convo.primaryMember?.did
|
||||||
const isSelf = currentAccount?.did === profile.did
|
const isSelf = currentAccount?.did === profile.did
|
||||||
let statusBadge: React.ReactNode | null = null
|
let statusBadge: React.ReactNode | null = null
|
||||||
if (isSelf) {
|
if (isSelf) {
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import {useState} from 'react'
|
import {useState} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {ChatBskyActorDefs, ChatBskyConvoDefs} from '@atproto/api'
|
import {
|
||||||
|
ChatBskyActorDefs,
|
||||||
|
ChatBskyConvoDefs,
|
||||||
|
ModerationOpts,
|
||||||
|
} from '@atproto/api'
|
||||||
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'
|
||||||
|
|
||||||
@@ -14,6 +18,7 @@ import {
|
|||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {ConvoProvider, isConvoActive, useConvo} from '#/state/messages/convo'
|
import {ConvoProvider, isConvoActive, useConvo} from '#/state/messages/convo'
|
||||||
import {ConvoStatus} from '#/state/messages/convo/types'
|
import {ConvoStatus} from '#/state/messages/convo/types'
|
||||||
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
import {useEditGroupChatName} from '#/state/queries/messages/edit-group-chat-name'
|
import {useEditGroupChatName} from '#/state/queries/messages/edit-group-chat-name'
|
||||||
import {useLeaveConvo} from '#/state/queries/messages/leave-conversation'
|
import {useLeaveConvo} from '#/state/queries/messages/leave-conversation'
|
||||||
import {useListConvoMembersQuery} from '#/state/queries/messages/list-convo-members'
|
import {useListConvoMembersQuery} from '#/state/queries/messages/list-convo-members'
|
||||||
@@ -99,6 +104,7 @@ function SettingsInner() {
|
|||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const convoState = useConvo()
|
const convoState = useConvo()
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
|
const moderationOpts = useModerationOpts()
|
||||||
|
|
||||||
if (convoState.status === ConvoStatus.Error) {
|
if (convoState.status === ConvoStatus.Error) {
|
||||||
return (
|
return (
|
||||||
@@ -111,7 +117,7 @@ function SettingsInner() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isConvoActive(convoState)) {
|
if (!isConvoActive(convoState) || !moderationOpts) {
|
||||||
return (
|
return (
|
||||||
<View style={[a.flex_1, a.align_center, a.justify_center]}>
|
<View style={[a.flex_1, a.align_center, a.justify_center]}>
|
||||||
<Loader size="xl" />
|
<Loader size="xl" />
|
||||||
@@ -135,7 +141,9 @@ function SettingsInner() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return <GroupSettings convo={convoState.convo} />
|
return (
|
||||||
|
<GroupSettings convo={convoState.convo} moderationOpts={moderationOpts} />
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function keyExtractor(item: Item) {
|
function keyExtractor(item: Item) {
|
||||||
@@ -157,8 +165,10 @@ function isGroupMember(
|
|||||||
|
|
||||||
function GroupSettings({
|
function GroupSettings({
|
||||||
convo,
|
convo,
|
||||||
|
moderationOpts,
|
||||||
}: {
|
}: {
|
||||||
convo: Extract<ConvoWithDetails, {kind: 'group'}>
|
convo: Extract<ConvoWithDetails, {kind: 'group'}>
|
||||||
|
moderationOpts: ModerationOpts
|
||||||
}) {
|
}) {
|
||||||
const initialNumToRender = useInitialNumToRender({minItemHeight: 68})
|
const initialNumToRender = useInitialNumToRender({minItemHeight: 68})
|
||||||
const bottomBarOffset = useBottomBarOffset()
|
const bottomBarOffset = useBottomBarOffset()
|
||||||
@@ -166,7 +176,7 @@ function GroupSettings({
|
|||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
|
|
||||||
const primaryMember = convo.primaryMember
|
const primaryMember = convo.primaryMember
|
||||||
const isOwner = primaryMember.did === currentAccount?.did
|
const isOwner = !!primaryMember && primaryMember.did === currentAccount?.did
|
||||||
|
|
||||||
const {data: memberListData = [], isPending} = useListConvoMembersQuery({
|
const {data: memberListData = [], isPending} = useListConvoMembersQuery({
|
||||||
convoId: convo.view.id,
|
convoId: convo.view.id,
|
||||||
@@ -209,8 +219,8 @@ function GroupSettings({
|
|||||||
...memberListData
|
...memberListData
|
||||||
.filter(isGroupMember)
|
.filter(isGroupMember)
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const aIsOwner = a.did === primaryMember.did
|
const aIsOwner = a.did === primaryMember?.did
|
||||||
const bIsOwner = b.did === primaryMember.did
|
const bIsOwner = b.did === primaryMember?.did
|
||||||
const aIsSelf = a.did === currentAccount?.did
|
const aIsSelf = a.did === currentAccount?.did
|
||||||
const bIsSelf = b.did === currentAccount?.did
|
const bIsSelf = b.did === currentAccount?.did
|
||||||
if (aIsOwner !== bIsOwner) return aIsOwner ? -1 : 1
|
if (aIsOwner !== bIsOwner) return aIsOwner ? -1 : 1
|
||||||
@@ -223,7 +233,7 @@ function GroupSettings({
|
|||||||
key: profile.did,
|
key: profile.did,
|
||||||
profile,
|
profile,
|
||||||
status:
|
status:
|
||||||
primaryMember.did === profile.did
|
primaryMember?.did === profile.did
|
||||||
? 'owner'
|
? 'owner'
|
||||||
: invites.includes(profile.did)
|
: invites.includes(profile.did)
|
||||||
? 'invited'
|
? 'invited'
|
||||||
@@ -271,7 +281,13 @@ function GroupSettings({
|
|||||||
desktopFixedHeight
|
desktopFixedHeight
|
||||||
initialNumToRender={initialNumToRender}
|
initialNumToRender={initialNumToRender}
|
||||||
keyExtractor={keyExtractor}
|
keyExtractor={keyExtractor}
|
||||||
ListHeaderComponent={<SettingsHeader convo={convo} isOwner={isOwner} />}
|
ListHeaderComponent={
|
||||||
|
<SettingsHeader
|
||||||
|
convo={convo}
|
||||||
|
isOwner={isOwner}
|
||||||
|
moderationOpts={moderationOpts}
|
||||||
|
/>
|
||||||
|
}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
sideBorders={false}
|
sideBorders={false}
|
||||||
windowSize={11}
|
windowSize={11}
|
||||||
@@ -282,9 +298,11 @@ function GroupSettings({
|
|||||||
function SettingsHeader({
|
function SettingsHeader({
|
||||||
convo,
|
convo,
|
||||||
isOwner,
|
isOwner,
|
||||||
|
moderationOpts,
|
||||||
}: {
|
}: {
|
||||||
convo: Extract<ConvoWithDetails, {kind: 'group'}>
|
convo: Extract<ConvoWithDetails, {kind: 'group'}>
|
||||||
isOwner: boolean
|
isOwner: boolean
|
||||||
|
moderationOpts: ModerationOpts
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {i18n, t: l} = useLingui()
|
const {i18n, t: l} = useLingui()
|
||||||
@@ -517,11 +535,15 @@ function SettingsHeader({
|
|||||||
onChangeText={setNewGroupName}
|
onChangeText={setNewGroupName}
|
||||||
onConfirm={handleEditName}
|
onConfirm={handleEditName}
|
||||||
/>
|
/>
|
||||||
<InviteLinkDialog
|
{convo.primaryMember && (
|
||||||
convo={convo}
|
<InviteLinkDialog
|
||||||
control={inviteLinkDialog}
|
convo={convo}
|
||||||
isOwner={isOwner}
|
owner={convo.primaryMember}
|
||||||
/>
|
control={inviteLinkDialog}
|
||||||
|
isOwner={isOwner}
|
||||||
|
moderationOpts={moderationOpts}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<LockChatPrompt control={lockChatPrompt} onConfirm={handleConfirmLock} />
|
<LockChatPrompt control={lockChatPrompt} onConfirm={handleConfirmLock} />
|
||||||
<LeaveChatPrompt
|
<LeaveChatPrompt
|
||||||
control={leaveChatPrompt}
|
control={leaveChatPrompt}
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ import {useHaptics} from '#/lib/haptics'
|
|||||||
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
|
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
|
||||||
import {decrementBadgeCount} from '#/lib/notifications/notifications'
|
import {decrementBadgeCount} from '#/lib/notifications/notifications'
|
||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
import {type Shadow, useProfileShadow} from '#/state/cache/profile-shadow'
|
import {
|
||||||
|
type Shadow,
|
||||||
|
useMaybeProfileShadow,
|
||||||
|
useProfileShadow,
|
||||||
|
} from '#/state/cache/profile-shadow'
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
import {
|
import {
|
||||||
precacheConvoQuery,
|
precacheConvoQuery,
|
||||||
@@ -120,7 +124,7 @@ function DirectChatItem({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseChatItem
|
<BaseChatItem
|
||||||
convo={convo.view}
|
convo={convo}
|
||||||
avatar={
|
avatar={
|
||||||
<PreviewableUserAvatar
|
<PreviewableUserAvatar
|
||||||
profile={profile}
|
profile={profile}
|
||||||
@@ -167,10 +171,11 @@ function GroupChatItem({
|
|||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const groupOwner = useProfileShadow(convo.primaryMember)
|
const groupOwner = useMaybeProfileShadow(convo.primaryMember)
|
||||||
|
|
||||||
const moderation = useMemo(
|
const moderation = useMemo(
|
||||||
() => moderateProfile(groupOwner, moderationOpts),
|
() =>
|
||||||
|
groupOwner ? moderateProfile(groupOwner, moderationOpts) : undefined,
|
||||||
[groupOwner, moderationOpts],
|
[groupOwner, moderationOpts],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -178,7 +183,7 @@ function GroupChatItem({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseChatItem
|
<BaseChatItem
|
||||||
convo={convo.view}
|
convo={convo}
|
||||||
avatar={<AvatarBubbles profiles={convo.members} size={52} />}
|
avatar={<AvatarBubbles profiles={convo.members} size={52} />}
|
||||||
title={chatName}
|
title={chatName}
|
||||||
accessibilityHint={l`Go to the group chat named "${chatName}"`}
|
accessibilityHint={l`Go to the group chat named "${chatName}"`}
|
||||||
@@ -208,15 +213,15 @@ function BaseChatItem({
|
|||||||
postAlerts,
|
postAlerts,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
convo: ChatBskyConvoDefs.ConvoView
|
convo: ConvoWithDetails
|
||||||
avatar: React.ReactNode
|
avatar: React.ReactNode
|
||||||
title: string
|
title: string
|
||||||
subtitle?: string
|
subtitle?: string
|
||||||
accessibilityHint: string
|
accessibilityHint: string
|
||||||
isDeletedAccount: boolean
|
isDeletedAccount: boolean
|
||||||
isBlockedAccount: boolean
|
isBlockedAccount: boolean
|
||||||
primaryProfile: Shadow<bsky.profile.AnyProfileView>
|
primaryProfile?: Shadow<bsky.profile.AnyProfileView>
|
||||||
primaryProfileModeration: ModerationDecision
|
primaryProfileModeration?: ModerationDecision
|
||||||
showMenu?: boolean
|
showMenu?: boolean
|
||||||
showProfileBadges: boolean
|
showProfileBadges: boolean
|
||||||
postAlerts?: React.ReactNode
|
postAlerts?: React.ReactNode
|
||||||
@@ -233,9 +238,16 @@ function BaseChatItem({
|
|||||||
|
|
||||||
const playHaptic = useHaptics()
|
const playHaptic = useHaptics()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const hasUnread = convo.unreadCount > 0 && !isDeletedAccount
|
const hasUnread =
|
||||||
|
convo.view.unreadCount > 0 &&
|
||||||
|
!isDeletedAccount &&
|
||||||
|
!(
|
||||||
|
convo.kind === 'group' &&
|
||||||
|
convo.details.lockStatus === 'locked-permanently'
|
||||||
|
)
|
||||||
|
|
||||||
const blockInfo = useMemo(() => {
|
const blockInfo = useMemo(() => {
|
||||||
|
if (!primaryProfileModeration) return {listBlocks: [], userBlock: undefined}
|
||||||
const modui = primaryProfileModeration.ui('profileView')
|
const modui = primaryProfileModeration.ui('profileView')
|
||||||
const blocks = modui.alerts.filter(alert => alert.type === 'blocking')
|
const blocks = modui.alerts.filter(alert => alert.type === 'blocking')
|
||||||
const listBlocks = blocks.filter(alert => alert.source.type === 'list')
|
const listBlocks = blocks.filter(alert => alert.source.type === 'list')
|
||||||
@@ -246,7 +258,11 @@ function BaseChatItem({
|
|||||||
}
|
}
|
||||||
}, [primaryProfileModeration])
|
}, [primaryProfileModeration])
|
||||||
|
|
||||||
const isDimStyle = convo.muted || isBlockedAccount || isDeletedAccount
|
const isDimStyle =
|
||||||
|
convo.view.muted ||
|
||||||
|
isBlockedAccount ||
|
||||||
|
isDeletedAccount ||
|
||||||
|
(convo.kind === 'group' && convo.details.lockStatus !== 'unlocked')
|
||||||
|
|
||||||
const {lastMessage, lastMessageSentAt, latestReportableMessage} =
|
const {lastMessage, lastMessageSentAt, latestReportableMessage} =
|
||||||
useMemo(() => {
|
useMemo(() => {
|
||||||
@@ -257,8 +273,8 @@ function BaseChatItem({
|
|||||||
let latestReportableMessage: ChatBskyConvoDefs.MessageView | undefined
|
let latestReportableMessage: ChatBskyConvoDefs.MessageView | undefined
|
||||||
|
|
||||||
// Deleted message
|
// Deleted message
|
||||||
if (ChatBskyConvoDefs.isDeletedMessageView(convo.lastMessage)) {
|
if (ChatBskyConvoDefs.isDeletedMessageView(convo.view.lastMessage)) {
|
||||||
lastMessageSentAt = convo.lastMessage.sentAt
|
lastMessageSentAt = convo.view.lastMessage.sentAt
|
||||||
|
|
||||||
lastMessage = isDeletedAccount
|
lastMessage = isDeletedAccount
|
||||||
? l`Conversation deleted`
|
? l`Conversation deleted`
|
||||||
@@ -266,9 +282,9 @@ function BaseChatItem({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Message
|
// Message
|
||||||
if (ChatBskyConvoDefs.isMessageView(convo.lastMessage)) {
|
if (ChatBskyConvoDefs.isMessageView(convo.view.lastMessage)) {
|
||||||
const info = getMessageInfo({
|
const info = getMessageInfo({
|
||||||
convo,
|
convo: convo.view,
|
||||||
currentAccountDid: currentAccount?.did,
|
currentAccountDid: currentAccount?.did,
|
||||||
i18n,
|
i18n,
|
||||||
})
|
})
|
||||||
@@ -280,9 +296,9 @@ function BaseChatItem({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reaction
|
// Reaction
|
||||||
if (ChatBskyConvoDefs.isMessageAndReactionView(convo.lastReaction)) {
|
if (ChatBskyConvoDefs.isMessageAndReactionView(convo.view.lastReaction)) {
|
||||||
const info = getReactionInfo({
|
const info = getReactionInfo({
|
||||||
convo,
|
convo: convo.view,
|
||||||
currentAccountDid: currentAccount?.did,
|
currentAccountDid: currentAccount?.did,
|
||||||
i18n,
|
i18n,
|
||||||
})
|
})
|
||||||
@@ -297,14 +313,14 @@ function BaseChatItem({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// System message
|
// System message
|
||||||
if (ChatBskyConvoDefs.isSystemMessageView(convo.lastMessage)) {
|
if (ChatBskyConvoDefs.isSystemMessageView(convo.view.lastMessage)) {
|
||||||
const info = getSystemMessageInfo(
|
const info = getSystemMessageInfo(
|
||||||
convo.lastMessage.data,
|
convo.view.lastMessage.data,
|
||||||
new Map(convo.members.map(m => [m.did, m])),
|
new Map(convo.view.members.map(m => [m.did, m])),
|
||||||
)
|
)
|
||||||
if (info) {
|
if (info) {
|
||||||
lastMessage = i18n._(info.message)
|
lastMessage = i18n._(info.message)
|
||||||
lastMessageSentAt = convo.lastMessage.sentAt
|
lastMessageSentAt = convo.view.lastMessage.sentAt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,11 +348,11 @@ function BaseChatItem({
|
|||||||
|
|
||||||
const onPress = useCallback(
|
const onPress = useCallback(
|
||||||
(e: GestureResponderEvent) => {
|
(e: GestureResponderEvent) => {
|
||||||
for (const member of convo.members) {
|
for (const member of convo.view.members) {
|
||||||
unstableCacheProfileView(queryClient, member)
|
unstableCacheProfileView(queryClient, member)
|
||||||
}
|
}
|
||||||
precacheConvoQuery(queryClient, convo)
|
precacheConvoQuery(queryClient, convo.view)
|
||||||
void decrementBadgeCount(convo.unreadCount)
|
void decrementBadgeCount(convo.view.unreadCount)
|
||||||
if (isDeletedAccount) {
|
if (isDeletedAccount) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
menuControl.open()
|
menuControl.open()
|
||||||
@@ -359,7 +375,7 @@ function BaseChatItem({
|
|||||||
icon: EnvelopeOpen,
|
icon: EnvelopeOpen,
|
||||||
action: () => {
|
action: () => {
|
||||||
markAsRead({
|
markAsRead({
|
||||||
convoId: convo.id,
|
convoId: convo.view.id,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -402,7 +418,7 @@ function BaseChatItem({
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
to={`/messages/${convo.id}`}
|
to={`/messages/${convo.view.id}`}
|
||||||
label={title}
|
label={title}
|
||||||
accessibilityHint={accessibilityHint}
|
accessibilityHint={accessibilityHint}
|
||||||
accessibilityActions={
|
accessibilityActions={
|
||||||
@@ -419,7 +435,7 @@ function BaseChatItem({
|
|||||||
]
|
]
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
onPressIn={() => precacheConvoQuery(queryClient, convo)}
|
onPressIn={() => precacheConvoQuery(queryClient, convo.view)}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
onLongPress={showMenu && IS_NATIVE ? onLongPress : undefined}
|
onLongPress={showMenu && IS_NATIVE ? onLongPress : undefined}
|
||||||
onAccessibilityAction={showMenu ? onLongPress : undefined}>
|
onAccessibilityAction={showMenu ? onLongPress : undefined}>
|
||||||
@@ -455,7 +471,7 @@ function BaseChatItem({
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{showProfileBadges && (
|
{showProfileBadges && primaryProfile && (
|
||||||
<ProfileBadges
|
<ProfileBadges
|
||||||
profile={primaryProfile}
|
profile={primaryProfile}
|
||||||
size="md"
|
size="md"
|
||||||
@@ -480,7 +496,7 @@ function BaseChatItem({
|
|||||||
</TimeElapsed>
|
</TimeElapsed>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{(convo.muted || isBlockedAccount) && (
|
{(convo.view.muted || isBlockedAccount) && (
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
a.text_sm,
|
a.text_sm,
|
||||||
@@ -550,13 +566,14 @@ function BaseChatItem({
|
|||||||
|
|
||||||
<ChatListItemPortal.Outlet />
|
<ChatListItemPortal.Outlet />
|
||||||
|
|
||||||
{showMenu && (
|
{/* TODO: Allow showing menu for groups where the owner has left! */}
|
||||||
|
{showMenu && primaryProfile && (
|
||||||
<ConvoMenu
|
<ConvoMenu
|
||||||
convo={convo}
|
convo={convo.view}
|
||||||
profile={primaryProfile}
|
profile={primaryProfile}
|
||||||
control={menuControl}
|
control={menuControl}
|
||||||
currentScreen="list"
|
currentScreen="list"
|
||||||
showMarkAsRead={convo.unreadCount > 0}
|
showMarkAsRead={convo.view.unreadCount > 0}
|
||||||
hideTrigger={IS_NATIVE}
|
hideTrigger={IS_NATIVE}
|
||||||
blockInfo={blockInfo}
|
blockInfo={blockInfo}
|
||||||
style={[
|
style={[
|
||||||
@@ -576,7 +593,7 @@ function BaseChatItem({
|
|||||||
|
|
||||||
<LeaveConvoPrompt
|
<LeaveConvoPrompt
|
||||||
control={leaveConvoControl}
|
control={leaveConvoControl}
|
||||||
convoId={convo.id}
|
convoId={convo.view.id}
|
||||||
currentScreen="list"
|
currentScreen="list"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {useEffect, useState} from 'react'
|
import {useEffect, useState} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
|
import {moderateProfile, type ModerationOpts} from '@atproto/api'
|
||||||
import {Trans, useLingui} from '@lingui/react/macro'
|
import {Trans, useLingui} from '@lingui/react/macro'
|
||||||
|
|
||||||
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
||||||
@@ -17,7 +18,10 @@ import {
|
|||||||
StackedButton,
|
StackedButton,
|
||||||
} from '#/components/Button'
|
} from '#/components/Button'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import {type ConvoWithDetails} from '#/components/dms/util'
|
import {
|
||||||
|
type ConvoWithDetails,
|
||||||
|
type GroupConvoMember,
|
||||||
|
} from '#/components/dms/util'
|
||||||
import * as Toggle from '#/components/forms/Toggle'
|
import * as Toggle from '#/components/forms/Toggle'
|
||||||
import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon} from '#/components/icons/Arrow'
|
import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon} from '#/components/icons/Arrow'
|
||||||
import {ArrowShareRight_Stroke2_Corner2_Rounded as ArrowShareRightIcon} from '#/components/icons/ArrowShareRight'
|
import {ArrowShareRight_Stroke2_Corner2_Rounded as ArrowShareRightIcon} from '#/components/icons/ArrowShareRight'
|
||||||
@@ -39,16 +43,24 @@ enum Step {
|
|||||||
export function InviteLinkDialog({
|
export function InviteLinkDialog({
|
||||||
convo,
|
convo,
|
||||||
control,
|
control,
|
||||||
|
owner,
|
||||||
isOwner,
|
isOwner,
|
||||||
|
moderationOpts,
|
||||||
}: {
|
}: {
|
||||||
convo: Extract<ConvoWithDetails, {kind: 'group'}>
|
convo: Extract<ConvoWithDetails, {kind: 'group'}>
|
||||||
control: Dialog.DialogOuterProps['control']
|
control: Dialog.DialogOuterProps['control']
|
||||||
|
owner: GroupConvoMember
|
||||||
isOwner: boolean
|
isOwner: boolean
|
||||||
|
moderationOpts: ModerationOpts
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {t: l, i18n} = useLingui()
|
const {t: l, i18n} = useLingui()
|
||||||
|
|
||||||
const ownerName = createSanitizedDisplayName(convo.primaryMember)
|
const ownerName = createSanitizedDisplayName(
|
||||||
|
owner,
|
||||||
|
false,
|
||||||
|
moderateProfile(owner, moderationOpts).ui('displayName'),
|
||||||
|
)
|
||||||
|
|
||||||
const {joinLink} = convo.details
|
const {joinLink} = convo.details
|
||||||
const enabledStatus = joinLink?.enabledStatus
|
const enabledStatus = joinLink?.enabledStatus
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {View} from 'react-native'
|
|||||||
import {Plural, Trans, useLingui} from '@lingui/react/macro'
|
import {Plural, Trans, useLingui} from '@lingui/react/macro'
|
||||||
|
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
import {useAddGroupMembers} from '#/state/queries/messages/add-group-members'
|
import {useAddGroupMembers} from '#/state/queries/messages/add-group-members'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
@@ -23,13 +24,14 @@ export function MessagesListInfoPanel({
|
|||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
|
const moderationOpts = useModerationOpts()
|
||||||
|
const convoId = convo.view.id
|
||||||
|
|
||||||
const addMembersControl = Dialog.useDialogControl()
|
const addMembersControl = Dialog.useDialogControl()
|
||||||
const inviteLinkControl = Dialog.useDialogControl()
|
const inviteLinkControl = Dialog.useDialogControl()
|
||||||
|
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
|
|
||||||
const convoId = convo.view.id
|
|
||||||
const {mutate: addGroupMembers} = useAddGroupMembers(convoId, {
|
const {mutate: addGroupMembers} = useAddGroupMembers(convoId, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
addMembersControl.close()
|
addMembersControl.close()
|
||||||
@@ -46,9 +48,9 @@ export function MessagesListInfoPanel({
|
|||||||
// (isOwner && groupConvo) ||
|
// (isOwner && groupConvo) ||
|
||||||
// (!isOwner && groupConvo && joinLink?.enabledStatus === 'enabled')
|
// (!isOwner && groupConvo && joinLink?.enabledStatus === 'enabled')
|
||||||
|
|
||||||
const isOwner = convo?.primaryMember.did === currentAccount?.did
|
const isOwner = convo.primaryMember?.did === currentAccount?.did
|
||||||
|
|
||||||
const members = (convo?.members ?? []).filter(
|
const members = (convo.members ?? []).filter(
|
||||||
profile => profile.did !== currentAccount?.did,
|
profile => profile.did !== currentAccount?.did,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -81,7 +83,7 @@ export function MessagesListInfoPanel({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<View style={[a.align_center, a.justify_center]}>
|
<View style={[a.align_center, a.justify_center]}>
|
||||||
<AvatarBubbles animate={true} profiles={convo?.members} />
|
<AvatarBubbles animate={true} profiles={convo.members} />
|
||||||
{convo.details.name ? (
|
{convo.details.name ? (
|
||||||
<Text style={[a.text_2xl, a.font_bold, a.mt_lg, t.atoms.text]}>
|
<Text style={[a.text_2xl, a.font_bold, a.mt_lg, t.atoms.text]}>
|
||||||
{convo.details.name}
|
{convo.details.name}
|
||||||
@@ -139,11 +141,15 @@ export function MessagesListInfoPanel({
|
|||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
<InviteLinkDialog
|
{convo.primaryMember && moderationOpts && (
|
||||||
isOwner={isOwner}
|
<InviteLinkDialog
|
||||||
convo={convo}
|
convo={convo}
|
||||||
control={inviteLinkControl}
|
owner={convo.primaryMember}
|
||||||
/>
|
moderationOpts={moderationOpts}
|
||||||
|
isOwner={isOwner}
|
||||||
|
control={inviteLinkControl}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Dialog.Outer
|
<Dialog.Outer
|
||||||
control={addMembersControl}
|
control={addMembersControl}
|
||||||
testID="addChatMembersDialog"
|
testID="addChatMembersDialog"
|
||||||
|
|||||||
@@ -25,19 +25,22 @@ export function RequestListItem({
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDeletedAccount = convo.primaryMember.handle === 'missing.invalid'
|
const isDeletedAccount =
|
||||||
|
!convo.primaryMember || convo.primaryMember.handle === 'missing.invalid'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.relative, a.flex_1]}>
|
<View style={[a.relative, a.flex_1]}>
|
||||||
<ChatListItem convo={convo.view} showMenu={false}>
|
<ChatListItem convo={convo.view} showMenu={false}>
|
||||||
<View style={[a.pt_xs, a.pb_2xs]}>
|
{convo.primaryMember && (
|
||||||
<KnownFollowers
|
<View style={[a.pt_xs, a.pb_2xs]}>
|
||||||
profile={convo.primaryMember}
|
<KnownFollowers
|
||||||
moderationOpts={moderationOpts}
|
profile={convo.primaryMember}
|
||||||
minimal
|
moderationOpts={moderationOpts}
|
||||||
showIfEmpty
|
minimal
|
||||||
/>
|
showIfEmpty
|
||||||
</View>
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
{/* spacer, since you can't nest pressables */}
|
{/* spacer, since you can't nest pressables */}
|
||||||
<View style={[a.pt_md, a.pb_xs, a.w_full, {opacity: 0}]} aria-hidden>
|
<View style={[a.pt_md, a.pb_xs, a.w_full, {opacity: 0}]} aria-hidden>
|
||||||
{/* Placeholder text so that it responds to the font height */}
|
{/* Placeholder text so that it responds to the font height */}
|
||||||
@@ -60,7 +63,7 @@ export function RequestListItem({
|
|||||||
paddingLeft: tokens.space.lg + 52 + tokens.space.md,
|
paddingLeft: tokens.space.lg + 52 + tokens.space.md,
|
||||||
},
|
},
|
||||||
]}>
|
]}>
|
||||||
{!isDeletedAccount ? (
|
{convo.primaryMember && !isDeletedAccount ? (
|
||||||
<>
|
<>
|
||||||
<AcceptChatButton convo={convo.view} currentScreen="list" />
|
<AcceptChatButton convo={convo.view} currentScreen="list" />
|
||||||
<RejectMenu
|
<RejectMenu
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {useCurrentConvoId} from '#/state/messages/current-convo-id'
|
|||||||
import {useMessagesEventBus} from '#/state/messages/events'
|
import {useMessagesEventBus} from '#/state/messages/events'
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
import {useAgent, useSession} from '#/state/session'
|
import {useAgent, useSession} from '#/state/session'
|
||||||
|
import {parseConvoView} from '#/components/dms/util'
|
||||||
import {useLeftConvos} from './leave-conversation'
|
import {useLeftConvos} from './leave-conversation'
|
||||||
|
|
||||||
export const RQKEY_ROOT = 'convo-list'
|
export const RQKEY_ROOT = 'convo-list'
|
||||||
@@ -453,19 +454,18 @@ function calculateCount(
|
|||||||
return (
|
return (
|
||||||
convos
|
convos
|
||||||
.filter(convo => convo.id !== currentConvoId)
|
.filter(convo => convo.id !== currentConvoId)
|
||||||
.reduce((acc, convo) => {
|
.reduce((acc, convoView) => {
|
||||||
const otherMember = convo.members.find(
|
const convo = parseConvoView(convoView, currentAccountDid)
|
||||||
member => member.did !== currentAccountDid,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!otherMember || !moderationOpts) return acc
|
if (!convo || !moderationOpts) return acc
|
||||||
|
|
||||||
const moderation = moderateProfile(otherMember, moderationOpts)
|
|
||||||
const shouldIgnore =
|
const shouldIgnore =
|
||||||
convo.muted ||
|
convo.view.muted ||
|
||||||
moderation.blocked ||
|
!convo.primaryMember ||
|
||||||
otherMember.handle === 'missing.invalid'
|
moderateProfile(convo.primaryMember, moderationOpts).blocked ||
|
||||||
const unreadCount = !shouldIgnore && convo.unreadCount > 0 ? 1 : 0
|
convo.primaryMember.handle === 'missing.invalid' ||
|
||||||
|
(convo.kind === 'group' && convo.details.lockStatus !== 'unlocked')
|
||||||
|
const unreadCount = !shouldIgnore && convo.view.unreadCount > 0 ? 1 : 0
|
||||||
|
|
||||||
return acc + unreadCount
|
return acc + unreadCount
|
||||||
}, 0) ?? 0
|
}, 0) ?? 0
|
||||||
|
|||||||
Reference in New Issue
Block a user