Add repost-only mutes (#11223)
Co-authored-by: DS Boyce <260543580+ds-boyce@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M2.293 2.293a1 1 0 0 1 1.414 0l18 18a1 1 0 0 1-1.414 1.414L17.586 19H6.414l1.293 1.293a1 1 0 0 1-1.414 1.414l-3-3a1 1 0 0 1 0-1.414l3-3a1 1 0 1 1 1.414 1.414L6.414 17h9.172L5.909 7.323A2 2 0 0 0 5 9v2a1 1 0 0 1-2 0V9c0-1.255.579-2.372 1.482-3.104L2.293 3.707a1 1 0 0 1 0-1.414M20 12a1 1 0 0 1 1 1v2a1 1 0 0 1-2 0v-2a1 1 0 0 1 1-1m-3.707-9.707a1 1 0 0 1 1.414 0l3 3a1 1 0 0 1 0 1.414l-3 3a1 1 0 0 1-1.414-1.414L17.586 7H11a1 1 0 0 1 0-2h6.586l-1.293-1.293a1 1 0 0 1 0-1.414"/></svg>
|
||||||
|
After Width: | Height: | Size: 575 B |
@@ -992,11 +992,6 @@
|
|||||||
"count": 3
|
"count": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/screens/Profile/Header/Shell.tsx": {
|
|
||||||
"typescript/no-explicit-any": {
|
|
||||||
"count": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"src/screens/Profile/KnownFollowers.tsx": {
|
"src/screens/Profile/KnownFollowers.tsx": {
|
||||||
"typescript/no-misused-promises": {
|
"typescript/no-misused-promises": {
|
||||||
"count": 2
|
"count": 2
|
||||||
|
|||||||
+1
-1
@@ -97,7 +97,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atproto/api": "0.20.37",
|
"@atproto/api": "0.20.37",
|
||||||
"@atproto/common-web": "0.5.6",
|
"@atproto/common-web": "0.5.7",
|
||||||
"@atproto/syntax": "0.7.2",
|
"@atproto/syntax": "0.7.2",
|
||||||
"@bitdrift/react-native": "^0.6.8",
|
"@bitdrift/react-native": "^0.6.8",
|
||||||
"@braintree/sanitize-url": "^6.0.2",
|
"@braintree/sanitize-url": "^6.0.2",
|
||||||
|
|||||||
Generated
+14
-14
@@ -245,8 +245,8 @@ importers:
|
|||||||
specifier: 0.20.37
|
specifier: 0.20.37
|
||||||
version: 0.20.37
|
version: 0.20.37
|
||||||
'@atproto/common-web':
|
'@atproto/common-web':
|
||||||
specifier: 0.5.6
|
specifier: 0.5.7
|
||||||
version: 0.5.6
|
version: 0.5.7
|
||||||
'@atproto/syntax':
|
'@atproto/syntax':
|
||||||
specifier: 0.7.2
|
specifier: 0.7.2
|
||||||
version: 0.7.2
|
version: 0.7.2
|
||||||
@@ -878,24 +878,24 @@ packages:
|
|||||||
resolution: {integrity: sha512-TEt8oKMbuC+1GvqyLKDBLqmptnk1mh52FdHPQEomlPIWxh88YcNnk0g0iKNVBevaeeLtWZF1a7Am/t/q255u5Q==}
|
resolution: {integrity: sha512-TEt8oKMbuC+1GvqyLKDBLqmptnk1mh52FdHPQEomlPIWxh88YcNnk0g0iKNVBevaeeLtWZF1a7Am/t/q255u5Q==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
'@atproto/common-web@0.5.6':
|
'@atproto/common-web@0.5.7':
|
||||||
resolution: {integrity: sha512-5Y4MIK9dpkJPiKiE6u7iEHitxj+g3aAU2GfGL686JlKE2zDKD4y18BJb+uVek6nXQKb5XOdNVvw+7BHarpn8Fw==}
|
resolution: {integrity: sha512-lvU8lFO5UByacsl3AReXkpTWjcPBS8mBqt40B45HwCt+s58a+xT/YI/+AUpmzSmsPVZCpeCmP6wgsOSC8RILXg==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
'@atproto/common-web@0.5.8':
|
'@atproto/common-web@0.5.8':
|
||||||
resolution: {integrity: sha512-GiYY2Jgbg1aWe9QGT3TswC/wZNicipBDz0nFo4FrP6XqENEJJRxGKAf/zjvqbesfIQFhHXi1VbSFY7GStB8v8w==}
|
resolution: {integrity: sha512-GiYY2Jgbg1aWe9QGT3TswC/wZNicipBDz0nFo4FrP6XqENEJJRxGKAf/zjvqbesfIQFhHXi1VbSFY7GStB8v8w==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
'@atproto/lex-data@0.1.5':
|
'@atproto/lex-data@0.1.6':
|
||||||
resolution: {integrity: sha512-TEM6GHuYpNm4O90LjNgbYq1Gmcr875S+BHrDxkg4PB5w/nlsz4HlbkGQG/WP/xbIV5O8TF5nNHDpCZ5ezTRrFA==}
|
resolution: {integrity: sha512-rvovRrYWDEuerX+ZeQbdI5dKs0bYvuMjhwDOtT+pkOvdY4MkfN41DJCz/RAYVAQ7noPdaalB6pAX9euukheE/w==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
'@atproto/lex-data@0.1.7':
|
'@atproto/lex-data@0.1.7':
|
||||||
resolution: {integrity: sha512-kW/dPLqo/WgCLV+XESR4JKwV6c1rZWJGOfuPupZGTjEDAKoBbKXdaEzX9/1vKQYbZ9U3j0DS/n7OFFK7wBugyQ==}
|
resolution: {integrity: sha512-kW/dPLqo/WgCLV+XESR4JKwV6c1rZWJGOfuPupZGTjEDAKoBbKXdaEzX9/1vKQYbZ9U3j0DS/n7OFFK7wBugyQ==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
'@atproto/lex-json@0.1.4':
|
'@atproto/lex-json@0.1.5':
|
||||||
resolution: {integrity: sha512-ENR2cWkVrES+UL6TovbCRdX9BJOyHHJUS8jYx3Lxp3j4vEphjn/u+DW7bWloST2O8ID2OuVlt6+28ftNEEjmQQ==}
|
resolution: {integrity: sha512-qOp6+Za5p/xDXACYIP3vySbKaf9CJDwiZEN78ghPT1gRoJohKG3ONt/WmvOxBVJqc6VTRaqEpbG9vX9oLF2qsA==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
'@atproto/lex-json@0.1.6':
|
'@atproto/lex-json@0.1.6':
|
||||||
@@ -9426,10 +9426,10 @@ snapshots:
|
|||||||
tlds: 1.261.0
|
tlds: 1.261.0
|
||||||
zod: 3.25.76
|
zod: 3.25.76
|
||||||
|
|
||||||
'@atproto/common-web@0.5.6':
|
'@atproto/common-web@0.5.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@atproto/lex-data': 0.1.5
|
'@atproto/lex-data': 0.1.6
|
||||||
'@atproto/lex-json': 0.1.4
|
'@atproto/lex-json': 0.1.5
|
||||||
'@atproto/syntax': 0.7.2
|
'@atproto/syntax': 0.7.2
|
||||||
zod: 3.25.76
|
zod: 3.25.76
|
||||||
|
|
||||||
@@ -9440,7 +9440,7 @@ snapshots:
|
|||||||
'@atproto/syntax': 0.7.3
|
'@atproto/syntax': 0.7.3
|
||||||
zod: 3.25.76
|
zod: 3.25.76
|
||||||
|
|
||||||
'@atproto/lex-data@0.1.5':
|
'@atproto/lex-data@0.1.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
multiformats: 13.4.2
|
multiformats: 13.4.2
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
@@ -9452,9 +9452,9 @@ snapshots:
|
|||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
unicode-segmenter: 0.14.5
|
unicode-segmenter: 0.14.5
|
||||||
|
|
||||||
'@atproto/lex-json@0.1.4':
|
'@atproto/lex-json@0.1.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@atproto/lex-data': 0.1.5
|
'@atproto/lex-data': 0.1.6
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@atproto/lex-json@0.1.6':
|
'@atproto/lex-json@0.1.6':
|
||||||
|
|||||||
@@ -510,6 +510,8 @@ export type Events = {
|
|||||||
}
|
}
|
||||||
'profile:mute': {}
|
'profile:mute': {}
|
||||||
'profile:unmute': {}
|
'profile:unmute': {}
|
||||||
|
'profile:muteReposts': {}
|
||||||
|
'profile:unmuteReposts': {}
|
||||||
'profile:block': {}
|
'profile:block': {}
|
||||||
'profile:unblock': {}
|
'profile:unblock': {}
|
||||||
'suggestedUser:follow': {
|
'suggestedUser:follow': {
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import {useMemo} from 'react'
|
import {useMemo} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {BSKY_LABELER_DID, type ModerationCause} from '@atproto/api'
|
import {BSKY_LABELER_DID, type ModerationCause} from '@atproto/api'
|
||||||
import {Trans} from '@lingui/react/macro'
|
import {Trans, useLingui} from '@lingui/react/macro'
|
||||||
|
|
||||||
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
|
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
|
||||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {atoms as a, useTheme, type ViewStyleProp} from '#/alf'
|
import {atoms as a, useTheme, type ViewStyleProp} from '#/alf'
|
||||||
import {Button} from '#/components/Button'
|
import {Button} from '#/components/Button'
|
||||||
|
import {RepostStrike_Stroke2_Corner0_Rounded as RepostStrikeIcon} from '#/components/icons/Repost'
|
||||||
import {
|
import {
|
||||||
ModerationDetailsDialog,
|
ModerationDetailsDialog,
|
||||||
useModerationDetailsDialogControl,
|
useModerationDetailsDialogControl,
|
||||||
@@ -215,3 +216,25 @@ export function FollowsYou({size = 'sm'}: CommonProps) {
|
|||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function MutedOnlyReposts(props: CommonProps) {
|
||||||
|
const t = useTheme()
|
||||||
|
const {t: l} = useLingui()
|
||||||
|
|
||||||
|
const size = props.size === 'lg' ? 16 : 12
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LabelBase
|
||||||
|
{...props}
|
||||||
|
label={l`Reposts Hidden`}
|
||||||
|
icon={
|
||||||
|
<RepostStrikeIcon
|
||||||
|
width={size}
|
||||||
|
fill={t.atoms.text_contrast_medium.color}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
disabled={true}
|
||||||
|
onPress={() => {}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -623,8 +623,9 @@ export function Labels({
|
|||||||
const moderation = moderateProfile(profile, moderationOpts)
|
const moderation = moderateProfile(profile, moderationOpts)
|
||||||
const modui = moderation.ui('profileList')
|
const modui = moderation.ui('profileList')
|
||||||
const followedBy = profile.viewer?.followedBy
|
const followedBy = profile.viewer?.followedBy
|
||||||
|
const mutedOnlyReposts = profile.viewer?.mutedOnlyReposts
|
||||||
|
|
||||||
if (!followedBy && !modui.inform && !modui.alert) {
|
if (!followedBy && !mutedOnlyReposts && !modui.inform && !modui.alert) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -637,6 +638,7 @@ export function Labels({
|
|||||||
{modui.informs.map(inform => (
|
{modui.informs.map(inform => (
|
||||||
<Pills.Label key={getModerationCauseKey(inform)} cause={inform} />
|
<Pills.Label key={getModerationCauseKey(inform)} cause={inform} />
|
||||||
))}
|
))}
|
||||||
|
{mutedOnlyReposts && <Pills.MutedOnlyReposts />}
|
||||||
</Pills.Row>
|
</Pills.Row>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,3 +15,7 @@ export const Repost_Stroke2_Corner3_Rounded = createSinglePathSVG({
|
|||||||
export const RepostRepost_Stroke2_Corner2_Rounded = createSinglePathSVG({
|
export const RepostRepost_Stroke2_Corner2_Rounded = createSinglePathSVG({
|
||||||
path: 'M6.043 14.293a1 1 0 1 1 1.414 1.414L5.164 18l2.293 2.293.068.076a1 1 0 0 1-1.406 1.406l-.076-.068-2.47-2.47a1.75 1.75 0 0 1 0-2.474l2.47-2.47Zm6.22 0a1 1 0 0 1 1.414 1.414L12.384 17H18a1 1 0 0 0 1-1v-3a1 1 0 1 1 2 0v3a3 3 0 0 1-3 3h-5.616l1.293 1.293.068.076a1 1 0 0 1-1.406 1.406l-.076-.068-2.47-2.47a1.75 1.75 0 0 1 0-2.474l2.47-2.47ZM3 11V8a3 3 0 0 1 3-3h5.586l-1.293-1.293-.068-.076a1 1 0 0 1 1.406-1.406l.076.068 2.47 2.47.12.133a1.75 1.75 0 0 1 0 2.209l-.12.132-2.47 2.47a1 1 0 1 1-1.414-1.414L11.586 7H6a1 1 0 0 0-1 1v3a1 1 0 1 1-2 0Zm13.543-8.707a1 1 0 0 1 1.338-.068l.076.068 2.47 2.47.12.133a1.75 1.75 0 0 1 0 2.209l-.12.132-2.47 2.47a1 1 0 1 1-1.414-1.414L18.836 6l-2.293-2.293-.068-.076a1 1 0 0 1 .068-1.338Z',
|
path: 'M6.043 14.293a1 1 0 1 1 1.414 1.414L5.164 18l2.293 2.293.068.076a1 1 0 0 1-1.406 1.406l-.076-.068-2.47-2.47a1.75 1.75 0 0 1 0-2.474l2.47-2.47Zm6.22 0a1 1 0 0 1 1.414 1.414L12.384 17H18a1 1 0 0 0 1-1v-3a1 1 0 1 1 2 0v3a3 3 0 0 1-3 3h-5.616l1.293 1.293.068.076a1 1 0 0 1-1.406 1.406l-.076-.068-2.47-2.47a1.75 1.75 0 0 1 0-2.474l2.47-2.47ZM3 11V8a3 3 0 0 1 3-3h5.586l-1.293-1.293-.068-.076a1 1 0 0 1 1.406-1.406l.076.068 2.47 2.47.12.133a1.75 1.75 0 0 1 0 2.209l-.12.132-2.47 2.47a1 1 0 1 1-1.414-1.414L11.586 7H6a1 1 0 0 0-1 1v3a1 1 0 1 1-2 0Zm13.543-8.707a1 1 0 0 1 1.338-.068l.076.068 2.47 2.47.12.133a1.75 1.75 0 0 1 0 2.209l-.12.132-2.47 2.47a1 1 0 1 1-1.414-1.414L18.836 6l-2.293-2.293-.068-.076a1 1 0 0 1 .068-1.338Z',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const RepostStrike_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M2.293 2.293a1 1 0 0 1 1.414 0l18 18a1 1 0 0 1-1.414 1.414L17.586 19H6.414l1.293 1.293a1 1 0 0 1-1.414 1.414l-3-3a1 1 0 0 1 0-1.414l3-3a1 1 0 1 1 1.414 1.414L6.414 17h9.172L5.909 7.323A2 2 0 0 0 5 9v2a1 1 0 0 1-2 0V9c0-1.255.579-2.372 1.482-3.104L2.293 3.707a1 1 0 0 1 0-1.414M20 12a1 1 0 0 1 1 1v2a1 1 0 0 1-2 0v-2a1 1 0 0 1 1-1M16.293 2.293a1 1 0 0 1 1.414 0l3 3a1 1 0 0 1 0 1.414l-3 3a1 1 0 0 1-1.414-1.414L17.586 7H11a1 1 0 0 1 0-2h6.586l-1.293-1.293a1 1 0 0 1 0-1.414',
|
||||||
|
})
|
||||||
|
|||||||
@@ -3,16 +3,21 @@ import {type ModerationDecision} from '@atproto/api'
|
|||||||
|
|
||||||
import {getModerationCauseKey, unique} from '#/lib/moderation'
|
import {getModerationCauseKey, unique} from '#/lib/moderation'
|
||||||
import * as Pills from '#/components/Pills'
|
import * as Pills from '#/components/Pills'
|
||||||
|
import type * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
export function ProfileHeaderAlerts({
|
export function ProfileHeaderAlerts({
|
||||||
moderation,
|
moderation,
|
||||||
|
profile,
|
||||||
style,
|
style,
|
||||||
}: {
|
}: {
|
||||||
moderation: ModerationDecision
|
moderation: ModerationDecision
|
||||||
|
profile: bsky.profile.AnyProfileView
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
}) {
|
}) {
|
||||||
const modui = moderation.ui('profileView')
|
const modui = moderation.ui('profileView')
|
||||||
if (!modui.alert && !modui.inform) {
|
const mutedOnlyReposts = profile.viewer?.mutedOnlyReposts
|
||||||
|
|
||||||
|
if (!mutedOnlyReposts && !modui.alert && !modui.inform) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,6 +37,7 @@ export function ProfileHeaderAlerts({
|
|||||||
cause={cause}
|
cause={cause}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
{mutedOnlyReposts && <Pills.MutedOnlyReposts size="lg" />}
|
||||||
</Pills.Row>
|
</Pills.Row>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export function GrowableBanner({
|
|||||||
backButton?: React.ReactNode
|
backButton?: React.ReactNode
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
onPress?: () => void
|
onPress?: () => void
|
||||||
bannerRef?: AnimatedRef<Animated.View>
|
bannerRef?: AnimatedRef
|
||||||
testID?: string
|
testID?: string
|
||||||
label?: string
|
label?: string
|
||||||
}) {
|
}) {
|
||||||
@@ -87,7 +87,7 @@ function GrowableBannerInner({
|
|||||||
backButton?: React.ReactNode
|
backButton?: React.ReactNode
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
onPress?: () => void
|
onPress?: () => void
|
||||||
bannerRef?: AnimatedRef<Animated.View>
|
bannerRef?: AnimatedRef
|
||||||
testID?: string
|
testID?: string
|
||||||
label?: string
|
label?: string
|
||||||
}) {
|
}) {
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ let ProfileHeaderShell = ({
|
|||||||
const liveStatusControl = useDialogControl()
|
const liveStatusControl = useDialogControl()
|
||||||
|
|
||||||
const aviRef = useAnimatedRef()
|
const aviRef = useAnimatedRef()
|
||||||
const bannerRef = useAnimatedRef<Animated.View>()
|
const bannerRef = useAnimatedRef()
|
||||||
|
|
||||||
const onPressBack = useCallback(() => {
|
const onPressBack = useCallback(() => {
|
||||||
if (navigation.canGoBack()) {
|
if (navigation.canGoBack()) {
|
||||||
@@ -73,7 +73,7 @@ let ProfileHeaderShell = ({
|
|||||||
const _openLightbox = useCallback(
|
const _openLightbox = useCallback(
|
||||||
(
|
(
|
||||||
uri: string,
|
uri: string,
|
||||||
thumbRef: AnimatedRef<any>,
|
thumbRef: AnimatedRef,
|
||||||
type: 'circle-avi' | 'rect-avi' | 'image' = 'circle-avi',
|
type: 'circle-avi' | 'rect-avi' | 'image' = 'circle-avi',
|
||||||
) => {
|
) => {
|
||||||
openLightbox({
|
openLightbox({
|
||||||
@@ -239,6 +239,7 @@ let ProfileHeaderShell = ({
|
|||||||
) : (
|
) : (
|
||||||
<ProfileHeaderAlerts
|
<ProfileHeaderAlerts
|
||||||
moderation={moderation}
|
moderation={moderation}
|
||||||
|
profile={profile}
|
||||||
style={[
|
style={[
|
||||||
a.px_lg,
|
a.px_lg,
|
||||||
a.pt_xs,
|
a.pt_xs,
|
||||||
|
|||||||
Vendored
+11
-1
@@ -41,6 +41,7 @@ export type {Shadow} from './types'
|
|||||||
export interface ProfileShadow {
|
export interface ProfileShadow {
|
||||||
followingUri: string | undefined
|
followingUri: string | undefined
|
||||||
muted: boolean | undefined
|
muted: boolean | undefined
|
||||||
|
mutedOnlyReposts: boolean | undefined
|
||||||
blockingUri: string | undefined
|
blockingUri: string | undefined
|
||||||
verification: AppBskyActorDefs.VerificationState
|
verification: AppBskyActorDefs.VerificationState
|
||||||
status: AppBskyActorDefs.StatusView | undefined
|
status: AppBskyActorDefs.StatusView | undefined
|
||||||
@@ -243,6 +244,11 @@ export function isProfileShadowApplied<
|
|||||||
if ('muted' in shadow) {
|
if ('muted' in shadow) {
|
||||||
if (profile.viewer?.muted !== shadow.muted) return false
|
if (profile.viewer?.muted !== shadow.muted) return false
|
||||||
}
|
}
|
||||||
|
if ('mutedOnlyReposts' in shadow) {
|
||||||
|
if (profile.viewer?.mutedOnlyReposts !== shadow.mutedOnlyReposts) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
if ('blockingUri' in shadow) {
|
if ('blockingUri' in shadow) {
|
||||||
if (profile.viewer?.blocking !== shadow.blockingUri) return false
|
if (profile.viewer?.blocking !== shadow.blockingUri) return false
|
||||||
}
|
}
|
||||||
@@ -274,13 +280,17 @@ export function mergeShadow<TProfileView extends bsky.profile.AnyProfileView>(
|
|||||||
? shadow.followingUri
|
? shadow.followingUri
|
||||||
: profile.viewer?.following,
|
: profile.viewer?.following,
|
||||||
muted: 'muted' in shadow ? shadow.muted : profile.viewer?.muted,
|
muted: 'muted' in shadow ? shadow.muted : profile.viewer?.muted,
|
||||||
|
mutedOnlyReposts:
|
||||||
|
'mutedOnlyReposts' in shadow
|
||||||
|
? shadow.mutedOnlyReposts
|
||||||
|
: profile.viewer?.mutedOnlyReposts,
|
||||||
blocking:
|
blocking:
|
||||||
'blockingUri' in shadow ? shadow.blockingUri : profile.viewer?.blocking,
|
'blockingUri' in shadow ? shadow.blockingUri : profile.viewer?.blocking,
|
||||||
activitySubscription:
|
activitySubscription:
|
||||||
'activitySubscription' in shadow
|
'activitySubscription' in shadow
|
||||||
? shadow.activitySubscription
|
? shadow.activitySubscription
|
||||||
: profile.viewer?.activitySubscription,
|
: profile.viewer?.activitySubscription,
|
||||||
},
|
} satisfies AppBskyActorDefs.ViewerState,
|
||||||
verification:
|
verification:
|
||||||
'verification' in shadow ? shadow.verification : profile.verification,
|
'verification' in shadow ? shadow.verification : profile.verification,
|
||||||
status:
|
status:
|
||||||
|
|||||||
@@ -449,9 +449,13 @@ export function useProfileMuteMutationQueue(
|
|||||||
})
|
})
|
||||||
|
|
||||||
const queueMute = useCallback(() => {
|
const queueMute = useCallback(() => {
|
||||||
// optimistically update
|
/*
|
||||||
|
* Optimistically update. A full mute replaces any stored repost-only
|
||||||
|
* scope on the server, so clear it here too.
|
||||||
|
*/
|
||||||
updateProfileShadow(queryClient, did, {
|
updateProfileShadow(queryClient, did, {
|
||||||
muted: true,
|
muted: true,
|
||||||
|
mutedOnlyReposts: false,
|
||||||
})
|
})
|
||||||
return queueToggle(true)
|
return queueToggle(true)
|
||||||
}, [queryClient, did, queueToggle])
|
}, [queryClient, did, queueToggle])
|
||||||
@@ -460,6 +464,7 @@ export function useProfileMuteMutationQueue(
|
|||||||
// optimistically update
|
// optimistically update
|
||||||
updateProfileShadow(queryClient, did, {
|
updateProfileShadow(queryClient, did, {
|
||||||
muted: false,
|
muted: false,
|
||||||
|
mutedOnlyReposts: false,
|
||||||
})
|
})
|
||||||
return queueToggle(false)
|
return queueToggle(false)
|
||||||
}, [queryClient, did, queueToggle])
|
}, [queryClient, did, queueToggle])
|
||||||
@@ -467,6 +472,65 @@ export function useProfileMuteMutationQueue(
|
|||||||
return [queueMute, queueUnmute] as const
|
return [queueMute, queueUnmute] as const
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles a repost-only mute: muting hides just the account's reposts,
|
||||||
|
* unmuting removes the mute entirely. Not applicable when the account is
|
||||||
|
* fully muted (viewer.muted).
|
||||||
|
*/
|
||||||
|
export function useProfileMuteRepostsMutationQueue(
|
||||||
|
profile: Shadow<bsky.profile.AnyProfileView>,
|
||||||
|
) {
|
||||||
|
const ax = useAnalytics()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const did = profile.did
|
||||||
|
const initialMutedOnlyReposts = !!profile.viewer?.mutedOnlyReposts
|
||||||
|
const muteRepostsMutation = useProfileMuteRepostsMutation()
|
||||||
|
const unmuteMutation = useProfileUnmuteMutation()
|
||||||
|
|
||||||
|
const queueToggle = useToggleMutationQueue({
|
||||||
|
initialState: initialMutedOnlyReposts,
|
||||||
|
runMutation: async (_prevMutedOnlyReposts, shouldMute) => {
|
||||||
|
if (shouldMute) {
|
||||||
|
await muteRepostsMutation.mutateAsync({
|
||||||
|
did,
|
||||||
|
})
|
||||||
|
ax.metric('profile:muteReposts', {})
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
await unmuteMutation.mutateAsync({
|
||||||
|
did,
|
||||||
|
})
|
||||||
|
ax.metric('profile:unmuteReposts', {})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess(finalMutedOnlyReposts) {
|
||||||
|
// finalize
|
||||||
|
updateProfileShadow(queryClient, did, {
|
||||||
|
mutedOnlyReposts: finalMutedOnlyReposts,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const queueMuteReposts = useCallback(() => {
|
||||||
|
// optimistically update
|
||||||
|
updateProfileShadow(queryClient, did, {
|
||||||
|
mutedOnlyReposts: true,
|
||||||
|
})
|
||||||
|
return queueToggle(true)
|
||||||
|
}, [queryClient, did, queueToggle])
|
||||||
|
|
||||||
|
const queueUnmuteReposts = useCallback(() => {
|
||||||
|
// optimistically update
|
||||||
|
updateProfileShadow(queryClient, did, {
|
||||||
|
mutedOnlyReposts: false,
|
||||||
|
})
|
||||||
|
return queueToggle(false)
|
||||||
|
}, [queryClient, did, queueToggle])
|
||||||
|
|
||||||
|
return [queueMuteReposts, queueUnmuteReposts] as const
|
||||||
|
}
|
||||||
|
|
||||||
function useProfileMuteMutation() {
|
function useProfileMuteMutation() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
@@ -480,6 +544,19 @@ function useProfileMuteMutation() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useProfileMuteRepostsMutation() {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const agent = useAgent()
|
||||||
|
return useMutation<void, Error, {did: string}>({
|
||||||
|
mutationFn: async ({did}) => {
|
||||||
|
await agent.mute(did, {onlyReposts: true})
|
||||||
|
},
|
||||||
|
onSuccess() {
|
||||||
|
void queryClient.invalidateQueries({queryKey: RQKEY_MY_MUTED()})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function useProfileUnmuteMutation() {
|
function useProfileUnmuteMutation() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
useProfileBlockMutationQueue,
|
useProfileBlockMutationQueue,
|
||||||
useProfileFollowMutationQueue,
|
useProfileFollowMutationQueue,
|
||||||
useProfileMuteMutationQueue,
|
useProfileMuteMutationQueue,
|
||||||
|
useProfileMuteRepostsMutationQueue,
|
||||||
} from '#/state/queries/profile'
|
} from '#/state/queries/profile'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {EventStopper} from '#/view/com/util/EventStopper'
|
import {EventStopper} from '#/view/com/util/EventStopper'
|
||||||
@@ -28,20 +29,24 @@ import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/i
|
|||||||
import {CircleCheck_Stroke2_Corner0_Rounded as CircleCheckIcon} from '#/components/icons/CircleCheck'
|
import {CircleCheck_Stroke2_Corner0_Rounded as CircleCheckIcon} from '#/components/icons/CircleCheck'
|
||||||
import {CircleX_Stroke2_Corner0_Rounded as CircleXIcon} from '#/components/icons/CircleX'
|
import {CircleX_Stroke2_Corner0_Rounded as CircleXIcon} from '#/components/icons/CircleX'
|
||||||
import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard'
|
import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard'
|
||||||
import {DotGrid3x1_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid'
|
import {DotGrid3x1_Stroke2_Corner0_Rounded as EllipsisIcon} from '#/components/icons/DotGrid'
|
||||||
import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag'
|
import {Flag_Stroke2_Corner0_Rounded as FlagIcon} from '#/components/icons/Flag'
|
||||||
import {ListSparkle_Stroke2_Corner0_Rounded as List} from '#/components/icons/ListSparkle'
|
import {ListSparkle_Stroke2_Corner0_Rounded as ListIcon} from '#/components/icons/ListSparkle'
|
||||||
import {Live_Stroke2_Corner0_Rounded as LiveIcon} from '#/components/icons/Live'
|
import {Live_Stroke2_Corner0_Rounded as LiveIcon} from '#/components/icons/Live'
|
||||||
import {MagnifyingGlass_Stroke2_Corner0_Rounded as SearchIcon} from '#/components/icons/MagnifyingGlass'
|
import {MagnifyingGlass_Stroke2_Corner0_Rounded as SearchIcon} from '#/components/icons/MagnifyingGlass'
|
||||||
import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
|
import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute'
|
||||||
import {PeopleRemove2_Stroke2_Corner0_Rounded as UserMinus} from '#/components/icons/PeopleRemove2'
|
import {PeopleRemove2_Stroke2_Corner0_Rounded as UserMinusIcon} from '#/components/icons/PeopleRemove2'
|
||||||
import {
|
import {
|
||||||
PersonCheck_Stroke2_Corner0_Rounded as PersonCheck,
|
PersonCheck_Stroke2_Corner0_Rounded as PersonCheckIcon,
|
||||||
PersonX_Stroke2_Corner0_Rounded as PersonX,
|
PersonX_Stroke2_Corner0_Rounded as PersonXIcon,
|
||||||
} from '#/components/icons/Person'
|
} from '#/components/icons/Person'
|
||||||
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
|
||||||
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
|
import {
|
||||||
import {StarterPack} from '#/components/icons/StarterPack'
|
Repost_Stroke2_Corner0_Rounded as RepostIcon,
|
||||||
|
RepostStrike_Stroke2_Corner0_Rounded as RepostStrikeIcon,
|
||||||
|
} from '#/components/icons/Repost'
|
||||||
|
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
|
||||||
|
import {StarterPack as StarterPackIcon} from '#/components/icons/StarterPack'
|
||||||
import * as Menu from '#/components/Menu'
|
import * as Menu from '#/components/Menu'
|
||||||
import {BlockDialog} from '#/components/moderation/BlockDialog'
|
import {BlockDialog} from '#/components/moderation/BlockDialog'
|
||||||
import {
|
import {
|
||||||
@@ -93,6 +98,8 @@ let ProfileMenu = ({
|
|||||||
const {mutate: saveNux} = useSaveNux()
|
const {mutate: saveNux} = useSaveNux()
|
||||||
|
|
||||||
const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile)
|
const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile)
|
||||||
|
const [queueMuteReposts, queueUnmuteReposts] =
|
||||||
|
useProfileMuteRepostsMutationQueue(profile)
|
||||||
const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
|
const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
|
||||||
const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
|
const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
|
||||||
profile,
|
profile,
|
||||||
@@ -162,6 +169,40 @@ let ProfileMenu = ({
|
|||||||
}
|
}
|
||||||
}, [ax, profile.viewer?.muted, queueUnmute, l, queueMute])
|
}, [ax, profile.viewer?.muted, queueUnmute, l, queueMute])
|
||||||
|
|
||||||
|
const onPressMuteReposts = useCallback(async () => {
|
||||||
|
if (profile.viewer?.mutedOnlyReposts) {
|
||||||
|
try {
|
||||||
|
await queueUnmuteReposts()
|
||||||
|
Toast.show(
|
||||||
|
l({message: 'Reposts will be shown in feeds', context: 'toast'}),
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
const e = err as Error
|
||||||
|
if (e?.name !== 'AbortError') {
|
||||||
|
ax.logger.error('Failed to show reposts', {message: e})
|
||||||
|
Toast.show(l`There was an issue! ${e.toString()}`, {
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await queueMuteReposts()
|
||||||
|
Toast.show(
|
||||||
|
l({message: 'Reposts will be hidden in feeds', context: 'toast'}),
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
const e = err as Error
|
||||||
|
if (e?.name !== 'AbortError') {
|
||||||
|
ax.logger.error('Failed to hide reposts', {message: e})
|
||||||
|
Toast.show(l`There was an issue! ${e.toString()}`, {
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [ax, profile.viewer, queueUnmuteReposts, l, queueMuteReposts])
|
||||||
|
|
||||||
const blockAccount = useCallback(async () => {
|
const blockAccount = useCallback(async () => {
|
||||||
if (profile.viewer?.blocking) {
|
if (profile.viewer?.blocking) {
|
||||||
try {
|
try {
|
||||||
@@ -265,7 +306,7 @@ let ProfileMenu = ({
|
|||||||
size="small"
|
size="small"
|
||||||
shape="round">
|
shape="round">
|
||||||
{statusNudgeActive && <Gradient style={[a.rounded_full]} />}
|
{statusNudgeActive && <Gradient style={[a.rounded_full]} />}
|
||||||
<ButtonIcon icon={Ellipsis} size="sm" />
|
<ButtonIcon icon={EllipsisIcon} size="sm" />
|
||||||
</Button>
|
</Button>
|
||||||
{statusNudgeActive && <Dot top={1} right={1} />}
|
{statusNudgeActive && <Dot top={1} right={1} />}
|
||||||
</>
|
</>
|
||||||
@@ -331,7 +372,9 @@ let ProfileMenu = ({
|
|||||||
<Trans>Follow account</Trans>
|
<Trans>Follow account</Trans>
|
||||||
)}
|
)}
|
||||||
</Menu.ItemText>
|
</Menu.ItemText>
|
||||||
<Menu.ItemIcon icon={isFollowing ? UserMinus : Plus} />
|
<Menu.ItemIcon
|
||||||
|
icon={isFollowing ? UserMinusIcon : PlusIcon}
|
||||||
|
/>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -343,7 +386,7 @@ let ProfileMenu = ({
|
|||||||
<Menu.ItemText>
|
<Menu.ItemText>
|
||||||
<Trans>Add to starter packs</Trans>
|
<Trans>Add to starter packs</Trans>
|
||||||
</Menu.ItemText>
|
</Menu.ItemText>
|
||||||
<Menu.ItemIcon icon={StarterPack} />
|
<Menu.ItemIcon icon={StarterPackIcon} />
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
testID="profileHeaderDropdownListAddRemoveBtn"
|
testID="profileHeaderDropdownListAddRemoveBtn"
|
||||||
@@ -352,7 +395,7 @@ let ProfileMenu = ({
|
|||||||
<Menu.ItemText>
|
<Menu.ItemText>
|
||||||
<Trans>Add to lists</Trans>
|
<Trans>Add to lists</Trans>
|
||||||
</Menu.ItemText>
|
</Menu.ItemText>
|
||||||
<Menu.ItemIcon icon={List} />
|
<Menu.ItemIcon icon={ListIcon} />
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
{isSelf && canGoLive && (
|
{isSelf && canGoLive && (
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
@@ -435,6 +478,32 @@ let ProfileMenu = ({
|
|||||||
<>
|
<>
|
||||||
{!profile.viewer?.blocking &&
|
{!profile.viewer?.blocking &&
|
||||||
!profile.viewer?.mutedByList && (
|
!profile.viewer?.mutedByList && (
|
||||||
|
<>
|
||||||
|
{!profile.viewer?.muted && (
|
||||||
|
<Menu.Item
|
||||||
|
testID="profileHeaderDropdownMuteRepostsBtn"
|
||||||
|
label={
|
||||||
|
profile.viewer?.mutedOnlyReposts
|
||||||
|
? l`Show reposts in feeds`
|
||||||
|
: l`Hide reposts in feeds`
|
||||||
|
}
|
||||||
|
onPress={() => void onPressMuteReposts()}>
|
||||||
|
<Menu.ItemText>
|
||||||
|
{profile.viewer?.mutedOnlyReposts ? (
|
||||||
|
<Trans>Show reposts in feeds</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>Hide reposts in feeds</Trans>
|
||||||
|
)}
|
||||||
|
</Menu.ItemText>
|
||||||
|
<Menu.ItemIcon
|
||||||
|
icon={
|
||||||
|
profile.viewer?.mutedOnlyReposts
|
||||||
|
? RepostIcon
|
||||||
|
: RepostStrikeIcon
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
testID="profileHeaderDropdownMuteBtn"
|
testID="profileHeaderDropdownMuteBtn"
|
||||||
label={
|
label={
|
||||||
@@ -451,9 +520,12 @@ let ProfileMenu = ({
|
|||||||
)}
|
)}
|
||||||
</Menu.ItemText>
|
</Menu.ItemText>
|
||||||
<Menu.ItemIcon
|
<Menu.ItemIcon
|
||||||
icon={profile.viewer?.muted ? Unmute : Mute}
|
icon={
|
||||||
|
profile.viewer?.muted ? UnmuteIcon : MuteIcon
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{!profile.viewer?.blockingByList && (
|
{!profile.viewer?.blockingByList && (
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
@@ -473,7 +545,9 @@ let ProfileMenu = ({
|
|||||||
</Menu.ItemText>
|
</Menu.ItemText>
|
||||||
<Menu.ItemIcon
|
<Menu.ItemIcon
|
||||||
icon={
|
icon={
|
||||||
profile.viewer?.blocking ? PersonCheck : PersonX
|
profile.viewer?.blocking
|
||||||
|
? PersonCheckIcon
|
||||||
|
: PersonXIcon
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
@@ -485,7 +559,7 @@ let ProfileMenu = ({
|
|||||||
<Menu.ItemText>
|
<Menu.ItemText>
|
||||||
<Trans>Report account</Trans>
|
<Trans>Report account</Trans>
|
||||||
</Menu.ItemText>
|
</Menu.ItemText>
|
||||||
<Menu.ItemIcon icon={Flag} />
|
<Menu.ItemIcon icon={FlagIcon} />
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user