Merge conflicts
This commit is contained in:
@@ -557,6 +557,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
|||||||
a.align_center,
|
a.align_center,
|
||||||
a.justify_center,
|
a.justify_center,
|
||||||
a.curve_continuous,
|
a.curve_continuous,
|
||||||
|
a.overflow_hidden,
|
||||||
baseStyles,
|
baseStyles,
|
||||||
style,
|
style,
|
||||||
...(state.hovered || state.pressed
|
...(state.hovered || state.pressed
|
||||||
|
|||||||
@@ -359,7 +359,13 @@ export function FlatListFooter({children}: {children: React.ReactNode}) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Handle({difference = false}: {difference?: boolean}) {
|
export function Handle({
|
||||||
|
difference = false,
|
||||||
|
fill,
|
||||||
|
}: {
|
||||||
|
difference?: boolean
|
||||||
|
fill?: string
|
||||||
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {screenReaderEnabled} = useA11y()
|
const {screenReaderEnabled} = useA11y()
|
||||||
@@ -390,7 +396,7 @@ export function Handle({difference = false}: {difference?: boolean}) {
|
|||||||
opacity: 0.75,
|
opacity: 0.75,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
backgroundColor: t.palette.contrast_975,
|
backgroundColor: fill || t.palette.contrast_975,
|
||||||
opacity: 0.5,
|
opacity: 0.5,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ export function Item({children, label, style, onPress, ...rest}: ItemProps) {
|
|||||||
a.gap_sm,
|
a.gap_sm,
|
||||||
a.px_md,
|
a.px_md,
|
||||||
a.rounded_md,
|
a.rounded_md,
|
||||||
|
a.overflow_hidden,
|
||||||
a.border,
|
a.border,
|
||||||
t.atoms.bg_contrast_25,
|
t.atoms.bg_contrast_25,
|
||||||
t.atoms.border_contrast_low,
|
t.atoms.border_contrast_low,
|
||||||
@@ -193,7 +194,6 @@ export function ItemText({children, style}: ItemTextProps) {
|
|||||||
a.text_md,
|
a.text_md,
|
||||||
a.font_semi_bold,
|
a.font_semi_bold,
|
||||||
t.atoms.text_contrast_high,
|
t.atoms.text_contrast_high,
|
||||||
{paddingTop: 3},
|
|
||||||
style,
|
style,
|
||||||
disabled && t.atoms.text_contrast_low,
|
disabled && t.atoms.text_contrast_low,
|
||||||
]}>
|
]}>
|
||||||
@@ -202,14 +202,16 @@ export function ItemText({children, style}: ItemTextProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ItemIcon({icon: Comp}: ItemIconProps) {
|
export function ItemIcon({icon: Comp, fill}: ItemIconProps) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {disabled} = useMenuItemContext()
|
const {disabled} = useMenuItemContext()
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
size="lg"
|
size="lg"
|
||||||
fill={
|
fill={
|
||||||
disabled
|
fill
|
||||||
|
? fill({disabled})
|
||||||
|
: disabled
|
||||||
? t.atoms.text_contrast_low.color
|
? t.atoms.text_contrast_low.color
|
||||||
: t.atoms.text_contrast_medium.color
|
: t.atoms.text_contrast_medium.color
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -262,6 +262,7 @@ export function Item({children, label, onPress, style, ...rest}: ItemProps) {
|
|||||||
a.gap_lg,
|
a.gap_lg,
|
||||||
a.py_sm,
|
a.py_sm,
|
||||||
a.rounded_xs,
|
a.rounded_xs,
|
||||||
|
a.overflow_hidden,
|
||||||
{minHeight: 32, paddingHorizontal: 10},
|
{minHeight: 32, paddingHorizontal: 10},
|
||||||
web({outline: 0}),
|
web({outline: 0}),
|
||||||
(hovered || focused) &&
|
(hovered || focused) &&
|
||||||
@@ -302,7 +303,7 @@ export function ItemText({children, style}: ItemTextProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ItemIcon({icon: Comp, position = 'left'}: ItemIconProps) {
|
export function ItemIcon({icon: Comp, position = 'left', fill}: ItemIconProps) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {disabled} = useMenuItemContext()
|
const {disabled} = useMenuItemContext()
|
||||||
return (
|
return (
|
||||||
@@ -319,7 +320,9 @@ export function ItemIcon({icon: Comp, position = 'left'}: ItemIconProps) {
|
|||||||
<Comp
|
<Comp
|
||||||
size="md"
|
size="md"
|
||||||
fill={
|
fill={
|
||||||
disabled
|
fill
|
||||||
|
? fill({disabled})
|
||||||
|
: disabled
|
||||||
? t.atoms.text_contrast_low.color
|
? t.atoms.text_contrast_low.color
|
||||||
: t.atoms.text_contrast_medium.color
|
: t.atoms.text_contrast_medium.color
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ export type ItemTextProps = React.PropsWithChildren<TextStyleProp & {}>
|
|||||||
export type ItemIconProps = React.PropsWithChildren<{
|
export type ItemIconProps = React.PropsWithChildren<{
|
||||||
icon: React.ComponentType<SVGIconProps>
|
icon: React.ComponentType<SVGIconProps>
|
||||||
position?: 'left' | 'right'
|
position?: 'left' | 'right'
|
||||||
|
fill?: (props: {disabled: boolean}) => string
|
||||||
}>
|
}>
|
||||||
|
|
||||||
export type GroupProps = React.PropsWithChildren<ViewStyleProp & {}>
|
export type GroupProps = React.PropsWithChildren<ViewStyleProp & {}>
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
import {useCallback} from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {Image} from 'expo-image'
|
||||||
|
import {LinearGradient} from 'expo-linear-gradient'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {isWeb} from '#/platform/detection'
|
||||||
|
import {atoms as a, useTheme, utils, web} from '#/alf'
|
||||||
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
|
import * as Dialog from '#/components/Dialog'
|
||||||
|
import {useNuxDialogContext} from '#/components/dialogs/nuxs'
|
||||||
|
import {
|
||||||
|
createIsEnabledCheck,
|
||||||
|
isExistingUserAsOf,
|
||||||
|
} from '#/components/dialogs/nuxs/utils'
|
||||||
|
import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
import {IS_E2E} from '#/env'
|
||||||
|
|
||||||
|
export const enabled = createIsEnabledCheck(props => {
|
||||||
|
return (
|
||||||
|
!IS_E2E &&
|
||||||
|
isExistingUserAsOf(
|
||||||
|
'2026-01-16T00:00:00.000Z',
|
||||||
|
props.currentProfile.createdAt,
|
||||||
|
) &&
|
||||||
|
props.gate('live_now_beta')
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export function LiveNowBetaDialog() {
|
||||||
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
|
const nuxDialogs = useNuxDialogContext()
|
||||||
|
const control = Dialog.useDialogControl()
|
||||||
|
|
||||||
|
Dialog.useAutoOpen(control)
|
||||||
|
|
||||||
|
const onClose = useCallback(() => {
|
||||||
|
nuxDialogs.dismissActiveNux()
|
||||||
|
}, [nuxDialogs])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog.Outer
|
||||||
|
control={control}
|
||||||
|
onClose={onClose}
|
||||||
|
nativeOptions={{preventExpansion: true}}>
|
||||||
|
<Dialog.Handle fill={t.palette.primary_700} />
|
||||||
|
|
||||||
|
<Dialog.ScrollableInner
|
||||||
|
label={_(msg`Show when you’re live`)}
|
||||||
|
style={[web({maxWidth: 440})]}
|
||||||
|
contentContainerStyle={[
|
||||||
|
{
|
||||||
|
paddingTop: 0,
|
||||||
|
paddingLeft: 0,
|
||||||
|
paddingRight: 0,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.align_center,
|
||||||
|
a.overflow_hidden,
|
||||||
|
{
|
||||||
|
gap: 16,
|
||||||
|
paddingTop: isWeb ? 24 : 40,
|
||||||
|
borderTopLeftRadius: a.rounded_md.borderRadius,
|
||||||
|
borderTopRightRadius: a.rounded_md.borderRadius,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<LinearGradient
|
||||||
|
colors={[
|
||||||
|
t.palette.primary_100,
|
||||||
|
utils.alpha(t.palette.primary_100, 0),
|
||||||
|
]}
|
||||||
|
locations={[0, 1]}
|
||||||
|
start={{x: 0, y: 0}}
|
||||||
|
end={{x: 0, y: 1}}
|
||||||
|
style={[a.absolute, a.inset_0]}
|
||||||
|
/>
|
||||||
|
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
|
||||||
|
<BeakerIcon fill={t.palette.primary_700} size="sm" />
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.font_semi_bold,
|
||||||
|
{
|
||||||
|
color: t.palette.primary_700,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<Trans>Beta Feature</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.relative,
|
||||||
|
a.w_full,
|
||||||
|
{
|
||||||
|
paddingTop: 8,
|
||||||
|
paddingHorizontal: 32,
|
||||||
|
paddingBottom: 32,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
borderRadius: 24,
|
||||||
|
aspectRatio: 326 / 105,
|
||||||
|
},
|
||||||
|
isWeb
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
boxShadow: `0px 10px 15px -3px ${utils.alpha(t.palette.primary_900, 0.4)}`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
t.atoms.shadow_md,
|
||||||
|
{
|
||||||
|
shadowColor: utils.alpha(t.palette.primary_900, 0.4),
|
||||||
|
shadowOpacity: 0.2,
|
||||||
|
shadowOffset: {
|
||||||
|
width: 0,
|
||||||
|
height: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]}>
|
||||||
|
<Image
|
||||||
|
accessibilityIgnoresInvertColors
|
||||||
|
source={require('../../../../assets/images/bookmarks_announcement_nux.webp')}
|
||||||
|
style={[
|
||||||
|
a.w_full,
|
||||||
|
{
|
||||||
|
aspectRatio: 326 / 105,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
alt={_(
|
||||||
|
msg({
|
||||||
|
message: `A screenshot of a post from @rtgame.bsky.social, showing the user is currently livestreaming content on Twitch. The post reads: "New stream. New energy. Same chaos. Tap in now — we're just getting started!"`,
|
||||||
|
comment:
|
||||||
|
'Contains a post that originally appeared in English. Consider translating the post text if it makes sense in your language, and noting that the post was translated from English.',
|
||||||
|
}),
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={[a.align_center, a.px_xl, a.gap_2xl, a.pb_sm]}>
|
||||||
|
<View style={[a.gap_sm, a.align_center]}>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.text_3xl,
|
||||||
|
a.leading_tight,
|
||||||
|
a.font_bold,
|
||||||
|
a.text_center,
|
||||||
|
{
|
||||||
|
fontSize: isWeb ? 28 : 32,
|
||||||
|
maxWidth: 360,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<Trans>Show when you’re live</Trans>
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.text_md,
|
||||||
|
a.leading_snug,
|
||||||
|
a.text_center,
|
||||||
|
{
|
||||||
|
maxWidth: 340,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<Trans>
|
||||||
|
Streaming on Twitch? Set your live status on Bluesky to add a
|
||||||
|
badge to your avatar.
Tapping it takes people straight to your
|
||||||
|
stream.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{!isWeb && (
|
||||||
|
<Button
|
||||||
|
label={_(msg`Close`)}
|
||||||
|
size="large"
|
||||||
|
color="primary"
|
||||||
|
onPress={() => {
|
||||||
|
control.close()
|
||||||
|
}}
|
||||||
|
style={[a.w_full]}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Close</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Dialog.Close />
|
||||||
|
</Dialog.ScrollableInner>
|
||||||
|
</Dialog.Outer>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -20,9 +20,9 @@ import {useProfileQuery} from '#/state/queries/profile'
|
|||||||
import {type SessionAccount, useSession} from '#/state/session'
|
import {type SessionAccount, useSession} from '#/state/session'
|
||||||
import {useOnboardingState} from '#/state/shell'
|
import {useOnboardingState} from '#/state/shell'
|
||||||
import {
|
import {
|
||||||
enabled as isFindContactsAnnouncementEnabled,
|
enabled as isLiveNowBetaDialogEnabled,
|
||||||
FindContactsAnnouncement,
|
LiveNowBetaDialog,
|
||||||
} from '#/components/dialogs/nuxs/FindContactsAnnouncement'
|
} from '#/components/dialogs/nuxs/LiveNowBetaDialog'
|
||||||
import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing'
|
import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing'
|
||||||
import {type EnabledCheckProps} from '#/components/dialogs/nuxs/utils'
|
import {type EnabledCheckProps} from '#/components/dialogs/nuxs/utils'
|
||||||
import {useGeolocation} from '#/geolocation'
|
import {useGeolocation} from '#/geolocation'
|
||||||
@@ -37,8 +37,8 @@ const queuedNuxs: {
|
|||||||
enabled?: (props: EnabledCheckProps) => boolean
|
enabled?: (props: EnabledCheckProps) => boolean
|
||||||
}[] = [
|
}[] = [
|
||||||
{
|
{
|
||||||
id: Nux.FindContactsAnnouncement,
|
id: Nux.LiveNowBetaDialog,
|
||||||
enabled: isFindContactsAnnouncementEnabled,
|
enabled: isLiveNowBetaDialogEnabled,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -186,9 +186,7 @@ function Inner({
|
|||||||
return (
|
return (
|
||||||
<Context.Provider value={ctx}>
|
<Context.Provider value={ctx}>
|
||||||
{/*For example, activeNux === Nux.NeueTypography && <NeueTypography />*/}
|
{/*For example, activeNux === Nux.NeueTypography && <NeueTypography />*/}
|
||||||
{activeNux === Nux.FindContactsAnnouncement && (
|
{activeNux === Nux.LiveNowBetaDialog && <LiveNowBetaDialog />}
|
||||||
<FindContactsAnnouncement />
|
|
||||||
)}
|
|
||||||
</Context.Provider>
|
</Context.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import {View, type ViewStyle} from 'react-native'
|
||||||
|
|
||||||
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The little blue dot used to nudge a user towards a certain feature. The dot
|
||||||
|
* is absolutely positioned, and is intended to be configured by passing in
|
||||||
|
* positional styles via `top`, `bottom`, `left`, and `right` props.
|
||||||
|
*/
|
||||||
|
export function Dot({
|
||||||
|
top,
|
||||||
|
bottom,
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
}: Pick<ViewStyle, 'top' | 'bottom' | 'left' | 'right'>) {
|
||||||
|
const t = useTheme()
|
||||||
|
return (
|
||||||
|
<View style={[a.absolute, {top, bottom, left, right}]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.rounded_full,
|
||||||
|
{
|
||||||
|
height: 8,
|
||||||
|
width: 8,
|
||||||
|
backgroundColor: t.palette.primary_500,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import {LinearGradient} from 'expo-linear-gradient'
|
||||||
|
|
||||||
|
import {atoms as a, useTheme, utils} from '#/alf'
|
||||||
|
|
||||||
|
export function Gradient() {
|
||||||
|
const t = useTheme()
|
||||||
|
return (
|
||||||
|
<LinearGradient
|
||||||
|
colors={[
|
||||||
|
utils.alpha(t.palette.primary_500, 0.2),
|
||||||
|
utils.alpha(t.palette.primary_500, 0.1),
|
||||||
|
]}
|
||||||
|
locations={[0, 1]}
|
||||||
|
start={{x: 0, y: 0}}
|
||||||
|
end={{x: 1, y: 0}}
|
||||||
|
style={[a.absolute, a.inset_0]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
import {isAfter, parseISO} from 'date-fns'
|
import {isAfter, parseISO} from 'date-fns'
|
||||||
|
|
||||||
import {useMaybeProfileShadow} from '#/state/cache/profile-shadow'
|
import {useMaybeProfileShadow} from '#/state/cache/profile-shadow'
|
||||||
import {useLiveNowConfig} from '#/state/service-config'
|
import {type LiveNowConfig, useLiveNowConfig} from '#/state/service-config'
|
||||||
import {useTickEveryMinute} from '#/state/shell'
|
import {useTickEveryMinute} from '#/state/shell'
|
||||||
import type * as bsky from '#/types/bsky'
|
import type * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ export function useActorStatus(actor?: bsky.profile.AnyProfileView) {
|
|||||||
tick! // revalidate every minute
|
tick! // revalidate every minute
|
||||||
|
|
||||||
if (shadowed && 'status' in shadowed && shadowed.status) {
|
if (shadowed && 'status' in shadowed && shadowed.status) {
|
||||||
const isValid = validateStatus(shadowed.did, shadowed.status, config)
|
const isValid = validateStatus(shadowed.status, config)
|
||||||
const isDisabled = shadowed.status.isDisabled || false
|
const isDisabled = shadowed.status.isDisabled || false
|
||||||
const isActive = isStatusStillActive(shadowed.status.expiresAt)
|
const isActive = isStatusStillActive(shadowed.status.expiresAt)
|
||||||
if (isValid && !isDisabled && isActive) {
|
if (isValid && !isDisabled && isActive) {
|
||||||
@@ -65,19 +65,14 @@ export function isStatusStillActive(timeStr: string | undefined) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function validateStatus(
|
export function validateStatus(
|
||||||
did: string,
|
|
||||||
status: AppBskyActorDefs.StatusView,
|
status: AppBskyActorDefs.StatusView,
|
||||||
config: {did: string; domains: string[]}[],
|
config: LiveNowConfig,
|
||||||
) {
|
) {
|
||||||
if (status.status !== 'app.bsky.actor.status#live') return false
|
if (status.status !== 'app.bsky.actor.status#live') return false
|
||||||
const sources = config.find(cfg => cfg.did === did)
|
|
||||||
if (!sources) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
if (AppBskyEmbedExternal.isView(status.embed)) {
|
if (AppBskyEmbedExternal.isView(status.embed)) {
|
||||||
const url = new URL(status.embed.external.uri)
|
const url = new URL(status.embed.external.uri)
|
||||||
return sources.domains.includes(url.hostname)
|
return config.allowedDomains.includes(url.hostname)
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export type Gate =
|
|||||||
| 'explore_show_suggested_feeds'
|
| 'explore_show_suggested_feeds'
|
||||||
| 'feed_reply_button_open_thread'
|
| 'feed_reply_button_open_thread'
|
||||||
| 'is_bsky_team_member' // special, do not remove
|
| 'is_bsky_team_member' // special, do not remove
|
||||||
|
| 'live_now_beta'
|
||||||
| 'old_postonboarding'
|
| 'old_postonboarding'
|
||||||
| 'onboarding_add_video_feed'
|
| 'onboarding_add_video_feed'
|
||||||
| 'onboarding_suggested_starterpacks'
|
| 'onboarding_suggested_starterpacks'
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ export enum Nux {
|
|||||||
BookmarksAnnouncement = 'BookmarksAnnouncement',
|
BookmarksAnnouncement = 'BookmarksAnnouncement',
|
||||||
FindContactsAnnouncement = 'FindContactsAnnouncement',
|
FindContactsAnnouncement = 'FindContactsAnnouncement',
|
||||||
FindContactsDismissibleBanner = 'FindContactsDismissibleBanner',
|
FindContactsDismissibleBanner = 'FindContactsDismissibleBanner',
|
||||||
|
LiveNowBetaDialog = 'LiveNowBetaDialog',
|
||||||
|
LiveNowBetaNudge = 'LiveNowBetaNudge',
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Blocking announcements. New IDs are required for each new announcement.
|
* Blocking announcements. New IDs are required for each new announcement.
|
||||||
@@ -62,6 +64,14 @@ export type AppNux = BaseNux<
|
|||||||
id: Nux.FindContactsDismissibleBanner
|
id: Nux.FindContactsDismissibleBanner
|
||||||
data: undefined
|
data: undefined
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
id: Nux.LiveNowBetaDialog
|
||||||
|
data: undefined
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
id: Nux.LiveNowBetaNudge
|
||||||
|
data: undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
|
|
||||||
export const NuxSchemas: Record<Nux, zod.ZodObject<any> | undefined> = {
|
export const NuxSchemas: Record<Nux, zod.ZodObject<any> | undefined> = {
|
||||||
@@ -75,4 +85,6 @@ export const NuxSchemas: Record<Nux, zod.ZodObject<any> | undefined> = {
|
|||||||
[Nux.BookmarksAnnouncement]: undefined,
|
[Nux.BookmarksAnnouncement]: undefined,
|
||||||
[Nux.FindContactsAnnouncement]: undefined,
|
[Nux.FindContactsAnnouncement]: undefined,
|
||||||
[Nux.FindContactsDismissibleBanner]: undefined,
|
[Nux.FindContactsDismissibleBanner]: undefined,
|
||||||
|
[Nux.LiveNowBetaDialog]: undefined,
|
||||||
|
[Nux.LiveNowBetaNudge]: undefined,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,16 +93,10 @@ export function useLiveNowConfig(): LiveNowConfig {
|
|||||||
const ctx = useContext(LiveNowContext)
|
const ctx = useContext(LiveNowContext)
|
||||||
const canGoLive = useCanGoLive()
|
const canGoLive = useCanGoLive()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
if (!canGoLive) return {allowedDomains: []}
|
if (!currentAccount?.did || !canGoLive) return {allowedDomains: []}
|
||||||
if (!currentAccount?.did) return {allowedDomains: []}
|
|
||||||
const vip = ctx.find(live => live.did === currentAccount.did)
|
const vip = ctx.find(live => live.did === currentAccount.did)
|
||||||
if (vip) {
|
|
||||||
return {
|
return {
|
||||||
allowedDomains: vip.domains.concat(DEFAULT_LIVE_ALLOWED_DOMAINS),
|
allowedDomains: DEFAULT_LIVE_ALLOWED_DOMAINS.concat(vip ? vip.domains : []),
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
allowedDomains: DEFAULT_LIVE_ALLOWED_DOMAINS,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,9 +104,7 @@ export function useCanGoLive() {
|
|||||||
const gate = useGate()
|
const gate = useGate()
|
||||||
const {hasSession} = useSession()
|
const {hasSession} = useSession()
|
||||||
if (!hasSession) return false
|
if (!hasSession) return false
|
||||||
const isInBeta = IS_DEV ? true : gate('live_now_beta')
|
return IS_DEV ? true : gate('live_now_beta')
|
||||||
if (!isInBeta) return false
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCheckEmailConfirmed() {
|
export function useCheckEmailConfirmed() {
|
||||||
|
|||||||
@@ -954,7 +954,7 @@ let PostFeed = ({
|
|||||||
const actor = post.author
|
const actor = post.author
|
||||||
if (
|
if (
|
||||||
actor.status &&
|
actor.status &&
|
||||||
validateStatus(actor.did, actor.status, liveNowConfig) &&
|
validateStatus(actor.status, liveNowConfig) &&
|
||||||
isStatusStillActive(actor.status.expiresAt)
|
isStatusStillActive(actor.status.expiresAt)
|
||||||
) {
|
) {
|
||||||
if (!seenActorWithStatusRef.current.has(actor.did)) {
|
if (!seenActorWithStatusRef.current.has(actor.did)) {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {logger} from '#/logger'
|
|||||||
import {isWeb} from '#/platform/detection'
|
import {isWeb} from '#/platform/detection'
|
||||||
import {type Shadow} from '#/state/cache/types'
|
import {type Shadow} from '#/state/cache/types'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
|
import {Nux, useNux, useSaveNux} from '#/state/queries/nuxs'
|
||||||
import {
|
import {
|
||||||
RQKEY as profileQueryKey,
|
RQKEY as profileQueryKey,
|
||||||
useProfileBlockMutationQueue,
|
useProfileBlockMutationQueue,
|
||||||
@@ -25,6 +26,7 @@ import {useCanGoLive} from '#/state/service-config'
|
|||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {EventStopper} from '#/view/com/util/EventStopper'
|
import {EventStopper} from '#/view/com/util/EventStopper'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Button, ButtonIcon} from '#/components/Button'
|
import {Button, ButtonIcon} from '#/components/Button'
|
||||||
import {useDialogControl} from '#/components/Dialog'
|
import {useDialogControl} from '#/components/Dialog'
|
||||||
import {StarterPackDialog} from '#/components/dialogs/StarterPackDialog'
|
import {StarterPackDialog} from '#/components/dialogs/StarterPackDialog'
|
||||||
@@ -59,6 +61,8 @@ import * as Prompt from '#/components/Prompt'
|
|||||||
import {useFullVerificationState} from '#/components/verification'
|
import {useFullVerificationState} from '#/components/verification'
|
||||||
import {VerificationCreatePrompt} from '#/components/verification/VerificationCreatePrompt'
|
import {VerificationCreatePrompt} from '#/components/verification/VerificationCreatePrompt'
|
||||||
import {VerificationRemovePrompt} from '#/components/verification/VerificationRemovePrompt'
|
import {VerificationRemovePrompt} from '#/components/verification/VerificationRemovePrompt'
|
||||||
|
import {Dot} from '#/features/nuxs/components/Dot'
|
||||||
|
import {Gradient} from '#/features/nuxs/components/Gradient'
|
||||||
import {useDevMode} from '#/storage/hooks/dev-mode'
|
import {useDevMode} from '#/storage/hooks/dev-mode'
|
||||||
|
|
||||||
let ProfileMenu = ({
|
let ProfileMenu = ({
|
||||||
@@ -66,6 +70,7 @@ let ProfileMenu = ({
|
|||||||
}: {
|
}: {
|
||||||
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
|
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
|
||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {currentAccount, hasSession} = useSession()
|
const {currentAccount, hasSession} = useSession()
|
||||||
const {openModal} = useModalControls()
|
const {openModal} = useModalControls()
|
||||||
@@ -79,8 +84,12 @@ let ProfileMenu = ({
|
|||||||
const isLabelerAndNotBlocked = !!profile.associated?.labeler && !isBlocked
|
const isLabelerAndNotBlocked = !!profile.associated?.labeler && !isBlocked
|
||||||
const [devModeEnabled] = useDevMode()
|
const [devModeEnabled] = useDevMode()
|
||||||
const verification = useFullVerificationState({profile})
|
const verification = useFullVerificationState({profile})
|
||||||
const canGoLive = useCanGoLive(currentAccount?.did)
|
const canGoLive = useCanGoLive()
|
||||||
const status = useActorStatus(profile)
|
const status = useActorStatus(profile)
|
||||||
|
const statusNudge = useNux(Nux.LiveNowBetaNudge)
|
||||||
|
const statusNudgeActive =
|
||||||
|
canGoLive && statusNudge.status === 'ready' && !statusNudge.nux?.completed
|
||||||
|
const {mutate: saveNux} = useSaveNux()
|
||||||
|
|
||||||
const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile)
|
const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile)
|
||||||
const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
|
const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
|
||||||
@@ -229,6 +238,7 @@ let ProfileMenu = ({
|
|||||||
<Menu.Trigger label={_(msg`More options`)}>
|
<Menu.Trigger label={_(msg`More options`)}>
|
||||||
{({props}) => {
|
{({props}) => {
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<Button
|
<Button
|
||||||
{...props}
|
{...props}
|
||||||
testID="profileHeaderDropdownBtn"
|
testID="profileHeaderDropdownBtn"
|
||||||
@@ -238,8 +248,12 @@ let ProfileMenu = ({
|
|||||||
color="secondary"
|
color="secondary"
|
||||||
size="small"
|
size="small"
|
||||||
shape="round">
|
shape="round">
|
||||||
|
<Gradient />
|
||||||
<ButtonIcon icon={Ellipsis} size="sm" />
|
<ButtonIcon icon={Ellipsis} size="sm" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{statusNudgeActive && <Dot top={1} right={1} />}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</Menu.Trigger>
|
</Menu.Trigger>
|
||||||
@@ -337,11 +351,19 @@ let ProfileMenu = ({
|
|||||||
? _(msg`Edit live status`)
|
? _(msg`Edit live status`)
|
||||||
: _(msg`Go live`)
|
: _(msg`Go live`)
|
||||||
}
|
}
|
||||||
onPress={
|
onPress={() => {
|
||||||
status.isDisabled
|
if (status.isDisabled) {
|
||||||
? goLiveDisabledDialogControl.open
|
goLiveDisabledDialogControl.open()
|
||||||
: goLiveDialogControl.open
|
} else {
|
||||||
}>
|
goLiveDialogControl.open()
|
||||||
|
}
|
||||||
|
saveNux({
|
||||||
|
id: Nux.LiveNowBetaNudge,
|
||||||
|
data: undefined,
|
||||||
|
completed: true,
|
||||||
|
})
|
||||||
|
}}>
|
||||||
|
<Gradient />
|
||||||
<Menu.ItemText>
|
<Menu.ItemText>
|
||||||
{status.isDisabled ? (
|
{status.isDisabled ? (
|
||||||
<Trans>Go live (disabled)</Trans>
|
<Trans>Go live (disabled)</Trans>
|
||||||
@@ -351,7 +373,26 @@ let ProfileMenu = ({
|
|||||||
<Trans>Go live</Trans>
|
<Trans>Go live</Trans>
|
||||||
)}
|
)}
|
||||||
</Menu.ItemText>
|
</Menu.ItemText>
|
||||||
<Menu.ItemIcon icon={LiveIcon} />
|
{statusNudgeActive && (
|
||||||
|
<Menu.ItemText
|
||||||
|
style={[
|
||||||
|
a.flex_0,
|
||||||
|
{
|
||||||
|
color: t.palette.primary_500,
|
||||||
|
right: isWeb ? -8 : -4,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<Trans>New</Trans>
|
||||||
|
</Menu.ItemText>
|
||||||
|
)}
|
||||||
|
<Menu.ItemIcon
|
||||||
|
icon={LiveIcon}
|
||||||
|
fill={
|
||||||
|
statusNudgeActive
|
||||||
|
? () => t.palette.primary_500
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
)}
|
)}
|
||||||
{verification.viewer.role === 'verifier' &&
|
{verification.viewer.role === 'verifier' &&
|
||||||
|
|||||||
Reference in New Issue
Block a user