From b38d4697b7a42a5e4c48d86a6528a20ace9c034e Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 24 Sep 2024 20:10:13 -0500 Subject: [PATCH 01/44] [Neue] Post avi, `PostMeta` cleanup (#5450) * Support emoji in text with custom font * Add emoji support to elements that need it * Remove unused file causing lint failure * Add web only link variant * Refactor PostMeta * Reduce avi size in feeds * Fix alignment, emoji, in PostMeta * Smaller avis in notifications * Shrink post placeholder avi * Handle the handle again * Link cleanup * Cleanup unused props * Fix text wrapping in timestamp * Fix underline color * Tighten up spacing * Web only whiteSpace --- src/App.web.tsx | 4 +- src/components/Link.tsx | 36 ++++- .../Conversation/MessageInputEmbed.tsx | 1 - src/view/com/post-thread/PostThreadItem.tsx | 8 +- src/view/com/post/Post.tsx | 3 +- src/view/com/posts/FeedItem.tsx | 5 +- src/view/com/posts/FeedSlice.tsx | 6 +- src/view/com/util/LoadingPlaceholder.tsx | 10 +- src/view/com/util/PostMeta.tsx | 153 +++++++++--------- src/view/com/util/post-embeds/QuoteEmbed.tsx | 11 +- 10 files changed, 120 insertions(+), 117 deletions(-) diff --git a/src/App.web.tsx b/src/App.web.tsx index 7d98737a3b..1664812d08 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -1,5 +1,5 @@ -import 'lib/sentry' // must be near top -import 'view/icons' +import '#/lib/sentry' // must be near top +import '#/view/icons' import './style.css' import React, {useEffect, useState} from 'react' diff --git a/src/components/Link.tsx b/src/components/Link.tsx index 6c25faffb8..c80b9f3707 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -9,6 +9,7 @@ import {sanitizeUrl} from '@braintree/sanitize-url' import {StackActions, useLinkProps} from '@react-navigation/native' import {BSKY_DOWNLOAD_URL} from '#/lib/constants' +import {useNavigationDeduped} from '#/lib/hooks/useNavigationDeduped' import {AllNavigatorParams} from '#/lib/routes/types' import {shareUrl} from '#/lib/sharing' import { @@ -17,11 +18,10 @@ import { isExternalUrl, linkRequiresWarning, } from '#/lib/strings/url-helpers' -import {isNative} from '#/platform/detection' +import {isNative, isWeb} from '#/platform/detection' import {shouldClickOpenNewTab} from '#/platform/urls' import {useModalControls} from '#/state/modals' import {useOpenLink} from '#/state/preferences/in-app-browser' -import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped' import {atoms as a, flatten, TextStyleProp, useTheme, web} from '#/alf' import {Button, ButtonProps} from '#/components/Button' import {useInteractionState} from '#/components/hooks/useInteractionState' @@ -244,7 +244,10 @@ export function Link({ export type InlineLinkProps = React.PropsWithChildren< BaseLinkProps & TextStyleProp & Pick > & - Pick + Pick & { + disableUnderline?: boolean + title?: TextProps['title'] + } export function InlineLinkText({ children, @@ -257,6 +260,7 @@ export function InlineLinkText({ selectable, label, shareOnLongPress, + disableUnderline, ...rest }: InlineLinkProps) { const t = useTheme() @@ -290,11 +294,12 @@ export function InlineLinkText({ {...rest} style={[ {color: t.palette.primary_500}, - (hovered || focused || pressed) && { - ...web({outline: 0}), - textDecorationLine: 'underline', - textDecorationColor: flattenedStyle.color ?? t.palette.primary_500, - }, + (hovered || focused || pressed) && + !disableUnderline && { + ...web({outline: 0}), + textDecorationLine: 'underline', + textDecorationColor: flattenedStyle.color ?? t.palette.primary_500, + }, flattenedStyle, ]} role="link" @@ -365,3 +370,18 @@ export function BaseLink({ ) } + +export function WebOnlyInlineLinkText({ + children, + to, + onPress, + ...props +}: InlineLinkProps) { + return isWeb ? ( + + {children} + + ) : ( + {children} + ) +} diff --git a/src/screens/Messages/Conversation/MessageInputEmbed.tsx b/src/screens/Messages/Conversation/MessageInputEmbed.tsx index bf28ed4fe9..2d1551019e 100644 --- a/src/screens/Messages/Conversation/MessageInputEmbed.tsx +++ b/src/screens/Messages/Conversation/MessageInputEmbed.tsx @@ -174,7 +174,6 @@ export function MessageInputEmbed({ showAvatar author={post.author} moderation={moderation} - authorHasWarning={!!post.author.labels?.length} timestamp={post.indexedAt} postHref={itemHref} style={a.flex_0} diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 3fb2309b96..ead9df1161 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -558,18 +558,14 @@ let PostThreadItemLoaded = ({ diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index 9033fb96f7..ec730a5e16 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -163,7 +163,7 @@ function PostInner({ diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index b1509b2719..fb9cdb065e 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -245,7 +245,7 @@ let FeedItemInner = ({ onBeforePress={onBeforePress} dataSet={{feedContext}}> - + {isThreadChild && ( onOpenAuthor?: () => void style?: StyleProp } let PostMeta = (opts: PostMetaOpts): React.ReactNode => { - const {i18n} = useLingui() + const t = useTheme() + const {i18n, _} = useLingui() - const pal = usePalette('default') const displayName = opts.author.displayName || opts.author.handle const handle = opts.author.handle const profileLink = makeProfileLink(opts.author) @@ -53,9 +49,18 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => { }, [queryClient, opts.author]) return ( - + {opts.showAvatar && ( - + { )} - - + - {forceLTR( - sanitizeDisplayName( - displayName, - opts.moderation?.ui('displayName'), - ), - )} - - } - href={profileLink} - onBeforePress={onBeforePressAuthor} - /> - + + {forceLTR( + sanitizeDisplayName( + displayName, + opts.moderation?.ui('displayName'), + ), + )} + + + - {NON_BREAKING_SPACE + sanitizeHandle(handle, '@')} - - } - href={profileLink} - onBeforePress={onBeforePressAuthor} - anchorNoUnderline - /> + disableUnderline + onPress={onBeforePressAuthor} + style={[a.text_md, t.atoms.text_contrast_medium, a.leading_tight]}> + + {NON_BREAKING_SPACE + sanitizeHandle(handle, '@')} + + - {!isAndroid && ( - - · - - )} + + + · + + {({timeElapsed}) => ( - + disableMismatchWarning + disableUnderline + onPress={onBeforePressPost} + style={[ + a.text_md, + t.atoms.text_contrast_medium, + a.leading_tight, + web({ + whiteSpace: 'nowrap', + }), + ]}> + {timeElapsed} + )} @@ -129,21 +138,3 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => { } PostMeta = memo(PostMeta) export {PostMeta} - -const styles = StyleSheet.create({ - container: { - flexDirection: 'row', - alignItems: 'flex-end', - paddingBottom: 2, - gap: 4, - zIndex: 1, - flex: 1, - }, - avatar: { - alignSelf: 'center', - }, - maxWidth: { - flex: isAndroid ? 1 : undefined, - flexShrink: isAndroid ? undefined : 1, - }, -}) diff --git a/src/view/com/util/post-embeds/QuoteEmbed.tsx b/src/view/com/util/post-embeds/QuoteEmbed.tsx index 79e3264046..3b8152c8b8 100644 --- a/src/view/com/util/post-embeds/QuoteEmbed.tsx +++ b/src/view/com/util/post-embeds/QuoteEmbed.tsx @@ -24,15 +24,15 @@ import {useLingui} from '@lingui/react' import {useQueryClient} from '@tanstack/react-query' import {HITSLOP_20} from '#/lib/constants' +import {usePalette} from '#/lib/hooks/usePalette' +import {InfoCircleIcon} from '#/lib/icons' import {moderatePost_wrapped} from '#/lib/moderatePost_wrapped' +import {makeProfileLink} from '#/lib/routes/links' import {s} from '#/lib/styles' import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {precacheProfile} from '#/state/queries/profile' import {useSession} from '#/state/session' -import {usePalette} from 'lib/hooks/usePalette' -import {InfoCircleIcon} from 'lib/icons' -import {makeProfileLink} from 'lib/routes/links' -import {precacheProfile} from 'state/queries/profile' -import {ComposerOptsQuote} from 'state/shell/composer' +import {ComposerOptsQuote} from '#/state/shell/composer' import {atoms as a, useTheme} from '#/alf' import {RichText} from '#/components/RichText' import {ContentHider} from '../../../../components/moderation/ContentHider' @@ -238,7 +238,6 @@ export function QuoteEmbed({ author={quote.author} moderation={moderation} showAvatar - authorHasWarning={false} postHref={itemHref} timestamp={quote.indexedAt} /> From 850cfc1cd567bf36c3c2ba9dfd92fb579e8e52bc Mon Sep 17 00:00:00 2001 From: noriaki watanabe Date: Wed, 25 Sep 2024 20:28:16 +0900 Subject: [PATCH 02/44] delete extractHtmlMeta (#5478) --- __tests__/lib/__mocks__/exampleComHtml.ts | 47 ------ __tests__/lib/__mocks__/tiktokHtml.ts | 4 - __tests__/lib/__mocks__/youtubeChannelHtml.ts | 63 -------- __tests__/lib/__mocks__/youtubeHtml.ts | 19 --- __tests__/lib/extractHtmlMeta.test.ts | 134 ------------------ src/lib/link-meta/html.ts | 71 ---------- src/lib/link-meta/twitter.ts | 20 --- src/lib/link-meta/youtube.ts | 31 ---- 8 files changed, 389 deletions(-) delete mode 100644 __tests__/lib/__mocks__/exampleComHtml.ts delete mode 100644 __tests__/lib/__mocks__/tiktokHtml.ts delete mode 100644 __tests__/lib/__mocks__/youtubeChannelHtml.ts delete mode 100644 __tests__/lib/__mocks__/youtubeHtml.ts delete mode 100644 __tests__/lib/extractHtmlMeta.test.ts delete mode 100644 src/lib/link-meta/html.ts delete mode 100644 src/lib/link-meta/twitter.ts delete mode 100644 src/lib/link-meta/youtube.ts diff --git a/__tests__/lib/__mocks__/exampleComHtml.ts b/__tests__/lib/__mocks__/exampleComHtml.ts deleted file mode 100644 index 6633e40ca5..0000000000 --- a/__tests__/lib/__mocks__/exampleComHtml.ts +++ /dev/null @@ -1,47 +0,0 @@ -export const exampleComHtml = ` - - - Example Domain - - - - - - - - - -
-

Example Domain

-

This domain is for use in illustrative examples in documents. You may use this - domain in literature without prior coordination or asking for permission.

-

More information...

-
- -` diff --git a/__tests__/lib/__mocks__/tiktokHtml.ts b/__tests__/lib/__mocks__/tiktokHtml.ts deleted file mode 100644 index fa3d112836..0000000000 --- a/__tests__/lib/__mocks__/tiktokHtml.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const tiktokHtml = ` -Coca-Cola and Mentos! Super Reaction! #cocacola #mentos #reaction #bal... | TikTok
Upload

For You

Log in to follow creators, like videos, and view comments.

Suggested accounts

© 2023 TikTok
Coca-Cola and Mentos! Super Reaction! #cocacola #mentos #reaction #balloon #sciencemoment #scienceexperiment #experiment #test #amazing #pvexp
00:00/00:00
Coca-Cola and Mentos! Super Reaction! #cocacola #mentos #reaction #balloon #sciencemoment #scienceexperiment #experiment #test #amazing #pvexp
_powervision_
Power Vision Tests · 2019-10-19

Related videos

Get TikTok App
-` diff --git a/__tests__/lib/__mocks__/youtubeChannelHtml.ts b/__tests__/lib/__mocks__/youtubeChannelHtml.ts deleted file mode 100644 index cc71995c45..0000000000 --- a/__tests__/lib/__mocks__/youtubeChannelHtml.ts +++ /dev/null @@ -1,63 +0,0 @@ -export const youtubeChannelHtml = ` - -
AboutPressCopyrightContact usCreatorsAdvertiseDevelopersTermsPrivacyPolicy & SafetyHow YouTube worksTest new features
penguinz0 - YouTube
- -` diff --git a/__tests__/lib/__mocks__/youtubeHtml.ts b/__tests__/lib/__mocks__/youtubeHtml.ts deleted file mode 100644 index 7fd9f819da..0000000000 --- a/__tests__/lib/__mocks__/youtubeHtml.ts +++ /dev/null @@ -1,19 +0,0 @@ -export const youtubeHTML = ` - -YouTube
- -` diff --git a/__tests__/lib/extractHtmlMeta.test.ts b/__tests__/lib/extractHtmlMeta.test.ts deleted file mode 100644 index cdd2a33848..0000000000 --- a/__tests__/lib/extractHtmlMeta.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -import {extractHtmlMeta} from '../../src/lib/link-meta/html' -import {exampleComHtml} from './__mocks__/exampleComHtml' -import {youtubeHTML} from './__mocks__/youtubeHtml' -import {tiktokHtml} from './__mocks__/tiktokHtml' -import {youtubeChannelHtml} from './__mocks__/youtubeChannelHtml' - -describe('extractHtmlMeta', () => { - const cases = [ - ['', {}], - ['nothing', {}], - ['title', {title: 'title'}], - [' aSd!@#AC ', {title: 'aSd!@#AC'}], - ['\n title\n ', {title: 'title'}], - ['', {title: 'meta title'}], - [ - '', - {description: 'meta description'}, - ], - ['', {title: 'og title'}], - [ - '', - {description: 'og description'}, - ], - [ - '', - {image: 'https://ogimage.com/foo.png'}, - ], - [ - '', - {title: 'twitter title'}, - ], - [ - '', - {description: 'twitter description'}, - ], - [ - '', - {image: 'https://twitterimage.com/foo.png'}, - ], - ['', {title: 'meta title'}], - ] - - it.each(cases)( - 'given the html tag %p, returns %p', - // @ts-ignore not worth fixing -prf - (input, expectedResult) => { - const output = extractHtmlMeta({html: input as string, hostname: ''}) - expect(output).toEqual(expectedResult) - }, - ) - - it('extracts title and description from a generic HTML page', () => { - const input = exampleComHtml - const expectedOutput = { - title: 'Example Domain', - description: 'An example website', - } - const output = extractHtmlMeta({html: input, hostname: 'example.com'}) - expect(output).toEqual(expectedOutput) - }) - - it('extracts title and description from a Tiktok HTML page', () => { - const input = tiktokHtml - const expectedOutput = { - title: - 'Coca-Cola and Mentos! Super Reaction! #cocacola #mentos #reaction #bal... | TikTok', - description: - '5.5M Likes, 20.8K Comments. TikTok video from Power Vision Tests (@_powervision_): "Coca-Cola and Mentos! Super Reaction! #cocacola #mentos #reaction #balloon #sciencemoment #scienceexperiment #experiment #test #amazing #pvexp". оригинальный звук - Power Vision Tests.', - } - const output = extractHtmlMeta({html: input, hostname: 'tiktok.com'}) - expect(output).toEqual(expectedOutput) - }) - - it('extracts title and description from a generic youtube page', () => { - const input = youtubeHTML - const expectedOutput = { - title: 'HD Video (1080p) with Relaxing Music of Native American Shamans', - description: - 'Stunning HD Video ( 1080p ) of Patagonian Nature with Relaxing Native American Shamanic Music. HD footage used from ', - image: 'https://i.ytimg.com/vi/x6UITRjhijI/sddefault.jpg', - } - const output = extractHtmlMeta({html: input, hostname: 'youtube.com'}) - expect(output).toEqual(expectedOutput) - }) - - it('extracts avatar from a youtube channel', () => { - const input = youtubeChannelHtml - const expectedOutput = { - title: 'penguinz0', - description: - 'Clips channel: https://www.youtube.com/channel/UC4EQHfzIbkL_Skit_iKt1aA\n\nTwitter: https://twitter.com/MoistCr1TiKaL\n\nInstagram: https://www.instagram.com/bigmoistcr1tikal/?hl=en\n\nTwitch: https://www.twitch.tv/moistcr1tikal\n\nSnapchat: Hugecharles\n\nTik Tok: Hugecharles\n\nI don't have any other public accounts.', - image: - 'https://yt3.googleusercontent.com/ytc/AL5GRJWOhJOuUC6C2b7gP-5D2q6ypXbcOOckyAE1En4RUQ=s176-c-k-c0x00ffffff-no-rj', - } - const output = extractHtmlMeta({html: input, hostname: 'youtube.com'}) - expect(output).toEqual(expectedOutput) - }) - - it('extracts username from the url a twitter profile page', () => { - const expectedOutput = { - title: '@bluesky on Twitter', - } - const output = extractHtmlMeta({ - html: '', - hostname: 'twitter.com', - pathname: '/bluesky', - }) - expect(output).toEqual(expectedOutput) - }) - - it('extracts username from the url a tweet', () => { - const expectedOutput = { - title: 'Tweet by @bluesky', - } - const output = extractHtmlMeta({ - html: '', - hostname: 'twitter.com', - pathname: '/bluesky/status/1582437529969917953', - }) - expect(output).toEqual(expectedOutput) - }) - - it("does not extract username from the url when it's not a tweet or profile page", () => { - const expectedOutput = { - title: 'Twitter', - } - const output = extractHtmlMeta({ - html: '', - hostname: 'twitter.com', - pathname: '/i/articles/follows/-1675653703?time_window=24', - }) - expect(output).toEqual(expectedOutput) - }) -}) diff --git a/src/lib/link-meta/html.ts b/src/lib/link-meta/html.ts deleted file mode 100644 index 220f8431d5..0000000000 --- a/src/lib/link-meta/html.ts +++ /dev/null @@ -1,71 +0,0 @@ -import {extractTwitterMeta} from './twitter' -import {extractYoutubeMeta} from './youtube' - -interface ExtractHtmlMetaInput { - html: string - hostname?: string - pathname?: string -} - -export const extractHtmlMeta = ({ - html, - hostname, - pathname, -}: ExtractHtmlMetaInput): Record => { - const htmlTitleRegex = /([^<]+)<\/title>/i - - let res: Record = {} - - const match = htmlTitleRegex.exec(html) - - if (match) { - res.title = match[1].trim() - } - - let metaMatch - let propMatch - const metaRe = /]+)>/gis - while ((metaMatch = metaRe.exec(html))) { - let propName - let propValue - const propRe = /(name|property|content)="([^"]+)"/gis - while ((propMatch = propRe.exec(metaMatch[1]))) { - if (propMatch[1] === 'content') { - propValue = propMatch[2] - } else { - propName = propMatch[2] - } - } - if (!propName || !propValue) { - continue - } - switch (propName?.trim()) { - case 'title': - case 'og:title': - case 'twitter:title': - res.title = propValue?.trim() - break - case 'description': - case 'og:description': - case 'twitter:description': - res.description = propValue?.trim() - break - case 'og:image': - case 'twitter:image': - res.image = propValue?.trim() - break - } - } - - const isYoutubeUrl = - hostname?.includes('youtube.') || hostname?.includes('youtu.be') - const isTwitterUrl = hostname?.includes('twitter.') - // Workaround for some websites not having a title or description in the meta tags in the initial serve - if (isYoutubeUrl) { - res = {...res, ...extractYoutubeMeta(html)} - } else if (isTwitterUrl && pathname) { - res = {...extractTwitterMeta({pathname})} - } - - return res -} diff --git a/src/lib/link-meta/twitter.ts b/src/lib/link-meta/twitter.ts deleted file mode 100644 index d785903c00..0000000000 --- a/src/lib/link-meta/twitter.ts +++ /dev/null @@ -1,20 +0,0 @@ -export const extractTwitterMeta = ({ - pathname, -}: { - pathname: string -}): Record => { - const res = {title: 'Twitter'} - const parsedPathname = pathname.split('/') - if (parsedPathname.length <= 1 || parsedPathname[1].length <= 1) { - // Excluding one letter usernames as they're reserved by twitter for things like cases like twitter.com/i/articles/follows/-1675653703 - return res - } - const username = parsedPathname?.[1] - const isUserProfile = parsedPathname?.length === 2 - - res.title = isUserProfile - ? `@${username} on Twitter` - : `Tweet by @${username}` - - return res -} diff --git a/src/lib/link-meta/youtube.ts b/src/lib/link-meta/youtube.ts deleted file mode 100644 index 42eed51e8f..0000000000 --- a/src/lib/link-meta/youtube.ts +++ /dev/null @@ -1,31 +0,0 @@ -export const extractYoutubeMeta = (html: string): Record => { - const res: Record = {} - const youtubeTitleRegex = /"videoDetails":.*"title":"([^"]*)"/i - const youtubeDescriptionRegex = - /"videoDetails":.*"shortDescription":"([^"]*)"/i - const youtubeThumbnailRegex = /"videoDetails":.*"url":"(.*)(default\.jpg)/i - const youtubeAvatarRegex = - /"avatar":{"thumbnails":\[{.*?url.*?url.*?url":"([^"]*)"/i - const youtubeTitleMatch = youtubeTitleRegex.exec(html) - const youtubeDescriptionMatch = youtubeDescriptionRegex.exec(html) - const youtubeThumbnailMatch = youtubeThumbnailRegex.exec(html) - const youtubeAvatarMatch = youtubeAvatarRegex.exec(html) - - if (youtubeTitleMatch && youtubeTitleMatch.length >= 1) { - res.title = decodeURI(youtubeTitleMatch[1]) - } - if (youtubeDescriptionMatch && youtubeDescriptionMatch.length >= 1) { - res.description = decodeURI(youtubeDescriptionMatch[1]).replace( - /\\n/g, - '\n', - ) - } - if (youtubeThumbnailMatch && youtubeThumbnailMatch.length >= 2) { - res.image = youtubeThumbnailMatch[1] + 'default.jpg' - } - if (!res.image && youtubeAvatarMatch && youtubeAvatarMatch.length >= 1) { - res.image = youtubeAvatarMatch[1] - } - - return res -} From bd393b1b387eeddff33a520f60f04387c9105379 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 25 Sep 2024 14:58:53 +0100 Subject: [PATCH 03/44] Allow profile header to overscroll (#5457) * add allowoverscroll prop * ensure spinner is visible * more generic prop for `` * rename to allowHeaderOverScroll --- .../StarterPack/ProfileStarterPacks.tsx | 17 ++++++----- src/screens/Profile/Header/index.tsx | 14 +++++---- src/screens/Profile/Sections/Feed.tsx | 14 +++++---- src/view/com/feeds/ProfileFeedgens.tsx | 5 ++-- src/view/com/lists/ProfileLists.tsx | 7 +++-- src/view/com/pager/PagerWithHeader.tsx | 29 +++++++++++++------ src/view/com/pager/PagerWithHeader.web.tsx | 2 +- src/view/com/posts/Feed.tsx | 9 ++++-- src/view/com/util/List.tsx | 17 ++++++----- src/view/screens/Profile.tsx | 5 ++-- 10 files changed, 72 insertions(+), 47 deletions(-) diff --git a/src/components/StarterPack/ProfileStarterPacks.tsx b/src/components/StarterPack/ProfileStarterPacks.tsx index 7fb0545a21..00afbdcfe9 100644 --- a/src/components/StarterPack/ProfileStarterPacks.tsx +++ b/src/components/StarterPack/ProfileStarterPacks.tsx @@ -12,15 +12,15 @@ import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' import {InfiniteData, UseInfiniteQueryResult} from '@tanstack/react-query' +import {useGenerateStarterPackMutation} from '#/lib/generate-starterpack' +import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset' +import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' +import {NavigationProp} from '#/lib/routes/types' +import {parseStarterPackUri} from '#/lib/strings/starter-pack' import {logger} from '#/logger' -import {useGenerateStarterPackMutation} from 'lib/generate-starterpack' -import {useBottomBarOffset} from 'lib/hooks/useBottomBarOffset' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {NavigationProp} from 'lib/routes/types' -import {parseStarterPackUri} from 'lib/strings/starter-pack' -import {List, ListRef} from 'view/com/util/List' -import {Text} from 'view/com/util/text/Text' -import {atoms as a, useTheme} from '#/alf' +import {List, ListRef} from '#/view/com/util/List' +import {Text} from '#/view/com/util/text/Text' +import {atoms as a, ios, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {useDialogControl} from '#/components/Dialog' import {LinearGradientBackground} from '#/components/LinearGradientBackground' @@ -132,6 +132,7 @@ export const ProfileStarterPacks = React.forwardRef< keyExtractor={keyExtractor} refreshing={isPTRing} headerOffset={headerOffset} + progressViewOffset={ios(0)} contentContainerStyle={{paddingBottom: headerOffset + bottomBarOffset}} indicatorStyle={t.name === 'light' ? 'black' : 'white'} removeClippedSubviews={true} diff --git a/src/screens/Profile/Header/index.tsx b/src/screens/Profile/Header/index.tsx index c7ef34b701..cdb0667d06 100644 --- a/src/screens/Profile/Header/index.tsx +++ b/src/screens/Profile/Header/index.tsx @@ -7,18 +7,22 @@ import { RichText as RichTextAPI, } from '@atproto/api' -import {usePalette} from 'lib/hooks/usePalette' -import {LoadingPlaceholder} from 'view/com/util/LoadingPlaceholder' +import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' +import {useTheme} from '#/alf' import {ProfileHeaderLabeler} from './ProfileHeaderLabeler' import {ProfileHeaderStandard} from './ProfileHeaderStandard' let ProfileHeaderLoading = (_props: {}): React.ReactNode => { - const pal = usePalette('default') + const t = useTheme() return ( - + + style={[ + t.atoms.bg, + {borderColor: t.atoms.bg.backgroundColor}, + styles.avi, + ]}> diff --git a/src/screens/Profile/Sections/Feed.tsx b/src/screens/Profile/Sections/Feed.tsx index fc4eff02c8..22ac5df9a7 100644 --- a/src/screens/Profile/Sections/Feed.tsx +++ b/src/screens/Profile/Sections/Feed.tsx @@ -4,17 +4,18 @@ import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useQueryClient} from '@tanstack/react-query' +import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' +import {usePalette} from '#/lib/hooks/usePalette' import {isNative} from '#/platform/detection' import {FeedDescriptor} from '#/state/queries/post-feed' import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' import {truncateAndInvalidate} from '#/state/queries/util' -import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender' -import {usePalette} from 'lib/hooks/usePalette' +import {Feed} from '#/view/com/posts/Feed' +import {EmptyState} from '#/view/com/util/EmptyState' +import {ListRef} from '#/view/com/util/List' +import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn' import {Text} from '#/view/com/util/text/Text' -import {Feed} from 'view/com/posts/Feed' -import {EmptyState} from 'view/com/util/EmptyState' -import {ListRef} from 'view/com/util/List' -import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' +import {ios} from '#/alf' import {SectionRef} from './types' interface FeedSectionProps { @@ -82,6 +83,7 @@ export const ProfileFeedSection = React.forwardRef< onScrolledDownChange={setIsScrolledDown} renderEmptyState={renderPostsEmpty} headerOffset={headerHeight} + progressViewOffset={ios(0)} renderEndOfFeed={ProfileEndOfFeed} ignoreFilterFor={ignoreFilterFor} initialNumToRender={ diff --git a/src/view/com/feeds/ProfileFeedgens.tsx b/src/view/com/feeds/ProfileFeedgens.tsx index 6f98cc49a4..693a8e361a 100644 --- a/src/view/com/feeds/ProfileFeedgens.tsx +++ b/src/view/com/feeds/ProfileFeedgens.tsx @@ -15,9 +15,9 @@ import {logger} from '#/logger' import {isNative, isWeb} from '#/platform/detection' import {usePreferencesQuery} from '#/state/queries/preferences' import {RQKEY, useProfileFeedgensQuery} from '#/state/queries/profile-feedgens' +import {EmptyState} from '#/view/com/util/EmptyState' import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' -import {EmptyState} from 'view/com/util/EmptyState' -import {atoms as a, useTheme} from '#/alf' +import {atoms as a, ios, useTheme} from '#/alf' import * as FeedCard from '#/components/FeedCard' import {ErrorMessage} from '../util/error/ErrorMessage' import {List, ListRef} from '../util/List' @@ -191,6 +191,7 @@ export const ProfileFeedgens = React.forwardRef< refreshing={isPTRing} onRefresh={onRefresh} headerOffset={headerOffset} + progressViewOffset={ios(0)} contentContainerStyle={isNative && {paddingBottom: headerOffset + 100}} indicatorStyle={t.name === 'light' ? 'black' : 'white'} removeClippedSubviews={true} diff --git a/src/view/com/lists/ProfileLists.tsx b/src/view/com/lists/ProfileLists.tsx index f633774c7a..117164413f 100644 --- a/src/view/com/lists/ProfileLists.tsx +++ b/src/view/com/lists/ProfileLists.tsx @@ -10,14 +10,14 @@ import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useQueryClient} from '@tanstack/react-query' +import {useAnalytics} from '#/lib/analytics/analytics' import {cleanError} from '#/lib/strings/errors' import {logger} from '#/logger' import {isNative, isWeb} from '#/platform/detection' import {RQKEY, useProfileListsQuery} from '#/state/queries/profile-lists' -import {useAnalytics} from 'lib/analytics/analytics' +import {EmptyState} from '#/view/com/util/EmptyState' import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' -import {EmptyState} from 'view/com/util/EmptyState' -import {atoms as a, useTheme} from '#/alf' +import {atoms as a, ios, useTheme} from '#/alf' import * as ListCard from '#/components/ListCard' import {ErrorMessage} from '../util/error/ErrorMessage' import {List, ListRef} from '../util/List' @@ -192,6 +192,7 @@ export const ProfileLists = React.forwardRef( refreshing={isPTRing} onRefresh={onRefresh} headerOffset={headerOffset} + progressViewOffset={ios(0)} contentContainerStyle={ isNative && {paddingBottom: headerOffset + 100} } diff --git a/src/view/com/pager/PagerWithHeader.tsx b/src/view/com/pager/PagerWithHeader.tsx index 7b1d8b78f4..559bc70f13 100644 --- a/src/view/com/pager/PagerWithHeader.tsx +++ b/src/view/com/pager/PagerWithHeader.tsx @@ -19,8 +19,8 @@ import Animated, { import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {ScrollProvider} from '#/lib/ScrollContext' -import {isIOS} from 'platform/detection' -import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager' +import {isIOS} from '#/platform/detection' +import {Pager, PagerRef, RenderTabBarFnProps} from '#/view/com/pager/Pager' import {ListMethods} from '../util/List' import {TabBar} from './TabBar' @@ -41,6 +41,7 @@ export interface PagerWithHeaderProps { initialPage?: number onPageSelected?: (index: number) => void onCurrentPageSelected?: (index: number) => void + allowHeaderOverScroll?: boolean } export const PagerWithHeader = React.forwardRef( function PageWithHeaderImpl( @@ -53,6 +54,7 @@ export const PagerWithHeader = React.forwardRef( initialPage, onPageSelected, onCurrentPageSelected, + allowHeaderOverScroll, }: PagerWithHeaderProps, ref, ) { @@ -92,6 +94,7 @@ export const PagerWithHeader = React.forwardRef( onSelect={props.onSelect} scrollY={scrollY} testID={testID} + allowHeaderOverScroll={allowHeaderOverScroll} /> ) }, @@ -106,6 +109,7 @@ export const PagerWithHeader = React.forwardRef( onHeaderOnlyLayout, scrollY, testID, + allowHeaderOverScroll, ], ) @@ -216,6 +220,7 @@ let PagerTabBar = ({ onTabBarLayout, onCurrentPageSelected, onSelect, + allowHeaderOverScroll, }: { currentPage: number headerOnlyHeight: number @@ -228,14 +233,20 @@ let PagerTabBar = ({ onTabBarLayout: (e: LayoutChangeEvent) => void onCurrentPageSelected?: (index: number) => void onSelect?: (index: number) => void + allowHeaderOverScroll?: boolean }): React.ReactNode => { - const headerTransform = useAnimatedStyle(() => ({ - transform: [ - { - translateY: Math.min(Math.min(scrollY.value, headerOnlyHeight) * -1, 0), - }, - ], - })) + const headerTransform = useAnimatedStyle(() => { + const translateY = Math.min(scrollY.value, headerOnlyHeight) * -1 + return { + transform: [ + { + translateY: allowHeaderOverScroll + ? translateY + : Math.min(translateY, 0), + }, + ], + } + }) const headerRef = React.useRef(null) return ( JSX.Element testID?: string headerOffset?: number + progressViewOffset?: number desktopFixedHeightOffset?: number ListHeaderComponent?: () => JSX.Element extraData?: any @@ -548,6 +550,7 @@ let Feed = ({ refreshing={isPTRing} onRefresh={onRefresh} headerOffset={headerOffset} + progressViewOffset={progressViewOffset} contentContainerStyle={{ minHeight: Dimensions.get('window').height * 1.5, }} diff --git a/src/view/com/util/List.tsx b/src/view/com/util/List.tsx index f9aeae1a86..53b0547d45 100644 --- a/src/view/com/util/List.tsx +++ b/src/view/com/util/List.tsx @@ -4,11 +4,11 @@ import {runOnJS, useSharedValue} from 'react-native-reanimated' import {updateActiveVideoViewAsync} from '@haileyok/bluesky-video' import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' -import {usePalette} from '#/lib/hooks/usePalette' +import {useDedupe} from '#/lib/hooks/useDedupe' import {useScrollHandlers} from '#/lib/ScrollContext' -import {useDedupe} from 'lib/hooks/useDedupe' -import {addStyle} from 'lib/styles' -import {isIOS} from 'platform/detection' +import {addStyle} from '#/lib/styles' +import {isIOS} from '#/platform/detection' +import {useTheme} from '#/alf' import {FlatList_INTERNAL} from './Views' export type ListMethods = FlatList_INTERNAL @@ -44,12 +44,13 @@ function ListImpl( onItemSeen, headerOffset, style, + progressViewOffset, ...props }: ListProps, ref: React.Ref, ) { const isScrolledDown = useSharedValue(false) - const pal = usePalette('default') + const t = useTheme() const dedupe = useDedupe(400) function handleScrolledDownChange(didScrollDown: boolean) { @@ -120,9 +121,9 @@ function ListImpl( ) } diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index 879632e9ef..b37445fad6 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -37,11 +37,11 @@ import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell' import {useComposerControls} from '#/state/shell/composer' import {ProfileFeedgens} from '#/view/com/feeds/ProfileFeedgens' import {ProfileLists} from '#/view/com/lists/ProfileLists' +import {PagerWithHeader} from '#/view/com/pager/PagerWithHeader' import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' import {FAB} from '#/view/com/util/fab/FAB' import {ListRef} from '#/view/com/util/List' import {CenteredView} from '#/view/com/util/Views' -import {PagerWithHeader} from 'view/com/pager/PagerWithHeader' import {ProfileHeader, ProfileHeaderLoading} from '#/screens/Profile/Header' import {ProfileFeedSection} from '#/screens/Profile/Sections/Feed' import {ProfileLabelsSection} from '#/screens/Profile/Sections/Labels' @@ -363,7 +363,8 @@ function ProfileScreenLoaded({ items={sectionTitles} onPageSelected={onPageSelected} onCurrentPageSelected={onCurrentPageSelected} - renderHeader={renderHeader}> + renderHeader={renderHeader} + allowHeaderOverScroll> {showFiltersTab ? ({headerHeight, isFocused, scrollElRef}) => ( Date: Wed, 25 Sep 2024 15:01:25 +0100 Subject: [PATCH 04/44] Header blurred banner on overscroll (take 2) (#5474) * grow banner when overscrolling * add blurview * make backdrop blur as it scrolls * add activity indicator * use rotated spinner instead of arrow * persist position of back button * make back button prettier * make blur less jarring * Unify effects * Tweak impl * determine if should animate based on scroll amount * sign comment --------- Co-authored-by: Dan Abramov --- package.json | 1 + src/screens/Profile/Header/GrowableBanner.tsx | 212 ++++++++++++++++++ src/screens/Profile/Header/Shell.tsx | 84 ++++--- src/state/queries/actor-starter-packs.ts | 4 +- src/state/queries/post-feed.ts | 20 +- src/state/queries/profile-feedgens.ts | 2 +- src/state/queries/profile-lists.ts | 2 +- src/view/com/pager/PagerHeaderContext.tsx | 41 ++++ src/view/com/pager/PagerWithHeader.tsx | 31 +-- src/view/com/util/UserBanner.tsx | 2 +- yarn.lock | 5 + 11 files changed, 341 insertions(+), 63 deletions(-) create mode 100644 src/screens/Profile/Header/GrowableBanner.tsx create mode 100644 src/view/com/pager/PagerHeaderContext.tsx diff --git a/package.json b/package.json index 4b3486545e..5e4d896cce 100644 --- a/package.json +++ b/package.json @@ -120,6 +120,7 @@ "eventemitter3": "^5.0.1", "expo": "^51.0.8", "expo-application": "^5.9.1", + "expo-blur": "^13.0.2", "expo-build-properties": "^0.12.1", "expo-camera": "~15.0.9", "expo-clipboard": "^6.0.3", diff --git a/src/screens/Profile/Header/GrowableBanner.tsx b/src/screens/Profile/Header/GrowableBanner.tsx new file mode 100644 index 0000000000..bccc1e57e5 --- /dev/null +++ b/src/screens/Profile/Header/GrowableBanner.tsx @@ -0,0 +1,212 @@ +import React, {useEffect, useState} from 'react' +import {View} from 'react-native' +import {ActivityIndicator} from 'react-native' +import Animated, { + Extrapolation, + interpolate, + runOnJS, + SharedValue, + useAnimatedProps, + useAnimatedReaction, + useAnimatedStyle, +} from 'react-native-reanimated' +import {BlurView} from 'expo-blur' +import {useIsFetching} from '@tanstack/react-query' + +import {isIOS} from '#/platform/detection' +import {RQKEY_ROOT as STARTERPACK_RQKEY_ROOT} from '#/state/queries/actor-starter-packs' +import {RQKEY_ROOT as FEED_RQKEY_ROOT} from '#/state/queries/post-feed' +import {RQKEY_ROOT as FEEDGEN_RQKEY_ROOT} from '#/state/queries/profile-feedgens' +import {RQKEY_ROOT as LIST_RQKEY_ROOT} from '#/state/queries/profile-lists' +import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext' +import {atoms as a} from '#/alf' + +const AnimatedBlurView = Animated.createAnimatedComponent(BlurView) + +export function GrowableBanner({ + backButton, + children, +}: { + backButton?: React.ReactNode + children: React.ReactNode +}) { + const pagerContext = usePagerHeaderContext() + + // pagerContext should only be present on iOS, but better safe than sorry + if (!pagerContext || !isIOS) { + return ( + + {backButton} + {children} + + ) + } + + const {scrollY} = pagerContext + + return ( + + {children} + + ) +} + +function GrowableBannerInner({ + scrollY, + backButton, + children, +}: { + scrollY: SharedValue + backButton?: React.ReactNode + children: React.ReactNode +}) { + const isFetching = useIsProfileFetching() + const animateSpinner = useShouldAnimateSpinner({isFetching, scrollY}) + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [ + { + scale: interpolate(scrollY.value, [-150, 0], [2, 1], { + extrapolateRight: Extrapolation.CLAMP, + }), + }, + ], + })) + + const animatedBlurViewProps = useAnimatedProps(() => { + return { + intensity: interpolate( + scrollY.value, + [-400, -100, -15], + [70, 60, 0], + Extrapolation.CLAMP, + ), + } + }) + + const animatedSpinnerStyle = useAnimatedStyle(() => { + return { + display: scrollY.value < 0 ? 'flex' : 'none', + opacity: interpolate( + scrollY.value, + [-60, -15], + [1, 0], + Extrapolation.CLAMP, + ), + transform: [ + {translateY: interpolate(scrollY.value, [-150, 0], [-75, 0])}, + {rotate: '90deg'}, + ], + } + }) + + const animatedBackButtonStyle = useAnimatedStyle(() => ({ + transform: [ + { + translateY: interpolate(scrollY.value, [-150, 60], [-150, 60], { + extrapolateRight: Extrapolation.CLAMP, + }), + }, + ], + })) + + return ( + <> + + {children} + + + + + + + + + {backButton} + + + ) +} + +function useIsProfileFetching() { + // are any of the profile-related queries fetching? + return [ + useIsFetching({queryKey: [FEED_RQKEY_ROOT]}), + useIsFetching({queryKey: [FEEDGEN_RQKEY_ROOT]}), + useIsFetching({queryKey: [LIST_RQKEY_ROOT]}), + useIsFetching({queryKey: [STARTERPACK_RQKEY_ROOT]}), + ].some(isFetching => isFetching) +} + +function useShouldAnimateSpinner({ + isFetching, + scrollY, +}: { + isFetching: boolean + scrollY: SharedValue +}) { + const [isOverscrolled, setIsOverscrolled] = useState(false) + // HACK: it reports a scroll pos of 0 for a tick when fetching finishes + // so paper over that by keeping it true for a bit -sfn + const stickyIsOverscrolled = useStickyToggle(isOverscrolled, 10) + + useAnimatedReaction( + () => scrollY.value < -5, + (value, prevValue) => { + if (value !== prevValue) { + runOnJS(setIsOverscrolled)(value) + } + }, + [scrollY], + ) + + const [isAnimating, setIsAnimating] = useState(isFetching) + + if (isFetching && !isAnimating) { + setIsAnimating(true) + } + + if (!isFetching && isAnimating && !stickyIsOverscrolled) { + setIsAnimating(false) + } + + return isAnimating +} + +// stayed true for at least `delay` ms before returning to false +function useStickyToggle(value: boolean, delay: number) { + const [prevValue, setPrevValue] = useState(value) + const [isSticking, setIsSticking] = useState(false) + + useEffect(() => { + if (isSticking) { + const timeout = setTimeout(() => setIsSticking(false), delay) + return () => clearTimeout(timeout) + } + }, [isSticking, delay]) + + if (value !== prevValue) { + setIsSticking(prevValue) // Going true -> false should stick. + setPrevValue(value) + return prevValue ? true : value + } + + return isSticking ? true : value +} diff --git a/src/screens/Profile/Header/Shell.tsx b/src/screens/Profile/Header/Shell.tsx index 90c2830907..d31912ddad 100644 --- a/src/screens/Profile/Header/Shell.tsx +++ b/src/screens/Profile/Header/Shell.tsx @@ -6,19 +6,20 @@ import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' +import {BACK_HITSLOP} from '#/lib/constants' +import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' +import {NavigationProp} from '#/lib/routes/types' +import {isIOS} from '#/platform/detection' import {Shadow} from '#/state/cache/types' import {ProfileImageLightbox, useLightboxControls} from '#/state/lightbox' import {useSession} from '#/state/session' -import {BACK_HITSLOP} from 'lib/constants' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {NavigationProp} from 'lib/routes/types' -import {isIOS} from 'platform/detection' -import {LoadingPlaceholder} from 'view/com/util/LoadingPlaceholder' -import {UserAvatar} from 'view/com/util/UserAvatar' -import {UserBanner} from 'view/com/util/UserBanner' +import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' +import {UserAvatar} from '#/view/com/util/UserAvatar' +import {UserBanner} from '#/view/com/util/UserBanner' import {atoms as a, useTheme} from '#/alf' import {LabelsOnMe} from '#/components/moderation/LabelsOnMe' import {ProfileHeaderAlerts} from '#/components/moderation/ProfileHeaderAlerts' +import {GrowableBanner} from './GrowableBanner' interface Props { profile: Shadow @@ -63,20 +64,45 @@ let ProfileHeaderShell = ({ return ( - - {isPlaceholderProfile ? ( - - ) : ( - - )} + + + {!isDesktop && !hideBackButton && ( + + + + + + )} + + }> + {isPlaceholderProfile ? ( + + ) : ( + + )} + {children} @@ -93,19 +119,6 @@ let ProfileHeaderShell = ({ )} - {!isDesktop && !hideBackButton && ( - - - - - - )} [RQKEY_ROOT, did] export function useActorStarterPacksQuery({did}: {did?: string}) { diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts index 7daf441adb..ae30ef0d69 100644 --- a/src/state/queries/post-feed.ts +++ b/src/state/queries/post-feed.ts @@ -15,24 +15,24 @@ import { useInfiniteQuery, } from '@tanstack/react-query' +import {AuthorFeedAPI} from '#/lib/api/feed/author' +import {CustomFeedAPI} from '#/lib/api/feed/custom' +import {FollowingFeedAPI} from '#/lib/api/feed/following' import {HomeFeedAPI} from '#/lib/api/feed/home' +import {LikesFeedAPI} from '#/lib/api/feed/likes' +import {ListFeedAPI} from '#/lib/api/feed/list' +import {MergeFeedAPI} from '#/lib/api/feed/merge' +import {FeedAPI, ReasonFeedSource} from '#/lib/api/feed/types' import {aggregateUserInterests} from '#/lib/api/feed/utils' +import {FeedTuner, FeedTunerFn} from '#/lib/api/feed-manip' import {DISCOVER_FEED_URI} from '#/lib/constants' +import {BSKY_FEED_OWNER_DIDS} from '#/lib/constants' import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped' import {logger} from '#/logger' import {STALE} from '#/state/queries' import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences/const' import {useAgent} from '#/state/session' import * as userActionHistory from '#/state/userActionHistory' -import {AuthorFeedAPI} from 'lib/api/feed/author' -import {CustomFeedAPI} from 'lib/api/feed/custom' -import {FollowingFeedAPI} from 'lib/api/feed/following' -import {LikesFeedAPI} from 'lib/api/feed/likes' -import {ListFeedAPI} from 'lib/api/feed/list' -import {MergeFeedAPI} from 'lib/api/feed/merge' -import {FeedAPI, ReasonFeedSource} from 'lib/api/feed/types' -import {FeedTuner, FeedTunerFn} from 'lib/api/feed-manip' -import {BSKY_FEED_OWNER_DIDS} from 'lib/constants' import {KnownError} from '#/view/com/posts/FeedErrorMessage' import {useFeedTuners} from '../preferences/feed-tuners' import {useModerationOpts} from '../preferences/moderation-opts' @@ -65,7 +65,7 @@ export interface FeedParams { type RQPageParam = {cursor: string | undefined; api: FeedAPI} | undefined -const RQKEY_ROOT = 'post-feed' +export const RQKEY_ROOT = 'post-feed' export function RQKEY(feedDesc: FeedDescriptor, params?: FeedParams) { return [RQKEY_ROOT, feedDesc, params || {}] } diff --git a/src/state/queries/profile-feedgens.ts b/src/state/queries/profile-feedgens.ts index b50a2a2890..79d9735c98 100644 --- a/src/state/queries/profile-feedgens.ts +++ b/src/state/queries/profile-feedgens.ts @@ -8,7 +8,7 @@ const PAGE_SIZE = 50 type RQPageParam = string | undefined // TODO refactor invalidate on mutate? -const RQKEY_ROOT = 'profile-feedgens' +export const RQKEY_ROOT = 'profile-feedgens' export const RQKEY = (did: string) => [RQKEY_ROOT, did] export function useProfileFeedgensQuery( diff --git a/src/state/queries/profile-lists.ts b/src/state/queries/profile-lists.ts index 03c983ff80..5c9f9f0d6f 100644 --- a/src/state/queries/profile-lists.ts +++ b/src/state/queries/profile-lists.ts @@ -7,7 +7,7 @@ import {useModerationOpts} from '../preferences/moderation-opts' const PAGE_SIZE = 30 type RQPageParam = string | undefined -const RQKEY_ROOT = 'profile-lists' +export const RQKEY_ROOT = 'profile-lists' export const RQKEY = (did: string) => [RQKEY_ROOT, did] export function useProfileListsQuery(did: string, opts?: {enabled?: boolean}) { diff --git a/src/view/com/pager/PagerHeaderContext.tsx b/src/view/com/pager/PagerHeaderContext.tsx new file mode 100644 index 0000000000..fd4cc74632 --- /dev/null +++ b/src/view/com/pager/PagerHeaderContext.tsx @@ -0,0 +1,41 @@ +import React, {useContext} from 'react' +import {SharedValue} from 'react-native-reanimated' + +import {isIOS} from '#/platform/detection' + +export const PagerHeaderContext = + React.createContext | null>(null) + +/** + * Passes the scrollY value to the pager header's banner, so it can grow on + * overscroll on iOS. Not necessary to use this context provider on other platforms. + * + * @platform ios + */ +export function PagerHeaderProvider({ + scrollY, + children, +}: { + scrollY: SharedValue + children: React.ReactNode +}) { + return ( + + {children} + + ) +} + +export function usePagerHeaderContext() { + const scrollY = useContext(PagerHeaderContext) + if (isIOS) { + if (!scrollY) { + throw new Error( + 'usePagerHeaderContext must be used within a HeaderProvider', + ) + } + return {scrollY} + } else { + return null + } +} diff --git a/src/view/com/pager/PagerWithHeader.tsx b/src/view/com/pager/PagerWithHeader.tsx index 559bc70f13..528f7fdf2e 100644 --- a/src/view/com/pager/PagerWithHeader.tsx +++ b/src/view/com/pager/PagerWithHeader.tsx @@ -22,6 +22,7 @@ import {ScrollProvider} from '#/lib/ScrollContext' import {isIOS} from '#/platform/detection' import {Pager, PagerRef, RenderTabBarFnProps} from '#/view/com/pager/Pager' import {ListMethods} from '../util/List' +import {PagerHeaderProvider} from './PagerHeaderContext' import {TabBar} from './TabBar' export interface PagerWithHeaderChildParams { @@ -82,20 +83,22 @@ export const PagerWithHeader = React.forwardRef( const renderTabBar = React.useCallback( (props: RenderTabBarFnProps) => { return ( - + + + ) }, [ diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx index 13f4081fce..0e07a57454 100644 --- a/src/view/com/util/UserBanner.tsx +++ b/src/view/com/util/UserBanner.tsx @@ -202,7 +202,7 @@ const styles = StyleSheet.create({ }, bannerImage: { width: '100%', - height: 150, + height: '100%', }, defaultBanner: { backgroundColor: '#0070ff', diff --git a/yarn.lock b/yarn.lock index 17fe862372..db8a707a40 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12165,6 +12165,11 @@ expo-asset@~10.0.6: invariant "^2.2.4" md5-file "^3.2.3" +expo-blur@^13.0.2: + version "13.0.2" + resolved "https://registry.yarnpkg.com/expo-blur/-/expo-blur-13.0.2.tgz#c2d179b19b13830db1d8b90c51373235f462e958" + integrity sha512-t2p7BChO3Reykued++QJRMZ/og6J3aXtSQ+bU31YcBeXhZLkHwjWEhiPKPnJka7J2/yTs4+jOCNDY0kCZmcE3w== + expo-build-properties@^0.12.1: version "0.12.1" resolved "https://registry.yarnpkg.com/expo-build-properties/-/expo-build-properties-0.12.1.tgz#8d11759b8f382e4654e2482ddcec4f9ad4530aad" From 2296ea338e8f7b4906a928e802267837c06754cc Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 25 Sep 2024 15:02:29 +0100 Subject: [PATCH 05/44] subtle avatar grow animation (#5480) --- src/screens/Profile/Header/GrowableAvatar.tsx | 61 +++++++++++++++++++ src/screens/Profile/Header/Shell.tsx | 49 ++++++++------- 2 files changed, 88 insertions(+), 22 deletions(-) create mode 100644 src/screens/Profile/Header/GrowableAvatar.tsx diff --git a/src/screens/Profile/Header/GrowableAvatar.tsx b/src/screens/Profile/Header/GrowableAvatar.tsx new file mode 100644 index 0000000000..20ac14892d --- /dev/null +++ b/src/screens/Profile/Header/GrowableAvatar.tsx @@ -0,0 +1,61 @@ +import React from 'react' +import {StyleProp, View, ViewStyle} from 'react-native' +import Animated, { + Extrapolation, + interpolate, + SharedValue, + useAnimatedStyle, +} from 'react-native-reanimated' + +import {isIOS} from '#/platform/detection' +import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext' + +export function GrowableAvatar({ + children, + style, +}: { + children: React.ReactNode + style?: StyleProp +}) { + const pagerContext = usePagerHeaderContext() + + // pagerContext should only be present on iOS, but better safe than sorry + if (!pagerContext || !isIOS) { + return {children} + } + + const {scrollY} = pagerContext + + return ( + + {children} + + ) +} + +function GrowableAvatarInner({ + scrollY, + children, + style, +}: { + scrollY: SharedValue + children: React.ReactNode + style?: StyleProp +}) { + const animatedStyle = useAnimatedStyle(() => ({ + transform: [ + { + scale: interpolate(scrollY.value, [-150, 0], [1.2, 1], { + extrapolateRight: Extrapolation.CLAMP, + }), + }, + ], + })) + + return ( + + {children} + + ) +} diff --git a/src/screens/Profile/Header/Shell.tsx b/src/screens/Profile/Header/Shell.tsx index d31912ddad..f7011fd359 100644 --- a/src/screens/Profile/Header/Shell.tsx +++ b/src/screens/Profile/Header/Shell.tsx @@ -19,6 +19,7 @@ import {UserBanner} from '#/view/com/util/UserBanner' import {atoms as a, useTheme} from '#/alf' import {LabelsOnMe} from '#/components/moderation/LabelsOnMe' import {ProfileHeaderAlerts} from '#/components/moderation/ProfileHeaderAlerts' +import {GrowableAvatar} from './GrowableAvatar' import {GrowableBanner} from './GrowableBanner' interface Props { @@ -119,27 +120,29 @@ let ProfileHeaderShell = ({ )} - - - - - + + + + + + + ) } @@ -168,10 +171,12 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', }, - avi: { + aviPosition: { position: 'absolute', top: 110, left: 10, + }, + avi: { width: 94, height: 94, borderRadius: 47, From be3c6ab93a5e3f573ceb8909df068d8a87f86474 Mon Sep 17 00:00:00 2001 From: Hailey Date: Wed, 25 Sep 2024 08:32:58 -0700 Subject: [PATCH 06/44] Improve style of reply bar (#5447) Co-authored-by: Samuel Newman --- src/lib/custom-animations/PressableScale.tsx | 53 +++++++++++ .../post-thread/PostThreadComposePrompt.tsx | 95 +++++++++++-------- 2 files changed, 107 insertions(+), 41 deletions(-) create mode 100644 src/lib/custom-animations/PressableScale.tsx diff --git a/src/lib/custom-animations/PressableScale.tsx b/src/lib/custom-animations/PressableScale.tsx new file mode 100644 index 0000000000..68315a9783 --- /dev/null +++ b/src/lib/custom-animations/PressableScale.tsx @@ -0,0 +1,53 @@ +import React from 'react' +import {Pressable, PressableProps} from 'react-native' +import Animated, { + cancelAnimation, + runOnJS, + useAnimatedStyle, + useSharedValue, + withTiming, +} from 'react-native-reanimated' + +import {isTouchDevice} from '#/lib/browser' +import {isNative} from '#/platform/detection' + +const DEFAULT_TARGET_SCALE = isNative || isTouchDevice ? 0.98 : 1 + +export function PressableScale({ + targetScale = DEFAULT_TARGET_SCALE, + children, + ...rest +}: {targetScale?: number} & Exclude< + PressableProps, + 'onPressIn' | 'onPressOut' +>) { + const scale = useSharedValue(1) + + const style = useAnimatedStyle(() => ({ + transform: [{scale: scale.value}], + })) + + return ( + { + 'worklet' + if (rest.onPressIn) { + runOnJS(rest.onPressIn)(e) + } + cancelAnimation(scale) + scale.value = withTiming(targetScale, {duration: 100}) + }} + onPressOut={e => { + 'worklet' + if (rest.onPressOut) { + runOnJS(rest.onPressOut)(e) + } + cancelAnimation(scale) + scale.value = withTiming(1, {duration: 100}) + }} + {...rest}> + {children as React.ReactNode} + + ) +} diff --git a/src/view/com/post-thread/PostThreadComposePrompt.tsx b/src/view/com/post-thread/PostThreadComposePrompt.tsx index 62b28cc759..7586bd9768 100644 --- a/src/view/com/post-thread/PostThreadComposePrompt.tsx +++ b/src/view/com/post-thread/PostThreadComposePrompt.tsx @@ -1,14 +1,17 @@ import React from 'react' -import {StyleSheet, TouchableOpacity} from 'react-native' +import {View} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {PressableScale} from '#/lib/custom-animations/PressableScale' +import {useHaptics} from '#/lib/haptics' +import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' +import {useHapticsDisabled} from '#/state/preferences' import {useProfileQuery} from '#/state/queries/profile' import {useSession} from '#/state/session' -import {usePalette} from 'lib/hooks/usePalette' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {Text} from '../util/text/Text' -import {UserAvatar} from '../util/UserAvatar' +import {UserAvatar} from '#/view/com/util/UserAvatar' +import {atoms as a, useTheme} from '#/alf' +import {Text} from '#/components/Typography' export function PostThreadComposePrompt({ onPressCompose, @@ -17,47 +20,57 @@ export function PostThreadComposePrompt({ }) { const {currentAccount} = useSession() const {data: profile} = useProfileQuery({did: currentAccount?.did}) - const pal = usePalette('default') const {_} = useLingui() - const {isDesktop} = useWebMediaQueries() + const {isTabletOrDesktop} = useWebMediaQueries() + const t = useTheme() + const playHaptics = useHaptics() + const isHapticsDisabled = useHapticsDisabled() + + const onPress = () => { + playHaptics('Light') + setTimeout( + () => { + onPressCompose() + }, + isHapticsDisabled ? 0 : 75, + ) + } + return ( - onPressCompose()} + - - + - Write your reply - - + + + Write your reply + + + ) } - -const styles = StyleSheet.create({ - prompt: { - paddingHorizontal: 16, - paddingTop: 10, - paddingBottom: 10, - flexDirection: 'row', - alignItems: 'center', - borderTopWidth: StyleSheet.hairlineWidth, - }, - labelMobile: { - paddingLeft: 12, - }, - labelDesktopWeb: { - paddingLeft: 12, - }, -}) From 224ff42c233007545152b2bfa809e736edb9cc34 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 25 Sep 2024 10:38:32 -0500 Subject: [PATCH 07/44] Add gate to increase post-feed page size (#5473) * Add gate to increase post-feed page size * Exclude Discover * Remove exception * Clarify intent * Let gate cache --- src/lib/statsig/gates.ts | 4 +++- src/state/queries/post-feed.ts | 22 ++++++++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts index 7966767d1b..866d87aef0 100644 --- a/src/lib/statsig/gates.ts +++ b/src/lib/statsig/gates.ts @@ -1,3 +1,5 @@ export type Gate = // Keep this alphabetic please. - 'debug_show_feedcontext' | 'suggested_feeds_interstitial' + | 'debug_show_feedcontext' + | 'post_feed_lang_window' + | 'suggested_feeds_interstitial' diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts index ae30ef0d69..07c5da81b7 100644 --- a/src/state/queries/post-feed.ts +++ b/src/state/queries/post-feed.ts @@ -28,6 +28,7 @@ import {FeedTuner, FeedTunerFn} from '#/lib/api/feed-manip' import {DISCOVER_FEED_URI} from '#/lib/constants' import {BSKY_FEED_OWNER_DIDS} from '#/lib/constants' import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped' +import {useGate} from '#/lib/statsig/statsig' import {logger} from '#/logger' import {STALE} from '#/state/queries' import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences/const' @@ -109,13 +110,19 @@ export interface FeedPage { fetchedAt: number } -const PAGE_SIZE = 30 +/** + * The minimum number of posts we want in a single "page" of results. Since we + * filter out unwanted content, we may fetch more than this number to ensure + * that we get _at least_ this number. + */ +const MIN_POSTS = 30 export function usePostFeedQuery( feedDesc: FeedDescriptor, params?: FeedParams, opts?: {enabled?: boolean; ignoreFilterFor?: string}, ) { + const gate = useGate() const feedTuners = useFeedTuners(feedDesc) const moderationOpts = useModerationOpts() const {data: preferences} = usePreferencesQuery() @@ -135,6 +142,13 @@ export function usePostFeedQuery( } | null>(null) const isDiscover = feedDesc.includes(DISCOVER_FEED_URI) + /** + * The number of posts to fetch in a single request. Because we filter + * unwanted content, we may over-fetch here to try and fill pages by + * `MIN_POSTS`. + */ + const fetchLimit = gate('post_feed_lang_window') ? 100 : MIN_POSTS + // Make sure this doesn't invalidate unless really needed. const selectArgs = React.useMemo( () => ({ @@ -175,7 +189,7 @@ export function usePostFeedQuery( } try { - const res = await api.fetch({cursor, limit: PAGE_SIZE}) + const res = await api.fetch({cursor, limit: fetchLimit}) /* * If this is a public view, we need to check if posts fail moderation. @@ -373,13 +387,13 @@ export function usePostFeedQuery( // Now track how many items we really want, and fetch more if needed. if (isLoading || isRefetching) { // During the initial fetch, we want to get an entire page's worth of items. - wantedItemCount.current = PAGE_SIZE + wantedItemCount.current = MIN_POSTS } else if (isFetchingNextPage) { if (itemCount > wantedItemCount.current) { // We have more items than wantedItemCount, so wantedItemCount must be out of date. // Some other code must have called fetchNextPage(), for example, from onEndReached. // Adjust the wantedItemCount to reflect that we want one more full page of items. - wantedItemCount.current = itemCount + PAGE_SIZE + wantedItemCount.current = itemCount + MIN_POSTS } } else if (hasNextPage) { // At this point we're not fetching anymore, so it's time to make a decision. From 3293c5e0e0bf6fe89596bd0a79dda6cd1a0c2fb5 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 25 Sep 2024 17:13:03 +0100 Subject: [PATCH 08/44] Reduce display name size (#5482) * reduce displayname size * only apply to small screens --- src/screens/Profile/Header/DisplayName.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/screens/Profile/Header/DisplayName.tsx b/src/screens/Profile/Header/DisplayName.tsx index e30162c3af..bcc56d7f66 100644 --- a/src/screens/Profile/Header/DisplayName.tsx +++ b/src/screens/Profile/Header/DisplayName.tsx @@ -5,7 +5,7 @@ import {AppBskyActorDefs, ModerationDecision} from '@atproto/api' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' import {Shadow} from '#/state/cache/types' -import {atoms as a, useTheme} from '#/alf' +import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {Text} from '#/components/Typography' export function ProfileHeaderDisplayName({ @@ -16,12 +16,19 @@ export function ProfileHeaderDisplayName({ moderation: ModerationDecision }) { const t = useTheme() + const {gtMobile} = useBreakpoints() + return ( + style={[ + t.atoms.text, + gtMobile ? a.text_4xl : a.text_3xl, + a.self_start, + {fontWeight: '600'}, + ]}> {sanitizeDisplayName( profile.displayName || sanitizeHandle(profile.handle), moderation.ui('displayName'), From 60b74435358d19322e5e4d08c45e48f58cd1efb1 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 25 Sep 2024 17:13:23 +0100 Subject: [PATCH 09/44] show a toast when a haptic is meant to fire while using simulator (#5481) buzzz! --- src/lib/haptics.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/lib/haptics.ts b/src/lib/haptics.ts index f588808fc3..234be777d3 100644 --- a/src/lib/haptics.ts +++ b/src/lib/haptics.ts @@ -1,8 +1,10 @@ import React from 'react' +import * as Device from 'expo-device' import {impactAsync, ImpactFeedbackStyle} from 'expo-haptics' import {isIOS, isWeb} from '#/platform/detection' import {useHapticsDisabled} from '#/state/preferences/disable-haptics' +import * as Toast from '#/view/com/util/Toast' export function useHaptics() { const isHapticsDisabled = useHapticsDisabled() @@ -18,6 +20,11 @@ export function useHaptics() { ? ImpactFeedbackStyle[strength] : ImpactFeedbackStyle.Light impactAsync(style) + + // DEV ONLY - show a toast when a haptic is meant to fire on simulator + if (__DEV__ && !Device.isDevice) { + Toast.show(`Buzzz!`) + } }, [isHapticsDisabled], ) From 47301661f786f032c5b2f20773a5ee9041fed64e Mon Sep 17 00:00:00 2001 From: Hailey Date: Wed, 25 Sep 2024 09:51:51 -0700 Subject: [PATCH 10/44] [Video] use dynamic import for hls.js (#5429) Co-authored-by: Dan Abramov --- .../VideoEmbedInner/VideoEmbedInnerWeb.tsx | 53 ++++++++++++++++--- .../web-controls/VideoControls.tsx | 7 ++- 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx index b49c49e4ab..fa2b7e3d3f 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx +++ b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx @@ -1,7 +1,7 @@ import React, {useEffect, useId, useRef, useState} from 'react' import {View} from 'react-native' import {AppBskyEmbedVideo} from '@atproto/api' -import Hls, {Events, FragChangedData, Fragment} from 'hls.js' +import type * as HlsTypes from 'hls.js' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {atoms as a} from '#/alf' @@ -23,6 +23,7 @@ export function VideoEmbedInnerWeb({ const videoRef = useRef(null) const [focused, setFocused] = useState(false) const [hasSubtitleTrack, setHasSubtitleTrack] = useState(false) + const [hlsLoading, setHlsLoading] = React.useState(false) const figId = useId() // send error up to error boundary @@ -37,6 +38,7 @@ export function VideoEmbedInnerWeb({ setHasSubtitleTrack, setError, videoRef, + setHlsLoading, }) return ( @@ -77,6 +79,7 @@ export function VideoEmbedInnerWeb({ setActive={setActive} focused={focused} setFocused={setFocused} + hlsLoading={hlsLoading} onScreen={onScreen} fullscreenRef={containerRef} hasSubtitleTrack={hasSubtitleTrack} @@ -99,31 +102,62 @@ export class VideoNotFoundError extends Error { } } +type CachedPromise = Promise & {value: undefined | T} +const promiseForHls = import( + // @ts-ignore + 'hls.js/dist/hls.min' +).then(mod => mod.default) as CachedPromise +promiseForHls.value = undefined +promiseForHls.then(Hls => { + promiseForHls.value = Hls +}) + function useHLS({ focused, playlist, setHasSubtitleTrack, setError, videoRef, + setHlsLoading, }: { focused: boolean playlist: string setHasSubtitleTrack: (v: boolean) => void setError: (v: Error | null) => void videoRef: React.RefObject + setHlsLoading: (v: boolean) => void }) { - const hlsRef = useRef(undefined) - const [lowQualityFragments, setLowQualityFragments] = useState([]) + const [Hls, setHls] = useState( + () => promiseForHls.value, + ) + useEffect(() => { + if (!Hls) { + setHlsLoading(true) + promiseForHls.then(loadedHls => { + setHls(() => loadedHls) + setHlsLoading(false) + }) + } + }, [Hls, setHlsLoading]) + + const hlsRef = useRef(undefined) + const [lowQualityFragments, setLowQualityFragments] = useState< + HlsTypes.Fragment[] + >([]) // purge low quality segments from buffer on next frag change const handleFragChange = useNonReactiveCallback( - (_event: Events.FRAG_CHANGED, {frag}: FragChangedData) => { + ( + _event: HlsTypes.Events.FRAG_CHANGED, + {frag}: HlsTypes.FragChangedData, + ) => { + if (!Hls) return if (!hlsRef.current) return const hls = hlsRef.current if (focused && hls.nextAutoLevel > 0) { // if the current quality level goes above 0, flush the low quality segments - const flushed: Fragment[] = [] + const flushed: HlsTypes.Fragment[] = [] for (const lowQualFrag of lowQualityFragments) { // avoid if close to the current fragment @@ -147,12 +181,15 @@ function useHLS({ useEffect(() => { if (!videoRef.current) return - if (!Hls.isSupported()) throw new HLSUnsupportedError() + if (!Hls) return + if (!Hls.isSupported()) { + throw new HLSUnsupportedError() + } const hls = new Hls({ maxMaxBufferLength: 10, // only load 10s ahead // note: the amount buffered is affected by both maxBufferLength and maxBufferSize - // it will buffer until it it's greater than *both* of those values + // it will buffer until it is greater than *both* of those values // so we use maxMaxBufferLength to set the actual maximum amount of buffering instead }) hlsRef.current = hls @@ -211,7 +248,7 @@ function useHLS({ hls.destroy() abortController.abort() } - }, [playlist, setError, setHasSubtitleTrack, videoRef, handleFragChange]) + }, [playlist, setError, setHasSubtitleTrack, videoRef, handleFragChange, Hls]) return hlsRef } diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx index 2d1427347d..dd0dafc335 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx +++ b/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx @@ -43,6 +43,7 @@ export function Controls({ setFocused, onScreen, fullscreenRef, + hlsLoading, hasSubtitleTrack, }: { videoRef: React.RefObject @@ -53,6 +54,7 @@ export function Controls({ setFocused: (focused: boolean) => void onScreen: boolean fullscreenRef: React.RefObject + hlsLoading: boolean hasSubtitleTrack: boolean }) { const { @@ -80,6 +82,7 @@ export function Controls({ const [isFullscreen, toggleFullscreen] = useFullscreen(fullscreenRef) const {state: hasFocus, onIn: onFocus, onOut: onBlur} = useInteractionState() const [interactingViaKeypress, setInteractingViaKeypress] = useState(false) + const showSpinner = hlsLoading || buffering const { state: volumeHovered, onIn: onVolumeHover, @@ -409,11 +412,11 @@ export function Controls({ )} - {(buffering || error) && ( + {(showSpinner || error) && ( - {buffering && } + {showSpinner && } {error && ( An error occurred From 498f957a1d9c702c3d2d6cfc5c175b0659ec99c0 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 25 Sep 2024 18:28:10 +0100 Subject: [PATCH 11/44] Use scale animation for tabs (#5483) * fix passing PressableScale oPressIn prop * use PressableScale for tabs --- src/lib/custom-animations/PressableScale.tsx | 27 ++++++++++++-------- src/view/shell/bottom-bar/BottomBar.tsx | 16 +++++++----- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/lib/custom-animations/PressableScale.tsx b/src/lib/custom-animations/PressableScale.tsx index 68315a9783..d6eabf8b22 100644 --- a/src/lib/custom-animations/PressableScale.tsx +++ b/src/lib/custom-animations/PressableScale.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {Pressable, PressableProps} from 'react-native' +import {Pressable, PressableProps, StyleProp, ViewStyle} from 'react-native' import Animated, { cancelAnimation, runOnJS, @@ -16,14 +16,17 @@ const DEFAULT_TARGET_SCALE = isNative || isTouchDevice ? 0.98 : 1 export function PressableScale({ targetScale = DEFAULT_TARGET_SCALE, children, + contentContainerStyle, + onPressIn, + onPressOut, ...rest -}: {targetScale?: number} & Exclude< - PressableProps, - 'onPressIn' | 'onPressOut' ->) { +}: { + targetScale?: number + contentContainerStyle?: StyleProp +} & Exclude) { const scale = useSharedValue(1) - const style = useAnimatedStyle(() => ({ + const animatedStyle = useAnimatedStyle(() => ({ transform: [{scale: scale.value}], })) @@ -32,22 +35,24 @@ export function PressableScale({ accessibilityRole="button" onPressIn={e => { 'worklet' - if (rest.onPressIn) { - runOnJS(rest.onPressIn)(e) + if (onPressIn) { + runOnJS(onPressIn)(e) } cancelAnimation(scale) scale.value = withTiming(targetScale, {duration: 100}) }} onPressOut={e => { 'worklet' - if (rest.onPressOut) { - runOnJS(rest.onPressOut)(e) + if (onPressOut) { + runOnJS(onPressOut)(e) } cancelAnimation(scale) scale.value = withTiming(1, {duration: 100}) }} {...rest}> - {children as React.ReactNode} + + {children as React.ReactNode} + ) } diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx index f6d16ae8e5..02d9427330 100644 --- a/src/view/shell/bottom-bar/BottomBar.tsx +++ b/src/view/shell/bottom-bar/BottomBar.tsx @@ -1,5 +1,5 @@ import React, {ComponentProps} from 'react' -import {GestureResponderEvent, TouchableOpacity, View} from 'react-native' +import {GestureResponderEvent, View} from 'react-native' import Animated from 'react-native-reanimated' import {useSafeAreaInsets} from 'react-native-safe-area-context' import {msg, Trans} from '@lingui/macro' @@ -8,6 +8,7 @@ import {BottomTabBarProps} from '@react-navigation/bottom-tabs' import {StackActions} from '@react-navigation/native' import {useAnalytics} from '#/lib/analytics/analytics' +import {PressableScale} from '#/lib/custom-animations/PressableScale' import {useHaptics} from '#/lib/haptics' import {useDedupe} from '#/lib/hooks/useDedupe' import {useMinimalShellFooterTransform} from '#/lib/hooks/useMinimalShellTransform' @@ -29,6 +30,7 @@ import {Text} from '#/view/com/util/text/Text' import {UserAvatar} from '#/view/com/util/UserAvatar' import {Logo} from '#/view/icons/Logo' import {Logotype} from '#/view/icons/Logotype' +import {atoms as a} from '#/alf' import {useDialogControl} from '#/components/Dialog' import {SwitchAccountDialog} from '#/components/dialogs/SwitchAccount' import { @@ -326,7 +328,7 @@ export function BottomBar({navigation}: BottomTabBarProps) { interface BtnProps extends Pick< - ComponentProps, + ComponentProps, | 'accessible' | 'accessibilityRole' | 'accessibilityHint' @@ -350,7 +352,7 @@ function Btn({ accessibilityLabel, }: BtnProps) { return ( - + accessibilityHint={accessibilityHint} + targetScale={0.8} + contentContainerStyle={[a.flex_1]}> {icon} {notificationCount ? ( - + {notificationCount} ) : undefined} - + ) } From f54241c4cf07d0119a8801d3dff2ddccdde4cf57 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 25 Sep 2024 19:42:33 +0100 Subject: [PATCH 12/44] constrain blur (#5485) --- src/screens/Profile/Header/GrowableBanner.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/screens/Profile/Header/GrowableBanner.tsx b/src/screens/Profile/Header/GrowableBanner.tsx index bccc1e57e5..e1bb8e00ef 100644 --- a/src/screens/Profile/Header/GrowableBanner.tsx +++ b/src/screens/Profile/Header/GrowableBanner.tsx @@ -77,8 +77,8 @@ function GrowableBannerInner({ return { intensity: interpolate( scrollY.value, - [-400, -100, -15], - [70, 60, 0], + [-300, -65, -15], + [50, 40, 0], Extrapolation.CLAMP, ), } From fd15bfec40a2579036a9f842a38bc0f2d673a3be Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 25 Sep 2024 15:39:03 -0500 Subject: [PATCH 13/44] Ensure notifications align with new post alignment (#5486) --- src/view/com/notifications/FeedItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx index 5fbaaa1550..3c1f51249f 100644 --- a/src/view/com/notifications/FeedItem.tsx +++ b/src/view/com/notifications/FeedItem.tsx @@ -625,7 +625,7 @@ const styles = StyleSheet.create({ flexDirection: 'row', }, layoutIcon: { - width: 70, + width: 60, alignItems: 'flex-end', paddingTop: 2, }, From 6bc001a30e4376e706fd1c10469065e0e78e1bf0 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 25 Sep 2024 15:48:40 -0500 Subject: [PATCH 14/44] Support emojis in settings account cards (#5487) --- src/view/screens/Settings/index.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/view/screens/Settings/index.tsx b/src/view/screens/Settings/index.tsx index 737ca2d28a..73bfaa83ef 100644 --- a/src/view/screens/Settings/index.tsx +++ b/src/view/screens/Settings/index.tsx @@ -94,10 +94,14 @@ function SettingsAccountCard({ /> - + {profile?.displayName || account.handle} - + {account.handle} From b1ca2503de55c41431aac38db4d164da7d506d4f Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 25 Sep 2024 15:54:43 -0500 Subject: [PATCH 15/44] Add language filtering UI to search (#5459) * Use new TextField for search bar * Add lang dropdown * Dialog * Revert "Dialog" This reverts commit 257573cd9c2a70d29df4ef5bdd503eea4ae411fe. * Extract util, test, cleanup * Fix formatting * Pass through other params * Fix sticky header * Fix stale data, hide/show * Improve query parsing * Replace memo * Couple tweaks * Revert cancel change * Remove unused placeholder --- src/alf/themes.ts | 3 + src/alf/types.ts | 1 + src/components/forms/TextField.tsx | 23 +- src/screens/Search/__tests__/utils.test.ts | 43 ++ src/screens/Search/utils.ts | 43 ++ src/view/screens/Search/Search.tsx | 461 ++++++++++++++------- src/view/screens/Storybook/Forms.tsx | 2 +- 7 files changed, 427 insertions(+), 149 deletions(-) create mode 100644 src/screens/Search/__tests__/utils.test.ts create mode 100644 src/screens/Search/utils.ts diff --git a/src/alf/themes.ts b/src/alf/themes.ts index 9f7ec5c673..0cfe09aadc 100644 --- a/src/alf/themes.ts +++ b/src/alf/themes.ts @@ -305,6 +305,7 @@ export function createThemes({ } as const const light: Theme = { + scheme: 'light', name: 'light', palette: lightPalette, atoms: { @@ -390,6 +391,7 @@ export function createThemes({ } const dark: Theme = { + scheme: 'dark', name: 'dark', palette: darkPalette, atoms: { @@ -479,6 +481,7 @@ export function createThemes({ const dim: Theme = { ...dark, + scheme: 'dark', name: 'dim', palette: dimPalette, atoms: { diff --git a/src/alf/types.ts b/src/alf/types.ts index 41822b8dd5..08ec593927 100644 --- a/src/alf/types.ts +++ b/src/alf/types.ts @@ -156,6 +156,7 @@ export type ThemedAtoms = { } } export type Theme = { + scheme: 'light' | 'dark' // for library support name: ThemeName palette: Palette atoms: ThemedAtoms diff --git a/src/components/forms/TextField.tsx b/src/components/forms/TextField.tsx index 94ee261e38..21928d3df3 100644 --- a/src/components/forms/TextField.tsx +++ b/src/components/forms/TextField.tsx @@ -135,6 +135,8 @@ export function createInput(Component: typeof TextInput) { placeholder, value, onChangeText, + onFocus, + onBlur, isInvalid, inputRef, style, @@ -173,8 +175,14 @@ export function createInput(Component: typeof TextInput) { ref={refs} value={value} onChangeText={onChangeText} - onFocus={ctx.onFocus} - onBlur={ctx.onBlur} + onFocus={e => { + ctx.onFocus() + onFocus?.(e) + }} + onBlur={e => { + ctx.onBlur() + onBlur?.(e) + }} placeholder={placeholder || label} placeholderTextColor={t.palette.contrast_500} keyboardAppearance={t.name === 'light' ? 'light' : 'dark'} @@ -188,8 +196,8 @@ export function createInput(Component: typeof TextInput) { a.px_xs, { // paddingVertical doesn't work w/multiline - esb - paddingTop: 14, - paddingBottom: 14, + paddingTop: 12, + paddingBottom: 13, lineHeight: a.text_md.fontSize * 1.1875, textAlignVertical: rest.multiline ? 'top' : undefined, minHeight: rest.multiline ? 80 : undefined, @@ -197,13 +205,14 @@ export function createInput(Component: typeof TextInput) { }, // fix for autofill styles covering border web({ - paddingTop: 12, - paddingBottom: 12, + paddingTop: 10, + paddingBottom: 11, marginTop: 2, marginBottom: 2, }), android({ - paddingBottom: 16, + paddingTop: 8, + paddingBottom: 8, }), style, ]} diff --git a/src/screens/Search/__tests__/utils.test.ts b/src/screens/Search/__tests__/utils.test.ts new file mode 100644 index 0000000000..81610cc59a --- /dev/null +++ b/src/screens/Search/__tests__/utils.test.ts @@ -0,0 +1,43 @@ +import {describe, expect, it} from '@jest/globals' + +import {parseSearchQuery} from '#/screens/Search/utils' + +describe(`parseSearchQuery`, () => { + const tests = [ + { + input: `bluesky`, + output: {query: `bluesky`, params: {}}, + }, + { + input: `bluesky from:esb.lol`, + output: {query: `bluesky`, params: {from: `esb.lol`}}, + }, + { + input: `bluesky "from:esb.lol"`, + output: {query: `bluesky "from:esb.lol"`, params: {}}, + }, + { + input: `bluesky mentions:@esb.lol`, + output: {query: `bluesky`, params: {mentions: `@esb.lol`}}, + }, + { + input: `bluesky since:2021-01-01:00:00:00`, + output: {query: `bluesky`, params: {since: `2021-01-01:00:00:00`}}, + }, + { + input: `bluesky lang:"en"`, + output: {query: `bluesky`, params: {lang: `en`}}, + }, + { + input: `bluesky "literal" lang:en "from:invalid"`, + output: {query: `bluesky "literal" "from:invalid"`, params: {lang: `en`}}, + }, + ] + + it.each(tests)( + `$input -> $output.query $output.params`, + ({input, output}) => { + expect(parseSearchQuery(input)).toEqual(output) + }, + ) +}) diff --git a/src/screens/Search/utils.ts b/src/screens/Search/utils.ts new file mode 100644 index 0000000000..dcf92c0926 --- /dev/null +++ b/src/screens/Search/utils.ts @@ -0,0 +1,43 @@ +export type Params = Record + +export function parseSearchQuery(rawQuery: string) { + let base = rawQuery + const rawLiterals = rawQuery.match(/[^:\w\d]".+?"/gi) || [] + + // remove literals from base + for (const literal of rawLiterals) { + base = base.replace(literal.trim(), '') + } + + // find remaining params in base + const rawParams = base.match(/[a-z]+:[a-z-\.@\d:"]+/gi) || [] + + for (const param of rawParams) { + base = base.replace(param, '') + } + + base = base.trim() + + const params = rawParams.reduce((params, param) => { + const [name, ...value] = param.split(/:/) + params[name] = value.join(':').replace(/"/g, '') // dates can contain additional colons + return params + }, {} as Params) + const literals = rawLiterals.map(l => String(l).trim()) + + return { + query: [base, literals.join(' ')].filter(Boolean).join(' '), + params, + } +} + +export function makeSearchQuery(query: string, params: Params) { + return [ + query, + Object.entries(params) + .map(([name, value]) => `${name}:${value}`) + .join(' '), + ] + .filter(Boolean) + .join(' ') +} diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx index 07d762c0fe..cfd77f7ef2 100644 --- a/src/view/screens/Search/Search.tsx +++ b/src/view/screens/Search/Search.tsx @@ -11,6 +11,7 @@ import { View, } from 'react-native' import {ScrollView as RNGHScrollView} from 'react-native-gesture-handler' +import RNPickerSelect from 'react-native-picker-select' import {AppBskyActorDefs, AppBskyFeedDefs, moderateProfile} from '@atproto/api' import { FontAwesomeIcon, @@ -21,6 +22,7 @@ import {useLingui} from '@lingui/react' import AsyncStorage from '@react-native-async-storage/async-storage' import {useFocusEffect, useNavigation} from '@react-navigation/native' +import {LANGUAGES} from '#/lib/../locale/languages' import {useAnalytics} from '#/lib/analytics/analytics' import {createHitslop} from '#/lib/constants' import {HITSLOP_10} from '#/lib/constants' @@ -35,10 +37,10 @@ import { SearchTabNavigatorParams, } from '#/lib/routes/types' import {augmentSearchQuery} from '#/lib/strings/helpers' -import {useTheme} from '#/lib/ThemeContext' import {logger} from '#/logger' import {isNative, isWeb} from '#/platform/detection' import {listenSoftReset} from '#/state/events' +import {useLanguagePrefs} from '#/state/preferences/languages' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete' import {useActorSearch} from '#/state/queries/actor-search' @@ -57,9 +59,16 @@ import {Text} from '#/view/com/util/text/Text' import {CenteredView, ScrollView} from '#/view/com/util/Views' import {Explore} from '#/view/screens/Search/Explore' import {SearchLinkCard, SearchProfileCard} from '#/view/shell/desktop/Search' -import {atoms as a, useTheme as useThemeNew} from '#/alf' +import {makeSearchQuery, parseSearchQuery} from '#/screens/Search/utils' +import {atoms as a, useBreakpoints, useTheme as useThemeNew, web} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as FeedCard from '#/components/FeedCard' +import * as TextField from '#/components/forms/TextField' +import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron' +import {MagnifyingGlass2_Stroke2_Corner0_Rounded as MagnifyingGlass} from '#/components/icons/MagnifyingGlass2' import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu' +import {SettingsGear2_Stroke2_Corner0_Rounded as Gear} from '#/components/icons/SettingsGear2' +import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' function Loader() { const pal = usePalette('default') @@ -251,7 +260,7 @@ let SearchScreenUserResults = ({ const {_} = useLingui() const {data: results, isFetched} = useActorSearch({ - query: query, + query, enabled: active, }) @@ -324,7 +333,137 @@ let SearchScreenFeedsResults = ({ } SearchScreenFeedsResults = React.memo(SearchScreenFeedsResults) -let SearchScreenInner = ({query}: {query?: string}): React.ReactNode => { +function SearchLanguageDropdown({ + value, + onChange, +}: { + value: string + onChange(value: string): void +}) { + const t = useThemeNew() + const {contentLanguages} = useLanguagePrefs() + + const items = React.useMemo(() => { + return LANGUAGES.filter(l => Boolean(l.code2)) + .map(l => ({ + label: l.name, + inputLabel: l.name, + value: l.code2, + key: l.code2 + l.code3, + })) + .sort(a => (contentLanguages.includes(a.value) ? -1 : 1)) + }, [contentLanguages]) + + const style = { + backgroundColor: t.atoms.bg_contrast_25.backgroundColor, + color: t.atoms.text.color, + fontSize: a.text_xs.fontSize, + fontFamily: 'inherit', + fontWeight: a.font_bold.fontWeight, + paddingHorizontal: 14, + paddingRight: 32, + paddingVertical: 8, + borderRadius: a.rounded_full.borderRadius, + borderWidth: a.border.borderWidth, + borderColor: t.atoms.border_contrast_low.borderColor, + } + + return ( + ( + + )} + useNativeAndroidPickerStyle={false} + style={{ + iconContainer: { + pointerEvents: 'none', + right: a.px_sm.paddingRight, + top: 0, + bottom: 0, + display: 'flex', + justifyContent: 'center', + }, + inputAndroid: { + ...style, + paddingVertical: 2, + }, + inputIOS: { + ...style, + }, + inputWeb: web({ + ...style, + cursor: 'pointer', + // @ts-ignore web only + '-moz-appearance': 'none', + '-webkit-appearance': 'none', + appearance: 'none', + outline: 0, + borderWidth: 0, + overflow: 'hidden', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + }), + }} + /> + ) +} + +function useQueryManager({initialQuery}: {initialQuery: string}) { + const {contentLanguages} = useLanguagePrefs() + const {query, params: initialParams} = React.useMemo(() => { + return parseSearchQuery(initialQuery || '') + }, [initialQuery]) + const prevInitialQuery = React.useRef(initialQuery) + const [lang, setLang] = React.useState( + initialParams.lang || contentLanguages[0], + ) + + if (initialQuery !== prevInitialQuery.current) { + // handle new queryParam change (from manual search entry) + prevInitialQuery.current = initialQuery + setLang(initialParams.lang || contentLanguages[0]) + } + + const params = React.useMemo( + () => ({ + // default stuff + ...initialParams, + // managed stuff + lang, + }), + [lang, initialParams], + ) + const handlers = React.useMemo( + () => ({ + setLang, + }), + [setLang], + ) + + return React.useMemo(() => { + return { + query, + queryWithParams: makeSearchQuery(query, params), + params: { + ...params, + ...handlers, + }, + } + }, [query, params, handlers]) +} + +let SearchScreenInner = ({ + query, + queryWithParams, + headerHeight, +}: { + query: string + queryWithParams: string + headerHeight: number +}): React.ReactNode => { const pal = usePalette('default') const setMinimalShellMode = useSetMinimalShellMode() const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled() @@ -349,7 +488,7 @@ let SearchScreenInner = ({query}: {query?: string}): React.ReactNode => { title: _(msg`Top`), component: ( @@ -359,7 +498,7 @@ let SearchScreenInner = ({query}: {query?: string}): React.ReactNode => { title: _(msg`Latest`), component: ( @@ -378,7 +517,7 @@ let SearchScreenInner = ({query}: {query?: string}): React.ReactNode => { ), }, ] - }, [_, query, activeTab]) + }, [_, query, queryWithParams, activeTab]) return query ? ( { renderTabBar={props => ( + style={[ + pal.border, + pal.view, + web({ + position: isWeb ? 'sticky' : '', + zIndex: 1, + }), + {top: isWeb ? headerHeight : undefined}, + ]}> section.title)} {...props} /> )} @@ -448,14 +595,14 @@ SearchScreenInner = React.memo(SearchScreenInner) export function SearchScreen( props: NativeStackScreenProps, ) { + const t = useThemeNew() + const {gtMobile} = useBreakpoints() const navigation = useNavigation() const textInput = React.useRef(null) const {_} = useLingui() - const pal = usePalette('default') const {track} = useAnalytics() const setDrawerOpen = useSetDrawerOpen() const setMinimalShellMode = useSetMinimalShellMode() - const {isTabletOrDesktop, isTabletOrMobile} = useWebMediaQueries() // Query terms const queryParam = props.route?.params?.q ?? '' @@ -469,6 +616,17 @@ export function SearchScreen( AppBskyActorDefs.ProfileViewBasic[] >([]) + const {params, query, queryWithParams} = useQueryManager({ + initialQuery: queryParam, + }) + const showFiltersButton = Boolean(query && !showAutocomplete) + const [showFilters, setShowFilters] = React.useState(false) + /* + * Arbitrary sizing, so guess and check, used for sticky header alignment and + * sizing. + */ + const headerHeight = 56 + (showFilters ? 40 : 0) + useFocusEffect( useNonReactiveCallback(() => { if (isWeb) { @@ -507,13 +665,6 @@ export function SearchScreen( textInput.current?.focus() }, []) - const onPressCancelSearch = React.useCallback(() => { - scrollToTopWeb() - textInput.current?.blur() - setShowAutocomplete(false) - setSearchText(queryParam) - }, [queryParam]) - const onChangeText = React.useCallback(async (text: string) => { scrollToTopWeb() setSearchText(text) @@ -586,6 +737,13 @@ export function SearchScreen( [updateSearchHistory, navigation], ) + const onPressCancelSearch = React.useCallback(() => { + scrollToTopWeb() + textInput.current?.blur() + setShowAutocomplete(false) + setSearchText(queryParam) + }, [setShowAutocomplete, setSearchText, queryParam]) + const onSubmit = React.useCallback(() => { navigateToItem(searchText) }, [navigateToItem, searchText]) @@ -624,6 +782,7 @@ export function SearchScreen( setSearchText('') navigation.setParams({q: ''}) } + setShowFilters(false) }, [navigation]) useFocusEffect( @@ -663,50 +822,107 @@ export function SearchScreen( [selectedProfiles], ) + const onSearchInputFocus = React.useCallback(() => { + if (isWeb) { + // Prevent a jump on iPad by ensuring that + // the initial focused render has no result list. + requestAnimationFrame(() => { + setShowAutocomplete(true) + }) + } else { + setShowAutocomplete(true) + } + setShowFilters(false) + }, [setShowAutocomplete]) + return ( - {isTabletOrMobile && ( - - - - )} - - {showAutocomplete && ( - - + + {!gtMobile && ( + + )} + + {showFiltersButton && ( + + )} + {showAutocomplete && ( + + )} + + + {showFilters && ( + + + + )} + - + ) @@ -747,7 +967,7 @@ let SearchInputBox = ({ textInput, searchText, showAutocomplete, - setShowAutocomplete, + onFocus, onChangeText, onSubmit, onPressClearQuery, @@ -755,83 +975,62 @@ let SearchInputBox = ({ textInput: React.RefObject searchText: string showAutocomplete: boolean - setShowAutocomplete: (show: boolean) => void + onFocus: () => void onChangeText: (text: string) => void onSubmit: () => void onPressClearQuery: () => void }): React.ReactNode => { - const pal = usePalette('default') const {_} = useLingui() - const theme = useTheme() + const t = useThemeNew() + return ( - { - textInput.current?.focus() - }}> - - { - if (isWeb) { - // Prevent a jump on iPad by ensuring that - // the initial focused render has no result list. - requestAnimationFrame(() => { - setShowAutocomplete(true) - }) - } else { - setShowAutocomplete(true) - } - }} - onChangeText={onChangeText} - onSubmitEditing={onSubmit} - autoFocus={false} - accessibilityRole="search" - accessibilityLabel={_(msg`Search`)} - accessibilityHint="" - autoCorrect={false} - autoComplete="off" - autoCapitalize="none" - /> + + + + + + {showAutocomplete && searchText.length > 0 && ( - - - + + + )} - + ) } SearchInputBox = React.memo(SearchInputBox) @@ -1029,21 +1228,7 @@ function scrollToTopWeb() { } } -const HEADER_HEIGHT = 46 - const styles = StyleSheet.create({ - header: { - flexDirection: 'row', - alignItems: 'center', - paddingHorizontal: 12, - paddingLeft: 13, - paddingVertical: 4, - height: HEADER_HEIGHT, - // @ts-ignore web only - position: isWeb ? 'sticky' : '', - top: 0, - zIndex: 1, - }, headerMenuBtn: { width: 30, height: 30, @@ -1075,12 +1260,6 @@ const styles = StyleSheet.create({ zIndex: -1, elevation: -1, // For Android }, - tabBarContainer: { - // @ts-ignore web only - position: isWeb ? 'sticky' : '', - top: isWeb ? HEADER_HEIGHT : 0, - zIndex: 1, - }, searchHistoryContainer: { width: '100%', paddingHorizontal: 12, diff --git a/src/view/screens/Storybook/Forms.tsx b/src/view/screens/Storybook/Forms.tsx index fc414d31f3..8ec118ae3e 100644 --- a/src/view/screens/Storybook/Forms.tsx +++ b/src/view/screens/Storybook/Forms.tsx @@ -32,7 +32,7 @@ export function Forms() { label="Text field" /> - + Date: Wed, 25 Sep 2024 15:05:33 -0700 Subject: [PATCH 16/44] Filter errors that get sent to Sentry (#5247) --- __tests__/lib/errors.test.ts | 14 +++++++------- src/lib/strings/errors.ts | 18 +++++++++++++----- src/logger/index.ts | 10 ++++++++-- src/view/com/util/UserAvatar.tsx | 3 ++- 4 files changed, 30 insertions(+), 15 deletions(-) diff --git a/__tests__/lib/errors.test.ts b/__tests__/lib/errors.test.ts index 39e8d189e0..e721396845 100644 --- a/__tests__/lib/errors.test.ts +++ b/__tests__/lib/errors.test.ts @@ -9,11 +9,11 @@ describe('isNetworkError', () => { ] const outputs = [true, false, false, true] - it('correctly distinguishes network errors', () => { - for (let i = 0; i < inputs.length; i++) { - const input = inputs[i] - const result = isNetworkError(input) - expect(result).toEqual(outputs[i]) - } - }) + for (let i = 0; i < inputs.length; i++) { + const input = inputs[i] + const output = outputs[i] + it(`correctly distinguishes network errors for ${input}`, () => { + expect(isNetworkError(input)).toEqual(output) + }) + } }) diff --git a/src/lib/strings/errors.ts b/src/lib/strings/errors.ts index 899d8ebce4..7d00c5e7f5 100644 --- a/src/lib/strings/errors.ts +++ b/src/lib/strings/errors.ts @@ -20,11 +20,19 @@ export function cleanError(str: any): string { return str } +const NETWORK_ERRORS = [ + 'Abort', + 'Network request failed', + 'Failed to fetch', + 'Load failed', +] + export function isNetworkError(e: unknown) { const str = String(e) - return ( - str.includes('Abort') || - str.includes('Network request failed') || - str.includes('Failed to fetch') - ) + for (const err of NETWORK_ERRORS) { + if (str.includes(err)) { + return true + } + } + return false } diff --git a/src/logger/index.ts b/src/logger/index.ts index d6d8d9fc1d..98635c6a97 100644 --- a/src/logger/index.ts +++ b/src/logger/index.ts @@ -1,10 +1,11 @@ import format from 'date-fns/format' import {nanoid} from 'nanoid/non-secure' -import {Sentry} from '#/logger/sentry' -import * as env from '#/env' import {DebugContext} from '#/logger/debugContext' import {add} from '#/logger/logDump' +import {Sentry} from '#/logger/sentry' +import {isNetworkError} from 'lib/strings/errors' +import * as env from '#/env' export enum LogLevel { Debug = 'debug', @@ -160,6 +161,11 @@ export const sentryTransport: Transport = ( timestamp: timestamp / 1000, // Sentry expects seconds }) + // We don't want to send any network errors to sentry + if (isNetworkError(message)) { + return + } + /** * Send all higher levels with `captureMessage`, with appropriate severity * level diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index 76d9d1503e..2b4376b698 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -327,7 +327,8 @@ let EditableUserAvatar = ({ onSelectNewAvatar(croppedImage) } catch (e: any) { - if (!String(e).includes('Canceled')) { + // Don't log errors for cancelling selection to sentry on ios or android + if (!String(e).toLowerCase().includes('cancel')) { logger.error('Failed to crop banner', {error: e}) } } From 2e5f95c8dd92c0be665414be41fb690e2434a941 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 25 Sep 2024 17:41:03 -0500 Subject: [PATCH 17/44] Add back empty placeholder (#5489) --- src/view/screens/Search/Search.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx index cfd77f7ef2..36639e7ed7 100644 --- a/src/view/screens/Search/Search.tsx +++ b/src/view/screens/Search/Search.tsx @@ -370,6 +370,7 @@ function SearchLanguageDropdown({ return ( Date: Thu, 26 Sep 2024 01:32:54 +0100 Subject: [PATCH 18/44] Messages list - make avatars link to profile (#5484) --- src/screens/Messages/List/ChatListItem.tsx | 25 ++++++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/screens/Messages/List/ChatListItem.tsx b/src/screens/Messages/List/ChatListItem.tsx index e9668b4e11..11c071082b 100644 --- a/src/screens/Messages/List/ChatListItem.tsx +++ b/src/screens/Messages/List/ChatListItem.tsx @@ -24,8 +24,9 @@ import {useProfileShadow} from '#/state/cache/profile-shadow' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useSession} from '#/state/session' import {TimeElapsed} from '#/view/com/util/TimeElapsed' -import {UserAvatar} from '#/view/com/util/UserAvatar' +import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' +import * as tokens from '#/alf/tokens' import {ConvoMenu} from '#/components/dms/ConvoMenu' import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/Bell2' import {Link} from '#/components/Link' @@ -203,6 +204,19 @@ function ChatListItemReady({ onFocus={onFocus} onBlur={onMouseLeave} style={[a.relative]}> + + + + - + {/* Avatar goes here */} + @@ -357,7 +368,7 @@ function ChatListItemReady({ a.self_end, a.justify_center, { - right: a.px_lg.paddingRight, + right: tokens.space.lg, opacity: !gtMobile || showActions || menuControl.isOpen ? 1 : 0, }, ]} From a9765fd23f59d31d60f5aaedd95e751f65adc969 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 26 Sep 2024 14:09:02 +0100 Subject: [PATCH 19/44] Fix banner height in edit profile modal (#5494) * fix banner height * fix user banner, it's not edit profile's fault --- src/view/com/modals/EditProfile.tsx | 8 ++++---- src/view/com/util/UserBanner.tsx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/view/com/modals/EditProfile.tsx b/src/view/com/modals/EditProfile.tsx index beea3ca1a8..b4cb8e013c 100644 --- a/src/view/com/modals/EditProfile.tsx +++ b/src/view/com/modals/EditProfile.tsx @@ -27,11 +27,11 @@ import {logger} from '#/logger' import {isWeb} from '#/platform/detection' import {useModalControls} from '#/state/modals' import {useProfileUpdateMutation} from '#/state/queries/profile' +import {Text} from '#/view/com/util/text/Text' +import * as Toast from '#/view/com/util/Toast' +import {EditableUserAvatar} from '#/view/com/util/UserAvatar' +import {UserBanner} from '#/view/com/util/UserBanner' import {ErrorMessage} from '../util/error/ErrorMessage' -import {Text} from '../util/text/Text' -import * as Toast from '../util/Toast' -import {EditableUserAvatar} from '../util/UserAvatar' -import {UserBanner} from '../util/UserBanner' const AnimatedTouchableOpacity = Animated.createAnimatedComponent(TouchableOpacity) diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx index 0e07a57454..13f4081fce 100644 --- a/src/view/com/util/UserBanner.tsx +++ b/src/view/com/util/UserBanner.tsx @@ -202,7 +202,7 @@ const styles = StyleSheet.create({ }, bannerImage: { width: '100%', - height: '100%', + height: 150, }, defaultBanner: { backgroundColor: '#0070ff', From f2a69c4528d318bfff9fff084ef75719fd5f41fb Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 26 Sep 2024 14:18:48 +0100 Subject: [PATCH 20/44] add emoji prop to composer reply to text (#5495) --- src/view/com/composer/ComposerReplyTo.tsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/view/com/composer/ComposerReplyTo.tsx b/src/view/com/composer/ComposerReplyTo.tsx index 74ca615070..7f4bb85f23 100644 --- a/src/view/com/composer/ComposerReplyTo.tsx +++ b/src/view/com/composer/ComposerReplyTo.tsx @@ -10,12 +10,12 @@ import { import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {sanitizeDisplayName} from 'lib/strings/display-names' -import {sanitizeHandle} from 'lib/strings/handles' -import {ComposerOptsPostRef} from 'state/shell/composer' -import {QuoteEmbed} from 'view/com/util/post-embeds/QuoteEmbed' -import {Text} from 'view/com/util/text/Text' -import {PreviewableUserAvatar} from 'view/com/util/UserAvatar' +import {sanitizeDisplayName} from '#/lib/strings/display-names' +import {sanitizeHandle} from '#/lib/strings/handles' +import {ComposerOptsPostRef} from '#/state/shell/composer' +import {QuoteEmbed} from '#/view/com/util/post-embeds/QuoteEmbed' +import {Text} from '#/view/com/util/text/Text' +import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, useTheme} from '#/alf' export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) { @@ -91,7 +91,7 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) { type={replyTo.author.associated?.labeler ? 'labeler' : 'user'} /> - + {sanitizeDisplayName( replyTo.author.displayName || sanitizeHandle(replyTo.author.handle), )} @@ -101,7 +101,8 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) { + numberOfLines={!showFull ? 6 : undefined} + emoji> {replyTo.text} From 625d7460d8af868ef0ea755cb3ac931dac4aad77 Mon Sep 17 00:00:00 2001 From: Igor Adrov Date: Thu, 26 Sep 2024 16:48:12 +0200 Subject: [PATCH 21/44] Make the counter more rounded (#5083) --- src/view/shell/desktop/LeftNav.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx index 6cceaccd92..c36c4ae758 100644 --- a/src/view/shell/desktop/LeftNav.tsx +++ b/src/view/shell/desktop/LeftNav.tsx @@ -470,7 +470,7 @@ const styles = StyleSheet.create({ fontSize: 12, fontWeight: '600', paddingHorizontal: 4, - borderRadius: 6, + borderRadius: 8, }, navItemCountTablet: { left: 18, From 179a913f20b9b77edbf9d167ebf32ad455f78eac Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 26 Sep 2024 09:59:32 -0500 Subject: [PATCH 22/44] Emoji in account list (#5497) --- src/components/AccountList.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/AccountList.tsx b/src/components/AccountList.tsx index 883c06c144..68bb482af2 100644 --- a/src/components/AccountList.tsx +++ b/src/components/AccountList.tsx @@ -126,10 +126,12 @@ function AccountItem({ - + {profile?.displayName || account.handle}{' '} - {account.handle} + + {account.handle} + {isCurrentAccount ? ( Date: Thu, 26 Sep 2024 09:59:47 -0500 Subject: [PATCH 23/44] Adjust line height to not cut off emoji (#5496) --- src/view/com/util/PostMeta.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx index 3f647f9784..adf9c5eb1b 100644 --- a/src/view/com/util/PostMeta.tsx +++ b/src/view/com/util/PostMeta.tsx @@ -77,7 +77,7 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => { disableMismatchWarning onPress={onBeforePressAuthor} style={[t.atoms.text]}> - + {forceLTR( sanitizeDisplayName( displayName, @@ -92,14 +92,10 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => { disableMismatchWarning disableUnderline onPress={onBeforePressAuthor} - style={[a.text_md, t.atoms.text_contrast_medium, a.leading_tight]}> + style={[a.text_md, t.atoms.text_contrast_medium, a.leading_snug]}> + style={[a.text_md, t.atoms.text_contrast_medium, a.leading_snug]}> {NON_BREAKING_SPACE + sanitizeHandle(handle, '@')} @@ -124,7 +120,7 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => { style={[ a.text_md, t.atoms.text_contrast_medium, - a.leading_tight, + a.leading_snug, web({ whiteSpace: 'nowrap', }), From 23dd638f6a730883df871e4968830067361d902b Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 26 Sep 2024 10:57:45 -0500 Subject: [PATCH 24/44] Clean up left nav on web (#5488) * Tweak nav item count style * Fix nav item icon width and alignment * Just refactor the thing * New compose button * Z index * Rounded * Tweak gradient * Tweak gradient * Tweak gradient * Solid * Adjust position of counter * Always a circle --- src/alf/tokens.ts | 9 ++ src/components/Button.tsx | 9 +- src/logger/index.ts | 2 +- src/view/shell/desktop/LeftNav.tsx | 141 +++++++++++------------------ 4 files changed, 68 insertions(+), 93 deletions(-) diff --git a/src/alf/tokens.ts b/src/alf/tokens.ts index 3f30702e85..f3ac17e6af 100644 --- a/src/alf/tokens.ts +++ b/src/alf/tokens.ts @@ -60,6 +60,15 @@ export const fontWeight = { } as const export const gradients = { + primary: { + values: [ + [0, '#054CFF'], + [0.4, '#1085FE'], + [0.6, '#1085FE'], + [1, '#59B9FF'], + ], + hover_value: '#1085FE', + }, sky: { values: [ [0, '#0A7AFF'], diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 8728b88c2c..17179994a9 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -24,6 +24,7 @@ export type ButtonColor = | 'secondary' | 'secondary_inverted' | 'negative' + | 'gradient_primary' | 'gradient_sky' | 'gradient_midnight' | 'gradient_sunrise' @@ -412,6 +413,7 @@ export const Button = React.forwardRef( secondary: tokens.gradients.sky, secondary_inverted: tokens.gradients.sky, negative: tokens.gradients.sky, + gradient_primary: tokens.gradients.primary, gradient_sky: tokens.gradients.sky, gradient_midnight: tokens.gradients.midnight, gradient_sunrise: tokens.gradients.sunrise, @@ -444,7 +446,7 @@ export const Button = React.forwardRef( [state, variant, color, size, disabled], ) - const flattenedBaseStyles = flatten(baseStyles) + const flattenedBaseStyles = flatten([baseStyles, style]) return ( ( a.align_center, a.justify_center, flattenedBaseStyles, - flatten(style), ...(state.hovered || state.pressed ? [hoverStyles, flatten(hoverStyleProp)] : []), @@ -626,9 +627,9 @@ export function useSharedButtonTextStyles() { } if (size === 'large') { - baseStyles.push(a.text_md, a.leading_tight, web({paddingTop: 1})) + baseStyles.push(a.text_md, a.leading_tight, web({top: -0.4})) } else if (size === 'small') { - baseStyles.push(a.text_sm, a.leading_tight, web({paddingTop: 1})) + baseStyles.push(a.text_sm, a.leading_tight, web({top: -0.4})) } else if (size === 'tiny') { baseStyles.push(a.text_xs, a.leading_tight) } diff --git a/src/logger/index.ts b/src/logger/index.ts index 98635c6a97..7bd812af00 100644 --- a/src/logger/index.ts +++ b/src/logger/index.ts @@ -1,10 +1,10 @@ import format from 'date-fns/format' import {nanoid} from 'nanoid/non-secure' +import {isNetworkError} from '#/lib/strings/errors' import {DebugContext} from '#/logger/debugContext' import {add} from '#/logger/logDump' import {Sentry} from '#/logger/sentry' -import {isNetworkError} from 'lib/strings/errors' import * as env from '#/env' export enum LogLevel { diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx index c36c4ae758..ecd00a9192 100644 --- a/src/view/shell/desktop/LeftNav.tsx +++ b/src/view/shell/desktop/LeftNav.tsx @@ -18,7 +18,6 @@ import {getCurrentRoute, isStateAtTabRoot, isTab} from '#/lib/routes/helpers' import {makeProfileLink} from '#/lib/routes/links' import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types' import {isInvalidHandle} from '#/lib/strings/handles' -import {colors, s} from '#/lib/styles' import {emitSoftReset} from '#/state/events' import {useFetchHandle} from '#/state/queries/handle' import {useUnreadMessageCount} from '#/state/queries/messages/list-converations' @@ -29,9 +28,10 @@ import {useComposerControls} from '#/state/shell/composer' import {Link} from '#/view/com/util/Link' import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {PressableWithHover} from '#/view/com/util/PressableWithHover' -import {Text} from '#/view/com/util/text/Text' import {UserAvatar} from '#/view/com/util/UserAvatar' import {NavSignupCard} from '#/view/shell/NavSignupCard' +import {atoms as a, useBreakpoints, useTheme} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' import { Bell_Filled_Corner0_Rounded as BellFilled, Bell_Stroke2_Corner0_Rounded as Bell, @@ -63,6 +63,7 @@ import { UserCircle_Filled_Corner0_Rounded as UserCircleFilled, UserCircle_Stroke2_Corner0_Rounded as UserCircle, } from '#/components/icons/UserCircle' +import {Text} from '#/components/Typography' import {router} from '../../../routes' const NAV_ICON_WIDTH = 28 @@ -149,9 +150,10 @@ interface NavItemProps { label: string } function NavItem({count, href, icon, iconFilled, label}: NavItemProps) { - const pal = usePalette('default') + const t = useTheme() const {currentAccount} = useSession() - const {isDesktop, isTablet} = useWebMediaQueries() + const {gtMobile, gtTablet} = useBreakpoints() + const isTablet = gtMobile && !gtTablet const [pathName] = React.useMemo(() => router.matchPath(href), [href]) const currentRouteInfo = useNavigationState(state => { if (!state) { @@ -183,8 +185,8 @@ function NavItem({count, href, icon, iconFilled, label}: NavItemProps) { return ( {isCurrent ? iconFilled : icon} {typeof count === 'string' && count ? ( {count} ) : null} - {isDesktop && ( - + {gtTablet && ( + {label} )} @@ -268,21 +297,20 @@ function ComposeBtn() { return null } return ( - - + ) } @@ -440,67 +468,4 @@ const styles = StyleSheet.create({ width: 30, height: 30, }, - - navItemWrapper: { - flexDirection: 'row', - alignItems: 'center', - paddingHorizontal: 12, - padding: 12, - borderRadius: 8, - gap: 10, - }, - navItemIconWrapper: { - alignItems: 'center', - justifyContent: 'center', - width: 28, - height: 24, - marginTop: 2, - zIndex: 1, - }, - navItemIconWrapperTablet: { - width: 40, - height: 40, - }, - navItemCount: { - position: 'absolute', - top: 0, - left: 15, - backgroundColor: colors.blue3, - color: colors.white, - fontSize: 12, - fontWeight: '600', - paddingHorizontal: 4, - borderRadius: 8, - }, - navItemCountTablet: { - left: 18, - fontSize: 14, - }, - - newPostBtnContainer: { - flexDirection: 'row', - }, - newPostBtn: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - borderRadius: 24, - paddingTop: 10, - paddingBottom: 12, // visually aligns the text vertically inside the button - paddingLeft: 16, - paddingRight: 18, // looks nicer like this - backgroundColor: colors.blue3, - marginLeft: 12, - marginTop: 20, - marginBottom: 10, - gap: 8, - }, - newPostBtnIconWrapper: { - marginTop: 2, // aligns the icon visually with the text - }, - newPostBtnLabel: { - color: colors.white, - fontSize: 16, - fontWeight: '600', - }, }) From 27e09e7adccc4d002359425492eb8773da73ebcb Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 26 Sep 2024 11:30:00 -0500 Subject: [PATCH 25/44] Fix font loading (#5500) --- src/alf/fonts.ts | 9 ++++++--- src/style.css | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/alf/fonts.ts b/src/alf/fonts.ts index b11ce939f8..46c741bc79 100644 --- a/src/alf/fonts.ts +++ b/src/alf/fonts.ts @@ -45,15 +45,18 @@ export function applyFonts( // '100': 'Inter-Thin', // '200': 'Inter-ExtraLight', // '300': 'Inter-Light', + // '500': 'Inter-Medium', + // '700': 'Inter-Bold', + // '900': 'Inter-Black', '100': 'Inter-Regular', '200': 'Inter-Regular', '300': 'Inter-Regular', '400': 'Inter-Regular', - '500': 'Inter-Medium', + '500': 'Inter-SemiBold', '600': 'Inter-SemiBold', - '700': 'Inter-Bold', + '700': 'Inter-SemiBold', '800': 'Inter-ExtraBold', - '900': 'Inter-Black', + '900': 'Inter-ExtraBold', }[style.fontWeight as string] || 'Inter-Regular' if (style.fontStyle === 'italic') { diff --git a/src/style.css b/src/style.css index 980d92ef77..b0b01ba3eb 100644 --- a/src/style.css +++ b/src/style.css @@ -9,7 +9,7 @@ @font-face { font-family: 'Inter-Regular'; src: local('Inter-Regular'), - url(/assets/fonts/inter/Inter-Regular.otf) format('font/otf'); + url(/assets/fonts/inter/Inter-Regular.otf) format('opentype'); font-weight: 400; font-style: normal; font-display: swap; @@ -17,7 +17,7 @@ @font-face { font-family: 'Inter-Italic'; src: local('Inter-Italic'), - url(/assets/fonts/inter/Inter-Italic.otf) format('font/otf'); + url(/assets/fonts/inter/Inter-Italic.otf) format('opentype'); font-weight: 400; font-style: italic; font-display: swap; @@ -25,14 +25,14 @@ /* @font-face { font-family: "Inter-Medium"; - src: local("Inter-Medium"), url(/assets/fonts/inter/Inter-Medium.otf) format("font/otf"); + src: local("Inter-Medium"), url(/assets/fonts/inter/Inter-Medium.otf) format("opentype"); font-weight: 500; font-style: normal; font-display: swap; } @font-face { font-family: "Inter-MediumItalic"; - src: local("Inter-MediumItalic"), url(/assets/fonts/inter/Inter-MediumItalic.otf) format("font/otf"); + src: local("Inter-MediumItalic"), url(/assets/fonts/inter/Inter-MediumItalic.otf) format("opentype"); font-weight: 500; font-style: italic; font-display: swap; @@ -41,7 +41,7 @@ @font-face { font-family: 'Inter-SemiBold'; src: local('Inter-SemiBold'), - url(/assets/fonts/inter/Inter-SemiBold.otf) format('font/otf'); + url(/assets/fonts/inter/Inter-SemiBold.otf) format('opentype'); font-weight: 600; font-style: normal; font-display: swap; @@ -49,7 +49,7 @@ @font-face { font-family: 'Inter-SemiBoldItalic'; src: local('Inter-SemiBoldItalic'), - url(/assets/fonts/inter/Inter-SemiBoldItalic.otf) format('font/otf'); + url(/assets/fonts/inter/Inter-SemiBoldItalic.otf) format('opentype'); font-weight: 600; font-style: italic; font-display: swap; @@ -57,14 +57,14 @@ /* @font-face { font-family: "Inter-Bold"; - src: local("Inter-Bold"), url(/assets/fonts/inter/Inter-Bold.otf) format("font/otf"); + src: local("Inter-Bold"), url(/assets/fonts/inter/Inter-Bold.otf) format("opentype"); font-weight: 700; font-style: normal; font-display: swap; } @font-face { font-family: "Inter-BoldItalic"; - src: local("Inter-BoldItalic"), url(/assets/fonts/inter/Inter-BoldItalic.otf) format("font/otf"); + src: local("Inter-BoldItalic"), url(/assets/fonts/inter/Inter-BoldItalic.otf) format("opentype"); font-weight: 700; font-style: italic; font-display: swap; @@ -73,7 +73,7 @@ @font-face { font-family: 'Inter-ExtraBold'; src: local('Inter-ExtraBold'), - url(/assets/fonts/inter/Inter-ExtraBold.otf) format('font/otf'); + url(/assets/fonts/inter/Inter-ExtraBold.otf) format('opentype'); font-weight: 800; font-style: normal; font-display: swap; @@ -81,7 +81,7 @@ @font-face { font-family: 'Inter-ExtraBoldItalic'; src: local('Inter-ExtraBoldItalic'), - url(/assets/fonts/inter/Inter-ExtraBoldItalic.otf) format('font/otf'); + url(/assets/fonts/inter/Inter-ExtraBoldItalic.otf) format('opentype'); font-weight: 800; font-style: italic; font-display: swap; @@ -89,14 +89,14 @@ /* @font-face { font-family: "Inter-Black"; - src: local("Inter-Black"), url(/assets/fonts/inter/Inter-Black.otf) format("font/otf"); + src: local("Inter-Black"), url(/assets/fonts/inter/Inter-Black.otf) format("opentype"); font-weight: 900; font-style: normal; font-display: swap; } @font-face { font-family: "Inter-BlackItalic"; - src: local("Inter-BlackItalic"), url(/assets/fonts/inter/Inter-BlackItalic.otf) format("font/otf"); + src: local("Inter-BlackItalic"), url(/assets/fonts/inter/Inter-BlackItalic.otf) format("opentype"); font-weight: 900; font-style: italic; font-display: swap; From 5a142161cb90f65c2593b58c7471b6657a2cd0a2 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 26 Sep 2024 11:47:13 -0500 Subject: [PATCH 26/44] Remove 10milly dialog, revert header logo changes (#5503) --- .../dialogs/nuxs/TenMillion/Trigger.tsx | 129 ---- .../nuxs/TenMillion/icons/OnePercent.tsx | 15 - .../nuxs/TenMillion/icons/PointOnePercent.tsx | 15 - .../nuxs/TenMillion/icons/TenPercent.tsx | 15 - .../TenMillion/icons/TwentyFivePercent.tsx | 15 - .../dialogs/nuxs/TenMillion/index.tsx | 712 ------------------ src/components/dialogs/nuxs/index.tsx | 9 +- src/state/queries/nuxs/definitions.ts | 15 +- src/view/com/home/HomeHeaderLayout.web.tsx | 78 +- src/view/com/home/HomeHeaderLayoutMobile.tsx | 75 +- 10 files changed, 28 insertions(+), 1050 deletions(-) delete mode 100644 src/components/dialogs/nuxs/TenMillion/Trigger.tsx delete mode 100644 src/components/dialogs/nuxs/TenMillion/icons/OnePercent.tsx delete mode 100644 src/components/dialogs/nuxs/TenMillion/icons/PointOnePercent.tsx delete mode 100644 src/components/dialogs/nuxs/TenMillion/icons/TenPercent.tsx delete mode 100644 src/components/dialogs/nuxs/TenMillion/icons/TwentyFivePercent.tsx delete mode 100644 src/components/dialogs/nuxs/TenMillion/index.tsx diff --git a/src/components/dialogs/nuxs/TenMillion/Trigger.tsx b/src/components/dialogs/nuxs/TenMillion/Trigger.tsx deleted file mode 100644 index 9616b3b1d3..0000000000 --- a/src/components/dialogs/nuxs/TenMillion/Trigger.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import React from 'react' -import {View} from 'react-native' -import Svg, {Circle, Path} from 'react-native-svg' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {Nux, useUpsertNuxMutation} from '#/state/queries/nuxs' -import {atoms as a, ViewStyleProp} from '#/alf' -import {Button, ButtonProps} from '#/components/Button' -import * as Dialog from '#/components/Dialog' -import {InlineLinkText} from '#/components/Link' -import * as Prompt from '#/components/Prompt' -import {TenMillion} from './' - -export function Trigger({children}: {children: ButtonProps['children']}) { - const {_} = useLingui() - const {mutate: upsertNux} = useUpsertNuxMutation() - const [show, setShow] = React.useState(false) - const [fallback, setFallback] = React.useState(false) - const control = Prompt.usePromptControl() - - const handleOnPress = () => { - if (!fallback) { - setShow(true) - upsertNux({ - id: Nux.TenMillionDialog, - completed: true, - data: undefined, - }) - } else { - control.open() - } - } - - const onHandleFallback = () => { - setFallback(true) - control.open() - } - - return ( - <> - - - {show && !fallback && ( - setShow(false)} - onFallback={onHandleFallback} - /> - )} - - - - - Bluesky is celebrating 10 million users! - - - - - Together, we're rebuilding the social internet. We're glad you're - here. - - - - - To learn more,{' '} - { - control.close() - }} - style={[a.text_md, a.leading_snug]}> - check out our post. - - - - - - - ) -} - -export function Icon({width, style}: {width: number} & ViewStyleProp) { - return ( - - - - - - - - - - - - - - - - - - ) -} diff --git a/src/components/dialogs/nuxs/TenMillion/icons/OnePercent.tsx b/src/components/dialogs/nuxs/TenMillion/icons/OnePercent.tsx deleted file mode 100644 index 9c8d47afd0..0000000000 --- a/src/components/dialogs/nuxs/TenMillion/icons/OnePercent.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react' -import Svg, {Path} from 'react-native-svg' - -export function OnePercent({fill}: {fill?: string}) { - return ( - - - - ) -} diff --git a/src/components/dialogs/nuxs/TenMillion/icons/PointOnePercent.tsx b/src/components/dialogs/nuxs/TenMillion/icons/PointOnePercent.tsx deleted file mode 100644 index 1f9467e442..0000000000 --- a/src/components/dialogs/nuxs/TenMillion/icons/PointOnePercent.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react' -import Svg, {Path} from 'react-native-svg' - -export function PointOnePercent({fill}: {fill?: string}) { - return ( - - - - ) -} diff --git a/src/components/dialogs/nuxs/TenMillion/icons/TenPercent.tsx b/src/components/dialogs/nuxs/TenMillion/icons/TenPercent.tsx deleted file mode 100644 index 4197be8357..0000000000 --- a/src/components/dialogs/nuxs/TenMillion/icons/TenPercent.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react' -import Svg, {Path} from 'react-native-svg' - -export function TenPercent({fill}: {fill?: string}) { - return ( - - - - ) -} diff --git a/src/components/dialogs/nuxs/TenMillion/icons/TwentyFivePercent.tsx b/src/components/dialogs/nuxs/TenMillion/icons/TwentyFivePercent.tsx deleted file mode 100644 index 0d37971410..0000000000 --- a/src/components/dialogs/nuxs/TenMillion/icons/TwentyFivePercent.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react' -import Svg, {Path} from 'react-native-svg' - -export function TwentyFivePercent({fill}: {fill?: string}) { - return ( - - - - ) -} diff --git a/src/components/dialogs/nuxs/TenMillion/index.tsx b/src/components/dialogs/nuxs/TenMillion/index.tsx deleted file mode 100644 index 21e775a108..0000000000 --- a/src/components/dialogs/nuxs/TenMillion/index.tsx +++ /dev/null @@ -1,712 +0,0 @@ -import React from 'react' -import {View} from 'react-native' -import Animated, {FadeIn} from 'react-native-reanimated' -import ViewShot from 'react-native-view-shot' -import {Image} from 'expo-image' -import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker' -import * as MediaLibrary from 'expo-media-library' -import {moderateProfile} from '@atproto/api' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {networkRetry} from '#/lib/async/retry' -import {getCanvas} from '#/lib/canvas' -import {shareUrl} from '#/lib/sharing' -import {logEvent} from '#/lib/statsig/statsig' -import {sanitizeDisplayName} from '#/lib/strings/display-names' -import {sanitizeHandle} from '#/lib/strings/handles' -import {isIOS, isNative} from '#/platform/detection' -import {useModerationOpts} from '#/state/preferences/moderation-opts' -import {useProfileQuery} from '#/state/queries/profile' -import {useAgent, useSession} from '#/state/session' -import {useComposerControls} from '#/state/shell' -import {formatCount} from '#/view/com/util/numeric/format' -import * as Toast from '#/view/com/util/Toast' -import {Logomark} from '#/view/icons/Logomark' -import { - atoms as a, - ThemeProvider, - tokens, - useBreakpoints, - useTheme, -} from '#/alf' -import {Button, ButtonIcon, ButtonText} from '#/components/Button' -import * as Dialog from '#/components/Dialog' -import {useNuxDialogContext} from '#/components/dialogs/nuxs' -import {OnePercent} from '#/components/dialogs/nuxs/TenMillion/icons/OnePercent' -import {PointOnePercent} from '#/components/dialogs/nuxs/TenMillion/icons/PointOnePercent' -import {TenPercent} from '#/components/dialogs/nuxs/TenMillion/icons/TenPercent' -import {Divider} from '#/components/Divider' -import {GradientFill} from '#/components/GradientFill' -import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox' -import {Download_Stroke2_Corner0_Rounded as Download} from '#/components/icons/Download' -import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image' -import {Loader} from '#/components/Loader' -import {Text} from '#/components/Typography' - -const DEBUG = false -const RATIO = 8 / 10 -const WIDTH = 2000 -const HEIGHT = WIDTH * RATIO - -function getFontSize(count: number) { - const length = count.toString().length - if (length < 7) { - return 80 - } else if (length < 5) { - return 100 - } else { - return 70 - } -} - -function getPercentBadge(percent: number) { - if (percent <= 0.001) { - return PointOnePercent - } else if (percent <= 0.01) { - return OnePercent - } else if (percent <= 0.1) { - return TenPercent - } - return null -} - -function Frame({children}: {children: React.ReactNode}) { - return ( - - {children} - - ) -} - -export function TenMillion({ - showTimeout, - onClose, - onFallback, -}: { - showTimeout?: number - onClose?: () => void - onFallback?: () => void -}) { - const agent = useAgent() - const nuxDialogs = useNuxDialogContext() - const [userNumber, setUserNumber] = React.useState(0) - const fetching = React.useRef(false) - - React.useEffect(() => { - async function fetchUserNumber() { - const isBlueskyHosted = agent.sessionManager.pdsUrl - ?.toString() - .includes('bsky.network') - - if (isBlueskyHosted && agent.session?.accessJwt) { - const res = await fetch( - `https://bsky.social/xrpc/com.atproto.temp.getSignupNumber`, - { - headers: { - Authorization: `Bearer ${agent.session.accessJwt}`, - }, - }, - ) - - if (!res.ok) { - throw new Error('Network request failed') - } - - const data = await res.json() - - if (data.number && data.number <= 10_000_000) { - setUserNumber(data.number) - } else { - // should be rare - nuxDialogs.dismissActiveNux() - onFallback?.() - } - } else { - nuxDialogs.dismissActiveNux() - onFallback?.() - } - } - - if (!fetching.current) { - fetching.current = true - networkRetry(3, fetchUserNumber).catch(() => { - nuxDialogs.dismissActiveNux() - onFallback?.() - }) - } - }, [ - agent.sessionManager.pdsUrl, - agent.session?.accessJwt, - setUserNumber, - nuxDialogs.dismissActiveNux, - nuxDialogs, - onFallback, - ]) - - return userNumber ? ( - - ) : null -} - -export function TenMillionInner({ - userNumber, - showTimeout, - onClose: onCloseOuter, -}: { - userNumber: number - showTimeout: number - onClose?: () => void -}) { - const t = useTheme() - const lightTheme = useTheme('light') - const {_, i18n} = useLingui() - const control = Dialog.useDialogControl() - const {gtMobile} = useBreakpoints() - const {openComposer} = useComposerControls() - const {currentAccount} = useSession() - const { - isLoading: isProfileLoading, - data: profile, - error: profileError, - } = useProfileQuery({ - did: currentAccount!.did, - }) - const moderationOpts = useModerationOpts() - const nuxDialogs = useNuxDialogContext() - const moderation = React.useMemo(() => { - return profile && moderationOpts - ? moderateProfile(profile, moderationOpts) - : undefined - }, [profile, moderationOpts]) - const [uri, setUri] = React.useState(null) - const percent = userNumber / 10_000_000 - const Badge = getPercentBadge(percent) - const isLoadingData = isProfileLoading || !moderation || !profile - const isLoadingImage = !uri - - const displayName = React.useMemo(() => { - if (!profile || !moderation) return '' - return sanitizeDisplayName( - profile.displayName || sanitizeHandle(profile.handle), - moderation.ui('displayName'), - ) - }, [profile, moderation]) - const handle = React.useMemo(() => { - if (!profile) return '' - return sanitizeHandle(profile.handle, '@') - }, [profile]) - const joinedDate = React.useMemo(() => { - if (!profile || !profile.createdAt) return '' - const date = i18n.date(profile.createdAt, { - month: 'short', - day: 'numeric', - year: 'numeric', - }) - return date - }, [i18n, profile]) - - const error: string = React.useMemo(() => { - if (profileError) { - return _( - msg`Oh no! We weren't able to generate an image for you to share. Rest assured, we're glad you're here 🦋`, - ) - } - return '' - }, [_, profileError]) - - /* - * Opening and closing - */ - React.useEffect(() => { - const timeout = setTimeout(() => { - control.open() - }, showTimeout) - return () => { - clearTimeout(timeout) - } - }, [control, showTimeout]) - const onClose = React.useCallback(() => { - nuxDialogs.dismissActiveNux() - onCloseOuter?.() - }, [nuxDialogs, onCloseOuter]) - - /* - * Actions - */ - const sharePost = React.useCallback(() => { - if (uri) { - control.close(() => { - setTimeout(() => { - logEvent('tmd:post', {}) - openComposer({ - text: _( - msg`Bluesky now has over 10 million users, and I was #${i18n.number( - userNumber, - )}!`, - ), - imageUris: [ - { - uri, - width: WIDTH, - height: HEIGHT, - altText: _( - msg`A virtual certificate with text "Celebrating 10M users on Bluesky, #${i18n.number( - userNumber, - )}, ${displayName} ${handle}, joined on ${joinedDate}"`, - ), - }, - ], - }) - }, 1e3) - }) - } - }, [ - _, - i18n, - control, - openComposer, - uri, - userNumber, - displayName, - handle, - joinedDate, - ]) - const onNativeShare = React.useCallback(() => { - if (uri) { - control.close(() => { - logEvent('tmd:share', {}) - shareUrl(uri) - }) - } - }, [uri, control]) - const onNativeDownload = React.useCallback(async () => { - if (uri) { - const res = await requestMediaLibraryPermissionsAsync() - - if (!res) { - Toast.show( - _( - msg`You must grant access to your photo library to save the image.`, - ), - 'xmark', - ) - return - } - - try { - await MediaLibrary.createAssetAsync(uri) - logEvent('tmd:download', {}) - Toast.show(_(msg`Image saved to your camera roll!`)) - } catch (e: unknown) { - console.log(e) - Toast.show(_(msg`An error occurred while saving the image!`), 'xmark') - return - } - } - }, [_, uri]) - const onWebDownload = React.useCallback(async () => { - if (uri) { - const canvas = await getCanvas(uri) - const imgHref = canvas - .toDataURL('image/png') - .replace('image/png', 'image/octet-stream') - const link = document.createElement('a') - link.setAttribute('download', `Bluesky 10M Users.png`) - link.setAttribute('href', imgHref) - link.click() - logEvent('tmd:download', {}) - } - }, [uri]) - - /* - * Canvas stuff - */ - const imageRef = React.useRef(null) - const captureInProgress = React.useRef(false) - const onCanvasReady = React.useCallback(async () => { - if ( - imageRef.current && - imageRef.current.capture && - !captureInProgress.current - ) { - captureInProgress.current = true - const uri = await imageRef.current.capture() - setUri(uri) - } - }, [setUri]) - const canvas = isLoadingData ? null : ( - - - - - - - - - - - - - - {/* Centered content */} - - - - Celebrating {formatCount(i18n, 10000000)} users - {' '} - 🎉 - - - - # - - - {i18n.number(userNumber)} - - - - {Badge && ( - - - - )} - - {/* End centered content */} - - - - {/* - - */} - - - {displayName} - - - - {handle} - - - {profile.createdAt && ( - - Joined on {joinedDate} - - )} - - - - - - - - - - - - ) - - return ( - - - - - - - {error ? ( - - - (╯°□°)╯︵ ┻━┻ - - - {error} - - - ) : isLoadingData || isLoadingImage ? ( - - ) : ( - - - - )} - - - - {canvas} - - - - Thanks for being one of our first 10 million users. - - - - - Together, we're rebuilding the social internet. We're glad - you're here. - - - - - - - {gtMobile && ( - - Brag a little! - - )} - - - - - - - - - - - ) -} diff --git a/src/components/dialogs/nuxs/index.tsx b/src/components/dialogs/nuxs/index.tsx index b93831ad35..c740e1e6ad 100644 --- a/src/components/dialogs/nuxs/index.tsx +++ b/src/components/dialogs/nuxs/index.tsx @@ -16,10 +16,11 @@ import { import {useProfileQuery} from '#/state/queries/profile' import {SessionAccount, useSession} from '#/state/session' import {useOnboardingState} from '#/state/shell' +/* + * NUXs + */ import {NeueTypography} from '#/components/dialogs/nuxs/NeueTypography' import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing' -// NUXs -import {TenMillion} from '#/components/dialogs/nuxs/TenMillion' import {IS_DEV} from '#/env' type Context = { @@ -36,9 +37,6 @@ const queuedNuxs: { preferences: UsePreferencesQueryResponse }) => boolean }[] = [ - { - id: Nux.TenMillionDialog, - }, { id: Nux.NeueTypography, enabled(props) { @@ -176,7 +174,6 @@ function Inner({ return ( - {activeNux === Nux.TenMillionDialog && } {activeNux === Nux.NeueTypography && } ) diff --git a/src/state/queries/nuxs/definitions.ts b/src/state/queries/nuxs/definitions.ts index 63a8079623..8166602c8a 100644 --- a/src/state/queries/nuxs/definitions.ts +++ b/src/state/queries/nuxs/definitions.ts @@ -3,23 +3,16 @@ import zod from 'zod' import {BaseNux} from '#/state/queries/nuxs/types' export enum Nux { - TenMillionDialog = 'TenMillionDialog', NeueTypography = 'NeueTypography', } export const nuxNames = new Set(Object.values(Nux)) -export type AppNux = - | BaseNux<{ - id: Nux.TenMillionDialog - data: undefined - }> - | BaseNux<{ - id: Nux.NeueTypography - data: undefined - }> +export type AppNux = BaseNux<{ + id: Nux.NeueTypography + data: undefined +}> export const NuxSchemas: Record | undefined> = { - [Nux.TenMillionDialog]: undefined, [Nux.NeueTypography]: undefined, } diff --git a/src/view/com/home/HomeHeaderLayout.web.tsx b/src/view/com/home/HomeHeaderLayout.web.tsx index 9bfa82cd22..7049306eba 100644 --- a/src/view/com/home/HomeHeaderLayout.web.tsx +++ b/src/view/com/home/HomeHeaderLayout.web.tsx @@ -1,25 +1,16 @@ import React from 'react' import {StyleSheet, View} from 'react-native' -import Animated, { - useAnimatedStyle, - useReducedMotion, - useSharedValue, - withDelay, - withRepeat, - withSequence, - withSpring, - withTiming, -} from 'react-native-reanimated' +import Animated from 'react-native-reanimated' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useMinimalShellHeaderTransform} from '#/lib/hooks/useMinimalShellTransform' +import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' +import {useKawaiiMode} from '#/state/preferences/kawaii' import {useSession} from '#/state/session' import {useShellLayout} from '#/state/shell/shell-layout' -import {useMinimalShellHeaderTransform} from 'lib/hooks/useMinimalShellTransform' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -// import {Logo} from '#/view/icons/Logo' +import {Logo} from '#/view/icons/Logo' import {atoms as a, useTheme} from '#/alf' -import {Icon, Trigger} from '#/components/dialogs/nuxs/TenMillion/Trigger' import {Hashtag_Stroke2_Corner0_Rounded as FeedsIcon} from '#/components/icons/Hashtag' import {Link} from '#/components/Link' import {HomeHeaderLayoutMobile} from './HomeHeaderLayoutMobile' @@ -48,43 +39,7 @@ function HomeHeaderLayoutDesktopAndTablet({ const {headerHeight} = useShellLayout() const {hasSession} = useSession() const {_} = useLingui() - - // TEMPORARY - REMOVE AFTER MILLY - // This will just cause the icon to shake a bit when the user first opens the app, drawing attention to the celebration - // 🎉 - const rotate = useSharedValue(0) - const reducedMotion = useReducedMotion() - - // Run this a single time on app mount. - React.useEffect(() => { - if (reducedMotion) return - - // Waits 1500ms, then rotates 10 degrees with a spring animation. Repeats once. - rotate.value = withDelay( - 1000, - withRepeat( - withSequence( - withTiming(10, {duration: 100}), - withSpring(0, { - mass: 1, - damping: 1, - stiffness: 200, - overshootClamping: false, - }), - ), - 2, - false, - ), - ) - }, [rotate, reducedMotion]) - - const animatedStyle = useAnimatedStyle(() => ({ - transform: [ - { - rotateZ: `${rotate.value}deg`, - }, - ], - })) + const kawaii = useKawaiiMode() return ( <> @@ -101,30 +56,21 @@ function HomeHeaderLayoutDesktopAndTablet({ t.atoms.bg, t.atoms.border_contrast_low, styles.bar, + kawaii && {paddingTop: 22, paddingBottom: 16}, ]}> - - - {ctx => ( - - )} - - {/* */} - + + { - if (reducedMotion) return - - // Waits 1500ms, then rotates 10 degrees with a spring animation. Repeats once. - rotate.value = withDelay( - 1000, - withRepeat( - withSequence( - withTiming(10, {duration: 100}), - withSpring(0, { - mass: 1, - damping: 1, - stiffness: 200, - overshootClamping: false, - }), - ), - 2, - false, - ), - ) - }, [rotate, reducedMotion]) - - const animatedStyle = useAnimatedStyle(() => ({ - transform: [ - { - rotateZ: `${rotate.value}deg`, - }, - ], - })) - return ( - - - {ctx => ( - - )} - - {/* */} - + + + Date: Thu, 26 Sep 2024 11:58:47 -0500 Subject: [PATCH 27/44] Adjust dialog timing (#5502) --- src/components/dialogs/nuxs/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/dialogs/nuxs/index.tsx b/src/components/dialogs/nuxs/index.tsx index c740e1e6ad..e7476972ad 100644 --- a/src/components/dialogs/nuxs/index.tsx +++ b/src/components/dialogs/nuxs/index.tsx @@ -41,7 +41,7 @@ const queuedNuxs: { id: Nux.NeueTypography, enabled(props) { if (props.currentProfile.createdAt) { - if (new Date(props.currentProfile.createdAt) < new Date('2024-09-25')) { + if (new Date(props.currentProfile.createdAt) < new Date('2024-10-01')) { return true } } From 1ae7fa6363259bd99b8c6d0f9c9656785e46309e Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 26 Sep 2024 11:59:04 -0500 Subject: [PATCH 28/44] Adjust header offset (#5501) --- src/components/hooks/useHeaderOffset.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/hooks/useHeaderOffset.ts b/src/components/hooks/useHeaderOffset.ts index e2290c04fb..5c80e18fe0 100644 --- a/src/components/hooks/useHeaderOffset.ts +++ b/src/components/hooks/useHeaderOffset.ts @@ -9,8 +9,8 @@ export function useHeaderOffset() { return 0 } const navBarHeight = 42 - const tabBarPad = 10 + 10 + 3 // padding + border - const normalLineHeight = 1.2 - const tabBarText = 16 * normalLineHeight * fontScale + const tabBarPad = 10 + 10 + 6 // padding + arbitrary + const normalLineHeight = 20 // matches tab bar + const tabBarText = normalLineHeight * fontScale return navBarHeight + tabBarPad + tabBarText } From 702dfa8536d1cf9168a40a1f6970da6daf8b2cfb Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 26 Sep 2024 12:47:27 -0500 Subject: [PATCH 29/44] Fix handle collapse on Android (#5504) --- src/view/com/util/PostMeta.tsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx index adf9c5eb1b..c0166a16ee 100644 --- a/src/view/com/util/PostMeta.tsx +++ b/src/view/com/util/PostMeta.tsx @@ -11,6 +11,7 @@ import {NON_BREAKING_SPACE} from '#/lib/strings/constants' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' import {niceDate} from '#/lib/strings/time' +import {isAndroid} from '#/platform/detection' import {precacheProfile} from '#/state/queries/profile' import {atoms as a, useTheme, web} from '#/alf' import {WebOnlyInlineLinkText} from '#/components/Link' @@ -70,7 +71,7 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => { )} - + { - - · - + {!isAndroid && ( + + · + + )} {({timeElapsed}) => ( From 863764b3fecd6905869f67eb46d80f7ffd7264b1 Mon Sep 17 00:00:00 2001 From: Hailey Date: Thu, 26 Sep 2024 10:51:22 -0700 Subject: [PATCH 30/44] [Share Extension] Use the proper identifier for if statement on iOS (#5505) --- modules/Share-with-Bluesky/Info.plist | 2 +- modules/Share-with-Bluesky/ShareViewController.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/Share-with-Bluesky/Info.plist b/modules/Share-with-Bluesky/Info.plist index 43f46a5e56..d73a9093fc 100644 --- a/modules/Share-with-Bluesky/Info.plist +++ b/modules/Share-with-Bluesky/Info.plist @@ -40,4 +40,4 @@ CFBundleShortVersionString $(MARKETING_VERSION) - + \ No newline at end of file diff --git a/modules/Share-with-Bluesky/ShareViewController.swift b/modules/Share-with-Bluesky/ShareViewController.swift index 63143277a5..e166c22d20 100644 --- a/modules/Share-with-Bluesky/ShareViewController.swift +++ b/modules/Share-with-Bluesky/ShareViewController.swift @@ -101,7 +101,7 @@ class ShareViewController: UIViewController { private func handleVideos(items: [NSItemProvider]) async { let firstItem = items.first - if let dataUri = try? await firstItem?.loadItem(forTypeIdentifier: "public.video") as? URL { + if let dataUri = try? await firstItem?.loadItem(forTypeIdentifier: "public.movie") as? URL { let ext = String(dataUri.lastPathComponent.split(separator: ".").last ?? "mp4") if let tempUrl = getTempUrl(ext: ext) { let data = try? Data(contentsOf: dataUri) From 175df72972343795a67be208e58edb02267e1ced Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 26 Sep 2024 12:58:47 -0500 Subject: [PATCH 31/44] Fix weird button wrapping on splash (#5507) * Fix weird button wrapping on splash * Web --- src/view/com/auth/SplashScreen.tsx | 9 ++++----- src/view/com/auth/SplashScreen.web.tsx | 7 ++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/view/com/auth/SplashScreen.tsx b/src/view/com/auth/SplashScreen.tsx index a18f17612e..ae18f13905 100644 --- a/src/view/com/auth/SplashScreen.tsx +++ b/src/view/com/auth/SplashScreen.tsx @@ -39,21 +39,21 @@ export const SplashScreen = ({ What's up? - + - - - )} - - )} + }> + {!link ? ( + + ) : ( + <> + + {isProcessing ? ( + + + + ) : ( + + + + + )} + + )} + ) } + +function Loading() { + return ( + + + + ) +} diff --git a/src/screens/Onboarding/StepProfile/PlaceholderCanvas.tsx b/src/screens/Onboarding/StepProfile/PlaceholderCanvas.tsx index d1d1af6d9f..eaad2113f8 100644 --- a/src/screens/Onboarding/StepProfile/PlaceholderCanvas.tsx +++ b/src/screens/Onboarding/StepProfile/PlaceholderCanvas.tsx @@ -1,14 +1,19 @@ import React from 'react' import {View} from 'react-native' -import ViewShot from 'react-native-view-shot' +import type ViewShot from 'react-native-view-shot' import {useAvatar} from '#/screens/Onboarding/StepProfile/index' import {atoms as a} from '#/alf' +const LazyViewShot = React.lazy( + // @ts-expect-error dynamic import + () => import('react-native-view-shot/src/index'), +) + const SIZE_MULTIPLIER = 5 export interface PlaceholderCanvasRef { - capture: () => Promise + capture: () => Promise } // This component is supposed to be invisible to the user. We only need this for ViewShot to have something to @@ -16,7 +21,7 @@ export interface PlaceholderCanvasRef { export const PlaceholderCanvas = React.forwardRef( function PlaceholderCanvas({}, ref) { const {avatar} = useAvatar() - const viewshotRef = React.useRef() + const viewshotRef = React.useRef(null) const Icon = avatar.placeholder.component const styles = React.useMemo( @@ -32,13 +37,16 @@ export const PlaceholderCanvas = React.forwardRef( ) React.useImperativeHandle(ref, () => ({ - // @ts-ignore this library doesn't have types - capture: viewshotRef.current.capture, + capture: async () => { + if (viewshotRef.current?.capture) { + return await viewshotRef.current.capture() + } + }, })) return ( - ( style={{color: 'white'}} /> - + ) }, diff --git a/src/screens/Onboarding/StepProfile/index.tsx b/src/screens/Onboarding/StepProfile/index.tsx index 5304aa5031..79957da31a 100644 --- a/src/screens/Onboarding/StepProfile/index.tsx +++ b/src/screens/Onboarding/StepProfile/index.tsx @@ -10,13 +10,13 @@ import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useAnalytics} from '#/lib/analytics/analytics' +import {usePhotoLibraryPermission} from '#/lib/hooks/usePermissions' +import {compressIfNeeded} from '#/lib/media/manip' +import {openCropper} from '#/lib/media/picker' +import {getDataUriSize} from '#/lib/media/util' +import {useRequestNotificationsPermission} from '#/lib/notifications/notifications' import {logEvent, useGate} from '#/lib/statsig/statsig' -import {usePhotoLibraryPermission} from 'lib/hooks/usePermissions' -import {compressIfNeeded} from 'lib/media/manip' -import {openCropper} from 'lib/media/picker' -import {getDataUriSize} from 'lib/media/util' -import {useRequestNotificationsPermission} from 'lib/notifications/notifications' -import {isNative, isWeb} from 'platform/detection' +import {isNative, isWeb} from '#/platform/detection' import { DescriptionText, OnboardingControls, @@ -132,6 +132,10 @@ export function StepProfile() { const onContinue = React.useCallback(async () => { let imageUri = avatar?.image?.path + + // In the event that view-shot didn't load in time and the user pressed continue, this will just be undefined + // and the default avatar will be used. We don't want to block getting through create if this fails for some + // reason if (!imageUri || avatar.useCreatedAvatar) { imageUri = await canvasRef.current?.capture() } diff --git a/src/screens/Onboarding/state.ts b/src/screens/Onboarding/state.ts index c41db5c3b7..70fa696408 100644 --- a/src/screens/Onboarding/state.ts +++ b/src/screens/Onboarding/state.ts @@ -51,13 +51,15 @@ export type OnboardingAction = | { type: 'setProfileStepResults' isCreatedAvatar: boolean - image?: OnboardingState['profileStepResults']['image'] - imageUri: string + image: OnboardingState['profileStepResults']['image'] | undefined + imageUri: string | undefined imageMime: string - creatorState?: { - emoji: Emoji - backgroundColor: AvatarColor - } + creatorState: + | { + emoji: Emoji + backgroundColor: AvatarColor + } + | undefined } export type ApiResponseMap = { diff --git a/src/screens/Signup/StepInfo/index.tsx b/src/screens/Signup/StepInfo/index.tsx index 2cdb4b7224..d9b680602a 100644 --- a/src/screens/Signup/StepInfo/index.tsx +++ b/src/screens/Signup/StepInfo/index.tsx @@ -6,8 +6,8 @@ import * as EmailValidator from 'email-validator' import type tldts from 'tldts' import {logEvent} from '#/lib/statsig/statsig' +import {isEmailMaybeInvalid} from '#/lib/strings/email' import {logger} from '#/logger' -import {isEmailMaybeInvalid} from 'lib/strings/email' import {ScreenTransition} from '#/screens/Login/ScreenTransition' import {is13, is18, useSignupContext} from '#/screens/Signup/state' import {Policies} from '#/screens/Signup/StepInfo/Policies' @@ -59,6 +59,9 @@ export function StepInfo({ import('tldts/dist/index.cjs.min.js').then(tldts => { tldtsRef.current = tldts }) + // This will get used in the avatar creator a few steps later, so lets preload it now + // @ts-expect-error - valid path + import('react-native-view-shot/src/index') }, []) const onNextPress = () => { From c7b48cbdca7f5e5000cdffa0d3307fb2c3aba872 Mon Sep 17 00:00:00 2001 From: Hailey Date: Thu, 26 Sep 2024 21:01:57 -0700 Subject: [PATCH 37/44] Tweak font size of "Write your reply" (#5513) --- src/view/com/post-thread/PostThreadComposePrompt.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/view/com/post-thread/PostThreadComposePrompt.tsx b/src/view/com/post-thread/PostThreadComposePrompt.tsx index 7586bd9768..67981618e1 100644 --- a/src/view/com/post-thread/PostThreadComposePrompt.tsx +++ b/src/view/com/post-thread/PostThreadComposePrompt.tsx @@ -63,11 +63,7 @@ export function PostThreadComposePrompt({ avatar={profile?.avatar} type={profile?.associated?.labeler ? 'labeler' : 'user'} /> - + Write your reply From dd2fedb2e68af57cac56b9019050af04119c7ff0 Mon Sep 17 00:00:00 2001 From: Hailey Date: Fri, 27 Sep 2024 00:19:12 -0700 Subject: [PATCH 38/44] add podcasts to spotify embeds (#5514) --- src/lib/strings/embed-player.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/lib/strings/embed-player.ts b/src/lib/strings/embed-player.ts index 3bae771c0a..d0d8277c86 100644 --- a/src/lib/strings/embed-player.ts +++ b/src/lib/strings/embed-player.ts @@ -1,7 +1,7 @@ import {Dimensions} from 'react-native' -import {isSafari} from 'lib/browser' -import {isWeb} from 'platform/detection' +import {isSafari} from '#/lib/browser' +import {isWeb} from '#/platform/detection' const {height: SCREEN_HEIGHT} = Dimensions.get('window') @@ -185,6 +185,20 @@ export function parseEmbedPlayerFromUrl( playerUri: `https://open.spotify.com/embed/track/${id ?? idOrType}`, } } + if (typeOrLocale === 'episode' || idOrType === 'episode') { + return { + type: 'spotify_song', + source: 'spotify', + playerUri: `https://open.spotify.com/embed/episode/${id ?? idOrType}`, + } + } + if (typeOrLocale === 'show' || idOrType === 'show') { + return { + type: 'spotify_song', + source: 'spotify', + playerUri: `https://open.spotify.com/embed/show/${id ?? idOrType}`, + } + } } } From 4553e6b64955c32225cefbe14117e4d08a0520ca Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 27 Sep 2024 10:09:00 +0100 Subject: [PATCH 39/44] Ignore bogus onScroll values (#5499) --- src/view/com/pager/PagerWithHeader.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/view/com/pager/PagerWithHeader.tsx b/src/view/com/pager/PagerWithHeader.tsx index 528f7fdf2e..6d601c2899 100644 --- a/src/view/com/pager/PagerWithHeader.tsx +++ b/src/view/com/pager/PagerWithHeader.tsx @@ -161,10 +161,17 @@ export const PagerWithHeader = React.forwardRef( (e: NativeScrollEvent) => { 'worklet' const nextScrollY = e.contentOffset.y - scrollY.value = nextScrollY - runOnJS(queueThrottledOnScroll)() + // HACK: onScroll is reporting some strange values on load (negative header height). + // Highly improbable that you'd be overscrolled by over 400px - + // in fact, I actually can't do it, so let's just ignore those. -sfn + const isPossiblyInvalid = + headerHeight > 0 && Math.round(nextScrollY * 2) / 2 === -headerHeight + if (!isPossiblyInvalid) { + scrollY.value = nextScrollY + runOnJS(queueThrottledOnScroll)() + } }, - [scrollY, queueThrottledOnScroll], + [scrollY, queueThrottledOnScroll, headerHeight], ) const onPageSelectedInner = React.useCallback( From d8f72c1ee10632860de9a67ce9c84831463ad07c Mon Sep 17 00:00:00 2001 From: Hailey Date: Fri, 27 Sep 2024 09:54:37 -0700 Subject: [PATCH 40/44] [Share Extension] Support on Android for sharing videos to app (#5466) --- .../ExpoReceiveAndroidIntentsModule.kt | 100 ++++++++++++++---- plugins/shareExtension/withIntentFilters.js | 23 ++++ 2 files changed, 102 insertions(+), 21 deletions(-) diff --git a/modules/expo-receive-android-intents/android/src/main/java/xyz/blueskyweb/app/exporeceiveandroidintents/ExpoReceiveAndroidIntentsModule.kt b/modules/expo-receive-android-intents/android/src/main/java/xyz/blueskyweb/app/exporeceiveandroidintents/ExpoReceiveAndroidIntentsModule.kt index 7ecea16314..c88442057c 100644 --- a/modules/expo-receive-android-intents/android/src/main/java/xyz/blueskyweb/app/exporeceiveandroidintents/ExpoReceiveAndroidIntentsModule.kt +++ b/modules/expo-receive-android-intents/android/src/main/java/xyz/blueskyweb/app/exporeceiveandroidintents/ExpoReceiveAndroidIntentsModule.kt @@ -12,6 +12,11 @@ import java.io.File import java.io.FileOutputStream import java.net.URLEncoder +enum class AttachmentType { + IMAGE, + VIDEO, +} + class ExpoReceiveAndroidIntentsModule : Module() { override fun definition() = ModuleDefinition { @@ -23,17 +28,26 @@ class ExpoReceiveAndroidIntentsModule : Module() { } private fun handleIntent(intent: Intent?) { - if (appContext.currentActivity == null || intent == null) return - - if (intent.action == Intent.ACTION_SEND) { - if (intent.type == "text/plain") { - handleTextIntent(intent) - } else if (intent.type.toString().startsWith("image/")) { - handleImageIntent(intent) + if (appContext.currentActivity == null) return + intent?.let { + if (it.action == Intent.ACTION_SEND && it.type == "text/plain") { + handleTextIntent(it) + return } - } else if (intent.action == Intent.ACTION_SEND_MULTIPLE) { - if (intent.type.toString().startsWith("image/")) { - handleImagesIntent(intent) + + val type = + if (it.type.toString().startsWith("image/")) { + AttachmentType.IMAGE + } else if (it.type.toString().startsWith("video/")) { + AttachmentType.VIDEO + } else { + return + } + + if (it.action == Intent.ACTION_SEND) { + handleAttachmentIntent(it, type) + } else if (it.action == Intent.ACTION_SEND_MULTIPLE) { + handleAttachmentsIntent(it, type) } } } @@ -48,26 +62,46 @@ class ExpoReceiveAndroidIntentsModule : Module() { } } - private fun handleImageIntent(intent: Intent) { + private fun handleAttachmentIntent( + intent: Intent, + type: AttachmentType, + ) { val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java) } else { intent.getParcelableExtra(Intent.EXTRA_STREAM) } - if (uri == null) return - handleImageIntents(listOf(uri)) + uri?.let { + when (type) { + AttachmentType.IMAGE -> handleImageIntents(listOf(it)) + AttachmentType.VIDEO -> handleVideoIntents(listOf(it)) + } + } } - private fun handleImagesIntent(intent: Intent) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM, Uri::class.java)?.let { - handleImageIntents(it.filterIsInstance().take(4)) + private fun handleAttachmentsIntent( + intent: Intent, + type: AttachmentType, + ) { + val uris = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent + .getParcelableArrayListExtra(Intent.EXTRA_STREAM, Uri::class.java) + ?.filterIsInstance() + ?.take(4) + } else { + intent + .getParcelableArrayListExtra(Intent.EXTRA_STREAM) + ?.filterIsInstance() + ?.take(4) } - } else { - intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM)?.let { - handleImageIntents(it.filterIsInstance().take(4)) + + uris?.let { + when (type) { + AttachmentType.IMAGE -> handleImageIntents(it) + else -> return } } } @@ -93,11 +127,33 @@ class ExpoReceiveAndroidIntentsModule : Module() { } } + private fun handleVideoIntents(uris: List) { + val uri = uris[0] + // If there is no extension for the file, substringAfterLast returns the original string - not + // null, so we check for that below + // It doesn't actually matter what the extension is, so defaulting to mp4 is fine, even if the + // video isn't actually an mp4 + var extension = uri.path?.substringAfterLast(".") + if (extension == null || extension == uri.path) { + extension = "mp4" + } + val file = createFile(extension) + + val out = FileOutputStream(file) + appContext.currentActivity?.contentResolver?.openInputStream(uri)?.use { + it.copyTo(out) + } + "bluesky://intent/compose?videoUri=${URLEncoder.encode(file.path, "UTF-8")}".toUri().let { + val newIntent = Intent(Intent.ACTION_VIEW, it) + appContext.currentActivity?.startActivity(newIntent) + } + } + private fun getImageInfo(uri: Uri): Map { val bitmap = MediaStore.Images.Media.getBitmap(appContext.currentActivity?.contentResolver, uri) // We have to save this so that we can access it later when uploading the image. // createTempFile will automatically place a unique string between "img" and "temp.jpeg" - val file = File.createTempFile("img", "temp.jpeg", appContext.currentActivity?.cacheDir) + val file = createFile("jpeg") val out = FileOutputStream(file) bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out) out.flush() @@ -110,6 +166,8 @@ class ExpoReceiveAndroidIntentsModule : Module() { ) } + private fun createFile(extension: String): File = File.createTempFile(extension, "temp.$extension", appContext.currentActivity?.cacheDir) + // We will pas the width and height to the app here, since getting measurements // on the RN side is a bit more involved, and we already have them here anyway. private fun buildUriData(info: Map): String { diff --git a/plugins/shareExtension/withIntentFilters.js b/plugins/shareExtension/withIntentFilters.js index 605fcfd052..16494893bb 100644 --- a/plugins/shareExtension/withIntentFilters.js +++ b/plugins/shareExtension/withIntentFilters.js @@ -27,6 +27,29 @@ const withIntentFilters = config => { }, ], }, + { + action: [ + { + $: { + 'android:name': 'android.intent.action.SEND', + }, + }, + ], + category: [ + { + $: { + 'android:name': 'android.intent.category.DEFAULT', + }, + }, + ], + data: [ + { + $: { + 'android:mimeType': 'video/*', + }, + }, + ], + }, { action: [ { From bcd096b85aee45c38de7cfbcf1115b0a544589ae Mon Sep 17 00:00:00 2001 From: Hailey Date: Fri, 27 Sep 2024 09:55:47 -0700 Subject: [PATCH 41/44] Fix alignment of cancel button on search (#5520) --- src/view/screens/Search/Search.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx index de46d18c0c..583999f87e 100644 --- a/src/view/screens/Search/Search.tsx +++ b/src/view/screens/Search/Search.tsx @@ -845,14 +845,14 @@ export function SearchScreen( a.gap_sm, t.atoms.bg, web({ - height: headerHeight, + height: headerHeight + a.mb_sm.marginBottom, position: 'sticky', top: 0, zIndex: 1, }), ]} sideBorders={gtMobile}> - + {!gtMobile && (