From 87d601e68d38b1337639b0d89cbd074147fa6236 Mon Sep 17 00:00:00 2001 From: "whey.party" <132627503+rimar1337@users.noreply.github.com> Date: Mon, 23 Sep 2024 22:24:36 +0700 Subject: [PATCH 01/24] changed white (gray_0) text to offwhite (gray_25) (#5453) --- src/alf/themes.ts | 2 +- src/lib/themes.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/alf/themes.ts b/src/alf/themes.ts index f5d2247f9f..9f7ec5c673 100644 --- a/src/alf/themes.ts +++ b/src/alf/themes.ts @@ -183,7 +183,7 @@ export function createThemes({ } as const const darkPalette: Palette = { - white: color.gray_0, + white: color.gray_25, black: color.trueBlack, contrast_25: color.gray_975, diff --git a/src/lib/themes.ts b/src/lib/themes.ts index 5030799932..eb11872fa3 100644 --- a/src/lib/themes.ts +++ b/src/lib/themes.ts @@ -325,11 +325,11 @@ export const darkTheme: Theme = { textInverted: colors.green2, }, inverted: { - background: lightPalette.white, + background: darkPalette.white, backgroundLight: lightPalette.contrast_50, text: lightPalette.black, textLight: lightPalette.contrast_700, - textInverted: lightPalette.white, + textInverted: darkPalette.white, link: lightPalette.primary_500, border: lightPalette.contrast_100, borderDark: lightPalette.contrast_200, From 5e333d4dfcc434f76fd54def2f965f54e112257b Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 23 Sep 2024 10:35:07 -0500 Subject: [PATCH 02/24] Resolve source files for fonts, remove hack (#5454) * Resolve source files for fonts, remove hack * Prettier * Prettier, add to hook --------- Co-authored-by: Dan Abramov --- package.json | 4 +++- src/alf/fonts.ts | 35 ----------------------------------- src/style.css | 28 ++++++++++++---------------- 3 files changed, 15 insertions(+), 52 deletions(-) diff --git a/package.json b/package.json index e3aff1fe0c..bb9706c39a 100644 --- a/package.json +++ b/package.json @@ -338,7 +338,9 @@ }, "lint-staged": { "*{.js,.jsx,.ts,.tsx}": [ - "eslint --cache --fix", + "eslint --cache --fix" + ], + "*{.js,.jsx,.ts,.tsx,.css}": [ "prettier --cache --write --ignore-unknown" ] } diff --git a/src/alf/fonts.ts b/src/alf/fonts.ts index 08cfd9f42d..b11ce939f8 100644 --- a/src/alf/fonts.ts +++ b/src/alf/fonts.ts @@ -1,5 +1,3 @@ -import {useFonts as defaultUseFonts} from 'expo-font' - import {isWeb} from '#/platform/detection' import {Device, device} from '#/storage' @@ -34,39 +32,6 @@ export function setFontFamily(fontFamily: Device['fontFamily']) { device.set(['fontFamily'], fontFamily) } -/* - * IMPORTANT: This is unused. Expo statically extracts these fonts, but we load - * them manually so that we can parallelize the loading along with the JS - * bundle. - * - * See `#/alf/util/useFonts` for the actually used hooks. - * - * All used fonts MUST be configured here. Unused fonts are commented out, but - * the files are there if we need them. - */ -export function DO_NOT_USE() { - return defaultUseFonts({ - // 'Inter-Thin': require('../../assets/fonts/inter/Inter-Thin.otf'), - // 'Inter-ThinItalic': require('../../assets/fonts/inter/Inter-ThinItalic.otf'), - // 'Inter-ExtraLight': require('../../assets/fonts/inter/Inter-ExtraLight.otf'), - // 'Inter-ExtraLightItalic': require('../../assets/fonts/inter/Inter-ExtraLightItalic.otf'), - // 'Inter-Light': require('../../assets/fonts/inter/Inter-Light.otf'), - // 'Inter-LightItalic': require('../../assets/fonts/inter/Inter-LightItalic.otf'), - 'Inter-Regular': require('../../assets/fonts/inter/Inter-Regular.otf'), - 'Inter-Italic': require('../../assets/fonts/inter/Inter-Italic.otf'), - // 'Inter-Medium': require('../../assets/fonts/inter/Inter-Medium.otf'), - // 'Inter-MediumItalic': require('../../assets/fonts/inter/Inter-MediumItalic.otf'), - 'Inter-SemiBold': require('../../assets/fonts/inter/Inter-SemiBold.otf'), - 'Inter-SemiBoldItalic': require('../../assets/fonts/inter/Inter-SemiBoldItalic.otf'), - // 'Inter-Bold': require('../../assets/fonts/inter/Inter-Bold.otf'), - // 'Inter-BoldItalic': require('../../assets/fonts/inter/Inter-BoldItalic.otf'), - 'Inter-ExtraBold': require('../../assets/fonts/inter/Inter-ExtraBold.otf'), - 'Inter-ExtraBoldItalic': require('../../assets/fonts/inter/Inter-ExtraBoldItalic.otf'), - // 'Inter-Black': require('../../assets/fonts/inter/Inter-Black.otf'), - // 'Inter-BlackItalic': require('../../assets/fonts/inter/Inter-BlackItalic.otf'), - }) -} - /* * Unused fonts are commented out, but the files are there if we need them. */ diff --git a/src/style.css b/src/style.css index 29e9770e32..ebb0471584 100644 --- a/src/style.css +++ b/src/style.css @@ -1,7 +1,7 @@ @font-face { font-family: 'Inter-Regular'; src: local('Inter-Regular'), - url(/static/media/Inter-Regular.1f5ed03b6dd9fd1f9982.otf) format('font/otf'); + url(/assets/fonts/inter/Inter-Regular.otf) format('font/otf'); font-weight: 400; font-style: normal; font-display: swap; @@ -9,7 +9,7 @@ @font-face { font-family: 'Inter-Italic'; src: local('Inter-Italic'), - url(/static/media/Inter-Italic.95778eb0c75dc956257e.otf) format('font/otf'); + url(/assets/fonts/inter/Inter-Italic.otf) format('font/otf'); font-weight: 400; font-style: italic; font-display: swap; @@ -17,14 +17,14 @@ /* @font-face { font-family: "Inter-Medium"; - src: local("Inter-Medium"), url(/static/media/Inter-Medium.296aa2d65964269836b3.otf) format("font/otf"); + src: local("Inter-Medium"), url(/assets/fonts/inter/Inter-Medium.otf) format("font/otf"); font-weight: 500; font-style: normal; font-display: swap; } @font-face { font-family: "Inter-MediumItalic"; - src: local("Inter-MediumItalic"), url(/static/media/Inter-MediumItalic.0e57e17a6311368e2114.otf) format("font/otf"); + src: local("Inter-MediumItalic"), url(/assets/fonts/inter/Inter-MediumItalic.otf) format("font/otf"); font-weight: 500; font-style: italic; font-display: swap; @@ -33,8 +33,7 @@ @font-face { font-family: 'Inter-SemiBold'; src: local('Inter-SemiBold'), - url(/static/media/Inter-SemiBold.2277990330981b8409bb.otf) - format('font/otf'); + url(/assets/fonts/inter/Inter-SemiBold.otf) format('font/otf'); font-weight: 600; font-style: normal; font-display: swap; @@ -42,8 +41,7 @@ @font-face { font-family: 'Inter-SemiBoldItalic'; src: local('Inter-SemiBoldItalic'), - url(/static/media/Inter-SemiBoldItalic.f62fea3df3a521d6c8a7.otf) - format('font/otf'); + url(/assets/fonts/inter/Inter-SemiBoldItalic.otf) format('font/otf'); font-weight: 600; font-style: italic; font-display: swap; @@ -51,14 +49,14 @@ /* @font-face { font-family: "Inter-Bold"; - src: local("Inter-Bold"), url(/static/media/Inter-Bold.8d330503e1d034ad68de.otf) format("font/otf"); + src: local("Inter-Bold"), url(/assets/fonts/inter/Inter-Bold.otf) format("font/otf"); font-weight: 700; font-style: normal; font-display: swap; } @font-face { font-family: "Inter-BoldItalic"; - src: local("Inter-BoldItalic"), url(/static/media/Inter-BoldItalic.bb17e63f9baa0d861a20.otf) format("font/otf"); + src: local("Inter-BoldItalic"), url(/assets/fonts/inter/Inter-BoldItalic.otf) format("font/otf"); font-weight: 700; font-style: italic; font-display: swap; @@ -67,8 +65,7 @@ @font-face { font-family: 'Inter-ExtraBold'; src: local('Inter-ExtraBold'), - url(/static/media/Inter-ExtraBold.ff2581a193bf6b7e0b06.otf) - format('font/otf'); + url(/assets/fonts/inter/Inter-ExtraBold.otf) format('font/otf'); font-weight: 800; font-style: normal; font-display: swap; @@ -76,8 +73,7 @@ @font-face { font-family: 'Inter-ExtraBoldItalic'; src: local('Inter-ExtraBoldItalic'), - url(/static/media/Inter-ExtraBoldItalic.0e50b40728d24d40fdf4.otf) - format('font/otf'); + url(/assets/fonts/inter/Inter-ExtraBoldItalic.otf) format('font/otf'); font-weight: 800; font-style: italic; font-display: swap; @@ -85,14 +81,14 @@ /* @font-face { font-family: "Inter-Black"; - src: local("Inter-Black"), url(/static/media/Inter-Black.66e9a87f1c921e844ed4.otf) format("font/otf"); + src: local("Inter-Black"), url(/assets/fonts/inter/Inter-Black.otf) format("font/otf"); font-weight: 900; font-style: normal; font-display: swap; } @font-face { font-family: "Inter-BlackItalic"; - src: local("Inter-BlackItalic"), url(/static/media/Inter-BlackItalic.27b9f0ad06fd13a7b9da.otf) format("font/otf"); + src: local("Inter-BlackItalic"), url(/assets/fonts/inter/Inter-BlackItalic.otf) format("font/otf"); font-weight: 900; font-style: italic; font-display: swap; From 443f3a64069f081764c2f49578108a9570e8e834 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Mon, 23 Sep 2024 16:35:16 +0100 Subject: [PATCH 03/24] Use pressable for video controls (#5452) * use pressable for video controls * add `as any` to preexisiting bad type * stop mutating prop --- src/view/com/pager/TabBar.tsx | 6 +-- src/view/com/util/PressableWithHover.tsx | 46 +++++++++---------- .../web-controls/ControlButton.tsx | 22 +++++---- .../web-controls/VideoControls.tsx | 11 +++-- 4 files changed, 43 insertions(+), 42 deletions(-) diff --git a/src/view/com/pager/TabBar.tsx b/src/view/com/pager/TabBar.tsx index 59bb77e367..d36d794b72 100644 --- a/src/view/com/pager/TabBar.tsx +++ b/src/view/com/pager/TabBar.tsx @@ -1,9 +1,9 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react' import {LayoutChangeEvent, ScrollView, StyleSheet, View} from 'react-native' +import {usePalette} from '#/lib/hooks/usePalette' +import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {isNative} from '#/platform/detection' -import {usePalette} from 'lib/hooks/usePalette' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {PressableWithHover} from '../util/PressableWithHover' import {Text} from '../util/text/Text' import {DraggableScrollView} from './DraggableScrollView' @@ -131,7 +131,7 @@ export function TabBar({ (itemRefs.current[i] = node)} + ref={node => (itemRefs.current[i] = node as any)} onLayout={e => onItemLayout(e, i)} style={styles.item} hoverStyle={pal.viewLight} diff --git a/src/view/com/util/PressableWithHover.tsx b/src/view/com/util/PressableWithHover.tsx index 77276f1843..48659e2295 100644 --- a/src/view/com/util/PressableWithHover.tsx +++ b/src/view/com/util/PressableWithHover.tsx @@ -1,39 +1,35 @@ -import React, { - useState, - useCallback, - PropsWithChildren, - forwardRef, - Ref, -} from 'react' +import React, {forwardRef, PropsWithChildren} from 'react' import {Pressable, PressableProps, StyleProp, ViewStyle} from 'react-native' -import {addStyle} from 'lib/styles' +import {View} from 'react-native' + +import {addStyle} from '#/lib/styles' +import {useInteractionState} from '#/components/hooks/useInteractionState' interface PressableWithHover extends PressableProps { hoverStyle: StyleProp } -export const PressableWithHover = forwardRef(function PressableWithHoverImpl( - { - children, - style, - hoverStyle, - ...props - }: PropsWithChildren, - ref: Ref, +export const PressableWithHover = forwardRef< + View, + PropsWithChildren +>(function PressableWithHoverImpl( + {children, style, hoverStyle, ...props}, + ref, ) { - const [isHovering, setIsHovering] = useState(false) - - const onHoverIn = useCallback(() => setIsHovering(true), [setIsHovering]) - const onHoverOut = useCallback(() => setIsHovering(false), [setIsHovering]) - style = - typeof style !== 'function' && isHovering - ? addStyle(style, hoverStyle) - : style + const { + state: hovered, + onIn: onHoverIn, + onOut: onHoverOut, + } = useInteractionState() return ( diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/ControlButton.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/ControlButton.tsx index 6b509d09a3..8ffe482a8f 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/ControlButton.tsx +++ b/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/ControlButton.tsx @@ -1,8 +1,8 @@ import React from 'react' import {SvgProps} from 'react-native-svg' -import {atoms as a, useTheme} from '#/alf' -import {Button} from '#/components/Button' +import {atoms as a, useTheme, web} from '#/alf' +import {PressableWithHover} from '../../../PressableWithHover' export function ControlButton({ active, @@ -21,19 +21,21 @@ export function ControlButton({ }) { const t = useTheme() return ( - + ) } diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx index 5bd7e0d179..2d1427347d 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx +++ b/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx @@ -358,9 +358,8 @@ export function Controls({ style={[ a.flex_1, a.px_xs, - a.pt_2xs, - a.pb_md, - a.gap_md, + a.pb_sm, + a.gap_sm, a.flex_row, a.align_center, ]}> @@ -373,7 +372,11 @@ export function Controls({ onPress={onPressPlayPause} /> - + {formatTime(currentTime)} / {formatTime(duration)} {hasSubtitleTrack && ( From 5eb294488f08534abac3335acfa366cffea9259e Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 23 Sep 2024 10:40:37 -0500 Subject: [PATCH 04/24] [Neue] Handle emoji within custom font (#5449) * Support emoji in text with custom font * Add emoji support to elements that need it * Remove unused file causing lint failure * Fix a few more emoji locations * Couple more * No throw --- package.json | 1 + src/components/FeedCard.tsx | 13 ++- src/components/KnownFollowers.tsx | 14 +-- src/components/LabelingServiceCard/index.tsx | 11 +- src/components/ListCard.tsx | 24 ++-- src/components/Pills.tsx | 1 + src/components/ProfileCard.tsx | 12 +- src/components/ReportDialog/SubmitView.tsx | 1 + src/components/RichText.tsx | 9 +- .../StarterPack/StarterPackCard.tsx | 27 ++--- .../StarterPack/Wizard/WizardListCard.tsx | 11 +- src/components/Typography.tsx | 108 +++++++++++++++++- src/components/dms/MessagesListHeader.tsx | 17 +-- .../moderation/LabelsOnMeDialog.tsx | 6 +- .../moderation/ModerationDetailsDialog.tsx | 8 +- src/screens/Messages/List/ChatListItem.tsx | 10 +- src/screens/Profile/Header/DisplayName.tsx | 1 + src/screens/Profile/Header/Handle.tsx | 11 +- .../com/composer/text-input/TextInput.tsx | 19 +-- .../composer/text-input/web/Autocomplete.tsx | 15 +-- src/view/com/feeds/FeedSourceCard.tsx | 12 +- src/view/com/modals/UserAddRemoveLists.tsx | 32 +++--- src/view/com/notifications/FeedItem.tsx | 23 ++-- src/view/com/pager/TabBar.tsx | 1 + src/view/com/post-thread/PostThreadItem.tsx | 31 ++--- src/view/com/posts/FeedItem.tsx | 26 +++-- src/view/com/profile/ProfileCard.tsx | 19 +-- src/view/com/util/PostMeta.tsx | 44 ++++--- src/view/com/util/UserInfoText.tsx | 33 +++--- .../util/post-embeds/ExternalLinkEmbed.tsx | 27 ++--- src/view/com/util/text/Text.tsx | 50 +++++--- src/view/com/util/text/ThemedText.tsx | 80 ------------- src/view/screens/Search/Search.tsx | 1 + src/view/shell/desktop/Search.tsx | 11 +- yarn.lock | 5 + 35 files changed, 424 insertions(+), 290 deletions(-) delete mode 100644 src/view/com/util/text/ThemedText.tsx diff --git a/package.json b/package.json index bb9706c39a..245c095f19 100644 --- a/package.json +++ b/package.json @@ -116,6 +116,7 @@ "deprecated-react-native-prop-types": "^5.0.0", "email-validator": "^2.0.4", "emoji-mart": "^5.5.2", + "emoji-regex": "^10.4.0", "eventemitter3": "^5.0.1", "expo": "^51.0.8", "expo-application": "^5.9.1", diff --git a/src/components/FeedCard.tsx b/src/components/FeedCard.tsx index e6d664cfda..b28f66f839 100644 --- a/src/components/FeedCard.tsx +++ b/src/components/FeedCard.tsx @@ -11,17 +11,17 @@ import {msg, plural, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useQueryClient} from '@tanstack/react-query' +import {sanitizeHandle} from '#/lib/strings/handles' import {logger} from '#/logger' +import {precacheFeedFromGeneratorView} from '#/state/queries/feed' import { useAddSavedFeedsMutation, usePreferencesQuery, useRemoveFeedMutation, } from '#/state/queries/preferences' -import {sanitizeHandle} from 'lib/strings/handles' -import {precacheFeedFromGeneratorView} from 'state/queries/feed' -import {useSession} from 'state/session' +import {useSession} from '#/state/session' +import * as Toast from '#/view/com/util/Toast' import {UserAvatar} from '#/view/com/util/UserAvatar' -import * as Toast from 'view/com/util/Toast' import {useTheme} from '#/alf' import {atoms as a} from '#/alf' import {Button, ButtonIcon} from '#/components/Button' @@ -121,7 +121,10 @@ export function TitleAndByline({ return ( - + {title} {creator && ( diff --git a/src/components/KnownFollowers.tsx b/src/components/KnownFollowers.tsx index 4017a7b0be..35a346c3a5 100644 --- a/src/components/KnownFollowers.tsx +++ b/src/components/KnownFollowers.tsx @@ -5,7 +5,7 @@ import {msg, Plural, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {makeProfileLink} from '#/lib/routes/links' -import {sanitizeDisplayName} from 'lib/strings/display-names' +import {sanitizeDisplayName} from '#/lib/strings/display-names' import {UserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, useTheme} from '#/alf' import {Link, LinkProps} from '#/components/Link' @@ -185,11 +185,11 @@ function KnownFollowersInner({ serverCount > 2 ? ( Followed by{' '} - + {slice[0].profile.displayName} ,{' '} - + {slice[1].profile.displayName} , and{' '} @@ -203,11 +203,11 @@ function KnownFollowersInner({ // only 2 Followed by{' '} - + {slice[0].profile.displayName} {' '} and{' '} - + {slice[1].profile.displayName} @@ -216,7 +216,7 @@ function KnownFollowersInner({ // 1-n followers, including blocks Followed by{' '} - + {slice[0].profile.displayName} {' '} and{' '} @@ -230,7 +230,7 @@ function KnownFollowersInner({ // only 1 Followed by{' '} - + {slice[0].profile.displayName} diff --git a/src/components/LabelingServiceCard/index.tsx b/src/components/LabelingServiceCard/index.tsx index 851645a48c..03b8ece6b1 100644 --- a/src/components/LabelingServiceCard/index.tsx +++ b/src/components/LabelingServiceCard/index.tsx @@ -44,17 +44,22 @@ export function Avatar({avatar}: {avatar?: string}) { } export function Title({value}: {value: string}) { - return {value} + return ( + + {value} + + ) } export function Description({value, handle}: {value?: string; handle: string}) { + const {_} = useLingui() return value ? ( ) : ( - - By {sanitizeHandle(handle, '@')} + + {_(msg`By ${sanitizeHandle(handle, '@')}`)} ) } diff --git a/src/components/ListCard.tsx b/src/components/ListCard.tsx index 829f36d471..ed5838fb04 100644 --- a/src/components/ListCard.tsx +++ b/src/components/ListCard.tsx @@ -7,13 +7,14 @@ import { moderateUserList, ModerationUI, } from '@atproto/api' -import {Trans} from '@lingui/macro' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' import {useQueryClient} from '@tanstack/react-query' -import {sanitizeHandle} from 'lib/strings/handles' -import {useModerationOpts} from 'state/preferences/moderation-opts' -import {precacheList} from 'state/queries/feed' -import {useSession} from 'state/session' +import {sanitizeHandle} from '#/lib/strings/handles' +import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {precacheList} from '#/state/queries/feed' +import {useSession} from '#/state/session' import {atoms as a, useTheme} from '#/alf' import { Avatar, @@ -111,6 +112,7 @@ export function TitleAndByline({ modUi?: ModerationUI }) { const t = useTheme() + const {_} = useLingui() const {currentAccount} = useSession() return ( @@ -130,6 +132,7 @@ export function TitleAndByline({ {title} @@ -139,15 +142,12 @@ export function TitleAndByline({ {creator && ( - {purpose === MODLIST ? ( - - Moderation list by {sanitizeHandle(creator.handle, '@')} - - ) : ( - List by {sanitizeHandle(creator.handle, '@')} - )} + {purpose === MODLIST + ? _(msg`Moderation list by ${sanitizeHandle(creator.handle, '@')}`) + : _(msg`List by ${sanitizeHandle(creator.handle, '@')}`)} )} diff --git a/src/components/Pills.tsx b/src/components/Pills.tsx index 6c8084743f..974d83593f 100644 --- a/src/components/Pills.tsx +++ b/src/components/Pills.tsx @@ -130,6 +130,7 @@ export function Label({ )} {name} {handle} diff --git a/src/components/ReportDialog/SubmitView.tsx b/src/components/ReportDialog/SubmitView.tsx index 2def0fa4b4..e323d15042 100644 --- a/src/components/ReportDialog/SubmitView.tsx +++ b/src/components/ReportDialog/SubmitView.tsx @@ -256,6 +256,7 @@ function LabelerToggle({title}: {title: string}) { a.z_10, ]}> , ) } else { - els.push(segment.text) + els.push( + + {segment.text} + , + ) } key++ } @@ -213,6 +219,7 @@ function RichTextTag({ {!noIcon ? : null} - + {record.name} - - - Starter pack by{' '} - {creator?.did === currentAccount?.did - ? _(msg`you`) - : `@${sanitizeHandle(creator.handle)}`} - + + {creator?.did === currentAccount?.did + ? _(msg`Starter pack by you`) + : _(msg`Starter pack by ${sanitizeHandle(creator.handle, '@')}`)} {!noDescription && record.description ? ( - + {record.description} ) : null} diff --git a/src/components/StarterPack/Wizard/WizardListCard.tsx b/src/components/StarterPack/Wizard/WizardListCard.tsx index ad02cdc306..44f01a1545 100644 --- a/src/components/StarterPack/Wizard/WizardListCard.tsx +++ b/src/components/StarterPack/Wizard/WizardListCard.tsx @@ -12,11 +12,11 @@ import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {DISCOVER_FEED_URI, STARTER_PACK_MAX_SIZE} from 'lib/constants' -import {sanitizeDisplayName} from 'lib/strings/display-names' -import {sanitizeHandle} from 'lib/strings/handles' -import {useSession} from 'state/session' -import {UserAvatar} from 'view/com/util/UserAvatar' +import {DISCOVER_FEED_URI, STARTER_PACK_MAX_SIZE} from '#/lib/constants' +import {sanitizeDisplayName} from '#/lib/strings/display-names' +import {sanitizeHandle} from '#/lib/strings/handles' +import {useSession} from '#/state/session' +import {UserAvatar} from '#/view/com/util/UserAvatar' import {WizardAction, WizardState} from '#/screens/StarterPack/Wizard/State' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' @@ -78,6 +78,7 @@ function WizardListCard({ /> & { /** * Lets the user select text, to use the native copy and paste functionality. */ selectable?: boolean + /** + * Provides `data-*` attributes to the underlying `UITextView` component on + * web only. + */ + dataSet?: Record + /** + * Appears as a small tooltip on web hover. + */ + title?: string +} & ( + | { + emoji: true + children: StringChild + } + | { + emoji?: false + children: RNTextProps['children'] + } + ) + +const EMOJI = createEmojiRegex() + +export function childHasEmoji(children: React.ReactNode) { + return (Array.isArray(children) ? children : [children]).some( + child => typeof child === 'string' && createEmojiRegex().test(child), + ) +} + +export function childIsString( + children: React.ReactNode, +): children is StringChild { + return ( + typeof children === 'string' || + (Array.isArray(children) && + children.every(child => typeof child === 'string' || child === null)) + ) +} + +export function renderChildrenWithEmoji(children: StringChild) { + const normalized = Array.isArray(children) ? children : [children] + + return ( + + {normalized.map(child => { + if (typeof child !== 'string') return child + + const emojis = child.match(EMOJI) + + if (emojis === null) { + return child + } + + return child.split(EMOJI).map((stringPart, index) => ( + + {stringPart} + {emojis[index] ? ( + + {emojis[index]} + + ) : null} + + )) + })} + + ) } /** @@ -64,7 +134,15 @@ export function normalizeTextStyles( /** * Our main text component. Use this most of the time. */ -export function Text({style, selectable, ...rest}: TextProps) { +export function Text({ + children, + emoji, + style, + selectable, + title, + dataSet, + ...rest +}: TextProps) { const {fonts, flags} = useAlf() const t = useTheme() const s = normalizeTextStyles([atoms.text_sm, t.atoms.text, flatten(style)], { @@ -73,7 +151,29 @@ export function Text({style, selectable, ...rest}: TextProps) { flags, }) - return + if (IS_DEV) { + if (!emoji && childHasEmoji(children)) { + logger.warn( + `Text: emoji detected but emoji not enabled: "${children}"\n\nPlease add '`, + ) + } + + if (emoji && !childIsString(children)) { + logger.error('Text: when , children can only be strings.') + } + } + + return ( + + {isIOS && emoji ? renderChildrenWithEmoji(children) : children} + + ) } export function createHeadingElement({level}: {level: number}) { diff --git a/src/components/dms/MessagesListHeader.tsx b/src/components/dms/MessagesListHeader.tsx index 1a6bbbe601..ab9ec16e4d 100644 --- a/src/components/dms/MessagesListHeader.tsx +++ b/src/components/dms/MessagesListHeader.tsx @@ -10,14 +10,14 @@ import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' -import {BACK_HITSLOP} from 'lib/constants' -import {makeProfileLink} from 'lib/routes/links' -import {NavigationProp} from 'lib/routes/types' -import {sanitizeDisplayName} from 'lib/strings/display-names' -import {isWeb} from 'platform/detection' -import {useProfileShadow} from 'state/cache/profile-shadow' -import {isConvoActive, useConvo} from 'state/messages/convo' -import {PreviewableUserAvatar} from 'view/com/util/UserAvatar' +import {BACK_HITSLOP} from '#/lib/constants' +import {makeProfileLink} from '#/lib/routes/links' +import {NavigationProp} from '#/lib/routes/types' +import {sanitizeDisplayName} from '#/lib/strings/display-names' +import {isWeb} from '#/platform/detection' +import {useProfileShadow} from '#/state/cache/profile-shadow' +import {isConvoActive, useConvo} from '#/state/messages/convo' +import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' import {ConvoMenu} from '#/components/dms/ConvoMenu' import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/Bell2' @@ -170,6 +170,7 @@ function HeaderReady({ - {strings.name} - + + {strings.name} + + {strings.description} diff --git a/src/components/moderation/ModerationDetailsDialog.tsx b/src/components/moderation/ModerationDetailsDialog.tsx index d95717cf43..2259178538 100644 --- a/src/components/moderation/ModerationDetailsDialog.tsx +++ b/src/components/moderation/ModerationDetailsDialog.tsx @@ -118,7 +118,11 @@ function ModerationDetailsDialogInner({ : _(msg`The author of this thread has hidden this reply.`) } else if (modcause.type === 'label') { name = desc.name - description = desc.description + description = ( + + {desc.description} + + ) } else { // should never happen name = '' @@ -127,7 +131,7 @@ function ModerationDetailsDialogInner({ return ( - + {name} diff --git a/src/screens/Messages/List/ChatListItem.tsx b/src/screens/Messages/List/ChatListItem.tsx index c45cc28d7a..e9668b4e11 100644 --- a/src/screens/Messages/List/ChatListItem.tsx +++ b/src/screens/Messages/List/ChatListItem.tsx @@ -10,6 +10,10 @@ import { import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useHaptics} from '#/lib/haptics' +import {decrementBadgeCount} from '#/lib/notifications/notifications' +import {logEvent} from '#/lib/statsig/statsig' +import {sanitizeDisplayName} from '#/lib/strings/display-names' import { postUriToRelativePath, toBskyAppUrl, @@ -19,10 +23,6 @@ import {isNative} from '#/platform/detection' import {useProfileShadow} from '#/state/cache/profile-shadow' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useSession} from '#/state/session' -import {useHaptics} from 'lib/haptics' -import {decrementBadgeCount} from 'lib/notifications/notifications' -import {logEvent} from 'lib/statsig/statsig' -import {sanitizeDisplayName} from 'lib/strings/display-names' import {TimeElapsed} from '#/view/com/util/TimeElapsed' import {UserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' @@ -248,6 +248,7 @@ function ChatListItemReady({ numberOfLines={1} style={[{maxWidth: '85%'}, web([a.leading_normal])]}> {sanitizeDisplayName( diff --git a/src/screens/Profile/Header/Handle.tsx b/src/screens/Profile/Header/Handle.tsx index 0344f1a234..ba869b6626 100644 --- a/src/screens/Profile/Header/Handle.tsx +++ b/src/screens/Profile/Header/Handle.tsx @@ -1,11 +1,12 @@ import React from 'react' import {View} from 'react-native' import {AppBskyActorDefs} from '@atproto/api' -import {Trans} from '@lingui/macro' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {isInvalidHandle} from '#/lib/strings/handles' +import {isIOS} from '#/platform/detection' import {Shadow} from '#/state/cache/types' -import {isInvalidHandle} from 'lib/strings/handles' -import {isIOS} from 'platform/detection' import {atoms as a, useTheme, web} from '#/alf' import {NewskieDialog} from '#/components/NewskieDialog' import {Text} from '#/components/Typography' @@ -18,6 +19,7 @@ export function ProfileHeaderHandle({ disableTaps?: boolean }) { const t = useTheme() + const {_} = useLingui() const invalidHandle = isInvalidHandle(profile.handle) const blockHide = profile.viewer?.blocking || profile.viewer?.blockedBy return ( @@ -33,6 +35,7 @@ export function ProfileHeaderHandle({ ) : undefined} - {invalidHandle ? ⚠Invalid Handle : `@${profile.handle}`} + {invalidHandle ? _(msg`⚠Invalid Handle`) : `@${profile.handle}`} ) diff --git a/src/view/com/composer/text-input/TextInput.tsx b/src/view/com/composer/text-input/TextInput.tsx index 778439259e..95c57ad899 100644 --- a/src/view/com/composer/text-input/TextInput.tsx +++ b/src/view/com/composer/text-input/TextInput.tsx @@ -19,19 +19,19 @@ import PasteInput, { PasteInputRef, } from '@mattermost/react-native-paste-input' +import {POST_IMG_MAX} from '#/lib/constants' +import {usePalette} from '#/lib/hooks/usePalette' +import {downloadAndResize} from '#/lib/media/manip' +import {isUriImage} from '#/lib/media/util' +import {cleanError} from '#/lib/strings/errors' +import {getMentionAt, insertMentionAt} from '#/lib/strings/mention-manip' +import {useTheme} from '#/lib/ThemeContext' import {isAndroid} from '#/platform/detection' -import {POST_IMG_MAX} from 'lib/constants' -import {usePalette} from 'lib/hooks/usePalette' -import {downloadAndResize} from 'lib/media/manip' -import {isUriImage} from 'lib/media/util' -import {cleanError} from 'lib/strings/errors' -import {getMentionAt, insertMentionAt} from 'lib/strings/mention-manip' -import {useTheme} from 'lib/ThemeContext' import { LinkFacetMatch, suggestLinkCardUri, -} from 'view/com/composer/text-input/text-input-util' -import {Text} from 'view/com/util/text/Text' +} from '#/view/com/composer/text-input/text-input-util' +import {Text} from '#/view/com/util/text/Text' import {atoms as a, useAlf} from '#/alf' import {normalizeTextStyles} from '#/components/Typography' import {Autocomplete} from './mobile/Autocomplete' @@ -216,6 +216,7 @@ export const TextInput = forwardRef(function TextInputImpl( return Array.from(richtext.segments()).map(segment => { return ( {segment.text} diff --git a/src/view/com/composer/text-input/web/Autocomplete.tsx b/src/view/com/composer/text-input/web/Autocomplete.tsx index 29b8f0bc65..a43e67c044 100644 --- a/src/view/com/composer/text-input/web/Autocomplete.tsx +++ b/src/view/com/composer/text-input/web/Autocomplete.tsx @@ -5,19 +5,20 @@ import React, { useState, } from 'react' import {Pressable, StyleSheet, View} from 'react-native' +import {Trans} from '@lingui/macro' import {ReactRenderer} from '@tiptap/react' -import tippy, {Instance as TippyInstance} from 'tippy.js' import { + SuggestionKeyDownProps, SuggestionOptions, SuggestionProps, - SuggestionKeyDownProps, } from '@tiptap/suggestion' +import tippy, {Instance as TippyInstance} from 'tippy.js' + +import {usePalette} from '#/lib/hooks/usePalette' import {ActorAutocompleteFn} from '#/state/queries/actor-autocomplete' -import {usePalette} from 'lib/hooks/usePalette' -import {Text} from 'view/com/util/text/Text' -import {UserAvatar} from 'view/com/util/UserAvatar' +import {Text} from '#/view/com/util/text/Text' +import {UserAvatar} from '#/view/com/util/UserAvatar' import {useGrapheme} from '../hooks/useGrapheme' -import {Trans} from '@lingui/macro' interface MentionListRef { onKeyDown: (props: SuggestionKeyDownProps) => boolean @@ -180,7 +181,7 @@ const MentionList = forwardRef( size={26} type={item.associated?.labeler ? 'labeler' : 'user'} /> - + {displayName} diff --git a/src/view/com/feeds/FeedSourceCard.tsx b/src/view/com/feeds/FeedSourceCard.tsx index 68437c37a0..3276cf8821 100644 --- a/src/view/com/feeds/FeedSourceCard.tsx +++ b/src/view/com/feeds/FeedSourceCard.tsx @@ -12,6 +12,10 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {msg, Plural, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useNavigationDeduped} from '#/lib/hooks/useNavigationDeduped' +import {usePalette} from '#/lib/hooks/usePalette' +import {sanitizeHandle} from '#/lib/strings/handles' +import {s} from '#/lib/styles' import {logger} from '#/logger' import {shouldClickOpenNewTab} from '#/platform/urls' import {FeedSourceInfo, useFeedSourceInfoQuery} from '#/state/queries/feed' @@ -21,12 +25,8 @@ import { UsePreferencesQueryResponse, useRemoveFeedMutation, } from '#/state/queries/preferences' -import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped' -import {usePalette} from 'lib/hooks/usePalette' -import {sanitizeHandle} from 'lib/strings/handles' -import {s} from 'lib/styles' import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' -import * as Toast from 'view/com/util/Toast' +import * as Toast from '#/view/com/util/Toast' import {useTheme} from '#/alf' import {atoms as a} from '#/alf' import * as Prompt from '#/components/Prompt' @@ -242,7 +242,7 @@ export function FeedSourceCardLoaded({ - + {feed.displayName} diff --git a/src/view/com/modals/UserAddRemoveLists.tsx b/src/view/com/modals/UserAddRemoveLists.tsx index 29caf46609..b0b76644f0 100644 --- a/src/view/com/modals/UserAddRemoveLists.tsx +++ b/src/view/com/modals/UserAddRemoveLists.tsx @@ -65,21 +65,27 @@ export function Component({ return [pal.border, {flex: 1, borderTopWidth: StyleSheet.hairlineWidth}] }, [pal.border, screenHeight]) + const headerStyles = [ + { + textAlign: 'center', + fontWeight: '600', + fontSize: 20, + marginBottom: 12, + paddingHorizontal: 12, + } as const, + pal.text, + ] + return ( - - Update {displayName} in Lists + + + Update{' '} + + {displayName} + {' '} + in Lists + + {forceLTR(firstAuthorName)} + + } disableMismatchWarning /> {authors.length > 1 ? ( @@ -570,12 +574,13 @@ function ExpandedAuthorsList({ numberOfLines={1} style={pal.text} lineHeight={1.2}> - {sanitizeDisplayName( - author.profile.displayName || author.profile.handle, - )} -   + + {sanitizeDisplayName( + author.profile.displayName || author.profile.handle, + )} + {' '} - {sanitizeHandle(author.profile.handle)} + {sanitizeHandle(author.profile.handle, '@')} @@ -592,7 +597,11 @@ function AdditionalPostText({post}: {post?: AppBskyFeedDefs.PostView}) { return ( <> - {text?.length > 0 && {text}} + {text?.length > 0 && ( + + {text} + + )} onPressItem(i)}> - + {sanitizeHandle(post.author.handle, '@')} diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index 7537a46448..b1509b2719 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -316,11 +316,19 @@ let FeedItemInner = ({ style={pal.textLight} lineHeight={1.2} numberOfLines={1} - text={sanitizeDisplayName( - reason.by.displayName || - sanitizeHandle(reason.by.handle), - moderation.ui('displayName'), - )} + text={ + + {sanitizeDisplayName( + reason.by.displayName || + sanitizeHandle(reason.by.handle), + moderation.ui('displayName'), + )} + + } href={makeProfileLink(reason.by)} onBeforePress={onOpenReposter} /> @@ -527,9 +535,11 @@ function ReplyToLabel({ numberOfLines={1} href={makeProfileLink(profile)} text={ - profile.displayName - ? sanitizeDisplayName(profile.displayName) - : sanitizeHandle(profile.handle) + + {profile.displayName + ? sanitizeDisplayName(profile.displayName) + : sanitizeHandle(profile.handle)} + } /> diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx index fd32e37a42..eab8611dd4 100644 --- a/src/view/com/profile/ProfileCard.tsx +++ b/src/view/com/profile/ProfileCard.tsx @@ -7,17 +7,17 @@ import { } from '@atproto/api' import {useQueryClient} from '@tanstack/react-query' +import {usePalette} from '#/lib/hooks/usePalette' +import {getModerationCauseKey, isJustAMute} from '#/lib/moderation' +import {makeProfileLink} from '#/lib/routes/links' +import {sanitizeDisplayName} from '#/lib/strings/display-names' +import {sanitizeHandle} from '#/lib/strings/handles' +import {s} from '#/lib/styles' import {useProfileShadow} from '#/state/cache/profile-shadow' import {Shadow} from '#/state/cache/types' import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {precacheProfile} from '#/state/queries/profile' import {useSession} from '#/state/session' -import {usePalette} from 'lib/hooks/usePalette' -import {getModerationCauseKey, isJustAMute} from 'lib/moderation' -import {makeProfileLink} from 'lib/routes/links' -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, @@ -103,6 +103,7 @@ export function ProfileCard({ - + {sanitizeHandle(profile.handle, '@')} {profile.description ? ( - + {profile.description as string} ) : null} diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx index 3bd350bf32..f2d717e962 100644 --- a/src/view/com/util/PostMeta.tsx +++ b/src/view/com/util/PostMeta.tsx @@ -4,16 +4,16 @@ import {AppBskyActorDefs, ModerationDecision, ModerationUI} from '@atproto/api' import {useLingui} from '@lingui/react' import {useQueryClient} from '@tanstack/react-query' +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' +import {TypographyVariant} from '#/lib/ThemeContext' +import {isAndroid} from '#/platform/detection' 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' -import {TypographyVariant} from 'lib/ThemeContext' -import {isAndroid} from 'platform/detection' import {ProfileHoverCard} from '#/components/ProfileHoverCard' import {TextLinkOnWebOnly} from './Link' import {Text} from './text/Text' @@ -73,12 +73,20 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => { style={[pal.text]} lineHeight={1.2} disableMismatchWarning - text={forceLTR( - sanitizeDisplayName( - displayName, - opts.moderation?.ui('displayName'), - ), - )} + text={ + + {forceLTR( + sanitizeDisplayName( + displayName, + opts.moderation?.ui('displayName'), + ), + )} + + } href={profileLink} onBeforePress={onBeforePressAuthor} /> @@ -86,7 +94,11 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => { type="md" disableMismatchWarning style={[pal.textLight, {flexShrink: 4}]} - text={NON_BREAKING_SPACE + sanitizeHandle(handle, '@')} + text={ + + {NON_BREAKING_SPACE + sanitizeHandle(handle, '@')} + + } href={profileLink} onBeforePress={onBeforePressAuthor} anchorNoUnderline diff --git a/src/view/com/util/UserInfoText.tsx b/src/view/com/util/UserInfoText.tsx index 9cb9997f60..8a444d5901 100644 --- a/src/view/com/util/UserInfoText.tsx +++ b/src/view/com/util/UserInfoText.tsx @@ -1,15 +1,16 @@ import React from 'react' -import {AppBskyActorGetProfile as GetProfile} from '@atproto/api' import {StyleProp, StyleSheet, TextStyle} from 'react-native' -import {TextLinkOnWebOnly} from './Link' -import {Text} from './text/Text' -import {LoadingPlaceholder} from './LoadingPlaceholder' -import {TypographyVariant} from 'lib/ThemeContext' -import {sanitizeDisplayName} from 'lib/strings/display-names' -import {sanitizeHandle} from 'lib/strings/handles' -import {makeProfileLink} from 'lib/routes/links' -import {useProfileQuery} from '#/state/queries/profile' +import {AppBskyActorGetProfile as GetProfile} from '@atproto/api' + +import {makeProfileLink} from '#/lib/routes/links' +import {sanitizeDisplayName} from '#/lib/strings/display-names' +import {sanitizeHandle} from '#/lib/strings/handles' +import {TypographyVariant} from '#/lib/ThemeContext' import {STALE} from '#/state/queries' +import {useProfileQuery} from '#/state/queries/profile' +import {TextLinkOnWebOnly} from './Link' +import {LoadingPlaceholder} from './LoadingPlaceholder' +import {Text} from './text/Text' export function UserInfoText({ type = 'md', @@ -50,11 +51,15 @@ export function UserInfoText({ lineHeight={1.2} numberOfLines={1} href={makeProfileLink(profile)} - text={`${prefix || ''}${sanitizeDisplayName( - typeof profile[attr] === 'string' && profile[attr] - ? (profile[attr] as string) - : sanitizeHandle(profile.handle), - )}`} + text={ + + {`${prefix || ''}${sanitizeDisplayName( + typeof profile[attr] === 'string' && profile[attr] + ? (profile[attr] as string) + : sanitizeHandle(profile.handle), + )}`} + + } /> ) } else { diff --git a/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx b/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx index 54e1eb4d55..98332c33b0 100644 --- a/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx +++ b/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx @@ -5,21 +5,21 @@ import {AppBskyEmbedExternal} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {usePalette} from 'lib/hooks/usePalette' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {shareUrl} from 'lib/sharing' -import {parseEmbedPlayerFromUrl} from 'lib/strings/embed-player' +import {usePalette} from '#/lib/hooks/usePalette' +import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' +import {shareUrl} from '#/lib/sharing' +import {parseEmbedPlayerFromUrl} from '#/lib/strings/embed-player' import { getStarterPackOgCard, parseStarterPackUri, -} from 'lib/strings/starter-pack' -import {toNiceDomain} from 'lib/strings/url-helpers' -import {isNative} from 'platform/detection' -import {useExternalEmbedsPrefs} from 'state/preferences' -import {Link} from 'view/com/util/Link' -import {ExternalGifEmbed} from 'view/com/util/post-embeds/ExternalGifEmbed' -import {ExternalPlayer} from 'view/com/util/post-embeds/ExternalPlayerEmbed' -import {GifEmbed} from 'view/com/util/post-embeds/GifEmbed' +} from '#/lib/strings/starter-pack' +import {toNiceDomain} from '#/lib/strings/url-helpers' +import {isNative} from '#/platform/detection' +import {useExternalEmbedsPrefs} from '#/state/preferences' +import {Link} from '#/view/com/util/Link' +import {ExternalGifEmbed} from '#/view/com/util/post-embeds/ExternalGifEmbed' +import {ExternalPlayer} from '#/view/com/util/post-embeds/ExternalPlayerEmbed' +import {GifEmbed} from '#/view/com/util/post-embeds/GifEmbed' import {atoms as a, useTheme} from '#/alf' import {MediaInsetBorder} from '#/components/MediaInsetBorder' import {Text} from '../text/Text' @@ -115,12 +115,13 @@ export const ExternalLinkEmbed = ({ {!embedPlayerParams?.isGif && !embedPlayerParams?.dimensions && ( - + {link.title || link.uri} )} {link.description ? ( diff --git a/src/view/com/util/text/Text.tsx b/src/view/com/util/text/Text.tsx index 52a45b0e2e..3d885480cc 100644 --- a/src/view/com/util/text/Text.tsx +++ b/src/view/com/util/text/Text.tsx @@ -2,27 +2,40 @@ import React from 'react' import {StyleSheet, Text as RNText, TextProps} from 'react-native' import {UITextView} from 'react-native-uitextview' -import {lh, s} from 'lib/styles' -import {TypographyVariant, useTheme} from 'lib/ThemeContext' -import {isIOS, isWeb} from 'platform/detection' +import {lh, s} from '#/lib/styles' +import {TypographyVariant, useTheme} from '#/lib/ThemeContext' +import {logger} from '#/logger' +import {isIOS} from '#/platform/detection' import {applyFonts, useAlf} from '#/alf' +import { + childHasEmoji, + childIsString, + renderChildrenWithEmoji, + StringChild, +} from '#/components/Typography' +import {IS_DEV} from '#/env' -export type CustomTextProps = TextProps & { +export type CustomTextProps = Omit & { type?: TypographyVariant lineHeight?: number title?: string dataSet?: Record selectable?: boolean -} - -const fontFamilyStyle = { - fontFamily: - '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", Helvetica, Arial, sans-serif', -} +} & ( + | { + emoji: true + children: StringChild + } + | { + emoji?: false + children: TextProps['children'] + } + ) export function Text({ type = 'md', children, + emoji, lineHeight, style, title, @@ -35,6 +48,18 @@ export function Text({ const lineHeightStyle = lineHeight ? lh(theme, type, lineHeight) : undefined const {fonts} = useAlf() + if (IS_DEV) { + if (!emoji && childHasEmoji(children)) { + logger.warn( + `Text: emoji detected but emoji not enabled: "${children}"\n\nPlease add '`, + ) + } + + if (emoji && !childIsString(children)) { + logger.error('Text: when , children can only be strings.') + } + } + if (selectable && isIOS) { const flattened = StyleSheet.flatten([ s.black, @@ -58,7 +83,7 @@ export function Text({ selectable={selectable} uiTextView {...props}> - {children} + {isIOS && emoji ? renderChildrenWithEmoji(children) : children} ) } @@ -66,7 +91,6 @@ export function Text({ const flattened = StyleSheet.flatten([ s.black, typography, - isWeb && fontFamilyStyle, lineHeightStyle, style, ]) @@ -87,7 +111,7 @@ export function Text({ dataSet={Object.assign({tooltip: title}, dataSet || {})} selectable={selectable} {...props}> - {children} + {isIOS && emoji ? renderChildrenWithEmoji(children) : children} ) } diff --git a/src/view/com/util/text/ThemedText.tsx b/src/view/com/util/text/ThemedText.tsx deleted file mode 100644 index 2844d273c2..0000000000 --- a/src/view/com/util/text/ThemedText.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import React from 'react' -import {CustomTextProps, Text} from './Text' -import {usePalette} from 'lib/hooks/usePalette' -import {addStyle} from 'lib/styles' - -export type ThemedTextProps = CustomTextProps & { - fg?: 'default' | 'light' | 'error' | 'inverted' | 'inverted-light' - bg?: 'default' | 'light' | 'error' | 'inverted' | 'inverted-light' - border?: 'default' | 'dark' | 'error' | 'inverted' | 'inverted-dark' - lineHeight?: number -} - -export function ThemedText({ - fg, - bg, - border, - style, - children, - ...props -}: React.PropsWithChildren) { - const pal = usePalette('default') - const palInverted = usePalette('inverted') - const palError = usePalette('error') - switch (fg) { - case 'default': - style = addStyle(style, pal.text) - break - case 'light': - style = addStyle(style, pal.textLight) - break - case 'error': - style = addStyle(style, {color: palError.colors.background}) - break - case 'inverted': - style = addStyle(style, palInverted.text) - break - case 'inverted-light': - style = addStyle(style, palInverted.textLight) - break - } - switch (bg) { - case 'default': - style = addStyle(style, pal.view) - break - case 'light': - style = addStyle(style, pal.viewLight) - break - case 'error': - style = addStyle(style, palError.view) - break - case 'inverted': - style = addStyle(style, palInverted.view) - break - case 'inverted-light': - style = addStyle(style, palInverted.viewLight) - break - } - switch (border) { - case 'default': - style = addStyle(style, pal.border) - break - case 'dark': - style = addStyle(style, pal.borderDark) - break - case 'error': - style = addStyle(style, palError.border) - break - case 'inverted': - style = addStyle(style, palInverted.border) - break - case 'inverted-dark': - style = addStyle(style, palInverted.borderDark) - break - } - return ( - - {children} - - ) -} diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx index e1e412648b..07d762c0fe 100644 --- a/src/view/screens/Search/Search.tsx +++ b/src/view/screens/Search/Search.tsx @@ -959,6 +959,7 @@ function SearchHistory({ accessibilityIgnoresInvertColors /> {profile.displayName || profile.handle} diff --git a/src/view/shell/desktop/Search.tsx b/src/view/shell/desktop/Search.tsx index 1ba2d3f3db..b43dbcce32 100644 --- a/src/view/shell/desktop/Search.tsx +++ b/src/view/shell/desktop/Search.tsx @@ -16,19 +16,19 @@ import {useLingui} from '@lingui/react' import {StackActions, useNavigation} from '@react-navigation/native' import {useQueryClient} from '@tanstack/react-query' +import {usePalette} from '#/lib/hooks/usePalette' import {makeProfileLink} from '#/lib/routes/links' +import {NavigationProp} from '#/lib/routes/types' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' import {s} from '#/lib/styles' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete' -import {usePalette} from 'lib/hooks/usePalette' -import {NavigationProp} from 'lib/routes/types' -import {precacheProfile} from 'state/queries/profile' +import {precacheProfile} from '#/state/queries/profile' +import {SearchInput} from '#/view/com/util/forms/SearchInput' import {Link} from '#/view/com/util/Link' +import {Text} from '#/view/com/util/text/Text' import {UserAvatar} from '#/view/com/util/UserAvatar' -import {SearchInput} from 'view/com/util/forms/SearchInput' -import {Text} from 'view/com/util/text/Text' import {atoms as a} from '#/alf' let SearchLinkCard = ({ @@ -126,6 +126,7 @@ let SearchProfileCard = ({ /> Date: Mon, 23 Sep 2024 20:30:34 +0100 Subject: [PATCH 05/24] [Video] Flush low quality segments once focused (#5430) * Update VideoEmbedInnerWeb.tsx * keep proper track and flush properly * consistent current * use current in listener * manually loop --- .../VideoEmbedInner/VideoEmbedInnerWeb.tsx | 77 +++++++++++++++++-- 1 file changed, 69 insertions(+), 8 deletions(-) diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx index fa577fb509..82b2503eb1 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx +++ b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx @@ -1,7 +1,7 @@ import React, {useEffect, useId, useRef, useState} from 'react' import {View} from 'react-native' import {AppBskyEmbedVideo} from '@atproto/api' -import Hls from 'hls.js' +import Hls, {Events, FragChangedData, Fragment} from 'hls.js' import {atoms as a} from '#/alf' import {MediaInsetBorder} from '#/components/MediaInsetBorder' @@ -19,7 +19,7 @@ export function VideoEmbedInnerWeb({ onScreen: boolean }) { const containerRef = useRef(null) - const ref = useRef(null) + const videoRef = useRef(null) const [focused, setFocused] = useState(false) const [hasSubtitleTrack, setHasSubtitleTrack] = useState(false) const figId = useId() @@ -31,13 +31,13 @@ export function VideoEmbedInnerWeb({ } const hlsRef = useRef(undefined) + const [lowQualityFragments, setLowQualityFragments] = useState([]) useEffect(() => { - if (!ref.current) return + if (!videoRef.current) return if (!Hls.isSupported()) throw new HLSUnsupportedError() const hls = new Hls({ - capLevelToPlayerSize: true, maxMaxBufferLength: 10, // only load 10s ahead // note: the amount buffered is affected by both maxBufferLength and maxBufferSize // it will buffer until it it's greater than *both* of those values @@ -45,18 +45,36 @@ export function VideoEmbedInnerWeb({ }) hlsRef.current = hls - hls.attachMedia(ref.current) + hls.attachMedia(videoRef.current) hls.loadSource(embed.playlist) // initial value, later on it's managed by Controls hls.autoLevelCapping = 0 + // manually loop, so if we've flushed the first buffer it doesn't get confused + const abortController = new AbortController() + const {signal} = abortController + videoRef.current.addEventListener( + 'ended', + function () { + this.currentTime = 0 + this.play() + }, + {signal}, + ) + hls.on(Hls.Events.SUBTITLE_TRACKS_UPDATED, (_event, data) => { if (data.subtitleTracks.length > 0) { setHasSubtitleTrack(true) } }) + hls.on(Hls.Events.FRAG_BUFFERED, (_event, {frag}) => { + if (frag.level === 0) { + setLowQualityFragments(prev => [...prev, frag]) + } + }) + hls.on(Hls.Events.ERROR, (_event, data) => { if (data.fatal) { if ( @@ -67,6 +85,8 @@ export function VideoEmbedInnerWeb({ } else { setError(data.error) } + } else { + console.error(data.error) } }) @@ -74,20 +94,61 @@ export function VideoEmbedInnerWeb({ hlsRef.current = undefined hls.detachMedia() hls.destroy() + abortController.abort() } }, [embed.playlist]) + // purge low quality segments from buffer on next frag change + useEffect(() => { + if (!hlsRef.current) return + + const current = hlsRef.current + + if (focused) { + function fragChanged( + _event: Events.FRAG_CHANGED, + {frag}: FragChangedData, + ) { + // if the current quality level goes above 0, flush the low quality segments + if (current.nextAutoLevel > 0) { + const flushed: Fragment[] = [] + + for (const lowQualFrag of lowQualityFragments) { + // avoid if close to the current fragment + if (Math.abs(frag.start - lowQualFrag.start) < 0.1) { + return + } + + current.trigger(Hls.Events.BUFFER_FLUSHING, { + startOffset: lowQualFrag.start, + endOffset: lowQualFrag.end, + type: 'video', + }) + + flushed.push(lowQualFrag) + } + + setLowQualityFragments(prev => prev.filter(f => !flushed.includes(f))) + } + } + current.on(Hls.Events.FRAG_CHANGED, fragChanged) + + return () => { + current.off(Hls.Events.FRAG_CHANGED, fragChanged) + } + } + }, [focused, lowQualityFragments]) + return (
Date: Mon, 23 Sep 2024 21:05:23 +0100 Subject: [PATCH 06/24] add sideborders to (#4995) --- src/view/screens/Profile.tsx | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index 5ef6459810..879632e9ef 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -16,9 +16,18 @@ import { useQueryClient, } from '@tanstack/react-query' +import {useAnalytics} from '#/lib/analytics/analytics' +import {useSetTitle} from '#/lib/hooks/useSetTitle' +import {ComposeIcon2} from '#/lib/icons' +import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' +import {combinedDisplayName} from '#/lib/strings/display-names' import {cleanError} from '#/lib/strings/errors' +import {isInvalidHandle} from '#/lib/strings/handles' +import {colors, s} from '#/lib/styles' import {useProfileShadow} from '#/state/cache/profile-shadow' +import {listenSoftReset} from '#/state/events' import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {useActorStarterPacksQuery} from '#/state/queries/actor-starter-packs' import {useLabelerInfoQuery} from '#/state/queries/labeler' import {resetProfilePostsQueries} from '#/state/queries/post-feed' import {useProfileQuery} from '#/state/queries/profile' @@ -26,29 +35,21 @@ import {useResolveDidQuery} from '#/state/queries/resolve-uri' import {useAgent, useSession} from '#/state/session' import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell' import {useComposerControls} from '#/state/shell/composer' -import {useAnalytics} from 'lib/analytics/analytics' -import {useSetTitle} from 'lib/hooks/useSetTitle' -import {ComposeIcon2} from 'lib/icons' -import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types' -import {combinedDisplayName} from 'lib/strings/display-names' -import {isInvalidHandle} from 'lib/strings/handles' -import {colors, s} from 'lib/styles' -import {listenSoftReset} from 'state/events' -import {useActorStarterPacksQuery} from 'state/queries/actor-starter-packs' +import {ProfileFeedgens} from '#/view/com/feeds/ProfileFeedgens' +import {ProfileLists} from '#/view/com/lists/ProfileLists' +import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' +import {FAB} from '#/view/com/util/fab/FAB' +import {ListRef} from '#/view/com/util/List' +import {CenteredView} from '#/view/com/util/Views' import {PagerWithHeader} from 'view/com/pager/PagerWithHeader' import {ProfileHeader, ProfileHeaderLoading} from '#/screens/Profile/Header' import {ProfileFeedSection} from '#/screens/Profile/Sections/Feed' import {ProfileLabelsSection} from '#/screens/Profile/Sections/Labels' +import {web} from '#/alf' import {ScreenHider} from '#/components/moderation/ScreenHider' import {ProfileStarterPacks} from '#/components/StarterPack/ProfileStarterPacks' import {navigate} from '#/Navigation' import {ExpoScrollForwarderView} from '../../../modules/expo-scroll-forwarder' -import {ProfileFeedgens} from '../com/feeds/ProfileFeedgens' -import {ProfileLists} from '../com/lists/ProfileLists' -import {ErrorScreen} from '../com/util/error/ErrorScreen' -import {FAB} from '../com/util/fab/FAB' -import {ListRef} from '../com/util/List' -import {CenteredView} from '../com/util/Views' interface SectionRef { scrollToTop: () => void @@ -107,7 +108,7 @@ export function ProfileScreen({route}: Props) { // Most pushes will happen here, since we will have only placeholder data if (isLoadingDid || isLoadingProfile || starterPacksQuery.isLoading) { return ( - + ) From b77031a074161f8c6cd7e666db324eea81a38af1 Mon Sep 17 00:00:00 2001 From: Hailey Date: Mon, 23 Sep 2024 13:06:22 -0700 Subject: [PATCH 07/24] invert the fab animation, play a haptic (#4309) --- src/lib/haptics.ts | 24 +++++++++++--------- src/view/com/util/fab/FABInner.tsx | 36 +++++++++++++++++++++++++----- 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/src/lib/haptics.ts b/src/lib/haptics.ts index 02940f793d..390b76a0e7 100644 --- a/src/lib/haptics.ts +++ b/src/lib/haptics.ts @@ -4,17 +4,21 @@ import {impactAsync, ImpactFeedbackStyle} from 'expo-haptics' import {isIOS, isWeb} from 'platform/detection' import {useHapticsDisabled} from 'state/preferences/disable-haptics' -const hapticImpact: ImpactFeedbackStyle = isIOS - ? ImpactFeedbackStyle.Medium - : ImpactFeedbackStyle.Light // Users said the medium impact was too strong on Android; see APP-537s - export function useHaptics() { const isHapticsDisabled = useHapticsDisabled() - return React.useCallback(() => { - if (isHapticsDisabled || isWeb) { - return - } - impactAsync(hapticImpact) - }, [isHapticsDisabled]) + return React.useCallback( + (strength: 'Light' | 'Medium' | 'Heavy' = 'Medium') => { + if (isHapticsDisabled || isWeb) { + return + } + + // Users said the medium impact was too strong on Android; see APP-537s + const style = isIOS + ? ImpactFeedbackStyle[strength] + : ImpactFeedbackStyle.Light + impactAsync(style) + }, + [isHapticsDisabled], + ) } diff --git a/src/view/com/util/fab/FABInner.tsx b/src/view/com/util/fab/FABInner.tsx index ee8e1f47a2..d1675b428c 100644 --- a/src/view/com/util/fab/FABInner.tsx +++ b/src/view/com/util/fab/FABInner.tsx @@ -1,6 +1,10 @@ import React, {ComponentProps} from 'react' import {StyleSheet, TouchableWithoutFeedback} from 'react-native' -import Animated, {useAnimatedStyle, withTiming} from 'react-native-reanimated' +import Animated, { + Easing, + useAnimatedStyle, + withTiming, +} from 'react-native-reanimated' import {useSafeAreaInsets} from 'react-native-safe-area-context' import {LinearGradient} from 'expo-linear-gradient' @@ -9,6 +13,8 @@ import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {clamp} from '#/lib/numbers' import {gradients} from '#/lib/styles' import {isWeb} from '#/platform/detection' +import {useHaptics} from 'lib/haptics' +import {useHapticsDisabled} from 'state/preferences' import {useInteractionState} from '#/components/hooks/useInteractionState' export interface FABProps @@ -17,15 +23,17 @@ export interface FABProps icon: JSX.Element } -export function FABInner({testID, icon, ...props}: FABProps) { +export function FABInner({testID, icon, onPress, ...props}: FABProps) { const insets = useSafeAreaInsets() const {isMobile, isTablet} = useWebMediaQueries() const fabMinimalShellTransform = useMinimalShellFabTransform() const { - state: pressed, + state: isPressed, onIn: onPressIn, onOut: onPressOut, } = useInteractionState() + const playHaptic = useHaptics() + const isHapticsDisabled = useHapticsDisabled() const size = isTablet ? styles.sizeLarge : styles.sizeRegular @@ -33,13 +41,29 @@ export function FABInner({testID, icon, ...props}: FABProps) { ? {right: 50, bottom: 50} : {right: 24, bottom: clamp(insets.bottom, 15, 60) + 15} - const scale = useAnimatedStyle(() => ({ - transform: [{scale: withTiming(pressed ? 0.95 : 1)}], + const animatedStyle = useAnimatedStyle(() => ({ + transform: [ + { + scale: withTiming(isPressed ? 1.1 : 1, { + duration: 250, + easing: Easing.out(Easing.quad), + }), + }, + ], })) return ( { + playHaptic() + setTimeout( + () => { + onPress?.(e) + }, + isHapticsDisabled ? 0 : 75, + ) + }} onPressIn={onPressIn} onPressOut={onPressOut} {...props}> @@ -50,7 +74,7 @@ export function FABInner({testID, icon, ...props}: FABProps) { tabletSpacing, isMobile && fabMinimalShellTransform, ]}> - + Date: Mon, 23 Sep 2024 15:21:32 -0500 Subject: [PATCH 08/24] Fix web splash (#5456) * Fix web splash * Untangle base styles * Fix id name, remove log --- bskyweb/templates/base.html | 49 ++++++++++++++- src/style.css | 122 ++++++++++++------------------------ web/index.html | 49 ++++++++++++++- 3 files changed, 135 insertions(+), 85 deletions(-) diff --git a/bskyweb/templates/base.html b/bskyweb/templates/base.html index eaa31aa4a3..03686ef5c4 100644 --- a/bskyweb/templates/base.html +++ b/bskyweb/templates/base.html @@ -32,6 +32,53 @@ --> + + {% include "scripts.html" %} @@ -48,7 +95,7 @@ {%- block body_all %}
-
+
diff --git a/src/style.css b/src/style.css index ebb0471584..980d92ef77 100644 --- a/src/style.css +++ b/src/style.css @@ -1,3 +1,11 @@ +/** + * IMPORTANT + * + * Some of these styles are duplicated in the `web/index.html` and + * `bskyweb/templates/base.html` files. Depending on what you're updating, you + * may need to touch all three. Ask Eric if you aren't sure. + */ + @font-face { font-family: 'Inter-Regular'; src: local('Inter-Regular'), @@ -96,46 +104,43 @@ */ /** - * Extend the react-native-web reset: - * https://github.com/necolas/react-native-web/blob/master/packages/react-native-web/src/exports/StyleSheet/initialRules.js + * BEGIN STYLES + * + * HTML & BODY STYLES IN `web/index.html` and `bskyweb/templates/base.html` */ -html, -body, -#root { - width: 100%; - /* To smooth any scrolling behavior */ - -webkit-overflow-scrolling: touch; - margin: 0px; - padding: 0px; - /* Allows content to fill the viewport and go beyond the bottom */ - min-height: 100%; +:root { + --text: black; + --background: white; + --backgroundLight: hsl(211, 20%, 95%); } -#root { - flex-shrink: 0; - flex-basis: auto; - flex-grow: 1; - display: flex; - flex: 1; +@media (prefers-color-scheme: dark) { + :root { + color-scheme: dark; + --text: white; + --background: black; + --backgroundLight: hsl(211, 20%, 20%); + } } -html { - /* Prevent text size change on orientation change https://gist.github.com/tfausak/2222823#file-ios-8-web-app-html-L138 */ - -webkit-text-size-adjust: 100%; - height: calc(100% + env(safe-area-inset-top)); - scrollbar-gutter: stable both-edges; +html.theme--light { + --text: black; + --background: white; + --backgroundLight: hsl(211, 20%, 95%); + background-color: white; } -html, -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, - 'Liberation Sans', Helvetica, Arial, sans-serif; +html.theme--dark { + color-scheme: dark; + background-color: black; + --text: white; + --background: black; + --backgroundLight: hsl(211, 20%, 20%); } - -#preload { - width: 100px; - position: fixed; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); +html.theme--dim { + color-scheme: dark; + background-color: hsl(211, 28%, 12%); + --text: white; + --background: hsl(211, 20%, 4%); + --backgroundLight: hsl(211, 20%, 10%); } /* Buttons and inputs have a font set by UA, so we'll have to reset that */ @@ -146,42 +151,6 @@ textarea { line-height: inherit; } -/* Color theming */ -/* Default will always be white */ -:root { - --text: black; - --background: white; - --backgroundLight: hsl(211, 20%, 95%); -} -/* This gives us a black background when system is dark and we have not loaded the theme/color scheme values in JS */ -@media (prefers-color-scheme: dark) { - :root { - --text: white; - --background: black; - --backgroundLight: hsl(211, 20%, 20%); - color-scheme: dark; - } -} - -/* Overwrite those preferences with the selected theme */ -html.theme--light { - --text: black; - --background: white; - --backgroundLight: hsl(211, 20%, 95%); -} -html.theme--dark { - --text: white; - --background: black; - --backgroundLight: hsl(211, 20%, 20%); - color-scheme: dark; -} -html.theme--dim { - --text: white; - --background: hsl(211, 20%, 4%); - --backgroundLight: hsl(211, 20%, 10%); - color-scheme: dark; -} - /* Remove autofill styles on Webkit */ input:autofill, input:-webkit-autofill, @@ -200,19 +169,6 @@ input::-webkit-date-and-time-value { text-align: left; } -body { - display: flex; - /* Allows you to scroll below the viewport; default value is visible */ - overflow-y: auto; - overscroll-behavior-y: none; - text-rendering: optimizeLegibility; - background-color: var(--background); - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - -ms-overflow-style: scrollbar; - font-synthesis-weight: none; -} - /* Remove default link styling */ a { color: inherit; diff --git a/web/index.html b/web/index.html index 512178327f..71e5ac0892 100644 --- a/web/index.html +++ b/web/index.html @@ -36,6 +36,53 @@ --> + + @@ -90,7 +137,7 @@
-
+
From e93cbbd56a70ab3fd44866009400c7b3df24286b Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 23 Sep 2024 16:34:59 -0500 Subject: [PATCH 09/24] Don't use flex on inputs (#5458) --- src/components/dialogs/Embed.tsx | 22 ++++++++++++---------- src/components/forms/TextField.tsx | 4 ++-- src/view/screens/Storybook/Forms.tsx | 21 +++++++++++++++++++++ src/view/screens/Storybook/index.tsx | 10 +++++----- 4 files changed, 40 insertions(+), 17 deletions(-) diff --git a/src/components/dialogs/Embed.tsx b/src/components/dialogs/Embed.tsx index 73ecf6616b..ca75b01390 100644 --- a/src/components/dialogs/Embed.tsx +++ b/src/components/dialogs/Embed.tsx @@ -106,16 +106,18 @@ function EmbedDialogInner({ - - - - + + + + + + + + - + - @@ -93,16 +92,17 @@ function StorybookInner() { + - + - ) - })} - - {!isMobile ? ( - - Transformations - - ) : null} - - {adjustments.map(({label, icon, onPress}) => ( - - ))} - - - - - - Accessibility - - setAltText(enforceLen(text, MAX_ALT_TEXT))} - accessibilityLabel={_(msg`Alt text`)} - accessibilityHint="" - accessibilityLabelledBy="alt-text" - /> - - - - - Cancel - - - - - - Done - - - - - - ) -}) - -const styles = StyleSheet.create({ - container: { - gap: 18, - height: '100%', - width: '100%', - }, - subsection: {marginTop: 12}, - gap18: {gap: 18}, - title: { - fontWeight: '600', - fontSize: 24, - }, - btns: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - }, - btn: { - borderRadius: 4, - paddingVertical: 8, - paddingHorizontal: 24, - }, - imgEditor: { - maxWidth: '100%', - }, - imgContainer: { - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - borderWidth: 1, - borderStyle: 'solid', - marginBottom: 4, - }, - flipVertical: { - transform: [{rotate: '90deg'}], - }, - flipBtn: { - paddingHorizontal: 4, - paddingVertical: 8, - }, - textArea: { - borderWidth: 1, - borderRadius: 6, - paddingTop: 10, - paddingHorizontal: 12, - fontSize: 16, - height: 100, - textAlignVertical: 'top', - }, - bottomSection: { - borderTopWidth: 1, - paddingTop: 18, - }, -}) diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index 3455e1cdf8..fd881ebc4b 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -9,7 +9,6 @@ import {FullWindowOverlay} from '#/components/FullWindowOverlay' import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop' import * as AddAppPassword from './AddAppPasswords' import * as AltImageModal from './AltImage' -import * as EditImageModal from './AltImage' import * as ChangeEmailModal from './ChangeEmail' import * as ChangeHandleModal from './ChangeHandle' import * as ChangePasswordModal from './ChangePassword' @@ -78,9 +77,6 @@ export function ModalsContainer() { } else if (activeModal?.name === 'alt-text-image') { snapPoints = AltImageModal.snapPoints element = - } else if (activeModal?.name === 'edit-image') { - snapPoints = AltImageModal.snapPoints - element = } else if (activeModal?.name === 'change-handle') { snapPoints = ChangeHandleModal.snapPoints element = diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index c4bab6fb18..fe24695d2c 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -15,7 +15,6 @@ import * as ChangePasswordModal from './ChangePassword' import * as CreateOrEditListModal from './CreateOrEditList' import * as CropImageModal from './crop-image/CropImage.web' import * as DeleteAccountModal from './DeleteAccount' -import * as EditImageModal from './EditImage' import * as EditProfileModal from './EditProfile' import * as InviteCodesModal from './InviteCodes' import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings' @@ -54,11 +53,7 @@ function Modal({modal}: {modal: ModalIface}) { } const onPressMask = () => { - if ( - modal.name === 'crop-image' || - modal.name === 'edit-image' || - modal.name === 'alt-text-image' - ) { + if (modal.name === 'crop-image' || modal.name === 'alt-text-image') { return // dont close on mask presses during crop } closeModal() @@ -95,8 +90,6 @@ function Modal({modal}: {modal: ModalIface}) { element = } else if (modal.name === 'alt-text-image') { element = - } else if (modal.name === 'edit-image') { - element = } else if (modal.name === 'verify-email') { element = } else if (modal.name === 'change-email') { diff --git a/src/view/shell/Composer.ios.tsx b/src/view/shell/Composer.ios.tsx index 7d3780801a..bbb837f1fe 100644 --- a/src/view/shell/Composer.ios.tsx +++ b/src/view/shell/Composer.ios.tsx @@ -2,16 +2,13 @@ import React, {useLayoutEffect} from 'react' import {Modal, View} from 'react-native' import {StatusBar} from 'expo-status-bar' import * as SystemUI from 'expo-system-ui' -import {observer} from 'mobx-react-lite' import {useComposerState} from '#/state/shell/composer' import {atoms as a, useTheme} from '#/alf' import {getBackgroundColor, useThemeName} from '#/alf/util/useColorModeTheme' import {ComposePost, useComposerCancelRef} from '../com/composer/Composer' -export const Composer = observer(function ComposerImpl({}: { - winHeight: number -}) { +export function Composer({}: {winHeight: number}) { const t = useTheme() const state = useComposerState() const ref = useComposerCancelRef() @@ -42,7 +39,7 @@ export const Composer = observer(function ComposerImpl({}: { ) -}) +} function Providers({ children, diff --git a/src/view/shell/Composer.tsx b/src/view/shell/Composer.tsx index 1c97df9c39..049f35d35d 100644 --- a/src/view/shell/Composer.tsx +++ b/src/view/shell/Composer.tsx @@ -1,17 +1,12 @@ import React, {useEffect} from 'react' import {Animated, Easing, StyleSheet, View} from 'react-native' -import {observer} from 'mobx-react-lite' -import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' -import {usePalette} from 'lib/hooks/usePalette' -import {useComposerState} from 'state/shell/composer' +import {useAnimatedValue} from '#/lib/hooks/useAnimatedValue' +import {usePalette} from '#/lib/hooks/usePalette' +import {useComposerState} from '#/state/shell/composer' import {ComposePost} from '../com/composer/Composer' -export const Composer = observer(function ComposerImpl({ - winHeight, -}: { - winHeight: number -}) { +export function Composer({winHeight}: {winHeight: number}) { const state = useComposerState() const pal = usePalette('default') const initInterp = useAnimatedValue(0) @@ -62,7 +57,7 @@ export const Composer = observer(function ComposerImpl({ /> ) -}) +} const styles = StyleSheet.create({ wrapper: { diff --git a/yarn.lock b/yarn.lock index 65b24915dc..860b49daec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16760,21 +16760,6 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -mobx-react-lite@^3.4.0: - version "3.4.3" - resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-3.4.3.tgz#3a4c22c30bfaa8b1b2aa48d12b2ba811c0947ab7" - integrity sha512-NkJREyFTSUXR772Qaai51BnE1voWx56LOL80xG7qkZr6vo8vEaLF3sz1JNUVh+rxmUzxYaqOhfuxTfqUh0FXUg== - -mobx-utils@^6.0.6: - version "6.0.8" - resolved "https://registry.yarnpkg.com/mobx-utils/-/mobx-utils-6.0.8.tgz#843e222c7694050c2e42842682fd24a84fdb7024" - integrity sha512-fPNt0vJnHwbQx9MojJFEnJLfM3EMGTtpy4/qOOW6xueh1mPofMajrbYAUvByMYAvCJnpy1A5L0t+ZVB5niKO4g== - -mobx@^6.6.1: - version "6.10.0" - resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.10.0.tgz#3537680fe98d45232cc19cc8f76280bd8bb6b0b7" - integrity sha512-WMbVpCMFtolbB8swQ5E2YRrU+Yu8iLozCVx3CdGjbBKlP7dFiCSuiG06uea3JCFN5DnvtAX7+G5Bp82e2xu0ww== - moo@^0.5.1: version "0.5.2" resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.2.tgz#f9fe82473bc7c184b0d32e2215d3f6e67278733c" From ed512d6dc5390555232bb4ac3f96f477751c33b1 Mon Sep 17 00:00:00 2001 From: Mary <148872143+mary-ext@users.noreply.github.com> Date: Tue, 24 Sep 2024 23:21:06 +0700 Subject: [PATCH 16/24] Revamp edit image alt text dialog (#5461) * revamp alt dialog * readd the limit check don't trim with enforceLen, it ruins copy-pasting long text and it's overall annoying behavior * Update src/view/com/composer/photos/ImageAltTextDialog.tsx Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> --------- Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> --- src/state/modals/index.tsx | 8 - src/view/com/composer/photos/Gallery.tsx | 14 +- .../composer/photos/ImageAltTextDialog.tsx | 121 +++++++++++ src/view/com/modals/AltImage.tsx | 189 ------------------ src/view/com/modals/Modal.tsx | 6 +- src/view/com/modals/Modal.web.tsx | 9 +- 6 files changed, 136 insertions(+), 211 deletions(-) create mode 100644 src/view/com/composer/photos/ImageAltTextDialog.tsx delete mode 100644 src/view/com/modals/AltImage.tsx diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx index 467853a258..9bc96cf5e4 100644 --- a/src/state/modals/index.tsx +++ b/src/state/modals/index.tsx @@ -3,7 +3,6 @@ import {Image as RNImage} from 'react-native-image-crop-picker' import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' -import {ComposerImage} from '../gallery' export interface EditProfileModal { name: 'edit-profile' @@ -43,12 +42,6 @@ export interface CropImageModal { onSelect: (img?: RNImage) => void } -export interface AltTextImageModal { - name: 'alt-text-image' - image: ComposerImage - onChange: (next: ComposerImage) => void -} - export interface DeleteAccountModal { name: 'delete-account' } @@ -131,7 +124,6 @@ export type Modal = | ListAddRemoveUsersModal // Posts - | AltTextImageModal | CropImageModal | SelfLabelModal diff --git a/src/view/com/composer/photos/Gallery.tsx b/src/view/com/composer/photos/Gallery.tsx index 775413e817..83c1e3c809 100644 --- a/src/view/com/composer/photos/Gallery.tsx +++ b/src/view/com/composer/photos/Gallery.tsx @@ -18,9 +18,10 @@ import {Dimensions} from '#/lib/media/types' import {colors, s} from '#/lib/styles' import {isNative} from '#/platform/detection' import {ComposerImage, cropImage} from '#/state/gallery' -import {useModalControls} from '#/state/modals' import {Text} from '#/view/com/util/text/Text' import {useTheme} from '#/alf' +import * as Dialog from '#/components/Dialog' +import {ImageAltTextDialog} from './ImageAltTextDialog' const IMAGE_GAP = 8 @@ -141,7 +142,8 @@ const GalleryItem = ({ }: GalleryItemProps): React.ReactNode => { const {_} = useLingui() const t = useTheme() - const {openModal} = useModalControls() + + const altTextControl = Dialog.useDialogControl() const onImageEdit = () => { if (isNative) { @@ -153,7 +155,7 @@ const GalleryItem = ({ const onAltTextEdit = () => { Keyboard.dismiss() - openModal({name: 'alt-text-image', image, onChange}) + altTextControl.open() } return ( @@ -229,6 +231,12 @@ const GalleryItem = ({ accessible={true} accessibilityIgnoresInvertColors /> + + ) } diff --git a/src/view/com/composer/photos/ImageAltTextDialog.tsx b/src/view/com/composer/photos/ImageAltTextDialog.tsx new file mode 100644 index 0000000000..123e1066a5 --- /dev/null +++ b/src/view/com/composer/photos/ImageAltTextDialog.tsx @@ -0,0 +1,121 @@ +import React from 'react' +import {ImageStyle, useWindowDimensions, View} from 'react-native' +import {Image} from 'expo-image' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {MAX_ALT_TEXT} from '#/lib/constants' +import {isWeb} from '#/platform/detection' +import {ComposerImage} from '#/state/gallery' +import {atoms as a, useTheme} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import * as TextField from '#/components/forms/TextField' +import {Text} from '#/components/Typography' + +type Props = { + control: Dialog.DialogOuterProps['control'] + image: ComposerImage + onChange: (next: ComposerImage) => void +} + +export const ImageAltTextDialog = (props: Props): React.ReactNode => { + return ( + + + + + + ) +} + +const ImageAltTextInner = ({ + control, + image, + onChange, +}: Props): React.ReactNode => { + const {_} = useLingui() + const t = useTheme() + + const windim = useWindowDimensions() + + const [altText, setAltText] = React.useState(image.alt) + + const onPressSubmit = React.useCallback(() => { + control.close() + onChange({...image, alt: altText.trim()}) + }, [control, image, altText, onChange]) + + const imageStyle = React.useMemo(() => { + const maxWidth = isWeb ? 450 : windim.width + const source = image.transformed ?? image.source + + if (source.height > source.width) { + return { + resizeMode: 'contain', + width: '100%', + aspectRatio: 1, + borderRadius: 8, + } + } + return { + width: '100%', + height: (maxWidth / source.width) * source.height, + borderRadius: 8, + } + }, [image, windim]) + + return ( + + + + + + Add alt text + + + + + + + + + + + Descriptive alt text + + + setAltText(text)} + value={altText} + multiline + numberOfLines={3} + autoFocus + /> + + + + + + ) +} diff --git a/src/view/com/modals/AltImage.tsx b/src/view/com/modals/AltImage.tsx deleted file mode 100644 index c711f73a57..0000000000 --- a/src/view/com/modals/AltImage.tsx +++ /dev/null @@ -1,189 +0,0 @@ -import React, {useCallback, useMemo, useState} from 'react' -import { - ImageStyle, - ScrollView as RNScrollView, - StyleSheet, - TextInput as RNTextInput, - TouchableOpacity, - useWindowDimensions, - View, -} from 'react-native' -import {Image} from 'expo-image' -import {LinearGradient} from 'expo-linear-gradient' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {ComposerImage} from '#/state/gallery' -import {useModalControls} from '#/state/modals' -import {MAX_ALT_TEXT} from 'lib/constants' -import {useIsKeyboardVisible} from 'lib/hooks/useIsKeyboardVisible' -import {usePalette} from 'lib/hooks/usePalette' -import {enforceLen} from 'lib/strings/helpers' -import {gradients, s} from 'lib/styles' -import {useTheme} from 'lib/ThemeContext' -import {isAndroid, isWeb} from 'platform/detection' -import {Text} from '../util/text/Text' -import {ScrollView, TextInput} from './util' - -export const snapPoints = ['100%'] - -interface Props { - image: ComposerImage - onChange: (next: ComposerImage) => void -} - -export function Component({image, onChange}: Props) { - const pal = usePalette('default') - const theme = useTheme() - const {_} = useLingui() - const [altText, setAltText] = useState(image.alt) - const windim = useWindowDimensions() - const {closeModal} = useModalControls() - const inputRef = React.useRef(null) - const scrollViewRef = React.useRef(null) - const keyboardShown = useIsKeyboardVisible() - - // Autofocus hack when we open the modal. We have to wait for the animation to complete first - React.useEffect(() => { - if (isAndroid) return - setTimeout(() => { - inputRef.current?.focus() - }, 500) - }, []) - - // We'd rather be at the bottom here so that we can easily dismiss the modal instead of having to scroll - // (especially on android, it acts weird) - React.useEffect(() => { - if (keyboardShown[0]) { - scrollViewRef.current?.scrollToEnd() - } - }, [keyboardShown]) - - const imageStyles = useMemo(() => { - const maxWidth = isWeb ? 450 : windim.width - const media = image.transformed ?? image.source - if (media.height > media.width) { - return { - resizeMode: 'contain', - width: '100%', - aspectRatio: 1, - borderRadius: 8, - } - } - return { - width: '100%', - height: (maxWidth / media.width) * media.height, - borderRadius: 8, - } - }, [image, windim]) - - const onUpdate = useCallback( - (v: string) => { - v = enforceLen(v, MAX_ALT_TEXT) - setAltText(v) - }, - [setAltText], - ) - - const onPressSave = useCallback(() => { - onChange({ - ...image, - alt: altText, - }) - - closeModal() - }, [closeModal, image, altText, onChange]) - - return ( - - - - - - - - - - - Done - - - - - - - ) -} - -const styles = StyleSheet.create({ - scrollContainer: { - flex: 1, - height: '100%', - paddingHorizontal: isWeb ? 0 : 12, - paddingVertical: isWeb ? 0 : 24, - }, - scrollInner: { - gap: 12, - paddingTop: isWeb ? 0 : 12, - }, - imageContainer: { - borderRadius: 8, - }, - textArea: { - borderWidth: 1, - borderRadius: 6, - paddingTop: 10, - paddingHorizontal: 12, - fontSize: 16, - height: 100, - textAlignVertical: 'top', - }, - button: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - width: '100%', - borderRadius: 32, - padding: 10, - }, - buttonControls: { - gap: 8, - paddingBottom: isWeb ? 0 : 50, - }, -}) diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index fd881ebc4b..90e93821c5 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -3,12 +3,11 @@ import {StyleSheet} from 'react-native' import {SafeAreaView} from 'react-native-safe-area-context' import BottomSheet from '@discord/bottom-sheet/src' +import {usePalette} from '#/lib/hooks/usePalette' import {useModalControls, useModals} from '#/state/modals' -import {usePalette} from 'lib/hooks/usePalette' import {FullWindowOverlay} from '#/components/FullWindowOverlay' import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop' import * as AddAppPassword from './AddAppPasswords' -import * as AltImageModal from './AltImage' import * as ChangeEmailModal from './ChangeEmail' import * as ChangeHandleModal from './ChangeHandle' import * as ChangePasswordModal from './ChangePassword' @@ -74,9 +73,6 @@ export function ModalsContainer() { } else if (activeModal?.name === 'self-label') { snapPoints = SelfLabelModal.snapPoints element = - } else if (activeModal?.name === 'alt-text-image') { - snapPoints = AltImageModal.snapPoints - element = } else if (activeModal?.name === 'change-handle') { snapPoints = ChangeHandleModal.snapPoints element = diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index fe24695d2c..c1024751f8 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -2,13 +2,12 @@ import React from 'react' import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native' import Animated, {FadeIn, FadeOut} from 'react-native-reanimated' +import {usePalette} from '#/lib/hooks/usePalette' import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock' +import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import type {Modal as ModalIface} from '#/state/modals' import {useModalControls, useModals} from '#/state/modals' -import {usePalette} from 'lib/hooks/usePalette' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import * as AddAppPassword from './AddAppPasswords' -import * as AltTextImageModal from './AltImage' import * as ChangeEmailModal from './ChangeEmail' import * as ChangeHandleModal from './ChangeHandle' import * as ChangePasswordModal from './ChangePassword' @@ -53,7 +52,7 @@ function Modal({modal}: {modal: ModalIface}) { } const onPressMask = () => { - if (modal.name === 'crop-image' || modal.name === 'alt-text-image') { + if (modal.name === 'crop-image') { return // dont close on mask presses during crop } closeModal() @@ -88,8 +87,6 @@ function Modal({modal}: {modal: ModalIface}) { element = } else if (modal.name === 'post-languages-settings') { element = - } else if (modal.name === 'alt-text-image') { - element = } else if (modal.name === 'verify-email') { element = } else if (modal.name === 'change-email') { From b9516202fa17325a3d54e54372ddd56149be129c Mon Sep 17 00:00:00 2001 From: Mary <148872143+mary-ext@users.noreply.github.com> Date: Tue, 24 Sep 2024 23:27:40 +0700 Subject: [PATCH 17/24] Revamp image editor (#5462) * new image editor * Rm react-avatar-editor --------- Co-authored-by: Dan Abramov --- package.json | 3 +- src/lib/media/picker.web.tsx | 4 +- src/lib/media/types.ts | 5 +- src/state/modals/index.tsx | 2 + .../com/composer/photos/EditImageDialog.tsx | 14 ++ .../composer/photos/EditImageDialog.web.tsx | 105 ++++++++ src/view/com/composer/photos/Gallery.tsx | 34 +-- src/view/com/modals/CropImage.web.tsx | 145 +++++++++++ src/view/com/modals/Modal.web.tsx | 2 +- .../com/modals/crop-image/CropImage.web.tsx | 228 ------------------ .../com/modals/crop-image/cropImageUtil.ts | 13 - src/view/com/util/UserAvatar.tsx | 18 +- src/view/com/util/UserBanner.tsx | 15 +- yarn.lock | 23 +- 14 files changed, 318 insertions(+), 293 deletions(-) create mode 100644 src/view/com/composer/photos/EditImageDialog.tsx create mode 100644 src/view/com/composer/photos/EditImageDialog.web.tsx create mode 100644 src/view/com/modals/CropImage.web.tsx delete mode 100644 src/view/com/modals/crop-image/CropImage.web.tsx delete mode 100644 src/view/com/modals/crop-image/cropImageUtil.ts diff --git a/package.json b/package.json index 117fc0b190..e1c0f99d8e 100644 --- a/package.json +++ b/package.json @@ -167,9 +167,9 @@ "postinstall-postinstall": "^2.1.0", "psl": "^1.9.0", "react": "18.2.0", - "react-avatar-editor": "^13.0.0", "react-compiler-runtime": "file:./lib/react-compiler-runtime", "react-dom": "^18.2.0", + "react-image-crop": "^11.0.7", "react-keyed-flatten-children": "^3.0.0", "react-native": "0.74.1", "react-native-compressor": "^1.8.24", @@ -236,7 +236,6 @@ "@types/lodash.set": "^4.3.7", "@types/lodash.shuffle": "^4.2.7", "@types/psl": "^1.1.1", - "@types/react-avatar-editor": "^13.0.0", "@types/react-dom": "^18.2.18", "@types/react-responsive": "^8.0.5", "@types/react-test-renderer": "^17.0.1", diff --git a/src/lib/media/picker.web.tsx b/src/lib/media/picker.web.tsx index 8782e14570..a53ffc9614 100644 --- a/src/lib/media/picker.web.tsx +++ b/src/lib/media/picker.web.tsx @@ -18,9 +18,11 @@ export async function openCropper(opts: CropperOptions): Promise { name: 'crop-image', uri: opts.path, dimensions: - opts.height && opts.width + opts.width && opts.height ? {width: opts.width, height: opts.height} : undefined, + aspect: opts.webAspectRatio, + circular: opts.webCircularCrop, onSelect: (img?: RNImage) => { if (img) { resolve(img) diff --git a/src/lib/media/types.ts b/src/lib/media/types.ts index e6f442759f..ec94256ea1 100644 --- a/src/lib/media/types.ts +++ b/src/lib/media/types.ts @@ -18,4 +18,7 @@ export interface CameraOpts { cropperCircleOverlay?: boolean } -export type CropperOptions = Parameters[0] +export type CropperOptions = Parameters[0] & { + webAspectRatio?: number + webCircularCrop?: boolean +} diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx index 9bc96cf5e4..5be21dfd39 100644 --- a/src/state/modals/index.tsx +++ b/src/state/modals/index.tsx @@ -39,6 +39,8 @@ export interface CropImageModal { name: 'crop-image' uri: string dimensions?: {width: number; height: number} + aspect?: number + circular?: boolean onSelect: (img?: RNImage) => void } diff --git a/src/view/com/composer/photos/EditImageDialog.tsx b/src/view/com/composer/photos/EditImageDialog.tsx new file mode 100644 index 0000000000..4263587fd4 --- /dev/null +++ b/src/view/com/composer/photos/EditImageDialog.tsx @@ -0,0 +1,14 @@ +import React from 'react' + +import {ComposerImage} from '#/state/gallery' +import * as Dialog from '#/components/Dialog' + +export type EditImageDialogProps = { + control: Dialog.DialogOuterProps['control'] + image: ComposerImage + onChange: (next: ComposerImage) => void +} + +export const EditImageDialog = ({}: EditImageDialogProps): React.ReactNode => { + return null +} diff --git a/src/view/com/composer/photos/EditImageDialog.web.tsx b/src/view/com/composer/photos/EditImageDialog.web.tsx new file mode 100644 index 0000000000..0afb83ed96 --- /dev/null +++ b/src/view/com/composer/photos/EditImageDialog.web.tsx @@ -0,0 +1,105 @@ +import 'react-image-crop/dist/ReactCrop.css' + +import React from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import ReactCrop, {PercentCrop} from 'react-image-crop' + +import { + ImageSource, + ImageTransformation, + manipulateImage, +} from '#/state/gallery' +import {atoms as a} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {Text} from '#/components/Typography' +import {EditImageDialogProps} from './EditImageDialog' + +export const EditImageDialog = (props: EditImageDialogProps) => { + return ( + + + + ) +} + +const EditImageInner = ({control, image, onChange}: EditImageDialogProps) => { + const {_} = useLingui() + + const source = image.source + + const initialCrop = getInitialCrop(source, image.manips) + const [crop, setCrop] = React.useState(initialCrop) + + const isEmpty = !crop || (crop.width || crop.height) === 0 + const isNew = initialCrop ? true : !isEmpty + + const onPressSubmit = React.useCallback(async () => { + const result = await manipulateImage(image, { + crop: + crop && (crop.width || crop.height) !== 0 + ? { + originX: (crop.x * source.width) / 100, + originY: (crop.y * source.height) / 100, + width: (crop.width * source.width) / 100, + height: (crop.height * source.height) / 100, + } + : undefined, + }) + + onChange(result) + control.close() + }, [crop, image, source, control, onChange]) + + return ( + + + + + Edit image + + + + setCrop(percentCrop)} + className="ReactCrop--no-animate"> + + + + + + + + + ) +} + +const getInitialCrop = ( + source: ImageSource, + manips: ImageTransformation | undefined, +): PercentCrop | undefined => { + const initialArea = manips?.crop + + if (initialArea) { + return { + unit: '%', + x: (initialArea.originX / source.width) * 100, + y: (initialArea.originY / source.height) * 100, + width: (initialArea.width / source.width) * 100, + height: (initialArea.height / source.height) * 100, + } + } +} diff --git a/src/view/com/composer/photos/Gallery.tsx b/src/view/com/composer/photos/Gallery.tsx index 83c1e3c809..369f08d745 100644 --- a/src/view/com/composer/photos/Gallery.tsx +++ b/src/view/com/composer/photos/Gallery.tsx @@ -21,6 +21,7 @@ import {ComposerImage, cropImage} from '#/state/gallery' import {Text} from '#/view/com/util/text/Text' import {useTheme} from '#/alf' import * as Dialog from '#/components/Dialog' +import {EditImageDialog} from './EditImageDialog' import {ImageAltTextDialog} from './ImageAltTextDialog' const IMAGE_GAP = 8 @@ -144,12 +145,15 @@ const GalleryItem = ({ const t = useTheme() const altTextControl = Dialog.useDialogControl() + const editControl = Dialog.useDialogControl() const onImageEdit = () => { if (isNative) { cropImage(image).then(next => { onChange(next) }) + } else { + editControl.open() } } @@ -185,21 +189,15 @@ const GalleryItem = ({ - {isNative && ( - - - - )} + + + + + ) } diff --git a/src/view/com/modals/CropImage.web.tsx b/src/view/com/modals/CropImage.web.tsx new file mode 100644 index 0000000000..41ca306573 --- /dev/null +++ b/src/view/com/modals/CropImage.web.tsx @@ -0,0 +1,145 @@ +import React from 'react' +import {StyleSheet, TouchableOpacity, View} from 'react-native' +import {Image as RNImage} from 'react-native-image-crop-picker' +import {manipulateAsync, SaveFormat} from 'expo-image-manipulator' +import {LinearGradient} from 'expo-linear-gradient' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import ReactCrop, {PercentCrop} from 'react-image-crop' + +import {usePalette} from '#/lib/hooks/usePalette' +import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' +import {getDataUriSize} from '#/lib/media/util' +import {gradients, s} from '#/lib/styles' +import {useModalControls} from '#/state/modals' +import {Text} from '#/view/com/util/text/Text' + +export const snapPoints = ['0%'] + +export function Component({ + uri, + aspect, + circular, + onSelect, +}: { + uri: string + aspect?: number + circular?: boolean + onSelect: (img?: RNImage) => void +}) { + const pal = usePalette('default') + const {_} = useLingui() + + const {closeModal} = useModalControls() + const {isMobile} = useWebMediaQueries() + + const imageRef = React.useRef(null) + const [crop, setCrop] = React.useState() + + const isEmpty = !crop || (crop.width || crop.height) === 0 + + const onPressCancel = () => { + onSelect(undefined) + closeModal() + } + const onPressDone = async () => { + const img = imageRef.current! + + const result = await manipulateAsync( + uri, + isEmpty + ? [] + : [ + { + crop: { + originX: (crop.x * img.naturalWidth) / 100, + originY: (crop.y * img.naturalHeight) / 100, + width: (crop.width * img.naturalWidth) / 100, + height: (crop.height * img.naturalHeight) / 100, + }, + }, + ], + { + base64: true, + format: SaveFormat.JPEG, + }, + ) + + onSelect({ + path: result.uri, + mime: 'image/jpeg', + size: result.base64 !== undefined ? getDataUriSize(result.base64) : 0, + width: result.width, + height: result.height, + }) + + closeModal() + } + + return ( + + + setCrop(percentCrop)} + circularCrop={circular}> + + + + + + + Cancel + + + + + + + Done + + + + + + ) +} + +const styles = StyleSheet.create({ + cropper: { + marginLeft: 'auto', + marginRight: 'auto', + borderWidth: 1, + borderRadius: 4, + overflow: 'hidden', + alignItems: 'center', + }, + ctrls: { + flexDirection: 'row', + alignItems: 'center', + marginTop: 10, + }, + btns: { + flexDirection: 'row', + alignItems: 'center', + marginTop: 10, + }, + btn: { + borderRadius: 4, + paddingVertical: 8, + paddingHorizontal: 24, + }, +}) diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index c1024751f8..a2acc23bb9 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -12,7 +12,7 @@ import * as ChangeEmailModal from './ChangeEmail' import * as ChangeHandleModal from './ChangeHandle' import * as ChangePasswordModal from './ChangePassword' import * as CreateOrEditListModal from './CreateOrEditList' -import * as CropImageModal from './crop-image/CropImage.web' +import * as CropImageModal from './CropImage.web' import * as DeleteAccountModal from './DeleteAccount' import * as EditProfileModal from './EditProfile' import * as InviteCodesModal from './InviteCodes' diff --git a/src/view/com/modals/crop-image/CropImage.web.tsx b/src/view/com/modals/crop-image/CropImage.web.tsx deleted file mode 100644 index 10cae2f174..0000000000 --- a/src/view/com/modals/crop-image/CropImage.web.tsx +++ /dev/null @@ -1,228 +0,0 @@ -import React from 'react' -import {StyleSheet, TouchableOpacity, View} from 'react-native' -import {Image as RNImage} from 'react-native-image-crop-picker' -import {LinearGradient} from 'expo-linear-gradient' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {Slider} from '@miblanchard/react-native-slider' -import ImageEditor from 'react-avatar-editor' - -import {useModalControls} from '#/state/modals' -import {usePalette} from 'lib/hooks/usePalette' -import {RectTallIcon, RectWideIcon, SquareIcon} from 'lib/icons' -import {Dimensions} from 'lib/media/types' -import {getDataUriSize} from 'lib/media/util' -import {gradients, s} from 'lib/styles' -import {Text} from 'view/com/util/text/Text' -import {calculateDimensions} from './cropImageUtil' - -enum AspectRatio { - Square = 'square', - Wide = 'wide', - Tall = 'tall', - Custom = 'custom', -} - -const DIMS: Record = { - [AspectRatio.Square]: {width: 1000, height: 1000}, - [AspectRatio.Wide]: {width: 1000, height: 750}, - [AspectRatio.Tall]: {width: 750, height: 1000}, -} - -export const snapPoints = ['0%'] - -export function Component({ - uri, - dimensions, - onSelect, -}: { - uri: string - dimensions?: Dimensions - onSelect: (img?: RNImage) => void -}) { - const {closeModal} = useModalControls() - const pal = usePalette('default') - const {_} = useLingui() - const defaultAspectStyle = dimensions - ? AspectRatio.Custom - : AspectRatio.Square - const [as, setAs] = React.useState(defaultAspectStyle) - const [scale, setScale] = React.useState(1) - const editorRef = React.useRef(null) - const imageEditorWidth = dimensions ? dimensions.width : DIMS[as].width - const imageEditorHeight = dimensions ? dimensions.height : DIMS[as].height - - const doSetAs = (v: AspectRatio) => () => setAs(v) - - const onPressCancel = () => { - onSelect(undefined) - closeModal() - } - const onPressDone = () => { - const canvas = editorRef.current?.getImageScaledToCanvas() - if (canvas) { - const dataUri = canvas.toDataURL('image/jpeg') - onSelect({ - path: dataUri, - mime: 'image/jpeg', - size: getDataUriSize(dataUri), - width: imageEditorWidth, - height: imageEditorHeight, - }) - } else { - onSelect(undefined) - } - closeModal() - } - - let cropperStyle - if (as === AspectRatio.Square) { - cropperStyle = styles.cropperSquare - } else if (as === AspectRatio.Wide) { - cropperStyle = styles.cropperWide - } else if (as === AspectRatio.Tall) { - cropperStyle = styles.cropperTall - } else if (as === AspectRatio.Custom) { - const cropperDimensions = calculateDimensions( - 550, - imageEditorHeight, - imageEditorWidth, - ) - cropperStyle = { - width: cropperDimensions.width, - height: cropperDimensions.height, - } - } - - return ( - - - - - - - setScale(Array.isArray(v) ? v[0] : v) - } - minimumValue={1} - maximumValue={3} - containerStyle={styles.slider} - /> - {as === AspectRatio.Custom ? null : ( - <> - - - - - - - - - - - )} - - - - - Cancel - - - - - - - Done - - - - - - ) -} - -const styles = StyleSheet.create({ - cropper: { - marginLeft: 'auto', - marginRight: 'auto', - borderWidth: 1, - borderRadius: 4, - overflow: 'hidden', - }, - cropperSquare: { - width: 400, - height: 400, - }, - cropperWide: { - width: 400, - height: 300, - }, - cropperTall: { - width: 300, - height: 400, - }, - imageEditor: { - maxWidth: '100%', - }, - ctrls: { - flexDirection: 'row', - alignItems: 'center', - marginTop: 10, - }, - slider: { - flex: 1, - marginRight: 10, - }, - btns: { - flexDirection: 'row', - alignItems: 'center', - marginTop: 10, - }, - btn: { - borderRadius: 4, - paddingVertical: 8, - paddingHorizontal: 24, - }, -}) diff --git a/src/view/com/modals/crop-image/cropImageUtil.ts b/src/view/com/modals/crop-image/cropImageUtil.ts deleted file mode 100644 index 303d15ba5b..0000000000 --- a/src/view/com/modals/crop-image/cropImageUtil.ts +++ /dev/null @@ -1,13 +0,0 @@ -export const calculateDimensions = ( - maxWidth: number, - originalHeight: number, - originalWidth: number, -) => { - const aspectRatio = originalWidth / originalHeight - const newHeight = maxWidth / aspectRatio - const newWidth = maxWidth - return { - width: newWidth, - height: newHeight, - } -} diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index b2f56c1385..76d9d1503e 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -8,17 +8,17 @@ import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useQueryClient} from '@tanstack/react-query' -import {logger} from '#/logger' -import {usePalette} from 'lib/hooks/usePalette' +import {usePalette} from '#/lib/hooks/usePalette' import { useCameraPermission, usePhotoLibraryPermission, -} from 'lib/hooks/usePermissions' -import {makeProfileLink} from 'lib/routes/links' -import {colors} from 'lib/styles' -import {isAndroid, isNative, isWeb} from 'platform/detection' -import {precacheProfile} from 'state/queries/profile' -import {HighPriorityImage} from 'view/com/util/images/Image' +} from '#/lib/hooks/usePermissions' +import {makeProfileLink} from '#/lib/routes/links' +import {colors} from '#/lib/styles' +import {logger} from '#/logger' +import {isAndroid, isNative, isWeb} from '#/platform/detection' +import {precacheProfile} from '#/state/queries/profile' +import {HighPriorityImage} from '#/view/com/util/images/Image' import {tokens, useTheme} from '#/alf' import { Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled, @@ -321,6 +321,8 @@ let EditableUserAvatar = ({ height: 1000, width: 1000, path: item.path, + webAspectRatio: 1, + webCircularCrop: true, }) onSelectNewAvatar(croppedImage) diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx index 93ea32750d..13f4081fce 100644 --- a/src/view/com/util/UserBanner.tsx +++ b/src/view/com/util/UserBanner.tsx @@ -6,16 +6,16 @@ import {ModerationUI} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {logger} from '#/logger' -import {usePalette} from 'lib/hooks/usePalette' +import {usePalette} from '#/lib/hooks/usePalette' import { useCameraPermission, usePhotoLibraryPermission, -} from 'lib/hooks/usePermissions' -import {colors} from 'lib/styles' -import {useTheme} from 'lib/ThemeContext' -import {isAndroid, isNative} from 'platform/detection' -import {EventStopper} from 'view/com/util/EventStopper' +} from '#/lib/hooks/usePermissions' +import {colors} from '#/lib/styles' +import {useTheme} from '#/lib/ThemeContext' +import {logger} from '#/logger' +import {isAndroid, isNative} from '#/platform/detection' +import {EventStopper} from '#/view/com/util/EventStopper' import {tokens, useTheme as useAlfTheme} from '#/alf' import { Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled, @@ -72,6 +72,7 @@ export function UserBanner({ path: items[0].path, width: 3000, height: 1000, + webAspectRatio: 3, }), ) } catch (e: any) { diff --git a/yarn.lock b/yarn.lock index 860b49daec..f3d6ae5fd3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2570,7 +2570,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-runtime@^7.0.0", "@babel/plugin-transform-runtime@^7.12.1", "@babel/plugin-transform-runtime@^7.16.4": +"@babel/plugin-transform-runtime@^7.0.0", "@babel/plugin-transform-runtime@^7.16.4": version "7.22.10" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.22.10.tgz#89eda6daf1d3af6f36fb368766553054c8d7cd46" integrity sha512-RchI7HePu1eu0CYNKHHHQdfenZcM4nz8rew5B1VWqeRKdcwW5aQ5HeG9eTUbWiAS1UrmHVLmoxTWHt3iLD/NhA== @@ -8262,13 +8262,6 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== -"@types/react-avatar-editor@^13.0.0": - version "13.0.0" - resolved "https://registry.yarnpkg.com/@types/react-avatar-editor/-/react-avatar-editor-13.0.0.tgz#5963e16c931746c47e478d669dd72d388b427393" - integrity sha512-5ymOayy6mfT35xTqzni7UjXvCNEg8/pH4pI5RenITp9PBc02KGTYjSV1WboXiQDYSh5KomLT0ngBLEAIhV1QoQ== - dependencies: - "@types/react" "*" - "@types/react-dom@^18.2.18": version "18.2.18" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.18.tgz#16946e6cd43971256d874bc3d0a72074bb8571dd" @@ -18935,15 +18928,6 @@ react-app-polyfill@^3.0.0: regenerator-runtime "^0.13.9" whatwg-fetch "^3.6.2" -react-avatar-editor@^13.0.0: - version "13.0.0" - resolved "https://registry.yarnpkg.com/react-avatar-editor/-/react-avatar-editor-13.0.0.tgz#55013625ee9ae715c1fe2dc553b8079994d8a5f2" - integrity sha512-0xw63MbRRQdDy7YI1IXU9+7tTFxYEFLV8CABvryYOGjZmXRTH2/UA0mafe57ns62uaEFX181kA4XlGlxCaeXKA== - dependencies: - "@babel/plugin-transform-runtime" "^7.12.1" - "@babel/runtime" "^7.12.5" - prop-types "^15.7.2" - "react-compiler-runtime@file:./lib/react-compiler-runtime": version "0.0.1" @@ -19003,6 +18987,11 @@ react-freeze@^1.0.0: resolved "https://registry.yarnpkg.com/react-freeze/-/react-freeze-1.0.3.tgz#5e3ca90e682fed1d73a7cb50c2c7402b3e85618d" integrity sha512-ZnXwLQnGzrDpHBHiC56TXFXvmolPeMjTn1UOm610M4EXGzbEDR7oOIyS2ZiItgbs6eZc4oU/a0hpk8PrcKvv5g== +react-image-crop@^11.0.7: + version "11.0.7" + resolved "https://registry.yarnpkg.com/react-image-crop/-/react-image-crop-11.0.7.tgz#25f3d37ccbb65a05d19d23b4740a5912835c741e" + integrity sha512-ZciKWHDYzmm366JDL18CbrVyjnjH0ojufGDmScfS4ZUqLHg4nm6ATY+K62C75W4ZRNt4Ii+tX0bSjNk9LQ2xzQ== + "react-is@^16.12.0 || ^17.0.0 || ^18.0.0", react-is@^18.0.0, react-is@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" From d2fae81b33ae0a73d0b9f87700365d60bc51f094 Mon Sep 17 00:00:00 2001 From: Hailey Date: Tue, 24 Sep 2024 09:28:12 -0700 Subject: [PATCH 18/24] Remove `react-native-fs` (#5463) * remove rnfs * tweak e2e * log * use `safeDeleteAsync` --- package.json | 1 - src/lib/api/upload-blob.ts | 8 +++++--- src/lib/media/picker.e2e.tsx | 34 +++++++++++++++++++++++----------- yarn.lock | 15 +-------------- 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index e1c0f99d8e..5b2369d2a1 100644 --- a/package.json +++ b/package.json @@ -175,7 +175,6 @@ "react-native-compressor": "^1.8.24", "react-native-date-picker": "^4.4.2", "react-native-drawer-layout": "^4.0.0-alpha.3", - "react-native-fs": "^2.20.0", "react-native-gesture-handler": "~2.16.2", "react-native-get-random-values": "~1.11.0", "react-native-image-crop-picker": "0.41.2", diff --git a/src/lib/api/upload-blob.ts b/src/lib/api/upload-blob.ts index 0814d5185b..07aeaf1a7e 100644 --- a/src/lib/api/upload-blob.ts +++ b/src/lib/api/upload-blob.ts @@ -1,6 +1,8 @@ -import RNFS from 'react-native-fs' +import {copyAsync} from 'expo-file-system' import {BskyAgent, ComAtprotoRepoUploadBlob} from '@atproto/api' +import {safeDeleteAsync} from '#/lib/media/manip' + /** * @param encoding Allows overriding the blob's type */ @@ -65,7 +67,7 @@ async function withSafeFile( // temporary file). const newPath = uri.replace(/\.jpe?g$/, '.bin') try { - await RNFS.copyFile(uri, newPath) + await copyAsync({from: uri, to: newPath}) } catch { // Failed to copy the file, just use the original return await fn(uri) @@ -74,7 +76,7 @@ async function withSafeFile( return await fn(newPath) } finally { // Remove the temporary file - await RNFS.unlink(newPath) + await safeDeleteAsync(newPath) } } else { return fn(uri) diff --git a/src/lib/media/picker.e2e.tsx b/src/lib/media/picker.e2e.tsx index e6b46ba774..fc6fcde45e 100644 --- a/src/lib/media/picker.e2e.tsx +++ b/src/lib/media/picker.e2e.tsx @@ -1,25 +1,37 @@ -import RNFS from 'react-native-fs' import { Image as RNImage, openCropper as openCropperFn, } from 'react-native-image-crop-picker' +import { + documentDirectory, + getInfoAsync, + readDirectoryAsync, +} from 'expo-file-system' import {compressIfNeeded} from './manip' import {CropperOptions} from './types' async function getFile() { - let files = await RNFS.readDir( - RNFS.LibraryDirectoryPath.split('/') - .slice(0, -5) - .concat(['Media', 'DCIM', '100APPLE']) - .join('/'), - ) - files = files.filter(file => file.path.endsWith('.JPG')) - const file = files[0] + const imagesDir = documentDirectory! + .split('/') + .slice(0, -6) + .concat(['Media', 'DCIM', '100APPLE']) + .join('/') + + let files = await readDirectoryAsync(imagesDir) + files = files.filter(file => file.endsWith('.JPG')) + const file = `${imagesDir}/${files[0]}` + + const fileInfo = await getInfoAsync(file) + + if (!fileInfo.exists) { + throw new Error('Failed to get file info') + } + return await compressIfNeeded({ - path: file.path, + path: file, mime: 'image/jpeg', - size: file.size, + size: fileInfo.size, width: 4288, height: 2848, }) diff --git a/yarn.lock b/yarn.lock index f3d6ae5fd3..225f109f74 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9540,7 +9540,7 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base-64@0.1.0, base-64@^0.1.0: +base-64@0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb" integrity sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA== @@ -19038,14 +19038,6 @@ react-native-drawer-layout@^4.0.0-alpha.3: dependencies: use-latest-callback "^0.1.9" -react-native-fs@^2.20.0: - version "2.20.0" - resolved "https://registry.yarnpkg.com/react-native-fs/-/react-native-fs-2.20.0.tgz#05a9362b473bfc0910772c0acbb73a78dbc810f6" - integrity sha512-VkTBzs7fIDUiy/XajOSNk0XazFE9l+QlMAce7lGuebZcag5CnjszB+u4BdqzwaQOdcYb5wsJIsqq4kxInIRpJQ== - dependencies: - base-64 "^0.1.0" - utf8 "^3.0.0" - react-native-gesture-handler@~2.16.2: version "2.16.2" resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.16.2.tgz#032bd2a07334292d7f6cff1dc9d1ec928f72e26d" @@ -21830,11 +21822,6 @@ use-sidecar@^1.1.2: detect-node-es "^1.1.0" tslib "^2.0.0" -utf8@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/utf8/-/utf8-3.0.0.tgz#f052eed1364d696e769ef058b183df88c87f69d1" - integrity sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ== - util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" From ea43d20c61547523e34ae864ca4ddffdedd8dfb1 Mon Sep 17 00:00:00 2001 From: Hailey Date: Tue, 24 Sep 2024 10:15:33 -0700 Subject: [PATCH 19/24] Remove image resizer (#5464) --- __tests__/lib/images.test.ts | 128 +++++++++--------- jest/jestSetup.js | 12 +- package.json | 1 - src/lib/media/manip.ts | 74 +++++++--- src/view/com/composer/useExternalLinkFetch.ts | 26 ++-- yarn.lock | 5 - 6 files changed, 147 insertions(+), 99 deletions(-) diff --git a/__tests__/lib/images.test.ts b/__tests__/lib/images.test.ts index 595f566c47..a5acad25f6 100644 --- a/__tests__/lib/images.test.ts +++ b/__tests__/lib/images.test.ts @@ -1,26 +1,30 @@ -import ImageResizer from '@bam.tech/react-native-image-resizer' +import {deleteAsync} from 'expo-file-system' +import {manipulateAsync, SaveFormat} from 'expo-image-manipulator' import RNFetchBlob from 'rn-fetch-blob' import { downloadAndResize, DownloadAndResizeOpts, + getResizedDimensions, } from '../../src/lib/media/manip' +const mockResizedImage = { + path: 'file://resized-image.jpg', + size: 100, + width: 100, + height: 100, + mime: 'image/jpeg', +} + describe('downloadAndResize', () => { const errorSpy = jest.spyOn(global.console, 'error') - const mockResizedImage = { - path: jest.fn().mockReturnValue('file://resized-image.jpg'), - size: 100, - width: 50, - height: 50, - mime: 'image/jpeg', - } - beforeEach(() => { - const mockedCreateResizedImage = - ImageResizer.createResizedImage as jest.Mock - mockedCreateResizedImage.mockResolvedValue(mockResizedImage) + const mockedCreateResizedImage = manipulateAsync as jest.Mock + mockedCreateResizedImage.mockResolvedValue({ + uri: 'file://resized-image.jpg', + ...mockResizedImage, + }) }) afterEach(() => { @@ -54,17 +58,17 @@ describe('downloadAndResize', () => { 'GET', 'https://example.com/image.jpg', ) - expect(ImageResizer.createResizedImage).toHaveBeenCalledWith( - 'file://downloaded-image.jpg', - 100, - 100, - 'JPEG', - 100, - undefined, - undefined, - undefined, - {mode: 'cover'}, + + // First time it gets called is to get dimensions + expect(manipulateAsync).toHaveBeenCalledWith(expect.any(String), [], {}) + expect(manipulateAsync).toHaveBeenCalledWith( + expect.any(String), + [{resize: {height: opts.height, width: opts.width}}], + {format: SaveFormat.JPEG, compress: 1.0}, ) + expect(deleteAsync).toHaveBeenCalledWith(expect.any(String), { + idempotent: true, + }) }) it('should return undefined for invalid URI', async () => { @@ -82,46 +86,6 @@ describe('downloadAndResize', () => { expect(result).toBeUndefined() }) - it('should return undefined for unsupported file type', async () => { - const mockedFetch = RNFetchBlob.fetch as jest.Mock - mockedFetch.mockResolvedValueOnce({ - path: jest.fn().mockReturnValue('file://downloaded-image'), - info: jest.fn().mockReturnValue({status: 200}), - flush: jest.fn(), - }) - - const opts: DownloadAndResizeOpts = { - uri: 'https://example.com/image', - width: 100, - height: 100, - maxSize: 500000, - mode: 'cover', - timeout: 10000, - } - - const result = await downloadAndResize(opts) - expect(result).toEqual(mockResizedImage) - expect(RNFetchBlob.config).toHaveBeenCalledWith({ - fileCache: true, - appendExt: 'jpeg', - }) - expect(RNFetchBlob.fetch).toHaveBeenCalledWith( - 'GET', - 'https://example.com/image', - ) - expect(ImageResizer.createResizedImage).toHaveBeenCalledWith( - 'file://downloaded-image', - 100, - 100, - 'JPEG', - 100, - undefined, - undefined, - undefined, - {mode: 'cover'}, - ) - }) - it('should return undefined for non-200 response', async () => { const mockedFetch = RNFetchBlob.fetch as jest.Mock mockedFetch.mockResolvedValueOnce({ @@ -143,4 +107,44 @@ describe('downloadAndResize', () => { expect(errorSpy).not.toHaveBeenCalled() expect(result).toBeUndefined() }) + + it('should not downsize whenever dimensions are below the max dimensions', () => { + const initialDimensionsOne = { + width: 1200, + height: 1000, + } + const resizedDimensionsOne = getResizedDimensions(initialDimensionsOne) + + const initialDimensionsTwo = { + width: 1000, + height: 1200, + } + const resizedDimensionsTwo = getResizedDimensions(initialDimensionsTwo) + + expect(resizedDimensionsOne).toEqual(initialDimensionsOne) + expect(resizedDimensionsTwo).toEqual(initialDimensionsTwo) + }) + + it('should resize dimensions and maintain aspect ratio if they are above the max dimensons', () => { + const initialDimensionsOne = { + width: 3000, + height: 1500, + } + const resizedDimensionsOne = getResizedDimensions(initialDimensionsOne) + + const initialDimensionsTwo = { + width: 2000, + height: 4000, + } + const resizedDimensionsTwo = getResizedDimensions(initialDimensionsTwo) + + expect(resizedDimensionsOne).toEqual({ + width: 2000, + height: 1000, + }) + expect(resizedDimensionsTwo).toEqual({ + width: 1000, + height: 2000, + }) + }) }) diff --git a/jest/jestSetup.js b/jest/jestSetup.js index a68c1dc4bf..50a33589ea 100644 --- a/jest/jestSetup.js +++ b/jest/jestSetup.js @@ -42,8 +42,16 @@ jest.mock('rn-fetch-blob', () => ({ fetch: jest.fn(), })) -jest.mock('@bam.tech/react-native-image-resizer', () => ({ - createResizedImage: jest.fn(), +jest.mock('expo-file-system', () => ({ + getInfoAsync: jest.fn().mockResolvedValue({exists: true, size: 100}), + deleteAsync: jest.fn(), +})) + +jest.mock('expo-image-manipulator', () => ({ + manipulateAsync: jest.fn().mockResolvedValue({ + uri: 'file://resized-image', + }), + SaveFormat: jest.requireActual('expo-image-manipulator').SaveFormat, })) jest.mock('@segment/analytics-react-native', () => ({ diff --git a/package.json b/package.json index 5b2369d2a1..4b3486545e 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,6 @@ }, "dependencies": { "@atproto/api": "^0.13.7", - "@bam.tech/react-native-image-resizer": "^3.0.4", "@braintree/sanitize-url": "^6.0.2", "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", "@emoji-mart/react": "^1.1.1", diff --git a/src/lib/media/manip.ts b/src/lib/media/manip.ts index 3f01e98c5e..e75f13755f 100644 --- a/src/lib/media/manip.ts +++ b/src/lib/media/manip.ts @@ -6,18 +6,20 @@ import { copyAsync, deleteAsync, EncodingType, + getInfoAsync, makeDirectoryAsync, StorageAccessFramework, writeAsStringAsync, } from 'expo-file-system' +import {manipulateAsync, SaveFormat} from 'expo-image-manipulator' import * as MediaLibrary from 'expo-media-library' import * as Sharing from 'expo-sharing' -import ImageResizer from '@bam.tech/react-native-image-resizer' import {Buffer} from 'buffer' import RNFetchBlob from 'rn-fetch-blob' +import {POST_IMG_MAX} from '#/lib/constants' import {logger} from '#/logger' -import {isAndroid, isIOS} from 'platform/detection' +import {isAndroid, isIOS} from '#/platform/detection' import {Dimensions} from './types' export async function compressIfNeeded( @@ -165,29 +167,47 @@ interface DoResizeOpts { } async function doResize(localUri: string, opts: DoResizeOpts): Promise { + // We need to get the dimensions of the image before we resize it. Previously, the library we used allowed us to enter + // a "max size", and it would do the "best possible size" calculation for us. + // Now instead, we have to supply the final dimensions to the manipulation function instead. + // Performing an "empty" manipulation lets us get the dimensions of the original image. React Native's Image.getSize() + // does not work for local files... + const imageRes = await manipulateAsync(localUri, [], {}) + const newDimensions = getResizedDimensions({ + width: imageRes.width, + height: imageRes.height, + }) + for (let i = 0; i < 9; i++) { - const quality = 100 - i * 10 - const resizeRes = await ImageResizer.createResizedImage( + // nearest 10th + const quality = Math.round((1 - 0.1 * i) * 10) / 10 + const resizeRes = await manipulateAsync( localUri, - opts.width, - opts.height, - 'JPEG', - quality, - undefined, - undefined, - undefined, - {mode: opts.mode}, + [{resize: newDimensions}], + { + format: SaveFormat.JPEG, + compress: quality, + }, ) - if (resizeRes.size < opts.maxSize) { + + const fileInfo = await getInfoAsync(resizeRes.uri) + if (!fileInfo.exists) { + throw new Error( + 'The image manipulation library failed to create a new image.', + ) + } + + if (fileInfo.size < opts.maxSize) { + safeDeleteAsync(imageRes.uri) return { - path: normalizePath(resizeRes.path), + path: normalizePath(resizeRes.uri), mime: 'image/jpeg', - size: resizeRes.size, + size: fileInfo.size, width: resizeRes.width, height: resizeRes.height, } } else { - safeDeleteAsync(resizeRes.path) + safeDeleteAsync(resizeRes.uri) } } throw new Error( @@ -311,3 +331,25 @@ async function withTempFile( safeDeleteAsync(tmpDirUri) } } + +export function getResizedDimensions(originalDims: { + width: number + height: number +}) { + if ( + originalDims.width <= POST_IMG_MAX.width && + originalDims.height <= POST_IMG_MAX.height + ) { + return originalDims + } + + const ratio = Math.min( + POST_IMG_MAX.width / originalDims.width, + POST_IMG_MAX.height / originalDims.height, + ) + + return { + width: Math.round(originalDims.width * ratio), + height: Math.round(originalDims.height * ratio), + } +} diff --git a/src/view/com/composer/useExternalLinkFetch.ts b/src/view/com/composer/useExternalLinkFetch.ts index 1a36b50348..60afadefea 100644 --- a/src/view/com/composer/useExternalLinkFetch.ts +++ b/src/view/com/composer/useExternalLinkFetch.ts @@ -2,23 +2,18 @@ import {useEffect, useState} from 'react' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {logger} from '#/logger' -import {createComposerImage} from '#/state/gallery' -import {useFetchDid} from '#/state/queries/handle' -import {useGetPost} from '#/state/queries/post' -import {useAgent} from '#/state/session' -import * as apilib from 'lib/api/index' -import {POST_IMG_MAX} from 'lib/constants' +import * as apilib from '#/lib/api/index' +import {POST_IMG_MAX} from '#/lib/constants' import { EmbeddingDisabledError, getFeedAsEmbed, getListAsEmbed, getPostAsQuote, getStarterPackAsEmbed, -} from 'lib/link-meta/bsky' -import {getLinkMeta} from 'lib/link-meta/link-meta' -import {resolveShortLink} from 'lib/link-meta/resolve-short-link' -import {downloadAndResize} from 'lib/media/manip' +} from '#/lib/link-meta/bsky' +import {getLinkMeta} from '#/lib/link-meta/link-meta' +import {resolveShortLink} from '#/lib/link-meta/resolve-short-link' +import {downloadAndResize} from '#/lib/media/manip' import { isBskyCustomFeedUrl, isBskyListUrl, @@ -26,8 +21,13 @@ import { isBskyStarterPackUrl, isBskyStartUrl, isShortLink, -} from 'lib/strings/url-helpers' -import {ComposerOpts} from 'state/shell/composer' +} from '#/lib/strings/url-helpers' +import {logger} from '#/logger' +import {createComposerImage} from '#/state/gallery' +import {useFetchDid} from '#/state/queries/handle' +import {useGetPost} from '#/state/queries/post' +import {useAgent} from '#/state/session' +import {ComposerOpts} from '#/state/shell/composer' export function useExternalLinkFetch({ setQuote, diff --git a/yarn.lock b/yarn.lock index 225f109f74..17fe862372 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2983,11 +2983,6 @@ "@babel/helper-validator-identifier" "^7.24.6" to-fast-properties "^2.0.0" -"@bam.tech/react-native-image-resizer@^3.0.4": - version "3.0.5" - resolved "https://registry.yarnpkg.com/@bam.tech/react-native-image-resizer/-/react-native-image-resizer-3.0.5.tgz#6661ba020de156268f73bdc92fbb93ef86f88a13" - integrity sha512-u5QGUQGGVZiVCJ786k9/kd7pPRZ6eYfJCYO18myVCH8FbVI7J8b5GT2Svjj2x808DlWeqfaZOOzxPqo27XYvrQ== - "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" From 6338083a73847a08dcbcfaa9497f5771448dd9e1 Mon Sep 17 00:00:00 2001 From: Hailey Date: Tue, 24 Sep 2024 13:25:05 -0700 Subject: [PATCH 20/24] [React Native] Patch `RCTFileReaderModule` `readAsDataURL` to prevent crash when `type` is `nil` (#5475) --- patches/react-native+0.74.1.patch | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/patches/react-native+0.74.1.patch b/patches/react-native+0.74.1.patch index 789ba84ace..c91e88c3e8 100644 --- a/patches/react-native+0.74.1.patch +++ b/patches/react-native+0.74.1.patch @@ -1,5 +1,18 @@ +diff --git a/node_modules/react-native/Libraries/Blob/RCTFileReaderModule.mm b/node_modules/react-native/Libraries/Blob/RCTFileReaderModule.mm +index caa5540..6027825 100644 +--- a/node_modules/react-native/Libraries/Blob/RCTFileReaderModule.mm ++++ b/node_modules/react-native/Libraries/Blob/RCTFileReaderModule.mm +@@ -71,7 +71,7 @@ @implementation RCTFileReaderModule + [NSString stringWithFormat:@"Unable to resolve data for blob: %@", [RCTConvert NSString:blob[@"blobId"]]], + nil); + } else { +- NSString *type = [RCTConvert NSString:blob[@"type"]]; ++ NSString *type = RCTNilIfNull([RCTConvert NSString:blob[@"type"]]); + NSString *text = [NSString stringWithFormat:@"data:%@;base64,%@", + type != nil && [type length] > 0 ? type : @"application/octet-stream", + [data base64EncodedStringWithOptions:0]]; diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm -index b0d71dc..9974932 100644 +index b0d71dc..41b9a0e 100644 --- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm +++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm @@ -377,10 +377,6 @@ - (void)textInputDidBeginEditing @@ -36,7 +49,7 @@ index e9b330f..1ecdf0a 100644 + @end diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m -index b09e653..4c32b31 100644 +index b09e653..f93cb46 100644 --- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m +++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m @@ -198,9 +198,53 @@ - (void)refreshControlValueChanged From 4f0217403d2812971f8ae89c4adb8c2419d978f0 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 24 Sep 2024 21:26:48 +0100 Subject: [PATCH 21/24] add a bunch of settings-related icons (#5471) --- assets/icons/accessibility_stroke2_corner2_rounded.svg | 1 + assets/icons/arrowBoxLeft_stroke2_corner2_rounded.svg | 1 + assets/icons/at_stroke2_corner2_rounded.svg | 1 + assets/icons/birthdayCake_stroke2_corner2_rounded.svg | 1 + assets/icons/bubbleInfo_stroke2_corner2_rounded.svg | 1 + assets/icons/circleQuestion_stroke2_corner2_rounded.svg | 1 + assets/icons/envelope_stroke2_corner2_rounded.svg | 1 + assets/icons/eye_stroke2_corner2_rounded.svg | 1 + assets/icons/lock_stroke2_corner2_rounded.svg | 1 + assets/icons/paintRoller_stroke2_corner2_rounded.svg | 1 + assets/icons/person_stroke2_corner2_rounded.svg | 1 + assets/icons/trash_stroke2_corner2_rounded.svg | 1 + assets/icons/verified_stroke2_corner2_rounded.svg | 1 + assets/icons/window_stroke2_corner2_rounded.svg | 1 + src/components/icons/Accessibility.tsx | 5 +++++ src/components/icons/ArrowBoxLeft.tsx | 4 ++++ src/components/icons/At.tsx | 6 +++++- src/components/icons/BirthdayCake.tsx | 5 +++++ src/components/icons/BubbleInfo.tsx | 5 +++++ src/components/icons/CircleQuestion.tsx | 5 +++++ src/components/icons/Envelope.tsx | 4 ++++ src/components/icons/Eye.tsx | 4 ++++ src/components/icons/Lock.tsx | 4 ++++ src/components/icons/PaintRoller.tsx | 5 +++++ src/components/icons/Person.tsx | 4 ++++ src/components/icons/Trash.tsx | 4 ++++ src/components/icons/Verified.tsx | 5 +++++ src/components/icons/Window.tsx | 5 +++++ 28 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 assets/icons/accessibility_stroke2_corner2_rounded.svg create mode 100644 assets/icons/arrowBoxLeft_stroke2_corner2_rounded.svg create mode 100644 assets/icons/at_stroke2_corner2_rounded.svg create mode 100644 assets/icons/birthdayCake_stroke2_corner2_rounded.svg create mode 100644 assets/icons/bubbleInfo_stroke2_corner2_rounded.svg create mode 100644 assets/icons/circleQuestion_stroke2_corner2_rounded.svg create mode 100644 assets/icons/envelope_stroke2_corner2_rounded.svg create mode 100644 assets/icons/eye_stroke2_corner2_rounded.svg create mode 100644 assets/icons/lock_stroke2_corner2_rounded.svg create mode 100644 assets/icons/paintRoller_stroke2_corner2_rounded.svg create mode 100644 assets/icons/person_stroke2_corner2_rounded.svg create mode 100644 assets/icons/trash_stroke2_corner2_rounded.svg create mode 100644 assets/icons/verified_stroke2_corner2_rounded.svg create mode 100644 assets/icons/window_stroke2_corner2_rounded.svg create mode 100644 src/components/icons/Accessibility.tsx create mode 100644 src/components/icons/BirthdayCake.tsx create mode 100644 src/components/icons/BubbleInfo.tsx create mode 100644 src/components/icons/CircleQuestion.tsx create mode 100644 src/components/icons/PaintRoller.tsx create mode 100644 src/components/icons/Verified.tsx create mode 100644 src/components/icons/Window.tsx diff --git a/assets/icons/accessibility_stroke2_corner2_rounded.svg b/assets/icons/accessibility_stroke2_corner2_rounded.svg new file mode 100644 index 0000000000..62184bd8d9 --- /dev/null +++ b/assets/icons/accessibility_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/arrowBoxLeft_stroke2_corner2_rounded.svg b/assets/icons/arrowBoxLeft_stroke2_corner2_rounded.svg new file mode 100644 index 0000000000..ea9afbc60f --- /dev/null +++ b/assets/icons/arrowBoxLeft_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/at_stroke2_corner2_rounded.svg b/assets/icons/at_stroke2_corner2_rounded.svg new file mode 100644 index 0000000000..37ccbda238 --- /dev/null +++ b/assets/icons/at_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/birthdayCake_stroke2_corner2_rounded.svg b/assets/icons/birthdayCake_stroke2_corner2_rounded.svg new file mode 100644 index 0000000000..542e1552f4 --- /dev/null +++ b/assets/icons/birthdayCake_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/bubbleInfo_stroke2_corner2_rounded.svg b/assets/icons/bubbleInfo_stroke2_corner2_rounded.svg new file mode 100644 index 0000000000..2cc08924f8 --- /dev/null +++ b/assets/icons/bubbleInfo_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/circleQuestion_stroke2_corner2_rounded.svg b/assets/icons/circleQuestion_stroke2_corner2_rounded.svg new file mode 100644 index 0000000000..a534f98716 --- /dev/null +++ b/assets/icons/circleQuestion_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/envelope_stroke2_corner2_rounded.svg b/assets/icons/envelope_stroke2_corner2_rounded.svg new file mode 100644 index 0000000000..39331f8a12 --- /dev/null +++ b/assets/icons/envelope_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/eye_stroke2_corner2_rounded.svg b/assets/icons/eye_stroke2_corner2_rounded.svg new file mode 100644 index 0000000000..81e31ba032 --- /dev/null +++ b/assets/icons/eye_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/lock_stroke2_corner2_rounded.svg b/assets/icons/lock_stroke2_corner2_rounded.svg new file mode 100644 index 0000000000..8e34c3b05c --- /dev/null +++ b/assets/icons/lock_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/paintRoller_stroke2_corner2_rounded.svg b/assets/icons/paintRoller_stroke2_corner2_rounded.svg new file mode 100644 index 0000000000..3ebb36aa82 --- /dev/null +++ b/assets/icons/paintRoller_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/person_stroke2_corner2_rounded.svg b/assets/icons/person_stroke2_corner2_rounded.svg new file mode 100644 index 0000000000..7088c2880c --- /dev/null +++ b/assets/icons/person_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/trash_stroke2_corner2_rounded.svg b/assets/icons/trash_stroke2_corner2_rounded.svg new file mode 100644 index 0000000000..e97dfe90c4 --- /dev/null +++ b/assets/icons/trash_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/verified_stroke2_corner2_rounded.svg b/assets/icons/verified_stroke2_corner2_rounded.svg new file mode 100644 index 0000000000..048b2816e3 --- /dev/null +++ b/assets/icons/verified_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/window_stroke2_corner2_rounded.svg b/assets/icons/window_stroke2_corner2_rounded.svg new file mode 100644 index 0000000000..859c00c4a5 --- /dev/null +++ b/assets/icons/window_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ + diff --git a/src/components/icons/Accessibility.tsx b/src/components/icons/Accessibility.tsx new file mode 100644 index 0000000000..1e5ec0c090 --- /dev/null +++ b/src/components/icons/Accessibility.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const Accessibility_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M4 12a8 8 0 1 1 16 0 8 8 0 0 1-16 0Zm8-10C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2Zm0 7.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Zm-2.86.26.014.002c.944.125 1.893.238 2.846.238.95 0 1.904-.113 2.846-.238l.014-.002h.003a1 1 0 0 1 .273 1.98l-.006.002-.017.002c-.67.089-1.341.162-2.014.21.195 1.32.65 2.33 1.626 3.357a1 1 0 0 1-1.45 1.378 8.3 8.3 0 0 1-1.234-1.647 8.2 8.2 0 0 1-1.342 1.673 1 1 0 0 1-1.398-1.43c.673-.658 1.088-1.274 1.342-1.922.163-.42.269-.878.32-1.404a33 33 0 0 1-2.075-.215l-.017-.002-.006-.001a1 1 0 0 1 .271-1.982l.004.001Z', +}) diff --git a/src/components/icons/ArrowBoxLeft.tsx b/src/components/icons/ArrowBoxLeft.tsx index 011bf6afa3..82e0d6e7f6 100644 --- a/src/components/icons/ArrowBoxLeft.tsx +++ b/src/components/icons/ArrowBoxLeft.tsx @@ -3,3 +3,7 @@ import {createSinglePathSVG} from './TEMPLATE' export const ArrowBoxLeft_Stroke2_Corner0_Rounded = createSinglePathSVG({ path: 'M3.293 3.293A1 1 0 0 1 4 3h7.25a1 1 0 1 1 0 2H5v14h6.25a1 1 0 1 1 0 2H4a1 1 0 0 1-1-1V4a1 1 0 0 1 .293-.707Zm11.5 3.5a1 1 0 0 1 1.414 0l4.5 4.5a1 1 0 0 1 0 1.414l-4.5 4.5a1 1 0 0 1-1.414-1.414L17.586 13H8.75a1 1 0 1 1 0-2h8.836l-2.793-2.793a1 1 0 0 1 0-1.414Z', }) + +export const ArrowBoxLeft_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M6 5a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h5.25a1 1 0 1 1 0 2H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h5.25a1 1 0 1 1 0 2H6Zm8.793 1.793a1 1 0 0 1 1.414 0l4.5 4.5a1 1 0 0 1 0 1.414l-4.5 4.5a1 1 0 0 1-1.414-1.414L17.586 13H8.75a1 1 0 1 1 0-2h8.836l-2.793-2.793a1 1 0 0 1 0-1.414Z', +}) diff --git a/src/components/icons/At.tsx b/src/components/icons/At.tsx index 2487250545..ef0d1003f1 100644 --- a/src/components/icons/At.tsx +++ b/src/components/icons/At.tsx @@ -1,5 +1,9 @@ import {createSinglePathSVG} from './TEMPLATE' export const At_Stroke2_Corner0_Rounded = createSinglePathSVG({ - path: 'M12 4a8 8 0 1 0 4.21 14.804 1 1 0 0 1 1.054 1.7A9.958 9.958 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10c0 1.104-.27 2.31-.949 3.243-.716.984-1.849 1.6-3.331 1.465a4.207 4.207 0 0 1-2.93-1.585c-.94 1.21-2.388 1.94-3.985 1.715-2.53-.356-4.04-2.91-3.682-5.458.358-2.547 2.514-4.586 5.044-4.23.905.127 1.68.536 2.286 1.126a1 1 0 0 1 1.964.368l-.515 3.545v.002a2.222 2.222 0 0 0 1.999 2.526c.75.068 1.212-.21 1.533-.65.358-.493.566-1.245.566-2.067a8 8 0 0 0-8-8Zm-.112 5.13c-1.195-.168-2.544.819-2.784 2.529-.24 1.71.784 3.03 1.98 3.198 1.195.168 2.543-.819 2.784-2.529.24-1.71-.784-3.03-1.98-3.198Z', + path: 'M12 4a8 8 0 1 0 4.21 14.804 1 1 0 0 1 1.054 1.7A9.96 9.96 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10c0 1.104-.27 2.31-.949 3.243-.716.984-1.849 1.6-3.331 1.465a4.2 4.2 0 0 1-2.93-1.585c-.94 1.21-2.388 1.94-3.985 1.715-2.53-.356-4.04-2.91-3.682-5.458s2.514-4.586 5.044-4.23c.905.127 1.68.536 2.286 1.126a1 1 0 0 1 1.964.368l-.515 3.545v.002a2.22 2.22 0 0 0 1.999 2.526c.75.068 1.212-.21 1.533-.65.358-.493.566-1.245.566-2.067a8 8 0 0 0-8-8Zm-.112 5.13c-1.195-.168-2.544.819-2.784 2.529s.784 3.03 1.98 3.198 2.543-.819 2.784-2.529-.784-3.03-1.98-3.198Z', +}) + +export const At_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M12 4a8 8 0 1 0 4.21 14.804 1 1 0 0 1 1.054 1.7A9.96 9.96 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10c0 1.104-.27 2.31-.949 3.243-.716.984-1.849 1.6-3.331 1.465a4.2 4.2 0 0 1-2.93-1.585c-.94 1.21-2.388 1.94-3.985 1.715-2.53-.356-4.04-2.91-3.682-5.458s2.514-4.586 5.044-4.23c.905.127 1.68.536 2.286 1.126a1 1 0 0 1 1.964.368l-.515 3.545v.002a2.22 2.22 0 0 0 1.999 2.526c.75.068 1.212-.21 1.533-.65.358-.493.566-1.245.566-2.067a8 8 0 0 0-8-8Zm-.112 5.13c-1.195-.168-2.544.819-2.784 2.529s.784 3.03 1.98 3.198 2.544-.819 2.784-2.529-.784-3.03-1.98-3.198Z', }) diff --git a/src/components/icons/BirthdayCake.tsx b/src/components/icons/BirthdayCake.tsx new file mode 100644 index 0000000000..8e41cbac11 --- /dev/null +++ b/src/components/icons/BirthdayCake.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const BirthdayCake_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'm12 .757 2.122 2.122A3 3 0 0 1 13 7.829V9h4.5a3 3 0 0 1 3 3v1.646c0 .603-.18 1.177-.5 1.658V19a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3v-3.696a3 3 0 0 1-.5-1.658V12a3 3 0 0 1 3-3H11V7.829a3 3 0 0 1-1.121-4.95L12 .757ZM6.5 11a1 1 0 0 0-1 1v1.646a1 1 0 0 0 .629.928l.5.2a1 1 0 0 0 .742 0l1.015-.405a3 3 0 0 1 2.228 0l1.015.405a1 1 0 0 0 .742 0l1.015-.405a3 3 0 0 1 2.228 0l1.015.405a1 1 0 0 0 .742 0l.5-.2a1 1 0 0 0 .629-.928V12a1 1 0 0 0-1-1h-11ZM6 16.674V19a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-2.326a3 3 0 0 1-2.114-.043l-1.015-.405a1 1 0 0 0-.742 0l-1.015.405a3 3 0 0 1-2.228 0l-1.015-.405a1 1 0 0 0-.742 0l-1.015.405A3 3 0 0 1 6 16.674ZM12.002 6a1 1 0 0 0 .706-1.707L12 3.586l-.707.707A1 1 0 0 0 12.002 6Z', +}) diff --git a/src/components/icons/BubbleInfo.tsx b/src/components/icons/BubbleInfo.tsx new file mode 100644 index 0000000000..2865713743 --- /dev/null +++ b/src/components/icons/BubbleInfo.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const BubbleInfo_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M6.002 5h12a1 1 0 0 1 1 1v10.036a1 1 0 0 1-1 1h-2.626a2 2 0 0 0-1.276.46l-2.098 1.738-2.065-1.731a2 2 0 0 0-1.285-.467h-2.65a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1Zm12-2h-12a3 3 0 0 0-3 3v10.036a3 3 0 0 0 3 3h2.65l2.704 2.266a1 1 0 0 0 1.28.004l2.74-2.27h2.626a3 3 0 0 0 3-3V6a3 3 0 0 0-3-3ZM13 11.75a1 1 0 1 0-2 0v2a1 1 0 1 0 2 0v-2ZM12 10a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5Z', +}) diff --git a/src/components/icons/CircleQuestion.tsx b/src/components/icons/CircleQuestion.tsx new file mode 100644 index 0000000000..4eb369379b --- /dev/null +++ b/src/components/icons/CircleQuestion.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const CircleQuestion_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Z" clip-rule="evenodd"/> 0 ? type : @"application/octet-stream", +- type != nil && [type length] > 0 ? type : @"application/octet-stream", ++ ![type isEqual:[NSNull null]] && [type length] > 0 ? type : @"application/octet-stream", [data base64EncodedStringWithOptions:0]]; + + resolve(text); diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm index b0d71dc..41b9a0e 100644 --- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm From 2429d5d1ae51fa21d46970263fa581fd2eb19cdd Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 24 Sep 2024 18:10:32 -0500 Subject: [PATCH 24/24] Fix composer jumpiness on native (#5476) --- .../com/composer/text-input/TextInput.tsx | 50 ++++++++----------- 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/src/view/com/composer/text-input/TextInput.tsx b/src/view/com/composer/text-input/TextInput.tsx index 95c57ad899..3df9cfca47 100644 --- a/src/view/com/composer/text-input/TextInput.tsx +++ b/src/view/com/composer/text-input/TextInput.tsx @@ -8,7 +8,7 @@ import React, { } from 'react' import { NativeSyntheticEvent, - StyleSheet, + Text as RNText, TextInput as RNTextInput, TextInputSelectionChangeEventData, View, @@ -20,18 +20,16 @@ import PasteInput, { } from '@mattermost/react-native-paste-input' import {POST_IMG_MAX} from '#/lib/constants' -import {usePalette} from '#/lib/hooks/usePalette' import {downloadAndResize} from '#/lib/media/manip' import {isUriImage} from '#/lib/media/util' import {cleanError} from '#/lib/strings/errors' import {getMentionAt, insertMentionAt} from '#/lib/strings/mention-manip' import {useTheme} from '#/lib/ThemeContext' -import {isAndroid} from '#/platform/detection' +import {isAndroid, isNative} from '#/platform/detection' import { LinkFacetMatch, suggestLinkCardUri, } from '#/view/com/composer/text-input/text-input-util' -import {Text} from '#/view/com/util/text/Text' import {atoms as a, useAlf} from '#/alf' import {normalizeTextStyles} from '#/components/Typography' import {Autocomplete} from './mobile/Autocomplete' @@ -70,7 +68,6 @@ export const TextInput = forwardRef(function TextInputImpl( ref, ) { const {theme: t, fonts} = useAlf() - const pal = usePalette('default') const textInput = useRef(null) const textInputSelection = useRef({start: 0, end: 0}) const theme = useTheme() @@ -193,10 +190,12 @@ export const TextInput = forwardRef(function TextInputImpl( }, ) - /* - * `PasteInput` appears to prefer no `lineHeight` + /** + * PasteInput doesn't like `lineHeight`, results in jumpiness */ - style.lineHeight = undefined + if (isNative) { + style.lineHeight = undefined + } /* * Android impl of `PasteInput` doesn't support the array syntax for `fontVariant` @@ -215,18 +214,23 @@ export const TextInput = forwardRef(function TextInputImpl( return Array.from(richtext.segments()).map(segment => { return ( - + style={[ + inputTextStyle, + { + color: segment.facet ? t.palette.primary_500 : t.atoms.text.color, + marginTop: -1, + }, + ]}> {segment.text} - + ) }) - }, [richtext, pal.link, pal.text, inputTextStyle]) + }, [t, richtext, inputTextStyle]) return ( - + {textDecorated} @@ -252,17 +256,3 @@ export const TextInput = forwardRef(function TextInputImpl( ) }) - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - textInput: { - flex: 1, - width: '100%', - padding: 5, - paddingBottom: 20, - marginLeft: 8, - alignSelf: 'flex-start', - }, -})