diff --git a/Dockerfile b/Dockerfile index 371e8402c7..32a45d2ca9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.25-bookworm AS build-env +FROM golang:1.26-bookworm AS build-env WORKDIR /usr/src/social-app diff --git a/Dockerfile.embedr b/Dockerfile.embedr index 29b042dddc..4f13b6d1a6 100644 --- a/Dockerfile.embedr +++ b/Dockerfile.embedr @@ -1,4 +1,4 @@ -FROM golang:1.25-bookworm AS build-env +FROM golang:1.26-bookworm AS build-env WORKDIR /usr/src/social-app diff --git a/assets/icons/paperPlaneVertical_filled_stroke2_corner1_rounded.svg b/assets/icons/paperPlaneVertical_filled_stroke2_corner1_rounded.svg new file mode 100644 index 0000000000..081ed9c746 --- /dev/null +++ b/assets/icons/paperPlaneVertical_filled_stroke2_corner1_rounded.svg @@ -0,0 +1 @@ + diff --git a/bskyembed/assets/logo_full_name.svg b/bskyembed/assets/logo_full_name.svg index 7e46f1426e..150b3121ea 100644 --- a/bskyembed/assets/logo_full_name.svg +++ b/bskyembed/assets/logo_full_name.svg @@ -1,10 +1,10 @@ - - - - - - - - - + + + + + + + + + diff --git a/bskyembed/src/components/container.tsx b/bskyembed/src/components/container.tsx index 554d47142e..893319f367 100644 --- a/bskyembed/src/components/container.tsx +++ b/bskyembed/src/components/container.tsx @@ -20,7 +20,7 @@ export function Container({ if (!entry) return let {height} = entry.contentRect - height += 2 // border top and bottom + height += 4 // border-2 = 2px top + 2px bottom if (height !== prevHeight.current) { prevHeight.current = height window.parent.postMessage( @@ -37,7 +37,7 @@ export function Container({ return (
{ if (ref.current && href) { // forwardRef requires preact/compat - let's keep it simple @@ -49,9 +49,7 @@ export function Container({ } }}> {href && } -
- {children} -
+
{children}
) } diff --git a/bskyembed/src/components/embed.tsx b/bskyembed/src/components/embed.tsx index a7c1d92156..5dbde9426b 100644 --- a/bskyembed/src/components/embed.tsx +++ b/bskyembed/src/components/embed.tsx @@ -16,6 +16,7 @@ import {useMemo} from 'preact/hooks' import infoIcon from '../../assets/circleInfo_stroke2_corner0_rounded.svg' import playIcon from '../../assets/play_filled_corner0_rounded.svg' import starterPackIcon from '../../assets/starterPack.svg' +import {Globe} from '../icons/Globe' import {CONTENT_LABELS, labelsToInfo} from '../labels' import * as bsky from '../types/bsky' import {getRkey} from '../util/rkey' @@ -93,7 +94,7 @@ export function Embed({ />
-

+

{record.author.displayName?.trim() || record.author.handle}

{verification.isVerified && ( @@ -103,7 +104,7 @@ export function Embed({ size={12} /> )} -

+

@{record.author.handle}

@@ -334,13 +335,18 @@ function ExternalEmbed({ /> )}
-

- {toNiceDomain(content.external.uri)} +

+ {content.external.title}

-

{content.external.title}

-

+

{content.external.description}

+
+ +

+ {toNiceDomain(content.external.uri)} +

+
) @@ -374,7 +380,7 @@ function GenericWithImageEmbed({
)}
-

{title}

+

{title}

{subtitle}

diff --git a/bskyembed/src/components/post.tsx b/bskyembed/src/components/post.tsx index fb32250464..4167dc0869 100644 --- a/bskyembed/src/components/post.tsx +++ b/bskyembed/src/components/post.tsx @@ -53,7 +53,7 @@ export function Post({thread}: Props) { return (
+ className="block font-semibold text-[15px] min-[400px]:text-[17px] leading-5 line-clamp-1 hover:underline underline-offset-2 text-ellipsis decoration-2"> {post.author.displayName?.trim() || post.author.handle} {verification.isVerified && ( @@ -87,72 +87,68 @@ export function Post({thread}: Props) { /> )}
- - @{post.author.handle} - +
+ + @{post.author.handle} + + · + + Follow + +
+ -
-
- {!!post.likeCount && ( -
- -

- {prettyNumber(post.likeCount)} -

-
- )} - {!!post.replyCount && ( -
- -

- {prettyNumber(post.replyCount)} -

-
- )} - - {!!post.repostCount && ( -
- -

- {prettyNumber(post.repostCount)} -

-
- )} +
+
+
+ {!!post.likeCount && ( +
+ +

+ {prettyNumber(post.likeCount)} +

+
+ )} + {!!post.replyCount && ( +
+ +

+ {prettyNumber(post.replyCount)} +

+
+ )} + {!!post.repostCount && ( +
+ +

+ {prettyNumber(post.repostCount)} +

+
+ )} +
+ + +
- - + +
-
- - - -
) } @@ -177,7 +173,7 @@ function PostContent({record}: {record: AppBskyFeedPost.Record | null}) { + className="text-brand hover:underline"> {segment.text} , ) @@ -205,7 +201,7 @@ function PostContent({record}: {record: AppBskyFeedPost.Record | null}) { + className="text-brand hover:underline"> {segment.text} , ) @@ -217,7 +213,7 @@ function PostContent({record}: {record: AppBskyFeedPost.Record | null}) { } return ( -

+

{richText}

) diff --git a/bskyembed/src/icons/Globe.tsx b/bskyembed/src/icons/Globe.tsx new file mode 100644 index 0000000000..adcac782d3 --- /dev/null +++ b/bskyembed/src/icons/Globe.tsx @@ -0,0 +1,24 @@ +import {h} from 'preact' + +export const Globe = ({ + size = 14, + className, +}: { + size?: number + className?: string +}) => ( + + + +) diff --git a/bskyembed/src/screens/landing.tsx b/bskyembed/src/screens/landing.tsx index bb650241f4..703cbad1b5 100644 --- a/bskyembed/src/screens/landing.tsx +++ b/bskyembed/src/screens/landing.tsx @@ -39,6 +39,7 @@ render(, root) function LandingPage() { const [uri, setUri] = useState('') const [colorMode, setColorMode] = useState('system') + const [error, setError] = useState(null) const [loading, setLoading] = useState(false) const [thread, setThread] = useState( diff --git a/bskyembed/src/util/nice-date.ts b/bskyembed/src/util/nice-date.ts index 016c97a694..c6efa6471b 100644 --- a/bskyembed/src/util/nice-date.ts +++ b/bskyembed/src/util/nice-date.ts @@ -1,11 +1,11 @@ export function niceDate(date: number | string | Date) { const d = new Date(date) - return `${d.toLocaleDateString('en-us', { + return `${d.toLocaleTimeString(undefined, { + hour: 'numeric', + minute: '2-digit', + })} · ${d.toLocaleDateString('en-us', { year: 'numeric', month: 'short', day: 'numeric', - })} at ${d.toLocaleTimeString(undefined, { - hour: 'numeric', - minute: '2-digit', })}` } diff --git a/bskyembed/tailwind.config.cjs b/bskyembed/tailwind.config.cjs index adbeadd4c3..e58a4e6b41 100644 --- a/bskyembed/tailwind.config.cjs +++ b/bskyembed/tailwind.config.cjs @@ -5,10 +5,13 @@ module.exports = { theme: { extend: { colors: { - brand: 'rgb(10,122,255)', + brand: 'rgb(0,106,255)', + brandHover: 'rgb(245,249,255)', + brandHoverDark: 'rgb(17,24,34)', brandLighten: 'rgb(32,139,254)', - textLight: 'rgb(66,87,108)', - textDimmed: 'rgb(174,187,201)', + textLight: 'rgb(63,82,104)', + textDimmed: 'rgb(164,179,197)', + textNeutral: 'rgb(102,123,153)', dimmedBgLighten: 'rgb(30,41,54)', dimmedBg: 'rgb(22,30,39)', dimmedBgDarken: 'rgb(18,25,32)', diff --git a/bskyweb/cmd/bskyweb/renderer.go b/bskyweb/cmd/bskyweb/renderer.go index 6e02456f53..8f74de0c89 100644 --- a/bskyweb/cmd/bskyweb/renderer.go +++ b/bskyweb/cmd/bskyweb/renderer.go @@ -54,7 +54,7 @@ func NewRenderer(prefix string, fs *embed.FS, debug bool) *Renderer { } } -func (r Renderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error { +func (r Renderer) Render(w io.Writer, name string, data any, c echo.Context) error { var ctx pongo2.Context if data != nil { diff --git a/bskyweb/cmd/embedr/render.go b/bskyweb/cmd/embedr/render.go index cc8f0759a0..171f13384c 100644 --- a/bskyweb/cmd/embedr/render.go +++ b/bskyweb/cmd/embedr/render.go @@ -11,6 +11,6 @@ type Template struct { templates *template.Template } -func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error { +func (t *Template) Render(w io.Writer, name string, data any, c echo.Context) error { return t.templates.ExecuteTemplate(w, name, data) } diff --git a/bskyweb/cmd/embedr/server.go b/bskyweb/cmd/embedr/server.go index 8168197ca3..8315949ae3 100644 --- a/bskyweb/cmd/embedr/server.go +++ b/bskyweb/cmd/embedr/server.go @@ -180,9 +180,9 @@ func serve(cctx *cli.Context) error { // Create CORS middleware for oembed oembedCORS := middleware.CORSWithConfig(middleware.CORSConfig{ - AllowOrigins: []string{"*"}, - AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodOptions}, - AllowHeaders: []string{"Origin", "Content-Type", "Accept"}, + AllowOrigins: []string{"*"}, + AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodOptions}, + AllowHeaders: []string{"Origin", "Content-Type", "Accept"}, }) e.GET("/robots.txt", echo.WrapHandler(staticHandler)) @@ -271,7 +271,7 @@ func (srv *Server) errorHandler(err error, c echo.Context) { code = he.Code } c.Logger().Error(err) - data := map[string]interface{}{ + data := map[string]any{ "statusCode": code, } c.Render(code, "error.html", data) diff --git a/bskyweb/go.mod b/bskyweb/go.mod index 654c40a131..5274340c14 100644 --- a/bskyweb/go.mod +++ b/bskyweb/go.mod @@ -1,6 +1,6 @@ module github.com/bluesky-social/social-app/bskyweb -go 1.25 +go 1.26 require ( github.com/bluesky-social/indigo v0.0.0-20250729223159-573ae927246a diff --git a/package.json b/package.json index f92fe3fcfe..66f6ddd0a6 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "@braintree/sanitize-url": "^6.0.2", "@bsky.app/alf": "^0.1.7", "@bsky.app/expo-image-crop-tool": "^0.5.0", + "@bsky.app/expo-scroll-edge-effect": "^0.1.4", "@bsky.app/expo-translate-text": "^0.2.9", "@bsky.app/react-native-mmkv": "2.12.5", "@bsky.app/sift": "^0.3.2", @@ -156,6 +157,7 @@ "expo-device": "~8.0.10", "expo-file-system": "~19.0.21", "expo-font": "~14.0.11", + "expo-glass-effect": "55.0.8", "expo-haptics": "~15.0.8", "expo-image": "~3.0.11", "expo-image-manipulator": "~14.0.8", @@ -213,7 +215,7 @@ "react-native-drawer-layout": "^4.2.2", "react-native-edge-to-edge": "^1.6.0", "react-native-gesture-handler": "~2.28.0", - "react-native-keyboard-controller": "^1.21.0", + "react-native-keyboard-controller": "^1.21.5", "react-native-pager-view": "6.8.0", "react-native-progress": "bluesky-social/react-native-progress", "react-native-qrcode-styled": "^0.3.3", @@ -245,7 +247,6 @@ "@babel/runtime": "^7.26.0", "@crowdin/cli": "^4.14.1", "@eslint/js": "^9.39.2", - "@expo/config-plugins": "~54.0.4", "@lingui/babel-plugin-lingui-macro": "^5.9.2", "@lingui/cli": "^5.9.2", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.15", diff --git a/patches/expo-glass-effect+55.0.8.patch b/patches/expo-glass-effect+55.0.8.patch new file mode 100644 index 0000000000..6619c811c1 --- /dev/null +++ b/patches/expo-glass-effect+55.0.8.patch @@ -0,0 +1,60 @@ +diff --git a/node_modules/expo-glass-effect/ios/GlassContainer.swift b/node_modules/expo-glass-effect/ios/GlassContainer.swift +index 61fb67c..b2d111e 100644 +--- a/node_modules/expo-glass-effect/ios/GlassContainer.swift ++++ b/node_modules/expo-glass-effect/ios/GlassContainer.swift +@@ -1,6 +1,7 @@ + // Copyright 2022-present 650 Industries. All rights reserved. + + import ExpoModulesCore ++import React + + public final class GlassContainer: ExpoView { + private var containerEffect: Any? +@@ -46,11 +47,19 @@ public final class GlassContainer: ExpoView { + } + } + +- public override func mountChildComponentView(_ childComponentView: UIView, index: Int) { ++ // Paper: redirect children into the container effect's contentView ++ public override func didUpdateReactSubviews() { ++ for subview in self.reactSubviews() { ++ containerEffectView.contentView.addSubview(subview) ++ } ++ } ++ ++ // Fabric: redirect children into the container effect's contentView ++ @objc public func mountChildComponentView(_ childComponentView: UIView, index: Int) { + containerEffectView.contentView.insertSubview(childComponentView, at: index) + } + +- public override func unmountChildComponentView(_ childComponentView: UIView, index: Int) { ++ @objc public func unmountChildComponentView(_ childComponentView: UIView, index: Int) { + childComponentView.removeFromSuperview() + } + } +diff --git a/node_modules/expo-glass-effect/ios/GlassView.swift b/node_modules/expo-glass-effect/ios/GlassView.swift +index 35cd8f3..9587306 100644 +--- a/node_modules/expo-glass-effect/ios/GlassView.swift ++++ b/node_modules/expo-glass-effect/ios/GlassView.swift +@@ -271,11 +271,19 @@ public final class GlassView: ExpoView { + #endif + } + } +- public override func mountChildComponentView(_ childComponentView: UIView, index: Int) { ++ // Paper: redirect children into the glass effect's contentView ++ public override func didUpdateReactSubviews() { ++ for subview in self.reactSubviews() { ++ glassEffectView.contentView.addSubview(subview) ++ } ++ } ++ ++ // Fabric: redirect children into the glass effect's contentView ++ @objc public func mountChildComponentView(_ childComponentView: UIView, index: Int) { + glassEffectView.contentView.insertSubview(childComponentView, at: index) + } + +- public override func unmountChildComponentView(_ childComponentView: UIView, index: Int) { ++ @objc public func unmountChildComponentView(_ childComponentView: UIView, index: Int) { + childComponentView.removeFromSuperview() + } + } diff --git a/patches/expo-glass-effect+55.0.8.patch.md b/patches/expo-glass-effect+55.0.8.patch.md new file mode 100644 index 0000000000..ab668c78ff --- /dev/null +++ b/patches/expo-glass-effect+55.0.8.patch.md @@ -0,0 +1,3 @@ +# expo-glass-effect patch + +Patches in support for Expo SDK 54. Please delete when we update Expo diff --git a/plugins/notificationsExtension/withAppEntitlements.js b/plugins/notificationsExtension/withAppEntitlements.js index 4ce81ea611..d7dad99945 100644 --- a/plugins/notificationsExtension/withAppEntitlements.js +++ b/plugins/notificationsExtension/withAppEntitlements.js @@ -1,7 +1,6 @@ -const {withEntitlementsPlist} = require('@expo/config-plugins') +const {withEntitlementsPlist} = require('expo/config-plugins') const withAppEntitlements = config => { - // eslint-disable-next-line no-shadow return withEntitlementsPlist(config, async config => { config.modResults['com.apple.security.application-groups'] = [ `group.app.bsky`, diff --git a/plugins/notificationsExtension/withExtensionEntitlements.js b/plugins/notificationsExtension/withExtensionEntitlements.js index 0cc1c4ca8c..29614ab091 100644 --- a/plugins/notificationsExtension/withExtensionEntitlements.js +++ b/plugins/notificationsExtension/withExtensionEntitlements.js @@ -1,10 +1,9 @@ -const {withInfoPlist} = require('@expo/config-plugins') +const {withInfoPlist} = require('expo/config-plugins') const plist = require('@expo/plist') const path = require('path') const fs = require('fs') const withExtensionEntitlements = (config, {extensionName}) => { - // eslint-disable-next-line no-shadow return withInfoPlist(config, config => { const extensionEntitlementsPath = path.join( config.modRequest.platformProjectRoot, diff --git a/plugins/notificationsExtension/withExtensionInfoPlist.js b/plugins/notificationsExtension/withExtensionInfoPlist.js index b0c6cfa89a..8de8c9910f 100644 --- a/plugins/notificationsExtension/withExtensionInfoPlist.js +++ b/plugins/notificationsExtension/withExtensionInfoPlist.js @@ -1,10 +1,9 @@ -const {withInfoPlist} = require('@expo/config-plugins') +const {withInfoPlist} = require('expo/config-plugins') const plist = require('@expo/plist') const path = require('path') const fs = require('fs') const withExtensionInfoPlist = (config, {extensionName}) => { - // eslint-disable-next-line no-shadow return withInfoPlist(config, config => { const plistPath = path.join( config.modRequest.projectRoot, diff --git a/plugins/notificationsExtension/withExtensionViewController.js b/plugins/notificationsExtension/withExtensionViewController.js index cd29bea7da..07aedcf345 100644 --- a/plugins/notificationsExtension/withExtensionViewController.js +++ b/plugins/notificationsExtension/withExtensionViewController.js @@ -1,4 +1,4 @@ -const {withXcodeProject} = require('@expo/config-plugins') +const {withXcodeProject} = require('expo/config-plugins') const path = require('path') const fs = require('fs') @@ -6,7 +6,6 @@ const withExtensionViewController = ( config, {controllerName, extensionName}, ) => { - // eslint-disable-next-line no-shadow return withXcodeProject(config, config => { const controllerPath = path.join( config.modRequest.projectRoot, diff --git a/plugins/notificationsExtension/withNotificationsExtension.js b/plugins/notificationsExtension/withNotificationsExtension.js index 6a00cfd231..a6b4fa8e50 100644 --- a/plugins/notificationsExtension/withNotificationsExtension.js +++ b/plugins/notificationsExtension/withNotificationsExtension.js @@ -1,4 +1,4 @@ -const {withPlugins} = require('@expo/config-plugins') +const {withPlugins} = require('expo/config-plugins') const {withAppEntitlements} = require('./withAppEntitlements') const {withXcodeTarget} = require('./withXcodeTarget') const {withExtensionEntitlements} = require('./withExtensionEntitlements') diff --git a/plugins/notificationsExtension/withSounds.js b/plugins/notificationsExtension/withSounds.js index 652afd5458..a626c13e8e 100644 --- a/plugins/notificationsExtension/withSounds.js +++ b/plugins/notificationsExtension/withSounds.js @@ -1,9 +1,8 @@ -const {withXcodeProject} = require('@expo/config-plugins') +const {withXcodeProject} = require('expo/config-plugins') const path = require('path') const fs = require('fs') const withSounds = (config, {extensionName, soundFiles}) => { - // eslint-disable-next-line no-shadow return withXcodeProject(config, config => { for (const file of soundFiles) { const soundPath = path.join(config.modRequest.projectRoot, 'assets', file) diff --git a/plugins/notificationsExtension/withXcodeTarget.js b/plugins/notificationsExtension/withXcodeTarget.js index e9c7dae39a..3307875c61 100644 --- a/plugins/notificationsExtension/withXcodeTarget.js +++ b/plugins/notificationsExtension/withXcodeTarget.js @@ -1,12 +1,9 @@ -const {withXcodeProject, IOSConfig} = require('@expo/config-plugins') -const path = require('path') -const PBXFile = require('xcode/lib/pbxFile') +const {withXcodeProject} = require('expo/config-plugins') const withXcodeTarget = ( config, {extensionName, controllerName, soundFiles}, ) => { - // eslint-disable-next-line no-shadow return withXcodeProject(config, config => { let pbxProject = config.modResults diff --git a/plugins/shareExtension/withAppEntitlements.js b/plugins/shareExtension/withAppEntitlements.js index 4ce81ea611..d7dad99945 100644 --- a/plugins/shareExtension/withAppEntitlements.js +++ b/plugins/shareExtension/withAppEntitlements.js @@ -1,7 +1,6 @@ -const {withEntitlementsPlist} = require('@expo/config-plugins') +const {withEntitlementsPlist} = require('expo/config-plugins') const withAppEntitlements = config => { - // eslint-disable-next-line no-shadow return withEntitlementsPlist(config, async config => { config.modResults['com.apple.security.application-groups'] = [ `group.app.bsky`, diff --git a/plugins/shareExtension/withExtensionEntitlements.js b/plugins/shareExtension/withExtensionEntitlements.js index 7bee79d1a6..9123d22969 100644 --- a/plugins/shareExtension/withExtensionEntitlements.js +++ b/plugins/shareExtension/withExtensionEntitlements.js @@ -1,10 +1,9 @@ -const {withInfoPlist} = require('@expo/config-plugins') +const {withInfoPlist} = require('expo/config-plugins') const plist = require('@expo/plist') const path = require('path') const fs = require('fs') const withExtensionEntitlements = (config, {extensionName}) => { - // eslint-disable-next-line no-shadow return withInfoPlist(config, config => { const extensionEntitlementsPath = path.join( config.modRequest.platformProjectRoot, diff --git a/plugins/shareExtension/withExtensionInfoPlist.js b/plugins/shareExtension/withExtensionInfoPlist.js index 9afc4d5f92..4e1c299b32 100644 --- a/plugins/shareExtension/withExtensionInfoPlist.js +++ b/plugins/shareExtension/withExtensionInfoPlist.js @@ -1,10 +1,9 @@ -const {withInfoPlist} = require('@expo/config-plugins') +const {withInfoPlist} = require('expo/config-plugins') const plist = require('@expo/plist') const path = require('path') const fs = require('fs') const withExtensionInfoPlist = (config, {extensionName}) => { - // eslint-disable-next-line no-shadow return withInfoPlist(config, config => { const plistPath = path.join( config.modRequest.projectRoot, diff --git a/plugins/shareExtension/withExtensionViewController.js b/plugins/shareExtension/withExtensionViewController.js index cd29bea7da..07aedcf345 100644 --- a/plugins/shareExtension/withExtensionViewController.js +++ b/plugins/shareExtension/withExtensionViewController.js @@ -1,4 +1,4 @@ -const {withXcodeProject} = require('@expo/config-plugins') +const {withXcodeProject} = require('expo/config-plugins') const path = require('path') const fs = require('fs') @@ -6,7 +6,6 @@ const withExtensionViewController = ( config, {controllerName, extensionName}, ) => { - // eslint-disable-next-line no-shadow return withXcodeProject(config, config => { const controllerPath = path.join( config.modRequest.projectRoot, diff --git a/plugins/shareExtension/withIntentFilters.js b/plugins/shareExtension/withIntentFilters.js index 16494893bb..9eda8ce842 100644 --- a/plugins/shareExtension/withIntentFilters.js +++ b/plugins/shareExtension/withIntentFilters.js @@ -1,7 +1,6 @@ -const {withAndroidManifest} = require('@expo/config-plugins') +const {withAndroidManifest} = require('expo/config-plugins') const withIntentFilters = config => { - // eslint-disable-next-line no-shadow return withAndroidManifest(config, config => { const intents = [ { diff --git a/plugins/shareExtension/withShareExtensions.js b/plugins/shareExtension/withShareExtensions.js index 55a26c75eb..1662f587fc 100644 --- a/plugins/shareExtension/withShareExtensions.js +++ b/plugins/shareExtension/withShareExtensions.js @@ -1,4 +1,4 @@ -const {withPlugins} = require('@expo/config-plugins') +const {withPlugins} = require('expo/config-plugins') const {withAppEntitlements} = require('./withAppEntitlements') const {withXcodeTarget} = require('./withXcodeTarget') const {withExtensionEntitlements} = require('./withExtensionEntitlements') diff --git a/plugins/shareExtension/withXcodeTarget.js b/plugins/shareExtension/withXcodeTarget.js index ce70b392d4..5a11566861 100644 --- a/plugins/shareExtension/withXcodeTarget.js +++ b/plugins/shareExtension/withXcodeTarget.js @@ -1,7 +1,6 @@ -const {withXcodeProject} = require('@expo/config-plugins') +const {withXcodeProject} = require('expo/config-plugins') const withXcodeTarget = (config, {extensionName, controllerName}) => { - // eslint-disable-next-line no-shadow return withXcodeProject(config, config => { const pbxProject = config.modResults diff --git a/plugins/starterPackAppClipExtension/withAppEntitlements.js b/plugins/starterPackAppClipExtension/withAppEntitlements.js index 1bffd82080..c0361ab8d6 100644 --- a/plugins/starterPackAppClipExtension/withAppEntitlements.js +++ b/plugins/starterPackAppClipExtension/withAppEntitlements.js @@ -1,7 +1,6 @@ -const {withEntitlementsPlist} = require('@expo/config-plugins') +const {withEntitlementsPlist} = require('expo/config-plugins') const withAppEntitlements = config => { - // eslint-disable-next-line no-shadow return withEntitlementsPlist(config, async config => { config.modResults['com.apple.security.application-groups'] = [ `group.app.bsky`, diff --git a/plugins/starterPackAppClipExtension/withClipEntitlements.js b/plugins/starterPackAppClipExtension/withClipEntitlements.js index 77636b5c94..3ba1d944c4 100644 --- a/plugins/starterPackAppClipExtension/withClipEntitlements.js +++ b/plugins/starterPackAppClipExtension/withClipEntitlements.js @@ -1,10 +1,9 @@ -const {withInfoPlist} = require('@expo/config-plugins') +const {withInfoPlist} = require('expo/config-plugins') const plist = require('@expo/plist') const path = require('path') const fs = require('fs') const withClipEntitlements = (config, {targetName}) => { - // eslint-disable-next-line no-shadow return withInfoPlist(config, config => { const entitlementsPath = path.join( config.modRequest.platformProjectRoot, diff --git a/plugins/starterPackAppClipExtension/withClipInfoPlist.js b/plugins/starterPackAppClipExtension/withClipInfoPlist.js index 4f104a6543..e5034cef31 100644 --- a/plugins/starterPackAppClipExtension/withClipInfoPlist.js +++ b/plugins/starterPackAppClipExtension/withClipInfoPlist.js @@ -1,10 +1,9 @@ -const {withInfoPlist} = require('@expo/config-plugins') +const {withInfoPlist} = require('expo/config-plugins') const plist = require('@expo/plist') const path = require('path') const fs = require('fs') const withClipInfoPlist = (config, {targetName}) => { - // eslint-disable-next-line no-shadow return withInfoPlist(config, config => { const targetPath = path.join( config.modRequest.platformProjectRoot, diff --git a/plugins/starterPackAppClipExtension/withFiles.js b/plugins/starterPackAppClipExtension/withFiles.js index ad99f5ae41..c010a52f78 100644 --- a/plugins/starterPackAppClipExtension/withFiles.js +++ b/plugins/starterPackAppClipExtension/withFiles.js @@ -1,11 +1,10 @@ -const {withXcodeProject} = require('@expo/config-plugins') +const {withXcodeProject} = require('expo/config-plugins') const path = require('path') const fs = require('fs') const FILES = ['AppDelegate.swift', 'ViewController.swift'] const withFiles = (config, {targetName}) => { - // eslint-disable-next-line no-shadow return withXcodeProject(config, config => { const basePath = path.join( config.modRequest.projectRoot, diff --git a/plugins/starterPackAppClipExtension/withStarterPackAppClip.js b/plugins/starterPackAppClipExtension/withStarterPackAppClip.js index 1e3f0b7029..a194ca34be 100644 --- a/plugins/starterPackAppClipExtension/withStarterPackAppClip.js +++ b/plugins/starterPackAppClipExtension/withStarterPackAppClip.js @@ -1,4 +1,4 @@ -const {withPlugins} = require('@expo/config-plugins') +const {withPlugins} = require('expo/config-plugins') const {withAppEntitlements} = require('./withAppEntitlements') const {withClipEntitlements} = require('./withClipEntitlements') const {withClipInfoPlist} = require('./withClipInfoPlist') diff --git a/plugins/starterPackAppClipExtension/withXcodeTarget.js b/plugins/starterPackAppClipExtension/withXcodeTarget.js index c14d27291a..0f96fa33f7 100644 --- a/plugins/starterPackAppClipExtension/withXcodeTarget.js +++ b/plugins/starterPackAppClipExtension/withXcodeTarget.js @@ -1,9 +1,8 @@ -const {withXcodeProject} = require('@expo/config-plugins') +const {withXcodeProject} = require('expo/config-plugins') const BUILD_PHASE_FILES = ['AppDelegate.swift', 'ViewController.swift'] const withXcodeTarget = (config, {targetName}) => { - // eslint-disable-next-line no-shadow return withXcodeProject(config, config => { const pbxProject = config.modResults diff --git a/plugins/withAndroidManifestIntentQueriesPlugin.js b/plugins/withAndroidManifestIntentQueriesPlugin.js index 04237b059e..4c53a8a018 100644 --- a/plugins/withAndroidManifestIntentQueriesPlugin.js +++ b/plugins/withAndroidManifestIntentQueriesPlugin.js @@ -1,7 +1,6 @@ -const {withAndroidManifest} = require('@expo/config-plugins') +const {withAndroidManifest} = require('expo/config-plugins') const withProcessTextQuery = config => - // eslint-disable-next-line no-shadow withAndroidManifest(config, config => { const manifest = config.modResults.manifest diff --git a/plugins/withAndroidNoJitpackPlugin.js b/plugins/withAndroidNoJitpackPlugin.js index 959eb10282..94755897db 100644 --- a/plugins/withAndroidNoJitpackPlugin.js +++ b/plugins/withAndroidNoJitpackPlugin.js @@ -1,4 +1,4 @@ -const {withProjectBuildGradle} = require('@expo/config-plugins') +const {withProjectBuildGradle} = require('expo/config-plugins') const jitpackRepository = "maven { url 'https://www.jitpack.io' }" diff --git a/plugins/withAndroidStylesAccentColorPlugin.js b/plugins/withAndroidStylesAccentColorPlugin.js index 51dd44f358..3f97fac298 100644 --- a/plugins/withAndroidStylesAccentColorPlugin.js +++ b/plugins/withAndroidStylesAccentColorPlugin.js @@ -3,7 +3,7 @@ * This way we get a sane default color for spinners, text inputs, etc. */ -const {withAndroidStyles, AndroidConfig} = require('@expo/config-plugins') +const {withAndroidStyles, AndroidConfig} = require('expo/config-plugins') module.exports = function withAndroidStylesAccentColorPlugin(appConfig) { return withAndroidStyles(appConfig, function (decoratedAppConfig) { diff --git a/plugins/withAppDelegateReferrer.js b/plugins/withAppDelegateReferrer.js index 69feec57d7..ca14875e17 100644 --- a/plugins/withAppDelegateReferrer.js +++ b/plugins/withAppDelegateReferrer.js @@ -1,11 +1,10 @@ -const {withAppDelegate} = require('@expo/config-plugins') -const {mergeContents} = require('@expo/config-plugins/build/utils/generateCode') +const {withAppDelegate, CodeGenerator} = require('expo/config-plugins') module.exports = config => withAppDelegate(config, config => { let contents = config.modResults.contents - contents = mergeContents({ + contents = CodeGenerator.mergeContents({ src: contents, anchor: '// Linking API', newSrc: ` @@ -22,7 +21,7 @@ module.exports = config => comment: '//', }).contents - contents = mergeContents({ + contents = CodeGenerator.mergeContents({ src: contents, anchor: '// Universal Links', newSrc: ` diff --git a/src/App.native.tsx b/src/App.native.tsx index af93a75c84..3a0a5a6a57 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -235,7 +235,7 @@ function App() { - + diff --git a/src/ageAssurance/data.tsx b/src/ageAssurance/data.tsx index cfea297f67..2839d2b488 100644 --- a/src/ageAssurance/data.tsx +++ b/src/ageAssurance/data.tsx @@ -183,7 +183,7 @@ export async function getServerState({agent}: {agent: AtpAgent}) { const geolocation = device.get(['mergedGeolocation']) if (!geolocation || !geolocation.countryCode) { logger.error(`getServerState: missing geolocation countryCode`) - return + return null } const {data} = await agent.app.bsky.ageassurance.getState({ countryCode: geolocation.countryCode, @@ -225,7 +225,9 @@ export async function prefetchServerState({agent}: {agent: AtpAgent}) { try { logger.debug(`prefetchServerState: resolving...`) const res = await networkRetry(3, () => getServerState({agent})) - qc.setQueryData(qk, res) + if (res) { + qc.setQueryData(qk, res) + } } catch (err) { const e = err as Error logger.warn(`prefetchServerState: failed`, { @@ -238,10 +240,12 @@ export async function refetchServerState({agent}: {agent: AtpAgent}) { if (!did) return logger.debug(`refetchServerState: fetching...`) const res = await networkRetry(3, () => getServerState({agent})) - qc.setQueryData( - createServerStateQueryKey({did}), - res, - ) + if (res) { + qc.setQueryData( + createServerStateQueryKey({did}), + res, + ) + } return res } export function usePatchServerState() { diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts index ffac94a5da..3f4ba19600 100644 --- a/src/alf/atoms.ts +++ b/src/alf/atoms.ts @@ -20,7 +20,7 @@ export const atoms = { */ util_screen_outer: [ web({ - minHeight: '100vh', + minHeight: '100dvh', }), native({ height: '100%', diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 058e0ea750..b7a67f3033 100644 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -338,6 +338,8 @@ export function Composer({ web({ caretColor: textStyle.color ?? 'black', overscrollBehavior: 'none', + scrollbarWidth: 'thin', + scrollbarColor: `${t.palette.contrast_200} transparent`, }), ]} {...rest} diff --git a/src/components/GlassView.tsx b/src/components/GlassView.tsx new file mode 100644 index 0000000000..a695a0678a --- /dev/null +++ b/src/components/GlassView.tsx @@ -0,0 +1,35 @@ +import {type StyleProp, View, type ViewStyle} from 'react-native' +import { + GlassView as ExpoGlassView, + type GlassViewProps as ExpoGlassViewProps, + isGlassEffectAPIAvailable, + isLiquidGlassAvailable, +} from 'expo-glass-effect' + +import {useTheme} from '#/alf' + +export const IS_GLASS_AVAILABLE = + isLiquidGlassAvailable() && isGlassEffectAPIAvailable() + +/** + * Liquid Glass View that uses `expo-glass-effect` + * + * If unavailable, falls back to a regular `View`. Use `fallbackStyle` to customize the fallback appearance. + */ +export const GlassView = IS_GLASS_AVAILABLE ? InnerGlassView : FallbackView + +export type GlassViewProps = ExpoGlassViewProps & { + fallbackStyle?: StyleProp +} + +function InnerGlassView({ + fallbackStyle: _fallbackStyle, + ...props +}: GlassViewProps) { + const t = useTheme() + return +} + +function FallbackView({fallbackStyle, style, ...props}: GlassViewProps) { + return +} diff --git a/src/components/PostControls/ShareMenu/RecentChats.tsx b/src/components/PostControls/ShareMenu/RecentChats.tsx index 178f8c0baa..24fcc87b3a 100644 --- a/src/components/PostControls/ShareMenu/RecentChats.tsx +++ b/src/components/PostControls/ShareMenu/RecentChats.tsx @@ -22,7 +22,13 @@ import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import type * as bsky from '#/types/bsky' -export function RecentChats({postUri}: {postUri: string}) { +export function RecentChats({ + postUri, + onBeforePress, +}: { + postUri: string + onBeforePress?: () => void +}) { const ax = useAnalytics() const control = useDialogContext() const {currentAccount} = useSession() @@ -32,6 +38,7 @@ export function RecentChats({postUri}: {postUri: string}) { const navigation = useNavigation() const onSelectChat = (convoId: string) => { + onBeforePress?.() control.close(() => { ax.metric('share:press:recentDm', {}) navigation.navigate('MessagesConversation', { diff --git a/src/components/PostControls/ShareMenu/ShareMenuItems.tsx b/src/components/PostControls/ShareMenu/ShareMenuItems.tsx index 8cdfe1a93d..627034f5d6 100644 --- a/src/components/PostControls/ShareMenu/ShareMenuItems.tsx +++ b/src/components/PostControls/ShareMenu/ShareMenuItems.tsx @@ -5,12 +5,14 @@ import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' +import {useQueryClient} from '@tanstack/react-query' import {makeProfileLink} from '#/lib/routes/links' import {type NavigationProp} from '#/lib/routes/types' import {shareText, shareUrl} from '#/lib/sharing' import {toShareUrl} from '#/lib/strings/url-helpers' import {useProfileShadow} from '#/state/cache/profile-shadow' +import {precachePost} from '#/state/queries/post' import {useSession} from '#/state/session' import {atoms as a} from '#/alf' import {Admonition} from '#/components/Admonition' @@ -40,6 +42,7 @@ let ShareMenuItems = ({ const sendViaChatControl = useDialogControl() const [devModeEnabled] = useDevMode() const aa = useAgeAssurance() + const queryClient = useQueryClient() const postUri = post.uri const postAuthor = useProfileShadow(post.author) @@ -77,7 +80,12 @@ let ShareMenuItems = ({ onShareProp() } + const onBeforeShareViaChat = () => { + precachePost(queryClient, postUri, post) + } + const onSelectChatToShareTo = (conversation: string) => { + onBeforeShareViaChat() navigation.navigate('MessagesConversation', { conversation, embed: postUri, @@ -98,7 +106,10 @@ let ShareMenuItems = ({ {hasSession && aa.state.access === aa.Access.Full && ( - + ( - + {formatPostStatCount(count)} )} diff --git a/src/components/dms/ChatEmptyPill.tsx b/src/components/dms/ChatEmptyPill.tsx index dba7dbdbe3..17dcf8c561 100644 --- a/src/components/dms/ChatEmptyPill.tsx +++ b/src/components/dms/ChatEmptyPill.tsx @@ -91,7 +91,8 @@ export function ChatEmptyPill() { onPressOut={onPressOut}> + selectable={false} + emoji> {prompts[promptIndex]} diff --git a/src/components/icons/PaperPlane.tsx b/src/components/icons/PaperPlane.tsx index eef38638cb..504c15f284 100644 --- a/src/components/icons/PaperPlane.tsx +++ b/src/components/icons/PaperPlane.tsx @@ -3,3 +3,8 @@ import {createSinglePathSVG} from './TEMPLATE' export const PaperPlane_Stroke2_Corner0_Rounded = createSinglePathSVG({ path: 'M3.374 3.22a1 1 0 0 1 1.073-.114l16 8a1 1 0 0 1 0 1.788l-16 8a1 1 0 0 1-1.417-1.136L4.97 12 3.03 4.243a1 1 0 0 1 .344-1.023ZM6.781 13l-1.284 5.133L17.764 12 5.497 5.867 6.781 11H9a1 1 0 1 1 0 2H6.78Z', }) + +export const PaperPlaneVertical_Filled_Stroke2_Corner1_Rounded = + createSinglePathSVG({ + path: 'M10.655 3.718c.55-1.116 2.14-1.116 2.69 0l7.548 15.317c.578 1.172-.515 2.471-1.768 2.103L13 19.336V15a1 1 0 0 0-2 0v4.336l-6.124 1.802c-1.254.369-2.346-.93-1.769-2.103l7.548-15.317Z', + }) diff --git a/src/features/liveNow/components/LiveStatusDialog.tsx b/src/features/liveNow/components/LiveStatusDialog.tsx index e351a406d0..545e8ab943 100644 --- a/src/features/liveNow/components/LiveStatusDialog.tsx +++ b/src/features/liveNow/components/LiveStatusDialog.tsx @@ -117,13 +117,14 @@ export function LiveStatus({ const reportDialogControl = useGlobalReportDialogControl() const dialogContext = Dialog.useDialogContext() const moderation = useMemo(() => { - return moderateStatus(profile, moderationOpts!) + if (!moderationOpts) return undefined + return moderateStatus(profile, moderationOpts) }, [profile, moderationOpts]) return ( <> {embed.external.thumb && ( - + @@ -279,7 +280,6 @@ function ModeratedImage() { return ( { if (!actor || !('status' in actor && actor.status)) return undefined - return moderateStatus(actor, moderationOpts!) + if (!moderationOpts) return undefined + return moderateStatus(actor, moderationOpts) }, [actor, moderationOpts]) return useMemo(() => { diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index b8ead93525..299ceb0c0c 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -694,8 +694,8 @@ msgstr "" #: src/Navigation.tsx:544 #: src/screens/Settings/AboutSettings.tsx:74 -#: src/screens/Settings/Settings.tsx:259 -#: src/screens/Settings/Settings.tsx:262 +#: src/screens/Settings/Settings.tsx:255 +#: src/screens/Settings/Settings.tsx:258 msgid "About" msgstr "" @@ -722,8 +722,8 @@ msgid "Accept this language suggestion" msgstr "" #: src/screens/Settings/AccessibilitySettings.tsx:45 -#: src/screens/Settings/Settings.tsx:235 -#: src/screens/Settings/Settings.tsx:238 +#: src/screens/Settings/Settings.tsx:233 +#: src/screens/Settings/Settings.tsx:236 msgid "Accessibility" msgstr "" @@ -734,8 +734,8 @@ msgstr "" #: src/Navigation.tsx:403 #: src/screens/Login/LoginForm.tsx:192 #: src/screens/Settings/AccountSettings.tsx:56 -#: src/screens/Settings/Settings.tsx:177 -#: src/screens/Settings/Settings.tsx:180 +#: src/screens/Settings/Settings.tsx:173 +#: src/screens/Settings/Settings.tsx:176 msgid "Account" msgstr "" @@ -774,7 +774,7 @@ msgstr "" msgid "Account Muted by List" msgstr "" -#: src/screens/Settings/Settings.tsx:641 +#: src/screens/Settings/Settings.tsx:637 msgid "Account options" msgstr "" @@ -782,7 +782,7 @@ msgstr "" msgid "Account provider" msgstr "" -#: src/screens/Settings/Settings.tsx:677 +#: src/screens/Settings/Settings.tsx:671 msgid "Account removed from quick access" msgstr "" @@ -868,8 +868,8 @@ msgstr "" msgid "Add alt text (optional)" msgstr "" -#: src/screens/Settings/Settings.tsx:579 -#: src/screens/Settings/Settings.tsx:582 +#: src/screens/Settings/Settings.tsx:575 +#: src/screens/Settings/Settings.tsx:578 #: src/view/shell/desktop/LeftNav.tsx:264 #: src/view/shell/desktop/LeftNav.tsx:268 msgid "Add another account" @@ -1437,8 +1437,8 @@ msgstr "" #: src/Navigation.tsx:395 #: src/screens/Settings/AppearanceSettings.tsx:73 -#: src/screens/Settings/Settings.tsx:227 -#: src/screens/Settings/Settings.tsx:230 +#: src/screens/Settings/Settings.tsx:225 +#: src/screens/Settings/Settings.tsx:228 msgid "Appearance" msgstr "" @@ -1447,8 +1447,8 @@ msgstr "" msgid "Apply default recommended feeds" msgstr "" -#: src/screens/Settings/Settings.tsx:510 -#: src/screens/Settings/Settings.tsx:512 +#: src/screens/Settings/Settings.tsx:506 +#: src/screens/Settings/Settings.tsx:508 msgid "Apply Pull Request" msgstr "" @@ -1619,7 +1619,7 @@ msgstr "" #: src/components/dms/dialogs/NewChatDialog.tsx:66 #: src/components/dms/MessageProfileButton.tsx:60 #: src/screens/Messages/ChatList.tsx:376 -#: src/screens/Messages/Conversation.tsx:226 +#: src/screens/Messages/Conversation.tsx:230 msgid "Before you can message another user, you must first verify your email." msgstr "" @@ -1960,7 +1960,7 @@ msgstr "" #: src/screens/Settings/components/ChangeHandleDialog.tsx:87 #: src/screens/Settings/components/ChangePasswordDialog.tsx:248 #: src/screens/Settings/components/ChangePasswordDialog.tsx:254 -#: src/screens/Settings/Settings.tsx:304 +#: src/screens/Settings/Settings.tsx:300 #: src/screens/Takendown.tsx:102 #: src/screens/Takendown.tsx:105 #: src/view/com/composer/Composer.tsx:1547 @@ -2205,11 +2205,11 @@ msgstr "" msgid "Choose your username" msgstr "" -#: src/screens/Settings/Settings.tsx:502 +#: src/screens/Settings/Settings.tsx:498 msgid "Clear all storage data" msgstr "" -#: src/screens/Settings/Settings.tsx:504 +#: src/screens/Settings/Settings.tsx:500 msgid "Clear all storage data (restart after this)" msgstr "" @@ -2536,8 +2536,8 @@ msgstr "" msgid "Content & Media" msgstr "" -#: src/screens/Settings/Settings.tsx:207 -#: src/screens/Settings/Settings.tsx:210 +#: src/screens/Settings/Settings.tsx:205 +#: src/screens/Settings/Settings.tsx:208 msgid "Content and media" msgstr "" @@ -2619,7 +2619,7 @@ msgstr "Continue to group name" msgid "Continue to next step" msgstr "" -#: src/screens/Messages/Conversation.tsx:57 +#: src/screens/Messages/Conversation.tsx:60 msgid "Conversation" msgstr "" @@ -2640,7 +2640,7 @@ msgstr "" #: src/components/dms/MessageContextMenu.tsx:61 #: src/components/PostControls/DiscoverDebug.tsx:36 #: src/components/PostControls/PostMenu/PostMenuItems.tsx:272 -#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:74 +#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:77 #: src/lib/sharing.ts:24 #: src/lib/sharing.ts:42 msgid "Copied to clipboard" @@ -2702,8 +2702,8 @@ msgstr "" msgid "Copy link to list" msgstr "" -#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:131 -#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:134 +#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:142 +#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:145 #: src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx:88 #: src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx:91 msgid "Copy link to post" @@ -2990,7 +2990,7 @@ msgstr "" msgid "Deactivate account" msgstr "" -#: src/screens/Settings/Settings.tsx:467 +#: src/screens/Settings/Settings.tsx:463 msgid "Debug Moderation" msgstr "" @@ -3044,7 +3044,7 @@ msgstr "" msgid "Delete chat" msgstr "" -#: src/screens/Settings/Settings.tsx:474 +#: src/screens/Settings/Settings.tsx:470 msgid "Delete chat declaration record" msgstr "" @@ -3157,8 +3157,8 @@ msgctxt "toast" msgid "Developer mode enabled" msgstr "" -#: src/screens/Settings/Settings.tsx:286 -#: src/screens/Settings/Settings.tsx:289 +#: src/screens/Settings/Settings.tsx:282 +#: src/screens/Settings/Settings.tsx:285 msgid "Developer options" msgstr "" @@ -4329,8 +4329,8 @@ msgstr "" msgid "Find Friends" msgstr "" -#: src/screens/Settings/Settings.tsx:218 -#: src/screens/Settings/Settings.tsx:221 +#: src/screens/Settings/Settings.tsx:216 +#: src/screens/Settings/Settings.tsx:219 msgid "Find friends from contacts" msgstr "" @@ -4938,8 +4938,8 @@ msgstr "" msgid "Held by Bluesky for 7 days to prevent abuse, then deleted" msgstr "" +#: src/screens/Settings/Settings.tsx:247 #: src/screens/Settings/Settings.tsx:251 -#: src/screens/Settings/Settings.tsx:255 #: src/view/shell/desktop/RightNav.tsx:129 #: src/view/shell/desktop/RightNav.tsx:132 #: src/view/shell/Drawer.tsx:369 @@ -5554,8 +5554,8 @@ msgid "Language Settings" msgstr "" #: src/screens/Settings/LanguageSettings.tsx:81 -#: src/screens/Settings/Settings.tsx:243 -#: src/screens/Settings/Settings.tsx:246 +#: src/screens/Settings/Settings.tsx:239 +#: src/screens/Settings/Settings.tsx:242 msgid "Languages" msgstr "" @@ -5926,7 +5926,7 @@ msgstr "" msgid "Live events appear occasionally when something exciting is happening. If you'd like, you can hide this particular event, or all events for this placement in your app interface." msgstr "" -#: src/features/liveNow/components/LiveStatusDialog.tsx:243 +#: src/features/liveNow/components/LiveStatusDialog.tsx:244 msgid "Live feature is in beta" msgstr "" @@ -6094,6 +6094,11 @@ msgstr "" msgid "Menu" msgstr "" +#: src/screens/Messages/components/MessageComposer.tsx:198 +#: src/screens/Messages/components/MessageInput.tsx:174 +msgid "Message" +msgstr "Message" + #. placeholder {0}: profile.handle #: src/components/dms/MessageProfileButton.tsx:99 msgid "Message {0}" @@ -6119,17 +6124,20 @@ msgstr "" msgid "Message from server: {0}" msgstr "" -#: src/screens/Messages/components/MessageComposer.tsx:166 -#: src/screens/Messages/components/MessageInput.tsx:154 +#: src/screens/Messages/components/MessageComposer.tsx:197 +#: src/screens/Messages/components/MessageInput.tsx:172 msgid "Message input field" msgstr "" -#: src/screens/Messages/components/MessageComposer.tsx:69 -#: src/screens/Messages/components/MessageInput.tsx:79 +#: src/screens/Messages/components/MessageInput.tsx:85 #: src/screens/Messages/components/MessageInput.web.tsx:60 msgid "Message is too long" msgstr "" +#: src/screens/Messages/components/MessageComposer.tsx:98 +msgid "Message is too long ({graphemeCount}/{MAX_DM_GRAPHEME_LENGTH})" +msgstr "Message is too long ({graphemeCount}/{MAX_DM_GRAPHEME_LENGTH})" + #: src/components/dms/MessageContextMenu.tsx:129 msgid "Message options" msgstr "" @@ -6161,11 +6169,14 @@ msgstr "" #: src/Navigation.tsx:181 #: src/screens/Moderation/index.tsx:102 -#: src/screens/Settings/Settings.tsx:191 -#: src/screens/Settings/Settings.tsx:194 msgid "Moderation" msgstr "" +#: src/screens/Settings/Settings.tsx:189 +#: src/screens/Settings/Settings.tsx:192 +msgid "Moderation and content filters" +msgstr "Moderation and content filters" + #: src/components/moderation/ModerationDetailsDialog.tsx:141 msgid "Moderation details" msgstr "" @@ -6835,8 +6846,8 @@ msgstr "" #: src/screens/Settings/NotificationSettings/ReplyNotificationSettings.tsx:30 #: src/screens/Settings/NotificationSettings/RepostNotificationSettings.tsx:30 #: src/screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings.tsx:30 -#: src/screens/Settings/Settings.tsx:199 -#: src/screens/Settings/Settings.tsx:202 +#: src/screens/Settings/Settings.tsx:197 +#: src/screens/Settings/Settings.tsx:200 #: src/view/screens/Notifications.tsx:130 #: src/view/shell/bottom-bar/BottomBar.tsx:255 #: src/view/shell/desktop/LeftNav.tsx:710 @@ -6908,7 +6919,7 @@ msgstr "On" msgid "on<0><1/><2><3/>" msgstr "" -#: src/screens/Settings/Settings.tsx:400 +#: src/screens/Settings/Settings.tsx:396 msgid "Onboarding reset" msgstr "" @@ -6989,7 +7000,7 @@ msgstr "" msgid "Open drawer menu" msgstr "" -#: src/screens/Messages/components/MessageComposer.tsx:143 +#: src/screens/Messages/components/MessageComposer.tsx:177 #: src/screens/Messages/components/MessageInput.web.tsx:180 #: src/view/com/composer/Composer.tsx:1982 msgid "Open emoji picker" @@ -7020,7 +7031,7 @@ msgstr "" msgid "Open message options" msgstr "" -#: src/screens/Settings/Settings.tsx:465 +#: src/screens/Settings/Settings.tsx:461 msgid "Open moderation debug page" msgstr "" @@ -7036,8 +7047,8 @@ msgstr "" msgid "Open post options menu" msgstr "" -#: src/features/liveNow/components/LiveStatusDialog.tsx:217 -#: src/features/liveNow/components/LiveStatusDialog.tsx:227 +#: src/features/liveNow/components/LiveStatusDialog.tsx:218 +#: src/features/liveNow/components/LiveStatusDialog.tsx:228 msgid "Open profile" msgstr "" @@ -7054,12 +7065,12 @@ msgstr "" msgid "Open starter pack menu" msgstr "" -#: src/screens/Settings/Settings.tsx:458 -#: src/screens/Settings/Settings.tsx:472 +#: src/screens/Settings/Settings.tsx:454 +#: src/screens/Settings/Settings.tsx:468 msgid "Open storybook page" msgstr "" -#: src/screens/Settings/Settings.tsx:451 +#: src/screens/Settings/Settings.tsx:447 msgid "Open system log" msgstr "" @@ -7127,7 +7138,7 @@ msgstr "" msgid "Opens GIF select dialog" msgstr "" -#: src/screens/Settings/Settings.tsx:252 +#: src/screens/Settings/Settings.tsx:248 msgid "Opens helpdesk in browser" msgstr "" @@ -7762,8 +7773,8 @@ msgstr "" msgid "Privacy" msgstr "" -#: src/screens/Settings/Settings.tsx:185 -#: src/screens/Settings/Settings.tsx:188 +#: src/screens/Settings/Settings.tsx:181 +#: src/screens/Settings/Settings.tsx:184 msgid "Privacy and security" msgstr "" @@ -8055,7 +8066,7 @@ msgstr "" #: src/components/StarterPack/Wizard/WizardListCard.tsx:113 #: src/screens/Bookmarks/index.tsx:268 #: src/screens/Moderation/index.tsx:486 -#: src/screens/Settings/Settings.tsx:679 +#: src/screens/Settings/Settings.tsx:673 #: src/view/com/modals/UserAddRemoveLists.tsx:236 #: src/view/com/posts/PostFeedErrorMessage.tsx:221 msgid "Remove" @@ -8073,8 +8084,8 @@ msgstr "" msgid "Remove {historyItem}" msgstr "" -#: src/screens/Settings/Settings.tsx:658 -#: src/screens/Settings/Settings.tsx:661 +#: src/screens/Settings/Settings.tsx:654 +#: src/screens/Settings/Settings.tsx:657 msgid "Remove account" msgstr "" @@ -8123,7 +8134,7 @@ msgstr "" msgid "Remove from my feeds" msgstr "" -#: src/screens/Settings/Settings.tsx:671 +#: src/screens/Settings/Settings.tsx:667 msgid "Remove from quick access?" msgstr "" @@ -8331,7 +8342,7 @@ msgstr "" #: src/components/dms/MessageContextMenu.tsx:172 #: src/components/dms/MessagesListBlockedFooter.tsx:86 #: src/components/dms/MessagesListBlockedFooter.tsx:93 -#: src/features/liveNow/components/LiveStatusDialog.tsx:265 +#: src/features/liveNow/components/LiveStatusDialog.tsx:266 msgid "Report" msgstr "" @@ -8395,7 +8406,7 @@ msgid "Report this list" msgstr "" #: src/components/moderation/ReportDialog/copy.ts:19 -#: src/features/liveNow/components/LiveStatusDialog.tsx:248 +#: src/features/liveNow/components/LiveStatusDialog.tsx:249 msgid "Report this livestream" msgstr "" @@ -8525,8 +8536,8 @@ msgstr "" msgid "Resend Verification Email" msgstr "" -#: src/screens/Settings/Settings.tsx:494 -#: src/screens/Settings/Settings.tsx:496 +#: src/screens/Settings/Settings.tsx:490 +#: src/screens/Settings/Settings.tsx:492 msgid "Reset activity subscription nudge" msgstr "" @@ -8534,8 +8545,8 @@ msgstr "" msgid "Reset code" msgstr "" -#: src/screens/Settings/Settings.tsx:479 -#: src/screens/Settings/Settings.tsx:481 +#: src/screens/Settings/Settings.tsx:475 +#: src/screens/Settings/Settings.tsx:477 msgid "Reset onboarding state" msgstr "" @@ -9093,13 +9104,13 @@ msgstr "" msgid "Send feedback" msgstr "" -#: src/screens/Messages/components/MessageComposer.tsx:216 -#: src/screens/Messages/components/MessageInput.tsx:194 +#: src/screens/Messages/components/MessageComposer.tsx:264 +#: src/screens/Messages/components/MessageInput.tsx:228 #: src/screens/Messages/components/MessageInput.web.tsx:234 msgid "Send message" msgstr "" -#: src/components/PostControls/ShareMenu/RecentChats.tsx:121 +#: src/components/PostControls/ShareMenu/RecentChats.tsx:128 msgid "Send post to {name}" msgstr "" @@ -9121,8 +9132,8 @@ msgstr "" msgid "Send verification email" msgstr "" -#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:105 -#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:111 +#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:116 +#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:122 #: src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx:105 #: src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx:111 msgid "Send via direct message" @@ -9166,7 +9177,7 @@ msgid "Sets email for password reset" msgstr "" #: src/Navigation.tsx:217 -#: src/screens/Settings/Settings.tsx:100 +#: src/screens/Settings/Settings.tsx:98 #: src/view/shell/desktop/LeftNav.tsx:806 #: src/view/shell/Drawer.tsx:597 msgid "Settings" @@ -9253,8 +9264,8 @@ msgstr "" msgid "Share anyway" msgstr "" -#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:165 -#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:168 +#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:176 +#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:179 msgid "Share author DID" msgstr "" @@ -9269,8 +9280,8 @@ msgstr "" msgid "Share link dialog" msgstr "" -#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:156 -#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:159 +#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:167 +#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:170 msgid "Share post at:// URI" msgstr "" @@ -9291,8 +9302,8 @@ msgstr "" msgid "Share this starter pack and help people join your community on Bluesky." msgstr "" -#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:121 -#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:124 +#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:132 +#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:135 #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:160 #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:166 #: src/screens/StarterPack/StarterPackScreen.tsx:640 @@ -9433,7 +9444,7 @@ msgstr "" msgid "Shows information about when this post was created" msgstr "" -#: src/screens/Settings/Settings.tsx:126 +#: src/screens/Settings/Settings.tsx:123 msgid "Shows other accounts you can switch to" msgstr "" @@ -9497,9 +9508,9 @@ msgstr "" msgid "Sign in to view post" msgstr "" -#: src/screens/Settings/Settings.tsx:269 -#: src/screens/Settings/Settings.tsx:271 -#: src/screens/Settings/Settings.tsx:303 +#: src/screens/Settings/Settings.tsx:265 +#: src/screens/Settings/Settings.tsx:267 +#: src/screens/Settings/Settings.tsx:299 #: src/screens/SignupQueued.tsx:94 #: src/screens/SignupQueued.tsx:97 #: src/screens/Takendown.tsx:88 @@ -9513,7 +9524,7 @@ msgstr "" msgid "Sign Out" msgstr "" -#: src/screens/Settings/Settings.tsx:300 +#: src/screens/Settings/Settings.tsx:296 #: src/view/shell/desktop/LeftNav.tsx:211 msgid "Sign out?" msgstr "" @@ -9607,7 +9618,7 @@ msgstr "" msgid "Something wasn't quite right with the data you're trying to report. Please contact support." msgstr "" -#: src/screens/Messages/Conversation.tsx:142 +#: src/screens/Messages/Conversation.tsx:144 msgid "Something went wrong" msgstr "" @@ -9679,7 +9690,7 @@ msgstr "" msgid "Sports" msgstr "" -#: src/components/PostControls/ShareMenu/RecentChats.tsx:199 +#: src/components/PostControls/ShareMenu/RecentChats.tsx:206 msgid "Start a conversation, and it will appear here." msgstr "" @@ -9761,7 +9772,7 @@ msgstr "" msgid "Step {0} of {1}" msgstr "" -#: src/screens/Settings/Settings.tsx:405 +#: src/screens/Settings/Settings.tsx:401 msgid "Storage cleared, you need to restart the app now." msgstr "" @@ -9770,7 +9781,7 @@ msgid "Stored as part of a secure code for matching with others" msgstr "" #: src/Navigation.tsx:310 -#: src/screens/Settings/Settings.tsx:460 +#: src/screens/Settings/Settings.tsx:456 msgid "Storybook" msgstr "" @@ -9883,9 +9894,9 @@ msgstr "" #: src/components/dialogs/SwitchAccount.tsx:47 #: src/components/dialogs/SwitchAccount.tsx:50 -#: src/screens/Settings/Settings.tsx:124 -#: src/screens/Settings/Settings.tsx:138 -#: src/screens/Settings/Settings.tsx:619 +#: src/screens/Settings/Settings.tsx:122 +#: src/screens/Settings/Settings.tsx:134 +#: src/screens/Settings/Settings.tsx:615 #: src/view/shell/desktop/LeftNav.tsx:249 msgid "Switch account" msgstr "" @@ -9909,7 +9920,7 @@ msgstr "" #: src/screens/Log.tsx:57 #: src/screens/Settings/AboutSettings.tsx:106 #: src/screens/Settings/AboutSettings.tsx:109 -#: src/screens/Settings/Settings.tsx:453 +#: src/screens/Settings/Settings.tsx:449 msgid "System log" msgstr "" @@ -10469,7 +10480,7 @@ msgstr "" msgid "This post has an unknown type of threadgate on it. Your app may be out of date." msgstr "" -#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:146 +#: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:157 msgid "This post is only visible to logged-in users." msgstr "" @@ -10497,7 +10508,7 @@ msgstr "" msgid "This service has not provided terms of service or a privacy policy." msgstr "" -#: src/features/liveNow/index.tsx:199 +#: src/features/liveNow/index.tsx:200 msgid "This service is not supported while the Live feature is in beta. Allowed services: {formatted}." msgstr "" @@ -10562,7 +10573,7 @@ msgid "This will irreversibly delete your Bluesky account <0>{currentHandle} msgstr "" #. placeholder {0}: account.handle -#: src/screens/Settings/Settings.tsx:673 +#: src/screens/Settings/Settings.tsx:668 msgid "This will remove @{0} from the quick access list." msgstr "" @@ -10735,7 +10746,7 @@ msgstr "" msgid "Two-factor authentication (2FA)" msgstr "" -#: src/screens/Messages/components/MessageInput.tsx:155 +#: src/screens/Messages/components/MessageInput.tsx:173 msgid "Type your message here" msgstr "" @@ -10773,11 +10784,11 @@ msgstr "" msgid "Unable to resolve handle" msgstr "" -#: src/screens/Settings/Settings.tsx:519 +#: src/screens/Settings/Settings.tsx:515 msgid "Unapply Pull Request" msgstr "" -#: src/screens/Settings/Settings.tsx:521 +#: src/screens/Settings/Settings.tsx:517 msgid "Unapply Pull Request {currentChannel}" msgstr "" @@ -10982,8 +10993,8 @@ msgstr "" msgid "Unpinned list" msgstr "" -#: src/screens/Settings/Settings.tsx:486 -#: src/screens/Settings/Settings.tsx:488 +#: src/screens/Settings/Settings.tsx:482 +#: src/screens/Settings/Settings.tsx:484 msgid "Unsnooze email reminder" msgstr "" @@ -11555,8 +11566,8 @@ msgstr "" msgid "Warn content and filter from feeds" msgstr "" -#: src/features/liveNow/components/LiveStatusDialog.tsx:188 -#: src/features/liveNow/components/LiveStatusDialog.tsx:197 +#: src/features/liveNow/components/LiveStatusDialog.tsx:189 +#: src/features/liveNow/components/LiveStatusDialog.tsx:198 msgid "Watch now" msgstr "" @@ -11580,7 +11591,7 @@ msgstr "" msgid "We couldn't find any results for that topic." msgstr "" -#: src/screens/Messages/Conversation.tsx:143 +#: src/screens/Messages/Conversation.tsx:145 msgid "We couldn't load this conversation" msgstr "" @@ -11856,8 +11867,6 @@ msgstr "" msgid "Would you like to save this as a draft to edit later?" msgstr "" -#: src/screens/Messages/components/MessageComposer.tsx:167 -#: src/screens/Messages/components/MessageInput.tsx:156 #: src/screens/Messages/components/MessageInput.web.tsx:213 msgid "Write a message" msgstr "" @@ -11953,7 +11962,7 @@ msgstr "" msgid "You are Live" msgstr "" -#: src/features/liveNow/index.tsx:367 +#: src/features/liveNow/index.tsx:368 msgid "You are no longer live" msgstr "" @@ -11965,7 +11974,7 @@ msgstr "" msgid "You are not following anyone yet" msgstr "" -#: src/features/liveNow/index.tsx:311 +#: src/features/liveNow/index.tsx:312 msgid "You are now live!" msgstr "" @@ -12241,7 +12250,7 @@ msgstr "" msgid "You previously deactivated @{0}." msgstr "" -#: src/screens/Settings/Settings.tsx:416 +#: src/screens/Settings/Settings.tsx:412 msgid "You probably want to restart the app now." msgstr "" @@ -12261,7 +12270,7 @@ msgstr "" msgid "You recently changed your birthdate" msgstr "" -#: src/screens/Settings/Settings.tsx:301 +#: src/screens/Settings/Settings.tsx:297 #: src/view/shell/desktop/LeftNav.tsx:212 msgid "You will be signed out of all your accounts." msgstr "" diff --git a/src/screens/Messages/Conversation.tsx b/src/screens/Messages/Conversation.tsx index f6051c9558..509907b331 100644 --- a/src/screens/Messages/Conversation.tsx +++ b/src/screens/Messages/Conversation.tsx @@ -5,16 +5,19 @@ import { moderateProfile, type ModerationDecision, } from '@atproto/api' +import {ScrollEdgeEffectProvider} from '@bsky.app/expo-scroll-edge-effect' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' import { type RouteProp, useFocusEffect, + useIsFocused, useNavigation, useRoute, } from '@react-navigation/native' import {type NativeStackScreenProps} from '@react-navigation/native-stack' +import {RemoveScrollBar} from 'react-remove-scroll-bar' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import { @@ -30,7 +33,7 @@ import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useProfileQuery} from '#/state/queries/profile' import {useSetMinimalShellMode} from '#/state/shell' import {MessagesList} from '#/screens/Messages/components/MessagesList' -import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' +import {atoms as a, useTheme, web} from '#/alf' import {AgeRestrictedScreen} from '#/components/ageAssurance/AgeRestrictedScreen' import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy' import { @@ -62,7 +65,6 @@ export function MessagesConversationScreen(props: Props) { } export function MessagesConversationScreenInner({route}: Props) { - const {gtMobile} = useBreakpoints() const setMinimalShellMode = useSetMinimalShellMode() const convoId = route.params.conversation @@ -71,25 +73,22 @@ export function MessagesConversationScreenInner({route}: Props) { useFocusEffect( useCallback(() => { setCurrentConvoId(convoId) - - if (IS_WEB && !gtMobile) { - setMinimalShellMode(true) - } else { - setMinimalShellMode(false) - } + setMinimalShellMode(true) return () => { setCurrentConvoId(undefined) setMinimalShellMode(false) } - }, [gtMobile, convoId, setCurrentConvoId, setMinimalShellMode]), + }, [convoId, setCurrentConvoId, setMinimalShellMode]), ) return ( - - - + + + + + ) } @@ -98,6 +97,7 @@ function Inner() { const t = useTheme() const convoState = useConvo() const {_} = useLingui() + const isFocused = useIsFocused() const moderationOpts = useModerationOpts() const {data: recipientUnshadowed} = useProfileQuery({ @@ -122,11 +122,13 @@ function Inner() { // Any time that we re-render the `Initializing` state, we have to reset `hasScrolled` to false. After entering this // state, we know that we're resetting the list of messages and need to re-scroll to the bottom when they get added. - useEffect(() => { + const [prevState, setPrevState] = useState(convoState.status) + if (prevState !== convoState.status) { + setPrevState(convoState.status) if (convoState.status === ConvoStatus.Initializing) { setHasScrolled(false) } - }, [convoState.status]) + } if (convoState.status === ConvoStatus.Error) { return ( @@ -150,6 +152,8 @@ function Inner() { return ( + {/* MessagesList does not use the body scroll */} + {isFocused && IS_WEB && } {!readyToShow && (moderation ? ( diff --git a/src/screens/Messages/components/MessageComposer.tsx b/src/screens/Messages/components/MessageComposer.tsx index 9d46534849..cb5da5628a 100644 --- a/src/screens/Messages/components/MessageComposer.tsx +++ b/src/screens/Messages/components/MessageComposer.tsx @@ -1,5 +1,19 @@ import {useEffect, useState} from 'react' import {Pressable, View} from 'react-native' +import { + useKeyboardHandler, + useReanimatedKeyboardAnimation, +} from 'react-native-keyboard-controller' +import Animated, { + Extrapolation, + interpolate, + runOnJS, + useAnimatedStyle, +} from 'react-native-reanimated' +import {useSafeAreaInsets} from 'react-native-safe-area-context' +import {GlassContainer} from 'expo-glass-effect' +import {LinearGradient} from 'expo-linear-gradient' +import {ScrollEdgeEffect} from '@bsky.app/expo-scroll-edge-effect' import {useLingui} from '@lingui/react/macro' import {countGraphemes} from 'unicode-segmenter/grapheme' @@ -17,20 +31,24 @@ import { EmojiPicker, type EmojiPickerState, } from '#/view/com/composer/text-input/web/EmojiPicker' -import {atoms as a, useTheme} from '#/alf' +import {atoms as a, native, platform, tokens, useTheme, utils} from '#/alf' import {Composer, useComposerInternalApiRef} from '#/components/Composer' -import {useInteractionState} from '#/components/hooks/useInteractionState' -import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji' -import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane' +import {GlassView} from '#/components/GlassView' +import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmileIcon} from '#/components/icons/Emoji' +import {PaperPlaneVertical_Filled_Stroke2_Corner1_Rounded as PaperPlaneIcon} from '#/components/icons/PaperPlane' import * as Toast from '#/components/Toast' -import {IS_WEB} from '#/env' +import {IS_ANDROID, IS_LIQUID_GLASS, IS_NATIVE, IS_WEB} from '#/env' + +const MIN_HEIGHT = 40 export function MessageComposer({ + textInputId, onSendMessage, hasEmbed, setEmbed, children, }: { + textInputId?: string onSendMessage: (message: string) => void hasEmbed: boolean setEmbed: (embedUrl: string | undefined) => void @@ -48,16 +66,25 @@ export function MessageComposer({ }) const composerInternalApiRef = useComposerInternalApiRef() - const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() - const { - state: hovered, - onIn: onHoverIn, - onOut: onHoverOut, - } = useInteractionState() - const [text, setText] = useState(getDraft) useSaveMessageDraft(text) + // Android interactive dismiss sometimes doesn't blur the input + const blur = () => { + composerInternalApiRef.current?.input?.blur() + } + + useKeyboardHandler({ + onEnd: evt => { + 'worklet' + if (IS_ANDROID && evt.progress === 0) { + runOnJS(blur)() + } + }, + }) + + const submitDisabled = !editable || (!hasEmbed && text.trim().length === 0) + const openEmojiPicker = (pos: any) => { setEmojiPickerState({isOpen: true, pos}) } @@ -65,10 +92,12 @@ export function MessageComposer({ const onSubmit = () => { if (!editable) return if (!hasEmbed && text.trim() === '') return - if (countGraphemes(text) > MAX_DM_GRAPHEME_LENGTH) { - Toast.show(l`Message is too long`, { - type: 'error', - }) + const graphemeCount = countGraphemes(text) + if (graphemeCount > MAX_DM_GRAPHEME_LENGTH) { + Toast.show( + l`Message is too long (${graphemeCount}/${MAX_DM_GRAPHEME_LENGTH})`, + {type: 'error'}, + ) return } @@ -91,152 +120,111 @@ export function MessageComposer({ return () => { textInputWebEmitter.removeListener('emoji-inserted', onEmojiInserted) } - }, []) + }, [composerInternalApiRef]) return ( - <> - - {children} + + {children} - { - composerInternalApiRef.current?.setAutocompleteAnchor(node) - } - } - // @ts-expect-error web only - onMouseEnter={onHoverIn} - onMouseLeave={onHoverOut} - style={[a.w_full, a.flex_row, a.gap_sm]}> - {IS_WEB && ( - { - e.currentTarget.measure((_fx, _fy, _width, _height, px, py) => { - openEmojiPicker?.({ - top: py, - left: px, - right: px, - bottom: py, - nextFocusRef: { - current: composerInternalApiRef.current?.input?.element, + + void composerInternalApiRef.current?.setAutocompleteAnchor(node), + )}> + + + {IS_WEB && ( + { + e.currentTarget.measure( + (_fx, _fy, _width, _height, px, py) => { + // TODO: rip this horrible system out + openEmojiPicker?.({ + top: py, + left: px - 400, + right: px - 400, + bottom: py, + nextFocusRef: { + current: + composerInternalApiRef.current?.input?.element, + }, + }) }, - }) - }) + ) + }} + style={[ + a.overflow_hidden, + a.absolute, + a.rounded_full, + a.align_center, + a.justify_center, + a.z_30, + { + height: 20, + width: 20, + top: 10, + right: 10, + }, + ]} + accessibilityLabel={l`Open emoji picker`} + accessibilityHint=""> + {state => ( + + )} + + )} + + - {state => ( - - - - )} - - )} - - { - if (facet.type === 'url' && isBskyPostUrl(facet.value)) { - setEmbed(facet.value) - } - }} - onRequestSubmit={req => { - if (req.platform === 'web' && req.shiftKey) return - req.nativeEvent.preventDefault() - onSubmit() - }} - /> - - {focused || text.length ? ( - - - - ) : null} - + onChange={setText} + onFacetCommitted={facet => { + if (facet.type === 'url' && isBskyPostUrl(facet.value)) { + setEmbed(facet.value) + } + }} + onRequestSubmit={req => { + if (req.platform === 'web' && req.shiftKey) return + req.nativeEvent.preventDefault() + onSubmit() + }} + /> + + + {IS_WEB && ( @@ -246,6 +234,114 @@ export function MessageComposer({ close={() => setEmojiPickerState(prev => ({...prev, isOpen: false}))} /> )} - + ) } + +function SubmitButton({ + onPress, + disabled, +}: { + onPress: () => void + disabled: boolean +}) { + const {t: l} = useLingui() + const t = useTheme() + + return ( + + + + + + ) +} + +// TODO: remove export when MessageInput is deleted +export function ComposerContainer({children}: {children: React.ReactNode}) { + const {bottom: bottomInset} = useSafeAreaInsets() + const {progress} = useReanimatedKeyboardAnimation() + const t = useTheme() + + const animatedContainerStyle = useAnimatedStyle(() => ({ + paddingHorizontal: interpolate( + progress.get(), + [0, 1], + [bottomInset, tokens.space.sm], + { + extrapolateRight: Extrapolation.CLAMP, + extrapolateLeft: Extrapolation.CLAMP, + }, + ), + })) + + if (IS_LIQUID_GLASS) { + return ( + + + {children} + + + ) + } else { + return ( + <> + + {children} + + {/* covers the gap between the keyboard and the input during keyboard animation */} + {IS_NATIVE && ( + + )} + + ) + } +} diff --git a/src/screens/Messages/components/MessageInput.tsx b/src/screens/Messages/components/MessageInput.tsx index f39205bd3b..643c0c86a6 100644 --- a/src/screens/Messages/components/MessageInput.tsx +++ b/src/screens/Messages/components/MessageInput.tsx @@ -1,17 +1,20 @@ import {useCallback, useState} from 'react' -import {Pressable, TextInput, useWindowDimensions, View} from 'react-native' +import {Pressable, TextInput, useWindowDimensions} from 'react-native' import { useFocusedInputHandler, + useKeyboardHandler, useReanimatedKeyboardAnimation, } from 'react-native-keyboard-controller' import Animated, { measure, + runOnJS, useAnimatedProps, useAnimatedRef, useAnimatedStyle, useSharedValue, } from 'react-native-reanimated' import {useSafeAreaInsets} from 'react-native-safe-area-context' +import {GlassContainer} from 'expo-glass-effect' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {countGraphemes} from 'unicode-segmenter/grapheme' @@ -24,21 +27,26 @@ import { useSaveMessageDraft, } from '#/state/messages/message-drafts' import {type EmojiPickerPosition} from '#/view/com/composer/text-input/web/EmojiPicker' -import {android, atoms as a, useTheme} from '#/alf' -import {useSharedInputStyles} from '#/components/forms/TextField' -import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane' +import {atoms as a, platform, tokens, useTheme} from '#/alf' +import {GlassView} from '#/components/GlassView' +import {PaperPlaneVertical_Filled_Stroke2_Corner1_Rounded as PaperPlaneIcon} from '#/components/icons/PaperPlane' import * as Toast from '#/components/Toast' -import {IS_IOS, IS_WEB} from '#/env' +import {IS_ANDROID, IS_IOS, IS_WEB} from '#/env' +import {ComposerContainer} from './MessageComposer' import {useExtractEmbedFromFacets} from './MessageInputEmbed' const AnimatedTextInput = Animated.createAnimatedComponent(TextInput) +const MIN_HEIGHT = 40 + export function MessageInput({ + textInputId, onSendMessage, hasEmbed, setEmbed, children, }: { + textInputId?: string onSendMessage: (message: string) => void hasEmbed: boolean setEmbed: (embedUrl: string | undefined) => void @@ -57,8 +65,6 @@ export function MessageInput({ const maxHeight = useSharedValue(undefined) const isInputScrollable = useSharedValue(false) - const inputStyles = useSharedInputStyles() - const [isFocused, setIsFocused] = useState(false) const [message, setMessage] = useState(getDraft) const inputRef = useAnimatedRef() const [shouldEnforceClear, setShouldEnforceClear] = useState(false) @@ -133,78 +139,114 @@ export function MessageInput({ scrollEnabled: isInputScrollable.get(), })) + const submitDisabled = needsEmailVerification || message.trim().length === 0 + + const blur = useCallback(() => { + inputRef.current?.blur() + }, [inputRef]) + + useKeyboardHandler({ + onEnd: evt => { + 'worklet' + // small hack: interactive dismiss on Android sometimes doesn't blur the input + if (IS_ANDROID && evt.progress === 0) { + runOnJS(blur)() + } + }, + }) + return ( - + {children} - - { - // bit of a hack: iOS automatically accepts autocomplete suggestions when you tap anywhere on the screen - // including the button we just pressed - and this overrides clearing the input! so we watch for the - // next change and double make sure the input is cleared. It should *always* send an onChange event after - // clearing via setMessage('') that happens in onSubmit() - // -sfn - if (IS_IOS && shouldEnforceClear) { - setShouldEnforceClear(false) - setMessage('') - return - } - const text = evt.nativeEvent.text - setMessage(text) - }} - multiline={true} - style={[ - a.flex_1, - a.text_md, - a.px_sm, - t.atoms.text, - android({paddingTop: 0}), - {paddingBottom: IS_IOS ? 5 : 0}, - animatedStyle, - ]} - keyboardAppearance={t.scheme} - submitBehavior="newline" - onFocus={() => setIsFocused(true)} - onBlur={() => setIsFocused(false)} - ref={inputRef} - hitSlop={HITSLOP_10} - animatedProps={animatedProps} - editable={!needsEmailVerification} - /> - - - - - + + + { + // bit of a hack: iOS automatically accepts autocomplete suggestions when you tap anywhere on the screen + // including the button we just pressed - and this overrides clearing the input! so we watch for the + // next change and double make sure the input is cleared. It should *always* send an onChange event after + // clearing via setMessage('') that happens in onSubmit() + // -sfn + if (IS_IOS && shouldEnforceClear) { + setShouldEnforceClear(false) + setMessage('') + return + } + const text = evt.nativeEvent.text + setMessage(text) + }} + multiline={true} + style={[ + {flexBasis: 'auto', minHeight: MIN_HEIGHT}, + a.flex_shrink_0, + a.flex_grow, + a.text_md, + a.px_lg, + t.atoms.text, + platform({ + android: {paddingTop: 2, paddingBottom: 3}, + ios: {paddingTop: 10, paddingBottom: 5}, + }), + animatedStyle, + ]} + verticalAlign="middle" + keyboardAppearance={t.scheme} + submitBehavior="newline" + ref={inputRef} + hitSlop={HITSLOP_10} + animatedProps={animatedProps} + editable={!needsEmailVerification} + /> + + + + + + + + ) } diff --git a/src/screens/Messages/components/MessagesList.tsx b/src/screens/Messages/components/MessagesList.tsx index 27f1c93387..eda3c593d9 100644 --- a/src/screens/Messages/components/MessagesList.tsx +++ b/src/screens/Messages/components/MessagesList.tsx @@ -1,22 +1,28 @@ -import {useCallback, useEffect, useRef, useState} from 'react' -import {type LayoutChangeEvent, View} from 'react-native' -import {useKeyboardHandler} from 'react-native-keyboard-controller' +import {useCallback, useEffect, useId, useRef, useState} from 'react' +import {type LayoutChangeEvent, type ScrollViewProps, View} from 'react-native' +import { + KeyboardChatScrollView, + type KeyboardChatScrollViewProps, + KeyboardGestureArea, +} from 'react-native-keyboard-controller' import Animated, { runOnJS, - scrollTo, + type ScrollEvent, + type SharedValue, useAnimatedRef, - useAnimatedStyle, + useDerivedValue, useSharedValue, } from 'react-native-reanimated' -import {type ReanimatedScrollEvent} from 'react-native-reanimated/lib/typescript/hook/commonTypes' +import {useSafeAreaInsets} from 'react-native-safe-area-context' import { type $Typed, type AppBskyEmbedRecord, AppBskyRichtextFacet, RichText, } from '@atproto/api' +import {useScrollEdgeEffectRef} from '@bsky.app/expo-scroll-edge-effect' -import {useHideBottomBarBorderForScreen} from '#/lib/hooks/useHideBottomBarBorder' +import {mergeRefs} from '#/lib/merge-refs' import {ScrollProvider} from '#/lib/ScrollContext' import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip' import { @@ -36,7 +42,6 @@ import { } from '#/state/messages/convo/types' import {useGetPost} from '#/state/queries/post' import {useAgent} from '#/state/session' -import {useShellLayout} from '#/state/shell/shell-layout' import { EmojiPicker, type EmojiPickerState, @@ -46,15 +51,17 @@ import {ChatDisabled} from '#/screens/Messages/components/ChatDisabled' import {MessageComposer} from '#/screens/Messages/components/MessageComposer' import {MessageInput} from '#/screens/Messages/components/MessageInput' import {MessageListError} from '#/screens/Messages/components/MessageListError' +import {atoms as a, platform, tokens, useTheme, web} from '#/alf' import {ChatEmptyPill} from '#/components/dms/ChatEmptyPill' import {MessageItem} from '#/components/dms/MessageItem' import {NewMessagesPill} from '#/components/dms/NewMessagesPill' import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' -import {IS_NATIVE, IS_WEB} from '#/env' +import {IS_ANDROID, IS_NATIVE, IS_WEB} from '#/env' import {ChatStatusInfo} from './ChatStatusInfo' import {MessageInputEmbed, useMessageEmbed} from './MessageInputEmbed' +import {KeyboardStickyView} from './vendor/KeyboardStickyView' function MaybeLoader({isLoading}: {isLoading: boolean}) { return ( @@ -108,9 +115,9 @@ export function MessagesList({ const agent = useAgent() const getPost = useGetPost() const {embedUri, setEmbed} = useMessageEmbed() + const t = useTheme() - useHideBottomBarBorderForScreen() - + const textInputId = 'chat-input-' + useId() const flatListRef = useAnimatedRef() const [newMessagesPill, setNewMessagesPill] = useState({ @@ -123,6 +130,18 @@ export function MessagesList({ pos: {top: 0, left: 0, right: 0, bottom: 0, nextFocusRef: null}, }) + const inputHeightUI = useSharedValue(0) + const [inputHeightJS, setInputHeightJS] = useState(0) + + const onInputLayout = useCallback( + (event: LayoutChangeEvent) => { + const inputHeight = event.nativeEvent.layout.height + inputHeightUI.set(inputHeight) + setInputHeightJS(inputHeight) + }, + [inputHeightUI], + ) + // We need to keep track of when the scroll offset is at the bottom of the list to know when to scroll as new items // are added to the list. For example, if the user is scrolled up to 1iew older messages, we don't want to scroll to // the bottom. @@ -226,12 +245,12 @@ export function MessagesList({ const onStartReached = useCallback(() => { if (hasScrolled && prevContentHeight.current > layoutHeight.get()) { - convoState.fetchMessageHistory() + void convoState.fetchMessageHistory() } }, [convoState, hasScrolled, layoutHeight]) const onScroll = useCallback( - (e: ReanimatedScrollEvent) => { + (e: ScrollEvent) => { 'worklet' layoutHeight.set(e.layoutMeasurement.height) const bottomOffset = e.contentOffset.y + e.layoutMeasurement.height @@ -256,56 +275,8 @@ export function MessagesList({ ) // -- Keyboard animation handling - const {footerHeight} = useShellLayout() - const keyboardHeight = useSharedValue(0) - const keyboardIsOpening = useSharedValue(false) - - // In some cases - like when the emoji piker opens - we don't want to animate the scroll in the list onLayout event. - // We use this value to keep track of when we want to disable the animation. - const layoutScrollWithoutAnimation = useSharedValue(false) - - useKeyboardHandler( - { - onStart: e => { - 'worklet' - // Immediate updates - like opening the emoji picker - will have a duration of zero. In those cases, we should - // just update the height here instead of having the `onMove` event do it (that event will not fire!) - if (e.duration === 0) { - layoutScrollWithoutAnimation.set(true) - keyboardHeight.set(e.height) - } else { - keyboardIsOpening.set(true) - } - }, - onMove: e => { - 'worklet' - keyboardHeight.set(e.height) - if (e.height > footerHeight.get()) { - scrollTo(flatListRef, 0, 1e7, false) - } - }, - onEnd: e => { - 'worklet' - keyboardHeight.set(e.height) - if (e.height > footerHeight.get()) { - scrollTo(flatListRef, 0, 1e7, false) - } - keyboardIsOpening.set(false) - }, - }, - [footerHeight], - ) - - const animatedListStyle = useAnimatedStyle(() => ({ - marginBottom: Math.max(keyboardHeight.get(), footerHeight.get()), - })) - - const animatedStickyViewStyle = useAnimatedStyle(() => ({ - transform: [ - {translateY: -Math.max(keyboardHeight.get(), footerHeight.get())}, - ], - })) + const {bottom: bottomInset} = useSafeAreaInsets() // -- Message sending const onSendMessage = useCallback( @@ -387,26 +358,6 @@ export function MessagesList({ [agent, convoState, embedUri, getPost, hasScrolled, setHasScrolled], ) - // -- List layout changes (opening emoji keyboard, etc.) - const onListLayout = useCallback( - (e: LayoutChangeEvent) => { - layoutHeight.set(e.nativeEvent.layout.height) - - if (IS_WEB || !keyboardIsOpening.get()) { - flatListRef.current?.scrollToEnd({ - animated: !layoutScrollWithoutAnimation.get(), - }) - layoutScrollWithoutAnimation.set(false) - } - }, - [ - flatListRef, - keyboardIsOpening, - layoutScrollWithoutAnimation, - layoutHeight, - ], - ) - const scrollToEndOnPress = useCallback(() => { flatListRef.current?.scrollToOffset({ offset: prevContentHeight.current, @@ -418,66 +369,100 @@ export function MessagesList({ setEmojiPickerState({isOpen: true, pos}) }, []) + const renderScrollComponent = useCallback( + (props: ScrollViewProps) => ( + + ), + [inputHeightUI], + ) + return ( <> - {/* Custom scroll provider so that we can use the `onScroll` event in our custom List implementation */} - - - } - /> - - - {convoState.status === ConvoStatus.Disabled ? ( - - ) : blocked ? ( - footer - ) : ( - - {ax.features.enabled(ax.features.DmsNewMessageComposerEnable) ? ( - - - - ) : ( - - - + + {/* Custom scroll provider so that we can use the `onScroll` event in our custom List implementation */} + + + } + // native only (prop is not supported on web) + renderScrollComponent={renderScrollComponent} + // pushes up the content under the input on web (renderScrollComponent handles it on native) + ListFooterComponent={web( + , )} - - )} - + style={web({ + scrollbarWidth: 'thin', + scrollbarColor: `${t.palette.contrast_100} transparent`, + scrollbarGutter: 'stable both-edges', + })} + /> + + + {convoState.status === ConvoStatus.Disabled ? ( + + ) : blocked ? ( + footer + ) : ( + + {ax.features.enabled(ax.features.DmsNewMessageComposerEnable) ? ( + + + + ) : ( + + + + )} + + )} + + {IS_WEB && ( + inputHeight: SharedValue +}) { + const scrollEdgeRef = useScrollEdgeEffectRef() + const {bottom: bottomInset} = useSafeAreaInsets() + + const offset = platform({ + ios: bottomInset - tokens.space.lg, + android: bottomInset, + default: 0, + }) + + const inputOffset = platform({ + ios: bottomInset - tokens.space.lg, + android: bottomInset, + default: 0, + }) + + const extraContentPadding = useDerivedValue( + () => inputHeight.get() + inputOffset, + ) + + return ( + + ) +} + +function WebInputSpacer({inputHeight}: {inputHeight: number}) { + if (!IS_WEB) return null + + return +} + type FooterState = 'loading' | 'new-chat' | 'request' | 'standard' function getFooterState( diff --git a/src/screens/Messages/components/vendor/KeyboardStickyView.tsx b/src/screens/Messages/components/vendor/KeyboardStickyView.tsx new file mode 100644 index 0000000000..bacb18fb6b --- /dev/null +++ b/src/screens/Messages/components/vendor/KeyboardStickyView.tsx @@ -0,0 +1,48 @@ +import { + type KeyboardStickyViewProps, + useReanimatedKeyboardAnimation, +} from 'react-native-keyboard-controller' +import Animated, {useAnimatedStyle} from 'react-native-reanimated' + +// Vendored from https://github.com/kirillzyusko/react-native-keyboard-controller/blob/main/src/components/KeyboardStickyView/index.tsx +// Converted to Reanimated to support `minimumOffset` clamping. +export function KeyboardStickyView({ + children, + offset: {closed = 0, opened = 0} = {}, + style, + enabled = true, + minimumOffset, + ...props +}: KeyboardStickyViewProps & { + /** + * Stop the stickyview going lower than this (i.e. bottom safe area) + */ + minimumOffset?: number +}) { + const {height, progress} = useReanimatedKeyboardAnimation() + + const animatedStyle = useAnimatedStyle(() => { + const offset = closed + (opened - closed) * progress.get() + let translateY: number + + if (enabled) { + let h = height.get() + if (minimumOffset != null) { + h = Math.min(h, -minimumOffset) + } + translateY = h + offset + } else { + translateY = closed + } + + return { + transform: [{translateY}], + } + }) + + return ( + + {children} + + ) +} diff --git a/src/screens/Settings/Settings.tsx b/src/screens/Settings/Settings.tsx index 223a003638..2e0138c5e2 100644 --- a/src/screens/Settings/Settings.tsx +++ b/src/screens/Settings/Settings.tsx @@ -2,9 +2,7 @@ import {useState} from 'react' import {Alert, LayoutAnimation, Linking, Pressable, View} from 'react-native' import {useReducedMotion} from 'react-native-reanimated' import {type AppBskyActorDefs, moderateProfile} from '@atproto/api' -import {msg} from '@lingui/core/macro' -import {useLingui} from '@lingui/react' -import {Trans} from '@lingui/react/macro' +import {Trans, useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' import {type NativeStackScreenProps} from '@react-navigation/native-stack' @@ -73,7 +71,7 @@ import {useActivitySubscriptionsNudged} from '#/storage/hooks/activity-subscript type Props = NativeStackScreenProps export function SettingsScreen({}: Props) { const ax = useAnalytics() - const {_} = useLingui() + const {t: l} = useLingui() const reducedMotion = useReducedMotion() const {logoutEveryAccount} = useSessionApi() const {accounts, currentAccount} = useSession() @@ -121,10 +119,8 @@ export function SettingsScreen({}: Props) { {accounts.length > 1 ? ( <> { if (!reducedMotion) { LayoutAnimation.configureNext( @@ -174,7 +170,7 @@ export function SettingsScreen({}: Props) { )} - + Account @@ -182,21 +178,23 @@ export function SettingsScreen({}: Props) { + label={l`Privacy and security`}> Privacy and security - + - Moderation + Moderation and content filters + label={l`Notifications`}> Notifications @@ -204,7 +202,7 @@ export function SettingsScreen({}: Props) { + label={l`Content and media`}> Content and media @@ -215,7 +213,7 @@ export function SettingsScreen({}: Props) { !ax.features.enabled(ax.features.ImportContactsSettingsDisable) && ( + label={l`Find friends from contacts`}> Find friends from contacts @@ -224,7 +222,7 @@ export function SettingsScreen({}: Props) { )} + label={l`Appearance`}> Appearance @@ -232,15 +230,13 @@ export function SettingsScreen({}: Props) { + label={l`Accessibility`}> Accessibility - + Languages @@ -248,15 +244,15 @@ export function SettingsScreen({}: Props) { void Linking.openURL(HELP_DESK_URL)} - label={_(msg`Help`)} - accessibilityHint={_(msg`Opens helpdesk in browser`)}> + label={l`Help`} + accessibilityHint={l`Opens helpdesk in browser`}> Help - + About @@ -266,7 +262,7 @@ export function SettingsScreen({}: Props) { signOutPromptControl.open()} - label={_(msg`Sign out`)}> + label={l`Sign out`}> Sign out @@ -283,7 +279,7 @@ export function SettingsScreen({}: Props) { } setShowDevOptions(d => !d) }} - label={_(msg`Developer options`)}> + label={l`Developer options`}> Developer options @@ -297,11 +293,11 @@ export function SettingsScreen({}: Props) { logoutEveryAccount('Settings')} - confirmButtonCta={_(msg`Sign out`)} - cancelButtonCta={_(msg`Cancel`)} + confirmButtonCta={l`Sign out`} + cancelButtonCta={l`Cancel`} confirmButtonColor="negative" /> @@ -378,7 +374,7 @@ function ProfilePreview({ } function DevOptions() { - const {_} = useLingui() + const {t: l} = useLingui() const agent = useAgent() const [override, setOverride] = useStorage(device, [ 'policyUpdateDebugOverride', @@ -397,12 +393,12 @@ function DevOptions() { const resetOnboarding = () => { navigation.navigate('Home') onboardingDispatch({type: 'start'}) - Toast.show(_(msg`Onboarding reset`)) + Toast.show(l`Onboarding reset`) } const clearAllStorage = async () => { await clearStorage() - Toast.show(_(msg`Storage cleared, you need to restart the app now.`)) + Toast.show(l`Storage cleared, you need to restart the app now.`) } const onPressUnsnoozeReminder = () => { @@ -413,7 +409,7 @@ function DevOptions() { ...persisted.get('reminders'), lastEmailConfirm: lastEmailConfirm.toISOString(), }) - Toast.show(_(msg`You probably want to restart the app now.`)) + Toast.show(l`You probably want to restart the app now.`) } const onPressActySubsUnNudge = () => { @@ -448,42 +444,42 @@ function DevOptions() { <> navigation.navigate('Log')} - label={_(msg`Open system log`)}> + label={l`Open system log`}> System log navigation.navigate('Debug')} - label={_(msg`Open storybook page`)}> + label={l`Open storybook page`}> Storybook navigation.navigate('DebugMod')} - label={_(msg`Open moderation debug page`)}> + label={l`Open moderation debug page`}> Debug Moderation deleteChatDeclarationRecord()} - label={_(msg`Open storybook page`)}> + label={l`Open storybook page`}> Delete chat declaration record void resetOnboarding()} - label={_(msg`Reset onboarding state`)}> + label={l`Reset onboarding state`}> Reset onboarding state + label={l`Unsnooze email reminder`}> Unsnooze email reminder @@ -491,7 +487,7 @@ function DevOptions() { {actyNotifNudged && ( + label={l`Reset activity subscription nudge`}> Reset activity subscription nudge @@ -499,7 +495,7 @@ function DevOptions() { )} void clearAllStorage()} - label={_(msg`Clear all storage data`)}> + label={l`Clear all storage data`}> Clear all storage data (restart after this) @@ -507,7 +503,7 @@ function DevOptions() { {IS_IOS ? ( + label={l`Apply Pull Request`}> Apply Pull Request @@ -516,7 +512,7 @@ function DevOptions() { {IS_NATIVE && isCurrentlyRunningPullRequestDeployment ? ( void revertToEmbedded()} - label={_(msg`Unapply Pull Request`)}> + label={l`Unapply Pull Request`}> Unapply Pull Request {currentChannel} @@ -564,7 +560,7 @@ function DevOptions() { } function AddAccountRow() { - const {_} = useLingui() + const {t: l} = useLingui() const {setShowLoggedOut} = useLoggedOutViewControls() const closeEverything = useCloseAllActiveElements() @@ -576,7 +572,7 @@ function AddAccountRow() { return ( + label={l`Add another account`}> Add another account @@ -599,7 +595,7 @@ function AccountRow({ logContext: 'Settings', ) => void }) { - const {_} = useLingui() + const {t: l} = useLingui() const t = useTheme() const moderationOpts = useModerationOpts() @@ -616,7 +612,7 @@ function AccountRow({ + label={l`Switch account`}> {moderationOpts && profile ? ( {!pendingDid && ( - + {({props, state}) => ( removePromptControl.open()}> Remove account @@ -668,15 +664,13 @@ function AccountRow({ { removeAccount(account) - Toast.show(_(msg`Account removed from quick access`)) + Toast.show(l`Account removed from quick access`) }} - confirmButtonCta={_(msg`Remove`)} + confirmButtonCta={l`Remove`} confirmButtonColor="negative" /> diff --git a/src/state/queries/post.ts b/src/state/queries/post.ts index 4024001a90..79df51d1bf 100644 --- a/src/state/queries/post.ts +++ b/src/state/queries/post.ts @@ -1,6 +1,11 @@ import {useCallback} from 'react' import {type AppBskyActorDefs, type AppBskyFeedDefs, AtUri} from '@atproto/api' -import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' +import { + type QueryClient, + useMutation, + useQuery, + useQueryClient, +} from '@tanstack/react-query' import {useToggleMutationQueue} from '#/lib/hooks/useToggleMutationQueue' import {updatePostShadow} from '#/state/cache/post-shadow' @@ -43,6 +48,14 @@ export function usePostQuery(uri: string | undefined) { }) } +export function precachePost( + queryClient: QueryClient, + uri: string, + post: AppBskyFeedDefs.PostView, +) { + queryClient.setQueryData(RQKEY(uri), post) +} + export function useGetPost() { const queryClient = useQueryClient() const agent = useAgent() diff --git a/webpack.config.js b/webpack.config.js index db67d72e92..37c110c7bb 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -19,6 +19,9 @@ const reactNativeWebWebviewConfiguration = { } module.exports = async function (env, argv) { + env.babel = { + dangerouslyAddModulePathsToTranspile: ['@bsky.app/expo'], + } let config = await createExpoWebpackConfigAsync(env, argv) config = withAlias(config, { 'react-native$': 'react-native-web', diff --git a/yarn.lock b/yarn.lock index 49dcae5214..dc073840d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2429,6 +2429,11 @@ resolved "https://registry.yarnpkg.com/@bsky.app/expo-image-crop-tool/-/expo-image-crop-tool-0.5.0.tgz#4308fbde5c15e6be9122601797bc3d9549c95e31" integrity sha512-gmhQr2HWTRFyPO00fn5OmtiEVtikXusHMrN5Zoq26pu1VZX3zVE+aoc668etTqrvsQcm2Qu8fo96k5F3Wu+6wg== +"@bsky.app/expo-scroll-edge-effect@^0.1.4": + version "0.1.4" + resolved "https://registry.yarnpkg.com/@bsky.app/expo-scroll-edge-effect/-/expo-scroll-edge-effect-0.1.4.tgz#8b785b606c3078b3f8d1ec200adaf13bc59c2fec" + integrity sha512-P94YcYBqZfuUy7ewTrWulPTxTbs6Yvjg2xS5WX4I/F3R3cSQU9fc8vEzb2Pnn4Ieg2wukJdAywqzt+AnyWgJbw== + "@bsky.app/expo-translate-text@^0.2.9": version "0.2.9" resolved "https://registry.yarnpkg.com/@bsky.app/expo-translate-text/-/expo-translate-text-0.2.9.tgz#4ed4552cd50bca7d02d14e706e419bd728d4ab51" @@ -9013,6 +9018,11 @@ expo-font@~14.0.11: dependencies: fontfaceobserver "^2.1.0" +expo-glass-effect@55.0.8: + version "55.0.8" + resolved "https://registry.yarnpkg.com/expo-glass-effect/-/expo-glass-effect-55.0.8.tgz#ace0e662d7c8fc2935c9d1260e8303eb2fde0bc3" + integrity sha512-IvUjHb/4t6r2H/LXDjcQ4uDoHrmO2cLOvEb9leLavQ4HX5+P4LRtQrMDMlkWAn5Wo5DkLcG8+1CrQU2nqgogTA== + expo-haptics@~15.0.8: version "15.0.8" resolved "https://registry.yarnpkg.com/expo-haptics/-/expo-haptics-15.0.8.tgz#f93f895ac5d76fe0c5ac26b3644e1dbb097833f3" @@ -13980,10 +13990,10 @@ react-native-is-edge-to-edge@^1.2.1: resolved "https://registry.yarnpkg.com/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz#64e10851abd9d176cbf2b40562f751622bde3358" integrity sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q== -react-native-keyboard-controller@^1.21.0: - version "1.21.0" - resolved "https://registry.yarnpkg.com/react-native-keyboard-controller/-/react-native-keyboard-controller-1.21.0.tgz#79ad48c67e6f5ec572b7dc896c7b05a98662a2a2" - integrity sha512-mLHJysehhSzYoM8BAD2DSjVZEcF69t16ZCJrCAos6sfVtbB3tL+kgGZFX+jNVz/f9BEhqnBFO0EA1tc/V6Hkgw== +react-native-keyboard-controller@^1.21.5: + version "1.21.5" + resolved "https://registry.yarnpkg.com/react-native-keyboard-controller/-/react-native-keyboard-controller-1.21.5.tgz#563aabb7e9ce8dbe2a0dd5f949883ba81620b6c0" + integrity sha512-wxR+vpJ+2g6QMQCP1mRQKySDUietf5xLntZ76cUNHOGsjyqk6LtznXwHBG9YsR9E/b2IrHXISylwqPnIit6Y6A== dependencies: react-native-is-edge-to-edge "^1.2.1"