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