From d5503d17de4a3b8f2d769f7819baf47cf9b0a209 Mon Sep 17 00:00:00 2001 From: Hailey Date: Mon, 8 Jul 2024 08:57:40 -0700 Subject: [PATCH 01/22] Reduce the size of the inner logo in the QR code (#4746) * use a scale of `1` on QR code inner logo * remove logo entirely * Revert "remove logo entirely" This reverts commit dee57f48c7bf5e3b5765631b8cbbd1c746e9c884. * use scale of 0.95 --- src/components/StarterPack/QrCode.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/StarterPack/QrCode.tsx b/src/components/StarterPack/QrCode.tsx index 08ee03d623..a8a2e34917 100644 --- a/src/components/StarterPack/QrCode.tsx +++ b/src/components/StarterPack/QrCode.tsx @@ -110,7 +110,7 @@ export function QrCodeInner({link}: {link: string}) { innerEyesOptions={{borderRadius: 3}} logo={{ href: require('../../../assets/logo.png'), - scale: 1.2, + scale: 0.95, padding: 2, hidePieces: true, }} From f8a59e10dd347e9187aa4414a0ece04eff3b9bc3 Mon Sep 17 00:00:00 2001 From: Hailey Date: Mon, 8 Jul 2024 10:06:00 -0700 Subject: [PATCH 02/22] Fix RTL text rendering for display names (#4747) * header display name rtl support * highlighted post rtl * move `NON_BREAKING_SPACE` to an external constant * rtl support in search dropdown * profile card rtl * old profile card rtl * hover card * wizard list card * new chat * account card * chat header * clean up notifications * just force LTR on meta display name --- src/components/ProfileCard.tsx | 4 +++- src/components/ProfileHoverCard/index.web.tsx | 3 ++- .../StarterPack/Wizard/WizardListCard.tsx | 8 +++++++- src/components/dms/MessagesListHeader.tsx | 7 ++++++- .../dms/dialogs/SearchablePeopleList.tsx | 2 +- src/lib/strings/constants.ts | 1 + src/screens/Profile/Header/DisplayName.tsx | 2 +- src/view/com/notifications/FeedItem.tsx | 9 ++++++--- src/view/com/post-thread/PostThreadItem.tsx | 2 +- src/view/com/profile/ProfileCard.tsx | 2 +- src/view/com/util/PostMeta.tsx | 18 ++++++++---------- src/view/screens/Settings/index.tsx | 3 ++- src/view/shell/desktop/Search.tsx | 3 ++- 13 files changed, 41 insertions(+), 23 deletions(-) create mode 100644 src/lib/strings/constants.ts diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx index 79f1108cb3..8bc9376e0c 100644 --- a/src/components/ProfileCard.tsx +++ b/src/components/ProfileCard.tsx @@ -166,7 +166,9 @@ export function NameAndHandle({ return ( - + {name} - + {sanitizeDisplayName( profile.displayName || sanitizeHandle(profile.handle), moderation.ui('displayName'), diff --git a/src/components/StarterPack/Wizard/WizardListCard.tsx b/src/components/StarterPack/Wizard/WizardListCard.tsx index aa1b2cf9bd..55cf0f02b3 100644 --- a/src/components/StarterPack/Wizard/WizardListCard.tsx +++ b/src/components/StarterPack/Wizard/WizardListCard.tsx @@ -78,7 +78,13 @@ function WizardListCard({ /> {displayName} diff --git a/src/components/dms/MessagesListHeader.tsx b/src/components/dms/MessagesListHeader.tsx index 8bf673d300..7b9f1a3a02 100644 --- a/src/components/dms/MessagesListHeader.tsx +++ b/src/components/dms/MessagesListHeader.tsx @@ -168,7 +168,12 @@ function HeaderReady({ {displayName} diff --git a/src/components/dms/dialogs/SearchablePeopleList.tsx b/src/components/dms/dialogs/SearchablePeopleList.tsx index d92ea68350..a13dfe509f 100644 --- a/src/components/dms/dialogs/SearchablePeopleList.tsx +++ b/src/components/dms/dialogs/SearchablePeopleList.tsx @@ -395,7 +395,7 @@ function ProfileCard({ /> {displayName} diff --git a/src/lib/strings/constants.ts b/src/lib/strings/constants.ts new file mode 100644 index 0000000000..4bce3ac626 --- /dev/null +++ b/src/lib/strings/constants.ts @@ -0,0 +1 @@ +export const NON_BREAKING_SPACE = '\u00A0' diff --git a/src/screens/Profile/Header/DisplayName.tsx b/src/screens/Profile/Header/DisplayName.tsx index c63658a44a..955e3d72c8 100644 --- a/src/screens/Profile/Header/DisplayName.tsx +++ b/src/screens/Profile/Header/DisplayName.tsx @@ -20,7 +20,7 @@ export function ProfileHeaderDisplayName({ + style={[t.atoms.text, a.text_4xl, a.self_start, {fontWeight: '500'}]}> {sanitizeDisplayName( profile.displayName || sanitizeHandle(profile.handle), moderation.ui('displayName'), diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx index 1932efbd5c..d31962ff35 100644 --- a/src/view/com/notifications/FeedItem.tsx +++ b/src/view/com/notifications/FeedItem.tsx @@ -58,6 +58,7 @@ import {useNavigation} from '@react-navigation/native' import {parseTenorGif} from '#/lib/strings/embed-player' import {logger} from '#/logger' import {NavigationProp} from 'lib/routes/types' +import {forceLTR} from 'lib/strings/bidi' import {DM_SERVICE_HEADERS} from 'state/queries/messages/const' import {useAgent} from 'state/session' import {Button, ButtonText} from '#/components/Button' @@ -274,13 +275,15 @@ let FeedItem = ({ showDmButton={item.type === 'starterpack-joined' || isFollowBack} /> - + diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 0f5350e790..4e6ed27bed 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -281,7 +281,7 @@ let PostThreadItemLoaded = ({ {sanitizeDisplayName( diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx index 7332d452ad..d18103f306 100644 --- a/src/view/com/profile/ProfileCard.tsx +++ b/src/view/com/profile/ProfileCard.tsx @@ -97,7 +97,7 @@ export function ProfileCard({ {sanitizeDisplayName( diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx index aec787e4e5..95168e8b3c 100644 --- a/src/view/com/util/PostMeta.tsx +++ b/src/view/com/util/PostMeta.tsx @@ -6,6 +6,8 @@ import {useQueryClient} from '@tanstack/react-query' import {precacheProfile} from '#/state/queries/profile' import {usePalette} from 'lib/hooks/usePalette' import {makeProfileLink} from 'lib/routes/links' +import {forceLTR} from 'lib/strings/bidi' +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' @@ -32,8 +34,6 @@ interface PostMetaOpts { style?: StyleProp } -const NON_BREAKING_SPACE = '\u00A0' - let PostMeta = (opts: PostMetaOpts): React.ReactNode => { const pal = usePalette('default') const displayName = opts.author.displayName || opts.author.handle @@ -70,14 +70,12 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => { style={[pal.text]} lineHeight={1.2} disableMismatchWarning - text={ - <> - {sanitizeDisplayName( - displayName, - opts.moderation?.ui('displayName'), - )} - - } + text={forceLTR( + sanitizeDisplayName( + displayName, + opts.moderation?.ui('displayName'), + ), + )} href={profileLink} onBeforePress={onBeforePressAuthor} /> diff --git a/src/view/screens/Settings/index.tsx b/src/view/screens/Settings/index.tsx index 1d8199b009..f59bfb1834 100644 --- a/src/view/screens/Settings/index.tsx +++ b/src/view/screens/Settings/index.tsx @@ -68,6 +68,7 @@ import {navigate, resetToTab} from '#/Navigation' import {Email2FAToggle} from './Email2FAToggle' import {ExportCarDialog} from './ExportCarDialog' import hairlineWidth = StyleSheet.hairlineWidth +import {atoms as a} from '#/alf' function SettingsAccountCard({ account, @@ -104,7 +105,7 @@ function SettingsAccountCard({ /> - + {profile?.displayName || account.handle} diff --git a/src/view/shell/desktop/Search.tsx b/src/view/shell/desktop/Search.tsx index 3829a6c0b2..d8aa518996 100644 --- a/src/view/shell/desktop/Search.tsx +++ b/src/view/shell/desktop/Search.tsx @@ -30,6 +30,7 @@ import {precacheProfile} from 'state/queries/profile' import {Link} from '#/view/com/util/Link' import {UserAvatar} from '#/view/com/util/UserAvatar' import {Text} from 'view/com/util/text/Text' +import {atoms as a} from '#/alf' let SearchLinkCard = ({ label, @@ -127,7 +128,7 @@ let SearchProfileCard = ({ {sanitizeDisplayName( From a3c43a74712cdd402dc1e36c3c6b487ffaa1589d Mon Sep 17 00:00:00 2001 From: Hailey Date: Mon, 8 Jul 2024 15:16:43 -0700 Subject: [PATCH 03/22] change `contentVisibility` to `contain` (#4752) --- .../Messages/Conversation/MessagesList.tsx | 4 +- src/screens/StarterPack/Wizard/StepFeeds.tsx | 2 +- .../StarterPack/Wizard/StepProfiles.tsx | 2 +- src/view/com/util/List.tsx | 5 +- src/view/com/util/List.web.tsx | 47 ++++++++++--------- src/view/screens/Storybook/ListContained.tsx | 2 +- 6 files changed, 33 insertions(+), 29 deletions(-) diff --git a/src/screens/Messages/Conversation/MessagesList.tsx b/src/screens/Messages/Conversation/MessagesList.tsx index f72515ac62..11b951e99d 100644 --- a/src/screens/Messages/Conversation/MessagesList.tsx +++ b/src/screens/Messages/Conversation/MessagesList.tsx @@ -386,10 +386,10 @@ export function MessagesList({ data={convoState.items} renderItem={renderItem} keyExtractor={keyExtractor} - containWeb={true} + disableFullWindowScroll={true} // Prevents wrong position in Firefox when sending a message // as well as scroll getting stuck on Chome when scrolling upwards. - disableContentVisibility={true} + disableContainStyle={true} disableVirtualization={true} style={animatedListStyle} // The extra two items account for the header and the footer components diff --git a/src/screens/StarterPack/Wizard/StepFeeds.tsx b/src/screens/StarterPack/Wizard/StepFeeds.tsx index 878d17ce01..de8d856aba 100644 --- a/src/screens/StarterPack/Wizard/StepFeeds.tsx +++ b/src/screens/StarterPack/Wizard/StepFeeds.tsx @@ -101,7 +101,7 @@ export function StepFeeds({moderationOpts}: {moderationOpts: ModerationOpts}) { onEndReachedThreshold={2} renderScrollComponent={props => } keyboardShouldPersistTaps="handled" - containWeb={true} + disableFullWindowScroll={true} sideBorders={false} style={{flex: 1}} ListEmptyComponent={ diff --git a/src/screens/StarterPack/Wizard/StepProfiles.tsx b/src/screens/StarterPack/Wizard/StepProfiles.tsx index f77a46e7ab..c14de847f6 100644 --- a/src/screens/StarterPack/Wizard/StepProfiles.tsx +++ b/src/screens/StarterPack/Wizard/StepProfiles.tsx @@ -80,7 +80,7 @@ export function StepProfiles({ keyExtractor={keyExtractor} renderScrollComponent={props => } keyboardShouldPersistTaps="handled" - containWeb={true} + disableFullWindowScroll={true} sideBorders={false} style={[a.flex_1]} onEndReached={ diff --git a/src/view/com/util/List.tsx b/src/view/com/util/List.tsx index ed43ed5f0d..e1a10e4741 100644 --- a/src/view/com/util/List.tsx +++ b/src/view/com/util/List.tsx @@ -24,11 +24,12 @@ export type ListProps = Omit< refreshing?: boolean onRefresh?: () => void onItemSeen?: (item: ItemT) => void - containWeb?: boolean desktopFixedHeight?: number | boolean + // Web only prop to contain the scroll to the container rather than the window + disableFullWindowScroll?: boolean sideBorders?: boolean // Web only prop to disable a perf optimization (which would otherwise be on). - disableContentVisibility?: boolean + disableContainStyle?: boolean } export type ListRef = React.MutableRefObject diff --git a/src/view/com/util/List.web.tsx b/src/view/com/util/List.web.tsx index f2b2add377..12d223db03 100644 --- a/src/view/com/util/List.web.tsx +++ b/src/view/com/util/List.web.tsx @@ -23,9 +23,11 @@ export type ListProps = Omit< onRefresh?: () => void onItemSeen?: (item: ItemT) => void desktopFixedHeight?: number | boolean - containWeb?: boolean + // Web only prop to contain the scroll to the container rather than the window + disableFullWindowScroll?: boolean sideBorders?: boolean - disableContentVisibility?: boolean + // Web only prop to disable a perf optimization (which would otherwise be on). + disableContainStyle?: boolean } export type ListRef = React.MutableRefObject // TODO: Better types. @@ -39,7 +41,7 @@ function ListImpl( ListHeaderComponent, ListFooterComponent, ListEmptyComponent, - containWeb, + disableFullWindowScroll, contentContainerStyle, data, desktopFixedHeight, @@ -58,7 +60,7 @@ function ListImpl( extraData, style, sideBorders = true, - disableContentVisibility, + disableContainStyle, ...props }: ListProps, ref: React.Ref, @@ -112,7 +114,7 @@ function ListImpl( } const getScrollableNode = React.useCallback(() => { - if (containWeb) { + if (disableFullWindowScroll) { const element = nativeRef.current as HTMLDivElement | null if (!element) return @@ -182,7 +184,7 @@ function ListImpl( }, } } - }, [containWeb]) + }, [disableFullWindowScroll]) const nativeRef = React.useRef(null) React.useImperativeHandle( @@ -267,7 +269,12 @@ function ListImpl( return () => { element?.removeEventListener('scroll', handleScroll) } - }, [isInsideVisibleTree, handleScroll, containWeb, getScrollableNode]) + }, [ + isInsideVisibleTree, + handleScroll, + disableFullWindowScroll, + getScrollableNode, + ]) // --- onScrolledDownChange --- const isScrolledDown = useRef(false) @@ -308,7 +315,7 @@ function ListImpl( {...props} style={[ style, - containWeb && { + disableFullWindowScroll && { flex: 1, // @ts-expect-error web only 'overflow-y': 'scroll', @@ -332,13 +339,13 @@ function ListImpl( pal.border, ]}> {onStartReached && !isEmpty && ( @@ -356,13 +363,13 @@ function ListImpl( renderItem={renderItem} extraData={extraData} onItemSeen={onItemSeen} - disableContentVisibility={disableContentVisibility} + disableContainStyle={disableContainStyle} /> ) })} {onEndReached && !isEmpty && ( ({ renderItem, extraData: _unused, onItemSeen, - disableContentVisibility, + disableContainStyle, }: { item: ItemT index: number @@ -416,7 +423,7 @@ let Row = function RowImpl({ | ((data: {index: number; item: any; separators: any}) => React.ReactNode) extraData: any onItemSeen: ((item: any) => void) | undefined - disableContentVisibility?: boolean + disableContainStyle?: boolean }): React.ReactNode { const rowRef = React.useRef(null) const intersectionTimeout = React.useRef(undefined) @@ -465,14 +472,10 @@ let Row = function RowImpl({ return null } - const shouldDisableContentVisibility = disableContentVisibility || isSafari + const shouldDisableContainStyle = disableContainStyle || isSafari return ( {renderItem({item, index, separators: null as any})} @@ -544,9 +547,9 @@ const styles = StyleSheet.create({ marginLeft: 'auto', marginRight: 'auto', }, - contentVisibilityAuto: { + contain: { // @ts-ignore web only - contentVisibility: 'auto', + contain: 'layout paint', }, minHeightViewport: { // @ts-ignore web only diff --git a/src/view/screens/Storybook/ListContained.tsx b/src/view/screens/Storybook/ListContained.tsx index b3ea091f40..20ec686570 100644 --- a/src/view/screens/Storybook/ListContained.tsx +++ b/src/view/screens/Storybook/ListContained.tsx @@ -47,7 +47,7 @@ export function ListContained() { ) }} keyExtractor={item => item.id.toString()} - containWeb={true} + disableFullWindowScroll={true} style={{flex: 1}} onStartReached={() => { console.log('Start Reached') From ca0a3270521172f19315ac9c33a1a18b4d2b78e1 Mon Sep 17 00:00:00 2001 From: Hailey Date: Mon, 8 Jul 2024 18:16:34 -0700 Subject: [PATCH 04/22] tweak top padding external (#4755) --- src/view/com/util/post-embeds/index.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx index 942ad57b81..3fd55f5c21 100644 --- a/src/view/com/util/post-embeds/index.tsx +++ b/src/view/com/util/post-embeds/index.tsx @@ -131,7 +131,7 @@ export function PostEmbeds({ const {alt, thumb, aspectRatio} = images[0] return ( - + - + - + ) } @@ -183,7 +187,7 @@ export function PostEmbeds({ } const styles = StyleSheet.create({ - imagesContainer: { + container: { marginTop: 8, }, altContainer: { From 4360087ced80a5633781cf6f1c307606a1b0a210 Mon Sep 17 00:00:00 2001 From: "A Malik. Muhd" Date: Tue, 9 Jul 2024 03:02:26 +0100 Subject: [PATCH 05/22] Update README.md (#4394) --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 49c4b016ff..64bcbcea5e 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Get the app itself: ## Development Resources -This is a [React Native](https://reactnative.dev/) application, written in the TypeScript programming language. It builds on the `atproto` TypeScript packages (like [`@atproto/api`](https://www.npmjs.com/package/@atproto/api)), code for which is also on open source, but in [a different git repository](https://github.com/bluesky-social/atproto). +This is a [React Native](https://reactnative.dev/) application, written in the TypeScript programming language. It builds on the `atproto` TypeScript packages (like [`@atproto/api`](https://www.npmjs.com/package/@atproto/api)), code for which is also open source, but in [a different git repository](https://github.com/bluesky-social/atproto). There is a small amount of Go language source code (in `./bskyweb/`), for a web service that returns the React Native Web application. @@ -42,10 +42,10 @@ The Bluesky Social application encompasses a set of schemas and APIs built in th - Open an issue and give some time for discussion before submitting a PR. - Stay away from PRs like... - Changing "Post" to "Skeet." - - Refactoring the codebase, eg to replace mobx with redux or something. + - Refactoring the codebase, e.g., to replace MobX with Redux or something. - Adding entirely new features without prior discussion. -Remember, we serve a wide community of users. Our day to day involves us constantly asking "which top priority is our top priority." If you submit well-written PRs that solve problems concisely, that's an awesome contribution. Otherwise, as much as we'd love to accept your ideas and contributions, we really don't have the bandwidth. That's what forking is for! +Remember, we serve a wide community of users. Our day-to-day involves us constantly asking "which top priority is our top priority." If you submit well-written PRs that solve problems concisely, that's an awesome contribution. Otherwise, as much as we'd love to accept your ideas and contributions, we really don't have the bandwidth. That's what forking is for! ## Forking guidelines @@ -63,7 +63,7 @@ If you discover any security issues, please send an email to security@bsky.app. ## Are you a developer interested in building on atproto? -Bluesky is an open social network built on the AT Protocol, a flexible technology that will never lock developers out of the ecosystems that they help build. With atproto, third-party can be as seamless as first-party through custom feeds, federated services, clients, and more. +Bluesky is an open social network built on the AT Protocol, a flexible technology that will never lock developers out of the ecosystems that they help build. With atproto, third-party integration can be as seamless as first-party through custom feeds, federated services, clients, and more. ## License (MIT) From 2d0eefebc338eee0d5d7e3e4c02bd6bba7f6baa0 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 9 Jul 2024 17:10:50 +0100 Subject: [PATCH 06/22] Add social proof to suggested follows (#4602) * replace unused `followers` prop with social proof * Introduce 'minimal' version * Gate social proof one explore page, fix space if no desc * Use smaller avis for minimal --------- Co-authored-by: Eric Bailey --- src/components/KnownFollowers.tsx | 20 ++++-- src/lib/statsig/gates.ts | 1 + src/view/com/profile/ProfileCard.tsx | 103 ++++++++++----------------- src/view/screens/Search/Explore.tsx | 15 +++- 4 files changed, 64 insertions(+), 75 deletions(-) diff --git a/src/components/KnownFollowers.tsx b/src/components/KnownFollowers.tsx index 7b861dc660..4017a7b0be 100644 --- a/src/components/KnownFollowers.tsx +++ b/src/components/KnownFollowers.tsx @@ -12,6 +12,7 @@ import {Link, LinkProps} from '#/components/Link' import {Text} from '#/components/Typography' const AVI_SIZE = 30 +const AVI_SIZE_SMALL = 20 const AVI_BORDER = 1 /** @@ -30,10 +31,12 @@ export function KnownFollowers({ profile, moderationOpts, onLinkPress, + minimal, }: { profile: AppBskyActorDefs.ProfileViewDetailed moderationOpts: ModerationOpts onLinkPress?: LinkProps['onPress'] + minimal?: boolean }) { const cache = React.useRef>( new Map(), @@ -59,6 +62,7 @@ export function KnownFollowers({ cachedKnownFollowers={cachedKnownFollowers} moderationOpts={moderationOpts} onLinkPress={onLinkPress} + minimal={minimal} /> ) } @@ -71,11 +75,13 @@ function KnownFollowersInner({ moderationOpts, cachedKnownFollowers, onLinkPress, + minimal, }: { profile: AppBskyActorDefs.ProfileViewDetailed moderationOpts: ModerationOpts cachedKnownFollowers: AppBskyActorDefs.KnownFollowers onLinkPress?: LinkProps['onPress'] + minimal?: boolean }) { const t = useTheme() const {_} = useLingui() @@ -110,6 +116,8 @@ function KnownFollowersInner({ */ if (slice.length === 0) return null + const SIZE = minimal ? AVI_SIZE_SMALL : AVI_SIZE + return ( @@ -129,8 +137,8 @@ function KnownFollowersInner({ diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts index 378b273494..1c86d01da4 100644 --- a/src/lib/statsig/gates.ts +++ b/src/lib/statsig/gates.ts @@ -1,6 +1,7 @@ export type Gate = // Keep this alphabetic please. | 'debug_show_feedcontext' + | 'explore_page_profile_card_social_proof' | 'native_pwi_disabled' | 'new_user_guided_tour' | 'new_user_progress_guide' diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx index d18103f306..9458fbaf8e 100644 --- a/src/view/com/profile/ProfileCard.tsx +++ b/src/view/com/profile/ProfileCard.tsx @@ -5,7 +5,6 @@ import { moderateProfile, ModerationDecision, } from '@atproto/api' -import {Trans} from '@lingui/macro' import {useQueryClient} from '@tanstack/react-query' import {useProfileShadow} from '#/state/cache/profile-shadow' @@ -19,12 +18,16 @@ import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeHandle} from 'lib/strings/handles' import {s} from 'lib/styles' import {precacheProfile} from 'state/queries/profile' +import {atoms as a} from '#/alf' +import { + KnownFollowers, + shouldShowKnownFollowers, +} from '#/components/KnownFollowers' import {Link} from '../util/Link' import {Text} from '../util/text/Text' import {PreviewableUserAvatar} from '../util/UserAvatar' import {FollowButton} from './FollowButton' import hairlineWidth = StyleSheet.hairlineWidth -import {atoms as a} from '#/alf' import * as Pills from '#/components/Pills' export function ProfileCard({ @@ -33,22 +36,22 @@ export function ProfileCard({ noModFilter, noBg, noBorder, - followers, renderButton, onPress, style, + showKnownFollowers, }: { testID?: string profile: AppBskyActorDefs.ProfileViewBasic noModFilter?: boolean noBg?: boolean noBorder?: boolean - followers?: AppBskyActorDefs.ProfileView[] | undefined renderButton?: ( profile: Shadow, ) => React.ReactNode onPress?: () => void style?: StyleProp + showKnownFollowers?: boolean }) { const queryClient = useQueryClient() const pal = usePalette('default') @@ -70,6 +73,11 @@ export function ProfileCard({ return null } + const knownFollowersVisible = + showKnownFollowers && + shouldShowKnownFollowers(profile.viewer?.knownFollowers) && + moderationOpts + return ( {renderButton(profile)} ) : undefined} - {profile.description ? ( + {profile.description || knownFollowersVisible ? ( - - {profile.description as string} - + {profile.description ? ( + + {profile.description as string} + + ) : null} + {knownFollowersVisible ? ( + + + + ) : null} ) : null} - ) } @@ -155,73 +179,20 @@ export function ProfileCardPills({ ) } -function FollowersList({ - followers, -}: { - followers?: AppBskyActorDefs.ProfileView[] | undefined -}) { - const pal = usePalette('default') - const moderationOpts = useModerationOpts() - - const followersWithMods = React.useMemo(() => { - if (!followers || !moderationOpts) { - return [] - } - - return followers - .map(f => ({ - f, - mod: moderateProfile(f, moderationOpts), - })) - .filter(({mod}) => !mod.ui('profileList').filter) - }, [followers, moderationOpts]) - - if (!followersWithMods?.length) { - return null - } - - return ( - - - - Followed by{' '} - {followersWithMods.map(({f}) => f.displayName || f.handle).join(', ')} - - - {followersWithMods.slice(0, 3).map(({f, mod}) => ( - - - - - - ))} - - ) -} - export function ProfileCardWithFollowBtn({ profile, noBg, noBorder, - followers, onPress, logContext = 'ProfileCard', + showKnownFollowers, }: { - profile: AppBskyActorDefs.ProfileViewBasic + profile: AppBskyActorDefs.ProfileView noBg?: boolean noBorder?: boolean - followers?: AppBskyActorDefs.ProfileView[] | undefined onPress?: () => void logContext?: 'ProfileCard' | 'StarterPackProfilesList' + showKnownFollowers?: boolean }) { const {currentAccount} = useSession() const isMe = profile.did === currentAccount?.did @@ -231,7 +202,6 @@ export function ProfileCardWithFollowBtn({ profile={profile} noBg={noBg} noBorder={noBorder} - followers={followers} renderButton={ isMe ? undefined @@ -240,6 +210,7 @@ export function ProfileCardWithFollowBtn({ ) } onPress={onPress} + showKnownFollowers={!isMe && showKnownFollowers} /> ) } diff --git a/src/view/screens/Search/Explore.tsx b/src/view/screens/Search/Explore.tsx index 85e8ffa4ec..05fd85effe 100644 --- a/src/view/screens/Search/Explore.tsx +++ b/src/view/screens/Search/Explore.tsx @@ -10,6 +10,7 @@ import { import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useGate} from '#/lib/statsig/statsig' import {logger} from '#/logger' import {isWeb} from '#/platform/detection' import {useModerationOpts} from '#/state/preferences/moderation-opts' @@ -241,7 +242,7 @@ type ExploreScreenItems = | { type: 'profile' key: string - profile: AppBskyActorDefs.ProfileViewBasic + profile: AppBskyActorDefs.ProfileView } | { type: 'feed' @@ -291,6 +292,7 @@ export function Explore() { error: feedsError, fetchNextPage: fetchNextFeedsPage, } = useGetPopularFeedsQuery({limit: 10}) + const gate = useGate() const isLoadingMoreProfiles = isFetchingNextProfilesPage && !isLoadingProfiles const onLoadMoreProfiles = React.useCallback(async () => { @@ -492,7 +494,14 @@ export function Explore() { case 'profile': { return ( - + ) } @@ -555,7 +564,7 @@ export function Explore() { } } }, - [t, moderationOpts], + [t, moderationOpts, gate], ) return ( From 7a476568168fa897d3dc9fcae3c1f77d4363366e Mon Sep 17 00:00:00 2001 From: dan Date: Tue, 9 Jul 2024 21:13:32 +0100 Subject: [PATCH 07/22] Log session events unconditionally (#4758) --- src/state/session/agent.ts | 2 ++ src/state/session/logging.ts | 14 +++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/state/session/agent.ts b/src/state/session/agent.ts index 4bcb4c11ca..d31b5588b4 100644 --- a/src/state/session/agent.ts +++ b/src/state/session/agent.ts @@ -12,6 +12,7 @@ import {tryFetchGates} from '#/lib/statsig/statsig' import {getAge} from '#/lib/strings/time' import {logger} from '#/logger' import {snoozeEmailConfirmationPrompt} from '#/state/shell/reminders' +import {addSessionEventLog} from './logging' import { configureModerationForAccount, configureModerationForGuest, @@ -194,6 +195,7 @@ async function prepareAgent( const account = agentToSessionAccountOrThrow(agent) agent.setPersistSessionHandler(event => { onSessionChange(agent, account.did, event) + addSessionEventLog(account.did, event) }) return {agent, account} } diff --git a/src/state/session/logging.ts b/src/state/session/logging.ts index 16aa66fe72..ab67785ca6 100644 --- a/src/state/session/logging.ts +++ b/src/state/session/logging.ts @@ -1,4 +1,4 @@ -import {AtpSessionData} from '@atproto/api' +import {AtpSessionData, AtpSessionEvent} from '@atproto/api' import {sha256} from 'js-sha256' import {Statsig} from 'statsig-react-native-expo' @@ -70,6 +70,18 @@ export function wrapSessionReducerForLogging(reducer: Reducer): Reducer { let nextMessageIndex = 0 const MAX_SLICE_LENGTH = 1000 +// Not gated. +export function addSessionEventLog(did: string, event: AtpSessionEvent) { + try { + if (!Statsig.initializeCalled() || !Statsig.getStableID()) { + return + } + Statsig.logEvent('session:event', null, {did, event}) + } catch (e) { + console.error(e) + } +} + export function addSessionDebugLog(log: Log) { try { if (!Statsig.initializeCalled() || !Statsig.getStableID()) { From ce0bf867ff3b50a495d8db242a7f55371bffeadc Mon Sep 17 00:00:00 2001 From: dan Date: Tue, 9 Jul 2024 23:03:59 +0100 Subject: [PATCH 08/22] Rename session event, scope to errors (#4761) --- src/state/session/agent.ts | 6 ++++-- src/state/session/logging.ts | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/state/session/agent.ts b/src/state/session/agent.ts index d31b5588b4..4456ab0bf9 100644 --- a/src/state/session/agent.ts +++ b/src/state/session/agent.ts @@ -12,7 +12,7 @@ import {tryFetchGates} from '#/lib/statsig/statsig' import {getAge} from '#/lib/strings/time' import {logger} from '#/logger' import {snoozeEmailConfirmationPrompt} from '#/state/shell/reminders' -import {addSessionEventLog} from './logging' +import {addSessionErrorLog} from './logging' import { configureModerationForAccount, configureModerationForGuest, @@ -195,7 +195,9 @@ async function prepareAgent( const account = agentToSessionAccountOrThrow(agent) agent.setPersistSessionHandler(event => { onSessionChange(agent, account.did, event) - addSessionEventLog(account.did, event) + if (event !== 'create' && event !== 'update') { + addSessionErrorLog(account.did, event) + } }) return {agent, account} } diff --git a/src/state/session/logging.ts b/src/state/session/logging.ts index ab67785ca6..9a703957d6 100644 --- a/src/state/session/logging.ts +++ b/src/state/session/logging.ts @@ -71,12 +71,12 @@ let nextMessageIndex = 0 const MAX_SLICE_LENGTH = 1000 // Not gated. -export function addSessionEventLog(did: string, event: AtpSessionEvent) { +export function addSessionErrorLog(did: string, event: AtpSessionEvent) { try { if (!Statsig.initializeCalled() || !Statsig.getStableID()) { return } - Statsig.logEvent('session:event', null, {did, event}) + Statsig.logEvent('session:error', null, {did, event}) } catch (e) { console.error(e) } From 9b9e09d0020283b9aee6911e0c96b5ddbb0c87aa Mon Sep 17 00:00:00 2001 From: dan Date: Wed, 10 Jul 2024 00:12:26 +0100 Subject: [PATCH 09/22] [Session] Experiment: Don't use withProxy (#4762) * Reorder statements * Remove withProxy() usage behind a gate --- src/components/ReportDialog/SubmitView.tsx | 35 +++++++++++---- .../moderation/LabelsOnMeDialog.tsx | 39 ++++++++++++---- src/lib/statsig/gates.ts | 1 + src/state/feed-feedback.tsx | 44 +++++++++++++------ 4 files changed, 88 insertions(+), 31 deletions(-) diff --git a/src/components/ReportDialog/SubmitView.tsx b/src/components/ReportDialog/SubmitView.tsx index 3731617fd3..7ceece75b6 100644 --- a/src/components/ReportDialog/SubmitView.tsx +++ b/src/components/ReportDialog/SubmitView.tsx @@ -6,6 +6,7 @@ import {useLingui} from '@lingui/react' import {getLabelingServiceTitle} from '#/lib/moderation' import {ReportOption} from '#/lib/moderation/useReportOptions' +import {useGate} from '#/lib/statsig/statsig' import {useAgent} from '#/state/session' import {CharProgress} from '#/view/com/composer/char-progress/CharProgress' import * as Toast from '#/view/com/util/Toast' @@ -36,6 +37,7 @@ export function SubmitView({ const t = useTheme() const {_} = useLingui() const agent = useAgent() + const gate = useGate() const [details, setDetails] = React.useState('') const [submitting, setSubmitting] = React.useState(false) const [selectedServices, setSelectedServices] = React.useState([ @@ -60,15 +62,29 @@ export function SubmitView({ reason: details, } const results = await Promise.all( - selectedServices.map(did => - agent - .withProxy('atproto_labeler', did) - .createModerationReport(report) - .then( - _ => true, - _ => false, - ), - ), + selectedServices.map(did => { + if (gate('session_withproxy_fix')) { + return agent + .createModerationReport(report, { + encoding: 'application/json', + headers: { + 'atproto-proxy': `${did}#atproto_labeler`, + }, + }) + .then( + _ => true, + _ => false, + ) + } else { + return agent + .withProxy('atproto_labeler', did) + .createModerationReport(report) + .then( + _ => true, + _ => false, + ) + } + }), ) setSubmitting(false) @@ -92,6 +108,7 @@ export function SubmitView({ onSubmitComplete, setError, agent, + gate, ]) return ( diff --git a/src/components/moderation/LabelsOnMeDialog.tsx b/src/components/moderation/LabelsOnMeDialog.tsx index d0f0d4ea0a..c226fbbf75 100644 --- a/src/components/moderation/LabelsOnMeDialog.tsx +++ b/src/components/moderation/LabelsOnMeDialog.tsx @@ -7,6 +7,7 @@ import {useMutation} from '@tanstack/react-query' import {useLabelInfo} from '#/lib/moderation/useLabelInfo' import {makeProfileLink} from '#/lib/routes/links' +import {useGate} from '#/lib/statsig/statsig' import {sanitizeHandle} from '#/lib/strings/handles' import {logger} from '#/logger' import {useAgent, useSession} from '#/state/session' @@ -201,22 +202,42 @@ function AppealForm({ const [details, setDetails] = React.useState('') const isAccountReport = 'did' in subject const agent = useAgent() + const gate = useGate() const {mutate, isPending} = useMutation({ mutationFn: async () => { const $type = !isAccountReport ? 'com.atproto.repo.strongRef' : 'com.atproto.admin.defs#repoRef' - await agent - .withProxy('atproto_labeler', label.src) - .createModerationReport({ - reasonType: ComAtprotoModerationDefs.REASONAPPEAL, - subject: { - $type, - ...subject, + if (gate('session_withproxy_fix')) { + await agent.createModerationReport( + { + reasonType: ComAtprotoModerationDefs.REASONAPPEAL, + subject: { + $type, + ...subject, + }, + reason: details, }, - reason: details, - }) + { + encoding: 'application/json', + headers: { + 'atproto-proxy': `${label.src}#atproto_labeler`, + }, + }, + ) + } else { + await agent + .withProxy('atproto_labeler', label.src) + .createModerationReport({ + reasonType: ComAtprotoModerationDefs.REASONAPPEAL, + subject: { + $type, + ...subject, + }, + reason: details, + }) + } }, onError: err => { logger.error('Failed to submit label appeal', {message: err}) diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts index 1c86d01da4..78e440d043 100644 --- a/src/lib/statsig/gates.ts +++ b/src/lib/statsig/gates.ts @@ -7,6 +7,7 @@ export type Gate = | 'new_user_progress_guide' | 'onboarding_minimum_interests' | 'request_notifications_permission_after_onboarding_v2' + | 'session_withproxy_fix' | 'show_avi_follow_button' | 'show_follow_back_label_v2' | 'suggested_feeds_interstitial' diff --git a/src/state/feed-feedback.tsx b/src/state/feed-feedback.tsx index 0a6c1d585e..59b4bf78a4 100644 --- a/src/state/feed-feedback.tsx +++ b/src/state/feed-feedback.tsx @@ -5,6 +5,7 @@ import throttle from 'lodash.throttle' import {PROD_DEFAULT_FEED} from '#/lib/constants' import {logEvent} from '#/lib/statsig/statsig' +import {useGate} from '#/lib/statsig/statsig' import {logger} from '#/logger' import {FeedDescriptor, FeedPostSliceItem} from '#/state/queries/post-feed' import {getFeedPostSlice} from '#/view/com/posts/Feed' @@ -24,6 +25,7 @@ const stateContext = React.createContext({ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) { const agent = useAgent() + const gate = useGate() const enabled = isDiscoverFeed(feed) && hasSession const queue = React.useRef>(new Set()) const history = React.useRef< @@ -43,22 +45,38 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) { ) const sendToFeedNoDelay = React.useCallback(() => { - const proxyAgent = agent.withProxy( - // @ts-ignore TODO need to update withProxy() to support this key -prf - 'bsky_fg', - // TODO when we start sending to other feeds, we need to grab their DID -prf - 'did:web:discover.bsky.app', - ) as BskyAgent - const interactions = Array.from(queue.current).map(toInteraction) queue.current.clear() // Send to the feed - proxyAgent.app.bsky.feed - .sendInteractions({interactions}) - .catch((e: any) => { - logger.warn('Failed to send feed interactions', {error: e}) - }) + if (gate('session_withproxy_fix')) { + agent.app.bsky.feed + .sendInteractions( + {interactions}, + { + encoding: 'application/json', + headers: { + // TODO when we start sending to other feeds, we need to grab their DID -prf + 'atproto-proxy': 'did:web:discover.bsky.app#bsky_fg', + }, + }, + ) + .catch((e: any) => { + logger.warn('Failed to send feed interactions', {error: e}) + }) + } else { + const proxyAgent = agent.withProxy( + // @ts-ignore TODO need to update withProxy() to support this key -prf + 'bsky_fg', + // TODO when we start sending to other feeds, we need to grab their DID -prf + 'did:web:discover.bsky.app', + ) as BskyAgent + proxyAgent.app.bsky.feed + .sendInteractions({interactions}) + .catch((e: any) => { + logger.warn('Failed to send feed interactions', {error: e}) + }) + } // Send to Statsig if (aggregatedStats.current === null) { @@ -66,7 +84,7 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) { } sendOrAggregateInteractionsForStats(aggregatedStats.current, interactions) throttledFlushAggregatedStats() - }, [agent, throttledFlushAggregatedStats]) + }, [agent, gate, throttledFlushAggregatedStats]) const sendToFeed = React.useMemo( () => From fa64d9393b9ab2cb420c900b2b843f48cf647807 Mon Sep 17 00:00:00 2001 From: dan Date: Wed, 10 Jul 2024 00:51:12 +0100 Subject: [PATCH 10/22] [Session] Include stack with the debug event (#4763) --- src/state/session/logging.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/state/session/logging.ts b/src/state/session/logging.ts index 9a703957d6..b57f1fa0b0 100644 --- a/src/state/session/logging.ts +++ b/src/state/session/logging.ts @@ -76,7 +76,12 @@ export function addSessionErrorLog(did: string, event: AtpSessionEvent) { if (!Statsig.initializeCalled() || !Statsig.getStableID()) { return } - Statsig.logEvent('session:error', null, {did, event}) + const stack = (new Error().stack ?? '').slice(0, MAX_SLICE_LENGTH) + Statsig.logEvent('session:error', null, { + did, + event, + stack, + }) } catch (e) { console.error(e) } From 65b5d31b581ad9f6c46a76175643437cd113903b Mon Sep 17 00:00:00 2001 From: Hailey Date: Wed, 10 Jul 2024 12:25:44 -0700 Subject: [PATCH 11/22] Fix alt text for GIFs (#4766) * fix alt text dialog for gifs * only apply to native --- src/view/com/composer/GifAltText.tsx | 9 +++++++-- src/view/com/util/post-embeds/GifEmbed.tsx | 13 +++++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/view/com/composer/GifAltText.tsx b/src/view/com/composer/GifAltText.tsx index b1f10bf2fc..b7690e1023 100644 --- a/src/view/com/composer/GifAltText.tsx +++ b/src/view/com/composer/GifAltText.tsx @@ -174,8 +174,13 @@ function AltTextInner({ Add alt text - - + + diff --git a/src/view/com/util/post-embeds/GifEmbed.tsx b/src/view/com/util/post-embeds/GifEmbed.tsx index 5e3a69391b..31c4419b50 100644 --- a/src/view/com/util/post-embeds/GifEmbed.tsx +++ b/src/view/com/util/post-embeds/GifEmbed.tsx @@ -1,5 +1,12 @@ import React from 'react' -import {Pressable, StyleSheet, TouchableOpacity, View} from 'react-native' +import { + Pressable, + StyleProp, + StyleSheet, + TouchableOpacity, + View, + ViewStyle, +} from 'react-native' import {AppBskyEmbedExternal} from '@atproto/api' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {msg, Trans} from '@lingui/macro' @@ -89,10 +96,12 @@ export function GifEmbed({ params, link, hideAlt, + style = {width: '100%'}, }: { params: EmbedPlayerParams link: AppBskyEmbedExternal.ViewExternal hideAlt?: boolean + style?: StyleProp }) { const {_} = useLingui() const autoplayDisabled = useAutoplayDisabled() @@ -124,7 +133,7 @@ export function GifEmbed({ ) return ( - + Date: Wed, 10 Jul 2024 14:48:34 -0700 Subject: [PATCH 12/22] ALF post button (#4767) --- src/view/com/composer/Composer.tsx | 73 ++++++++++++++---------------- 1 file changed, 35 insertions(+), 38 deletions(-) diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index c8a77385ea..eed5cb6c54 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -14,7 +14,6 @@ import { KeyboardAvoidingView, LayoutChangeEvent, StyleSheet, - TouchableOpacity, View, } from 'react-native' import Animated, { @@ -24,7 +23,6 @@ import Animated, { withTiming, } from 'react-native-reanimated' import {useSafeAreaInsets} from 'react-native-safe-area-context' -import {LinearGradient} from 'expo-linear-gradient' import { AppBskyFeedDefs, AppBskyFeedGetPostThread, @@ -61,21 +59,21 @@ import {useAgent, useSession} from '#/state/session' import {useComposerControls} from '#/state/shell/composer' import {useAnalytics} from 'lib/analytics/analytics' import * as apilib from 'lib/api/index' -import {HITSLOP_10, MAX_GRAPHEME_LENGTH} from 'lib/constants' +import {MAX_GRAPHEME_LENGTH} from 'lib/constants' import {useIsKeyboardVisible} from 'lib/hooks/useIsKeyboardVisible' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {cleanError} from 'lib/strings/errors' import {insertMentionAt} from 'lib/strings/mention-manip' import {shortenLinks} from 'lib/strings/rich-text-manip' -import {colors, gradients, s} from 'lib/styles' +import {colors, s} from 'lib/styles' import {isAndroid, isIOS, isNative, isWeb} from 'platform/detection' import {useDialogStateControlContext} from 'state/dialogs' import {GalleryModel} from 'state/models/media/gallery' import {ComposerOpts} from 'state/shell/composer' import {ComposerReplyTo} from 'view/com/composer/ComposerReplyTo' import {atoms as a, useTheme} from '#/alf' -import {Button} from '#/components/Button' +import {Button, ButtonText} from '#/components/Button' import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji' import * as Prompt from '#/components/Prompt' import {QuoteEmbed, QuoteX} from '../util/post-embeds/QuoteEmbed' @@ -458,20 +456,25 @@ export const ComposePost = observer(function ComposePost({ - - + )}> + Cancel - - + + {isProcessing ? ( <> @@ -488,31 +491,25 @@ export const ComposePost = observer(function ComposePost({ hasMedia={hasMedia} /> {canPost ? ( - - - - {replyTo ? ( - Reply - ) : ( - Post - )} - - - + variant="solid" + color="primary" + shape="default" + size="small" + style={[a.rounded_full, a.py_sm]} + onPress={onPressPublish}> + + {replyTo ? ( + Reply + ) : ( + Post + )} + + ) : ( @@ -828,7 +825,7 @@ const styles = StyleSheet.create({ topbarInner: { flexDirection: 'row', alignItems: 'center', - paddingHorizontal: 16, + paddingHorizontal: 8, height: 54, gap: 4, }, From ea0586cd67427ce68f867ee25e03bb92169f23c3 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 11 Jul 2024 15:20:22 -0500 Subject: [PATCH 13/22] Filter yourself out of follows interstitial (#4770) --- src/components/FeedInterstitials.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/FeedInterstitials.tsx b/src/components/FeedInterstitials.tsx index 243db0a491..53c1fac0b9 100644 --- a/src/components/FeedInterstitials.tsx +++ b/src/components/FeedInterstitials.tsx @@ -13,6 +13,7 @@ import {logger} from '#/logger' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useGetPopularFeedsQuery} from '#/state/queries/feed' import {useProfilesQuery} from '#/state/queries/profile' +import {useSession} from '#/state/session' import {useProgressGuide} from '#/state/shell/progress-guide' import * as userActionHistory from '#/state/userActionHistory' import {SeenPost} from '#/state/userActionHistory' @@ -127,6 +128,7 @@ function sortSeenPosts(postA: SeenPost, postB: SeenPost): 0 | 1 | -1 { } function useExperimentalSuggestedUsersQuery() { + const {currentAccount} = useSession() const userActionSnapshot = userActionHistory.useActionHistorySnapshot() const dids = React.useMemo(() => { const {likes, follows, seen} = userActionSnapshot @@ -138,8 +140,10 @@ function useExperimentalSuggestedUsersQuery() { .sort(sortSeenPosts) .map(l => new AtUri(l.uri)) .map(uri => uri.host) - return [...new Set([...likeDids, ...seenDids])] - }, [userActionSnapshot]) + return [...new Set([...likeDids, ...seenDids])].filter( + did => did !== currentAccount?.did, + ) + }, [userActionSnapshot, currentAccount]) const {data, isLoading, error} = useProfilesQuery({ handles: dids.slice(0, 16), }) From 74186950b2d7f3f5829e3d5bb8919c4a43293ba8 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 11 Jul 2024 16:59:12 -0500 Subject: [PATCH 14/22] [ALF] Theme & palette cleanup (#4769) * Invert primary scale * Invert negative palette * Replace theme specific styles in Toggle * Remove theme specific colors from Button, improves secondary solid on dark mode * TextField * Remove from MessageItem * Threadgate editor * IconCircle * Muted words * Generate themes from hues * Cleanup * Deprecate more values, fix circular import * Invert positive too, hardly use * Button tweaks, some theme diffs * Match disabled state for negative button * Fix unread noty bg --- src/alf/index.tsx | 21 +- src/alf/themes.ts | 1016 +++++++++++-------- src/alf/tokens.ts | 78 -- src/alf/types.ts | 172 +++- src/alf/util/colorGeneration.ts | 4 + src/alf/util/themeSelector.ts | 2 +- src/alf/util/useColorModeTheme.ts | 3 +- src/components/Button.tsx | 62 +- src/components/IconCircle.tsx | 11 +- src/components/dialogs/MutedWords.tsx | 7 +- src/components/dialogs/ThreadgateEditor.tsx | 5 +- src/components/dms/MessageItem.tsx | 10 +- src/components/forms/TextField.tsx | 9 +- src/components/forms/Toggle.tsx | 21 +- src/components/icons/common.tsx | 5 +- src/lib/ThemeContext.tsx | 5 +- src/lib/themes.ts | 14 +- src/view/screens/Storybook/Buttons.tsx | 50 +- src/view/screens/Storybook/Forms.tsx | 10 + 19 files changed, 851 insertions(+), 654 deletions(-) diff --git a/src/alf/index.tsx b/src/alf/index.tsx index 75ea28eb0b..ade2ce1451 100644 --- a/src/alf/index.tsx +++ b/src/alf/index.tsx @@ -1,7 +1,9 @@ import React from 'react' import {Dimensions} from 'react-native' -import * as themes from '#/alf/themes' +import {createThemes, defaultTheme} from '#/alf/themes' +import {Theme, ThemeName} from '#/alf/types' +import {BLUE_HUE, GREEN_HUE, RED_HUE} from '#/alf/util/colorGeneration' export {atoms} from '#/alf/atoms' export * as tokens from '#/alf/tokens' @@ -39,8 +41,8 @@ function getActiveBreakpoints({width}: {width: number}) { * Context */ export const Context = React.createContext<{ - themeName: themes.ThemeName - theme: themes.Theme + themeName: ThemeName + theme: Theme breakpoints: { active: BreakpointName | undefined gtPhone: boolean @@ -49,7 +51,7 @@ export const Context = React.createContext<{ } }>({ themeName: 'light', - theme: themes.light, + theme: defaultTheme, breakpoints: { active: undefined, gtPhone: false, @@ -61,7 +63,16 @@ export const Context = React.createContext<{ export function ThemeProvider({ children, theme: themeName, -}: React.PropsWithChildren<{theme: themes.ThemeName}>) { +}: React.PropsWithChildren<{theme: ThemeName}>) { + const themes = React.useMemo(() => { + return createThemes({ + hues: { + primary: BLUE_HUE, + negative: RED_HUE, + positive: GREEN_HUE, + }, + }) + }, []) const theme = themes[themeName] const [breakpoints, setBreakpoints] = React.useState(() => getActiveBreakpoints({width: Dimensions.get('window').width}), diff --git a/src/alf/themes.ts b/src/alf/themes.ts index acfd269f8c..b430fa65e1 100644 --- a/src/alf/themes.ts +++ b/src/alf/themes.ts @@ -1,456 +1,578 @@ import {atoms} from '#/alf/atoms' -import * as tokens from '#/alf/tokens' -import type {Mutable} from '#/alf/types' -import {BLUE_HUE, GREEN_HUE, RED_HUE} from '#/alf/util/colorGeneration' +import {Palette, Theme} from '#/alf/types' +import { + BLUE_HUE, + defaultScale, + dimScale, + GREEN_HUE, + RED_HUE, +} from '#/alf/util/colorGeneration' -export type ThemeName = 'light' | 'dim' | 'dark' -export type ReadonlyTheme = typeof light -export type Theme = Mutable & {name: ThemeName} -export type ReadonlyPalette = typeof lightPalette -export type Palette = Mutable - -export const lightPalette = { - white: tokens.color.gray_0, - black: tokens.color.gray_1000, - - contrast_25: tokens.color.gray_25, - contrast_50: tokens.color.gray_50, - contrast_100: tokens.color.gray_100, - contrast_200: tokens.color.gray_200, - contrast_300: tokens.color.gray_300, - contrast_400: tokens.color.gray_400, - contrast_500: tokens.color.gray_500, - contrast_600: tokens.color.gray_600, - contrast_700: tokens.color.gray_700, - contrast_800: tokens.color.gray_800, - contrast_900: tokens.color.gray_900, - contrast_950: tokens.color.gray_950, - contrast_975: tokens.color.gray_975, - - primary_25: tokens.color.blue_25, - primary_50: tokens.color.blue_50, - primary_100: tokens.color.blue_100, - primary_200: tokens.color.blue_200, - primary_300: tokens.color.blue_300, - primary_400: tokens.color.blue_400, - primary_500: tokens.color.blue_500, - primary_600: tokens.color.blue_600, - primary_700: tokens.color.blue_700, - primary_800: tokens.color.blue_800, - primary_900: tokens.color.blue_900, - primary_950: tokens.color.blue_950, - primary_975: tokens.color.blue_975, - - positive_25: tokens.color.green_25, - positive_50: tokens.color.green_50, - positive_100: tokens.color.green_100, - positive_200: tokens.color.green_200, - positive_300: tokens.color.green_300, - positive_400: tokens.color.green_400, - positive_500: tokens.color.green_500, - positive_600: tokens.color.green_600, - positive_700: tokens.color.green_700, - positive_800: tokens.color.green_800, - positive_900: tokens.color.green_900, - positive_950: tokens.color.green_950, - positive_975: tokens.color.green_975, - - negative_25: tokens.color.red_25, - negative_50: tokens.color.red_50, - negative_100: tokens.color.red_100, - negative_200: tokens.color.red_200, - negative_300: tokens.color.red_300, - negative_400: tokens.color.red_400, - negative_500: tokens.color.red_500, - negative_600: tokens.color.red_600, - negative_700: tokens.color.red_700, - negative_800: tokens.color.red_800, - negative_900: tokens.color.red_900, - negative_950: tokens.color.red_950, - negative_975: tokens.color.red_975, -} as const - -export const darkPalette: Palette = { - white: tokens.color.gray_0, - black: tokens.color.trueBlack, - - contrast_25: tokens.color.gray_1000, - contrast_50: tokens.color.gray_975, - contrast_100: tokens.color.gray_950, - contrast_200: tokens.color.gray_900, - contrast_300: tokens.color.gray_800, - contrast_400: tokens.color.gray_700, - contrast_500: tokens.color.gray_600, - contrast_600: tokens.color.gray_500, - contrast_700: tokens.color.gray_400, - contrast_800: tokens.color.gray_300, - contrast_900: tokens.color.gray_200, - contrast_950: tokens.color.gray_100, - contrast_975: tokens.color.gray_50, - - primary_25: tokens.color.blue_25, - primary_50: tokens.color.blue_50, - primary_100: tokens.color.blue_100, - primary_200: tokens.color.blue_200, - primary_300: tokens.color.blue_300, - primary_400: tokens.color.blue_400, - primary_500: tokens.color.blue_500, - primary_600: tokens.color.blue_600, - primary_700: tokens.color.blue_700, - primary_800: tokens.color.blue_800, - primary_900: tokens.color.blue_900, - primary_950: tokens.color.blue_950, - primary_975: tokens.color.blue_975, - - positive_25: tokens.color.green_25, - positive_50: tokens.color.green_50, - positive_100: tokens.color.green_100, - positive_200: tokens.color.green_200, - positive_300: tokens.color.green_300, - positive_400: tokens.color.green_400, - positive_500: tokens.color.green_500, - positive_600: tokens.color.green_600, - positive_700: tokens.color.green_700, - positive_800: tokens.color.green_800, - positive_900: tokens.color.green_900, - positive_950: tokens.color.green_950, - positive_975: tokens.color.green_975, - - negative_25: tokens.color.red_25, - negative_50: tokens.color.red_50, - negative_100: tokens.color.red_100, - negative_200: tokens.color.red_200, - negative_300: tokens.color.red_300, - negative_400: tokens.color.red_400, - negative_500: tokens.color.red_500, - negative_600: tokens.color.red_600, - negative_700: tokens.color.red_700, - negative_800: tokens.color.red_800, - negative_900: tokens.color.red_900, - negative_950: tokens.color.red_950, - negative_975: tokens.color.red_975, -} as const - -export const dimPalette: Palette = { - ...darkPalette, - black: `hsl(${BLUE_HUE}, 28%, ${tokens.dimScale[0]}%)`, - - contrast_25: `hsl(${BLUE_HUE}, 28%, ${tokens.dimScale[1]}%)`, - contrast_50: `hsl(${BLUE_HUE}, 28%, ${tokens.dimScale[2]}%)`, - contrast_100: `hsl(${BLUE_HUE}, 28%, ${tokens.dimScale[3]}%)`, - contrast_200: `hsl(${BLUE_HUE}, 28%, ${tokens.dimScale[4]}%)`, - contrast_300: `hsl(${BLUE_HUE}, 24%, ${tokens.dimScale[5]}%)`, - contrast_400: `hsl(${BLUE_HUE}, 24%, ${tokens.dimScale[6]}%)`, - contrast_500: `hsl(${BLUE_HUE}, 20%, ${tokens.dimScale[7]}%)`, - contrast_600: `hsl(${BLUE_HUE}, 20%, ${tokens.dimScale[8]}%)`, - contrast_700: `hsl(${BLUE_HUE}, 20%, ${tokens.dimScale[9]}%)`, - contrast_800: `hsl(${BLUE_HUE}, 20%, ${tokens.dimScale[10]}%)`, - contrast_900: `hsl(${BLUE_HUE}, 20%, ${tokens.dimScale[11]}%)`, - contrast_950: `hsl(${BLUE_HUE}, 20%, ${tokens.dimScale[12]}%)`, - contrast_975: `hsl(${BLUE_HUE}, 20%, ${tokens.dimScale[13]}%)`, - - primary_25: `hsl(${BLUE_HUE}, 99%, ${tokens.dimScale[13]}%)`, - primary_50: `hsl(${BLUE_HUE}, 99%, ${tokens.dimScale[12]}%)`, - primary_100: `hsl(${BLUE_HUE}, 99%, ${tokens.dimScale[11]}%)`, - primary_200: `hsl(${BLUE_HUE}, 99%, ${tokens.dimScale[10]}%)`, - primary_300: `hsl(${BLUE_HUE}, 99%, ${tokens.dimScale[9]}%)`, - primary_400: `hsl(${BLUE_HUE}, 99%, ${tokens.dimScale[8]}%)`, - primary_500: `hsl(${BLUE_HUE}, 99%, ${tokens.dimScale[7]}%)`, - primary_600: `hsl(${BLUE_HUE}, 95%, ${tokens.dimScale[6]}%)`, - primary_700: `hsl(${BLUE_HUE}, 90%, ${tokens.dimScale[5]}%)`, - primary_800: `hsl(${BLUE_HUE}, 82%, ${tokens.dimScale[4]}%)`, - primary_900: `hsl(${BLUE_HUE}, 70%, ${tokens.dimScale[3]}%)`, - primary_950: `hsl(${BLUE_HUE}, 60%, ${tokens.dimScale[2]}%)`, - primary_975: `hsl(${BLUE_HUE}, 50%, ${tokens.dimScale[1]}%)`, - - positive_25: `hsl(${GREEN_HUE}, 82%, ${tokens.dimScale[13]}%)`, - positive_50: `hsl(${GREEN_HUE}, 82%, ${tokens.dimScale[12]}%)`, - positive_100: `hsl(${GREEN_HUE}, 82%, ${tokens.dimScale[11]}%)`, - positive_200: `hsl(${GREEN_HUE}, 82%, ${tokens.dimScale[10]}%)`, - positive_300: `hsl(${GREEN_HUE}, 82%, ${tokens.dimScale[9]}%)`, - positive_400: `hsl(${GREEN_HUE}, 82%, ${tokens.dimScale[8]}%)`, - positive_500: `hsl(${GREEN_HUE}, 82%, ${tokens.dimScale[7]}%)`, - positive_600: `hsl(${GREEN_HUE}, 82%, ${tokens.dimScale[6]}%)`, - positive_700: `hsl(${GREEN_HUE}, 82%, ${tokens.dimScale[5]}%)`, - positive_800: `hsl(${GREEN_HUE}, 82%, ${tokens.dimScale[4]}%)`, - positive_900: `hsl(${GREEN_HUE}, 70%, ${tokens.dimScale[3]}%)`, - positive_950: `hsl(${GREEN_HUE}, 60%, ${tokens.dimScale[2]}%)`, - positive_975: `hsl(${GREEN_HUE}, 50%, ${tokens.dimScale[1]}%)`, - - negative_25: `hsl(${RED_HUE}, 91%, ${tokens.dimScale[13]}%)`, - negative_50: `hsl(${RED_HUE}, 91%, ${tokens.dimScale[12]}%)`, - negative_100: `hsl(${RED_HUE}, 91%, ${tokens.dimScale[11]}%)`, - negative_200: `hsl(${RED_HUE}, 91%, ${tokens.dimScale[10]}%)`, - negative_300: `hsl(${RED_HUE}, 91%, ${tokens.dimScale[9]}%)`, - negative_400: `hsl(${RED_HUE}, 91%, ${tokens.dimScale[8]}%)`, - negative_500: `hsl(${RED_HUE}, 91%, ${tokens.dimScale[7]}%)`, - negative_600: `hsl(${RED_HUE}, 91%, ${tokens.dimScale[6]}%)`, - negative_700: `hsl(${RED_HUE}, 91%, ${tokens.dimScale[5]}%)`, - negative_800: `hsl(${RED_HUE}, 88%, ${tokens.dimScale[4]}%)`, - negative_900: `hsl(${RED_HUE}, 84%, ${tokens.dimScale[3]}%)`, - negative_950: `hsl(${RED_HUE}, 80%, ${tokens.dimScale[2]}%)`, - negative_975: `hsl(${RED_HUE}, 70%, ${tokens.dimScale[1]}%)`, -} as const - -export const light = { - name: 'light' as ThemeName, - palette: lightPalette, - atoms: { - text: { - color: lightPalette.black, - }, - text_contrast_low: { - color: lightPalette.contrast_400, - }, - text_contrast_medium: { - color: lightPalette.contrast_700, - }, - text_contrast_high: { - color: lightPalette.contrast_900, - }, - text_inverted: { - color: lightPalette.white, - }, - bg: { - backgroundColor: lightPalette.white, - }, - bg_contrast_25: { - backgroundColor: lightPalette.contrast_25, - }, - bg_contrast_50: { - backgroundColor: lightPalette.contrast_50, - }, - bg_contrast_100: { - backgroundColor: lightPalette.contrast_100, - }, - bg_contrast_200: { - backgroundColor: lightPalette.contrast_200, - }, - bg_contrast_300: { - backgroundColor: lightPalette.contrast_300, - }, - bg_contrast_400: { - backgroundColor: lightPalette.contrast_400, - }, - bg_contrast_500: { - backgroundColor: lightPalette.contrast_500, - }, - bg_contrast_600: { - backgroundColor: lightPalette.contrast_600, - }, - bg_contrast_700: { - backgroundColor: lightPalette.contrast_700, - }, - bg_contrast_800: { - backgroundColor: lightPalette.contrast_800, - }, - bg_contrast_900: { - backgroundColor: lightPalette.contrast_900, - }, - bg_contrast_950: { - backgroundColor: lightPalette.contrast_950, - }, - bg_contrast_975: { - backgroundColor: lightPalette.contrast_975, - }, - border_contrast_low: { - borderColor: lightPalette.contrast_100, - }, - border_contrast_medium: { - borderColor: lightPalette.contrast_200, - }, - border_contrast_high: { - borderColor: lightPalette.contrast_300, - }, - shadow_sm: { - ...atoms.shadow_sm, - shadowColor: lightPalette.black, - }, - shadow_md: { - ...atoms.shadow_md, - shadowColor: lightPalette.black, - }, - shadow_lg: { - ...atoms.shadow_lg, - shadowColor: lightPalette.black, - }, +const themes = createThemes({ + hues: { + primary: BLUE_HUE, + negative: RED_HUE, + positive: GREEN_HUE, }, -} +}) -export const dark: Theme = { - name: 'dark' as ThemeName, - palette: darkPalette, - atoms: { - text: { - color: darkPalette.white, - }, - text_contrast_low: { - color: darkPalette.contrast_400, - }, - text_contrast_medium: { - color: darkPalette.contrast_700, - }, - text_contrast_high: { - color: darkPalette.contrast_900, - }, - text_inverted: { - color: darkPalette.black, - }, - bg: { - backgroundColor: darkPalette.black, - }, - bg_contrast_25: { - backgroundColor: darkPalette.contrast_25, - }, - bg_contrast_50: { - backgroundColor: darkPalette.contrast_50, - }, - bg_contrast_100: { - backgroundColor: darkPalette.contrast_100, - }, - bg_contrast_200: { - backgroundColor: darkPalette.contrast_200, - }, - bg_contrast_300: { - backgroundColor: darkPalette.contrast_300, - }, - bg_contrast_400: { - backgroundColor: darkPalette.contrast_400, - }, - bg_contrast_500: { - backgroundColor: darkPalette.contrast_500, - }, - bg_contrast_600: { - backgroundColor: darkPalette.contrast_600, - }, - bg_contrast_700: { - backgroundColor: darkPalette.contrast_700, - }, - bg_contrast_800: { - backgroundColor: darkPalette.contrast_800, - }, - bg_contrast_900: { - backgroundColor: darkPalette.contrast_900, - }, - bg_contrast_950: { - backgroundColor: darkPalette.contrast_950, - }, - bg_contrast_975: { - backgroundColor: darkPalette.contrast_975, - }, - border_contrast_low: { - borderColor: darkPalette.contrast_100, - }, - border_contrast_medium: { - borderColor: darkPalette.contrast_200, - }, - border_contrast_high: { - borderColor: darkPalette.contrast_300, - }, - shadow_sm: { - ...atoms.shadow_sm, - shadowOpacity: 0.7, - shadowColor: tokens.color.trueBlack, - }, - shadow_md: { - ...atoms.shadow_md, - shadowOpacity: 0.7, - shadowColor: tokens.color.trueBlack, - }, - shadow_lg: { - ...atoms.shadow_lg, - shadowOpacity: 0.7, - shadowColor: tokens.color.trueBlack, - }, - }, -} +/** + * @deprecated use ALF and access palette from `useTheme()` + */ +export const lightPalette = themes.lightPalette +/** + * @deprecated use ALF and access palette from `useTheme()` + */ +export const darkPalette = themes.darkPalette +/** + * @deprecated use ALF and access palette from `useTheme()` + */ +export const dimPalette = themes.dimPalette +/** + * @deprecated use ALF and access theme from `useTheme()` + */ +export const light = themes.light +/** + * @deprecated use ALF and access theme from `useTheme()` + */ +export const dark = themes.dark +/** + * @deprecated use ALF and access theme from `useTheme()` + */ +export const dim = themes.dim -export const dim: Theme = { - ...dark, - name: 'dim' as ThemeName, - palette: dimPalette, - atoms: { - ...dark.atoms, - text: { - color: dimPalette.white, +export const defaultTheme = themes.light + +export function createThemes({ + hues, +}: { + hues: { + primary: number + negative: number + positive: number + } +}): { + lightPalette: Palette + darkPalette: Palette + dimPalette: Palette + light: Theme + dark: Theme + dim: Theme +} { + const color = { + trueBlack: '#000000', + + gray_0: `hsl(${hues.primary}, 20%, ${defaultScale[14]}%)`, + gray_25: `hsl(${hues.primary}, 20%, ${defaultScale[13]}%)`, + gray_50: `hsl(${hues.primary}, 20%, ${defaultScale[12]}%)`, + gray_100: `hsl(${hues.primary}, 20%, ${defaultScale[11]}%)`, + gray_200: `hsl(${hues.primary}, 20%, ${defaultScale[10]}%)`, + gray_300: `hsl(${hues.primary}, 20%, ${defaultScale[9]}%)`, + gray_400: `hsl(${hues.primary}, 20%, ${defaultScale[8]}%)`, + gray_500: `hsl(${hues.primary}, 20%, ${defaultScale[7]}%)`, + gray_600: `hsl(${hues.primary}, 24%, ${defaultScale[6]}%)`, + gray_700: `hsl(${hues.primary}, 24%, ${defaultScale[5]}%)`, + gray_800: `hsl(${hues.primary}, 28%, ${defaultScale[4]}%)`, + gray_900: `hsl(${hues.primary}, 28%, ${defaultScale[3]}%)`, + gray_950: `hsl(${hues.primary}, 28%, ${defaultScale[2]}%)`, + gray_975: `hsl(${hues.primary}, 28%, ${defaultScale[1]}%)`, + gray_1000: `hsl(${hues.primary}, 28%, ${defaultScale[0]}%)`, + + primary_25: `hsl(${hues.primary}, 99%, 97%)`, + primary_50: `hsl(${hues.primary}, 99%, 95%)`, + primary_100: `hsl(${hues.primary}, 99%, 90%)`, + primary_200: `hsl(${hues.primary}, 99%, 80%)`, + primary_300: `hsl(${hues.primary}, 99%, 70%)`, + primary_400: `hsl(${hues.primary}, 99%, 60%)`, + primary_500: `hsl(${hues.primary}, 99%, 53%)`, + primary_600: `hsl(${hues.primary}, 99%, 42%)`, + primary_700: `hsl(${hues.primary}, 99%, 34%)`, + primary_800: `hsl(${hues.primary}, 99%, 26%)`, + primary_900: `hsl(${hues.primary}, 99%, 18%)`, + primary_950: `hsl(${hues.primary}, 99%, 10%)`, + primary_975: `hsl(${hues.primary}, 99%, 7%)`, + + green_25: `hsl(${hues.positive}, 82%, 97%)`, + green_50: `hsl(${hues.positive}, 82%, 95%)`, + green_100: `hsl(${hues.positive}, 82%, 90%)`, + green_200: `hsl(${hues.positive}, 82%, 80%)`, + green_300: `hsl(${hues.positive}, 82%, 70%)`, + green_400: `hsl(${hues.positive}, 82%, 60%)`, + green_500: `hsl(${hues.positive}, 82%, 50%)`, + green_600: `hsl(${hues.positive}, 82%, 42%)`, + green_700: `hsl(${hues.positive}, 82%, 34%)`, + green_800: `hsl(${hues.positive}, 82%, 26%)`, + green_900: `hsl(${hues.positive}, 82%, 18%)`, + green_950: `hsl(${hues.positive}, 82%, 10%)`, + green_975: `hsl(${hues.positive}, 82%, 7%)`, + + red_25: `hsl(${hues.negative}, 91%, 97%)`, + red_50: `hsl(${hues.negative}, 91%, 95%)`, + red_100: `hsl(${hues.negative}, 91%, 90%)`, + red_200: `hsl(${hues.negative}, 91%, 80%)`, + red_300: `hsl(${hues.negative}, 91%, 70%)`, + red_400: `hsl(${hues.negative}, 91%, 60%)`, + red_500: `hsl(${hues.negative}, 91%, 50%)`, + red_600: `hsl(${hues.negative}, 91%, 42%)`, + red_700: `hsl(${hues.negative}, 91%, 34%)`, + red_800: `hsl(${hues.negative}, 91%, 26%)`, + red_900: `hsl(${hues.negative}, 91%, 18%)`, + red_950: `hsl(${hues.negative}, 91%, 10%)`, + red_975: `hsl(${hues.negative}, 91%, 7%)`, + } as const + + const lightPalette = { + white: color.gray_0, + black: color.gray_1000, + + contrast_25: color.gray_25, + contrast_50: color.gray_50, + contrast_100: color.gray_100, + contrast_200: color.gray_200, + contrast_300: color.gray_300, + contrast_400: color.gray_400, + contrast_500: color.gray_500, + contrast_600: color.gray_600, + contrast_700: color.gray_700, + contrast_800: color.gray_800, + contrast_900: color.gray_900, + contrast_950: color.gray_950, + contrast_975: color.gray_975, + + primary_25: color.primary_25, + primary_50: color.primary_50, + primary_100: color.primary_100, + primary_200: color.primary_200, + primary_300: color.primary_300, + primary_400: color.primary_400, + primary_500: color.primary_500, + primary_600: color.primary_600, + primary_700: color.primary_700, + primary_800: color.primary_800, + primary_900: color.primary_900, + primary_950: color.primary_950, + primary_975: color.primary_975, + + positive_25: color.green_25, + positive_50: color.green_50, + positive_100: color.green_100, + positive_200: color.green_200, + positive_300: color.green_300, + positive_400: color.green_400, + positive_500: color.green_500, + positive_600: color.green_600, + positive_700: color.green_700, + positive_800: color.green_800, + positive_900: color.green_900, + positive_950: color.green_950, + positive_975: color.green_975, + + negative_25: color.red_25, + negative_50: color.red_50, + negative_100: color.red_100, + negative_200: color.red_200, + negative_300: color.red_300, + negative_400: color.red_400, + negative_500: color.red_500, + negative_600: color.red_600, + negative_700: color.red_700, + negative_800: color.red_800, + negative_900: color.red_900, + negative_950: color.red_950, + negative_975: color.red_975, + } as const + + const darkPalette: Palette = { + white: color.gray_0, + black: color.trueBlack, + + contrast_25: color.gray_1000, + contrast_50: color.gray_975, + contrast_100: color.gray_950, + contrast_200: color.gray_900, + contrast_300: color.gray_800, + contrast_400: color.gray_700, + contrast_500: color.gray_600, + contrast_600: color.gray_500, + contrast_700: color.gray_400, + contrast_800: color.gray_300, + contrast_900: color.gray_200, + contrast_950: color.gray_100, + contrast_975: color.gray_50, + + primary_25: color.primary_975, + primary_50: color.primary_950, + primary_100: color.primary_900, + primary_200: color.primary_800, + primary_300: color.primary_700, + primary_400: color.primary_600, + primary_500: color.primary_500, + primary_600: color.primary_400, + primary_700: color.primary_300, + primary_800: color.primary_200, + primary_900: color.primary_100, + primary_950: color.primary_50, + primary_975: color.primary_25, + + positive_25: color.green_975, + positive_50: color.green_950, + positive_100: color.green_900, + positive_200: color.green_800, + positive_300: color.green_700, + positive_400: color.green_600, + positive_500: color.green_500, + positive_600: color.green_400, + positive_700: color.green_300, + positive_800: color.green_200, + positive_900: color.green_100, + positive_950: color.green_50, + positive_975: color.green_25, + + negative_25: color.red_975, + negative_50: color.red_950, + negative_100: color.red_900, + negative_200: color.red_800, + negative_300: color.red_700, + negative_400: color.red_600, + negative_500: color.red_500, + negative_600: color.red_400, + negative_700: color.red_300, + negative_800: color.red_200, + negative_900: color.red_100, + negative_950: color.red_50, + negative_975: color.red_25, + } as const + + const dimPalette: Palette = { + ...darkPalette, + black: `hsl(${hues.primary}, 28%, ${dimScale[0]}%)`, + + contrast_25: `hsl(${hues.primary}, 28%, ${dimScale[1]}%)`, + contrast_50: `hsl(${hues.primary}, 28%, ${dimScale[2]}%)`, + contrast_100: `hsl(${hues.primary}, 28%, ${dimScale[3]}%)`, + contrast_200: `hsl(${hues.primary}, 28%, ${dimScale[4]}%)`, + contrast_300: `hsl(${hues.primary}, 24%, ${dimScale[5]}%)`, + contrast_400: `hsl(${hues.primary}, 24%, ${dimScale[6]}%)`, + contrast_500: `hsl(${hues.primary}, 20%, ${dimScale[7]}%)`, + contrast_600: `hsl(${hues.primary}, 20%, ${dimScale[8]}%)`, + contrast_700: `hsl(${hues.primary}, 20%, ${dimScale[9]}%)`, + contrast_800: `hsl(${hues.primary}, 20%, ${dimScale[10]}%)`, + contrast_900: `hsl(${hues.primary}, 20%, ${dimScale[11]}%)`, + contrast_950: `hsl(${hues.primary}, 20%, ${dimScale[12]}%)`, + contrast_975: `hsl(${hues.primary}, 20%, ${dimScale[13]}%)`, + + primary_25: `hsl(${hues.primary}, 99%, ${dimScale[1]}%)`, + primary_50: `hsl(${hues.primary}, 99%, ${dimScale[2]}%)`, + primary_100: `hsl(${hues.primary}, 99%, ${dimScale[3]}%)`, + primary_200: `hsl(${hues.primary}, 99%, ${dimScale[4]}%)`, + primary_300: `hsl(${hues.primary}, 99%, ${dimScale[5]}%)`, + primary_400: `hsl(${hues.primary}, 99%, ${dimScale[6]}%)`, + primary_500: `hsl(${hues.primary}, 99%, ${dimScale[7]}%)`, + primary_600: `hsl(${hues.primary}, 95%, ${dimScale[8]}%)`, + primary_700: `hsl(${hues.primary}, 90%, ${dimScale[9]}%)`, + primary_800: `hsl(${hues.primary}, 82%, ${dimScale[10]}%)`, + primary_900: `hsl(${hues.primary}, 70%, ${dimScale[11]}%)`, + primary_950: `hsl(${hues.primary}, 60%, ${dimScale[12]}%)`, + primary_975: `hsl(${hues.primary}, 50%, ${dimScale[13]}%)`, + + positive_25: `hsl(${hues.positive}, 82%, ${dimScale[1]}%)`, + positive_50: `hsl(${hues.positive}, 82%, ${dimScale[2]}%)`, + positive_100: `hsl(${hues.positive}, 82%, ${dimScale[3]}%)`, + positive_200: `hsl(${hues.positive}, 82%, ${dimScale[4]}%)`, + positive_300: `hsl(${hues.positive}, 82%, ${dimScale[5]}%)`, + positive_400: `hsl(${hues.positive}, 82%, ${dimScale[6]}%)`, + positive_500: `hsl(${hues.positive}, 82%, ${dimScale[7]}%)`, + positive_600: `hsl(${hues.positive}, 82%, ${dimScale[8]}%)`, + positive_700: `hsl(${hues.positive}, 82%, ${dimScale[9]}%)`, + positive_800: `hsl(${hues.positive}, 82%, ${dimScale[10]}%)`, + positive_900: `hsl(${hues.positive}, 70%, ${dimScale[11]}%)`, + positive_950: `hsl(${hues.positive}, 60%, ${dimScale[12]}%)`, + positive_975: `hsl(${hues.positive}, 50%, ${dimScale[13]}%)`, + + negative_25: `hsl(${hues.negative}, 91%, ${dimScale[1]}%)`, + negative_50: `hsl(${hues.negative}, 91%, ${dimScale[2]}%)`, + negative_100: `hsl(${hues.negative}, 91%, ${dimScale[3]}%)`, + negative_200: `hsl(${hues.negative}, 91%, ${dimScale[4]}%)`, + negative_300: `hsl(${hues.negative}, 91%, ${dimScale[5]}%)`, + negative_400: `hsl(${hues.negative}, 91%, ${dimScale[6]}%)`, + negative_500: `hsl(${hues.negative}, 91%, ${dimScale[7]}%)`, + negative_600: `hsl(${hues.negative}, 91%, ${dimScale[8]}%)`, + negative_700: `hsl(${hues.negative}, 91%, ${dimScale[9]}%)`, + negative_800: `hsl(${hues.negative}, 88%, ${dimScale[10]}%)`, + negative_900: `hsl(${hues.negative}, 84%, ${dimScale[11]}%)`, + negative_950: `hsl(${hues.negative}, 80%, ${dimScale[12]}%)`, + negative_975: `hsl(${hues.negative}, 70%, ${dimScale[13]}%)`, + } as const + + const light: Theme = { + name: 'light', + palette: lightPalette, + atoms: { + text: { + color: lightPalette.black, + }, + text_contrast_low: { + color: lightPalette.contrast_400, + }, + text_contrast_medium: { + color: lightPalette.contrast_700, + }, + text_contrast_high: { + color: lightPalette.contrast_900, + }, + text_inverted: { + color: lightPalette.white, + }, + bg: { + backgroundColor: lightPalette.white, + }, + bg_contrast_25: { + backgroundColor: lightPalette.contrast_25, + }, + bg_contrast_50: { + backgroundColor: lightPalette.contrast_50, + }, + bg_contrast_100: { + backgroundColor: lightPalette.contrast_100, + }, + bg_contrast_200: { + backgroundColor: lightPalette.contrast_200, + }, + bg_contrast_300: { + backgroundColor: lightPalette.contrast_300, + }, + bg_contrast_400: { + backgroundColor: lightPalette.contrast_400, + }, + bg_contrast_500: { + backgroundColor: lightPalette.contrast_500, + }, + bg_contrast_600: { + backgroundColor: lightPalette.contrast_600, + }, + bg_contrast_700: { + backgroundColor: lightPalette.contrast_700, + }, + bg_contrast_800: { + backgroundColor: lightPalette.contrast_800, + }, + bg_contrast_900: { + backgroundColor: lightPalette.contrast_900, + }, + bg_contrast_950: { + backgroundColor: lightPalette.contrast_950, + }, + bg_contrast_975: { + backgroundColor: lightPalette.contrast_975, + }, + border_contrast_low: { + borderColor: lightPalette.contrast_100, + }, + border_contrast_medium: { + borderColor: lightPalette.contrast_200, + }, + border_contrast_high: { + borderColor: lightPalette.contrast_300, + }, + shadow_sm: { + ...atoms.shadow_sm, + shadowColor: lightPalette.black, + }, + shadow_md: { + ...atoms.shadow_md, + shadowColor: lightPalette.black, + }, + shadow_lg: { + ...atoms.shadow_lg, + shadowColor: lightPalette.black, + }, }, - text_contrast_low: { - color: dimPalette.contrast_400, + } + + const dark: Theme = { + name: 'dark', + palette: darkPalette, + atoms: { + text: { + color: darkPalette.white, + }, + text_contrast_low: { + color: darkPalette.contrast_400, + }, + text_contrast_medium: { + color: darkPalette.contrast_700, + }, + text_contrast_high: { + color: darkPalette.contrast_900, + }, + text_inverted: { + color: darkPalette.black, + }, + bg: { + backgroundColor: darkPalette.black, + }, + bg_contrast_25: { + backgroundColor: darkPalette.contrast_25, + }, + bg_contrast_50: { + backgroundColor: darkPalette.contrast_50, + }, + bg_contrast_100: { + backgroundColor: darkPalette.contrast_100, + }, + bg_contrast_200: { + backgroundColor: darkPalette.contrast_200, + }, + bg_contrast_300: { + backgroundColor: darkPalette.contrast_300, + }, + bg_contrast_400: { + backgroundColor: darkPalette.contrast_400, + }, + bg_contrast_500: { + backgroundColor: darkPalette.contrast_500, + }, + bg_contrast_600: { + backgroundColor: darkPalette.contrast_600, + }, + bg_contrast_700: { + backgroundColor: darkPalette.contrast_700, + }, + bg_contrast_800: { + backgroundColor: darkPalette.contrast_800, + }, + bg_contrast_900: { + backgroundColor: darkPalette.contrast_900, + }, + bg_contrast_950: { + backgroundColor: darkPalette.contrast_950, + }, + bg_contrast_975: { + backgroundColor: darkPalette.contrast_975, + }, + border_contrast_low: { + borderColor: darkPalette.contrast_100, + }, + border_contrast_medium: { + borderColor: darkPalette.contrast_200, + }, + border_contrast_high: { + borderColor: darkPalette.contrast_300, + }, + shadow_sm: { + ...atoms.shadow_sm, + shadowOpacity: 0.7, + shadowColor: color.trueBlack, + }, + shadow_md: { + ...atoms.shadow_md, + shadowOpacity: 0.7, + shadowColor: color.trueBlack, + }, + shadow_lg: { + ...atoms.shadow_lg, + shadowOpacity: 0.7, + shadowColor: color.trueBlack, + }, }, - text_contrast_medium: { - color: dimPalette.contrast_700, + } + + const dim: Theme = { + ...dark, + name: 'dim', + palette: dimPalette, + atoms: { + ...dark.atoms, + text: { + color: dimPalette.white, + }, + text_contrast_low: { + color: dimPalette.contrast_400, + }, + text_contrast_medium: { + color: dimPalette.contrast_700, + }, + text_contrast_high: { + color: dimPalette.contrast_900, + }, + text_inverted: { + color: dimPalette.black, + }, + bg: { + backgroundColor: dimPalette.black, + }, + bg_contrast_25: { + backgroundColor: dimPalette.contrast_25, + }, + bg_contrast_50: { + backgroundColor: dimPalette.contrast_50, + }, + bg_contrast_100: { + backgroundColor: dimPalette.contrast_100, + }, + bg_contrast_200: { + backgroundColor: dimPalette.contrast_200, + }, + bg_contrast_300: { + backgroundColor: dimPalette.contrast_300, + }, + bg_contrast_400: { + backgroundColor: dimPalette.contrast_400, + }, + bg_contrast_500: { + backgroundColor: dimPalette.contrast_500, + }, + bg_contrast_600: { + backgroundColor: dimPalette.contrast_600, + }, + bg_contrast_700: { + backgroundColor: dimPalette.contrast_700, + }, + bg_contrast_800: { + backgroundColor: dimPalette.contrast_800, + }, + bg_contrast_900: { + backgroundColor: dimPalette.contrast_900, + }, + bg_contrast_950: { + backgroundColor: dimPalette.contrast_950, + }, + bg_contrast_975: { + backgroundColor: dimPalette.contrast_975, + }, + border_contrast_low: { + borderColor: dimPalette.contrast_100, + }, + border_contrast_medium: { + borderColor: dimPalette.contrast_200, + }, + border_contrast_high: { + borderColor: dimPalette.contrast_300, + }, + shadow_sm: { + ...atoms.shadow_sm, + shadowOpacity: 0.7, + shadowColor: `hsl(${hues.primary}, 28%, 6%)`, + }, + shadow_md: { + ...atoms.shadow_md, + shadowOpacity: 0.7, + shadowColor: `hsl(${hues.primary}, 28%, 6%)`, + }, + shadow_lg: { + ...atoms.shadow_lg, + shadowOpacity: 0.7, + shadowColor: `hsl(${hues.primary}, 28%, 6%)`, + }, }, - text_contrast_high: { - color: dimPalette.contrast_900, - }, - text_inverted: { - color: dimPalette.black, - }, - bg: { - backgroundColor: dimPalette.black, - }, - bg_contrast_25: { - backgroundColor: dimPalette.contrast_25, - }, - bg_contrast_50: { - backgroundColor: dimPalette.contrast_50, - }, - bg_contrast_100: { - backgroundColor: dimPalette.contrast_100, - }, - bg_contrast_200: { - backgroundColor: dimPalette.contrast_200, - }, - bg_contrast_300: { - backgroundColor: dimPalette.contrast_300, - }, - bg_contrast_400: { - backgroundColor: dimPalette.contrast_400, - }, - bg_contrast_500: { - backgroundColor: dimPalette.contrast_500, - }, - bg_contrast_600: { - backgroundColor: dimPalette.contrast_600, - }, - bg_contrast_700: { - backgroundColor: dimPalette.contrast_700, - }, - bg_contrast_800: { - backgroundColor: dimPalette.contrast_800, - }, - bg_contrast_900: { - backgroundColor: dimPalette.contrast_900, - }, - bg_contrast_950: { - backgroundColor: dimPalette.contrast_950, - }, - bg_contrast_975: { - backgroundColor: dimPalette.contrast_975, - }, - border_contrast_low: { - borderColor: dimPalette.contrast_100, - }, - border_contrast_medium: { - borderColor: dimPalette.contrast_200, - }, - border_contrast_high: { - borderColor: dimPalette.contrast_300, - }, - shadow_sm: { - ...atoms.shadow_sm, - shadowOpacity: 0.7, - shadowColor: `hsl(${BLUE_HUE}, 28%, 6%)`, - }, - shadow_md: { - ...atoms.shadow_md, - shadowOpacity: 0.7, - shadowColor: `hsl(${BLUE_HUE}, 28%, 6%)`, - }, - shadow_lg: { - ...atoms.shadow_lg, - shadowOpacity: 0.7, - shadowColor: `hsl(${BLUE_HUE}, 28%, 6%)`, - }, - }, + } + + return { + lightPalette, + darkPalette, + dimPalette, + light, + dark, + dim, + } } diff --git a/src/alf/tokens.ts b/src/alf/tokens.ts index 675844e296..0208945eeb 100644 --- a/src/alf/tokens.ts +++ b/src/alf/tokens.ts @@ -1,77 +1,6 @@ -import { - BLUE_HUE, - generateScale, - GREEN_HUE, - RED_HUE, -} from '#/alf/util/colorGeneration' - -export const scale = generateScale(6, 100) -// dim shifted 6% lighter -export const dimScale = generateScale(12, 100) - export const color = { - trueBlack: '#000000', - temp_purple: 'rgb(105 0 255)', temp_purple_dark: 'rgb(83 0 202)', - - gray_0: `hsl(${BLUE_HUE}, 20%, ${scale[14]}%)`, - gray_25: `hsl(${BLUE_HUE}, 20%, ${scale[13]}%)`, - gray_50: `hsl(${BLUE_HUE}, 20%, ${scale[12]}%)`, - gray_100: `hsl(${BLUE_HUE}, 20%, ${scale[11]}%)`, - gray_200: `hsl(${BLUE_HUE}, 20%, ${scale[10]}%)`, - gray_300: `hsl(${BLUE_HUE}, 20%, ${scale[9]}%)`, - gray_400: `hsl(${BLUE_HUE}, 20%, ${scale[8]}%)`, - gray_500: `hsl(${BLUE_HUE}, 20%, ${scale[7]}%)`, - gray_600: `hsl(${BLUE_HUE}, 24%, ${scale[6]}%)`, - gray_700: `hsl(${BLUE_HUE}, 24%, ${scale[5]}%)`, - gray_800: `hsl(${BLUE_HUE}, 28%, ${scale[4]}%)`, - gray_900: `hsl(${BLUE_HUE}, 28%, ${scale[3]}%)`, - gray_950: `hsl(${BLUE_HUE}, 28%, ${scale[2]}%)`, - gray_975: `hsl(${BLUE_HUE}, 28%, ${scale[1]}%)`, - gray_1000: `hsl(${BLUE_HUE}, 28%, ${scale[0]}%)`, - - blue_25: `hsl(${BLUE_HUE}, 99%, 97%)`, - blue_50: `hsl(${BLUE_HUE}, 99%, 95%)`, - blue_100: `hsl(${BLUE_HUE}, 99%, 90%)`, - blue_200: `hsl(${BLUE_HUE}, 99%, 80%)`, - blue_300: `hsl(${BLUE_HUE}, 99%, 70%)`, - blue_400: `hsl(${BLUE_HUE}, 99%, 60%)`, - blue_500: `hsl(${BLUE_HUE}, 99%, 53%)`, - blue_600: `hsl(${BLUE_HUE}, 99%, 42%)`, - blue_700: `hsl(${BLUE_HUE}, 99%, 34%)`, - blue_800: `hsl(${BLUE_HUE}, 99%, 26%)`, - blue_900: `hsl(${BLUE_HUE}, 99%, 18%)`, - blue_950: `hsl(${BLUE_HUE}, 99%, 10%)`, - blue_975: `hsl(${BLUE_HUE}, 99%, 7%)`, - - green_25: `hsl(${GREEN_HUE}, 82%, 97%)`, - green_50: `hsl(${GREEN_HUE}, 82%, 95%)`, - green_100: `hsl(${GREEN_HUE}, 82%, 90%)`, - green_200: `hsl(${GREEN_HUE}, 82%, 80%)`, - green_300: `hsl(${GREEN_HUE}, 82%, 70%)`, - green_400: `hsl(${GREEN_HUE}, 82%, 60%)`, - green_500: `hsl(${GREEN_HUE}, 82%, 50%)`, - green_600: `hsl(${GREEN_HUE}, 82%, 42%)`, - green_700: `hsl(${GREEN_HUE}, 82%, 34%)`, - green_800: `hsl(${GREEN_HUE}, 82%, 26%)`, - green_900: `hsl(${GREEN_HUE}, 82%, 18%)`, - green_950: `hsl(${GREEN_HUE}, 82%, 10%)`, - green_975: `hsl(${GREEN_HUE}, 82%, 7%)`, - - red_25: `hsl(${RED_HUE}, 91%, 97%)`, - red_50: `hsl(${RED_HUE}, 91%, 95%)`, - red_100: `hsl(${RED_HUE}, 91%, 90%)`, - red_200: `hsl(${RED_HUE}, 91%, 80%)`, - red_300: `hsl(${RED_HUE}, 91%, 70%)`, - red_400: `hsl(${RED_HUE}, 91%, 60%)`, - red_500: `hsl(${RED_HUE}, 91%, 50%)`, - red_600: `hsl(${RED_HUE}, 91%, 42%)`, - red_700: `hsl(${RED_HUE}, 91%, 34%)`, - red_800: `hsl(${RED_HUE}, 91%, 26%)`, - red_900: `hsl(${RED_HUE}, 91%, 18%)`, - red_950: `hsl(${RED_HUE}, 91%, 10%)`, - red_975: `hsl(${RED_HUE}, 91%, 7%)`, } as const export const space = { @@ -178,10 +107,3 @@ export const gradients = { hover_value: '#755B62', }, } as const - -export type Color = keyof typeof color -export type Space = keyof typeof space -export type FontSize = keyof typeof fontSize -export type LineHeight = keyof typeof lineHeight -export type BorderRadius = keyof typeof borderRadius -export type FontWeight = keyof typeof fontWeight diff --git a/src/alf/types.ts b/src/alf/types.ts index dd8d816d2b..41822b8dd5 100644 --- a/src/alf/types.ts +++ b/src/alf/types.ts @@ -1,21 +1,4 @@ -import {StyleProp, ViewStyle, TextStyle} from 'react-native' - -type LiteralToCommon = T extends number - ? number - : T extends string - ? string - : T extends symbol - ? symbol - : never - -/** - * @see https://stackoverflow.com/questions/68249999/use-as-const-in-typescript-without-adding-readonly-modifiers - */ -export type Mutable = { - -readonly [K in keyof T]: T[K] extends PropertyKey - ? LiteralToCommon - : Mutable -} +import {StyleProp, TextStyle, ViewStyle} from 'react-native' export type TextStyleProp = { style?: StyleProp @@ -24,3 +7,156 @@ export type TextStyleProp = { export type ViewStyleProp = { style?: StyleProp } + +export type ThemeName = 'light' | 'dim' | 'dark' +export type Palette = { + white: string + black: string + + contrast_25: string + contrast_50: string + contrast_100: string + contrast_200: string + contrast_300: string + contrast_400: string + contrast_500: string + contrast_600: string + contrast_700: string + contrast_800: string + contrast_900: string + contrast_950: string + contrast_975: string + + primary_25: string + primary_50: string + primary_100: string + primary_200: string + primary_300: string + primary_400: string + primary_500: string + primary_600: string + primary_700: string + primary_800: string + primary_900: string + primary_950: string + primary_975: string + + positive_25: string + positive_50: string + positive_100: string + positive_200: string + positive_300: string + positive_400: string + positive_500: string + positive_600: string + positive_700: string + positive_800: string + positive_900: string + positive_950: string + positive_975: string + + negative_25: string + negative_50: string + negative_100: string + negative_200: string + negative_300: string + negative_400: string + negative_500: string + negative_600: string + negative_700: string + negative_800: string + negative_900: string + negative_950: string + negative_975: string +} +export type ThemedAtoms = { + text: { + color: string + } + text_contrast_low: { + color: string + } + text_contrast_medium: { + color: string + } + text_contrast_high: { + color: string + } + text_inverted: { + color: string + } + bg: { + backgroundColor: string + } + bg_contrast_25: { + backgroundColor: string + } + bg_contrast_50: { + backgroundColor: string + } + bg_contrast_100: { + backgroundColor: string + } + bg_contrast_200: { + backgroundColor: string + } + bg_contrast_300: { + backgroundColor: string + } + bg_contrast_400: { + backgroundColor: string + } + bg_contrast_500: { + backgroundColor: string + } + bg_contrast_600: { + backgroundColor: string + } + bg_contrast_700: { + backgroundColor: string + } + bg_contrast_800: { + backgroundColor: string + } + bg_contrast_900: { + backgroundColor: string + } + bg_contrast_950: { + backgroundColor: string + } + bg_contrast_975: { + backgroundColor: string + } + border_contrast_low: { + borderColor: string + } + border_contrast_medium: { + borderColor: string + } + border_contrast_high: { + borderColor: string + } + shadow_sm: { + shadowRadius: number + shadowOpacity: number + elevation: number + shadowColor: string + } + shadow_md: { + shadowRadius: number + shadowOpacity: number + elevation: number + shadowColor: string + } + shadow_lg: { + shadowRadius: number + shadowOpacity: number + elevation: number + shadowColor: string + } +} +export type Theme = { + name: ThemeName + palette: Palette + atoms: ThemedAtoms +} diff --git a/src/alf/util/colorGeneration.ts b/src/alf/util/colorGeneration.ts index 929a01d3a7..8d769b51b1 100644 --- a/src/alf/util/colorGeneration.ts +++ b/src/alf/util/colorGeneration.ts @@ -15,3 +15,7 @@ export function generateScale(start: number, end: number) { return start + range * stop }) } + +export const defaultScale = generateScale(6, 100) +// dim shifted 6% lighter +export const dimScale = generateScale(12, 100) diff --git a/src/alf/util/themeSelector.ts b/src/alf/util/themeSelector.ts index f484bc5267..e79f863872 100644 --- a/src/alf/util/themeSelector.ts +++ b/src/alf/util/themeSelector.ts @@ -1,4 +1,4 @@ -import {ThemeName} from '#/alf/themes' +import {ThemeName} from '#/alf/types' export function select(name: ThemeName, options: Record) { switch (name) { diff --git a/src/alf/util/useColorModeTheme.ts b/src/alf/util/useColorModeTheme.ts index ce15587478..12840c7062 100644 --- a/src/alf/util/useColorModeTheme.ts +++ b/src/alf/util/useColorModeTheme.ts @@ -4,7 +4,8 @@ import * as SystemUI from 'expo-system-ui' import {isWeb} from 'platform/detection' import {useThemePrefs} from 'state/shell' -import {dark, dim, light, ThemeName} from '#/alf/themes' +import {dark, dim, light} from '#/alf/themes' +import {ThemeName} from '#/alf/types' export function useColorModeTheme(): ThemeName { const theme = useThemeName() diff --git a/src/components/Button.tsx b/src/components/Button.tsx index dfdcde6edf..457164d111 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -13,7 +13,7 @@ import { } from 'react-native' import {LinearGradient} from 'expo-linear-gradient' -import {android, atoms as a, flatten, tokens, useTheme} from '#/alf' +import {android, atoms as a, flatten, select, tokens, useTheme} from '#/alf' import {Props as SVGIconProps} from '#/components/icons/common' import {normalizeTextStyles} from '#/components/Typography' @@ -152,7 +152,6 @@ export const Button = React.forwardRef( const {baseStyles, hoverStyles} = React.useMemo(() => { const baseStyles: ViewStyle[] = [] const hoverStyles: ViewStyle[] = [] - const light = t.name === 'light' if (color === 'primary') { if (variant === 'solid') { @@ -165,7 +164,11 @@ export const Button = React.forwardRef( }) } else { baseStyles.push({ - backgroundColor: t.palette.primary_700, + backgroundColor: select(t.name, { + light: t.palette.primary_700, + dim: t.palette.primary_300, + dark: t.palette.primary_300, + }), }) } } else if (variant === 'outline') { @@ -178,24 +181,18 @@ export const Button = React.forwardRef( borderColor: t.palette.primary_500, }) hoverStyles.push(a.border, { - backgroundColor: light - ? t.palette.primary_50 - : t.palette.primary_950, + backgroundColor: t.palette.primary_50, }) } else { baseStyles.push(a.border, { - borderColor: light - ? t.palette.primary_200 - : t.palette.primary_900, + borderColor: t.palette.primary_200, }) } } else if (variant === 'ghost') { if (!disabled) { baseStyles.push(t.atoms.bg) hoverStyles.push({ - backgroundColor: light - ? t.palette.primary_100 - : t.palette.primary_900, + backgroundColor: t.palette.primary_100, }) } } @@ -203,14 +200,26 @@ export const Button = React.forwardRef( if (variant === 'solid') { if (!disabled) { baseStyles.push({ - backgroundColor: t.palette.contrast_25, + backgroundColor: select(t.name, { + light: t.palette.contrast_25, + dim: t.palette.contrast_100, + dark: t.palette.contrast_100, + }), }) hoverStyles.push({ - backgroundColor: t.palette.contrast_50, + backgroundColor: select(t.name, { + light: t.palette.contrast_50, + dim: t.palette.contrast_200, + dark: t.palette.contrast_200, + }), }) } else { baseStyles.push({ - backgroundColor: t.palette.contrast_100, + backgroundColor: select(t.name, { + light: t.palette.contrast_100, + dim: t.palette.contrast_25, + dark: t.palette.contrast_25, + }), }) } } else if (variant === 'outline') { @@ -247,7 +256,7 @@ export const Button = React.forwardRef( }) } else { baseStyles.push({ - backgroundColor: t.palette.contrast_700, + backgroundColor: t.palette.contrast_600, }) } } else if (variant === 'outline') { @@ -284,7 +293,11 @@ export const Button = React.forwardRef( }) } else { baseStyles.push({ - backgroundColor: t.palette.negative_700, + backgroundColor: select(t.name, { + light: t.palette.negative_700, + dim: t.palette.negative_300, + dark: t.palette.negative_300, + }), }) } } else if (variant === 'outline') { @@ -297,24 +310,18 @@ export const Button = React.forwardRef( borderColor: t.palette.negative_500, }) hoverStyles.push(a.border, { - backgroundColor: light - ? t.palette.negative_50 - : t.palette.negative_975, + backgroundColor: t.palette.negative_50, }) } else { baseStyles.push(a.border, { - borderColor: light - ? t.palette.negative_200 - : t.palette.negative_900, + borderColor: t.palette.negative_200, }) } } else if (variant === 'ghost') { if (!disabled) { baseStyles.push(t.atoms.bg) hoverStyles.push({ - backgroundColor: light - ? t.palette.negative_100 - : t.palette.negative_975, + backgroundColor: t.palette.negative_100, }) } } @@ -482,7 +489,6 @@ export function useSharedButtonTextStyles() { const {color, variant, disabled, size} = useButtonContext() return React.useMemo(() => { const baseStyles: TextStyle[] = [] - const light = t.name === 'light' if (color === 'primary') { if (variant === 'solid') { @@ -494,7 +500,7 @@ export function useSharedButtonTextStyles() { } else if (variant === 'outline') { if (!disabled) { baseStyles.push({ - color: light ? t.palette.primary_600 : t.palette.primary_500, + color: t.palette.primary_600, }) } else { baseStyles.push({color: t.palette.primary_600, opacity: 0.5}) diff --git a/src/components/IconCircle.tsx b/src/components/IconCircle.tsx index aa779e37f3..806d35c385 100644 --- a/src/components/IconCircle.tsx +++ b/src/components/IconCircle.tsx @@ -2,14 +2,14 @@ import React from 'react' import {View} from 'react-native' import { - useTheme, atoms as a, - ViewStyleProp, - TextStyleProp, flatten, + TextStyleProp, + useTheme, + ViewStyleProp, } from '#/alf' -import {Growth_Stroke2_Corner0_Rounded as Growth} from '#/components/icons/Growth' import {Props} from '#/components/icons/common' +import {Growth_Stroke2_Corner0_Rounded as Growth} from '#/components/icons/Growth' export function IconCircle({ icon: Icon, @@ -32,8 +32,7 @@ export function IconCircle({ { width: size === 'lg' ? 52 : 64, height: size === 'lg' ? 52 : 64, - backgroundColor: - t.name === 'light' ? t.palette.primary_50 : t.palette.primary_950, + backgroundColor: t.palette.primary_50, }, flatten(style), ]}> diff --git a/src/components/dialogs/MutedWords.tsx b/src/components/dialogs/MutedWords.tsx index 534263422d..526652be95 100644 --- a/src/components/dialogs/MutedWords.tsx +++ b/src/components/dialogs/MutedWords.tsx @@ -357,12 +357,11 @@ function TargetToggle({children}: React.PropsWithChildren<{}>) { a.px_sm, gtMobile && a.px_md, a.rounded_sm, - t.atoms.bg_contrast_50, - (ctx.hovered || ctx.focused) && t.atoms.bg_contrast_100, + t.atoms.bg_contrast_25, + (ctx.hovered || ctx.focused) && t.atoms.bg_contrast_50, ctx.selected && [ { - backgroundColor: - t.name === 'light' ? t.palette.primary_50 : t.palette.primary_975, + backgroundColor: t.palette.primary_50, }, ], ctx.disabled && { diff --git a/src/components/dialogs/ThreadgateEditor.tsx b/src/components/dialogs/ThreadgateEditor.tsx index 104766e267..92dd157b22 100644 --- a/src/components/dialogs/ThreadgateEditor.tsx +++ b/src/components/dialogs/ThreadgateEditor.tsx @@ -196,10 +196,7 @@ function Selectable({ t.atoms.bg_contrast_50, (hovered || focused) && t.atoms.bg_contrast_100, isSelected && { - backgroundColor: - t.name === 'light' - ? t.palette.primary_50 - : t.palette.primary_975, + backgroundColor: t.palette.primary_100, }, style, ]}> diff --git a/src/components/dms/MessageItem.tsx b/src/components/dms/MessageItem.tsx index 61358c9893..573c24f77d 100644 --- a/src/components/dms/MessageItem.tsx +++ b/src/components/dms/MessageItem.tsx @@ -72,8 +72,7 @@ let MessageItem = ({ lastInGroupRef.current = isLastInGroup } - const pendingColor = - t.name === 'light' ? t.palette.primary_200 : t.palette.primary_800 + const pendingColor = t.palette.primary_200 const rt = useMemo(() => { return new RichTextAPI({text: message.text, facets: message.facets}) @@ -110,12 +109,7 @@ let MessageItem = ({ }> Buttons - {['primary', 'secondary', 'secondary_inverted'].map(color => ( - - {['solid', 'outline', 'ghost'].map(variant => ( - - - - - ))} - - ))} + {['primary', 'secondary', 'secondary_inverted', 'negative'].map( + color => ( + + {['solid', 'outline', 'ghost'].map(variant => ( + + + + + ))} + + ), + )} @@ -68,6 +70,7 @@ export function Buttons() { ), )} + {/* {['gradient_sunset', 'gradient_nordic', 'gradient_bonfire'].map( name => ( @@ -91,6 +94,7 @@ export function Buttons() { ), )} + */} diff --git a/src/view/screens/Storybook/Forms.tsx b/src/view/screens/Storybook/Forms.tsx index 1e4efdcc7d..8c888f04d7 100644 --- a/src/view/screens/Storybook/Forms.tsx +++ b/src/view/screens/Storybook/Forms.tsx @@ -41,6 +41,16 @@ export function Forms() { /> + + + + + Text field From 119b4d3e0aaff1328952294d975bacb88490d9b8 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 11 Jul 2024 17:35:13 -0500 Subject: [PATCH 15/22] Fix search hashtags by author (#4773) --- src/screens/Hashtag.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/screens/Hashtag.tsx b/src/screens/Hashtag.tsx index 34539f5102..8bcb9359e9 100644 --- a/src/screens/Hashtag.tsx +++ b/src/screens/Hashtag.tsx @@ -169,7 +169,7 @@ function HashtagScreenTab({ const queryParam = React.useMemo(() => { if (!author) return fullTag - return `${fullTag} from:${sanitizeHandle(author)}` + return `${fullTag} from:${author}` }, [fullTag, author]) const { From b433469ab954db846ab7bb9109cfe7262711c5bb Mon Sep 17 00:00:00 2001 From: Hailey Date: Thu, 11 Jul 2024 18:14:47 -0700 Subject: [PATCH 16/22] bump 1.89.0 (#4776) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0ea23a2749..6b11fc133b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bsky.app", - "version": "1.88.0", + "version": "1.89.0", "private": true, "engines": { "node": ">=18" From 2397104ad6169ced02b1acd9fbbbe426f4cc4da0 Mon Sep 17 00:00:00 2001 From: Hailey Date: Thu, 11 Jul 2024 18:15:35 -0700 Subject: [PATCH 17/22] Lint native files (#4768) --- .editorconfig | 2 + modules/BlueskyNSE/NotificationService.swift | 18 +-- .../ShareViewController.swift | 25 ++- .../BackgroundNotificationHandler.kt | 2 +- ...ExpoBackgroundNotificationHandlerModule.kt | 95 ++++++------ .../NotificationPrefs.kt | 107 +++++++------ ...oBackgroundNotificationHandlerModule.swift | 47 +++--- .../AppCompatImageViewExtended.kt | 7 +- .../ExpoBlueskyGifViewModule.kt | 83 +++++----- .../expo/modules/blueskygifview/GifView.kt | 145 +++++++++--------- .../ios/ExpoBlueskyGifViewModule.swift | 16 +- .../expo-bluesky-gif-view/ios/GifView.swift | 15 +- .../ExpoBlueskyDevicePrefsModule.kt | 7 +- .../referrer/ExpoBlueskyReferrerModule.kt | 83 +++++----- .../ExpoReceiveAndroidIntentsModule.kt | 36 +++-- .../ios/ExpoScrollForwarderModule.swift | 2 +- .../ios/ExpoScrollForwarderView.swift | 76 +++++---- package.json | 2 + 18 files changed, 393 insertions(+), 375 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..32a53f185a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,2 @@ +[*.{kt,kts}] +indent_size=2 diff --git a/modules/BlueskyNSE/NotificationService.swift b/modules/BlueskyNSE/NotificationService.swift index 384180d8b0..f863eaf223 100644 --- a/modules/BlueskyNSE/NotificationService.swift +++ b/modules/BlueskyNSE/NotificationService.swift @@ -13,43 +13,43 @@ class NotificationService: UNNotificationServiceExtension { contentHandler(request.content) return } - + if reason == "chat-message" { mutateWithChatMessage(bestAttempt) } else { mutateWithBadge(bestAttempt) } - + contentHandler(bestAttempt) } - + override func serviceExtensionTimeWillExpire() { // If for some reason the alloted time expires, we don't actually want to display a notification } - + func createCopy(_ content: UNNotificationContent) -> UNMutableNotificationContent? { return content.mutableCopy() as? UNMutableNotificationContent } - + func mutateWithBadge(_ content: UNMutableNotificationContent) { var count = prefs?.integer(forKey: "badgeCount") ?? 0 count += 1 - + // Set the new badge number for the notification, then store that value for using later content.badge = NSNumber(value: count) prefs?.setValue(count, forKey: "badgeCount") } - + func mutateWithChatMessage(_ content: UNMutableNotificationContent) { if self.prefs?.bool(forKey: "playSoundChat") == true { mutateWithDmSound(content) } } - + func mutateWithDefaultSound(_ content: UNMutableNotificationContent) { content.sound = UNNotificationSound.default } - + func mutateWithDmSound(_ content: UNMutableNotificationContent) { content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "dm.aiff")) } diff --git a/modules/Share-with-Bluesky/ShareViewController.swift b/modules/Share-with-Bluesky/ShareViewController.swift index 4c1d635ce3..c045d578fe 100644 --- a/modules/Share-with-Bluesky/ShareViewController.swift +++ b/modules/Share-with-Bluesky/ShareViewController.swift @@ -30,12 +30,11 @@ class ShareViewController: UIViewController { } } - private func handleText(item: NSItemProvider) async -> Void { + private func handleText(item: NSItemProvider) async { do { if let data = try await item.loadItem(forTypeIdentifier: "public.text") as? String { if let encoded = data.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed), - let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)") - { + let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)") { _ = self.openURL(url) } } @@ -45,12 +44,11 @@ class ShareViewController: UIViewController { } } - private func handleUrl(item: NSItemProvider) async -> Void { + private func handleUrl(item: NSItemProvider) async { do { if let data = try await item.loadItem(forTypeIdentifier: "public.url") as? URL { if let encoded = data.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed), - let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)") - { + let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)") { _ = self.openURL(url) } } @@ -60,7 +58,7 @@ class ShareViewController: UIViewController { } } - private func handleImages(items: [NSItemProvider]) async -> Void { + private func handleImages(items: [NSItemProvider]) async { let firstFourItems: [NSItemProvider] if items.count < 4 { firstFourItems = items @@ -72,7 +70,7 @@ class ShareViewController: UIViewController { var imageUris = "" for (index, item) in firstFourItems.enumerated() { - var imageUriInfo: String? = nil + var imageUriInfo: String? do { if let dataUri = try await item.loadItem(forTypeIdentifier: "public.image") as? URL { @@ -100,8 +98,7 @@ class ShareViewController: UIViewController { if valid, let encoded = imageUris.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed), - let url = URL(string: "\(self.appScheme)://intent/compose?imageUris=\(encoded)") - { + let url = URL(string: "\(self.appScheme)://intent/compose?imageUris=\(encoded)") { _ = self.openURL(url) } @@ -119,13 +116,11 @@ class ShareViewController: UIViewController { // extension does. if let dir = FileManager() .containerURL( - forSecurityApplicationGroupIdentifier: "group.app.bsky") - { + forSecurityApplicationGroupIdentifier: "group.app.bsky") { let filePath = "\(dir.absoluteString)\(ProcessInfo.processInfo.globallyUniqueString).jpeg" if let newUri = URL(string: filePath), - let jpegData = image.jpegData(compressionQuality: 1) - { + let jpegData = image.jpegData(compressionQuality: 1) { try jpegData.write(to: newUri) return "\(newUri.absoluteString)|\(image.size.width)|\(image.size.height)" } @@ -136,7 +131,7 @@ class ShareViewController: UIViewController { } } - private func completeRequest() -> Void { + private func completeRequest() { self.extensionContext?.completeRequest(returningItems: nil) } diff --git a/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandler.kt b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandler.kt index 0a8737b88f..7c1494c706 100644 --- a/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandler.kt +++ b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandler.kt @@ -5,7 +5,7 @@ import com.google.firebase.messaging.RemoteMessage class BackgroundNotificationHandler( private val context: Context, - private val notifInterface: BackgroundNotificationHandlerInterface + private val notifInterface: BackgroundNotificationHandlerInterface, ) { fun handleMessage(remoteMessage: RemoteMessage) { if (ExpoBackgroundNotificationHandlerModule.isForegrounded) { diff --git a/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/ExpoBackgroundNotificationHandlerModule.kt b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/ExpoBackgroundNotificationHandlerModule.kt index c876f899ac..0cc5fa6ab1 100644 --- a/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/ExpoBackgroundNotificationHandlerModule.kt +++ b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/ExpoBackgroundNotificationHandlerModule.kt @@ -8,67 +8,68 @@ class ExpoBackgroundNotificationHandlerModule : Module() { var isForegrounded = false } - override fun definition() = ModuleDefinition { - Name("ExpoBackgroundNotificationHandler") + override fun definition() = + ModuleDefinition { + Name("ExpoBackgroundNotificationHandler") - OnCreate { - NotificationPrefs(appContext.reactContext).initialize() - } + OnCreate { + NotificationPrefs(appContext.reactContext).initialize() + } - OnActivityEntersForeground { - isForegrounded = true - } + OnActivityEntersForeground { + isForegrounded = true + } - OnActivityEntersBackground { - isForegrounded = false - } + OnActivityEntersBackground { + isForegrounded = false + } - AsyncFunction("getAllPrefsAsync") { - return@AsyncFunction NotificationPrefs(appContext.reactContext).getAllPrefs() - } + AsyncFunction("getAllPrefsAsync") { + return@AsyncFunction NotificationPrefs(appContext.reactContext).getAllPrefs() + } - AsyncFunction("getBoolAsync") { forKey: String -> - return@AsyncFunction NotificationPrefs(appContext.reactContext).getBoolean(forKey) - } + AsyncFunction("getBoolAsync") { forKey: String -> + return@AsyncFunction NotificationPrefs(appContext.reactContext).getBoolean(forKey) + } - AsyncFunction("getStringAsync") { forKey: String -> - return@AsyncFunction NotificationPrefs(appContext.reactContext).getString(forKey) - } + AsyncFunction("getStringAsync") { forKey: String -> + return@AsyncFunction NotificationPrefs(appContext.reactContext).getString(forKey) + } - AsyncFunction("getStringArrayAsync") { forKey: String -> - return@AsyncFunction NotificationPrefs(appContext.reactContext).getStringArray(forKey) - } + AsyncFunction("getStringArrayAsync") { forKey: String -> + return@AsyncFunction NotificationPrefs(appContext.reactContext).getStringArray(forKey) + } - AsyncFunction("setBoolAsync") { forKey: String, value: Boolean -> - NotificationPrefs(appContext.reactContext).setBoolean(forKey, value) - } + AsyncFunction("setBoolAsync") { forKey: String, value: Boolean -> + NotificationPrefs(appContext.reactContext).setBoolean(forKey, value) + } - AsyncFunction("setStringAsync") { forKey: String, value: String -> - NotificationPrefs(appContext.reactContext).setString(forKey, value) - } + AsyncFunction("setStringAsync") { forKey: String, value: String -> + NotificationPrefs(appContext.reactContext).setString(forKey, value) + } - AsyncFunction("setStringArrayAsync") { forKey: String, value: Array -> - NotificationPrefs(appContext.reactContext).setStringArray(forKey, value) - } + AsyncFunction("setStringArrayAsync") { forKey: String, value: Array -> + NotificationPrefs(appContext.reactContext).setStringArray(forKey, value) + } - AsyncFunction("addToStringArrayAsync") { forKey: String, string: String -> - NotificationPrefs(appContext.reactContext).addToStringArray(forKey, string) - } + AsyncFunction("addToStringArrayAsync") { forKey: String, string: String -> + NotificationPrefs(appContext.reactContext).addToStringArray(forKey, string) + } - AsyncFunction("removeFromStringArrayAsync") { forKey: String, string: String -> - NotificationPrefs(appContext.reactContext).removeFromStringArray(forKey, string) - } + AsyncFunction("removeFromStringArrayAsync") { forKey: String, string: String -> + NotificationPrefs(appContext.reactContext).removeFromStringArray(forKey, string) + } - AsyncFunction("addManyToStringArrayAsync") { forKey: String, strings: Array -> - NotificationPrefs(appContext.reactContext).addManyToStringArray(forKey, strings) - } + AsyncFunction("addManyToStringArrayAsync") { forKey: String, strings: Array -> + NotificationPrefs(appContext.reactContext).addManyToStringArray(forKey, strings) + } - AsyncFunction("removeManyFromStringArrayAsync") { forKey: String, strings: Array -> - NotificationPrefs(appContext.reactContext).removeManyFromStringArray(forKey, strings) - } + AsyncFunction("removeManyFromStringArrayAsync") { forKey: String, strings: Array -> + NotificationPrefs(appContext.reactContext).removeManyFromStringArray(forKey, strings) + } - AsyncFunction("setBadgeCountAsync") { _: Int -> - // This does nothing on Android + AsyncFunction("setBadgeCountAsync") { _: Int -> + // This does nothing on Android + } } - } } diff --git a/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/NotificationPrefs.kt b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/NotificationPrefs.kt index 17ef9205ef..97b05b4d73 100644 --- a/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/NotificationPrefs.kt +++ b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/NotificationPrefs.kt @@ -2,20 +2,24 @@ package expo.modules.backgroundnotificationhandler import android.content.Context -val DEFAULTS = mapOf( - "playSoundChat" to true, - "playSoundFollow" to false, - "playSoundLike" to false, - "playSoundMention" to false, - "playSoundQuote" to false, - "playSoundReply" to false, - "playSoundRepost" to false, - "mutedThreads" to mapOf>() -) +val DEFAULTS = + mapOf( + "playSoundChat" to true, + "playSoundFollow" to false, + "playSoundLike" to false, + "playSoundMention" to false, + "playSoundQuote" to false, + "playSoundReply" to false, + "playSoundRepost" to false, + "mutedThreads" to mapOf>(), + ) -class NotificationPrefs (private val context: Context?) { - private val prefs = context?.getSharedPreferences("xyz.blueskyweb.app", Context.MODE_PRIVATE) - ?: throw Error("Context is null") +class NotificationPrefs( + private val context: Context?, +) { + private val prefs = + context?.getSharedPreferences("xyz.blueskyweb.app", Context.MODE_PRIVATE) + ?: throw Error("Context is null") fun initialize() { prefs @@ -41,94 +45,99 @@ class NotificationPrefs (private val context: Context?) { } } } - } - .apply() + }.apply() } - fun getAllPrefs(): MutableMap { - return prefs.all - } + fun getAllPrefs(): MutableMap = prefs.all - fun getBoolean(key: String): Boolean { - return prefs.getBoolean(key, false) - } + fun getBoolean(key: String): Boolean = prefs.getBoolean(key, false) - fun getString(key: String): String? { - return prefs.getString(key, null) - } + fun getString(key: String): String? = prefs.getString(key, null) - fun getStringArray(key: String): Array? { - return prefs.getStringSet(key, null)?.toTypedArray() - } + fun getStringArray(key: String): Array? = prefs.getStringSet(key, null)?.toTypedArray() - fun setBoolean(key: String, value: Boolean) { + fun setBoolean( + key: String, + value: Boolean, + ) { prefs .edit() .apply { putBoolean(key, value) - } - .apply() + }.apply() } - fun setString(key: String, value: String) { + fun setString( + key: String, + value: String, + ) { prefs .edit() .apply { putString(key, value) - } - .apply() + }.apply() } - fun setStringArray(key: String, value: Array) { + fun setStringArray( + key: String, + value: Array, + ) { prefs .edit() .apply { putStringSet(key, value.toSet()) - } - .apply() + }.apply() } - fun addToStringArray(key: String, string: String) { + fun addToStringArray( + key: String, + string: String, + ) { prefs .edit() .apply { val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf() set.add(string) putStringSet(key, set) - } - .apply() + }.apply() } - fun removeFromStringArray(key: String, string: String) { + fun removeFromStringArray( + key: String, + string: String, + ) { prefs .edit() .apply { val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf() set.remove(string) putStringSet(key, set) - } - .apply() + }.apply() } - fun addManyToStringArray(key: String, strings: Array) { + fun addManyToStringArray( + key: String, + strings: Array, + ) { prefs .edit() .apply { val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf() set.addAll(strings.toSet()) putStringSet(key, set) - } - .apply() + }.apply() } - fun removeManyFromStringArray(key: String, strings: Array) { + fun removeManyFromStringArray( + key: String, + strings: Array, + ) { prefs .edit() .apply { val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf() set.removeAll(strings.toSet()) putStringSet(key, set) - } - .apply() + }.apply() } -} \ No newline at end of file +} diff --git a/modules/expo-background-notification-handler/ios/ExpoBackgroundNotificationHandlerModule.swift b/modules/expo-background-notification-handler/ios/ExpoBackgroundNotificationHandlerModule.swift index 5f8c7fc3bb..3845fe765c 100644 --- a/modules/expo-background-notification-handler/ios/ExpoBackgroundNotificationHandlerModule.swift +++ b/modules/expo-background-notification-handler/ios/ExpoBackgroundNotificationHandlerModule.swift @@ -2,16 +2,16 @@ import ExpoModulesCore let APP_GROUP = "group.app.bsky" -let DEFAULTS: [String:Any] = [ - "playSoundChat" : true, +let DEFAULTS: [String: Any] = [ + "playSoundChat": true, "playSoundFollow": false, "playSoundLike": false, "playSoundMention": false, "playSoundQuote": false, "playSoundReply": false, "playSoundRepost": false, - "mutedThreads": [:] as! [String:[String]], - "badgeCount": 0, + "mutedThreads": [:] as! [String: [String]], + "badgeCount": 0 ] /* @@ -23,10 +23,10 @@ let DEFAULTS: [String:Any] = [ */ public class ExpoBackgroundNotificationHandlerModule: Module { let userDefaults = UserDefaults(suiteName: APP_GROUP) - + public func definition() -> ModuleDefinition { Name("ExpoBackgroundNotificationHandler") - + OnCreate { DEFAULTS.forEach { p in if userDefaults?.value(forKey: p.key) == nil { @@ -34,57 +34,56 @@ public class ExpoBackgroundNotificationHandlerModule: Module { } } } - - AsyncFunction("getAllPrefsAsync") { () -> [String:Any]? in + + AsyncFunction("getAllPrefsAsync") { () -> [String: Any]? in var keys: [String] = [] DEFAULTS.forEach { p in keys.append(p.key) } return userDefaults?.dictionaryWithValues(forKeys: keys) } - + AsyncFunction("getBoolAsync") { (forKey: String) -> Bool in if let pref = userDefaults?.bool(forKey: forKey) { return pref } return false } - + AsyncFunction("getStringAsync") { (forKey: String) -> String? in if let pref = userDefaults?.string(forKey: forKey) { return pref } return nil } - + AsyncFunction("getStringArrayAsync") { (forKey: String) -> [String]? in if let pref = userDefaults?.stringArray(forKey: forKey) { return pref } return nil } - - AsyncFunction("setBoolAsync") { (forKey: String, value: Bool) -> Void in + + AsyncFunction("setBoolAsync") { (forKey: String, value: Bool) in userDefaults?.setValue(value, forKey: forKey) } - - AsyncFunction("setStringAsync") { (forKey: String, value: String) -> Void in + + AsyncFunction("setStringAsync") { (forKey: String, value: String) in userDefaults?.setValue(value, forKey: forKey) } - - AsyncFunction("setStringArrayAsync") { (forKey: String, value: [String]) -> Void in + + AsyncFunction("setStringArrayAsync") { (forKey: String, value: [String]) in userDefaults?.setValue(value, forKey: forKey) } - + AsyncFunction("addToStringArrayAsync") { (forKey: String, string: String) in if var curr = userDefaults?.stringArray(forKey: forKey), - !curr.contains(string) - { + !curr.contains(string) { curr.append(string) userDefaults?.setValue(curr, forKey: forKey) } } - + AsyncFunction("removeFromStringArrayAsync") { (forKey: String, string: String) in if var curr = userDefaults?.stringArray(forKey: forKey) { curr.removeAll { s in @@ -93,7 +92,7 @@ public class ExpoBackgroundNotificationHandlerModule: Module { userDefaults?.setValue(curr, forKey: forKey) } } - + AsyncFunction("addManyToStringArrayAsync") { (forKey: String, strings: [String]) in if var curr = userDefaults?.stringArray(forKey: forKey) { strings.forEach { s in @@ -104,7 +103,7 @@ public class ExpoBackgroundNotificationHandlerModule: Module { userDefaults?.setValue(curr, forKey: forKey) } } - + AsyncFunction("removeManyFromStringArrayAsync") { (forKey: String, strings: [String]) in if var curr = userDefaults?.stringArray(forKey: forKey) { strings.forEach { s in @@ -113,7 +112,7 @@ public class ExpoBackgroundNotificationHandlerModule: Module { userDefaults?.setValue(curr, forKey: forKey) } } - + AsyncFunction("setBadgeCountAsync") { (count: Int) in userDefaults?.setValue(count, forKey: "badgeCount") } diff --git a/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/AppCompatImageViewExtended.kt b/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/AppCompatImageViewExtended.kt index 5d20848453..5ba5ccf71a 100644 --- a/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/AppCompatImageViewExtended.kt +++ b/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/AppCompatImageViewExtended.kt @@ -5,7 +5,10 @@ import android.graphics.Canvas import android.graphics.drawable.Animatable import androidx.appcompat.widget.AppCompatImageView -class AppCompatImageViewExtended(context: Context, private val parent: GifView): AppCompatImageView(context) { +class AppCompatImageViewExtended( + context: Context, + private val parent: GifView, +) : AppCompatImageView(context) { override fun onDraw(canvas: Canvas) { super.onDraw(canvas) @@ -34,4 +37,4 @@ class AppCompatImageViewExtended(context: Context, private val parent: GifView): drawable.start() } } -} \ No newline at end of file +} diff --git a/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/ExpoBlueskyGifViewModule.kt b/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/ExpoBlueskyGifViewModule.kt index 625e1d45f9..b0b3e8eeb7 100644 --- a/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/ExpoBlueskyGifViewModule.kt +++ b/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/ExpoBlueskyGifViewModule.kt @@ -6,49 +6,50 @@ import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition class ExpoBlueskyGifViewModule : Module() { - override fun definition() = ModuleDefinition { - Name("ExpoBlueskyGifView") + override fun definition() = + ModuleDefinition { + Name("ExpoBlueskyGifView") - AsyncFunction("prefetchAsync") { sources: List -> - val activity = appContext.currentActivity ?: return@AsyncFunction - val glide = Glide.with(activity) + AsyncFunction("prefetchAsync") { sources: List -> + val activity = appContext.currentActivity ?: return@AsyncFunction + val glide = Glide.with(activity) - sources.forEach { source -> - glide - .download(source) - .diskCacheStrategy(DiskCacheStrategy.DATA) - .submit() + sources.forEach { source -> + glide + .download(source) + .diskCacheStrategy(DiskCacheStrategy.DATA) + .submit() + } + } + + View(GifView::class) { + Events( + "onPlayerStateChange", + ) + + Prop("source") { view: GifView, source: String -> + view.source = source + } + + Prop("placeholderSource") { view: GifView, source: String -> + view.placeholderSource = source + } + + Prop("autoplay") { view: GifView, autoplay: Boolean -> + view.autoplay = autoplay + } + + AsyncFunction("playAsync") { view: GifView -> + view.play() + } + + AsyncFunction("pauseAsync") { view: GifView -> + view.pause() + } + + AsyncFunction("toggleAsync") { view: GifView -> + view.toggle() + } } } - - View(GifView::class) { - Events( - "onPlayerStateChange" - ) - - Prop("source") { view: GifView, source: String -> - view.source = source - } - - Prop("placeholderSource") { view: GifView, source: String -> - view.placeholderSource = source - } - - Prop("autoplay") { view: GifView, autoplay: Boolean -> - view.autoplay = autoplay - } - - AsyncFunction("playAsync") { view: GifView -> - view.play() - } - - AsyncFunction("pauseAsync") { view: GifView -> - view.pause() - } - - AsyncFunction("toggleAsync") { view: GifView -> - view.toggle() - } - } - } } diff --git a/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/GifView.kt b/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/GifView.kt index be5830df7a..5e467a1603 100644 --- a/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/GifView.kt +++ b/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/GifView.kt @@ -1,6 +1,5 @@ package expo.modules.blueskygifview - import android.content.Context import android.graphics.Color import android.graphics.drawable.Animatable @@ -15,7 +14,10 @@ import expo.modules.kotlin.exception.Exceptions import expo.modules.kotlin.viewevent.EventDispatcher import expo.modules.kotlin.views.ExpoView -class GifView(context: Context, appContext: AppContext) : ExpoView(context, appContext) { +class GifView( + context: Context, + appContext: AppContext, +) : ExpoView(context, appContext) { // Events private val onPlayerStateChange by EventDispatcher() @@ -44,8 +46,7 @@ class GifView(context: Context, appContext: AppContext) : ExpoView(context, appC } } - - // + // init { this.setBackgroundColor(Color.TRANSPARENT) @@ -70,80 +71,82 @@ class GifView(context: Context, appContext: AppContext) : ExpoView(context, appC super.onDetachedFromWindow() } - // + // - // + // private fun load() { if (placeholderSource == null || source == null) { return } - this.webpRequest = glide.load(source) - .diskCacheStrategy(DiskCacheStrategy.DATA) - .skipMemoryCache(false) - .listener(object: RequestListener { - override fun onResourceReady( - resource: Drawable?, - model: Any?, - target: Target?, - dataSource: com.bumptech.glide.load.DataSource?, - isFirstResource: Boolean - ): Boolean { - if (placeholderRequest != null) { - glide.clear(placeholderRequest) - } - return false - } + this.webpRequest = + glide + .load(source) + .diskCacheStrategy(DiskCacheStrategy.DATA) + .skipMemoryCache(false) + .listener( + object : RequestListener { + override fun onResourceReady( + resource: Drawable?, + model: Any?, + target: Target?, + dataSource: com.bumptech.glide.load.DataSource?, + isFirstResource: Boolean, + ): Boolean { + if (placeholderRequest != null) { + glide.clear(placeholderRequest) + } + return false + } - override fun onLoadFailed( - e: GlideException?, - model: Any?, - target: Target?, - isFirstResource: Boolean - ): Boolean { - return true - } - }) - .into(this.imageView) + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean, + ): Boolean = true + }, + ).into(this.imageView) if (this.imageView.drawable == null || this.imageView.drawable !is Animatable) { - this.placeholderRequest = glide.load(placeholderSource) - .diskCacheStrategy(DiskCacheStrategy.DATA) - // Let's not bloat the memory cache with placeholders - .skipMemoryCache(true) - .listener(object: RequestListener { - override fun onResourceReady( - resource: Drawable?, - model: Any?, - target: Target?, - dataSource: com.bumptech.glide.load.DataSource?, - isFirstResource: Boolean - ): Boolean { - // Incase this request finishes after the webp, let's just not set - // the drawable. This shouldn't happen because the request should get cancelled - if (imageView.drawable == null) { - imageView.setImageDrawable(resource) - } - return true - } + this.placeholderRequest = + glide + .load(placeholderSource) + .diskCacheStrategy(DiskCacheStrategy.DATA) + // Let's not bloat the memory cache with placeholders + .skipMemoryCache(true) + .listener( + object : RequestListener { + override fun onResourceReady( + resource: Drawable?, + model: Any?, + target: Target?, + dataSource: com.bumptech.glide.load.DataSource?, + isFirstResource: Boolean, + ): Boolean { + // Incase this request finishes after the webp, let's just not set + // the drawable. This shouldn't happen because the request should get cancelled + if (imageView.drawable == null) { + imageView.setImageDrawable(resource) + } + return true + } - override fun onLoadFailed( - e: GlideException?, - model: Any?, - target: Target?, - isFirstResource: Boolean - ): Boolean { - return true - } - }) - .submit() + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean, + ): Boolean = true + }, + ).submit() } } - // + // - // + // fun play() { this.imageView.play() @@ -165,16 +168,18 @@ class GifView(context: Context, appContext: AppContext) : ExpoView(context, appC } } - // + // - // + // fun firePlayerStateChange() { - onPlayerStateChange(mapOf( - "isPlaying" to this.isPlaying, - "isLoaded" to this.isLoaded, - )) + onPlayerStateChange( + mapOf( + "isPlaying" to this.isPlaying, + "isLoaded" to this.isLoaded, + ), + ) } - // + // } diff --git a/modules/expo-bluesky-gif-view/ios/ExpoBlueskyGifViewModule.swift b/modules/expo-bluesky-gif-view/ios/ExpoBlueskyGifViewModule.swift index 7c7132290d..9156bd1990 100644 --- a/modules/expo-bluesky-gif-view/ios/ExpoBlueskyGifViewModule.swift +++ b/modules/expo-bluesky-gif-view/ios/ExpoBlueskyGifViewModule.swift @@ -5,11 +5,11 @@ import SDWebImageWebPCoder public class ExpoBlueskyGifViewModule: Module { public func definition() -> ModuleDefinition { Name("ExpoBlueskyGifView") - + OnCreate { SDImageCodersManager.shared.addCoder(SDImageGIFCoder.shared) } - + AsyncFunction("prefetchAsync") { (sources: [URL]) in SDWebImagePrefetcher.shared.prefetchURLs(sources, context: Util.createContext(), progress: nil) } @@ -18,27 +18,27 @@ public class ExpoBlueskyGifViewModule: Module { Events( "onPlayerStateChange" ) - + Prop("source") { (view: GifView, prop: String) in view.source = prop } - + Prop("placeholderSource") { (view: GifView, prop: String) in view.placeholderSource = prop } - + Prop("autoplay") { (view: GifView, prop: Bool) in view.autoplay = prop } - + AsyncFunction("toggleAsync") { (view: GifView) in view.toggle() } - + AsyncFunction("playAsync") { (view: GifView) in view.play() } - + AsyncFunction("pauseAsync") { (view: GifView) in view.pause() } diff --git a/modules/expo-bluesky-gif-view/ios/GifView.swift b/modules/expo-bluesky-gif-view/ios/GifView.swift index de722d7a63..b42a4735c6 100644 --- a/modules/expo-bluesky-gif-view/ios/GifView.swift +++ b/modules/expo-bluesky-gif-view/ios/GifView.swift @@ -16,14 +16,14 @@ public class GifView: ExpoView, AVPlayerViewControllerDelegate { ) private var isPlaying = true private var isLoaded = false - + // Requests private var webpOperation: SDWebImageCombinedOperation? private var placeholderOperation: SDWebImageCombinedOperation? // Props - var source: String? = nil - var placeholderSource: String? = nil + var source: String? + var placeholderSource: String? var autoplay = true { didSet { if !autoplay { @@ -78,8 +78,7 @@ public class GifView: ExpoView, AVPlayerViewControllerDelegate { // See: // https://github.com/SDWebImage/SDWebImage/blob/master/Docs/HowToUse.md#using-asynchronous-image-caching-independently if !SDImageCache.shared.diskImageDataExists(withKey: source), - let url = URL(string: placeholderSource) - { + let url = URL(string: placeholderSource) { self.placeholderOperation = imageManager.loadImage( with: url, options: [.retryFailed], @@ -132,8 +131,7 @@ public class GifView: ExpoView, AVPlayerViewControllerDelegate { if let placeholderSource = self.placeholderSource, imageUrl?.absoluteString == placeholderSource, self.imageView.image == nil, - let image = image - { + let image = image { self.setImage(image) return } @@ -142,8 +140,7 @@ public class GifView: ExpoView, AVPlayerViewControllerDelegate { imageUrl?.absoluteString == source, // UIImage perf suckssss if the image is animated let data = data, - let animatedImage = SDAnimatedImage(data: data) - { + let animatedImage = SDAnimatedImage(data: data) { self.placeholderOperation?.cancel() self.isPlaying = self.autoplay self.isLoaded = true diff --git a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/deviceprefs/ExpoBlueskyDevicePrefsModule.kt b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/deviceprefs/ExpoBlueskyDevicePrefsModule.kt index 29017f17aa..51f9fe45de 100644 --- a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/deviceprefs/ExpoBlueskyDevicePrefsModule.kt +++ b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/deviceprefs/ExpoBlueskyDevicePrefsModule.kt @@ -4,7 +4,8 @@ import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition class ExpoBlueskyDevicePrefsModule : Module() { - override fun definition() = ModuleDefinition { - Name("ExpoBlueskyDevicePrefs") - } + override fun definition() = + ModuleDefinition { + Name("ExpoBlueskyDevicePrefs") + } } diff --git a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/referrer/ExpoBlueskyReferrerModule.kt b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/referrer/ExpoBlueskyReferrerModule.kt index 3589b364e0..ac6ed90b84 100644 --- a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/referrer/ExpoBlueskyReferrerModule.kt +++ b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/referrer/ExpoBlueskyReferrerModule.kt @@ -3,52 +3,55 @@ package expo.modules.blueskyswissarmy.referrer import android.util.Log import com.android.installreferrer.api.InstallReferrerClient import com.android.installreferrer.api.InstallReferrerStateListener +import expo.modules.kotlin.Promise import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition -import expo.modules.kotlin.Promise class ExpoBlueskyReferrerModule : Module() { - override fun definition() = ModuleDefinition { - Name("ExpoBlueskyReferrer") + override fun definition() = + ModuleDefinition { + Name("ExpoBlueskyReferrer") - AsyncFunction("getGooglePlayReferrerInfoAsync") { promise: Promise -> - val referrerClient = InstallReferrerClient.newBuilder(appContext.reactContext).build() - referrerClient.startConnection(object : InstallReferrerStateListener { - override fun onInstallReferrerSetupFinished(responseCode: Int) { - if (responseCode == InstallReferrerClient.InstallReferrerResponse.OK) { - Log.d("ExpoGooglePlayReferrer", "Successfully retrieved referrer info.") + AsyncFunction("getGooglePlayReferrerInfoAsync") { promise: Promise -> + val referrerClient = InstallReferrerClient.newBuilder(appContext.reactContext).build() + referrerClient.startConnection( + object : InstallReferrerStateListener { + override fun onInstallReferrerSetupFinished(responseCode: Int) { + if (responseCode == InstallReferrerClient.InstallReferrerResponse.OK) { + Log.d("ExpoGooglePlayReferrer", "Successfully retrieved referrer info.") - val response = referrerClient.installReferrer - Log.d("ExpoGooglePlayReferrer", "Install referrer: ${response.installReferrer}") + val response = referrerClient.installReferrer + Log.d("ExpoGooglePlayReferrer", "Install referrer: ${response.installReferrer}") - promise.resolve( - mapOf( - "installReferrer" to response.installReferrer, - "clickTimestamp" to response.referrerClickTimestampSeconds, - "installTimestamp" to response.installBeginTimestampSeconds + promise.resolve( + mapOf( + "installReferrer" to response.installReferrer, + "clickTimestamp" to response.referrerClickTimestampSeconds, + "installTimestamp" to response.installBeginTimestampSeconds, + ), + ) + } else { + Log.d("ExpoGooglePlayReferrer", "Failed to get referrer info. Unknown error.") + promise.reject( + "ERR_GOOGLE_PLAY_REFERRER_UNKNOWN", + "Failed to get referrer info", + Exception("Failed to get referrer info"), + ) + } + referrerClient.endConnection() + } + + override fun onInstallReferrerServiceDisconnected() { + Log.d("ExpoGooglePlayReferrer", "Failed to get referrer info. Service disconnected.") + referrerClient.endConnection() + promise.reject( + "ERR_GOOGLE_PLAY_REFERRER_DISCONNECTED", + "Failed to get referrer info", + Exception("Failed to get referrer info"), ) - ) - } else { - Log.d("ExpoGooglePlayReferrer", "Failed to get referrer info. Unknown error.") - promise.reject( - "ERR_GOOGLE_PLAY_REFERRER_UNKNOWN", - "Failed to get referrer info", - Exception("Failed to get referrer info") - ) - } - referrerClient.endConnection() - } - - override fun onInstallReferrerServiceDisconnected() { - Log.d("ExpoGooglePlayReferrer", "Failed to get referrer info. Service disconnected.") - referrerClient.endConnection() - promise.reject( - "ERR_GOOGLE_PLAY_REFERRER_DISCONNECTED", - "Failed to get referrer info", - Exception("Failed to get referrer info") - ) - } - }) + } + }, + ) + } } - } -} \ No newline at end of file +} 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 c2e17fb80a..7ecea16314 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 @@ -13,16 +13,17 @@ import java.io.FileOutputStream import java.net.URLEncoder class ExpoReceiveAndroidIntentsModule : Module() { - override fun definition() = ModuleDefinition { - Name("ExpoReceiveAndroidIntents") + override fun definition() = + ModuleDefinition { + Name("ExpoReceiveAndroidIntents") - OnNewIntent { - handleIntent(it) + OnNewIntent { + handleIntent(it) + } } - } private fun handleIntent(intent: Intent?) { - if(appContext.currentActivity == null || intent == null) return + if (appContext.currentActivity == null || intent == null) return if (intent.action == Intent.ACTION_SEND) { if (intent.type == "text/plain") { @@ -40,7 +41,7 @@ class ExpoReceiveAndroidIntentsModule : Module() { private fun handleTextIntent(intent: Intent) { intent.getStringExtra(Intent.EXTRA_TEXT)?.let { val encoded = URLEncoder.encode(it, "UTF-8") - "bluesky://intent/compose?text=${encoded}".toUri().let { uri -> + "bluesky://intent/compose?text=$encoded".toUri().let { uri -> val newIntent = Intent(Intent.ACTION_VIEW, uri) appContext.currentActivity?.startActivity(newIntent) } @@ -48,11 +49,12 @@ class ExpoReceiveAndroidIntentsModule : Module() { } private fun handleImageIntent(intent: Intent) { - 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) - } + 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)) @@ -76,16 +78,16 @@ class ExpoReceiveAndroidIntentsModule : Module() { uris.forEachIndexed { index, uri -> val info = getImageInfo(uri) val params = buildUriData(info) - allParams = "${allParams}${params}" + allParams = "${allParams}$params" if (index < uris.count() - 1) { - allParams = "${allParams}," + allParams = "$allParams," } } val encoded = URLEncoder.encode(allParams, "UTF-8") - "bluesky://intent/compose?imageUris=${encoded}".toUri().let { + "bluesky://intent/compose?imageUris=$encoded".toUri().let { val newIntent = Intent(Intent.ACTION_VIEW, it) appContext.currentActivity?.startActivity(newIntent) } @@ -104,7 +106,7 @@ class ExpoReceiveAndroidIntentsModule : Module() { return mapOf( "width" to bitmap.width, "height" to bitmap.height, - "path" to file.path.toString() + "path" to file.path.toString(), ) } @@ -114,6 +116,6 @@ class ExpoReceiveAndroidIntentsModule : Module() { val path = info.getValue("path") val width = info.getValue("width") val height = info.getValue("height") - return "file://${path}|${width}|${height}" + return "file://$path|$width|$height" } } diff --git a/modules/expo-scroll-forwarder/ios/ExpoScrollForwarderModule.swift b/modules/expo-scroll-forwarder/ios/ExpoScrollForwarderModule.swift index c4ecc788e5..53e2588258 100644 --- a/modules/expo-scroll-forwarder/ios/ExpoScrollForwarderModule.swift +++ b/modules/expo-scroll-forwarder/ios/ExpoScrollForwarderModule.swift @@ -3,7 +3,7 @@ import ExpoModulesCore public class ExpoScrollForwarderModule: Module { public func definition() -> ModuleDefinition { Name("ExpoScrollForwarder") - + View(ExpoScrollForwarderView.self) { Prop("scrollViewTag") { (view: ExpoScrollForwarderView, prop: Int) in view.scrollViewTag = prop diff --git a/modules/expo-scroll-forwarder/ios/ExpoScrollForwarderView.swift b/modules/expo-scroll-forwarder/ios/ExpoScrollForwarderView.swift index 9c0e2f8728..15993ef29e 100644 --- a/modules/expo-scroll-forwarder/ios/ExpoScrollForwarderView.swift +++ b/modules/expo-scroll-forwarder/ios/ExpoScrollForwarderView.swift @@ -8,17 +8,17 @@ class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate { self.tryFindScrollView() } } - + private var rctScrollView: RCTScrollView? private var rctRefreshCtrl: RCTRefreshControl? private var cancelGestureRecognizers: [UIGestureRecognizer]? private var animTimer: Timer? private var initialOffset: CGFloat = 0.0 private var didImpact: Bool = false - + required init(appContext: AppContext? = nil) { super.init(appContext: appContext) - + let pg = UIPanGestureRecognizer(target: self, action: #selector(callOnPan(_:))) pg.delegate = self self.addGestureRecognizer(pg) @@ -34,28 +34,27 @@ class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate { self.cancelGestureRecognizers = [lpg, tg] } - // We don't want to recognize the scroll pan gesture and the swipe back gesture together func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { if gestureRecognizer is UIPanGestureRecognizer, otherGestureRecognizer is UIPanGestureRecognizer { return false } - + return true } - + // We only want the "scroll" gesture to happen whenever the pan is vertical, otherwise it will // interfere with the native swipe back gesture. override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { guard let gestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer else { return true } - + let velocity = gestureRecognizer.velocity(in: self) return abs(velocity.y) > abs(velocity.x) } - + // This will be used to cancel the scroll animation whenever we tap inside of the header. We don't need another // recognizer for this one. override func touchesBegan(_ touches: Set, with event: UIEvent?) { @@ -64,32 +63,32 @@ class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate { // This will be used to cancel the animation whenever we press inside of the scroll view. We don't want to change // the scroll view gesture's delegate, so we add an additional recognizer to detect this. - @IBAction func callOnPress(_ sender: UITapGestureRecognizer) -> Void { + @IBAction func callOnPress(_ sender: UITapGestureRecognizer) { self.stopTimer() } - - @IBAction func callOnPan(_ sender: UIPanGestureRecognizer) -> Void { + + @IBAction func callOnPan(_ sender: UIPanGestureRecognizer) { guard let rctsv = self.rctScrollView, let sv = rctsv.scrollView else { return } let translation = sender.translation(in: self).y - + if sender.state == .began { if sv.contentOffset.y < 0 { sv.contentOffset.y = 0 } - + self.initialOffset = sv.contentOffset.y } if sender.state == .changed { sv.contentOffset.y = self.dampenOffset(-translation + self.initialOffset) - + if sv.contentOffset.y <= -130, !didImpact { let generator = UIImpactFeedbackGenerator(style: .light) generator.impactOccurred() - + self.didImpact = true } } @@ -97,7 +96,7 @@ class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate { if sender.state == .ended { let velocity = sender.velocity(in: self).y self.didImpact = false - + if sv.contentOffset.y <= -130 { self.rctRefreshCtrl?.forwarderBeginRefreshing() return @@ -108,40 +107,40 @@ class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate { if abs(velocity) < 250, sv.contentOffset.y >= 0 { return } - + self.startDecayAnimation(translation, velocity) } } - + func startDecayAnimation(_ translation: CGFloat, _ velocity: CGFloat) { guard let sv = self.rctScrollView?.scrollView else { return } - + var velocity = velocity - + self.enableCancelGestureRecognizers() - + if velocity > 0 { velocity = min(velocity, 5000) } else { velocity = max(velocity, -5000) } - + var animTranslation = -translation - self.animTimer = Timer.scheduledTimer(withTimeInterval: 1.0 / 120, repeats: true) { timer in + self.animTimer = Timer.scheduledTimer(withTimeInterval: 1.0 / 120, repeats: true) { _ in velocity *= 0.9875 animTranslation = (-velocity / 120) + animTranslation - + let nextOffset = self.dampenOffset(animTranslation + self.initialOffset) - + if nextOffset <= 0 { if self.initialOffset <= 1 { self.scrollToOffset(0) } else { sv.contentOffset.y = 0 } - + self.stopTimer() return } else { @@ -153,61 +152,60 @@ class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate { } } } - + func dampenOffset(_ offset: CGFloat) -> CGFloat { if offset < 0 { return offset - (offset * 0.55) } - + return offset } - + func tryFindScrollView() { guard let scrollViewTag = scrollViewTag else { return } - + // Before we switch to a different scrollview, we always want to remove the cancel gesture recognizer. // Otherwise we might end up with duplicates when we switch back to that scrollview. self.removeCancelGestureRecognizers() - + self.rctScrollView = self.appContext? .findView(withTag: scrollViewTag, ofType: RCTScrollView.self) self.rctRefreshCtrl = self.rctScrollView?.scrollView.refreshControl as? RCTRefreshControl - + self.addCancelGestureRecognizers() } - + func addCancelGestureRecognizers() { self.cancelGestureRecognizers?.forEach { r in self.rctScrollView?.scrollView?.addGestureRecognizer(r) } } - + func removeCancelGestureRecognizers() { self.cancelGestureRecognizers?.forEach { r in self.rctScrollView?.scrollView?.removeGestureRecognizer(r) } } - func enableCancelGestureRecognizers() { self.cancelGestureRecognizers?.forEach { r in r.isEnabled = true } } - + func disableCancelGestureRecognizers() { self.cancelGestureRecognizers?.forEach { r in r.isEnabled = false } } - - func scrollToOffset(_ offset: Int, animated: Bool = true) -> Void { + + func scrollToOffset(_ offset: Int, animated: Bool = true) { self.rctScrollView?.scroll(toOffset: CGPoint(x: 0, y: offset), animated: animated) } - func stopTimer() -> Void { + func stopTimer() { self.disableCancelGestureRecognizers() self.animTimer?.invalidate() self.animTimer = nil diff --git a/package.json b/package.json index 6b11fc133b..141646d107 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,8 @@ "test-ci": "NODE_ENV=test jest --ci --forceExit --reporters=default --reporters=jest-junit", "test-coverage": "NODE_ENV=test jest --coverage", "lint": "eslint --cache --ext .js,.jsx,.ts,.tsx src", + "lint-native": "swiftlint ./modules && ktlint ./modules", + "lint-native:fix": "swiftlint --fix ./modules && ktlint --format ./modules", "typecheck": "tsc --project ./tsconfig.check.json", "e2e:mock-server": "./jest/dev-infra/with-test-redis-and-db.sh ts-node --project tsconfig.e2e.json __e2e__/mock-server.ts", "e2e:metro": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios", From 83e8522e0a89be28b1733f4c50dbd4379d98d03b Mon Sep 17 00:00:00 2001 From: Hailey Date: Thu, 11 Jul 2024 18:37:43 -0700 Subject: [PATCH 18/22] Create shared preferences API (#4654) --- __e2e__/flows/shared-prefs.yml | 31 +++ .../ExpoBlueskyDevicePrefsModule.kt | 11 - .../ExpoBlueskySharedPrefsModule.kt | 69 +++++ .../sharedprefs/SharedPrefs.kt | 241 ++++++++++++++++++ .../expo-module.config.json | 4 +- modules/expo-bluesky-swiss-army/index.ts | 4 +- .../ExpoBlueskyDevicePrefsModule.swift | 23 -- .../ExpoBlueskySharedPrefsModule.swift | 62 +++++ .../ios/SharedPrefs/SharedPrefs.swift | 89 +++++++ .../src/DevicePrefs/index.ios.ts | 18 -- .../src/DevicePrefs/index.ts | 16 -- .../src/SharedPrefs/index.native.ts | 51 ++++ .../src/SharedPrefs/index.ts | 36 +++ package.json | 1 + src/Navigation.tsx | 6 + .../hooks/useStarterPackEntry.native.ts | 14 +- src/lib/routes/types.ts | 1 + .../E2E/SharedPreferencesTesterScreen.tsx | 113 ++++++++ src/view/screens/Storybook/Dialogs.tsx | 13 + 19 files changed, 722 insertions(+), 81 deletions(-) create mode 100644 __e2e__/flows/shared-prefs.yml delete mode 100644 modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/deviceprefs/ExpoBlueskyDevicePrefsModule.kt create mode 100644 modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/sharedprefs/ExpoBlueskySharedPrefsModule.kt create mode 100644 modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/sharedprefs/SharedPrefs.kt delete mode 100644 modules/expo-bluesky-swiss-army/ios/DevicePrefs/ExpoBlueskyDevicePrefsModule.swift create mode 100644 modules/expo-bluesky-swiss-army/ios/SharedPrefs/ExpoBlueskySharedPrefsModule.swift create mode 100644 modules/expo-bluesky-swiss-army/ios/SharedPrefs/SharedPrefs.swift delete mode 100644 modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ios.ts delete mode 100644 modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ts create mode 100644 modules/expo-bluesky-swiss-army/src/SharedPrefs/index.native.ts create mode 100644 modules/expo-bluesky-swiss-army/src/SharedPrefs/index.ts create mode 100644 src/screens/E2E/SharedPreferencesTesterScreen.tsx diff --git a/__e2e__/flows/shared-prefs.yml b/__e2e__/flows/shared-prefs.yml new file mode 100644 index 0000000000..73a0668298 --- /dev/null +++ b/__e2e__/flows/shared-prefs.yml @@ -0,0 +1,31 @@ +appId: xyz.blueskyweb.app +--- +- runScript: + file: ../setupServer.js + env: + SERVER_PATH: "?users&posts&feeds" +- runFlow: + file: ../setupApp.yml +- tapOn: + id: "e2eSignInAlice" +- tapOn: "/sys/debug" +- tapOn: + id: "sharedPrefsTestOpenBtn" +- tapOn: + id: "setStringBtn" +- assertVisible: "Hello" +- tapOn: + id: "removeStringBtn" +- assertVisible: "null" +- tapOn: + id: "setBoolBtn" +- assertVisible: "true" +- tapOn: + id: "setNumberBtn" +- assertVisible: "123" +- tapOn: + id: "addToSetBtn" +- assertVisible: "true" +- tapOn: + id: "removeFromSetBtn" +- assertVisible: "false" diff --git a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/deviceprefs/ExpoBlueskyDevicePrefsModule.kt b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/deviceprefs/ExpoBlueskyDevicePrefsModule.kt deleted file mode 100644 index 51f9fe45de..0000000000 --- a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/deviceprefs/ExpoBlueskyDevicePrefsModule.kt +++ /dev/null @@ -1,11 +0,0 @@ -package expo.modules.blueskyswissarmy.deviceprefs - -import expo.modules.kotlin.modules.Module -import expo.modules.kotlin.modules.ModuleDefinition - -class ExpoBlueskyDevicePrefsModule : Module() { - override fun definition() = - ModuleDefinition { - Name("ExpoBlueskyDevicePrefs") - } -} diff --git a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/sharedprefs/ExpoBlueskySharedPrefsModule.kt b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/sharedprefs/ExpoBlueskySharedPrefsModule.kt new file mode 100644 index 0000000000..271956878a --- /dev/null +++ b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/sharedprefs/ExpoBlueskySharedPrefsModule.kt @@ -0,0 +1,69 @@ +package expo.modules.blueskyswissarmy.sharedprefs + +import android.content.Context +import android.util.Log +import expo.modules.kotlin.jni.JavaScriptValue +import expo.modules.kotlin.modules.Module +import expo.modules.kotlin.modules.ModuleDefinition + +class ExpoBlueskySharedPrefsModule : Module() { + private fun getContext(): Context { + val context = appContext.reactContext ?: throw Error("Context is null") + return context + } + + override fun definition() = + ModuleDefinition { + Name("ExpoBlueskySharedPrefs") + + Function("setString") { key: String, value: String -> + return@Function SharedPrefs(getContext()).setValue(key, value) + } + + Function("setValue") { key: String, value: JavaScriptValue -> + val context = getContext() + Log.d("ExpoBlueskySharedPrefs", "Setting value for key: $key") + try { + if (value.isNumber()) { + SharedPrefs(context).setValue(key, value.getFloat()) + } else if (value.isBool()) { + SharedPrefs(context).setValue(key, value.getBool()) + } else if (value.isNull() || value.isUndefined()) { + SharedPrefs(context).removeValue(key) + } else { + Log.d(NAME, "Unsupported type: ${value.kind()}") + } + } catch (e: Error) { + Log.d(NAME, "Error setting value: $e") + } + } + + Function("removeValue") { key: String -> + return@Function SharedPrefs(getContext()).removeValue(key) + } + + Function("getString") { key: String -> + return@Function SharedPrefs(getContext()).getString(key) + } + + Function("getNumber") { key: String -> + return@Function SharedPrefs(getContext()).getFloat(key) + } + + Function("getBool") { key: String -> + return@Function SharedPrefs(getContext()).getBoolean(key) + } + + Function("addToSet") { key: String, value: String -> + return@Function SharedPrefs(getContext()).addToSet(key, value) + } + + Function("removeFromSet") { key: String, value: String -> + return@Function SharedPrefs(getContext()).removeFromSet(key, value) + } + + Function("setContains") { key: String, value: String -> + return@Function SharedPrefs(getContext()).setContains(key, value) + } + } +} diff --git a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/sharedprefs/SharedPrefs.kt b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/sharedprefs/SharedPrefs.kt new file mode 100644 index 0000000000..38d79abf1f --- /dev/null +++ b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/sharedprefs/SharedPrefs.kt @@ -0,0 +1,241 @@ +package expo.modules.blueskyswissarmy.sharedprefs + +import android.content.Context +import android.content.SharedPreferences +import android.util.Log + +val DEFAULTS = + mapOf( + "playSoundChat" to true, + "playSoundFollow" to false, + "playSoundLike" to false, + "playSoundMention" to false, + "playSoundQuote" to false, + "playSoundReply" to false, + "playSoundRepost" to false, + "badgeCount" to 0, + ) + +const val NAME = "SharedPrefs" + +class SharedPrefs( + private val context: Context, +) { + companion object { + private var hasInitialized = false + + private var instance: SharedPreferences? = null + + fun getInstance( + context: Context, + info: String? = "(no info)", + ): SharedPreferences { + if (instance == null) { + Log.d(NAME, "No preferences instance found, creating one.") + instance = context.getSharedPreferences("xyz.blueskyweb.app", Context.MODE_PRIVATE) + } + + val safeInstance = instance ?: throw Error("Preferences is null: $info") + + if (!hasInitialized) { + Log.d(NAME, "Preferences instance has not been initialized yet.") + initialize(safeInstance) + hasInitialized = true + Log.d(NAME, "Preferences instance has been initialized.") + } + + return safeInstance + } + + private fun initialize(instance: SharedPreferences) { + instance + .edit() + .apply { + DEFAULTS.forEach { (key, value) -> + if (instance.contains(key)) { + return@forEach + } + + when (value) { + is Boolean -> { + putBoolean(key, value) + } + + is String -> { + putString(key, value) + } + + is Array<*> -> { + putStringSet(key, value.map { it.toString() }.toSet()) + } + + is Map<*, *> -> { + putStringSet(key, value.map { it.toString() }.toSet()) + } + } + } + }.apply() + } + } + + fun setValue( + key: String, + value: String, + ) { + val safeInstance = getInstance(context) + safeInstance + .edit() + .apply { + putString(key, value) + }.apply() + } + + fun setValue( + key: String, + value: Float, + ) { + val safeInstance = getInstance(context) + safeInstance + .edit() + .apply { + putFloat(key, value) + }.apply() + } + + fun setValue( + key: String, + value: Boolean, + ) { + val safeInstance = getInstance(context) + safeInstance + .edit() + .apply { + putBoolean(key, value) + }.apply() + } + + fun setValue( + key: String, + value: Set, + ) { + val safeInstance = getInstance(context) + safeInstance + .edit() + .apply { + putStringSet(key, value) + }.apply() + } + + fun removeValue(key: String) { + val safeInstance = getInstance(context) + safeInstance + .edit() + .apply { + remove(key) + }.apply() + } + + fun getString(key: String): String? { + val safeInstance = getInstance(context) + return safeInstance.getString(key, null) + } + + fun getFloat(key: String): Float? { + val safeInstance = getInstance(context) + if (!safeInstance.contains(key)) { + return null + } + return safeInstance.getFloat(key, 0.0f) + } + + @Suppress("ktlint:standard:function-naming") + fun _setAnyValue( + key: String, + value: Any, + ) { + val safeInstance = getInstance(context) + safeInstance + .edit() + .apply { + when (value) { + is String -> putString(key, value) + is Float -> putFloat(key, value) + is Boolean -> putBoolean(key, value) + is Set<*> -> putStringSet(key, value.map { it.toString() }.toSet()) + else -> throw Error("Unsupported type: ${value::class.java}") + } + }.apply() + } + + fun getBoolean(key: String): Boolean? { + val safeInstance = getInstance(context) + if (!safeInstance.contains(key)) { + return null + } + Log.d(NAME, "Getting boolean for key: $key") + val res = safeInstance.getBoolean(key, false) + Log.d(NAME, "Got boolean for key: $key, value: $res") + return res + } + + fun addToSet( + key: String, + value: String, + ) { + val safeInstance = getInstance(context) + val set = safeInstance.getStringSet(key, setOf()) ?: setOf() + val newSet = + set.toMutableSet().apply { + add(value) + } + safeInstance + .edit() + .apply { + putStringSet(key, newSet) + }.apply() + } + + fun removeFromSet( + key: String, + value: String, + ) { + val safeInstance = getInstance(context) + val set = safeInstance.getStringSet(key, setOf()) ?: setOf() + val newSet = + set.toMutableSet().apply { + remove(value) + } + safeInstance + .edit() + .apply { + putStringSet(key, newSet) + }.apply() + } + + fun setContains( + key: String, + value: String, + ): Boolean { + val safeInstance = getInstance(context) + val set = safeInstance.getStringSet(key, setOf()) ?: setOf() + return set.contains(value) + } + + fun hasValue(key: String): Boolean { + val safeInstance = getInstance(context) + return safeInstance.contains(key) + } + + fun getValues(keys: Set): Map { + val safeInstance = getInstance(context) + return keys.associateWith { key -> + when (val value = safeInstance.all[key]) { + is String -> value + is Float -> value + is Boolean -> value + is Set<*> -> value + else -> null + } + } + } +} diff --git a/modules/expo-bluesky-swiss-army/expo-module.config.json b/modules/expo-bluesky-swiss-army/expo-module.config.json index 730bc6114f..1111f8a0be 100644 --- a/modules/expo-bluesky-swiss-army/expo-module.config.json +++ b/modules/expo-bluesky-swiss-army/expo-module.config.json @@ -1,11 +1,11 @@ { "platforms": ["ios", "tvos", "android", "web"], "ios": { - "modules": ["ExpoBlueskyDevicePrefsModule", "ExpoBlueskyReferrerModule"] + "modules": ["ExpoBlueskySharedPrefsModule", "ExpoBlueskyReferrerModule"] }, "android": { "modules": [ - "expo.modules.blueskyswissarmy.deviceprefs.ExpoBlueskyDevicePrefsModule", + "expo.modules.blueskyswissarmy.sharedprefs.ExpoBlueskySharedPrefsModule", "expo.modules.blueskyswissarmy.referrer.ExpoBlueskyReferrerModule" ] } diff --git a/modules/expo-bluesky-swiss-army/index.ts b/modules/expo-bluesky-swiss-army/index.ts index 1b2f892494..89cea00a28 100644 --- a/modules/expo-bluesky-swiss-army/index.ts +++ b/modules/expo-bluesky-swiss-army/index.ts @@ -1,4 +1,4 @@ -import * as DevicePrefs from './src/DevicePrefs' import * as Referrer from './src/Referrer' +import * as SharedPrefs from './src/SharedPrefs' -export {DevicePrefs, Referrer} +export {Referrer, SharedPrefs} diff --git a/modules/expo-bluesky-swiss-army/ios/DevicePrefs/ExpoBlueskyDevicePrefsModule.swift b/modules/expo-bluesky-swiss-army/ios/DevicePrefs/ExpoBlueskyDevicePrefsModule.swift deleted file mode 100644 index b13a9fe3fa..0000000000 --- a/modules/expo-bluesky-swiss-army/ios/DevicePrefs/ExpoBlueskyDevicePrefsModule.swift +++ /dev/null @@ -1,23 +0,0 @@ -import ExpoModulesCore - -public class ExpoBlueskyDevicePrefsModule: Module { - func getDefaults(_ useAppGroup: Bool) -> UserDefaults? { - if useAppGroup { - return UserDefaults(suiteName: "group.app.bsky") - } else { - return UserDefaults.standard - } - } - - public func definition() -> ModuleDefinition { - Name("ExpoBlueskyDevicePrefs") - - AsyncFunction("getStringValueAsync") { (key: String, useAppGroup: Bool) in - return self.getDefaults(useAppGroup)?.string(forKey: key) - } - - AsyncFunction("setStringValueAsync") { (key: String, value: String?, useAppGroup: Bool) in - self.getDefaults(useAppGroup)?.setValue(value, forKey: key) - } - } -} diff --git a/modules/expo-bluesky-swiss-army/ios/SharedPrefs/ExpoBlueskySharedPrefsModule.swift b/modules/expo-bluesky-swiss-army/ios/SharedPrefs/ExpoBlueskySharedPrefsModule.swift new file mode 100644 index 0000000000..8549e5b48b --- /dev/null +++ b/modules/expo-bluesky-swiss-army/ios/SharedPrefs/ExpoBlueskySharedPrefsModule.swift @@ -0,0 +1,62 @@ +import Foundation +import ExpoModulesCore + +public class ExpoBlueskySharedPrefsModule: Module { + let defaults = UserDefaults(suiteName: "group.app.bsky") + + func getDefaults(_ info: String = "(no info)") -> UserDefaults? { + guard let defaults = self.defaults else { + NSLog("Failed to get defaults for app group: \(info)") + return nil + } + return defaults + } + + public func definition() -> ModuleDefinition { + Name("ExpoBlueskySharedPrefs") + + // JavaScripValue causes a crash when trying to check `isString()`. Let's + // explicitly define setString instead. + Function("setString") { (key: String, value: String?) in + SharedPrefs.shared.setValue(key, value) + } + + Function("setValue") { (key: String, value: JavaScriptValue) in + if value.isNumber() { + SharedPrefs.shared.setValue(key, value.getDouble()) + } else if value.isBool() { + SharedPrefs.shared.setValue(key, value.getBool()) + } else if value.isNull() || value.isUndefined() { + SharedPrefs.shared.removeValue(key) + } + } + + Function("removeValue") { (key: String) in + SharedPrefs.shared.removeValue(key) + } + + Function("getString") { (key: String) in + return SharedPrefs.shared.getString(key) + } + + Function("getBool") { (key: String) in + return SharedPrefs.shared.getBool(key) + } + + Function("getNumber") { (key: String) in + return SharedPrefs.shared.getNumber(key) + } + + Function("addToSet") { (key: String, value: String) in + SharedPrefs.shared.addToSet(key, value) + } + + Function("removeFromSet") { (key: String, value: String) in + SharedPrefs.shared.removeFromSet(key, value) + } + + Function("setContains") { (key: String, value: String) in + return SharedPrefs.shared.setContains(key, value) + } + } +} diff --git a/modules/expo-bluesky-swiss-army/ios/SharedPrefs/SharedPrefs.swift b/modules/expo-bluesky-swiss-army/ios/SharedPrefs/SharedPrefs.swift new file mode 100644 index 0000000000..a11a71834c --- /dev/null +++ b/modules/expo-bluesky-swiss-army/ios/SharedPrefs/SharedPrefs.swift @@ -0,0 +1,89 @@ +import Foundation + +public class SharedPrefs { + public static let shared = SharedPrefs() + + private let defaults = UserDefaults(suiteName: "group.app.bsky") + + init() { + if defaults == nil { + NSLog("Failed to get user defaults for app group.") + } + } + + private func getDefaults(_ info: String = "(no info)") -> UserDefaults? { + guard let defaults = self.defaults else { + NSLog("Failed to get defaults for app group: \(info)") + return nil + } + return defaults + } + + public func setValue(_ key: String, _ value: String?) { + getDefaults(key)?.setValue(value, forKey: key) + } + + public func setValue(_ key: String, _ value: Double?) { + getDefaults(key)?.setValue(value, forKey: key) + } + + public func setValue(_ key: String, _ value: Bool?) { + getDefaults(key)?.setValue(value, forKey: key) + } + + public func _setAnyValue(_ key: String, _ value: Any?) { + getDefaults(key)?.setValue(value, forKey: key) + } + + public func removeValue(_ key: String) { + getDefaults(key)?.removeObject(forKey: key) + } + + public func getString(_ key: String) -> String? { + return getDefaults(key)?.string(forKey: key) + } + + public func getNumber(_ key: String) -> Double? { + return getDefaults(key)?.double(forKey: key) + } + + public func getBool(_ key: String) -> Bool? { + return getDefaults(key)?.bool(forKey: key) + } + + public func addToSet(_ key: String, _ value: String) { + var dict: [String: Bool]? + if var currDict = getDefaults(key)?.dictionary(forKey: key) as? [String: Bool] { + currDict[value] = true + dict = currDict + } else { + dict = [ + value: true + ] + } + getDefaults(key)?.setValue(dict, forKey: key) + } + + public func removeFromSet(_ key: String, _ value: String) { + guard var dict = getDefaults(key)?.dictionary(forKey: key) as? [String: Bool] else { + return + } + dict.removeValue(forKey: value) + getDefaults(key)?.setValue(dict, forKey: key) + } + + public func setContains(_ key: String, _ value: String) -> Bool { + guard let dict = getDefaults(key)?.dictionary(forKey: key) as? [String: Bool] else { + return false + } + return dict[value] == true + } + + public func hasValue(_ key: String) -> Bool { + return getDefaults(key)?.value(forKey: key) != nil + } + + public func getValues(_ keys: [String]) -> [String: Any?]? { + return getDefaults("keys:\(keys)")?.dictionaryWithValues(forKeys: keys) + } +} diff --git a/modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ios.ts b/modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ios.ts deleted file mode 100644 index 4271850866..0000000000 --- a/modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ios.ts +++ /dev/null @@ -1,18 +0,0 @@ -import {requireNativeModule} from 'expo-modules-core' - -const NativeModule = requireNativeModule('ExpoBlueskyDevicePrefs') - -export function getStringValueAsync( - key: string, - useAppGroup?: boolean, -): Promise { - return NativeModule.getStringValueAsync(key, useAppGroup) -} - -export function setStringValueAsync( - key: string, - value: string | null, - useAppGroup?: boolean, -): Promise { - return NativeModule.setStringValueAsync(key, value, useAppGroup) -} diff --git a/modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ts b/modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ts deleted file mode 100644 index f1eee6c282..0000000000 --- a/modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {NotImplementedError} from '../NotImplemented' - -export function getStringValueAsync( - key: string, - useAppGroup?: boolean, -): Promise { - throw new NotImplementedError({key, useAppGroup}) -} - -export function setStringValueAsync( - key: string, - value: string | null, - useAppGroup?: boolean, -): Promise { - throw new NotImplementedError({key, value, useAppGroup}) -} diff --git a/modules/expo-bluesky-swiss-army/src/SharedPrefs/index.native.ts b/modules/expo-bluesky-swiss-army/src/SharedPrefs/index.native.ts new file mode 100644 index 0000000000..0cea7c53ba --- /dev/null +++ b/modules/expo-bluesky-swiss-army/src/SharedPrefs/index.native.ts @@ -0,0 +1,51 @@ +import {requireNativeModule} from 'expo-modules-core' + +const NativeModule = requireNativeModule('ExpoBlueskySharedPrefs') + +export function setValue( + key: string, + value: string | number | boolean | null | undefined, +): void { + // A bug on Android causes `JavaScripValue.isString()` to cause a crash on some occasions, seemingly because of a + // memory violation. Instead, we will use a specific function to set strings on this platform. + if (typeof value === 'string') { + return NativeModule.setString(key, value) + } + return NativeModule.setValue(key, value) +} + +export function removeValue(key: string): void { + return NativeModule.removeValue(key) +} + +export function getString(key: string): string | undefined { + return nullToUndefined(NativeModule.getString(key)) +} + +export function getNumber(key: string): number | undefined { + return nullToUndefined(NativeModule.getNumber(key)) +} + +export function getBool(key: string): boolean | undefined { + return nullToUndefined(NativeModule.getBool(key)) +} + +export function addToSet(key: string, value: string): void { + return NativeModule.addToSet(key, value) +} + +export function removeFromSet(key: string, value: string): void { + return NativeModule.removeFromSet(key, value) +} + +export function setContains(key: string, value: string): boolean { + return NativeModule.setContains(key, value) +} + +// iOS returns `null` if a value does not exist, and Android returns `undefined. Normalize these here for JS types +function nullToUndefined(value: any) { + if (value == null) { + return undefined + } + return value +} diff --git a/modules/expo-bluesky-swiss-army/src/SharedPrefs/index.ts b/modules/expo-bluesky-swiss-army/src/SharedPrefs/index.ts new file mode 100644 index 0000000000..769344007b --- /dev/null +++ b/modules/expo-bluesky-swiss-army/src/SharedPrefs/index.ts @@ -0,0 +1,36 @@ +import {NotImplementedError} from '../NotImplemented' + +export function setValue( + key: string, + value: string | number | boolean | null | undefined, +): void { + throw new NotImplementedError({key, value}) +} + +export function removeValue(key: string): void { + throw new NotImplementedError({key}) +} + +export function getString(key: string): string | null { + throw new NotImplementedError({key}) +} + +export function getNumber(key: string): number | null { + throw new NotImplementedError({key}) +} + +export function getBool(key: string): boolean | null { + throw new NotImplementedError({key}) +} + +export function addToSet(key: string, value: string): void { + throw new NotImplementedError({key, value}) +} + +export function removeFromSet(key: string, value: string): void { + throw new NotImplementedError({key, value}) +} + +export function setContains(key: string, value: string): boolean { + throw new NotImplementedError({key, value}) +} diff --git a/package.json b/package.json index 141646d107..b56dd9e39b 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "typecheck": "tsc --project ./tsconfig.check.json", "e2e:mock-server": "./jest/dev-infra/with-test-redis-and-db.sh ts-node --project tsconfig.e2e.json __e2e__/mock-server.ts", "e2e:metro": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios", + "e2e:metro-android": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:android", "e2e:run": "maestro test __e2e__", "perf:test": "NODE_ENV=test maestro test", "perf:test:run": "NODE_ENV=test maestro test __e2e__/perf-test.yml", diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 4ecf3fff8c..4954351223 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -39,6 +39,7 @@ import {ModerationMutedAccounts} from 'view/screens/ModerationMutedAccounts' import {PreferencesFollowingFeed} from 'view/screens/PreferencesFollowingFeed' import {PreferencesThreads} from 'view/screens/PreferencesThreads' import {SavedFeeds} from 'view/screens/SavedFeeds' +import {SharedPreferencesTesterScreen} from '#/screens/E2E/SharedPreferencesTesterScreen' import HashtagScreen from '#/screens/Hashtag' import {ModerationScreen} from '#/screens/Moderation' import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers' @@ -233,6 +234,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { getComponent={() => DebugModScreen} options={{title: title(msg`Moderation states`), requireAuth: true}} /> + SharedPreferencesTesterScreen} + options={{title: title(msg`Shared Preferences Tester`)}} + /> LogScreen} diff --git a/src/components/hooks/useStarterPackEntry.native.ts b/src/components/hooks/useStarterPackEntry.native.ts index b6e4ab05b1..212ecae71d 100644 --- a/src/components/hooks/useStarterPackEntry.native.ts +++ b/src/components/hooks/useStarterPackEntry.native.ts @@ -7,7 +7,7 @@ import { import {isAndroid} from 'platform/detection' import {useHasCheckedForStarterPack} from 'state/preferences/used-starter-packs' import {useSetActiveStarterPack} from 'state/shell/starter-pack' -import {DevicePrefs, Referrer} from '../../../modules/expo-bluesky-swiss-army' +import {Referrer, SharedPrefs} from '../../../modules/expo-bluesky-swiss-army' export function useStarterPackEntry() { const [ready, setReady] = React.useState(false) @@ -39,14 +39,10 @@ export function useStarterPackEntry() { uri = createStarterPackLinkFromAndroidReferrer(res.installReferrer) } } else { - const res = await DevicePrefs.getStringValueAsync( - 'starterPackUri', - true, - ) - - if (res) { - uri = httpStarterPackUriToAtUri(res) - DevicePrefs.setStringValueAsync('starterPackUri', null, true) + const starterPackUri = SharedPrefs.getString('starterPackUri') + if (starterPackUri) { + uri = httpStarterPackUriToAtUri(starterPackUri) + SharedPrefs.setValue('starterPackUri', null) } } diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index 9d102f2483..bda93fb40d 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -25,6 +25,7 @@ export type CommonNavigatorParams = { ProfileLabelerLikedBy: {name: string} Debug: undefined DebugMod: undefined + SharedPreferencesTester: undefined Log: undefined Support: undefined PrivacyPolicy: undefined diff --git a/src/screens/E2E/SharedPreferencesTesterScreen.tsx b/src/screens/E2E/SharedPreferencesTesterScreen.tsx new file mode 100644 index 0000000000..380f1080b0 --- /dev/null +++ b/src/screens/E2E/SharedPreferencesTesterScreen.tsx @@ -0,0 +1,113 @@ +import React from 'react' +import {View} from 'react-native' + +import {ScrollView} from 'view/com/util/Views' +import {atoms as a} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import {Text} from '#/components/Typography' +import {SharedPrefs} from '../../../modules/expo-bluesky-swiss-army' + +export function SharedPreferencesTesterScreen() { + const [currentTestOutput, setCurrentTestOutput] = React.useState('') + + return ( + + + + {currentTestOutput} + + + + + + + + + + + + ) +} diff --git a/src/view/screens/Storybook/Dialogs.tsx b/src/view/screens/Storybook/Dialogs.tsx index 6d166d4b64..ca2420fed0 100644 --- a/src/view/screens/Storybook/Dialogs.tsx +++ b/src/view/screens/Storybook/Dialogs.tsx @@ -1,7 +1,9 @@ import React from 'react' import {View} from 'react-native' +import {useNavigation} from '@react-navigation/native' import {useDialogStateControlContext} from '#/state/dialogs' +import {NavigationProp} from 'lib/routes/types' import {atoms as a} from '#/alf' import {Button, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' @@ -18,6 +20,7 @@ export function Dialogs() { const [shouldRenderUnmountTest, setShouldRenderUnmountTest] = React.useState(false) const unmountTestInterval = React.useRef() + const navigation = useNavigation() const onUnmountTestStartPressWithClose = () => { setShouldRenderUnmountTest(true) @@ -134,6 +137,16 @@ export function Dialogs() { End Unmount Test + + This is a prompt From 8b121af2e438ca77cc5f5b1715516107c18aff6f Mon Sep 17 00:00:00 2001 From: Hailey Date: Thu, 11 Jul 2024 18:43:10 -0700 Subject: [PATCH 19/22] referrers for all platforms (#4514) --- app.config.js | 1 + .../referrer/ExpoBlueskyReferrerModule.kt | 46 +++++++++++++++++++ .../src/Referrer/index.android.ts | 8 +++- .../src/Referrer/index.ios.ts | 37 +++++++++++++++ .../src/Referrer/index.ts | 7 ++- .../src/Referrer/index.web.ts | 34 ++++++++++++++ .../src/Referrer/types.ts | 17 ++++--- plugins/withAppDelegateReferrer.js | 41 +++++++++++++++++ src/Navigation.tsx | 15 +++++- src/lib/hooks/useIntentHandler.ts | 15 +++++- src/lib/statsig/events.ts | 5 ++ src/lib/statsig/statsig.tsx | 21 --------- 12 files changed, 213 insertions(+), 34 deletions(-) create mode 100644 modules/expo-bluesky-swiss-army/src/Referrer/index.ios.ts create mode 100644 modules/expo-bluesky-swiss-army/src/Referrer/index.web.ts create mode 100644 plugins/withAppDelegateReferrer.js diff --git a/app.config.js b/app.config.js index 1467f762fd..cd8a4b034b 100644 --- a/app.config.js +++ b/app.config.js @@ -221,6 +221,7 @@ module.exports = function (config) { './plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js', './plugins/shareExtension/withShareExtensions.js', './plugins/notificationsExtension/withNotificationsExtension.js', + './plugins/withAppDelegateReferrer.js', ].filter(Boolean), extra: { eas: { diff --git a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/referrer/ExpoBlueskyReferrerModule.kt b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/referrer/ExpoBlueskyReferrerModule.kt index ac6ed90b84..bac555233e 100644 --- a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/referrer/ExpoBlueskyReferrerModule.kt +++ b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/referrer/ExpoBlueskyReferrerModule.kt @@ -1,5 +1,8 @@ package expo.modules.blueskyswissarmy.referrer +import android.content.Intent +import android.net.Uri +import android.os.Build import android.util.Log import com.android.installreferrer.api.InstallReferrerClient import com.android.installreferrer.api.InstallReferrerStateListener @@ -8,10 +11,53 @@ import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition class ExpoBlueskyReferrerModule : Module() { + private var intent: Intent? = null + private var activityReferrer: Uri? = null + override fun definition() = ModuleDefinition { Name("ExpoBlueskyReferrer") + OnNewIntent { + intent = it + activityReferrer = appContext.currentActivity?.referrer + } + + AsyncFunction("getReferrerInfoAsync") { + val intentReferrer = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent?.getParcelableExtra(Intent.EXTRA_REFERRER, Uri::class.java) + } else { + intent?.getParcelableExtra(Intent.EXTRA_REFERRER) + } + + // Some apps explicitly set a referrer, like Chrome. In these cases, we prefer this since + // it's the actual website that the user came from rather than the app. + if (intentReferrer is Uri) { + val res = + mapOf( + "referrer" to intentReferrer.toString(), + "hostname" to intentReferrer.host, + ) + intent = null + return@AsyncFunction res + } + + // In all other cases, we'll just record the app that sent the intent. + if (activityReferrer != null) { + // referrer could become null here. `.toString()` though can be called on null + val res = + mapOf( + "referrer" to activityReferrer.toString(), + "hostname" to (activityReferrer?.host ?: ""), + ) + activityReferrer = null + return@AsyncFunction res + } + + return@AsyncFunction null + } + AsyncFunction("getGooglePlayReferrerInfoAsync") { promise: Promise -> val referrerClient = InstallReferrerClient.newBuilder(appContext.reactContext).build() referrerClient.startConnection( diff --git a/modules/expo-bluesky-swiss-army/src/Referrer/index.android.ts b/modules/expo-bluesky-swiss-army/src/Referrer/index.android.ts index 06dfd2d09c..ec2bcb57db 100644 --- a/modules/expo-bluesky-swiss-army/src/Referrer/index.android.ts +++ b/modules/expo-bluesky-swiss-army/src/Referrer/index.android.ts @@ -1,9 +1,13 @@ import {requireNativeModule} from 'expo' -import {GooglePlayReferrerInfo} from './types' +import {GooglePlayReferrerInfo, ReferrerInfo} from './types' export const NativeModule = requireNativeModule('ExpoBlueskyReferrer') -export function getGooglePlayReferrerInfoAsync(): Promise { +export function getGooglePlayReferrerInfoAsync(): Promise { return NativeModule.getGooglePlayReferrerInfoAsync() } + +export function getReferrerInfoAsync(): Promise { + return NativeModule.getReferrerInfoAsync() +} diff --git a/modules/expo-bluesky-swiss-army/src/Referrer/index.ios.ts b/modules/expo-bluesky-swiss-army/src/Referrer/index.ios.ts new file mode 100644 index 0000000000..2bf1497a0a --- /dev/null +++ b/modules/expo-bluesky-swiss-army/src/Referrer/index.ios.ts @@ -0,0 +1,37 @@ +import {SharedPrefs} from '../../index' +import {NotImplementedError} from '../NotImplemented' +import {GooglePlayReferrerInfo, ReferrerInfo} from './types' + +export function getGooglePlayReferrerInfoAsync(): Promise { + throw new NotImplementedError() +} + +export function getReferrerInfoAsync(): Promise { + const referrer = SharedPrefs.getString('referrer') + if (referrer) { + SharedPrefs.removeValue('referrer') + try { + const url = new URL(referrer) + return { + referrer, + hostname: url.hostname, + } + } catch (e) { + return { + referrer, + hostname: undefined, + } + } + } + + const referrerApp = SharedPrefs.getString('referrerApp') + if (referrerApp) { + SharedPrefs.removeValue('referrerApp') + return { + referrer: referrerApp, + hostname: referrerApp, + } + } + + return null +} diff --git a/modules/expo-bluesky-swiss-army/src/Referrer/index.ts b/modules/expo-bluesky-swiss-army/src/Referrer/index.ts index 2553985527..a60f7b6db8 100644 --- a/modules/expo-bluesky-swiss-army/src/Referrer/index.ts +++ b/modules/expo-bluesky-swiss-army/src/Referrer/index.ts @@ -1,7 +1,10 @@ import {NotImplementedError} from '../NotImplemented' -import {GooglePlayReferrerInfo} from './types' +import {GooglePlayReferrerInfo, ReferrerInfo} from './types' -// @ts-ignore throws export function getGooglePlayReferrerInfoAsync(): Promise { throw new NotImplementedError() } + +export function getReferrerInfoAsync(): Promise { + throw new NotImplementedError() +} diff --git a/modules/expo-bluesky-swiss-army/src/Referrer/index.web.ts b/modules/expo-bluesky-swiss-army/src/Referrer/index.web.ts new file mode 100644 index 0000000000..76f03e7c87 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/src/Referrer/index.web.ts @@ -0,0 +1,34 @@ +import {Platform} from 'react-native' + +import {NotImplementedError} from '../NotImplemented' +import {GooglePlayReferrerInfo, ReferrerInfo} from './types' + +export function getGooglePlayReferrerInfoAsync(): Promise { + throw new NotImplementedError() +} + +export function getReferrerInfoAsync(): Promise { + if ( + Platform.OS === 'web' && + // for ssr + typeof document !== 'undefined' && + document != null && + document.referrer + ) { + try { + const url = new URL(document.referrer) + if (url.hostname !== 'bsky.app') { + return { + referrer: url.href, + hostname: url.hostname, + } + } + } catch { + // If something happens to the URL parsing, we don't want to actually cause any problems for the user. Just + // log the error so we might catch it + console.error('Failed to parse referrer URL') + } + } + + return null +} diff --git a/modules/expo-bluesky-swiss-army/src/Referrer/types.ts b/modules/expo-bluesky-swiss-army/src/Referrer/types.ts index 55faaff4d3..921e3a6921 100644 --- a/modules/expo-bluesky-swiss-army/src/Referrer/types.ts +++ b/modules/expo-bluesky-swiss-army/src/Referrer/types.ts @@ -1,7 +1,10 @@ -export type GooglePlayReferrerInfo = - | { - installReferrer?: string - clickTimestamp?: number - installTimestamp?: number - } - | undefined +export type GooglePlayReferrerInfo = { + installReferrer?: string + clickTimestamp?: number + installTimestamp?: number +} + +export type ReferrerInfo = { + referrer: string + hostname: string +} diff --git a/plugins/withAppDelegateReferrer.js b/plugins/withAppDelegateReferrer.js new file mode 100644 index 0000000000..de773df075 --- /dev/null +++ b/plugins/withAppDelegateReferrer.js @@ -0,0 +1,41 @@ +const {withAppDelegate} = require('@expo/config-plugins') +const {mergeContents} = require('@expo/config-plugins/build/utils/generateCode') +const path = require('path') +const fs = require('fs') + +module.exports = config => { + // eslint-disable-next-line no-shadow + return withAppDelegate(config, async config => { + const delegatePath = path.join( + config.modRequest.platformProjectRoot, + 'AppDelegate.mm', + ) + + let newContents = config.modResults.contents + newContents = mergeContents({ + src: newContents, + anchor: '// Linking API', + newSrc: ` + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + [defaults setObject:options[UIApplicationOpenURLOptionsSourceApplicationKey] forKey:@"referrerApp"];\n`, + offset: 2, + tag: 'referrer info - deep links', + comment: '//', + }).contents + + newContents = mergeContents({ + src: newContents, + anchor: '// Universal Links', + newSrc: ` + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + [defaults setURL:userActivity.referrerURL forKey:@"referrer"];\n`, + offset: 2, + tag: 'referrer info - universal links', + comment: '//', + }).contents + + config.modResults.contents = newContents + + return config + }) +} diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 4954351223..8c815a3fe2 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -31,7 +31,7 @@ import { } from 'lib/routes/types' import {RouteParams, State} from 'lib/routes/types' import {bskyTitle} from 'lib/strings/headings' -import {isAndroid, isNative} from 'platform/detection' +import {isAndroid, isNative, isWeb} from 'platform/detection' import {PreferencesExternalEmbeds} from '#/view/screens/PreferencesExternalEmbeds' import {AppPasswords} from 'view/screens/AppPasswords' import {ModerationBlockedAccounts} from 'view/screens/ModerationBlockedAccounts' @@ -49,6 +49,7 @@ import { StarterPackScreenShort, } from '#/screens/StarterPack/StarterPackScreen' import {Wizard} from '#/screens/StarterPack/Wizard' +import {Referrer} from '../modules/expo-bluesky-swiss-army' import {init as initAnalytics} from './lib/analytics/analytics' import {useWebScrollRestoration} from './lib/hooks/useWebScrollRestoration' import {attachRouteToLogEvents, logEvent} from './lib/statsig/statsig' @@ -769,6 +770,18 @@ function logModuleInitTime() { initMs, }) + if (isWeb) { + Referrer.getReferrerInfoAsync().then(info => { + if (info && info.hostname !== 'bsky.app') { + logEvent('deepLink:referrerReceived', { + to: window.location.href, + referrer: info?.referrer, + hostname: info?.hostname, + }) + } + }) + } + if (__DEV__) { // This log is noisy, so keep false committed const shouldLog = false diff --git a/src/lib/hooks/useIntentHandler.ts b/src/lib/hooks/useIntentHandler.ts index 8741530b50..3235e1a6a5 100644 --- a/src/lib/hooks/useIntentHandler.ts +++ b/src/lib/hooks/useIntentHandler.ts @@ -1,9 +1,12 @@ import React from 'react' import * as Linking from 'expo-linking' + +import {logEvent} from 'lib/statsig/statsig' import {isNative} from 'platform/detection' -import {useComposerControls} from 'state/shell' import {useSession} from 'state/session' +import {useComposerControls} from 'state/shell' import {useCloseAllActiveElements} from 'state/util' +import {Referrer} from '../../../modules/expo-bluesky-swiss-army' type IntentType = 'compose' @@ -15,6 +18,16 @@ export function useIntentHandler() { React.useEffect(() => { const handleIncomingURL = (url: string) => { + Referrer.getReferrerInfoAsync().then(info => { + if (info && info.hostname !== 'bsky.app') { + logEvent('deepLink:referrerReceived', { + to: url, + referrer: info?.referrer, + hostname: info?.hostname, + }) + } + }) + // We want to be able to support bluesky:// deeplinks. It's unnatural for someone to use a deeplink with three // slashes, like bluesky:///intent/follow. However, supporting just two slashes causes us to have to take care // of two cases when parsing the url. If we ensure there is a third slash, we can always ensure the first diff --git a/src/lib/statsig/events.ts b/src/lib/statsig/events.ts index 4946fb7f2d..159061eac9 100644 --- a/src/lib/statsig/events.ts +++ b/src/lib/statsig/events.ts @@ -25,6 +25,11 @@ export type LogEvents = { } 'state:foreground:sampled': {} 'router:navigate:sampled': {} + 'deepLink:referrerReceived': { + to: string + referrer: string + hostname: string + } // Screen events 'splash:signInPressed': {} diff --git a/src/lib/statsig/statsig.tsx b/src/lib/statsig/statsig.tsx index 94a1e63d0e..81707d2bf5 100644 --- a/src/lib/statsig/statsig.tsx +++ b/src/lib/statsig/statsig.tsx @@ -28,8 +28,6 @@ type StatsigUser = { bundleDate: number refSrc: string refUrl: string - referrer: string - referrerHostname: string appLanguage: string contentLanguages: string[] } @@ -37,29 +35,12 @@ type StatsigUser = { let refSrc = '' let refUrl = '' -let referrer = '' -let referrerHostname = '' if (isWeb && typeof window !== 'undefined') { const params = new URLSearchParams(window.location.search) refSrc = params.get('ref_src') ?? '' refUrl = decodeURIComponent(params.get('ref_url') ?? '') } -if ( - isWeb && - typeof document !== 'undefined' && - document != null && - document.referrer -) { - try { - const url = new URL(document.referrer) - if (url.hostname !== 'bsky.app') { - referrer = document.referrer - referrerHostname = url.hostname - } - } catch {} -} - export type {LogEvents} function createStatsigOptions(prefetchUsers: StatsigUser[]) { @@ -222,8 +203,6 @@ function toStatsigUser(did: string | undefined): StatsigUser { custom: { refSrc, refUrl, - referrer, - referrerHostname, platform: Platform.OS as 'ios' | 'android' | 'web', bundleIdentifier: BUNDLE_IDENTIFIER, bundleDate: BUNDLE_DATE, From a71134f467449bc4c74967ca3958416feac6b91d Mon Sep 17 00:00:00 2001 From: Minseo Lee Date: Fri, 12 Jul 2024 11:42:27 +0900 Subject: [PATCH 20/22] `Reposted by you` and `Reply to you` (#4478) Co-authored-by: Hailey --- src/view/com/post/Post.tsx | 30 +++++++++------ src/view/com/posts/FeedItem.tsx | 67 +++++++++++++++++++++------------ 2 files changed, 62 insertions(+), 35 deletions(-) diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index 51a1381ec8..a05339d4dc 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -15,6 +15,7 @@ import {useQueryClient} from '@tanstack/react-query' import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped' import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow' import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {useSession} from '#/state/session' import {useComposerControls} from '#/state/shell/composer' import {MAX_POST_LINES} from 'lib/constants' import {usePalette} from 'lib/hooks/usePalette' @@ -145,6 +146,9 @@ function PostInner({ precacheProfile(queryClient, post.author) }, [queryClient, post.author]) + const {currentAccount} = useSession() + const isMe = replyAuthorDid === currentAccount?.did + return ( - - Reply to{' '} - - - - + {isMe ? ( + Reply to you + ) : ( + + Reply to{' '} + + + + + )} )} diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index cc767a4a3d..a59eeea52e 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -43,6 +43,7 @@ import {Text} from '../util/text/Text' import {PreviewableUserAvatar} from '../util/UserAvatar' import {AviFollowButton} from './AviFollowButton' import hairlineWidth = StyleSheet.hairlineWidth +import {useSession} from '#/state/session' import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost' interface FeedItemProps { @@ -200,6 +201,11 @@ let FeedItemInner = ({ }, ] + const {currentAccount} = useSession() + const isOwner = + AppBskyFeedDefs.isReasonRepost(reason) && + reason.by.did === currentAccount?.did + return ( - - Reposted by{' '} - - - - + {isOwner ? ( + Reposted by you + ) : ( + + Reposted by{' '} + + + + + )} ) : null} @@ -421,6 +435,9 @@ function ReplyToLabel({ blocked?: boolean }) { const pal = usePalette('default') + const {currentAccount} = useSession() + const isMe = profile.did === currentAccount?.did + return ( - {blocked ? ( + {isMe ? ( + Reply to you + ) : blocked ? ( Reply to a blocked post ) : ( From cd9bba4b69d1b5eeebae15544bb2aaa55f42873f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Go=C5=82da?= Date: Fri, 12 Jul 2024 04:42:41 +0200 Subject: [PATCH 21/22] Fix: emoji picker closes when hitting spacebar (#4422) (#4441) Co-authored-by: Hailey --- .../text-input/web/EmojiPicker.web.tsx | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/src/view/com/composer/text-input/web/EmojiPicker.web.tsx b/src/view/com/composer/text-input/web/EmojiPicker.web.tsx index 1493621160..1f4178f7f8 100644 --- a/src/view/com/composer/text-input/web/EmojiPicker.web.tsx +++ b/src/view/com/composer/text-input/web/EmojiPicker.web.tsx @@ -1,11 +1,13 @@ import React from 'react' -import Picker from '@emoji-mart/react' import { - StyleSheet, + GestureResponderEvent, TouchableWithoutFeedback, useWindowDimensions, View, } from 'react-native' +import Picker from '@emoji-mart/react' + +import {atoms as a} from '#/alf' import {textInputWebEmitter} from '../TextInput.web' const HEIGHT_OFFSET = 40 @@ -96,12 +98,29 @@ export function EmojiPicker({state, close}: IProps) { if (!state.isOpen) return null + const onPressBackdrop = (e: GestureResponderEvent) => { + // @ts-ignore web only + if (e.nativeEvent?.pointerId === -1) return + close() + } + return ( - + {/* eslint-disable-next-line react-native-a11y/has-valid-accessibility-descriptors */} e.stopPropagation()}> @@ -118,20 +137,3 @@ export function EmojiPicker({state, close}: IProps) { ) } - -const styles = StyleSheet.create({ - mask: { - // @ts-ignore web ony - position: 'fixed', - top: 0, - left: 0, - right: 0, - width: '100%', - height: '100%', - alignItems: 'center', - }, - picker: { - marginHorizontal: 'auto', - paddingRight: 50, - }, -}) From f021c06468820773fafeb93d14deecff81e08a17 Mon Sep 17 00:00:00 2001 From: Hailey Date: Thu, 11 Jul 2024 19:51:54 -0700 Subject: [PATCH 22/22] Return a promise for `getReferrerInfoAsync` on web (#4777) --- modules/expo-bluesky-swiss-army/src/Referrer/index.web.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/expo-bluesky-swiss-army/src/Referrer/index.web.ts b/modules/expo-bluesky-swiss-army/src/Referrer/index.web.ts index 76f03e7c87..c092a67e73 100644 --- a/modules/expo-bluesky-swiss-army/src/Referrer/index.web.ts +++ b/modules/expo-bluesky-swiss-army/src/Referrer/index.web.ts @@ -7,7 +7,7 @@ export function getGooglePlayReferrerInfoAsync(): Promise { +export async function getReferrerInfoAsync(): Promise { if ( Platform.OS === 'web' && // for ssr @@ -29,6 +29,5 @@ export function getReferrerInfoAsync(): Promise { console.error('Failed to parse referrer URL') } } - return null }