Compare commits

...

13 Commits

Author SHA1 Message Date
David Buchanan 1ad06548b1 move yt embed script to its own source file (#9337) 2025-11-06 10:18:04 -08:00
Samuel Newman f7274cbc2a always send #interactionReply (#9330) 2025-11-05 14:27:11 +02:00
Samuel Newman a232372ce7 Prevent sparse arrays in snapToOffsets (#9329)
* prevent sparse arrays in snapToOffsets

* wait, sparse arrays don't work like that

* more readable check even if it's technically useless

* make the second check actually work
2025-11-05 14:27:02 +02:00
Samuel Newman 0d1060419f add even more optional chaining to link thing (#9327) 2025-11-05 14:26:53 +02:00
Samuel Newman b41184d8e4 disable wake-from-background OTA on Android (#9317) 2025-11-05 14:26:23 +02:00
Samuel Newman caba72c848 Update web dim <html> background color (#9307)
* update dim background color

* Update other css colors

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2025-11-05 14:24:09 +02:00
Samuel Newman 82f7d3f4c8 remove await on dismissBrowser (#9304) 2025-10-30 17:16:20 +02:00
Samuel Newman fa98a66755 Override Reduce Motion on load-bearing animation (#9294) 2025-10-28 21:19:55 +02:00
Samuel Newman f1803068ae Android - add fallbacks when saving image to gallery (#9293)
* add fallback paths when saving image to gallery

* `debug` -> `info`
2025-10-28 21:19:36 +02:00
Samuel Newman f9a502f393 Close IAB when it triggers an intent (#8454)
* Close IAB if it opens a link to the app

* native only

* await close
2025-10-28 21:19:32 +02:00
Samuel Newman 2a7223822f Remove feed:seen from statsig (#9261)
(cherry picked from commit 78c8a6037f)
2025-10-27 13:24:12 -05:00
bnewbold 74db306096 clarify in content hider if label is on overall account (#9264)
* clarify in content hider if label is on overall account

* fix bool typo

* Update src/components/moderation/ContentHider.tsx

Co-authored-by: Samuel Newman <mozzius@protonmail.com>

* rename variable for clarity

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
(cherry picked from commit 28e132ae03)
2025-10-27 13:24:09 -05:00
Samuel Newman ca7e009836 Revert "Disable Sentry sampling for now (#9262)" (#9282)
This reverts commit de68915bc0.

(cherry picked from commit 25a2a2c7f6)
2025-10-27 13:24:04 -05:00
15 changed files with 131 additions and 71 deletions
+1 -1
View File
@@ -223,7 +223,7 @@ func serve(cctx *cli.Context) error {
e.GET("/robots.txt", echo.WrapHandler(staticHandler))
}
e.GET("/iframe/youtube.html", echo.WrapHandler(staticHandler))
e.GET("/iframe/*", echo.WrapHandler(staticHandler))
e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler)), func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
c.Response().Before(func() {
+1 -37
View File
@@ -16,40 +16,4 @@
}
</style>
<div class="container"><div class="video" id="player"></div></div>
<script>
const url = new URL(window.location)
const viewport = document.querySelector("meta[name=viewport]")
const tag = document.createElement("script")
tag.src = "https://www.youtube.com/iframe_api"
const firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
let player
function onYouTubeIframeAPIReady() {
let videoId = url.searchParams.get('videoId')
videoId = decodeURIComponent(videoId)
videoId = videoId.replace(/[^a-zA-Z0-9_-]/g, "")
if (videoId.length !== 11) throw new Error("Invalid video ID")
let start = url.searchParams.get('start')
start = start.replace(/[^0-9]/g, "")
player = new YT.Player('player', {
width: "1000",
height: "1000",
videoId,
playerVars: {
autoplay: 1,
start,
rel: 0,
loop: 0,
playsinline: 1,
origin: url.origin
},
});
}
function onPlayerReady(event) {
event.target.playVideo();
}
</script>
<script src="youtube.js"></script>
+35
View File
@@ -0,0 +1,35 @@
const url = new URL(window.location)
const viewport = document.querySelector("meta[name=viewport]")
const tag = document.createElement("script")
tag.src = "https://www.youtube.com/iframe_api"
const firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
let player
function onYouTubeIframeAPIReady() {
let videoId = url.searchParams.get('videoId')
videoId = decodeURIComponent(videoId)
videoId = videoId.replace(/[^a-zA-Z0-9_-]/g, "")
if (videoId.length !== 11) throw new Error("Invalid video ID")
let start = url.searchParams.get('start')
start = start.replace(/[^0-9]/g, "")
player = new YT.Player('player', {
width: "1000",
height: "1000",
videoId,
playerVars: {
autoplay: 1,
start,
rel: 0,
loop: 0,
playsinline: 1,
origin: url.origin
},
});
}
function onPlayerReady(event) {
event.target.playVideo();
}
+7 -2
View File
@@ -83,7 +83,12 @@ export function InterestTabs({
function handleTabLayout(index: number, x: number, width: number) {
if (!tabOffsets.length) {
pendingTabOffsets.current[index] = {x, width}
if (pendingTabOffsets.current.length === interests.length) {
// not only do we check if the length is equal to the number of interests,
// but we also need to ensure that the array isn't sparse. `.filter()`
// removes any empty slots from the array
if (
pendingTabOffsets.current.filter(o => !!o).length === interests.length
) {
setTabOffsets(pendingTabOffsets.current)
}
}
@@ -205,7 +210,7 @@ export function InterestTabs({
showsHorizontalScrollIndicator={false}
decelerationRate="fast"
snapToOffsets={
tabOffsets.length === interests.length
tabOffsets.filter(o => !!o).length === interests.length
? tabOffsets.map(o => o.x - tokens.space.xl)
: undefined
}
+1 -1
View File
@@ -166,7 +166,7 @@ export function useLink({
const state = navigation.getState()
// if screen is not in the current navigator, it means it's
// most likely a tab screen. note: state can be undefined
if (!state?.routeNames.includes(screen)) {
if (!state?.routeNames?.includes?.(screen)) {
const parent = navigation.getParent()
if (
parent &&
+6 -1
View File
@@ -85,7 +85,11 @@ function ContentHiderActive({
blur.type !== 'label' ||
(blur.type === 'label' && blur.source.type !== 'user')
) {
return desc.name
if (desc.isSubjectAccount) {
return _(msg`${desc.name} (Account)`)
} else {
return desc.name
}
}
let hasAdultContentLabel = false
@@ -127,6 +131,7 @@ function ContentHiderActive({
modui?.blurs,
blur,
desc.name,
desc.isSubjectAccount,
labelDefs,
i18n.locale,
globalLabelStrings,
+11 -2
View File
@@ -1,10 +1,11 @@
import React from 'react'
import {Alert} from 'react-native'
import * as Linking from 'expo-linking'
import * as WebBrowser from 'expo-web-browser'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {logger} from '#/logger'
import {isNative} from '#/platform/detection'
import {isIOS, isNative} from '#/platform/detection'
import {useSession} from '#/state/session'
import {useCloseAllActiveElements} from '#/state/util'
import {
@@ -32,7 +33,15 @@ export function useIntentHandler() {
const {tryApplyUpdate} = useApplyPullRequestOTAUpdate()
React.useEffect(() => {
const handleIncomingURL = (url: string) => {
const handleIncomingURL = async (url: string) => {
if (isIOS) {
// Close in-app browser if it's open (iOS only)
// TEMP: promise never resolves if the browser is not open, so don't await
// https://github.com/expo/expo/issues/40710
// add the await back when possible since it's needed to fix the IAB share bug -sfn
/* await */ WebBrowser.dismissBrowser().catch(() => {})
}
const referrerInfo = Referrer.getReferrerInfo()
if (referrerInfo && referrerInfo.hostname !== 'bsky.app') {
logger.metric('deepLink:referrerReceived', {
+8 -1
View File
@@ -12,7 +12,7 @@ import {
import {isNetworkError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {isIOS} from '#/platform/detection'
import {isAndroid, isIOS} from '#/platform/detection'
import {IS_TESTFLIGHT} from '#/env'
const MINIMUM_MINIMIZE_TIME = 15 * 60e3
@@ -193,6 +193,13 @@ export function useOTAUpdates() {
return
}
// TEMP: disable wake-from-background OTA loading on Android.
// This is causing a crash when the thread view is open due to
// `maintainVisibleContentPosition`. See repro repo for more details:
// https://github.com/mozzius/ota-crash-repro
// Old Arch only - re-enable once we're on the New Archictecture! -sfn
if (isAndroid) return
const subscription = AppState.addEventListener(
'change',
async nextAppState => {
+34 -2
View File
@@ -114,8 +114,39 @@ export async function saveImageToMediaLibrary({uri}: {uri: string}) {
// as the starting image, or put it directly into the album
const album = await MediaLibrary.getAlbumAsync(ALBUM_NAME)
if (album) {
// if album exists, put the image straight in there
await MediaLibrary.createAssetAsync(imagePath, album)
// try and migrate if needed
try {
if (await MediaLibrary.albumNeedsMigrationAsync(album)) {
await MediaLibrary.migrateAlbumIfNeededAsync(album)
}
} catch (err) {
logger.info('Attempted and failed to migrate album', {
safeMessage: err,
})
}
try {
// if album exists, put the image straight in there
await MediaLibrary.createAssetAsync(imagePath, album)
} catch (err) {
logger.info('Failed to create asset', {safeMessage: err})
// however, it's possible that we don't have write permission to the album
// try making a new one!
try {
await MediaLibrary.createAlbumAsync(
ALBUM_NAME,
undefined,
undefined,
imagePath,
)
} catch (err2) {
logger.info('Failed to create asset in a fresh album', {
safeMessage: err2,
})
// ... and if all else fails, just put it in DCIM
await MediaLibrary.createAssetAsync(imagePath)
}
}
} else {
// otherwise, create album with asset (albums must always have at least one asset)
await MediaLibrary.createAlbumAsync(
@@ -132,6 +163,7 @@ export async function saveImageToMediaLibrary({uri}: {uri: string}) {
logger.error(err instanceof Error ? err : String(err), {
message: 'Failed to save image to media library',
})
throw err
} finally {
safeDeleteAsync(imagePath)
}
@@ -28,6 +28,7 @@ export interface ModerationCauseDescription {
sourceType?: ModerationCauseSource['type']
sourceAvi?: string
sourceDid?: string
isSubjectAccount?: boolean
}
export function useModerationCauseDescription(
@@ -162,6 +163,7 @@ export function useModerationCauseDescription(
sourceType: cause.source.type,
sourceAvi: labeler?.creator.avatar,
sourceDid: cause.label.src,
isSubjectAccount: cause.label.uri.startsWith('did:'),
}
}
// should never happen
+1 -1
View File
@@ -29,5 +29,5 @@ init({
* @see https://docs.sentry.io/platforms/react-native/configuration/options/#attach-stacktrace
*/
attachStacktrace: false,
// sampleRate: env.IS_INTERNAL ? 1.0 : 0.1,
sampleRate: env.IS_INTERNAL ? 1.0 : 0.1,
})
+8 -4
View File
@@ -341,10 +341,14 @@ function flushToStatsig(stats: AggregatedStats | null, feedDescriptor: string) {
}
if (stats.seenCount > 0) {
logger.metric('feed:seen', {
count: stats.seenCount,
feed: feedDescriptor,
})
logger.metric(
'feed:seen',
{
count: stats.seenCount,
feed: feedDescriptor,
},
{statsig: false},
)
stats.seenCount = 0
}
}
+7 -7
View File
@@ -14,21 +14,21 @@
:root {
--text: black;
--background: white;
--backgroundLight: hsl(211, 20%, 95%);
--backgroundLight: #f9fafb;
}
@media (prefers-color-scheme: dark) {
:root {
color-scheme: dark;
--text: white;
--background: black;
--backgroundLight: hsl(211, 20%, 20%);
--backgroundLight: #111822;
}
}
html.theme--light {
--text: black;
--background: white;
--backgroundLight: hsl(211, 20%, 95%);
--backgroundLight: #f9fafb;
background-color: white;
}
html.theme--dark {
@@ -36,14 +36,14 @@ html.theme--dark {
background-color: black;
--text: white;
--background: black;
--backgroundLight: hsl(211, 20%, 20%);
--backgroundLight: #111822;
}
html.theme--dim {
color-scheme: dark;
background-color: hsl(211, 28%, 12%);
background-color: #151d28;
--text: white;
--background: hsl(211, 20%, 4%);
--backgroundLight: hsl(211, 20%, 10%);
--background: #151d28;
--backgroundLight: #1c2736;
}
/* Buttons and inputs have a font set by UA, so we'll have to reset that */
@@ -18,6 +18,7 @@ import Animated, {
cancelAnimation,
interpolate,
measure,
ReduceMotion,
runOnJS,
type SharedValue,
useAnimatedReaction,
@@ -516,6 +517,7 @@ function LightboxImage({
velocity: e.velocityY,
velocityFactor: Math.max(3500 / Math.abs(e.velocityY), 1), // Speed up if it's too slow.
deceleration: 1, // Danger! This relies on the reaction below stopping it.
reduceMotion: ReduceMotion.Never, // If this animation doesn't run, the image gets stuck - therefore override Reduce Motion
})
})
} else {
@@ -524,6 +526,7 @@ function LightboxImage({
return withSpring(0, {
stiffness: 700,
damping: 50,
reduceMotion: ReduceMotion.Never,
})
})
}
+6 -12
View File
@@ -175,24 +175,18 @@ let FeedItemInner = ({
const {sendInteraction, feedSourceInfo} = useFeedFeedbackContext()
const onPressReply = () => {
sendInteraction({
item: post.uri,
event: 'app.bsky.feed.defs#interactionReply',
feedContext,
reqId,
})
if (gate('feed_reply_button_open_thread')) {
sendInteraction({
item: post.uri,
event: 'app.bsky.feed.defs#clickthroughItem',
feedContext,
reqId,
})
navigation.navigate('PostThread', {
name: post.author.did,
rkey,
})
} else {
sendInteraction({
item: post.uri,
event: 'app.bsky.feed.defs#interactionReply',
feedContext,
reqId,
})
openComposer({
replyTo: {
uri: post.uri,