diff --git a/bskyweb/cmd/bskyweb/main.go b/bskyweb/cmd/bskyweb/main.go index d9235afdee..3f46c4b00b 100644 --- a/bskyweb/cmd/bskyweb/main.go +++ b/bskyweb/cmd/bskyweb/main.go @@ -60,6 +60,12 @@ func run(args []string) { Value: "", EnvVars: []string{"LINK_HOST"}, }, + &cli.StringFlag{ + Name: "ipcc-host", + Usage: "scheme, hostname, and port of ipcc service", + Value: "https://localhost:8730", + EnvVars: []string{"IPCC_HOST"}, + }, &cli.BoolFlag{ Name: "debug", Usage: "Enable debug mode", diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index 203ed62f4e..afd9247ace 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -1,12 +1,17 @@ package main import ( + "bytes" "context" "crypto/subtle" + "crypto/tls" + "encoding/base64" + "encoding/json" "errors" "fmt" "io/fs" "net/http" + "net/netip" "net/url" "os" "os/signal" @@ -41,6 +46,7 @@ type Config struct { appviewHost string ogcardHost string linkHost string + ipccHost string } func serve(cctx *cli.Context) error { @@ -49,6 +55,7 @@ func serve(cctx *cli.Context) error { appviewHost := cctx.String("appview-host") ogcardHost := cctx.String("ogcard-host") linkHost := cctx.String("link-host") + ipccHost := cctx.String("ipcc-host") basicAuthPassword := cctx.String("basic-auth-password") // Echo @@ -91,6 +98,7 @@ func serve(cctx *cli.Context) error { appviewHost: appviewHost, ogcardHost: ogcardHost, linkHost: linkHost, + ipccHost: ipccHost, }, } @@ -261,6 +269,9 @@ func serve(cctx *cli.Context) error { e.GET("/starter-pack/:handleOrDID/:rkey", server.WebStarterPack) e.GET("/start/:handleOrDID/:rkey", server.WebStarterPack) + // ipcc + e.GET("/ipcc", server.WebIpCC) + if linkHost != "" { linkUrl, err := url.Parse(linkHost) if err != nil { @@ -520,3 +531,61 @@ func (srv *Server) WebProfile(c echo.Context) error { data["requestHost"] = req.Host return c.Render(http.StatusOK, "profile.html", data) } + +type IPCCRequest struct { + IP string `json:"ip"` +} +type IPCCResponse struct { + CC string `json:"countryCode"` +} + +func (srv *Server) WebIpCC(c echo.Context) error { + realIP := c.RealIP() + addr, err := netip.ParseAddr(realIP) + if err != nil { + log.Warnf("could not parse IP %q %s", realIP, err) + return c.JSON(400, IPCCResponse{}) + } + var request []byte + if addr.Is4() { + ip4 := addr.As4() + var dest [8]byte + base64.StdEncoding.Encode(dest[:], ip4[:]) + request, _ = json.Marshal(IPCCRequest{IP: string(dest[:])}) + } else if addr.Is6() { + ip6 := addr.As16() + var dest [24]byte + base64.StdEncoding.Encode(dest[:], ip6[:]) + request, _ = json.Marshal(IPCCRequest{IP: string(dest[:])}) + } + + ipccUrlBuilder, err := url.Parse(srv.cfg.ipccHost) + if err != nil { + log.Errorf("ipcc misconfigured bad url %s", err) + return c.JSON(500, IPCCResponse{}) + } + ipccUrlBuilder.Path = "ipccdata.IpCcService/Lookup" + ipccUrl := ipccUrlBuilder.String() + cl := http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + }, + } + postBodyReader := bytes.NewReader(request) + response, err := cl.Post(ipccUrl, "application/json", postBodyReader) + if err != nil { + log.Warnf("ipcc backend error %s", err) + return c.JSON(500, IPCCResponse{}) + } + defer response.Body.Close() + dec := json.NewDecoder(response.Body) + var outResponse IPCCResponse + err = dec.Decode(&outResponse) + if err != nil { + log.Warnf("ipcc bad response %s", err) + return c.JSON(500, IPCCResponse{}) + } + return c.JSON(200, outResponse) +} diff --git a/package.json b/package.json index 3f9f0bced7..eff665a649 100644 --- a/package.json +++ b/package.json @@ -139,7 +139,7 @@ "expo-system-ui": "~3.0.4", "expo-task-manager": "~11.8.1", "expo-updates": "~0.25.14", - "expo-video": "https://github.com/bluesky-social/expo/raw/expo-video-1.2.4-patch/packages/expo-video/expo-video-v1.2.4-1.tgz", + "expo-video": "https://github.com/bluesky-social/expo/raw/expo-video-1.2.4-patch/packages/expo-video/expo-video-v1.2.4-2.tgz", "expo-web-browser": "~13.0.3", "fast-text-encoding": "^1.0.6", "history": "^5.3.0", diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 0bf0e9f93e..2beba4f9dc 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -661,16 +661,15 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) { linking={LINKING} theme={theme} onStateChange={() => { - logEvent('router:navigate:sampled', { - from: prevLoggedRouteName.current, - }) - prevLoggedRouteName.current = getCurrentRouteName() + const routeName = getCurrentRouteName() + if (routeName === 'Notifications') { + logEvent('router:navigate:notifications:sampled', {}) + } }} onReady={() => { attachRouteToLogEvents(getCurrentRouteName) logModuleInitTime() onReady() - logEvent('router:navigate:sampled', {}) }}> {children} diff --git a/src/components/FeedInterstitials.tsx b/src/components/FeedInterstitials.tsx index 65e981f77a..5031f584e5 100644 --- a/src/components/FeedInterstitials.tsx +++ b/src/components/FeedInterstitials.tsx @@ -8,7 +8,6 @@ import {useNavigation} from '@react-navigation/native' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {NavigationProp} from '#/lib/routes/types' -import {useGate} from '#/lib/statsig/statsig' import {logEvent} from '#/lib/statsig/statsig' import {logger} from '#/logger' import {useModerationOpts} from '#/state/preferences/moderation-opts' @@ -177,14 +176,9 @@ function useExperimentalSuggestedUsersQuery() { } export function SuggestedFollows({feed}: {feed: FeedDescriptor}) { - const gate = useGate() const [feedType, feedUri] = feed.split('|') if (feedType === 'author') { - if (gate('show_follow_suggestions_in_profile')) { - return - } else { - return null - } + return } else { return } diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx index a263d19461..6f6d68049a 100644 --- a/src/components/ProfileCard.tsx +++ b/src/components/ProfileCard.tsx @@ -276,8 +276,8 @@ export function DescriptionPlaceholder() { export type FollowButtonProps = { profile: AppBskyActorDefs.ProfileViewBasic moderationOpts: ModerationOpts - logContext: LogEvents['profile:follow']['logContext'] & - LogEvents['profile:unfollow']['logContext'] + logContext: LogEvents['profile:follow:sampled']['logContext'] & + LogEvents['profile:unfollow:sampled']['logContext'] } & Partial export function FollowButton(props: FollowButtonProps) { diff --git a/src/components/StarterPack/Main/ProfilesList.tsx b/src/components/StarterPack/Main/ProfilesList.tsx index 6174bff021..a5c7cd1b73 100644 --- a/src/components/StarterPack/Main/ProfilesList.tsx +++ b/src/components/StarterPack/Main/ProfilesList.tsx @@ -40,7 +40,7 @@ export const ProfilesList = React.forwardRef( ref, ) { const t = useTheme() - const bottomBarOffset = useBottomBarOffset(200) + const bottomBarOffset = useBottomBarOffset(300) const initialNumToRender = useInitialNumToRender() const {currentAccount} = useSession() const {data, refetch, isError} = useAllListMembersQuery(listUri) diff --git a/src/components/hooks/useFollowMethods.ts b/src/components/hooks/useFollowMethods.ts index d67c3690f9..31a1e43daa 100644 --- a/src/components/hooks/useFollowMethods.ts +++ b/src/components/hooks/useFollowMethods.ts @@ -15,8 +15,8 @@ export function useFollowMethods({ logContext, }: { profile: Shadow - logContext: LogEvents['profile:follow']['logContext'] & - LogEvents['profile:unfollow']['logContext'] + logContext: LogEvents['profile:follow:sampled']['logContext'] & + LogEvents['profile:unfollow:sampled']['logContext'] }) { const {_} = useLingui() const requireAuth = useRequireAuth() diff --git a/src/components/video/PlayButtonIcon.tsx b/src/components/video/PlayButtonIcon.tsx index 5415084ff1..90e93f744b 100644 --- a/src/components/video/PlayButtonIcon.tsx +++ b/src/components/video/PlayButtonIcon.tsx @@ -2,24 +2,47 @@ import React from 'react' import {View} from 'react-native' import {atoms as a, useTheme} from '#/alf' -import {Play_Filled_Corner2_Rounded as PlayIcon} from '#/components/icons/Play' +import {Play_Filled_Corner0_Rounded as PlayIcon} from '#/components/icons/Play' -export function PlayButtonIcon({size = 44}: {size?: number}) { +export function PlayButtonIcon({size = 36}: {size?: number}) { const t = useTheme() + const bg = t.name === 'light' ? t.palette.contrast_25 : t.palette.contrast_975 + const fg = t.name === 'light' ? t.palette.contrast_975 : t.palette.contrast_25 return ( - + + ) } diff --git a/src/lib/statsig/events.ts b/src/lib/statsig/events.ts index 4768bdc238..1871894902 100644 --- a/src/lib/statsig/events.ts +++ b/src/lib/statsig/events.ts @@ -25,7 +25,7 @@ export type LogEvents = { secondsActive: number } 'state:foreground:sampled': {} - 'router:navigate:sampled': {} + 'router:navigate:notifications:sampled': {} 'deepLink:referrerReceived': { to: string referrer: string @@ -127,25 +127,25 @@ export type LogEvents = { langs: string logContext: 'Composer' } - 'post:like': { + 'post:like:sampled': { doesLikerFollowPoster: boolean | undefined doesPosterFollowLiker: boolean | undefined likerClout: number | undefined postClout: number | undefined logContext: 'FeedItem' | 'PostThreadItem' | 'Post' } - 'post:repost': { + 'post:repost:sampled': { logContext: 'FeedItem' | 'PostThreadItem' | 'Post' } - 'post:unlike': { + 'post:unlike:sampled': { logContext: 'FeedItem' | 'PostThreadItem' | 'Post' } - 'post:unrepost': { + 'post:unrepost:sampled': { logContext: 'FeedItem' | 'PostThreadItem' | 'Post' } 'post:mute': {} 'post:unmute': {} - 'profile:follow': { + 'profile:follow:sampled': { didBecomeMutual: boolean | undefined followeeClout: number | undefined followerClout: number | undefined @@ -162,7 +162,7 @@ export type LogEvents = { | 'FeedInterstitial' | 'ProfileHeaderSuggestedFollows' } - 'profile:unfollow': { + 'profile:unfollow:sampled': { logContext: | 'RecommendedFollowsItem' | 'PostThreadItem' diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts index be40548adf..61a48e441a 100644 --- a/src/lib/statsig/gates.ts +++ b/src/lib/statsig/gates.ts @@ -1,10 +1,8 @@ export type Gate = // Keep this alphabetic please. | 'debug_show_feedcontext' - | 'fixed_bottom_bar' | 'onboarding_minimum_interests' | 'suggested_feeds_interstitial' - | 'show_follow_suggestions_in_profile' | 'video_debug' // not recommended | 'video_upload' // upload videos | 'video_view_on_posts' // see posted videos diff --git a/src/lib/statsig/statsig.tsx b/src/lib/statsig/statsig.tsx index 7d86f4078c..c50bfeb862 100644 --- a/src/lib/statsig/statsig.tsx +++ b/src/lib/statsig/statsig.tsx @@ -89,8 +89,9 @@ export function toClout(n: number | null | undefined): number | undefined { } } +const DOWNSAMPLE_RATE = 0.95 // 95% likely const DOWNSAMPLED_EVENTS: Set = new Set([ - 'router:navigate:sampled', + 'router:navigate:notifications:sampled', 'state:background:sampled', 'state:foreground:sampled', 'home:feedDisplayed:sampled', @@ -99,8 +100,14 @@ const DOWNSAMPLED_EVENTS: Set = new Set([ 'discover:clickthrough:sampled', 'discover:engaged:sampled', 'discover:seen:sampled', + 'post:like:sampled', + 'post:unlike:sampled', + 'post:repost:sampled', + 'post:unrepost:sampled', + 'profile:follow:sampled', + 'profile:unfollow:sampled', ]) -const isDownsampledSession = Math.random() < 0.9 // 90% likely +const isDownsampledSession = Math.random() < DOWNSAMPLE_RATE export function logEvent( eventName: E & string, @@ -117,12 +124,16 @@ export function logEvent( ) } - if (isDownsampledSession && DOWNSAMPLED_EVENTS.has(eventName)) { + const isDownsampledEvent = DOWNSAMPLED_EVENTS.has(eventName) + if (isDownsampledSession && isDownsampledEvent) { return } const fullMetadata = { ...rawMetadata, } as Record // Statsig typings are unnecessarily strict here. + if (isDownsampledEvent) { + fullMetadata.downsampleRate = DOWNSAMPLE_RATE.toString() + } fullMetadata.routeName = getCurrentRouteName() ?? '(Uninitialized)' if (Statsig.initializeCalled()) { Statsig.logEvent(eventName, null, fullMetadata) diff --git a/src/screens/Messages/Conversation/MessageInput.tsx b/src/screens/Messages/Conversation/MessageInput.tsx index dc63a869a6..674edc41eb 100644 --- a/src/screens/Messages/Conversation/MessageInput.tsx +++ b/src/screens/Messages/Conversation/MessageInput.tsx @@ -23,6 +23,7 @@ import { useSaveMessageDraft, } from '#/state/messages/message-drafts' import {isIOS} from 'platform/detection' +import {EmojiPickerPosition} from '#/view/com/composer/text-input/web/EmojiPicker.web' import * as Toast from '#/view/com/util/Toast' import {atoms as a, useTheme} from '#/alf' import {useSharedInputStyles} from '#/components/forms/TextField' @@ -41,6 +42,7 @@ export function MessageInput({ hasEmbed: boolean setEmbed: (embedUrl: string | undefined) => void children?: React.ReactNode + openEmojiPicker?: (pos: EmojiPickerPosition) => void }) { const {_} = useLingui() const t = useTheme() diff --git a/src/screens/Messages/Conversation/MessageInput.web.tsx b/src/screens/Messages/Conversation/MessageInput.web.tsx index a4a8a78522..0b7e479209 100644 --- a/src/screens/Messages/Conversation/MessageInput.web.tsx +++ b/src/screens/Messages/Conversation/MessageInput.web.tsx @@ -12,9 +12,16 @@ import { } from '#/state/messages/message-drafts' import {isSafari, isTouchDevice} from 'lib/browser' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +import {textInputWebEmitter} from '#/view/com/composer/text-input/textInputWebEmitter' +import { + Emoji, + EmojiPickerPosition, +} from '#/view/com/composer/text-input/web/EmojiPicker.web' import * as Toast from '#/view/com/util/Toast' import {atoms as a, useTheme} from '#/alf' +import {Button} from '#/components/Button' import {useSharedInputStyles} from '#/components/forms/TextField' +import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji' import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane' import {useExtractEmbedFromFacets} from './MessageInputEmbed' @@ -23,11 +30,13 @@ export function MessageInput({ hasEmbed, setEmbed, children, + openEmojiPicker, }: { onSendMessage: (message: string) => void hasEmbed: boolean setEmbed: (embedUrl: string | undefined) => void children?: React.ReactNode + openEmojiPicker?: (pos: EmojiPickerPosition) => void }) { const {isTabletOrDesktop} = useWebMediaQueries() const {_} = useLingui() @@ -40,6 +49,7 @@ export function MessageInput({ const [isFocused, setIsFocused] = React.useState(false) const [isHovered, setIsHovered] = React.useState(false) const [textAreaHeight, setTextAreaHeight] = React.useState(38) + const textAreaRef = React.useRef(null) const onSubmit = React.useCallback(() => { if (!hasEmbed && message.trim() === '') { @@ -94,6 +104,23 @@ export function MessageInput({ [], ) + const onEmojiInserted = React.useCallback( + (emoji: Emoji) => { + const position = textAreaRef.current?.selectionStart ?? 0 + setMessage( + message => + message.slice(0, position) + emoji.native + message.slice(position), + ) + }, + [setMessage], + ) + React.useEffect(() => { + textInputWebEmitter.addListener('emoji-inserted', onEmojiInserted) + return () => { + textInputWebEmitter.removeListener('emoji-inserted', onEmojiInserted) + } + }, [onEmojiInserted]) + useSaveMessageDraft(message) useExtractEmbedFromFacets(message, setEmbed) @@ -106,7 +133,7 @@ export function MessageInput({ t.atoms.bg_contrast_25, { paddingRight: a.p_sm.padding - 2, - paddingLeft: a.p_md.padding - 2, + paddingLeft: a.p_sm.padding - 2, borderWidth: 1, borderRadius: 23, borderColor: 'transparent', @@ -118,7 +145,44 @@ export function MessageInput({ // @ts-expect-error web only onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)}> + ({ + isOpen: false, + pos: {top: 0, left: 0, right: 0, bottom: 0}, + }) + // We need to keep track of when the scroll offset is at the bottom of the list to know when to scroll as new items // are added to the list. For example, if the user is scrolled up to 1iew older messages, we don't want to scroll to // the bottom. @@ -422,13 +432,22 @@ export function MessagesList({ + setEmbed={setEmbed} + openEmojiPicker={pos => setEmojiPickerState({isOpen: true, pos})}> )} + {isWeb && ( + setEmojiPickerState(prev => ({...prev, isOpen: false}))} + /> + )} + {newMessagesPill.show && } ) diff --git a/src/screens/Profile/Header/ProfileHeaderStandard.tsx b/src/screens/Profile/Header/ProfileHeaderStandard.tsx index 2036023c30..cf5fcb97e3 100644 --- a/src/screens/Profile/Header/ProfileHeaderStandard.tsx +++ b/src/screens/Profile/Header/ProfileHeaderStandard.tsx @@ -6,11 +6,9 @@ import { ModerationOpts, RichText as RichTextAPI, } from '@atproto/api' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useGate} from '#/lib/statsig/statsig' import {logger} from '#/logger' import {isIOS} from '#/platform/detection' import {Shadow} from '#/state/cache/types' @@ -23,10 +21,9 @@ import {useRequireAuth, useSession} from '#/state/session' import {useAnalytics} from 'lib/analytics/analytics' import {sanitizeDisplayName} from 'lib/strings/display-names' import {useProfileShadow} from 'state/cache/profile-shadow' -import {ProfileHeaderSuggestedFollows} from '#/view/com/profile/ProfileHeaderSuggestedFollows' import {ProfileMenu} from '#/view/com/profile/ProfileMenu' import * as Toast from '#/view/com/util/Toast' -import {atoms as a, useTheme} from '#/alf' +import {atoms as a} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {MessageProfileButton} from '#/components/dms/MessageProfileButton' import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' @@ -59,8 +56,6 @@ let ProfileHeaderStandard = ({ }: Props): React.ReactNode => { const profile: Shadow = useProfileShadow(profileUnshadowed) - const t = useTheme() - const gate = useGate() const {currentAccount, hasSession} = useSession() const {_} = useLingui() const {openModal} = useModalControls() @@ -69,7 +64,6 @@ let ProfileHeaderStandard = ({ () => moderateProfile(profile, moderationOpts), [profile, moderationOpts], ) - const [showSuggestedFollows, setShowSuggestedFollows] = React.useState(false) const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue( profile, 'ProfileHeader', @@ -202,34 +196,7 @@ let ProfileHeaderStandard = ({ ) ) : !profile.viewer?.blockedBy ? ( <> - {hasSession && ( - <> - - {!gate('show_follow_suggestions_in_profile') && ( - - )} - - )} + {hasSession && } - - - - - {isLoading ? ( - <> - - - - - - - ) : data ? ( - data.suggestions - .filter(s => (s.associated?.labeler ? false : true)) - .map(profile => ( - { - logEvent('profile:header:suggestedFollowsCard:press', {}) - }} - style={[a.flex_1]}> - {({hovered, pressed}) => ( - - - - - - - - - - - )} - - )) - ) : ( - - )} - - - - - ) -} diff --git a/src/view/com/util/MainScrollProvider.tsx b/src/view/com/util/MainScrollProvider.tsx index 3163d85445..c87ee209ec 100644 --- a/src/view/com/util/MainScrollProvider.tsx +++ b/src/view/com/util/MainScrollProvider.tsx @@ -9,7 +9,6 @@ import { import EventEmitter from 'eventemitter3' import {ScrollProvider} from '#/lib/ScrollContext' -import {useGate} from '#/lib/statsig/statsig' import {useMinimalShellMode} from '#/state/shell' import {useShellLayout} from '#/state/shell/shell-layout' import {isNative, isWeb} from 'platform/detection' @@ -23,12 +22,10 @@ function clamp(num: number, min: number, max: number) { export function MainScrollProvider({children}: {children: React.ReactNode}) { const {headerHeight} = useShellLayout() - const {headerMode, footerMode} = useMinimalShellMode() + const {headerMode} = useMinimalShellMode() const startDragOffset = useSharedValue(null) const startMode = useSharedValue(null) const didJustRestoreScroll = useSharedValue(false) - const gate = useGate() - const isFixedBottomBar = gate('fixed_bottom_bar') const setMode = React.useCallback( (v: boolean) => { @@ -37,14 +34,8 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) { headerMode.value = withSpring(v ? 1 : 0, { overshootClamping: true, }) - if (!isFixedBottomBar) { - cancelAnimation(footerMode) - footerMode.value = withSpring(v ? 1 : 0, { - overshootClamping: true, - }) - } }, - [headerMode, footerMode, isFixedBottomBar], + [headerMode], ) useEffect(() => { @@ -147,10 +138,6 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) { // Cancel any any existing animation cancelAnimation(headerMode) headerMode.value = newValue - if (!isFixedBottomBar) { - cancelAnimation(footerMode) - footerMode.value = newValue - } } } else { if (didJustRestoreScroll.value) { @@ -173,12 +160,10 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) { [ headerHeight, headerMode, - footerMode, setMode, startDragOffset, startMode, didJustRestoreScroll, - isFixedBottomBar, ], ) diff --git a/src/view/com/util/Toast.tsx b/src/view/com/util/Toast.tsx index f7c6bc2c91..51e76bdc39 100644 --- a/src/view/com/util/Toast.tsx +++ b/src/view/com/util/Toast.tsx @@ -59,7 +59,6 @@ function Toast({ a.flex_1, t.atoms.bg, a.shadow_lg, - a.rounded_sm, t.atoms.border_contrast_medium, a.rounded_sm, a.px_md, diff --git a/src/view/com/util/fab/FABInner.tsx b/src/view/com/util/fab/FABInner.tsx index e6fb0ad465..ee8e1f47a2 100644 --- a/src/view/com/util/fab/FABInner.tsx +++ b/src/view/com/util/fab/FABInner.tsx @@ -79,6 +79,7 @@ const styles = StyleSheet.create({ // @ts-ignore web-only position: isWeb ? 'fixed' : 'absolute', zIndex: 1, + cursor: 'pointer', }, inner: { justifyContent: 'center', diff --git a/src/view/com/util/post-embeds/ActiveVideoNativeContext.tsx b/src/view/com/util/post-embeds/ActiveVideoNativeContext.tsx index da8c7a98c4..95fa0bb0ec 100644 --- a/src/view/com/util/post-embeds/ActiveVideoNativeContext.tsx +++ b/src/view/com/util/post-embeds/ActiveVideoNativeContext.tsx @@ -1,7 +1,7 @@ import React from 'react' import {useVideoPlayer, VideoPlayer} from 'expo-video' -import {isNative} from '#/platform/detection' +import {isAndroid, isNative} from '#/platform/detection' const Context = React.createContext<{ activeSource: string @@ -26,7 +26,18 @@ export function Provider({children}: {children: React.ReactNode}) { }) const setActiveSourceOuter = (src: string | null, viewId: string | null) => { - setActiveSource(src ? src : '') + // HACK + // expo-video doesn't like it when you try and move a `player` to another `VideoView`. Instead, we need to actually + // unregister that player to let the new screen register it. This is only a problem on Android, so we only need to + // apply it there. + if (src === activeSource && isAndroid) { + setActiveSource('') + setTimeout(() => { + setActiveSource(src ? src : '') + }, 100) + } else { + setActiveSource(src ? src : '') + } setActiveViewId(viewId ? viewId : '') } diff --git a/src/view/com/util/post-embeds/VideoEmbed.tsx b/src/view/com/util/post-embeds/VideoEmbed.tsx index e5457555ba..9c3a34dda8 100644 --- a/src/view/com/util/post-embeds/VideoEmbed.tsx +++ b/src/view/com/util/post-embeds/VideoEmbed.tsx @@ -71,7 +71,7 @@ function InnerWrapper({embed}: Props) { const [playerStatus, setPlayerStatus] = useState< VideoPlayerStatus | 'paused' - >(player.playing ? 'readyToPlay' : 'paused') + >('paused') const [isMuted, setIsMuted] = useState(player.muted) const [isFullscreen, setIsFullscreen] = React.useState(false) const [timeRemaining, setTimeRemaining] = React.useState(0) diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx index 31e8630382..de9a2c74c2 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx +++ b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx @@ -8,6 +8,7 @@ import {useLingui} from '@lingui/react' import {HITSLOP_30} from '#/lib/constants' import {clamp} from '#/lib/numbers' +import {isAndroid} from 'platform/detection' import {useActiveVideoNative} from 'view/com/util/post-embeds/ActiveVideoNativeContext' import {atoms as a, useTheme} from '#/alf' import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute' @@ -61,6 +62,9 @@ export function VideoEmbedInnerNative({ PlatformInfo.setAudioActive(true) player.muted = false setIsFullscreen(true) + if (isAndroid) { + player.play() + } }} onFullscreenExit={() => { PlatformInfo.setAudioCategory(AudioCategory.Ambient) diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index fb487ad6bf..c790a815b6 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -1,24 +1,18 @@ import React from 'react' -import {ActivityIndicator, AppState, StyleSheet, View} from 'react-native' +import {ActivityIndicator, StyleSheet, View} from 'react-native' import {useFocusEffect} from '@react-navigation/native' import {PROD_DEFAULT_FEED} from '#/lib/constants' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {useSetTitle} from '#/lib/hooks/useSetTitle' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {logEvent, LogEvents} from '#/lib/statsig/statsig' -import {useGate} from '#/lib/statsig/statsig' import {emitSoftReset} from '#/state/events' import {SavedFeedSourceInfo, usePinnedFeedsInfos} from '#/state/queries/feed' import {FeedParams} from '#/state/queries/post-feed' import {usePreferencesQuery} from '#/state/queries/preferences' import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types' import {useSession} from '#/state/session' -import { - useMinimalShellMode, - useSetDrawerSwipeDisabled, - useSetMinimalShellMode, -} from '#/state/shell' +import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell' import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed' import {useOTAUpdates} from 'lib/hooks/useOTAUpdates' import {useRequestNotificationsPermission} from 'lib/notifications/notifications' @@ -87,7 +81,6 @@ function HomeScreenReady({ const selectedIndex = Math.max(0, maybeFoundIndex) const selectedFeed = allFeeds[selectedIndex] const requestNotificationsPermission = useRequestNotificationsPermission() - const gate = useGate() useSetTitle(pinnedFeedInfos[selectedIndex]?.displayName) useOTAUpdates() @@ -134,29 +127,6 @@ function HomeScreenReady({ }), ) - const {footerMode} = useMinimalShellMode() - const {isMobile} = useWebMediaQueries() - useFocusEffect( - React.useCallback(() => { - if (gate('fixed_bottom_bar')) { - // Unnecessary because it's always there. - return - } - const listener = AppState.addEventListener('change', nextAppState => { - if (nextAppState === 'active') { - if (isMobile && footerMode.value === 1) { - // Reveal the bottom bar so you don't miss notifications or messages. - // TODO: Experiment with only doing it when unread > 0. - setMinimalShellMode(false) - } - } - }) - return () => { - listener.remove() - } - }, [setMinimalShellMode, footerMode, isMobile, gate]), - ) - const onPageSelected = React.useCallback( (index: number) => { setMinimalShellMode(false) diff --git a/src/view/shell/Composer.web.tsx b/src/view/shell/Composer.web.tsx index 5d80dc422b..42696139e0 100644 --- a/src/view/shell/Composer.web.tsx +++ b/src/view/shell/Composer.web.tsx @@ -61,7 +61,7 @@ export function Composer({}: {winHeight: number}) { quoteCount={state?.quoteCount} onPost={state.onPost} mention={state.mention} - openPicker={onOpenPicker} + openEmojiPicker={onOpenPicker} text={state.text} /> diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx index 7d080e57b1..aed92cbb7a 100644 --- a/src/view/shell/index.tsx +++ b/src/view/shell/index.tsx @@ -11,7 +11,7 @@ import Animated from 'react-native-reanimated' import {useSafeAreaInsets} from 'react-native-safe-area-context' import * as NavigationBar from 'expo-navigation-bar' import {StatusBar} from 'expo-status-bar' -import {useNavigationState} from '@react-navigation/native' +import {useNavigation, useNavigationState} from '@react-navigation/native' import {useSession} from '#/state/session' import { @@ -20,6 +20,7 @@ import { useSetDrawerOpen, } from '#/state/shell' import {useCloseAnyActiveElement} from '#/state/util' +import {useDedupe} from 'lib/hooks/useDedupe' import {useNotificationsHandler} from 'lib/hooks/useNotificationHandler' import {usePalette} from 'lib/hooks/usePalette' import {useNotificationsRegistration} from 'lib/notifications/notifications' @@ -33,6 +34,7 @@ import {ErrorBoundary} from 'view/com/util/ErrorBoundary' import {MutedWordsDialog} from '#/components/dialogs/MutedWords' import {SigninDialog} from '#/components/dialogs/Signin' import {Outlet as PortalOutlet} from '#/components/Portal' +import {updateActiveViewAsync} from '../../../modules/expo-bluesky-swiss-army/src/VisibilityView' import {RoutesContainer, TabsNavigator} from '../../Navigation' import {Composer} from './Composer' import {DrawerContent} from './Drawer' @@ -76,6 +78,27 @@ function ShellInner() { } }, [closeAnyActiveElement]) + // HACK + // expo-video doesn't like it when you try and move a `player` to another `VideoView`. Instead, we need to actually + // unregister that player to let the new screen register it. This is only a problem on Android, so we only need to + // apply it there. + // The `state` event should only fire whenever we push or pop to a screen, and should not fire consecutively quickly. + // To be certain though, we will also dedupe these calls. + const navigation = useNavigation() + const dedupe = useDedupe(1000) + React.useEffect(() => { + if (!isAndroid) return + const onFocusOrBlur = () => { + setTimeout(() => { + dedupe(updateActiveViewAsync) + }, 500) + } + navigation.addListener('state', onFocusOrBlur) + return () => { + navigation.removeListener('state', onFocusOrBlur) + } + }, [dedupe, navigation]) + return ( <>