Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 052a3e4f41 | |||
| 29f8bc998d | |||
| 4760e59ea4 | |||
| d9e37a222a | |||
| 3b98f2f8ab | |||
| ab85b509c0 | |||
| 65faee4f08 | |||
| 94be661794 | |||
| 10aa46aa9e | |||
| 8f82ad66df | |||
| 1cb362b596 | |||
| f960d2fbc6 |
@@ -37,14 +37,6 @@
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
html {
|
||||
background-color: white;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background-color: black;
|
||||
}
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0px;
|
||||
@@ -59,6 +51,19 @@
|
||||
-ms-overflow-style: scrollbar;
|
||||
font-synthesis-weight: none;
|
||||
}
|
||||
:root {
|
||||
--text: black;
|
||||
--background: white;
|
||||
--backgroundLight: #e2e7ee;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--text: white;
|
||||
--background: black;
|
||||
--backgroundLight: #232e3e;
|
||||
}
|
||||
}
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
@@ -67,6 +72,32 @@
|
||||
min-height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
html.theme--light,
|
||||
html.theme--light body,
|
||||
html.theme--light #root {
|
||||
background-color: white;
|
||||
--text: black;
|
||||
--background: white;
|
||||
--backgroundLight: #DCE2EA;
|
||||
}
|
||||
html.theme--dark,
|
||||
html.theme--dark body,
|
||||
html.theme--dark #root {
|
||||
color-scheme: dark;
|
||||
background-color: black;
|
||||
--text: white;
|
||||
--background: black;
|
||||
--backgroundLight: #232E3E;
|
||||
}
|
||||
html.theme--dim,
|
||||
html.theme--dim body,
|
||||
html.theme--dim #root {
|
||||
color-scheme: dark;
|
||||
background-color: #151D28;
|
||||
--text: white;
|
||||
--background: #151D28;
|
||||
--backgroundLight: #2C3A4E;
|
||||
}
|
||||
#splash {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
@@ -93,6 +124,12 @@
|
||||
overflow-y: scroll;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
const theme = localStorage.getItem('ALF_THEME')
|
||||
if (theme) {
|
||||
document.documentElement.classList.add(`theme--${theme}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
{% include "scripts.html" %}
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ staticCDNHost }}/static/apple-touch-icon.png">
|
||||
|
||||
@@ -156,6 +156,7 @@
|
||||
"expo-location": "~19.0.8",
|
||||
"expo-media-library": "~18.2.1",
|
||||
"expo-notifications": "~0.32.14",
|
||||
"expo-privacy-sensitive": "^0.1.0",
|
||||
"expo-screen-orientation": "~9.0.8",
|
||||
"expo-sharing": "~14.0.8",
|
||||
"expo-sms": "^14.0.7",
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
diff --git a/node_modules/expo-image/build/Image.types.d.ts b/node_modules/expo-image/build/Image.types.d.ts
|
||||
index 022ae48..416504f 100644
|
||||
--- a/node_modules/expo-image/build/Image.types.d.ts
|
||||
+++ b/node_modules/expo-image/build/Image.types.d.ts
|
||||
@@ -152,6 +152,16 @@ export interface ImageProps extends Omit<ViewProps, 'style' | 'children'> {
|
||||
* @default 'normal'
|
||||
*/
|
||||
priority?: 'low' | 'normal' | 'high' | null;
|
||||
+ /**
|
||||
+ * The loading behavior for the image. Maps to the native HTML `loading` attribute on web.
|
||||
+ *
|
||||
+ * - `'lazy'` - Defers loading until the image is near the viewport.
|
||||
+ * - `'eager'` - Loads the image immediately.
|
||||
+ *
|
||||
+ * @default undefined
|
||||
+ * @platform web
|
||||
+ */
|
||||
+ loading?: 'lazy' | 'eager' | null;
|
||||
/**
|
||||
* Determines whether to cache the image and where: on the disk, in the memory or both.
|
||||
*
|
||||
diff --git a/node_modules/expo-image/src/ExpoImage.web.tsx b/node_modules/expo-image/src/ExpoImage.web.tsx
|
||||
index 2a49ff0..1c3de93 100644
|
||||
--- a/node_modules/expo-image/src/ExpoImage.web.tsx
|
||||
+++ b/node_modules/expo-image/src/ExpoImage.web.tsx
|
||||
@@ -70,6 +70,7 @@ export default function ExpoImage({
|
||||
onLoadEnd,
|
||||
onDisplay,
|
||||
priority,
|
||||
+ loading,
|
||||
blurRadius,
|
||||
recyclingKey,
|
||||
style,
|
||||
@@ -118,6 +119,7 @@ export default function ExpoImage({
|
||||
accessibilityLabel={accessibilityLabel ?? alt}
|
||||
cachePolicy={cachePolicy}
|
||||
priority={priority}
|
||||
+ loading={loading}
|
||||
tintColor={tintColor}
|
||||
/>
|
||||
),
|
||||
@@ -149,6 +151,7 @@ export default function ExpoImage({
|
||||
className={className}
|
||||
cachePolicy={cachePolicy}
|
||||
priority={priority}
|
||||
+ loading={loading}
|
||||
contentPosition={selectedSource ? contentPosition : { top: '50%', left: '50%' }}
|
||||
hashPlaceholderContentPosition={contentPosition}
|
||||
hashPlaceholderStyle={imageHashStyle}
|
||||
diff --git a/node_modules/expo-image/src/Image.types.ts b/node_modules/expo-image/src/Image.types.ts
|
||||
index 9dec0e7..61c1621 100644
|
||||
--- a/node_modules/expo-image/src/Image.types.ts
|
||||
+++ b/node_modules/expo-image/src/Image.types.ts
|
||||
@@ -178,6 +178,17 @@ export interface ImageProps extends Omit<ViewProps, 'style' | 'children'> {
|
||||
*/
|
||||
priority?: 'low' | 'normal' | 'high' | null;
|
||||
|
||||
+ /**
|
||||
+ * The loading behavior for the image. Maps to the native HTML `loading` attribute on web.
|
||||
+ *
|
||||
+ * - `'lazy'` - Defers loading until the image is near the viewport.
|
||||
+ * - `'eager'` - Loads the image immediately.
|
||||
+ *
|
||||
+ * @default undefined
|
||||
+ * @platform web
|
||||
+ */
|
||||
+ loading?: 'lazy' | 'eager' | null;
|
||||
+
|
||||
/**
|
||||
* Determines whether to cache the image and where: on the disk, in the memory or both.
|
||||
*
|
||||
diff --git a/node_modules/expo-image/src/web/ImageWrapper.tsx b/node_modules/expo-image/src/web/ImageWrapper.tsx
|
||||
index e8f891d..89a5cb1 100644
|
||||
--- a/node_modules/expo-image/src/web/ImageWrapper.tsx
|
||||
+++ b/node_modules/expo-image/src/web/ImageWrapper.tsx
|
||||
@@ -30,6 +30,7 @@ const ImageWrapper = React.forwardRef(
|
||||
contentPosition,
|
||||
hashPlaceholderContentPosition,
|
||||
priority,
|
||||
+ loading,
|
||||
style,
|
||||
hashPlaceholderStyle,
|
||||
tintColor,
|
||||
@@ -82,6 +83,7 @@ const ImageWrapper = React.forwardRef(
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line react/no-unknown-property
|
||||
fetchPriority={getFetchPriorityFromImagePriority(priority || 'normal')}
|
||||
+ loading={loading || undefined}
|
||||
{...getImageWrapperEventHandler(events, sourceWithHeaders)}
|
||||
{...getImgPropsFromSource(source)}
|
||||
{...props}
|
||||
diff --git a/node_modules/expo-image/src/web/ImageWrapper.types.ts b/node_modules/expo-image/src/web/ImageWrapper.types.ts
|
||||
index 19bbe2f..179837f 100644
|
||||
--- a/node_modules/expo-image/src/web/ImageWrapper.types.ts
|
||||
+++ b/node_modules/expo-image/src/web/ImageWrapper.types.ts
|
||||
@@ -29,6 +29,7 @@ export type ImageWrapperProps = {
|
||||
contentPosition?: ImageContentPositionObject;
|
||||
hashPlaceholderContentPosition?: ImageContentPositionObject;
|
||||
priority?: string | null;
|
||||
+ loading?: 'lazy' | 'eager' | null;
|
||||
style: CSSProperties;
|
||||
tintColor?: string | null;
|
||||
hashPlaceholderStyle?: CSSProperties;
|
||||
@@ -12,12 +12,12 @@ export const enabled = (IS_DEV && false) || IS_E2E
|
||||
|
||||
export const geolocation: Geolocation | undefined = enabled
|
||||
? {
|
||||
countryCode: 'BB',
|
||||
countryCode: 'AA',
|
||||
regionCode: undefined,
|
||||
}
|
||||
: undefined
|
||||
|
||||
const deviceGeolocationEnabled = false
|
||||
const deviceGeolocationEnabled = false || IS_E2E
|
||||
export const deviceGeolocation: Geolocation | undefined =
|
||||
enabled && deviceGeolocationEnabled
|
||||
? {
|
||||
@@ -46,7 +46,7 @@ export const config: AppBskyAgeassuranceDefs.Config = {
|
||||
rules: [
|
||||
{
|
||||
$type: ids.Default,
|
||||
access: 'none',
|
||||
access: 'full',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -51,6 +51,7 @@ function updateDocument(theme: ThemeName) {
|
||||
html.classList.add(`theme--${theme}`)
|
||||
// set color to 'theme-color' meta tag
|
||||
meta?.setAttribute('content', getBackgroundColor(theme))
|
||||
window.localStorage.setItem('ALF_THEME', theme)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -226,6 +226,7 @@ export function ExternalPlayer({
|
||||
style={[a.flex_1]}
|
||||
source={{uri: link.thumb}}
|
||||
accessibilityIgnoresInvertColors
|
||||
loading="lazy"
|
||||
/>
|
||||
<Fill
|
||||
style={[
|
||||
|
||||
@@ -100,6 +100,7 @@ export const ExternalEmbed = ({
|
||||
style={[a.aspect_card]}
|
||||
source={{uri: imageUri}}
|
||||
accessibilityIgnoresInvertColors
|
||||
loading="lazy"
|
||||
/>
|
||||
) : undefined}
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ import {Image} from 'expo-image'
|
||||
|
||||
import {useLightboxControls} from '#/state/lightbox'
|
||||
import {type Dimensions} from '#/view/com/lightbox/ImageViewing/@types'
|
||||
import {AutoSizedImage} from '#/view/com/util/images/AutoSizedImage'
|
||||
import {ImageLayoutGrid} from '#/view/com/util/images/ImageLayoutGrid'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {AutoSizedImage} from '#/components/images/AutoSizedImage'
|
||||
import {ImageLayoutGrid} from '#/components/images/ImageLayoutGrid'
|
||||
import {PostEmbedViewContext} from '#/components/Post/Embed/types'
|
||||
import {type EmbedType} from '#/types/bsky/post'
|
||||
import {type CommonProps} from './types'
|
||||
|
||||
@@ -6,10 +6,10 @@ import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
|
||||
import {ConstrainedImage} from '#/view/com/util/images/AutoSizedImage'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import {useThrottledValue} from '#/components/hooks/useThrottledValue'
|
||||
import {ConstrainedImage} from '#/components/images/AutoSizedImage'
|
||||
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
|
||||
import {VideoEmbedInnerNative} from './VideoEmbedInner/VideoEmbedInnerNative'
|
||||
import * as VideoFallback from './VideoEmbedInner/VideoFallback'
|
||||
|
||||
@@ -13,10 +13,10 @@ import {useLingui} from '@lingui/react'
|
||||
|
||||
import {isFirefox} from '#/lib/browser'
|
||||
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
|
||||
import {ConstrainedImage} from '#/view/com/util/images/AutoSizedImage'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {useIsWithinMessage} from '#/components/dms/MessageContext'
|
||||
import {useFullscreen} from '#/components/hooks/useFullscreen'
|
||||
import {ConstrainedImage} from '#/components/images/AutoSizedImage'
|
||||
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||
import {
|
||||
HLSUnsupportedError,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {lazy} from 'react'
|
||||
import {lazy, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
// @ts-expect-error missing types
|
||||
import QRCode from 'react-native-qrcode-styled'
|
||||
@@ -102,39 +102,74 @@ export function QrCode({
|
||||
|
||||
export function QrCodeInner({link}: {link: string}) {
|
||||
const t = useTheme()
|
||||
const [logoArea, setLogoArea] = useState<{
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
} | null>(null)
|
||||
|
||||
const onLogoAreaChange = (area: {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
}) => {
|
||||
setLogoArea(area)
|
||||
}
|
||||
|
||||
return (
|
||||
<QRCode
|
||||
data={link}
|
||||
style={[
|
||||
a.rounded_sm,
|
||||
{height: 225, width: 225, backgroundColor: '#f3f3f3'},
|
||||
]}
|
||||
pieceSize={isWeb ? 8 : 6}
|
||||
padding={20}
|
||||
// pieceLiquidRadius={2}
|
||||
pieceBorderRadius={isWeb ? 4.5 : 3.5}
|
||||
outerEyesOptions={{
|
||||
topLeft: {
|
||||
borderRadius: [12, 12, 0, 12],
|
||||
color: t.palette.primary_500,
|
||||
},
|
||||
topRight: {
|
||||
borderRadius: [12, 12, 12, 0],
|
||||
color: t.palette.primary_500,
|
||||
},
|
||||
bottomLeft: {
|
||||
borderRadius: [12, 0, 12, 12],
|
||||
color: t.palette.primary_500,
|
||||
},
|
||||
}}
|
||||
innerEyesOptions={{borderRadius: 3}}
|
||||
logo={{
|
||||
href: require('../../../assets/logo.png'),
|
||||
scale: 0.95,
|
||||
padding: 2,
|
||||
hidePieces: true,
|
||||
}}
|
||||
/>
|
||||
<View style={{position: 'relative'}}>
|
||||
{/* An SVG version of the logo is placed on top of normal `QRCode` `logo` prop, since the PNG fails to load before the export completes on web. */}
|
||||
{isWeb && logoArea && (
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: logoArea.x,
|
||||
top: logoArea.y + 1,
|
||||
zIndex: 1,
|
||||
padding: 4,
|
||||
}}>
|
||||
<Logo width={logoArea.width - 14} height={logoArea.height - 14} />
|
||||
</View>
|
||||
)}
|
||||
<QRCode
|
||||
data={link}
|
||||
style={[
|
||||
a.rounded_sm,
|
||||
{height: 225, width: 225, backgroundColor: '#f3f3f3'},
|
||||
]}
|
||||
pieceSize={isWeb ? 8 : 6}
|
||||
padding={20}
|
||||
pieceBorderRadius={isWeb ? 4.5 : 3.5}
|
||||
outerEyesOptions={{
|
||||
topLeft: {
|
||||
borderRadius: [12, 12, 0, 12],
|
||||
color: t.palette.primary_500,
|
||||
},
|
||||
topRight: {
|
||||
borderRadius: [12, 12, 12, 0],
|
||||
color: t.palette.primary_500,
|
||||
},
|
||||
bottomLeft: {
|
||||
borderRadius: [12, 0, 12, 12],
|
||||
color: t.palette.primary_500,
|
||||
},
|
||||
}}
|
||||
innerEyesOptions={{borderRadius: 3}}
|
||||
logo={{
|
||||
href: require('../../../assets/logo.png'),
|
||||
...(isWeb && {
|
||||
onChange: onLogoAreaChange,
|
||||
padding: 28,
|
||||
}),
|
||||
...(!isWeb && {
|
||||
padding: 2,
|
||||
scale: 0.95,
|
||||
}),
|
||||
hidePieces: true,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -68,10 +68,12 @@ export function SubscribeProfileButton({
|
||||
|
||||
const Icon = isSubscribed ? BellRingingIcon : BellPlusIcon
|
||||
|
||||
const tooltipVisible = showTooltip && !disableHint
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip.Outer
|
||||
visible={showTooltip && !disableHint}
|
||||
visible={tooltipVisible}
|
||||
onVisibleChange={onDismissTooltip}
|
||||
position="bottom">
|
||||
<Tooltip.Target>
|
||||
@@ -79,7 +81,7 @@ export function SubscribeProfileButton({
|
||||
accessibilityRole="button"
|
||||
testID="dmBtn"
|
||||
size="small"
|
||||
color="secondary"
|
||||
color={tooltipVisible ? 'primary_subtle' : 'secondary'}
|
||||
shape="round"
|
||||
label={_(msg`Get notified when ${name} posts`)}
|
||||
onPress={wrappedOnPress}>
|
||||
|
||||
+3
-2
@@ -1,4 +1,4 @@
|
||||
import React, {useRef} from 'react'
|
||||
import {useMemo, useRef} from 'react'
|
||||
import {type DimensionValue, Pressable, View} from 'react-native'
|
||||
import Animated, {
|
||||
type AnimatedRef,
|
||||
@@ -34,7 +34,7 @@ export function ConstrainedImage({
|
||||
* Computed as a % value to apply as `paddingTop`, this basically controls
|
||||
* the height of the image.
|
||||
*/
|
||||
const outerAspectRatio = React.useMemo<DimensionValue>(() => {
|
||||
const outerAspectRatio = useMemo<DimensionValue>(() => {
|
||||
const ratio = isNative
|
||||
? Math.min(1 / aspectRatio, minMobileAspectRatio ?? 16 / 9) // 9:16 bounding box
|
||||
: Math.min(1 / aspectRatio, 1) // 1:1 bounding box
|
||||
@@ -127,6 +127,7 @@ export function AutoSizedImage({
|
||||
}
|
||||
}
|
||||
}}
|
||||
loading="lazy"
|
||||
/>
|
||||
<MediaInsetBorder />
|
||||
|
||||
@@ -29,7 +29,7 @@ interface Props {
|
||||
viewContext?: PostEmbedViewContext
|
||||
insetBorderStyle?: StyleProp<ViewStyle>
|
||||
containerRefs: AnimatedRef<any>[]
|
||||
thumbDimsRef: React.MutableRefObject<(Dimensions | null)[]>
|
||||
thumbDimsRef: React.RefObject<(Dimensions | null)[]>
|
||||
}
|
||||
|
||||
export function GalleryItem({
|
||||
@@ -87,6 +87,7 @@ export function GalleryItem({
|
||||
height: e.source.height,
|
||||
}
|
||||
}}
|
||||
loading="lazy"
|
||||
/>
|
||||
<MediaInsetBorder style={insetBorderStyle} />
|
||||
</Pressable>
|
||||
+3
-3
@@ -1,11 +1,11 @@
|
||||
import React from 'react'
|
||||
import {useRef} from 'react'
|
||||
import {type StyleProp, View, type ViewStyle} from 'react-native'
|
||||
import {type AnimatedRef, useAnimatedRef} from 'react-native-reanimated'
|
||||
import {type AppBskyEmbedImages} from '@atproto/api'
|
||||
|
||||
import {type Dimensions} from '#/view/com/lightbox/ImageViewing/@types'
|
||||
import {atoms as a, useBreakpoints} from '#/alf'
|
||||
import {PostEmbedViewContext} from '#/components/Post/Embed/types'
|
||||
import {type Dimensions} from '../../lightbox/ImageViewing/@types'
|
||||
import {GalleryItem} from './Gallery'
|
||||
|
||||
interface ImageLayoutGridProps {
|
||||
@@ -60,7 +60,7 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
|
||||
const containerRef2 = useAnimatedRef()
|
||||
const containerRef3 = useAnimatedRef()
|
||||
const containerRef4 = useAnimatedRef()
|
||||
const thumbDimsRef = React.useRef<(Dimensions | null)[]>([])
|
||||
const thumbDimsRef = useRef<(Dimensions | null)[]>([])
|
||||
|
||||
switch (count) {
|
||||
case 2: {
|
||||
@@ -241,6 +241,10 @@ export const BLUESKY_MOD_SERVICE_HEADERS = {
|
||||
'atproto-proxy': `${BSKY_LABELER_DID}#atproto_labeler`,
|
||||
}
|
||||
|
||||
export const BLUESKY_NOTIF_SERVICE_HEADERS = {
|
||||
'atproto-proxy': `${BLUESKY_PROXY_DID}#bsky_notif`,
|
||||
}
|
||||
|
||||
export const webLinks = {
|
||||
tos: `https://bsky.social/about/support/tos`,
|
||||
privacy: `https://bsky.social/about/support/privacy-policy`,
|
||||
|
||||
@@ -2,10 +2,15 @@ import {useCallback, useEffect} from 'react'
|
||||
import {Platform} from 'react-native'
|
||||
import * as Notifications from 'expo-notifications'
|
||||
import {getBadgeCountAsync, setBadgeCountAsync} from 'expo-notifications'
|
||||
import {type AppBskyNotificationRegisterPush, type AtpAgent} from '@atproto/api'
|
||||
import {type AtpAgent} from '@atproto/api'
|
||||
import {type AppBskyNotificationRegisterPush} from '@atproto/api'
|
||||
import debounce from 'lodash.debounce'
|
||||
|
||||
import {PUBLIC_APPVIEW_DID, PUBLIC_STAGING_APPVIEW_DID} from '#/lib/constants'
|
||||
import {
|
||||
BLUESKY_NOTIF_SERVICE_HEADERS,
|
||||
PUBLIC_APPVIEW_DID,
|
||||
PUBLIC_STAGING_APPVIEW_DID,
|
||||
} from '#/lib/constants'
|
||||
import {logger as notyLogger} from '#/lib/notifications/util'
|
||||
import {isNetworkError} from '#/lib/strings/errors'
|
||||
import {isNative} from '#/platform/detection'
|
||||
@@ -44,7 +49,9 @@ async function _registerPushToken({
|
||||
|
||||
notyLogger.debug(`registerPushToken: registering`, {...payload})
|
||||
|
||||
await agent.app.bsky.notification.registerPush(payload)
|
||||
await agent.app.bsky.notification.registerPush(payload, {
|
||||
headers: BLUESKY_NOTIF_SERVICE_HEADERS,
|
||||
})
|
||||
|
||||
notyLogger.debug(`registerPushToken: success`)
|
||||
} catch (error) {
|
||||
@@ -286,3 +293,33 @@ export async function resetBadgeCount() {
|
||||
await BackgroundNotificationHandler.setBadgeCountAsync(0)
|
||||
await setBadgeCountAsync(0)
|
||||
}
|
||||
|
||||
export async function unregisterPushToken(agents: AtpAgent[]) {
|
||||
if (!isNative) return
|
||||
|
||||
try {
|
||||
const token = await getPushToken()
|
||||
if (token) {
|
||||
for (const agent of agents) {
|
||||
await agent.app.bsky.notification.unregisterPush(
|
||||
{
|
||||
serviceDid: agent.serviceUrl.hostname.includes('staging')
|
||||
? PUBLIC_STAGING_APPVIEW_DID
|
||||
: PUBLIC_APPVIEW_DID,
|
||||
platform: Platform.OS,
|
||||
token: token.data,
|
||||
appId: 'xyz.blueskyweb.app',
|
||||
},
|
||||
{
|
||||
headers: BLUESKY_NOTIF_SERVICE_HEADERS,
|
||||
},
|
||||
)
|
||||
notyLogger.debug(`Push token unregistered for ${agent.session?.handle}`)
|
||||
}
|
||||
} else {
|
||||
notyLogger.debug('Tried to unregister push token, but could not find one')
|
||||
}
|
||||
} catch (error) {
|
||||
notyLogger.debug('Failed to unregister push token', {message: error})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -506,7 +506,7 @@ msgstr ""
|
||||
msgid "<0>{date}</0> at {time}"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Hashtag.tsx:225
|
||||
#: src/screens/Hashtag.tsx:238
|
||||
#: src/screens/Search/SearchResults.tsx:294
|
||||
msgid "<0>Sign in</0><1> or </1><2>create an account</2><3> </3><4>to search for news, sports, politics, and everything else happening on Bluesky.</4>"
|
||||
msgstr ""
|
||||
@@ -1830,6 +1830,10 @@ msgstr ""
|
||||
msgid "Captions & alt text"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/RichTextTag.tsx:52
|
||||
msgid "Cashtag {tag}"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Settings/components/Email2FAToggle.tsx:31
|
||||
msgid "Change"
|
||||
msgstr ""
|
||||
@@ -2062,8 +2066,8 @@ msgstr ""
|
||||
msgid "Click here to update your email"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/RichTextTag.tsx:54
|
||||
msgid "Click to open tag menu for {tag}"
|
||||
#: src/components/RichTextTag.tsx:55
|
||||
msgid "Click to open tag menu for {0}"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/dms/MessageItem.tsx:318
|
||||
@@ -2662,7 +2666,7 @@ msgstr ""
|
||||
|
||||
#: src/components/dialogs/Signin.tsx:86
|
||||
#: src/components/dialogs/Signin.tsx:88
|
||||
#: src/screens/Hashtag.tsx:234
|
||||
#: src/screens/Hashtag.tsx:247
|
||||
#: src/screens/Search/SearchResults.tsx:303
|
||||
msgid "Create an account"
|
||||
msgstr ""
|
||||
@@ -4088,7 +4092,7 @@ msgstr ""
|
||||
#: src/components/ProfileCard.tsx:559
|
||||
#: src/components/ProfileHoverCard/index.web.tsx:497
|
||||
#: src/components/ProfileHoverCard/index.web.tsx:508
|
||||
#: src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx:131
|
||||
#: src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx:140
|
||||
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:378
|
||||
#: src/screens/VideoFeed/index.tsx:879
|
||||
#: src/view/com/notifications/NotificationFeedItem.tsx:841
|
||||
@@ -4096,7 +4100,7 @@ msgstr ""
|
||||
msgid "Follow"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx:113
|
||||
#: src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx:127
|
||||
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:366
|
||||
msgid "Follow {0}"
|
||||
msgstr ""
|
||||
@@ -4140,7 +4144,7 @@ msgstr ""
|
||||
|
||||
#. User is not following this account, click to follow back
|
||||
#: src/components/ProfileCard.tsx:553
|
||||
#: src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx:129
|
||||
#: src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx:138
|
||||
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:376
|
||||
#: src/view/com/notifications/NotificationFeedItem.tsx:841
|
||||
#: src/view/com/notifications/NotificationFeedItem.tsx:848
|
||||
@@ -4185,7 +4189,7 @@ msgstr ""
|
||||
#: src/components/ProfileCard.tsx:546
|
||||
#: src/components/ProfileHoverCard/index.web.tsx:496
|
||||
#: src/components/ProfileHoverCard/index.web.tsx:507
|
||||
#: src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx:134
|
||||
#: src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx:143
|
||||
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:374
|
||||
#: src/screens/VideoFeed/index.tsx:877
|
||||
#: src/view/com/notifications/NotificationFeedItem.tsx:819
|
||||
@@ -4289,7 +4293,7 @@ msgstr ""
|
||||
msgid "From"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Hashtag.tsx:127
|
||||
#: src/screens/Hashtag.tsx:136
|
||||
msgid "From @{sanitizedAuthor}"
|
||||
msgstr ""
|
||||
|
||||
@@ -4338,7 +4342,7 @@ msgstr ""
|
||||
msgid "Get notifications when people repost your posts."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/activity-notifications/SubscribeProfileButton.tsx:91
|
||||
#: src/components/activity-notifications/SubscribeProfileButton.tsx:93
|
||||
msgid "Get notified about new posts"
|
||||
msgstr ""
|
||||
|
||||
@@ -4354,7 +4358,7 @@ msgstr ""
|
||||
msgid "Get notified of this account’s activity"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/activity-notifications/SubscribeProfileButton.tsx:84
|
||||
#: src/components/activity-notifications/SubscribeProfileButton.tsx:86
|
||||
msgid "Get notified when {name} posts"
|
||||
msgstr ""
|
||||
|
||||
@@ -4554,7 +4558,7 @@ msgstr ""
|
||||
msgid "Hashtag"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/RichTextTag.tsx:51
|
||||
#: src/components/RichTextTag.tsx:52
|
||||
msgid "Hashtag {tag}"
|
||||
msgstr ""
|
||||
|
||||
@@ -4867,7 +4871,7 @@ msgstr ""
|
||||
msgid "If you're trying to change your handle or email, do so before you deactivate."
|
||||
msgstr ""
|
||||
|
||||
#: src/view/com/util/images/Gallery.tsx:75
|
||||
#: src/components/images/Gallery.tsx:75
|
||||
msgid "Image"
|
||||
msgstr ""
|
||||
|
||||
@@ -5166,7 +5170,7 @@ msgstr ""
|
||||
msgid "Last initiated just now"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Hashtag.tsx:102
|
||||
#: src/screens/Hashtag.tsx:111
|
||||
#: src/screens/Search/SearchResults.tsx:62
|
||||
#: src/screens/Topic.tsx:78
|
||||
msgid "Latest"
|
||||
@@ -5559,8 +5563,8 @@ msgstr ""
|
||||
msgid "Logo by <0>@sawaratsuki.bsky.social</0>"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/RichTextTag.tsx:53
|
||||
msgid "Long press to open tag menu for #{tag}"
|
||||
#: src/components/RichTextTag.tsx:54
|
||||
msgid "Long press to open tag menu for {0}"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Login/SetNewPasswordForm.tsx:122
|
||||
@@ -5806,8 +5810,8 @@ msgctxt "video"
|
||||
msgid "Mute"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/RichTextTag.tsx:141
|
||||
#: src/components/RichTextTag.tsx:154
|
||||
#: src/components/RichTextTag.tsx:150
|
||||
#: src/components/RichTextTag.tsx:163
|
||||
msgid "Mute {tag}"
|
||||
msgstr ""
|
||||
|
||||
@@ -8205,7 +8209,7 @@ msgstr ""
|
||||
msgid "Search GIFs"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Hashtag.tsx:223
|
||||
#: src/screens/Hashtag.tsx:236
|
||||
#: src/screens/Search/SearchResults.tsx:292
|
||||
msgid "Search is currently unavailable when logged out"
|
||||
msgstr ""
|
||||
@@ -8246,19 +8250,27 @@ msgstr ""
|
||||
msgid "Security step required"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/RichTextTag.tsx:112
|
||||
#: src/components/RichTextTag.tsx:113
|
||||
msgid "See {0} posts"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/RichTextTag.tsx:130
|
||||
msgid "See {0} posts by user"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/RichTextTag.tsx:121
|
||||
msgid "See {tag} posts"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/RichTextTag.tsx:125
|
||||
#: src/components/RichTextTag.tsx:139
|
||||
msgid "See {tag} posts by user"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/RichTextTag.tsx:119
|
||||
#: src/components/RichTextTag.tsx:123
|
||||
msgid "See #{tag} posts"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/RichTextTag.tsx:133
|
||||
#: src/components/RichTextTag.tsx:141
|
||||
msgid "See #{tag} posts by user"
|
||||
msgstr ""
|
||||
|
||||
@@ -8611,7 +8623,7 @@ msgid "Sexually Suggestive"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/StarterPack/QrCodeDialog.tsx:192
|
||||
#: src/screens/Hashtag.tsx:133
|
||||
#: src/screens/Hashtag.tsx:142
|
||||
#: src/screens/StarterPack/StarterPackScreen.tsx:433
|
||||
#: src/screens/Topic.tsx:103
|
||||
msgid "Share"
|
||||
@@ -8820,7 +8832,7 @@ msgstr ""
|
||||
#: src/components/dialogs/Signin.tsx:99
|
||||
#: src/components/WelcomeModal.tsx:194
|
||||
#: src/components/WelcomeModal.tsx:206
|
||||
#: src/screens/Hashtag.tsx:227
|
||||
#: src/screens/Hashtag.tsx:240
|
||||
#: src/screens/Login/index.tsx:136
|
||||
#: src/screens/Login/index.tsx:157
|
||||
#: src/screens/Login/LoginForm.tsx:181
|
||||
@@ -9573,8 +9585,8 @@ msgstr ""
|
||||
#: src/components/PostControls/PostMenu/PostMenuItems.tsx:423
|
||||
#: src/components/PostControls/PostMenu/PostMenuItems.tsx:436
|
||||
#: src/components/PostControls/PostMenu/PostMenuItems.tsx:446
|
||||
#: src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx:90
|
||||
#: src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx:101
|
||||
#: src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx:104
|
||||
#: src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx:115
|
||||
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:88
|
||||
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:253
|
||||
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:279
|
||||
@@ -9950,7 +9962,7 @@ msgstr ""
|
||||
msgid "Too many contacts - you've exceeded the number of contacts you can import to find your friends"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Hashtag.tsx:91
|
||||
#: src/screens/Hashtag.tsx:100
|
||||
#: src/screens/Search/SearchResults.tsx:52
|
||||
#: src/screens/Topic.tsx:72
|
||||
msgid "Top"
|
||||
@@ -10105,13 +10117,13 @@ msgstr ""
|
||||
msgid "Unblock list"
|
||||
msgstr ""
|
||||
|
||||
#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:72
|
||||
#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:78
|
||||
#: src/components/PostControls/BookmarkButton.tsx:42
|
||||
msgctxt "Button label to undo saving/removing a post from saved posts."
|
||||
msgid "Undo"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/PostControls/BookmarkButton.tsx:42
|
||||
msgctxt "Button label to undo saving/removing a post from saved posts."
|
||||
#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:72
|
||||
#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:78
|
||||
msgid "Undo"
|
||||
msgstr ""
|
||||
|
||||
@@ -10182,8 +10194,8 @@ msgstr ""
|
||||
msgid "Unmute"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/RichTextTag.tsx:141
|
||||
#: src/components/RichTextTag.tsx:154
|
||||
#: src/components/RichTextTag.tsx:150
|
||||
#: src/components/RichTextTag.tsx:163
|
||||
msgid "Unmute {tag}"
|
||||
msgstr ""
|
||||
|
||||
@@ -10749,8 +10761,8 @@ msgstr ""
|
||||
msgid "View your verifications"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/com/util/images/AutoSizedImage.tsx:205
|
||||
#: src/view/com/util/images/AutoSizedImage.tsx:232
|
||||
#: src/components/images/AutoSizedImage.tsx:206
|
||||
#: src/components/images/AutoSizedImage.tsx:233
|
||||
msgid "Views full image"
|
||||
msgstr ""
|
||||
|
||||
@@ -10810,8 +10822,8 @@ msgstr ""
|
||||
msgid "We could not find this list. It was probably deleted."
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Hashtag.tsx:258
|
||||
msgid "We couldn't find any results for that hashtag."
|
||||
#: src/screens/Hashtag.tsx:271
|
||||
msgid "We couldn't find any results for that tag."
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Topic.tsx:180
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import {useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {PrivacySensitive} from 'expo-privacy-sensitive'
|
||||
|
||||
import {useAppState} from '#/lib/hooks/useAppState'
|
||||
import {isIOS} from '#/platform/detection'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {sizes as iconSizes} from '#/components/icons/common'
|
||||
import {Mark as Logo} from '#/components/icons/Logo'
|
||||
|
||||
const ICON_SIZE = 'xl' as const
|
||||
|
||||
export function GrowthHack({
|
||||
children,
|
||||
align = 'right',
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
align?: 'left' | 'right'
|
||||
}) {
|
||||
const t = useTheme()
|
||||
|
||||
// the button has a variable width and is absolutely positioned, so we need to manually
|
||||
// set the minimum width of the underlying button
|
||||
const [width, setWidth] = useState<number | undefined>(undefined)
|
||||
|
||||
const appState = useAppState()
|
||||
|
||||
if (!isIOS || appState !== 'active') return children
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.relative,
|
||||
a.justify_center,
|
||||
align === 'right' ? a.align_end : a.align_start,
|
||||
width === undefined ? {opacity: 0} : {minWidth: width},
|
||||
]}>
|
||||
<PrivacySensitive
|
||||
style={[
|
||||
a.absolute,
|
||||
a.z_10,
|
||||
a.flex_col,
|
||||
align === 'right'
|
||||
? [a.right_0, a.align_end]
|
||||
: [a.left_0, a.align_start],
|
||||
// when finding the size of the button, we need the containing
|
||||
// element to have a concrete size otherwise the text will
|
||||
// collapse to 0 width. so set it to a really big number
|
||||
// and hide the entire thing (see above)
|
||||
width === undefined && {width: 10000},
|
||||
]}>
|
||||
<View
|
||||
onLayout={evt => setWidth(evt.nativeEvent.layout.width)}
|
||||
style={[
|
||||
t.atoms.bg,
|
||||
// make sure it covers the icon! the won't always be a button
|
||||
{minWidth: iconSizes[ICON_SIZE], minHeight: iconSizes[ICON_SIZE]},
|
||||
]}>
|
||||
{children}
|
||||
</View>
|
||||
</PrivacySensitive>
|
||||
<Logo size={ICON_SIZE} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -381,7 +381,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
|
||||
</View>
|
||||
</Link>
|
||||
{showFollowButton && (
|
||||
<View collapsable={false}>
|
||||
<View collapsable={false} style={[a.self_center]}>
|
||||
<ThreadItemAnchorFollowButton did={post.author.did} />
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {isIOS} from '#/platform/detection'
|
||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {
|
||||
useProfileFollowMutationQueue,
|
||||
@@ -14,10 +15,23 @@ import {useRequireAuth} from '#/state/session'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {atoms as a, useBreakpoints} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||
import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
|
||||
import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
|
||||
import {GrowthHack} from './GrowthHack'
|
||||
|
||||
export function ThreadItemAnchorFollowButton({did}: {did: string}) {
|
||||
if (isIOS) {
|
||||
return (
|
||||
<GrowthHack>
|
||||
<ThreadItemAnchorFollowButtonInner did={did} />
|
||||
</GrowthHack>
|
||||
)
|
||||
}
|
||||
|
||||
return <ThreadItemAnchorFollowButtonInner did={did} />
|
||||
}
|
||||
|
||||
export function ThreadItemAnchorFollowButtonInner({did}: {did: string}) {
|
||||
const {data: profile, isLoading} = useProfileQuery({did})
|
||||
|
||||
// We will never hit this - the profile will always be cached or loaded above
|
||||
@@ -113,15 +127,10 @@ function PostThreadFollowBtnLoaded({
|
||||
label={_(msg`Follow ${profile.handle}`)}
|
||||
onPress={onPress}
|
||||
size="small"
|
||||
variant="solid"
|
||||
color={isFollowing ? 'secondary' : 'secondary_inverted'}
|
||||
style={[a.rounded_full]}>
|
||||
{gtMobile && (
|
||||
<ButtonIcon
|
||||
icon={isFollowing ? Check : Plus}
|
||||
position="left"
|
||||
size="sm"
|
||||
/>
|
||||
<ButtonIcon icon={isFollowing ? CheckIcon : PlusIcon} size="sm" />
|
||||
)}
|
||||
<ButtonText>
|
||||
{!isFollowing ? (
|
||||
|
||||
@@ -116,6 +116,7 @@ const schema = z.object({
|
||||
}),
|
||||
hiddenPosts: z.array(z.string()).optional(), // should move to server
|
||||
useInAppBrowser: z.boolean().optional(),
|
||||
/** @deprecated */
|
||||
lastSelectedHomeFeed: z.string().optional(),
|
||||
pdsAddressHistory: z.array(z.string()).optional(),
|
||||
disableHaptics: z.boolean().optional(),
|
||||
|
||||
@@ -12,6 +12,11 @@ jest.mock('jwt-decode', () => ({
|
||||
|
||||
jest.mock('../../birthdate')
|
||||
jest.mock('../../../ageAssurance/data')
|
||||
jest.mock('#/lib/notifications/notifications', () => ({
|
||||
unregisterPushToken(_agents: BskyAgent[]) {
|
||||
return Promise.resolve()
|
||||
},
|
||||
}))
|
||||
|
||||
describe('session', () => {
|
||||
it('can log in and out', () => {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import {type AtpSessionEvent, type BskyAgent} from '@atproto/api'
|
||||
import {type AtpAgent, type AtpSessionEvent} from '@atproto/api'
|
||||
|
||||
import {unregisterPushToken} from '#/lib/notifications/notifications'
|
||||
import {logger} from '#/lib/notifications/util'
|
||||
import {createPublicAgent} from './agent'
|
||||
import {wrapSessionReducerForLogging} from './logging'
|
||||
import {type SessionAccount} from './types'
|
||||
import {createTemporaryAgentsAndResume} from './util'
|
||||
|
||||
// A hack so that the reducer can't read anything from the agent.
|
||||
// From the reducer's point of view, it should be a completely opaque object.
|
||||
@@ -137,6 +140,23 @@ let reducer = (state: State, action: Action): State => {
|
||||
}
|
||||
case 'removed-account': {
|
||||
const {accountDid} = action
|
||||
|
||||
// side effect
|
||||
const account = state.accounts.find(a => a.did === accountDid)
|
||||
if (account) {
|
||||
createTemporaryAgentsAndResume([account])
|
||||
.then(agents => unregisterPushToken(agents))
|
||||
.then(() =>
|
||||
logger.debug('Push token unregistered', {did: accountDid}),
|
||||
)
|
||||
.catch(err => {
|
||||
logger.error('Failed to unregister push token', {
|
||||
did: accountDid,
|
||||
error: err,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
accounts: state.accounts.filter(a => a.did !== accountDid),
|
||||
currentAgentState:
|
||||
@@ -148,9 +168,26 @@ let reducer = (state: State, action: Action): State => {
|
||||
}
|
||||
case 'logged-out-current-account': {
|
||||
const {currentAgentState} = state
|
||||
const accountDid = currentAgentState.did
|
||||
// side effect
|
||||
const account = state.accounts.find(a => a.did === accountDid)
|
||||
if (account && accountDid) {
|
||||
createTemporaryAgentsAndResume([account])
|
||||
.then(agents => unregisterPushToken(agents))
|
||||
.then(() =>
|
||||
logger.debug('Push token unregistered', {did: accountDid}),
|
||||
)
|
||||
.catch(err => {
|
||||
logger.error('Failed to unregister push token', {
|
||||
did: accountDid,
|
||||
error: err,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
accounts: state.accounts.map(a =>
|
||||
a.did === currentAgentState.did
|
||||
a.did === accountDid
|
||||
? {
|
||||
...a,
|
||||
refreshJwt: undefined,
|
||||
@@ -163,6 +200,15 @@ let reducer = (state: State, action: Action): State => {
|
||||
}
|
||||
}
|
||||
case 'logged-out-every-account': {
|
||||
createTemporaryAgentsAndResume(state.accounts)
|
||||
.then(agents => unregisterPushToken(agents))
|
||||
.then(() => logger.debug('Push token unregistered'))
|
||||
.catch(err => {
|
||||
logger.error('Failed to unregister push token', {
|
||||
error: err,
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
accounts: state.accounts.map(a => ({
|
||||
...a,
|
||||
@@ -187,7 +233,7 @@ let reducer = (state: State, action: Action): State => {
|
||||
}
|
||||
case 'partial-refresh-session': {
|
||||
const {accountDid, patch} = action
|
||||
const agent = state.currentAgentState.agent as BskyAgent
|
||||
const agent = state.currentAgentState.agent as AtpAgent
|
||||
|
||||
/*
|
||||
* Only mutating values that are safe. Be very careful with this.
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import AtpAgent from '@atproto/api'
|
||||
import {jwtDecode} from 'jwt-decode'
|
||||
|
||||
import {isJwtExpired} from '#/lib/jwt'
|
||||
import {hasProp} from '#/lib/type-guards'
|
||||
import * as persisted from '#/state/persisted'
|
||||
import {sessionAccountToSession} from './agent'
|
||||
import {type SessionAccount} from './types'
|
||||
|
||||
export function readLastActiveAccount() {
|
||||
@@ -28,3 +30,32 @@ export function isSessionExpired(account: SessionAccount) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and attempted to resumeSession for every stored session.
|
||||
* Intended to be used to send push token revokations just before logout.
|
||||
*/
|
||||
export async function createTemporaryAgentsAndResume(
|
||||
accounts: SessionAccount[],
|
||||
) {
|
||||
const agents = await Promise.allSettled(
|
||||
accounts.map(async account => {
|
||||
const agent: AtpAgent = new AtpAgent({service: account.service})
|
||||
if (account.pdsUrl) {
|
||||
agent.sessionManager.pdsUrl = new URL(account.pdsUrl)
|
||||
}
|
||||
|
||||
const session = sessionAccountToSession(account)
|
||||
const res = await agent.resumeSession(session)
|
||||
if (!res.success) throw new Error('Failed to resume session')
|
||||
|
||||
agent.assertAuthenticated() // confirm auth success
|
||||
|
||||
return agent
|
||||
}),
|
||||
)
|
||||
|
||||
return agents
|
||||
.filter(x => x.status === 'fulfilled')
|
||||
.map(promise => promise.value)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import React from 'react'
|
||||
import {createContext, useCallback, useContext, useState} from 'react'
|
||||
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import * as persisted from '#/state/persisted'
|
||||
import {type FeedDescriptor} from '#/state/queries/post-feed'
|
||||
import {useSession} from '#/state/session'
|
||||
import {account} from '#/storage'
|
||||
|
||||
type StateContext = FeedDescriptor | null
|
||||
type SetContext = (v: FeedDescriptor) => void
|
||||
|
||||
const stateContext = React.createContext<StateContext>(null)
|
||||
const stateContext = createContext<StateContext>(null)
|
||||
stateContext.displayName = 'SelectedFeedStateContext'
|
||||
const setContext = React.createContext<SetContext>((_: string) => {})
|
||||
const setContext = createContext<SetContext>((_: string) => {})
|
||||
setContext.displayName = 'SelectedFeedSetContext'
|
||||
|
||||
function getInitialFeed(): FeedDescriptor | null {
|
||||
function getInitialFeed(did?: string): FeedDescriptor | null {
|
||||
if (isWeb) {
|
||||
if (window.location.pathname === '/') {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
@@ -30,27 +31,35 @@ function getInitialFeed(): FeedDescriptor | null {
|
||||
}
|
||||
}
|
||||
|
||||
const feedFromPersisted = persisted.get('lastSelectedHomeFeed')
|
||||
if (feedFromPersisted) {
|
||||
// Fall back to the last chosen one across all tabs.
|
||||
return feedFromPersisted as FeedDescriptor
|
||||
if (did) {
|
||||
const feedFromStorage = account.get([did, 'lastSelectedHomeFeed'])
|
||||
if (feedFromStorage) {
|
||||
// Fall back to the last chosen one across all tabs.
|
||||
return feedFromStorage as FeedDescriptor
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
const [state, setState] = React.useState(() => getInitialFeed())
|
||||
const {currentAccount} = useSession()
|
||||
const [state, setState] = useState(() => getInitialFeed(currentAccount?.did))
|
||||
|
||||
const saveState = React.useCallback((feed: FeedDescriptor) => {
|
||||
setState(feed)
|
||||
if (isWeb) {
|
||||
try {
|
||||
sessionStorage.setItem('lastSelectedHomeFeed', feed)
|
||||
} catch {}
|
||||
}
|
||||
persisted.write('lastSelectedHomeFeed', feed)
|
||||
}, [])
|
||||
const saveState = useCallback(
|
||||
(feed: FeedDescriptor) => {
|
||||
setState(feed)
|
||||
if (isWeb) {
|
||||
try {
|
||||
sessionStorage.setItem('lastSelectedHomeFeed', feed)
|
||||
} catch {}
|
||||
}
|
||||
if (currentAccount?.did) {
|
||||
account.set([currentAccount?.did, 'lastSelectedHomeFeed'], feed)
|
||||
}
|
||||
},
|
||||
[currentAccount?.did],
|
||||
)
|
||||
|
||||
return (
|
||||
<stateContext.Provider value={state}>
|
||||
@@ -60,9 +69,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
}
|
||||
|
||||
export function useSelectedFeed() {
|
||||
return React.useContext(stateContext)
|
||||
return useContext(stateContext)
|
||||
}
|
||||
|
||||
export function useSetSelectedFeed() {
|
||||
return React.useContext(setContext)
|
||||
return useContext(setContext)
|
||||
}
|
||||
|
||||
@@ -66,4 +66,6 @@ export type Account = {
|
||||
* this device.
|
||||
*/
|
||||
birthdateLastUpdatedAt?: string
|
||||
|
||||
lastSelectedHomeFeed?: string
|
||||
}
|
||||
|
||||
@@ -11,40 +11,6 @@
|
||||
*
|
||||
* HTML & BODY STYLES IN `web/index.html` and `bskyweb/templates/base.html`
|
||||
*/
|
||||
:root {
|
||||
--text: black;
|
||||
--background: white;
|
||||
--backgroundLight: #f9fafb;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--text: white;
|
||||
--background: black;
|
||||
--backgroundLight: #111822;
|
||||
}
|
||||
}
|
||||
|
||||
html.theme--light {
|
||||
--text: black;
|
||||
--background: white;
|
||||
--backgroundLight: #f9fafb;
|
||||
background-color: white;
|
||||
}
|
||||
html.theme--dark {
|
||||
color-scheme: dark;
|
||||
background-color: black;
|
||||
--text: white;
|
||||
--background: black;
|
||||
--backgroundLight: #111822;
|
||||
}
|
||||
html.theme--dim {
|
||||
color-scheme: dark;
|
||||
background-color: #151d28;
|
||||
--text: white;
|
||||
--background: #151d28;
|
||||
--backgroundLight: #1c2736;
|
||||
}
|
||||
|
||||
/* Buttons and inputs have a font set by UA, so we'll have to reset that */
|
||||
button,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {memo, useCallback, useMemo, useState} from 'react'
|
||||
import {
|
||||
Image,
|
||||
Image as RNImage,
|
||||
Pressable,
|
||||
type StyleProp,
|
||||
StyleSheet,
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
import Svg, {Circle, Path, Rect} from 'react-native-svg'
|
||||
import {Image as ExpoImage} from 'expo-image'
|
||||
import {type ModerationUI} from '@atproto/api'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
@@ -37,7 +38,6 @@ import {
|
||||
} from '#/state/gallery'
|
||||
import {unstableCacheProfileView} from '#/state/queries/unstable-profile-cache'
|
||||
import {EditImageDialog} from '#/view/com/composer/photos/EditImageDialog'
|
||||
import {HighPriorityImage} from '#/view/com/util/images/Image'
|
||||
import {atoms as a, tokens, useTheme} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
@@ -289,7 +289,7 @@ let UserAvatar = ({
|
||||
!((moderation?.blur && isAndroid) /* android crashes with blur */) ? (
|
||||
<View style={containerStyle}>
|
||||
{usePlainRNImage ? (
|
||||
<Image
|
||||
<RNImage
|
||||
accessibilityIgnoresInvertColors
|
||||
testID="userAvatarImage"
|
||||
style={aviStyle}
|
||||
@@ -301,7 +301,7 @@ let UserAvatar = ({
|
||||
onLoad={onLoad}
|
||||
/>
|
||||
) : (
|
||||
<HighPriorityImage
|
||||
<ExpoImage
|
||||
testID="userAvatarImage"
|
||||
style={aviStyle}
|
||||
contentFit="cover"
|
||||
@@ -441,7 +441,7 @@ let EditableUserAvatar = ({
|
||||
{({props}) => (
|
||||
<Pressable {...props} testID="changeAvatarBtn">
|
||||
{avatar ? (
|
||||
<HighPriorityImage
|
||||
<ExpoImage
|
||||
testID="userAvatarImage"
|
||||
style={aviStyle}
|
||||
source={{uri: avatar}}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import {Image, type ImageProps, type ImageSource} from 'expo-image'
|
||||
|
||||
interface HighPriorityImageProps extends ImageProps {
|
||||
source: ImageSource
|
||||
}
|
||||
export function HighPriorityImage({source, ...props}: HighPriorityImageProps) {
|
||||
const updatedSource = {
|
||||
uri: typeof source === 'object' && source ? source.uri : '',
|
||||
} satisfies ImageSource
|
||||
return (
|
||||
<Image accessibilityIgnoresInvertColors source={updatedSource} {...props} />
|
||||
)
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import {Image} from 'react-native'
|
||||
|
||||
export const HighPriorityImage = Image
|
||||
@@ -74,9 +74,11 @@ export function DesktopFeeds() {
|
||||
overflowY: 'auto',
|
||||
}),
|
||||
]}>
|
||||
{pinnedFeedInfos.map(feedInfo => {
|
||||
{pinnedFeedInfos.map((feedInfo, index) => {
|
||||
const feed = feedInfo.feedDescriptor
|
||||
const current = route.name === 'Home' && feed === selectedFeed
|
||||
const current =
|
||||
route.name === 'Home' &&
|
||||
(selectedFeed ? feed === selectedFeed : index === 0)
|
||||
|
||||
return (
|
||||
<FeedItem
|
||||
|
||||
+46
-8
@@ -19,6 +19,7 @@
|
||||
<title>%WEB_TITLE%</title>
|
||||
|
||||
<link rel="preload" as="font" type="font/woff2" href="/static/media/InterVariable.c504db5c06caaf7cdfba.woff2" crossorigin>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
|
||||
<style>
|
||||
/**
|
||||
@@ -42,14 +43,6 @@
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
html {
|
||||
background-color: white;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background-color: black;
|
||||
}
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0px;
|
||||
@@ -64,6 +57,19 @@
|
||||
-ms-overflow-style: scrollbar;
|
||||
font-synthesis-weight: none;
|
||||
}
|
||||
:root {
|
||||
--text: black;
|
||||
--background: white;
|
||||
--backgroundLight: #e2e7ee;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--text: white;
|
||||
--background: black;
|
||||
--backgroundLight: #232e3e;
|
||||
}
|
||||
}
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
@@ -72,6 +78,32 @@
|
||||
min-height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
html.theme--light,
|
||||
html.theme--light body,
|
||||
html.theme--light #root {
|
||||
background-color: white;
|
||||
--text: black;
|
||||
--background: white;
|
||||
--backgroundLight: #DCE2EA;
|
||||
}
|
||||
html.theme--dark,
|
||||
html.theme--dark body,
|
||||
html.theme--dark #root {
|
||||
color-scheme: dark;
|
||||
background-color: black;
|
||||
--text: white;
|
||||
--background: black;
|
||||
--backgroundLight: #232E3E;
|
||||
}
|
||||
html.theme--dim,
|
||||
html.theme--dim body,
|
||||
html.theme--dim #root {
|
||||
color-scheme: dark;
|
||||
background-color: #151D28;
|
||||
--text: white;
|
||||
--background: #151D28;
|
||||
--backgroundLight: #2C3A4E;
|
||||
}
|
||||
#splash {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
@@ -98,6 +130,12 @@
|
||||
overflow-y: scroll;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
const theme = localStorage.getItem('ALF_THEME')
|
||||
if (theme) {
|
||||
document.documentElement.classList.add(`theme--${theme}`)
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
@@ -11372,6 +11372,11 @@ expo-notifications@~0.32.14:
|
||||
expo-application "~7.0.8"
|
||||
expo-constants "~18.0.11"
|
||||
|
||||
expo-privacy-sensitive@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/expo-privacy-sensitive/-/expo-privacy-sensitive-0.1.0.tgz#2177d7a3cb8ed352df94c5806d012dfb7b48bc84"
|
||||
integrity sha512-N0xa8yz+u7HvGY5CqZeo5cwtTOyFQxOxxt15jeW1eAjLKZAcNrtrDGGJP18TX2eh5TfJ3I6OtmECJg9Q8+Yorw==
|
||||
|
||||
expo-pwa@0.0.127:
|
||||
version "0.0.127"
|
||||
resolved "https://registry.yarnpkg.com/expo-pwa/-/expo-pwa-0.0.127.tgz#b8d2fd28efff408a24e0f2539bfb47e09f8e4ebe"
|
||||
|
||||
Reference in New Issue
Block a user