From 4e8d86db317864257f6416cd867d7bb07baca6d0 Mon Sep 17 00:00:00 2001 From: Mary <148872143+mary-ext@users.noreply.github.com> Date: Mon, 23 Sep 2024 21:38:04 +0700 Subject: [PATCH 1/5] Let Expo/Webpack handle CSS assets (#3942) * chore: handle built css assets * chore: let prettier handle css code * refactor: let webpack build css assets * chore: prettier on bskyembed * chore: touch empty.txt on css directory * chore: do the same to the workflow --- .github/workflows/golang-test-lint.yml | 4 +- .prettierignore | 4 +- Dockerfile.embedr | 1 + bskyembed/src/index.css | 2 +- bskyweb/.gitignore | 4 + bskyweb/static/css/.gitkeep | 0 bskyweb/templates/base.html | 381 ----------------------- package.json | 2 +- scripts/post-web-build.js | 25 +- src/App.web.tsx | 1 + src/style.css | 403 +++++++++++++++++++++++++ web/index.html | 382 ----------------------- 12 files changed, 440 insertions(+), 769 deletions(-) create mode 100644 bskyweb/static/css/.gitkeep create mode 100644 src/style.css diff --git a/.github/workflows/golang-test-lint.yml b/.github/workflows/golang-test-lint.yml index 36e28841d5..c8124dbade 100644 --- a/.github/workflows/golang-test-lint.yml +++ b/.github/workflows/golang-test-lint.yml @@ -21,7 +21,7 @@ jobs: with: go-version: '1.22' - name: Dummy Static Files - run: touch bskyweb/static/js/blah.js && touch bskyweb/static/media/blah.txt + run: touch bskyweb/static/js/blah.js && touch bskyweb/static/css/blah.txt && touch bskyweb/static/media/blah.txt - name: Check run: cd bskyweb/ && make check - name: Build (binary) @@ -38,6 +38,6 @@ jobs: with: go-version: '1.22' - name: Dummy Static Files - run: touch bskyweb/static/js/blah.js && touch bskyweb/static/media/blah.txt + run: touch bskyweb/static/js/blah.js && touch bskyweb/static/css/blah.txt && touch bskyweb/static/media/blah.txt - name: Lint run: cd bskyweb/ && make lint diff --git a/.prettierignore b/.prettierignore index 641a1b8bfc..8ccbae2148 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1,4 @@ -# Ignore everything except JS-ey code. +# Ignore everything except JS-ey or CSS code. # Based on https://stackoverflow.com/a/70715829/458193 * !**/*.js @@ -7,6 +7,8 @@ !**/*.tsx !*/ +!**/*.css + # More specific ignores go below. .expo android diff --git a/Dockerfile.embedr b/Dockerfile.embedr index 9ff04aa5c7..663cbcfc51 100644 --- a/Dockerfile.embedr +++ b/Dockerfile.embedr @@ -40,6 +40,7 @@ RUN find ./bskyweb/embedr-static && find ./bskyweb/embedr-templates && find ./bs # hack around issue with empty directory and go:embed RUN touch bskyweb/static/js/empty.txt +RUN touch bskyweb/static/css/empty.txt RUN touch bskyweb/static/media/empty.txt # diff --git a/bskyembed/src/index.css b/bskyembed/src/index.css index 23457ec28d..22b2b8be5c 100644 --- a/bskyembed/src/index.css +++ b/bskyembed/src/index.css @@ -4,4 +4,4 @@ .break-word { word-break: break-word; -} \ No newline at end of file +} diff --git a/bskyweb/.gitignore b/bskyweb/.gitignore index 05b3ad7ab5..a63a381f94 100644 --- a/bskyweb/.gitignore +++ b/bskyweb/.gitignore @@ -10,6 +10,10 @@ static/js/*.js static/js/*.map static/js/*.js.LICENSE.txt static/js/empty.txt +static/css/*.css +static/css/*.map +static/css/*.css.LICENSE.txt +static/css/empty.txt static/media/*.png static/media/empty.txt templates/scripts.html diff --git a/bskyweb/static/css/.gitkeep b/bskyweb/static/css/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bskyweb/templates/base.html b/bskyweb/templates/base.html index 609c17c7ce..eaa31aa4a3 100644 --- a/bskyweb/templates/base.html +++ b/bskyweb/templates/base.html @@ -32,387 +32,6 @@ --> - {% include "scripts.html" %} diff --git a/package.json b/package.json index 05b5f086db..e3aff1fe0c 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "web": "expo start --web", "use-build-number": "./scripts/useBuildNumberEnv.sh", "use-build-number-with-bump": "./scripts/useBuildNumberEnvWithBump.sh", - "build-web": "expo export:web && node ./scripts/post-web-build.js && cp -v ./web-build/static/js/*.* ./bskyweb/static/js/ && cp -v ./web-build/static/media/* ./bskyweb/static/media/", + "build-web": "expo export:web && node ./scripts/post-web-build.js", "build-all": "yarn intl:build && yarn use-build-number-with-bump eas build --platform all", "build-ios": "yarn use-build-number-with-bump eas build -p ios", "build-android": "yarn use-build-number-with-bump eas build -p android", diff --git a/scripts/post-web-build.js b/scripts/post-web-build.js index baaa7cb8b7..7bbee38554 100644 --- a/scripts/post-web-build.js +++ b/scripts/post-web-build.js @@ -20,7 +20,30 @@ console.log(`Writing ${templateFile}`) const outputFile = entrypoints .map(name => { const file = path.basename(name) - return `` + const ext = path.extname(file) + + if (ext === '.js') { + return `` + } + if (ext === '.css') { + return `` + } + + return '' }) .join('\n') fs.writeFileSync(templateFile, outputFile) + +function copyFiles(sourceDir, targetDir) { + const files = fs.readdirSync(path.join(projectRoot, sourceDir)) + files.forEach(file => { + const sourcePath = path.join(projectRoot, sourceDir, file) + const targetPath = path.join(projectRoot, targetDir, file) + fs.copyFileSync(sourcePath, targetPath) + console.log(`Copied ${sourcePath} to ${targetPath}`) + }) +} + +copyFiles('web-build/static/js', 'bskyweb/static/js') +copyFiles('web-build/static/css', 'bskyweb/static/css') +copyFiles('web-build/static/media', 'bskyweb/static/media') diff --git a/src/App.web.tsx b/src/App.web.tsx index c81ed10d33..7d98737a3b 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -1,5 +1,6 @@ import 'lib/sentry' // must be near top import 'view/icons' +import './style.css' import React, {useEffect, useState} from 'react' import {KeyboardProvider} from 'react-native-keyboard-controller' diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000000..29e9770e32 --- /dev/null +++ b/src/style.css @@ -0,0 +1,403 @@ +@font-face { + font-family: 'Inter-Regular'; + src: local('Inter-Regular'), + url(/static/media/Inter-Regular.1f5ed03b6dd9fd1f9982.otf) format('font/otf'); + font-weight: 400; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: 'Inter-Italic'; + src: local('Inter-Italic'), + url(/static/media/Inter-Italic.95778eb0c75dc956257e.otf) format('font/otf'); + font-weight: 400; + font-style: italic; + font-display: swap; +} +/* +@font-face { + font-family: "Inter-Medium"; + src: local("Inter-Medium"), url(/static/media/Inter-Medium.296aa2d65964269836b3.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"); + font-weight: 500; + font-style: italic; + font-display: swap; +} +*/ +@font-face { + font-family: 'Inter-SemiBold'; + src: local('Inter-SemiBold'), + url(/static/media/Inter-SemiBold.2277990330981b8409bb.otf) + format('font/otf'); + font-weight: 600; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: 'Inter-SemiBoldItalic'; + src: local('Inter-SemiBoldItalic'), + url(/static/media/Inter-SemiBoldItalic.f62fea3df3a521d6c8a7.otf) + format('font/otf'); + font-weight: 600; + font-style: italic; + font-display: swap; +} +/* +@font-face { + font-family: "Inter-Bold"; + src: local("Inter-Bold"), url(/static/media/Inter-Bold.8d330503e1d034ad68de.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"); + font-weight: 700; + font-style: italic; + font-display: swap; +} +*/ +@font-face { + font-family: 'Inter-ExtraBold'; + src: local('Inter-ExtraBold'), + url(/static/media/Inter-ExtraBold.ff2581a193bf6b7e0b06.otf) + format('font/otf'); + font-weight: 800; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: 'Inter-ExtraBoldItalic'; + src: local('Inter-ExtraBoldItalic'), + url(/static/media/Inter-ExtraBoldItalic.0e50b40728d24d40fdf4.otf) + format('font/otf'); + font-weight: 800; + font-style: italic; + font-display: swap; +} +/* +@font-face { + font-family: "Inter-Black"; + src: local("Inter-Black"), url(/static/media/Inter-Black.66e9a87f1c921e844ed4.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"); + font-weight: 900; + font-style: italic; + font-display: swap; +} +*/ + +/** + * Extend the react-native-web reset: + * https://github.com/necolas/react-native-web/blob/master/packages/react-native-web/src/exports/StyleSheet/initialRules.js + */ +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 { + flex-shrink: 0; + flex-basis: auto; + flex-grow: 1; + display: flex; + flex: 1; +} + +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, +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, + 'Liberation Sans', Helvetica, Arial, sans-serif; +} + +#preload { + width: 100px; + position: fixed; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); +} + +/* Buttons and inputs have a font set by UA, so we'll have to reset that */ +button, +input, +textarea { + font: inherit; + 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, +input:-webkit-autofill:hover, +input:-webkit-autofill:focus, +input:-webkit-autofill:active { + -webkit-background-clip: text; + -webkit-text-fill-color: var(--text); + transition: background-color 5000s ease-in-out 0s; + box-shadow: inset 0 0 20px 20px var(--background); + background: var(--background); + color: var(--text); +} +/* Force left-align date/time inputs on iOS mobile */ +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; +} +a[role='link']:hover { + text-decoration: underline; +} +a[role='link'][data-no-underline='1']:hover { + text-decoration: none; +} + +/* Styling hacks */ +*[data-word-wrap] { + word-break: break-word; +} +*[data-stable-gutters] { + scrollbar-gutter: stable both-edges; +} + +/* ProseMirror */ +.ProseMirror { + font: 18px -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, + 'Liberation Sans', Helvetica, Arial, sans-serif; + min-height: 140px; +} +.ProseMirror-dark { + color: white; +} +.ProseMirror p { + margin: 0; +} +.ProseMirror p.is-editor-empty:first-child::before { + color: #8d8e96; + content: attr(data-placeholder); + float: left; + height: 0; + pointer-events: none; +} +.ProseMirror .mention { + color: #0085ff; +} +.ProseMirror a, +.ProseMirror .autolink { + color: #0085ff; +} +/* OLLIE: TODO -- this is not accessible */ +/* Remove focus state on inputs */ +.ProseMirror-focused { + outline: 0; +} +textarea:focus, +input:focus { + outline: 0; +} +.tippy-content .items { + width: fit-content; +} + +/* Tooltips */ +[data-tooltip] { + position: relative; + z-index: 10; +} +[data-tooltip]::after { + content: attr(data-tooltip); + display: none; + position: absolute; + bottom: 0; + left: 50%; + transform: translateY(100%) translateY(8px) translateX(-50%); + padding: 4px 10px; + border-radius: 10px; + background: var(--backgroundLight); + color: var(--text); + text-align: center; + white-space: nowrap; + font-size: 12px; + z-index: 10; +} +[data-tooltip]::before { + content: ''; + display: none; + position: absolute; + border-bottom: 6px solid var(--backgroundLight); + border-left: 6px solid transparent; + border-right: 6px solid transparent; + bottom: 0; + left: 50%; + transform: translateY(100%) translateY(2px) translateX(-50%); + z-index: 10; +} +[data-tooltip]:hover::after, +[data-tooltip]:hover::before { + display: block; +} + +/* NativeDropdown component */ +.radix-dropdown-item:focus, +.nativeDropdown-item:focus { + outline: none; +} + +/* Spinner component */ +@keyframes rotate { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} +.rotate-500ms { + position: absolute; + inset: 0; + animation: rotate 500ms linear infinite; +} + +@keyframes avatarHoverFadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes avatarHoverFadeOut { + from { + opacity: 1; + } + to { + opacity: 0; + } +} + +.force-no-clicks > *, +.force-no-clicks * { + pointer-events: none !important; +} + +input[type='range'][orient='vertical'] { + writing-mode: vertical-lr; + direction: rtl; + appearance: slider-vertical; + width: 16px; + vertical-align: bottom; + -webkit-appearance: none; + appearance: none; + background: transparent; + cursor: pointer; +} + +input[type='range'][orient='vertical']::-webkit-slider-runnable-track { + background: white; + height: 100%; + width: 4px; + border-radius: 4px; +} + +input[type='range'][orient='vertical']::-moz-range-track { + background: white; + height: 100%; + width: 4px; + border-radius: 4px; +} + +input[type='range']::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + border-radius: 50%; + background-color: white; + height: 16px; + width: 16px; + margin-left: -6px; +} + +input[type='range'][orient='vertical']::-moz-range-thumb { + border: none; + border-radius: 50%; + background-color: white; + height: 16px; + width: 16px; + margin-left: -6px; +} diff --git a/web/index.html b/web/index.html index 3a132d25b8..512178327f 100644 --- a/web/index.html +++ b/web/index.html @@ -36,388 +36,6 @@ --> - - 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 2/5] 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 3/5] 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 4/5] 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 5/5] [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 = ({ />