diff --git a/assets/icons/play_filled_corner2_rounded.svg b/assets/icons/play_filled_corner2_rounded.svg
new file mode 100644
index 0000000000..e25e8d4628
--- /dev/null
+++ b/assets/icons/play_filled_corner2_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/play_stroke2_corner2_rounded.svg b/assets/icons/play_stroke2_corner2_rounded.svg
new file mode 100644
index 0000000000..54bba91fb9
--- /dev/null
+++ b/assets/icons/play_stroke2_corner2_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/package.json b/package.json
index 51177943bb..91b427ae91 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "bsky.app",
- "version": "1.89.0",
+ "version": "1.90.0",
"private": true,
"engines": {
"node": ">=18"
@@ -143,6 +143,7 @@
"expo-web-browser": "~13.0.3",
"fast-text-encoding": "^1.0.6",
"history": "^5.3.0",
+ "hls.js": "^1.5.11",
"js-sha256": "^0.9.0",
"jwt-decode": "^4.0.0",
"lande": "^1.0.10",
@@ -269,7 +270,7 @@
"react-scripts": "^5.0.1",
"react-test-renderer": "18.2.0",
"ts-node": "^10.9.1",
- "typescript": "^5.3.3",
+ "typescript": "^5.5.4",
"url-loader": "^4.1.1",
"webpack": "^5.75.0",
"webpack-bundle-analyzer": "^4.10.1",
diff --git a/patches/expo-video+1.1.10.patch b/patches/expo-video+1.1.10.patch
new file mode 100644
index 0000000000..b183be9d41
--- /dev/null
+++ b/patches/expo-video+1.1.10.patch
@@ -0,0 +1,20 @@
+--- a/node_modules/expo-video/android/src/main/java/expo/modules/video/PlayerViewExtension.kt
++++ b/node_modules/expo-video/android/src/main/java/expo/modules/video/PlayerViewExtension.kt
+@@ -11,6 +11,7 @@ internal fun PlayerView.applyRequiresLinearPlayback(requireLinearPlayback: Boole
+ setShowPreviousButton(!requireLinearPlayback)
+ setShowNextButton(!requireLinearPlayback)
+ setTimeBarInteractive(requireLinearPlayback)
++ setShowSubtitleButton(true)
+ }
+
+ @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
+@@ -27,7 +28,8 @@ internal fun PlayerView.setTimeBarInteractive(interactive: Boolean) {
+
+ @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
+ internal fun PlayerView.setFullscreenButtonVisibility(visible: Boolean) {
+- val fullscreenButton = findViewById(androidx.media3.ui.R.id.exo_fullscreen)
++ val fullscreenButton =
++ findViewById(androidx.media3.ui.R.id.exo_fullscreen)
+ fullscreenButton?.visibility = if (visible) {
+ android.view.View.VISIBLE
+ } else {
diff --git a/src/App.native.tsx b/src/App.native.tsx
index ed76c753b5..d2c20fc8e7 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -23,10 +23,12 @@ import {
} from '#/lib/statsig/statsig'
import {s} from '#/lib/styles'
import {ThemeProvider} from '#/lib/ThemeContext'
+import I18nProvider from '#/locale/i18nProvider'
import {logger} from '#/logger'
import {Provider as A11yProvider} from '#/state/a11y'
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
import {Provider as DialogStateProvider} from '#/state/dialogs'
+import {listenSessionDropped} from '#/state/events'
import {Provider as InvitesStateProvider} from '#/state/invites'
import {Provider as LightboxStateProvider} from '#/state/lightbox'
import {MessagesProvider} from '#/state/messages'
@@ -49,6 +51,7 @@ import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
import {TestCtrls} from '#/view/com/testing/TestCtrls'
+import {ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoContext'
import * as Toast from '#/view/com/util/Toast'
import {Shell} from '#/view/shell'
import {ThemeProvider as Alf} from '#/alf'
@@ -58,8 +61,6 @@ import {Provider as PortalProvider} from '#/components/Portal'
import {Splash} from '#/Splash'
import {Provider as TourProvider} from '#/tours'
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
-import I18nProvider from './locale/i18nProvider'
-import {listenSessionDropped} from './state/events'
SplashScreen.preventAutoHideAsync()
@@ -107,42 +108,44 @@ function InnerApp() {
-
-
-
-
-
- {/* LabelDefsProvider MUST come before ModerationOptsProvider */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+ {/* LabelDefsProvider MUST come before ModerationOptsProvider */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/App.web.tsx b/src/App.web.tsx
index a64988f380..df6fbf2449 100644
--- a/src/App.web.tsx
+++ b/src/App.web.tsx
@@ -12,10 +12,12 @@ import {useIntentHandler} from '#/lib/hooks/useIntentHandler'
import {QueryProvider} from '#/lib/react-query'
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
import {ThemeProvider} from '#/lib/ThemeContext'
+import I18nProvider from '#/locale/i18nProvider'
import {logger} from '#/logger'
import {Provider as A11yProvider} from '#/state/a11y'
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
import {Provider as DialogStateProvider} from '#/state/dialogs'
+import {listenSessionDropped} from '#/state/events'
import {Provider as InvitesStateProvider} from '#/state/invites'
import {Provider as LightboxStateProvider} from '#/state/lightbox'
import {MessagesProvider} from '#/state/messages'
@@ -37,6 +39,7 @@ import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out'
import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
+import {ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoContext'
import * as Toast from '#/view/com/util/Toast'
import {ToastContainer} from '#/view/com/util/Toast.web'
import {Shell} from '#/view/shell/index'
@@ -46,8 +49,6 @@ import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
import {Provider as PortalProvider} from '#/components/Portal'
import {Provider as TourProvider} from '#/tours'
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
-import I18nProvider from './locale/i18nProvider'
-import {listenSessionDropped} from './state/events'
function InnerApp() {
const [isReady, setIsReady] = React.useState(false)
@@ -92,39 +93,41 @@ function InnerApp() {
-
-
-
-
- {/* LabelDefsProvider MUST come before ModerationOptsProvider */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+ {/* LabelDefsProvider MUST come before ModerationOptsProvider */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts
index 650997c919..e918e370d8 100644
--- a/src/alf/atoms.ts
+++ b/src/alf/atoms.ts
@@ -2,7 +2,6 @@ import {Platform, StyleSheet} from 'react-native'
import * as tokens from '#/alf/tokens'
import {native, web} from '#/alf/util/platform'
-import hairlineWidth = StyleSheet.hairlineWidth
export const atoms = {
/*
@@ -284,19 +283,19 @@ export const atoms = {
borderWidth: 0,
},
border: {
- borderWidth: hairlineWidth,
+ borderWidth: StyleSheet.hairlineWidth,
},
border_t: {
- borderTopWidth: hairlineWidth,
+ borderTopWidth: StyleSheet.hairlineWidth,
},
border_b: {
- borderBottomWidth: hairlineWidth,
+ borderBottomWidth: StyleSheet.hairlineWidth,
},
border_l: {
- borderLeftWidth: hairlineWidth,
+ borderLeftWidth: StyleSheet.hairlineWidth,
},
border_r: {
- borderRightWidth: hairlineWidth,
+ borderRightWidth: StyleSheet.hairlineWidth,
},
/*
diff --git a/src/components/Button.tsx b/src/components/Button.tsx
index 457164d111..4fe0ab4b12 100644
--- a/src/components/Button.tsx
+++ b/src/components/Button.tsx
@@ -70,6 +70,9 @@ export type ButtonProps = Pick<
AccessibilityProps &
VariantProps & {
testID?: string
+ /**
+ * For a11y, try to make this descriptive and clear
+ */
label: string
style?: StyleProp
hoverStyle?: StyleProp
diff --git a/src/components/FeedCard.tsx b/src/components/FeedCard.tsx
index 82d675a8e4..e6d664cfda 100644
--- a/src/components/FeedCard.tsx
+++ b/src/components/FeedCard.tsx
@@ -40,7 +40,7 @@ type Props = {
export function Default(props: Props) {
const {view} = props
return (
-
+
@@ -58,7 +58,7 @@ export function Link({
view,
children,
...props
-}: Props & Omit) {
+}: Props & Omit) {
const queryClient = useQueryClient()
const href = React.useMemo(() => {
@@ -70,7 +70,11 @@ export function Link({
}, [view, queryClient])
return (
-
+
{children}
)
diff --git a/src/components/FeedInterstitials.tsx b/src/components/FeedInterstitials.tsx
index 53c1fac0b9..2e8724143d 100644
--- a/src/components/FeedInterstitials.tsx
+++ b/src/components/FeedInterstitials.tsx
@@ -190,7 +190,7 @@ export function SuggestedFollows() {
{profiles.slice(0, maxLength).map(profile => (
{
logEvent('feed:interstitial:profileCard:press', {})
}}
@@ -266,7 +266,10 @@ export function SuggestedFollows() {
a.pt_xs,
a.gap_md,
]}>
-
+
Browse more suggestions
@@ -396,7 +399,10 @@ export function SuggestedFeeds() {
a.pt_xs,
a.gap_md,
]}>
-
+
Browse more suggestions
diff --git a/src/components/Link.tsx b/src/components/Link.tsx
index a8b478be78..6c25faffb8 100644
--- a/src/components/Link.tsx
+++ b/src/components/Link.tsx
@@ -40,11 +40,6 @@ type BaseLinkProps = Pick<
> & {
testID?: string
- /**
- * Label for a11y. Defaults to the href.
- */
- label?: string
-
/**
* The React Navigation `StackAction` to perform when the link is pressed.
*/
@@ -197,7 +192,7 @@ export function useLink({
}
export type LinkProps = Omit &
- Omit
+ Omit
/**
* A interactive element that renders as a `` tag on the web. On mobile it
@@ -224,7 +219,6 @@ export function Link({
return (
-
+
{state => (
-
+
{state => (
-
+
{state => (
Adult content can only be enabled via the Web at{' '}
{
evt.preventDefault()
@@ -569,7 +579,9 @@ function PwiOptOut() {
-
+
Learn more about what is public on Bluesky.
diff --git a/src/screens/Signup/StepInfo/Policies.tsx b/src/screens/Signup/StepInfo/Policies.tsx
index f25bda274f..a3a0672223 100644
--- a/src/screens/Signup/StepInfo/Policies.tsx
+++ b/src/screens/Signup/StepInfo/Policies.tsx
@@ -45,14 +45,20 @@ export const Policies = ({
const els = []
if (tos) {
els.push(
-
+
{_(msg`Terms of Service`)}
,
)
}
if (pp) {
els.push(
-
+
{_(msg`Privacy Policy`)}
,
)
diff --git a/src/screens/Signup/index.tsx b/src/screens/Signup/index.tsx
index da0383884b..189760460e 100644
--- a/src/screens/Signup/index.tsx
+++ b/src/screens/Signup/index.tsx
@@ -166,6 +166,7 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
Having trouble?{' '}
Contact support
diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts
index 315c9cfadd..1d6ec80d91 100644
--- a/src/state/queries/post-feed.ts
+++ b/src/state/queries/post-feed.ts
@@ -78,7 +78,10 @@ export interface FeedPostSliceItem {
uri: string
post: AppBskyFeedDefs.PostView
record: AppBskyFeedPost.Record
- reason?: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource
+ reason?:
+ | AppBskyFeedDefs.ReasonRepost
+ | ReasonFeedSource
+ | {[k: string]: unknown; $type: string}
feedContext: string | undefined
moderation: ModerationDecision
parentAuthor?: AppBskyActorDefs.ProfileViewBasic
@@ -311,7 +314,7 @@ export function usePostFeedQuery(
if (isDiscover) {
userActionHistory.seen(
slice.items.map(item => ({
- feedContext: item.feedContext,
+ feedContext: slice.feedContext,
likeCount: item.post.likeCount ?? 0,
repostCount: item.post.repostCount ?? 0,
replyCount: item.post.replyCount ?? 0,
@@ -323,10 +326,10 @@ export function usePostFeedQuery(
)
}
- return {
+ const feedPostSlice: FeedPostSlice = {
_reactKey: slice._reactKey,
_isFeedPostSlice: true,
- rootUri: slice.rootItem.post.uri,
+ rootUri: slice.uri,
isThread:
slice.items.length > 1 &&
slice.items.every(
@@ -341,35 +344,42 @@ export function usePostFeedQuery(
AppBskyFeedPost.validateRecord(item.post.record)
.success
) {
- const parentAuthor =
- item.reply?.parent?.author ??
- slice.items[i + 1]?.reply?.grandparentAuthor
+ const parent = item.reply?.parent
+ let parentAuthor:
+ | AppBskyActorDefs.ProfileViewBasic
+ | undefined
+ if (AppBskyFeedDefs.isPostView(parent)) {
+ parentAuthor = parent.author
+ }
+ if (!parentAuthor) {
+ parentAuthor =
+ slice.items[i + 1]?.reply?.grandparentAuthor
+ }
const replyRef = item.reply
const isParentBlocked = AppBskyFeedDefs.isBlockedPost(
replyRef?.parent,
)
- return {
+ const feedPostSliceItem: FeedPostSliceItem = {
_reactKey: `${slice._reactKey}-${i}-${item.post.uri}`,
uri: item.post.uri,
post: item.post,
record: item.post.record,
- reason:
- i === 0 && slice.source
- ? slice.source
- : item.reason,
- feedContext: item.feedContext || slice.feedContext,
+ reason: slice.reason,
+ feedContext: slice.feedContext,
moderation: moderations[i],
parentAuthor,
isParentBlocked,
}
+ return feedPostSliceItem
}
return undefined
})
- .filter(Boolean) as FeedPostSliceItem[],
+ .filter(n => !!n),
}
+ return feedPostSlice
})
- .filter(Boolean) as FeedPostSlice[],
+ .filter(n => !!n),
})),
],
}
diff --git a/src/state/queries/threadgate.ts b/src/state/queries/threadgate.ts
index 67c6f8c084..8b6aeba6c1 100644
--- a/src/state/queries/threadgate.ts
+++ b/src/state/queries/threadgate.ts
@@ -4,7 +4,7 @@ export type ThreadgateSetting =
| {type: 'nobody'}
| {type: 'mention'}
| {type: 'following'}
- | {type: 'list'; list: string}
+ | {type: 'list'; list: unknown}
export function threadgateViewToSettings(
threadgate: AppBskyFeedDefs.ThreadgateView | undefined,
@@ -21,18 +21,18 @@ export function threadgateViewToSettings(
if (!record.allow?.length) {
return [{type: 'nobody'}]
}
- return record.allow
+ const settings: ThreadgateSetting[] = record.allow
.map(allow => {
+ let setting: ThreadgateSetting | undefined
if (allow.$type === 'app.bsky.feed.threadgate#mentionRule') {
- return {type: 'mention'}
+ setting = {type: 'mention'}
+ } else if (allow.$type === 'app.bsky.feed.threadgate#followingRule') {
+ setting = {type: 'following'}
+ } else if (allow.$type === 'app.bsky.feed.threadgate#listRule') {
+ setting = {type: 'list', list: allow.list}
}
- if (allow.$type === 'app.bsky.feed.threadgate#followingRule') {
- return {type: 'following'}
- }
- if (allow.$type === 'app.bsky.feed.threadgate#listRule') {
- return {type: 'list', list: allow.list}
- }
- return undefined
+ return setting
})
- .filter(Boolean) as ThreadgateSetting[]
+ .filter(n => !!n)
+ return settings
}
diff --git a/src/view/com/auth/SplashScreen.web.tsx b/src/view/com/auth/SplashScreen.web.tsx
index 6df4e439aa..9ffcbfb9df 100644
--- a/src/view/com/auth/SplashScreen.web.tsx
+++ b/src/view/com/auth/SplashScreen.web.tsx
@@ -132,6 +132,7 @@ export const SplashScreen = ({
function Footer() {
const t = useTheme()
+ const {_} = useLingui()
return (
-
+
Business
-
+
Blog
-
+
Jobs
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index eed5cb6c54..72b6fae5fd 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -99,7 +99,6 @@ import {SelectVideoBtn} from './videos/SelectVideoBtn'
import {useVideoState} from './videos/state'
import {VideoPreview} from './videos/VideoPreview'
import {VideoTranscodeProgress} from './videos/VideoTranscodeProgress'
-import hairlineWidth = StyleSheet.hairlineWidth
type CancelRef = {
onPressCancel: () => void
@@ -763,7 +762,7 @@ function useAnimatedBorders() {
const topBarAnimatedStyle = useAnimatedStyle(() => {
return {
- borderBottomWidth: hairlineWidth,
+ borderBottomWidth: StyleSheet.hairlineWidth,
borderColor: interpolateColor(
hasScrolledTop.value,
[0, 1],
@@ -773,7 +772,7 @@ function useAnimatedBorders() {
})
const bottomBarAnimatedStyle = useAnimatedStyle(() => {
return {
- borderTopWidth: hairlineWidth,
+ borderTopWidth: StyleSheet.hairlineWidth,
borderColor: interpolateColor(
hasScrolledBottom.value,
[0, 1],
@@ -855,7 +854,7 @@ const styles = StyleSheet.create({
marginBottom: 8,
},
errorIcon: {
- borderWidth: hairlineWidth,
+ borderWidth: StyleSheet.hairlineWidth,
borderColor: colors.red4,
color: colors.red4,
borderRadius: 30,
@@ -891,6 +890,6 @@ const styles = StyleSheet.create({
paddingLeft: 7,
paddingRight: 16,
alignItems: 'center',
- borderTopWidth: hairlineWidth,
+ borderTopWidth: StyleSheet.hairlineWidth,
},
})
diff --git a/src/view/com/composer/Prompt.tsx b/src/view/com/composer/Prompt.tsx
index 54defb101c..20637c7e9f 100644
--- a/src/view/com/composer/Prompt.tsx
+++ b/src/view/com/composer/Prompt.tsx
@@ -9,7 +9,6 @@ import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {Text} from '../util/text/Text'
import {UserAvatar} from '../util/UserAvatar'
-import hairlineWidth = StyleSheet.hairlineWidth
export function ComposePrompt({onPressCompose}: {onPressCompose: () => void}) {
const {currentAccount} = useSession()
@@ -49,7 +48,7 @@ const styles = StyleSheet.create({
paddingBottom: 10,
flexDirection: 'row',
alignItems: 'center',
- borderTopWidth: hairlineWidth,
+ borderTopWidth: StyleSheet.hairlineWidth,
},
labelMobile: {
paddingLeft: 12,
diff --git a/src/view/com/feeds/FeedSourceCard.tsx b/src/view/com/feeds/FeedSourceCard.tsx
index c44353c8bf..68437c37a0 100644
--- a/src/view/com/feeds/FeedSourceCard.tsx
+++ b/src/view/com/feeds/FeedSourceCard.tsx
@@ -13,6 +13,7 @@ import {msg, Plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {logger} from '#/logger'
+import {shouldClickOpenNewTab} from '#/platform/urls'
import {FeedSourceInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
import {
useAddSavedFeedsMutation,
@@ -32,8 +33,6 @@ import * as Prompt from '#/components/Prompt'
import {RichText} from '#/components/RichText'
import {Text} from '../util/text/Text'
import {UserAvatar} from '../util/UserAvatar'
-import hairlineWidth = StyleSheet.hairlineWidth
-import {shouldClickOpenNewTab} from '#/platform/urls'
export function FeedSourceCard({
feedUri,
@@ -209,7 +208,7 @@ export function FeedSourceCardLoaded({
styles.container,
pal.border,
style,
- {borderTopWidth: hideTopBorder ? 0 : hairlineWidth},
+ {borderTopWidth: hideTopBorder ? 0 : StyleSheet.hairlineWidth},
]}
onPress={e => {
const shouldOpenInNewTab = shouldClickOpenNewTab(e)
@@ -330,7 +329,7 @@ const styles = StyleSheet.create({
gap: 14,
},
border: {
- borderTopWidth: hairlineWidth,
+ borderTopWidth: StyleSheet.hairlineWidth,
},
headerContainer: {
flexDirection: 'row',
diff --git a/src/view/com/home/HomeHeaderLayoutMobile.tsx b/src/view/com/home/HomeHeaderLayoutMobile.tsx
index ed353cf168..e537abfaad 100644
--- a/src/view/com/home/HomeHeaderLayoutMobile.tsx
+++ b/src/view/com/home/HomeHeaderLayoutMobile.tsx
@@ -73,7 +73,7 @@ export function HomeHeaderLayoutMobile({
]}>
{IS_DEV && (
<>
-
+
>
diff --git a/src/view/com/lists/ListCard.tsx b/src/view/com/lists/ListCard.tsx
index 3adf5c09da..587885502b 100644
--- a/src/view/com/lists/ListCard.tsx
+++ b/src/view/com/lists/ListCard.tsx
@@ -14,7 +14,6 @@ import {RichText as RichTextCom} from '#/components/RichText'
import {Link} from '../util/Link'
import {Text} from '../util/text/Text'
import {UserAvatar} from '../util/UserAvatar'
-import hairlineWidth = StyleSheet.hairlineWidth
export const ListCard = ({
testID,
@@ -134,7 +133,7 @@ export const ListCard = ({
const styles = StyleSheet.create({
outer: {
- borderTopWidth: hairlineWidth,
+ borderTopWidth: StyleSheet.hairlineWidth,
paddingHorizontal: 6,
},
outerNoBorder: {
diff --git a/src/view/com/modals/UserAddRemoveLists.tsx b/src/view/com/modals/UserAddRemoveLists.tsx
index 16c366d8af..f6db94ed85 100644
--- a/src/view/com/modals/UserAddRemoveLists.tsx
+++ b/src/view/com/modals/UserAddRemoveLists.tsx
@@ -29,7 +29,6 @@ import {Button} from '../util/forms/Button'
import {Text} from '../util/text/Text'
import * as Toast from '../util/Toast'
import {UserAvatar} from '../util/UserAvatar'
-import hairlineWidth = StyleSheet.hairlineWidth
export const snapPoints = ['fullscreen']
@@ -63,7 +62,7 @@ export function Component({
return [pal.border, {height: screenHeight / 1.5}]
}
- return [pal.border, {flex: 1, borderTopWidth: hairlineWidth}]
+ return [pal.border, {flex: 1, borderTopWidth: StyleSheet.hairlineWidth}]
}, [pal.border, screenHeight])
return (
@@ -188,7 +187,7 @@ function ListItem({
style={[
styles.listItem,
pal.border,
- index !== 0 && {borderTopWidth: hairlineWidth},
+ index !== 0 && {borderTopWidth: StyleSheet.hairlineWidth},
]}>
@@ -248,7 +247,7 @@ const styles = StyleSheet.create({
gap: 10,
paddingTop: 10,
paddingBottom: isAndroid ? 10 : 0,
- borderTopWidth: hairlineWidth,
+ borderTopWidth: StyleSheet.hairlineWidth,
},
footerBtn: {
paddingHorizontal: 24,
diff --git a/src/view/com/notifications/Feed.tsx b/src/view/com/notifications/Feed.tsx
index 3e7fdfc713..82755de1d3 100644
--- a/src/view/com/notifications/Feed.tsx
+++ b/src/view/com/notifications/Feed.tsx
@@ -24,7 +24,6 @@ import {NotificationFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceho
import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn'
import {CenteredView} from '#/view/com/util/Views'
import {FeedItem} from './FeedItem'
-import hairlineWidth = StyleSheet.hairlineWidth
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
@@ -137,7 +136,7 @@ export function Feed({
diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx
index d31962ff35..520a059ae2 100644
--- a/src/view/com/notifications/FeedItem.tsx
+++ b/src/view/com/notifications/FeedItem.tsx
@@ -20,20 +20,28 @@ import {
import {AtUri} from '@atproto/api'
import {msg, plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
+import {useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
import {useGate} from '#/lib/statsig/statsig'
+import {parseTenorGif} from '#/lib/strings/embed-player'
+import {logger} from '#/logger'
import {FeedNotification} from '#/state/queries/notifications/feed'
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
import {usePalette} from 'lib/hooks/usePalette'
import {makeProfileLink} from 'lib/routes/links'
+import {NavigationProp} from 'lib/routes/types'
+import {forceLTR} from 'lib/strings/bidi'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles'
import {niceDate} from 'lib/strings/time'
import {colors, s} from 'lib/styles'
import {isWeb} from 'platform/detection'
+import {DM_SERVICE_HEADERS} from 'state/queries/messages/const'
import {precacheProfile} from 'state/queries/profile'
+import {useAgent} from 'state/session'
import {atoms as a, useTheme} from '#/alf'
+import {Button, ButtonText} from '#/components/Button'
import {
ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon,
ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon,
@@ -41,8 +49,10 @@ import {
import {Heart2_Filled_Stroke2_Corner0_Rounded as HeartIconFilled} from '#/components/icons/Heart2'
import {PersonPlus_Filled_Stroke2_Corner0_Rounded as PersonPlusIcon} from '#/components/icons/Person'
import {Repost_Stroke2_Corner2_Rounded as RepostIcon} from '#/components/icons/Repost'
+import {StarterPack} from '#/components/icons/StarterPack'
import {Link as NewLink} from '#/components/Link'
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
+import {Notification as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
import {FeedSourceCard} from '../feeds/FeedSourceCard'
import {Post} from '../post/Post'
import {ImageHorzList} from '../util/images/ImageHorzList'
@@ -52,19 +62,6 @@ import {Text} from '../util/text/Text'
import {TimeElapsed} from '../util/TimeElapsed'
import {PreviewableUserAvatar, UserAvatar} from '../util/UserAvatar'
-import hairlineWidth = StyleSheet.hairlineWidth
-import {useNavigation} from '@react-navigation/native'
-
-import {parseTenorGif} from '#/lib/strings/embed-player'
-import {logger} from '#/logger'
-import {NavigationProp} from 'lib/routes/types'
-import {forceLTR} from 'lib/strings/bidi'
-import {DM_SERVICE_HEADERS} from 'state/queries/messages/const'
-import {useAgent} from 'state/session'
-import {Button, ButtonText} from '#/components/Button'
-import {StarterPack} from '#/components/icons/StarterPack'
-import {Notification as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
-
const MAX_AUTHORS = 5
const EXPANDED_AUTHOR_EL_HEIGHT = 35
@@ -171,7 +168,6 @@ let FeedItem = ({
)
}
- let isFollowBack = false
let action = ''
let icon = (
1 ? formatCount(authors.length - 1) : ''
+ const formattedCount =
+ authors.length > 1 ? formatCount(authors.length - 1) : ''
+ const firstAuthorName = sanitizeDisplayName(
+ authors[0].profile.displayName || authors[0].profile.handle,
+ )
+ const niceTimestamp = niceDate(item.notification.indexedAt)
+ const a11yLabelUsers =
+ authors.length > 1
+ ? _(msg` and `) +
+ plural(authors.length - 1, {
+ one: `${formattedCount} other`,
+ other: `${formattedCount} others`,
+ })
+ : ''
+ const a11yLabel = `${firstAuthorName}${a11yLabelUsers} ${action} ${niceTimestamp}`
+
return (
1
@@ -272,19 +284,18 @@ let FeedItem = ({
visible={!isAuthorsExpanded}
authors={authors}
onToggleAuthorsExpanded={onToggleAuthorsExpanded}
- showDmButton={item.type === 'starterpack-joined' || isFollowBack}
+ showDmButton={item.type === 'starterpack-joined'}
/>
-
+
{authors.length > 1 ? (
@@ -306,7 +317,7 @@ let FeedItem = ({
{({timeElapsed}) => (
+ title={niceTimestamp}>
{' ' + timeElapsed}
)}
@@ -458,7 +469,6 @@ function CondensedAuthorsList({
profile={authors[0].profile}
moderation={authors[0].moderation.ui('avatar')}
type={authors[0].profile.associated?.labeler ? 'labeler' : 'user'}
- accessible={false}
/>
{showDmButton ? : null}
@@ -476,7 +486,6 @@ function CondensedAuthorsList({
profile={author.profile}
moderation={author.moderation.ui('avatar')}
type={author.profile.associated?.labeler ? 'labeler' : 'user'}
- accessible={false}
/>
))}
diff --git a/src/view/com/pager/TabBar.tsx b/src/view/com/pager/TabBar.tsx
index 1c83ecd6e2..4801ae5e32 100644
--- a/src/view/com/pager/TabBar.tsx
+++ b/src/view/com/pager/TabBar.tsx
@@ -7,7 +7,6 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {PressableWithHover} from '../util/PressableWithHover'
import {Text} from '../util/text/Text'
import {DraggableScrollView} from './DraggableScrollView'
-import hairlineWidth = StyleSheet.hairlineWidth
export interface TabBarProps {
testID?: string
@@ -208,6 +207,6 @@ const mobileStyles = StyleSheet.create({
left: 0,
right: 0,
top: '100%',
- borderBottomWidth: hairlineWidth,
+ borderBottomWidth: StyleSheet.hairlineWidth,
},
})
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index 4e6ed27bed..63dac300a8 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -44,7 +44,6 @@ import {PostEmbeds} from '../util/post-embeds'
import {PostMeta} from '../util/PostMeta'
import {Text} from '../util/text/Text'
import {PreviewableUserAvatar} from '../util/UserAvatar'
-import hairlineWidth = StyleSheet.hairlineWidth
export function PostThreadItem({
post,
@@ -604,7 +603,7 @@ function PostOuterWrapper({
{
flexDirection: 'row',
paddingHorizontal: isMobile ? 10 : 6,
- borderTopWidth: depth === 1 ? hairlineWidth : 0,
+ borderTopWidth: depth === 1 ? StyleSheet.hairlineWidth : 0,
},
]}>
{Array.from(Array(depth - 1)).map((_, n: number) => (
@@ -699,7 +698,7 @@ function getThreadAuthor(
const styles = StyleSheet.create({
outer: {
- borderTopWidth: hairlineWidth,
+ borderTopWidth: StyleSheet.hairlineWidth,
paddingLeft: 8,
},
outerHighlighted: {
@@ -709,7 +708,7 @@ const styles = StyleSheet.create({
paddingRight: 8,
},
outerHighlightedRoot: {
- borderTopWidth: hairlineWidth,
+ borderTopWidth: StyleSheet.hairlineWidth,
paddingTop: 16,
},
noTopBorder: {
@@ -761,8 +760,8 @@ const styles = StyleSheet.create({
expandedInfo: {
flexDirection: 'row',
padding: 10,
- borderTopWidth: hairlineWidth,
- borderBottomWidth: hairlineWidth,
+ borderTopWidth: StyleSheet.hairlineWidth,
+ borderBottomWidth: StyleSheet.hairlineWidth,
marginTop: 5,
marginBottom: 10,
},
diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx
index a05339d4dc..ade0e7f10f 100644
--- a/src/view/com/post/Post.tsx
+++ b/src/view/com/post/Post.tsx
@@ -37,7 +37,6 @@ import {PostMeta} from '../util/PostMeta'
import {Text} from '../util/text/Text'
import {PreviewableUserAvatar} from '../util/UserAvatar'
import {UserInfoText} from '../util/UserInfoText'
-import hairlineWidth = StyleSheet.hairlineWidth
export function Post({
post,
@@ -155,7 +154,7 @@ function PostInner({
style={[
styles.outer,
pal.border,
- !hideTopBorder && {borderTopWidth: hairlineWidth},
+ !hideTopBorder && {borderTopWidth: StyleSheet.hairlineWidth},
style,
]}
onBeforePress={onBeforePress}>
diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx
index a59eeea52e..9ddc54a989 100644
--- a/src/view/com/posts/FeedItem.tsx
+++ b/src/view/com/posts/FeedItem.tsx
@@ -16,8 +16,10 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
+import {useGate} from '#/lib/statsig/statsig'
import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow'
import {useFeedFeedbackContext} from '#/state/feed-feedback'
+import {useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell/composer'
import {isReasonFeedSource, ReasonFeedSource} from 'lib/api/feed/types'
import {MAX_POST_LINES} from 'lib/constants'
@@ -29,6 +31,7 @@ import {countLines} from 'lib/strings/helpers'
import {s} from 'lib/styles'
import {precacheProfile} from 'state/queries/profile'
import {atoms as a} from '#/alf'
+import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost'
import {ContentHider} from '#/components/moderation/ContentHider'
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
import {RichText} from '#/components/RichText'
@@ -38,17 +41,19 @@ import {FeedNameText} from '../util/FeedInfoText'
import {Link, TextLink, TextLinkOnWebOnly} from '../util/Link'
import {PostCtrls} from '../util/post-ctrls/PostCtrls'
import {PostEmbeds} from '../util/post-embeds'
+import {VideoEmbed} from '../util/post-embeds/VideoEmbed'
import {PostMeta} from '../util/PostMeta'
import {Text} from '../util/text/Text'
import {PreviewableUserAvatar} from '../util/UserAvatar'
import {AviFollowButton} from './AviFollowButton'
-import hairlineWidth = StyleSheet.hairlineWidth
-import {useSession} from '#/state/session'
-import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost'
interface FeedItemProps {
record: AppBskyFeedPost.Record
- reason: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource | undefined
+ reason:
+ | AppBskyFeedDefs.ReasonRepost
+ | ReasonFeedSource
+ | {[k: string]: unknown; $type: string}
+ | undefined
moderation: ModerationDecision
parentAuthor: AppBskyActorDefs.ProfileViewBasic | undefined
showReplyTo: boolean
@@ -132,6 +137,8 @@ let FeedItemInner = ({
const {openComposer} = useComposerControls()
const pal = usePalette('default')
const {_} = useLingui()
+ const gate = useGate()
+
const href = useMemo(() => {
const urip = new AtUri(post.uri)
return makeProfileLink(post.author, 'post', urip.rkey)
@@ -197,7 +204,8 @@ let FeedItemInner = ({
isThreadLastChild || (!isThreadChild && !isThreadParent)
? 8
: undefined,
- borderTopWidth: hideTopBorder || isThreadChild ? 0 : hairlineWidth,
+ borderTopWidth:
+ hideTopBorder || isThreadChild ? 0 : StyleSheet.hairlineWidth,
},
]
@@ -337,9 +345,11 @@ let FeedItemInner = ({
postHref={href}
onOpenAuthor={onOpenAuthor}
/>
- {!isThreadChild && showReplyTo && parentAuthor && (
-
- )}
+ {!isThreadChild &&
+ showReplyTo &&
+ (parentAuthor || isParentBlocked) && (
+
+ )}
+ {__DEV__ && gate('videos') && (
+
+ )}
Reply to a blocked post
+ } else if (profile != null) {
+ const isMe = profile.did === currentAccount?.did
+ if (isMe) {
+ label = Reply to you
+ } else {
+ label = (
+
+ Reply to{' '}
+
+
+
+
+ )
+ }
+ }
+
+ if (!label) {
+ // Should not happen.
+ return null
+ }
return (
@@ -450,29 +497,7 @@ function ReplyToLabel({
style={[pal.textLight, s.mr2]}
lineHeight={1.2}
numberOfLines={1}>
- {isMe ? (
- Reply to you
- ) : blocked ? (
- Reply to a blocked post
- ) : (
-
- Reply to{' '}
-
-
-
-
- )}
+ {label}
)
diff --git a/src/view/com/posts/FeedShutdownMsg.tsx b/src/view/com/posts/FeedShutdownMsg.tsx
index 36b1706cb1..f12ecf12b6 100644
--- a/src/view/com/posts/FeedShutdownMsg.tsx
+++ b/src/view/com/posts/FeedShutdownMsg.tsx
@@ -99,6 +99,7 @@ export function FeedShutdownMsg({feedUri}: {feedUri: string}) {
This feed is no longer online. We are showing{' '}
Discover
diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx
index 9458fbaf8e..fd32e37a42 100644
--- a/src/view/com/profile/ProfileCard.tsx
+++ b/src/view/com/profile/ProfileCard.tsx
@@ -23,12 +23,11 @@ import {
KnownFollowers,
shouldShowKnownFollowers,
} from '#/components/KnownFollowers'
+import * as Pills from '#/components/Pills'
import {Link} from '../util/Link'
import {Text} from '../util/text/Text'
import {PreviewableUserAvatar} from '../util/UserAvatar'
import {FollowButton} from './FollowButton'
-import hairlineWidth = StyleSheet.hairlineWidth
-import * as Pills from '#/components/Pills'
export function ProfileCard({
testID,
@@ -217,7 +216,7 @@ export function ProfileCardWithFollowBtn({
const styles = StyleSheet.create({
outer: {
- borderTopWidth: hairlineWidth,
+ borderTopWidth: StyleSheet.hairlineWidth,
paddingHorizontal: 6,
paddingVertical: 4,
},
diff --git a/src/view/com/profile/ProfileSubpageHeader.tsx b/src/view/com/profile/ProfileSubpageHeader.tsx
index ac5febcda1..e07acef281 100644
--- a/src/view/com/profile/ProfileSubpageHeader.tsx
+++ b/src/view/com/profile/ProfileSubpageHeader.tsx
@@ -15,15 +15,13 @@ import {makeProfileLink} from 'lib/routes/links'
import {NavigationProp} from 'lib/routes/types'
import {sanitizeHandle} from 'lib/strings/handles'
import {isNative} from 'platform/detection'
+import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
+import {StarterPack} from '#/components/icons/StarterPack'
import {TextLink} from '../util/Link'
import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
import {Text} from '../util/text/Text'
import {UserAvatar, UserAvatarType} from '../util/UserAvatar'
import {CenteredView} from '../util/Views'
-import hairlineWidth = StyleSheet.hairlineWidth
-
-import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
-import {StarterPack} from '#/components/icons/StarterPack'
export function ProfileSubpageHeader({
isLoading,
@@ -84,7 +82,7 @@ export function ProfileSubpageHeader({
{
flexDirection: 'row',
alignItems: 'center',
- borderBottomWidth: hairlineWidth,
+ borderBottomWidth: StyleSheet.hairlineWidth,
paddingTop: isNative ? 0 : 8,
paddingBottom: 8,
paddingHorizontal: isMobile ? 12 : 14,
diff --git a/src/view/com/util/LoadingPlaceholder.tsx b/src/view/com/util/LoadingPlaceholder.tsx
index adeefd4ddb..6e75e88ca6 100644
--- a/src/view/com/util/LoadingPlaceholder.tsx
+++ b/src/view/com/util/LoadingPlaceholder.tsx
@@ -17,7 +17,6 @@ import {
Heart2_Stroke2_Corner0_Rounded as HeartIconOutline,
} from '#/components/icons/Heart2'
import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost'
-import hairlineWidth = StyleSheet.hairlineWidth
export function LoadingPlaceholder({
width,
@@ -236,7 +235,7 @@ export function FeedLoadingPlaceholder({
{
paddingHorizontal: 12,
paddingVertical: 18,
- borderTopWidth: showTopBorder ? hairlineWidth : 0,
+ borderTopWidth: showTopBorder ? StyleSheet.hairlineWidth : 0,
},
pal.border,
style,
diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx
index b727234093..8862bd0e4a 100644
--- a/src/view/com/util/UserAvatar.tsx
+++ b/src/view/com/util/UserAvatar.tsx
@@ -55,7 +55,6 @@ interface PreviewableUserAvatarProps extends BaseUserAvatarProps {
profile: AppBskyActorDefs.ProfileViewBasic
disableHoverCard?: boolean
onBeforePress?: () => void
- accessible?: boolean
}
const BLUR_AMOUNT = isWeb ? 5 : 100
@@ -412,7 +411,6 @@ let PreviewableUserAvatar = ({
profile,
disableHoverCard,
onBeforePress,
- accessible = true,
...rest
}: PreviewableUserAvatarProps): React.ReactNode => {
const {_} = useLingui()
@@ -426,12 +424,8 @@ let PreviewableUserAvatar = ({
return (
@@ -243,7 +242,7 @@ const styles = StyleSheet.create({
marginRight: 'auto',
},
border: {
- borderBottomWidth: hairlineWidth,
+ borderBottomWidth: StyleSheet.hairlineWidth,
},
titleContainer: {
marginLeft: 'auto',
diff --git a/src/view/com/util/Views.web.tsx b/src/view/com/util/Views.web.tsx
index 21998bfbdd..ed18c981a6 100644
--- a/src/view/com/util/Views.web.tsx
+++ b/src/view/com/util/Views.web.tsx
@@ -26,7 +26,6 @@ import Animated from 'react-native-reanimated'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {addStyle} from 'lib/styles'
-import hairlineWidth = StyleSheet.hairlineWidth
interface AddedProps {
desktopFixedHeight?: boolean | number
@@ -50,8 +49,8 @@ export const CenteredView = React.forwardRef(function CenteredView(
}
if (sideBorders) {
style = addStyle(style, {
- borderLeftWidth: hairlineWidth,
- borderRightWidth: hairlineWidth,
+ borderLeftWidth: StyleSheet.hairlineWidth,
+ borderRightWidth: StyleSheet.hairlineWidth,
})
style = addStyle(style, pal.border)
}
@@ -163,8 +162,8 @@ export const ScrollView = React.forwardRef(function ScrollViewImpl(
const styles = StyleSheet.create({
contentContainer: {
- borderLeftWidth: hairlineWidth,
- borderRightWidth: hairlineWidth,
+ borderLeftWidth: StyleSheet.hairlineWidth,
+ borderRightWidth: StyleSheet.hairlineWidth,
// @ts-ignore web only
minHeight: '100vh',
},
diff --git a/src/view/com/util/load-latest/LoadLatestBtn.tsx b/src/view/com/util/load-latest/LoadLatestBtn.tsx
index 9de03a4f33..2310b1f271 100644
--- a/src/view/com/util/load-latest/LoadLatestBtn.tsx
+++ b/src/view/com/util/load-latest/LoadLatestBtn.tsx
@@ -13,7 +13,6 @@ import {clamp} from '#/lib/numbers'
import {colors} from '#/lib/styles'
import {isWeb} from '#/platform/detection'
import {useSession} from '#/state/session'
-import hairlineWidth = StyleSheet.hairlineWidth
const AnimatedTouchableOpacity =
Animated.createAnimatedComponent(TouchableOpacity)
@@ -74,7 +73,7 @@ const styles = StyleSheet.create({
// @ts-ignore 'fixed' is web only -prf
position: isWeb ? 'fixed' : 'absolute',
left: 18,
- borderWidth: hairlineWidth,
+ borderWidth: StyleSheet.hairlineWidth,
width: 52,
height: 52,
borderRadius: 26,
diff --git a/src/view/com/util/post-embeds/ActiveVideoContext.tsx b/src/view/com/util/post-embeds/ActiveVideoContext.tsx
new file mode 100644
index 0000000000..6804436a7e
--- /dev/null
+++ b/src/view/com/util/post-embeds/ActiveVideoContext.tsx
@@ -0,0 +1,48 @@
+import React, {useCallback, useId, useMemo, useState} from 'react'
+
+import {VideoPlayerProvider} from './VideoPlayerContext'
+
+const ActiveVideoContext = React.createContext<{
+ activeViewId: string | null
+ setActiveView: (viewId: string, src: string) => void
+} | null>(null)
+
+export function ActiveVideoProvider({children}: {children: React.ReactNode}) {
+ const [activeViewId, setActiveViewId] = useState(null)
+ const [source, setSource] = useState(null)
+
+ const value = useMemo(
+ () => ({
+ activeViewId,
+ setActiveView: (viewId: string, src: string) => {
+ setActiveViewId(viewId)
+ setSource(src)
+ },
+ }),
+ [activeViewId],
+ )
+
+ return (
+
+
+ {children}
+
+
+ )
+}
+
+export function useActiveVideoView() {
+ const context = React.useContext(ActiveVideoContext)
+ if (!context) {
+ throw new Error('useActiveVideo must be used within a ActiveVideoProvider')
+ }
+ const id = useId()
+
+ return {
+ active: context.activeViewId === id,
+ setActive: useCallback(
+ (source: string) => context.setActiveView(id, source),
+ [context, id],
+ ),
+ }
+}
diff --git a/src/view/com/util/post-embeds/QuoteEmbed.tsx b/src/view/com/util/post-embeds/QuoteEmbed.tsx
index 4e2e19f587..20c05b692b 100644
--- a/src/view/com/util/post-embeds/QuoteEmbed.tsx
+++ b/src/view/com/util/post-embeds/QuoteEmbed.tsx
@@ -39,7 +39,6 @@ import {Link} from '../Link'
import {PostMeta} from '../PostMeta'
import {Text} from '../text/Text'
import {PostEmbeds} from '.'
-import hairlineWidth = StyleSheet.hairlineWidth
export function MaybeQuoteEmbed({
embed,
@@ -262,7 +261,7 @@ const styles = StyleSheet.create({
marginTop: 8,
paddingVertical: 12,
paddingHorizontal: 12,
- borderWidth: hairlineWidth,
+ borderWidth: StyleSheet.hairlineWidth,
},
errorContainer: {
flexDirection: 'row',
@@ -272,7 +271,7 @@ const styles = StyleSheet.create({
marginTop: 8,
paddingVertical: 14,
paddingHorizontal: 14,
- borderWidth: hairlineWidth,
+ borderWidth: StyleSheet.hairlineWidth,
},
alert: {
marginBottom: 6,
diff --git a/src/view/com/util/post-embeds/VideoEmbed.tsx b/src/view/com/util/post-embeds/VideoEmbed.tsx
new file mode 100644
index 0000000000..5e5293a553
--- /dev/null
+++ b/src/view/com/util/post-embeds/VideoEmbed.tsx
@@ -0,0 +1,44 @@
+import React, {useCallback} from 'react'
+import {View} from 'react-native'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {atoms as a, useTheme} from '#/alf'
+import {Button, ButtonIcon} from '#/components/Button'
+import {Play_Filled_Corner2_Rounded as PlayIcon} from '#/components/icons/Play'
+import {useActiveVideoView} from './ActiveVideoContext'
+import {VideoEmbedInner} from './VideoEmbedInner'
+
+export function VideoEmbed({source}: {source: string}) {
+ const t = useTheme()
+ const {active, setActive} = useActiveVideoView()
+ const {_} = useLingui()
+
+ const onPress = useCallback(() => setActive(source), [setActive, source])
+
+ return (
+
+ {active ? (
+
+ ) : (
+
+ )}
+
+ )
+}
diff --git a/src/view/com/util/post-embeds/VideoEmbedInner.tsx b/src/view/com/util/post-embeds/VideoEmbedInner.tsx
new file mode 100644
index 0000000000..ef06787097
--- /dev/null
+++ b/src/view/com/util/post-embeds/VideoEmbedInner.tsx
@@ -0,0 +1,138 @@
+import React, {useCallback, useEffect, useRef, useState} from 'react'
+import {Pressable, StyleSheet, useWindowDimensions, View} from 'react-native'
+import Animated, {
+ measure,
+ runOnJS,
+ useAnimatedRef,
+ useFrameCallback,
+ useSharedValue,
+} from 'react-native-reanimated'
+import {VideoPlayer, VideoView} from 'expo-video'
+
+import {atoms as a} from '#/alf'
+import {Text} from '#/components/Typography'
+import {useVideoPlayer} from './VideoPlayerContext'
+
+export const VideoEmbedInner = ({}: {source: string}) => {
+ const player = useVideoPlayer()
+ const aref = useAnimatedRef()
+ const {height: windowHeight} = useWindowDimensions()
+ const hasLeftView = useSharedValue(false)
+ const ref = useRef(null)
+
+ const onEnterView = useCallback(() => {
+ if (player.status === 'readyToPlay') {
+ player.play()
+ }
+ }, [player])
+
+ const onLeaveView = useCallback(() => {
+ player.pause()
+ }, [player])
+
+ const enterFullscreen = useCallback(() => {
+ if (ref.current) {
+ ref.current.enterFullscreen()
+ }
+ }, [])
+
+ useFrameCallback(() => {
+ const measurement = measure(aref)
+
+ if (measurement) {
+ if (hasLeftView.value) {
+ // Check if the video is in view
+ if (
+ measurement.pageY >= 0 &&
+ measurement.pageY + measurement.height <= windowHeight
+ ) {
+ runOnJS(onEnterView)()
+ hasLeftView.value = false
+ }
+ } else {
+ // Check if the video is out of view
+ if (
+ measurement.pageY + measurement.height < 0 ||
+ measurement.pageY > windowHeight
+ ) {
+ runOnJS(onLeaveView)()
+ hasLeftView.value = true
+ }
+ }
+ }
+ })
+
+ return (
+
+
+
+
+ )
+}
+
+function VideoControls({
+ player,
+ enterFullscreen,
+}: {
+ player: VideoPlayer
+ enterFullscreen: () => void
+}) {
+ const [currentTime, setCurrentTime] = useState(Math.floor(player.currentTime))
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setCurrentTime(Math.floor(player.duration - player.currentTime))
+ // how often should we update the time?
+ // 1000 gets out of sync with the video time
+ }, 250)
+
+ return () => {
+ clearInterval(interval)
+ }
+ }, [player])
+
+ const minutes = Math.floor(currentTime / 60)
+ const seconds = String(currentTime % 60).padStart(2, '0')
+
+ return (
+
+
+
+ {minutes}:{seconds}
+
+
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ timeContainer: {
+ backgroundColor: 'rgba(0, 0, 0, 0.75)',
+ borderRadius: 6,
+ paddingHorizontal: 6,
+ paddingVertical: 3,
+ position: 'absolute',
+ left: 5,
+ bottom: 5,
+ },
+ timeElapsed: {
+ color: 'white',
+ fontSize: 12,
+ fontWeight: 'bold',
+ },
+})
diff --git a/src/view/com/util/post-embeds/VideoEmbedInner.web.tsx b/src/view/com/util/post-embeds/VideoEmbedInner.web.tsx
new file mode 100644
index 0000000000..cb02743c6f
--- /dev/null
+++ b/src/view/com/util/post-embeds/VideoEmbedInner.web.tsx
@@ -0,0 +1,52 @@
+import React, {useEffect, useRef} from 'react'
+import Hls from 'hls.js'
+
+import {atoms as a} from '#/alf'
+
+export const VideoEmbedInner = ({source}: {source: string}) => {
+ const ref = useRef(null)
+
+ // Use HLS.js to play HLS video
+ useEffect(() => {
+ if (ref.current) {
+ if (ref.current.canPlayType('application/vnd.apple.mpegurl')) {
+ ref.current.src = source
+ } else if (Hls.isSupported()) {
+ var hls = new Hls()
+ hls.loadSource(source)
+ hls.attachMedia(ref.current)
+ } else {
+ // TODO: fallback
+ }
+ }
+ }, [source])
+
+ useEffect(() => {
+ if (ref.current) {
+ const observer = new IntersectionObserver(
+ ([entry]) => {
+ if (ref.current) {
+ if (entry.isIntersecting) {
+ if (ref.current.paused) {
+ ref.current.play()
+ }
+ } else {
+ if (!ref.current.paused) {
+ ref.current.pause()
+ }
+ }
+ }
+ },
+ {threshold: 0},
+ )
+
+ observer.observe(ref.current)
+
+ return () => {
+ observer.disconnect()
+ }
+ }
+ }, [])
+
+ return
+}
diff --git a/src/view/com/util/post-embeds/VideoPlayerContext.tsx b/src/view/com/util/post-embeds/VideoPlayerContext.tsx
new file mode 100644
index 0000000000..bc5d9d370c
--- /dev/null
+++ b/src/view/com/util/post-embeds/VideoPlayerContext.tsx
@@ -0,0 +1,41 @@
+import React, {useContext, useEffect} from 'react'
+import type {VideoPlayer} from 'expo-video'
+import {useVideoPlayer as useExpoVideoPlayer} from 'expo-video'
+
+const VideoPlayerContext = React.createContext(null)
+
+export function VideoPlayerProvider({
+ viewId,
+ source,
+ children,
+}: {
+ viewId: string | null
+ source: string
+ children: React.ReactNode
+}) {
+ // eslint-disable-next-line @typescript-eslint/no-shadow
+ const player = useExpoVideoPlayer(source, player => {
+ player.loop = true
+ player.play()
+ })
+
+ // make sure we're playing every time the viewId changes
+ // this means the video is different
+ useEffect(() => {
+ player.play()
+ }, [viewId, player])
+
+ return (
+
+ {children}
+
+ )
+}
+
+export function useVideoPlayer() {
+ const context = useContext(VideoPlayerContext)
+ if (!context) {
+ throw new Error('useVideoPlayer must be used within a VideoPlayerProvider')
+ }
+ return context
+}
diff --git a/src/view/com/util/post-embeds/VideoPlayerContext.web.tsx b/src/view/com/util/post-embeds/VideoPlayerContext.web.tsx
new file mode 100644
index 0000000000..329fb1206e
--- /dev/null
+++ b/src/view/com/util/post-embeds/VideoPlayerContext.web.tsx
@@ -0,0 +1,9 @@
+import React from 'react'
+
+export function VideoPlayerProvider({children}: {children: React.ReactNode}) {
+ return children
+}
+
+export function useVideoPlayer() {
+ throw new Error('useVideoPlayer must not be used on web')
+}
diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx
index 3fd55f5c21..a0dc94e4d8 100644
--- a/src/view/com/util/post-embeds/index.tsx
+++ b/src/view/com/util/post-embeds/index.tsx
@@ -19,18 +19,17 @@ import {
} from '@atproto/api'
import {ImagesLightbox, useLightboxControls} from '#/state/lightbox'
+import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
import {usePalette} from 'lib/hooks/usePalette'
import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
import {atoms as a} from '#/alf'
+import {Embed as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
import {ContentHider} from '../../../../components/moderation/ContentHider'
import {AutoSizedImage} from '../images/AutoSizedImage'
import {ImageLayoutGrid} from '../images/ImageLayoutGrid'
import {ExternalLinkEmbed} from './ExternalLinkEmbed'
import {ListEmbed} from './ListEmbed'
import {MaybeQuoteEmbed} from './QuoteEmbed'
-import hairlineWidth = StyleSheet.hairlineWidth
-import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
-import {Embed as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
type Embed =
| AppBskyEmbedRecord.View
@@ -205,7 +204,7 @@ const styles = StyleSheet.create({
fontWeight: 'bold',
},
customFeedOuter: {
- borderWidth: hairlineWidth,
+ borderWidth: StyleSheet.hairlineWidth,
borderRadius: 8,
marginTop: 4,
paddingHorizontal: 12,
diff --git a/src/view/screens/Feeds.tsx b/src/view/screens/Feeds.tsx
index 5a2d71087c..310c3dc602 100644
--- a/src/view/screens/Feeds.tsx
+++ b/src/view/screens/Feeds.tsx
@@ -33,14 +33,13 @@ import {ViewHeader} from 'view/com/util/ViewHeader'
import {NoFollowingFeed} from '#/screens/Feeds/NoFollowingFeed'
import {NoSavedFeedsOfAnyType} from '#/screens/Feeds/NoSavedFeedsOfAnyType'
import {atoms as a, useTheme} from '#/alf'
+import {Divider} from '#/components/Divider'
+import * as FeedCard from '#/components/FeedCard'
import {IconCircle} from '#/components/IconCircle'
+import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline'
import {ListMagnifyingGlass_Stroke2_Corner0_Rounded} from '#/components/icons/ListMagnifyingGlass'
import {ListSparkle_Stroke2_Corner0_Rounded} from '#/components/icons/ListSparkle'
-import hairlineWidth = StyleSheet.hairlineWidth
-import {Divider} from '#/components/Divider'
-import * as FeedCard from '#/components/FeedCard'
-import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
import * as ListCard from '#/components/ListCard'
type Props = NativeStackScreenProps
@@ -793,13 +792,13 @@ const styles = StyleSheet.create({
paddingHorizontal: 16,
paddingVertical: 14,
gap: 12,
- borderBottomWidth: hairlineWidth,
+ borderBottomWidth: StyleSheet.hairlineWidth,
},
savedFeedMobile: {
paddingVertical: 10,
},
offlineSlug: {
- borderWidth: hairlineWidth,
+ borderWidth: StyleSheet.hairlineWidth,
borderRadius: 4,
paddingHorizontal: 4,
paddingVertical: 2,
diff --git a/src/view/screens/Lists.tsx b/src/view/screens/Lists.tsx
index 12ea6f48be..9daeaba187 100644
--- a/src/view/screens/Lists.tsx
+++ b/src/view/screens/Lists.tsx
@@ -16,7 +16,6 @@ import {MyLists} from '#/view/com/lists/MyLists'
import {Button} from 'view/com/util/forms/Button'
import {SimpleViewHeader} from 'view/com/util/SimpleViewHeader'
import {Text} from 'view/com/util/text/Text'
-import hairlineWidth = StyleSheet.hairlineWidth
type Props = NativeStackScreenProps
export function ListsScreen({}: Props) {
@@ -55,8 +54,11 @@ export function ListsScreen({}: Props) {
style={[
pal.border,
isMobile
- ? {borderBottomWidth: hairlineWidth}
- : {borderLeftWidth: hairlineWidth, borderRightWidth: hairlineWidth},
+ ? {borderBottomWidth: StyleSheet.hairlineWidth}
+ : {
+ borderLeftWidth: StyleSheet.hairlineWidth,
+ borderRightWidth: StyleSheet.hairlineWidth,
+ },
]}>
diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx
index 89682af5ff..0ed44758d4 100644
--- a/src/view/screens/ProfileList.tsx
+++ b/src/view/screens/ProfileList.tsx
@@ -65,7 +65,6 @@ import {useDialogControl} from '#/components/Dialog'
import * as Prompt from '#/components/Prompt'
import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
import {RichText} from '#/components/RichText'
-import hairlineWidth = StyleSheet.hairlineWidth
const SECTION_TITLES_CURATE = ['Posts', 'About']
const SECTION_TITLES_MOD = ['About']
@@ -803,7 +802,7 @@ const AboutSection = React.forwardRef(
diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx
index ef76cdcb22..3402db2c19 100644
--- a/src/view/screens/SavedFeeds.tsx
+++ b/src/view/screens/SavedFeeds.tsx
@@ -32,7 +32,6 @@ import {NoFollowingFeed} from '#/screens/Feeds/NoFollowingFeed'
import {NoSavedFeedsOfAnyType} from '#/screens/Feeds/NoSavedFeedsOfAnyType'
import {atoms as a, useTheme} from '#/alf'
import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline'
-import hairlineWidth = StyleSheet.hairlineWidth
const HITSLOP_TOP = {
top: 20,
@@ -93,7 +92,8 @@ export function SavedFeeds({}: Props) {
{noSavedFeedsOfAnyType && (
-
+
)}
@@ -135,7 +135,8 @@ export function SavedFeeds({}: Props) {
)}
{noFollowingFeed && (
-
+
)}
@@ -437,12 +438,12 @@ const styles = StyleSheet.create({
paddingHorizontal: 14,
paddingTop: 20,
paddingBottom: 10,
- borderBottomWidth: hairlineWidth,
+ borderBottomWidth: StyleSheet.hairlineWidth,
},
itemContainer: {
flexDirection: 'row',
alignItems: 'center',
- borderBottomWidth: hairlineWidth,
+ borderBottomWidth: StyleSheet.hairlineWidth,
},
footerText: {
paddingHorizontal: 26,
diff --git a/src/view/screens/Search/Explore.tsx b/src/view/screens/Search/Explore.tsx
index 05fd85effe..5510fbee25 100644
--- a/src/view/screens/Search/Explore.tsx
+++ b/src/view/screens/Search/Explore.tsx
@@ -75,17 +75,17 @@ function SuggestedItemsHeader({
)
}
-type LoadMoreItems =
+type LoadMoreItem =
| {
type: 'profile'
key: string
- avatar: string
+ avatar: string | undefined
moderation: ModerationDecision
}
| {
type: 'feed'
key: string
- avatar: string
+ avatar: string | undefined
moderation: undefined
}
@@ -98,27 +98,28 @@ function LoadMore({
}) {
const t = useTheme()
const {_} = useLingui()
- const items = React.useMemo(() => {
+ const items: LoadMoreItem[] = React.useMemo(() => {
return item.items
.map(_item => {
+ let loadMoreItem: LoadMoreItem | undefined
if (_item.type === 'profile') {
- return {
+ loadMoreItem = {
type: 'profile',
key: _item.profile.did,
avatar: _item.profile.avatar,
moderation: moderateProfile(_item.profile, moderationOpts!),
}
} else if (_item.type === 'feed') {
- return {
+ loadMoreItem = {
type: 'feed',
key: _item.feed.uri,
avatar: _item.feed.avatar,
moderation: undefined,
}
}
- return undefined
+ return loadMoreItem
})
- .filter(Boolean) as LoadMoreItems[]
+ .filter(n => !!n)
}, [item.items, moderationOpts])
if (items.length === 0) return null
diff --git a/src/view/screens/Settings/ExportCarDialog.tsx b/src/view/screens/Settings/ExportCarDialog.tsx
index 0daa3c8c97..a6ddb38204 100644
--- a/src/view/screens/Settings/ExportCarDialog.tsx
+++ b/src/view/screens/Settings/ExportCarDialog.tsx
@@ -94,6 +94,7 @@ export function ExportCarDialog({
This feature is in beta. You can read more about repository
exports in{' '}
this blogpost
diff --git a/src/view/screens/Settings/index.tsx b/src/view/screens/Settings/index.tsx
index f59bfb1834..db74d5c0d5 100644
--- a/src/view/screens/Settings/index.tsx
+++ b/src/view/screens/Settings/index.tsx
@@ -62,13 +62,12 @@ import {UserAvatar} from 'view/com/util/UserAvatar'
import {ScrollView} from 'view/com/util/Views'
import {DeactivateAccountDialog} from '#/screens/Settings/components/DeactivateAccountDialog'
import {useTheme} from '#/alf'
+import {atoms as a} from '#/alf'
import {useDialogControl} from '#/components/Dialog'
import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings'
import {navigate, resetToTab} from '#/Navigation'
import {Email2FAToggle} from './Email2FAToggle'
import {ExportCarDialog} from './ExportCarDialog'
-import hairlineWidth = StyleSheet.hairlineWidth
-import {atoms as a} from '#/alf'
function SettingsAccountCard({
account,
@@ -326,7 +325,7 @@ export function SettingsScreen({}: Props) {
showBackButton={isMobile}
style={[
pal.border,
- {borderBottomWidth: hairlineWidth},
+ {borderBottomWidth: StyleSheet.hairlineWidth},
!isMobile && {borderLeftWidth: 1, borderRightWidth: 1},
]}>
diff --git a/src/view/screens/Storybook/Links.tsx b/src/view/screens/Storybook/Links.tsx
index d35db79bc4..465ce0d6f3 100644
--- a/src/view/screens/Storybook/Links.tsx
+++ b/src/view/screens/Storybook/Links.tsx
@@ -13,18 +13,20 @@ export function Links() {
Links
-
+
https://google.com
-
+
External with custom children (google.com)
Internal (bsky.social)
Internal (bsky.app)
diff --git a/src/view/shell/bottom-bar/BottomBarStyles.tsx b/src/view/shell/bottom-bar/BottomBarStyles.tsx
index ba8743474f..c575e3d9b1 100644
--- a/src/view/shell/bottom-bar/BottomBarStyles.tsx
+++ b/src/view/shell/bottom-bar/BottomBarStyles.tsx
@@ -1,7 +1,6 @@
import {StyleSheet} from 'react-native'
import {colors} from 'lib/styles'
-import hairlineWidth = StyleSheet.hairlineWidth
export const styles = StyleSheet.create({
bottomBar: {
@@ -10,7 +9,7 @@ export const styles = StyleSheet.create({
left: 0,
right: 0,
flexDirection: 'row',
- borderTopWidth: hairlineWidth,
+ borderTopWidth: StyleSheet.hairlineWidth,
paddingLeft: 5,
paddingRight: 10,
},
diff --git a/src/view/shell/desktop/RightNav.tsx b/src/view/shell/desktop/RightNav.tsx
index 8dfa671cff..ed3d8212cf 100644
--- a/src/view/shell/desktop/RightNav.tsx
+++ b/src/view/shell/desktop/RightNav.tsx
@@ -11,10 +11,9 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {s} from 'lib/styles'
import {TextLink} from 'view/com/util/Link'
import {Text} from 'view/com/util/text/Text'
+import {ProgressGuideList} from '#/components/ProgressGuide/List'
import {DesktopFeeds} from './Feeds'
import {DesktopSearch} from './Search'
-import hairlineWidth = StyleSheet.hairlineWidth
-import {ProgressGuideList} from '#/components/ProgressGuide/List'
export function DesktopRightNav({routeName}: {routeName: string}) {
const pal = usePalette('default')
@@ -135,8 +134,8 @@ const styles = StyleSheet.create({
marginBottom: 10,
},
desktopFeedsContainer: {
- borderTopWidth: hairlineWidth,
- borderBottomWidth: hairlineWidth,
+ borderTopWidth: StyleSheet.hairlineWidth,
+ borderBottomWidth: StyleSheet.hairlineWidth,
marginTop: 18,
marginBottom: 18,
},
diff --git a/yarn.lock b/yarn.lock
index 6450d33b79..675fda4c2f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -13345,6 +13345,11 @@ history@^5.3.0:
dependencies:
"@babel/runtime" "^7.7.6"
+hls.js@^1.5.11:
+ version "1.5.11"
+ resolved "https://registry.yarnpkg.com/hls.js/-/hls.js-1.5.11.tgz#3941347df454983859ae8c75fe19e8818719a826"
+ integrity sha512-q3We1izi2+qkOO+TvZdHv+dx6aFzdtk3xc1/Qesrvto4thLTT/x/1FK85c5h1qZE4MmMBNgKg+MIW8nxQfxwBw==
+
hmac-drbg@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
@@ -21373,10 +21378,10 @@ typedarray-to-buffer@^3.1.5:
dependencies:
is-typedarray "^1.0.0"
-typescript@^5.3.3:
- version "5.3.3"
- resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37"
- integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==
+typescript@^5.5.4:
+ version "5.5.4"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba"
+ integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==
ua-parser-js@^0.7.33:
version "0.7.35"