{
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/>2>0>"
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}0>
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"