Compare commits

...

12 Commits

Author SHA1 Message Date
Eric Bailey 052a3e4f41 Reset default AA debug for E2E 2026-01-15 10:53:02 -06:00
Samuel Newman 29f8bc998d Highlight first feed in sidebar if non selected (#9703) 2026-01-15 07:12:15 -08:00
Eric Bailey 4760e59ea4 Add back deleted css block (#9702) 2026-01-15 08:09:24 -06:00
Samuel Newman d9e37a222a scope last-selected-feed to per-account (#9500) 2026-01-15 06:07:09 -08:00
estrattonbailey 3b98f2f8ab Nightly source-language update 2026-01-15 03:06:32 +00:00
Samuel Newman ab85b509c0 Watermark posts in screenshots (#9637)
* watermark posts in screenshots

* disable when app is backgrounded

* fix alignment

* show watermark even when there isn't a button
2026-01-14 21:03:13 -06:00
Samuel Newman 65faee4f08 Unregister push token on signout (#8661)
* unregister push - WIP

* create agent and submit push token revokation

* mock unregisterpush

* fix import

* Add proxy headers

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2026-01-14 20:55:35 -06:00
pfrazee 94be661794 Nightly source-language update 2026-01-15 02:48:02 +00:00
Samuel Newman 10aa46aa9e Prevent flash of wrong theme on startup (web) (#9577)
* prevent flash of wrong theme on startup

* move bg color to #root

* Update and use existing system

* Darken slightly, better contrast

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2026-01-14 20:41:59 -06:00
Samuel Newman 8f82ad66df make button blue when tooltip is visible (#9668) 2026-01-14 20:34:59 -06:00
Samuel Newman 1cb362b596 Add loading="lazy" to expo-image on web (#9480)
* add `loading="lazy"` to expo-image

* add `loading="lazy"` to embed cards, avatars

* get rid of useless image wrapper indirection

* move image components to components dir

* fix imports

* fix import

* Keep avis eager

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2026-01-14 20:29:13 -06:00
Eric Bailey f960d2fbc6 fix: make logo show in qr code by absolutely positioning svg on top of it (#9373) (#9700)
* fix: make logo show in qr code by absolutely positioning svg on top of it

* fix: remove log and add explanation comment

* fix: only apply qrcode fix to web

Co-authored-by: Elijah Seed-Arita <elijaharita@gmail.com>
2026-01-14 20:28:48 -06:00
34 changed files with 594 additions and 195 deletions
+45 -8
View File
@@ -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">
+1
View File
@@ -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",
+103
View File
@@ -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;
+3 -3
View File
@@ -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',
},
],
},
+1
View File
@@ -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}
+2 -2
View File
@@ -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,
+68 -33
View File
@@ -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}>
@@ -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>
@@ -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: {
+4
View File
@@ -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`,
+40 -3
View File
@@ -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})
}
}
+50 -38
View File
@@ -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 accounts 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 ? (
+1
View File
@@ -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', () => {
+49 -3
View File
@@ -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.
+31
View File
@@ -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)
}
+30 -21
View File
@@ -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)
}
+2
View File
@@ -66,4 +66,6 @@ export type Account = {
* this device.
*/
birthdateLastUpdatedAt?: string
lastSelectedHomeFeed?: string
}
-34
View File
@@ -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,
+5 -5
View File
@@ -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}}
-13
View File
@@ -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} />
)
}
-3
View File
@@ -1,3 +0,0 @@
import {Image} from 'react-native'
export const HighPriorityImage = Image
+4 -2
View File
@@ -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
View File
@@ -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>
+5
View File
@@ -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"