diff --git a/bskyogcard/package.json b/bskyogcard/package.json
index 0fb28334cb..50248e4cf9 100644
--- a/bskyogcard/package.json
+++ b/bskyogcard/package.json
@@ -18,7 +18,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
- "@atproto/api": "0.20.6",
+ "@atproto/api": "0.20.11",
"@atproto/common": "^0.6.1",
"@resvg/resvg-js": "^2.6.2",
"express": "^4.19.2",
diff --git a/bskyogcard/pnpm-lock.yaml b/bskyogcard/pnpm-lock.yaml
index 39685ea8ce..2bebbe145c 100644
--- a/bskyogcard/pnpm-lock.yaml
+++ b/bskyogcard/pnpm-lock.yaml
@@ -208,8 +208,8 @@ importers:
.:
dependencies:
'@atproto/api':
- specifier: 0.20.6
- version: 0.20.6
+ specifier: 0.20.11
+ version: 0.20.11
'@atproto/common':
specifier: ^0.6.1
version: 0.6.1
@@ -259,8 +259,8 @@ importers:
packages:
- '@atproto/api@0.20.6':
- resolution: {integrity: sha512-WnFPcUl+qZdXmt27+Tg93BDIvBt/WpXfLIiBzBTp3ms9aszM5hAsfc7G8KEsnsmnRvcm0xRfiKEjIt5FxTKdYg==}
+ '@atproto/api@0.20.11':
+ resolution: {integrity: sha512-1NoVJpBDAdotxo1iMZdMd75JstpdKWgBYOnxfVD4m+52bRjgU4cFg3EOGNUognZntFgL/bIHyEgyN7SJWVf6Ig==}
engines: {node: '>=22'}
'@atproto/common-web@0.5.0':
@@ -1154,7 +1154,7 @@ packages:
snapshots:
- '@atproto/api@0.20.6':
+ '@atproto/api@0.20.11':
dependencies:
'@atproto/common-web': 0.5.0
'@atproto/lexicon': 0.7.1
diff --git a/bskyogcard/src/routes/chat-invite.tsx b/bskyogcard/src/routes/chat-invite.tsx
index e3267cc0d0..36482743b8 100644
--- a/bskyogcard/src/routes/chat-invite.tsx
+++ b/bskyogcard/src/routes/chat-invite.tsx
@@ -1,6 +1,6 @@
import assert from 'node:assert'
-import {type ChatBskyGroupDefs} from '@atproto/api'
+import {ChatBskyGroupDefs} from '@atproto/api'
import resvg from '@resvg/resvg-js'
import {type Express} from 'express'
import satori from 'satori'
@@ -32,7 +32,7 @@ export default function (ctx: AppContext, app: Express) {
codes: [code],
})
const found = result.data.joinLinkPreviews[0]
- if (!found) {
+ if (!ChatBskyGroupDefs.isJoinLinkPreviewView(found)) {
return res.status(404).end('not found')
}
preview = found
diff --git a/bskyweb/cmd/bskyweb/jsonld.go b/bskyweb/cmd/bskyweb/jsonld.go
index ca0f542545..4f49f6afc2 100644
--- a/bskyweb/cmd/bskyweb/jsonld.go
+++ b/bskyweb/cmd/bskyweb/jsonld.go
@@ -177,9 +177,9 @@ func bskyProfileURL(handle string) string {
return fmt.Sprintf("https://bsky.app/profile/%s", handle)
}
-// extractPostMedia returns thumbnail URLs for the post's image or video
-// embed, byte-identical to what we put in og:image. Callers derive
-// thumbnailUrl from urls[0].
+// extractPostMedia returns thumbnail URLs for the post's image, gallery,
+// or video embed, byte-identical to what we put in og:image. Callers
+// derive thumbnailUrl from urls[0].
func extractPostMedia(pv *appbsky.FeedDefs_PostView, embedHidden bool) []string {
if pv == nil || pv.Embed == nil || embedHidden {
return nil
@@ -188,6 +188,9 @@ func extractPostMedia(pv *appbsky.FeedDefs_PostView, embedHidden bool) []string
if pv.Embed.EmbedImages_View != nil {
return imageThumbs(pv.Embed.EmbedImages_View.Images)
}
+ if pv.Embed.EmbedGallery_View != nil {
+ return galleryThumbs(pv.Embed.EmbedGallery_View.Items)
+ }
if pv.Embed.EmbedVideo_View != nil && pv.Embed.EmbedVideo_View.Thumbnail != nil {
return []string{*pv.Embed.EmbedVideo_View.Thumbnail}
}
@@ -196,6 +199,9 @@ func extractPostMedia(pv *appbsky.FeedDefs_PostView, embedHidden bool) []string
if media.EmbedImages_View != nil {
return imageThumbs(media.EmbedImages_View.Images)
}
+ if media.EmbedGallery_View != nil {
+ return galleryThumbs(media.EmbedGallery_View.Items)
+ }
if media.EmbedVideo_View != nil && media.EmbedVideo_View.Thumbnail != nil {
return []string{*media.EmbedVideo_View.Thumbnail}
}
@@ -215,6 +221,31 @@ func imageThumbs(images []*appbsky.EmbedImages_ViewImage) []string {
return urls
}
+// galleryThumbs returns the thumbnail URLs of image items in a gallery
+// embed, or nil if empty. Items_Elem is a union; non-image variants and
+// nil entries are skipped so future gallery item types don't break SEO
+// extraction. Empty Thumbnail strings are also skipped to avoid emitting
+// if the appview ever returns one.
+func galleryThumbs(items []*appbsky.EmbedGallery_View_Items_Elem) []string {
+ if len(items) == 0 {
+ return nil
+ }
+ urls := make([]string, 0, len(items))
+ for _, item := range items {
+ if item == nil || item.EmbedGallery_ViewImage == nil {
+ continue
+ }
+ if item.EmbedGallery_ViewImage.Thumbnail == "" {
+ continue
+ }
+ urls = append(urls, item.EmbedGallery_ViewImage.Thumbnail)
+ }
+ if len(urls) == 0 {
+ return nil
+ }
+ return urls
+}
+
// findVideoEmbed returns the post's video embed view, or nil if there is
// none or embeds are hidden. Shared with extractVideoMeta so og:video and
// JSON-LD VideoObject stay in sync.
diff --git a/bskyweb/cmd/bskyweb/jsonld_test.go b/bskyweb/cmd/bskyweb/jsonld_test.go
index f8c6011fff..ee44008030 100644
--- a/bskyweb/cmd/bskyweb/jsonld_test.go
+++ b/bskyweb/cmd/bskyweb/jsonld_test.go
@@ -72,6 +72,60 @@ func withImages(thumbs ...string) func(*appbsky.FeedDefs_PostView) {
}
}
+// withGallery adds an app.bsky.embed.gallery view with image items.
+func withGallery(thumbs ...string) func(*appbsky.FeedDefs_PostView) {
+ return func(pv *appbsky.FeedDefs_PostView) {
+ var items []*appbsky.EmbedGallery_View_Items_Elem
+ for _, t := range thumbs {
+ items = append(items, &appbsky.EmbedGallery_View_Items_Elem{
+ EmbedGallery_ViewImage: &appbsky.EmbedGallery_ViewImage{
+ Thumbnail: t,
+ Fullsize: t + "_full",
+ },
+ })
+ }
+ pv.Embed = &appbsky.FeedDefs_PostView_Embed{
+ EmbedGallery_View: &appbsky.EmbedGallery_View{Items: items},
+ }
+ }
+}
+
+// withRecordWithMediaGallery adds a record-with-media embed whose media slot
+// is an app.bsky.embed.gallery view.
+func withRecordWithMediaGallery(qHandle, qDid, qRkey string, thumbs ...string) func(*appbsky.FeedDefs_PostView) {
+ return func(pv *appbsky.FeedDefs_PostView) {
+ var items []*appbsky.EmbedGallery_View_Items_Elem
+ for _, t := range thumbs {
+ items = append(items, &appbsky.EmbedGallery_View_Items_Elem{
+ EmbedGallery_ViewImage: &appbsky.EmbedGallery_ViewImage{
+ Thumbnail: t,
+ Fullsize: t + "_full",
+ },
+ })
+ }
+ pv.Embed = &appbsky.FeedDefs_PostView_Embed{
+ EmbedRecordWithMedia_View: &appbsky.EmbedRecordWithMedia_View{
+ Record: &appbsky.EmbedRecord_View{
+ Record: &appbsky.EmbedRecord_View_Record{
+ EmbedRecord_ViewRecord: &appbsky.EmbedRecord_ViewRecord{
+ Uri: "at://" + qDid + "/app.bsky.feed.post/" + qRkey,
+ Cid: "bafy-quoted",
+ Author: &appbsky.ActorDefs_ProfileViewBasic{
+ Did: qDid,
+ Handle: qHandle,
+ },
+ IndexedAt: "2024-01-01T00:00:00Z",
+ },
+ },
+ },
+ Media: &appbsky.EmbedRecordWithMedia_View_Media{
+ EmbedGallery_View: &appbsky.EmbedGallery_View{Items: items},
+ },
+ },
+ }
+ }
+}
+
// withVideo adds a video embed with a thumbnail.
func withVideo(thumb string) func(*appbsky.FeedDefs_PostView) {
return func(pv *appbsky.FeedDefs_PostView) {
@@ -299,6 +353,88 @@ func TestBuildPostJSONLD_WithImages(t *testing.T) {
}
}
+func TestBuildPostJSONLD_WithGallery(t *testing.T) {
+ thumb1 := "https://cdn.bsky.app/img/feed_thumbnail/plain/did:plc:alice/g1@jpeg"
+ thumb2 := "https://cdn.bsky.app/img/feed_thumbnail/plain/did:plc:alice/g2@jpeg"
+ thumb3 := "https://cdn.bsky.app/img/feed_thumbnail/plain/did:plc:alice/g3@jpeg"
+ pv := makePostView("alice.bsky.social", "did:plc:alice", "abc123", "gallery", withGallery(thumb1, thumb2, thumb3))
+ out, err := buildPostJSONLD(pv, nil, "https://bsky.app/profile/alice.bsky.social/post/abc123", hideEmbedLabels, hideReplyLabels)
+ if err != nil {
+ t.Fatal(err)
+ }
+ main := unmarshalLD(t, out)["mainEntity"].(map[string]any)
+ imgs, ok := main["image"].([]any)
+ if !ok {
+ t.Fatalf("image should be array, got %T", main["image"])
+ }
+ if len(imgs) != 3 {
+ t.Errorf("expected 3 gallery images, got %d", len(imgs))
+ }
+ if imgs[0] != thumb1 || imgs[1] != thumb2 || imgs[2] != thumb3 {
+ t.Errorf("gallery image[] order wrong: %v", imgs)
+ }
+ if main["thumbnailUrl"] != thumb1 {
+ t.Errorf("thumbnailUrl should equal image[0] (Google byte-equality requirement), got %v", main["thumbnailUrl"])
+ }
+}
+
+// Gallery in the media slot of a record-with-media embed should still
+// produce og:image / JSON-LD image[]. Quote-post URL still emits
+// alongside via isBasedOn.
+func TestBuildPostJSONLD_GalleryInRecordWithMedia(t *testing.T) {
+ thumb := "https://cdn.bsky.app/img/feed_thumbnail/plain/did:plc:alice/g@jpeg"
+ pv := makePostView("alice.bsky.social", "did:plc:alice", "abc123", "quote+gallery",
+ withRecordWithMediaGallery("bob.example.com", "did:plc:bob", "xyz", thumb))
+ out, err := buildPostJSONLD(pv, nil, "u", hideEmbedLabels, hideReplyLabels)
+ if err != nil {
+ t.Fatal(err)
+ }
+ main := unmarshalLD(t, out)["mainEntity"].(map[string]any)
+ imgs, ok := main["image"].([]any)
+ if !ok || len(imgs) != 1 || imgs[0] != thumb {
+ t.Errorf("expected single gallery thumb in image[], got %v", main["image"])
+ }
+ if main["thumbnailUrl"] != thumb {
+ t.Errorf("thumbnailUrl wrong: %v", main["thumbnailUrl"])
+ }
+ if main["isBasedOn"] != "https://bsky.app/profile/bob.example.com/post/xyz" {
+ t.Errorf("isBasedOn should still emit for record-with-media gallery, got %v", main["isBasedOn"])
+ }
+}
+
+// Forward-compat: nil items, unknown-variant union elements, and empty
+// Thumbnail strings must be skipped, not panic or leak as . Unknown variants are dropped silently
+// so older deploys keep working when new gallery item types ship.
+func TestExtractPostMedia_GallerySkipsUnknownItems(t *testing.T) {
+ thumb := "https://cdn.bsky.app/img/feed_thumbnail/plain/did:plc:alice/g@jpeg"
+ pv := makePostView("alice.bsky.social", "did:plc:alice", "abc123", "gallery")
+ pv.Embed = &appbsky.FeedDefs_PostView_Embed{
+ EmbedGallery_View: &appbsky.EmbedGallery_View{
+ Items: []*appbsky.EmbedGallery_View_Items_Elem{
+ nil,
+ {}, // empty union, no variant set
+ {EmbedGallery_ViewImage: &appbsky.EmbedGallery_ViewImage{Thumbnail: ""}}, // empty Thumbnail
+ {EmbedGallery_ViewImage: &appbsky.EmbedGallery_ViewImage{Thumbnail: thumb}},
+ },
+ },
+ }
+ got := extractPostMedia(pv, false)
+ if len(got) != 1 || got[0] != thumb {
+ t.Errorf("expected single thumb, got %v", got)
+ }
+
+ // All-nil / all-unknown gallery should produce no thumbs (not [""]).
+ pv.Embed = &appbsky.FeedDefs_PostView_Embed{
+ EmbedGallery_View: &appbsky.EmbedGallery_View{
+ Items: []*appbsky.EmbedGallery_View_Items_Elem{nil, {}},
+ },
+ }
+ if got := extractPostMedia(pv, false); got != nil {
+ t.Errorf("expected nil for empty/unknown-only gallery, got %v", got)
+ }
+}
+
func TestBuildPostJSONLD_WithVideo(t *testing.T) {
thumb := "https://cdn.bsky.app/img/video_thumbnail/plain/did:plc:alice/v@jpeg"
pv := makePostView("alice.bsky.social", "did:plc:alice", "abc123", "watch", withVideo(thumb))
@@ -361,6 +497,25 @@ func TestBuildPostJSONLD_HiddenEmbed(t *testing.T) {
}
}
+// Symmetric guard for the gallery extraction path. Functionally redundant
+// with the early-return at the top of extractPostMedia, but exists so the
+// hide-embed contract is asserted directly against the gallery branch -
+// catches anyone who later moves the embedHidden check inside an
+// embed-shape branch.
+func TestBuildPostJSONLD_HiddenEmbed_Gallery(t *testing.T) {
+ thumb := "https://cdn.bsky.app/img/g@jpeg"
+ pv := makePostView("alice.bsky.social", "did:plc:alice", "abc123", "nsfw",
+ withGallery(thumb), withSelfLabel("porn"))
+ out, _ := buildPostJSONLD(pv, nil, "u", hideEmbedLabels, hideReplyLabels)
+ main := unmarshalLD(t, out)["mainEntity"].(map[string]any)
+ if _, present := main["image"]; present {
+ t.Errorf("hidden-embed gallery post should not emit image")
+ }
+ if _, present := main["thumbnailUrl"]; present {
+ t.Errorf("hidden-embed gallery post should not emit thumbnailUrl")
+ }
+}
+
func TestBuildPostJSONLD_TextEscaping(t *testing.T) {
// Includes ", \, newline, , and a unicode char.
tricky := "hello \"world\" \\ <\\>\n 🎉"
diff --git a/bskyweb/cmd/bskyweb/render_test.go b/bskyweb/cmd/bskyweb/render_test.go
index f7c9edd677..30b27a53d5 100644
--- a/bskyweb/cmd/bskyweb/render_test.go
+++ b/bskyweb/cmd/bskyweb/render_test.go
@@ -98,6 +98,40 @@ func TestRenderPost_OGImageMatchesJSONLD(t *testing.T) {
}
}
+// Gallery posts must hit the same og:image / JSON-LD image[] byte-equality
+// contract that legacy images posts do. Regression guard for the
+// app.bsky.embed.gallery extraction path.
+func TestRenderPost_OGImageMatchesJSONLD_Gallery(t *testing.T) {
+ thumb1 := "https://cdn.bsky.app/img/feed_thumbnail/plain/did:plc:alice/g1@jpeg"
+ thumb2 := "https://cdn.bsky.app/img/feed_thumbnail/plain/did:plc:alice/g2@jpeg"
+ pv := makePostView("alice.bsky.social", "did:plc:alice", "abc123", "gallery", withGallery(thumb1, thumb2))
+ thumbs := extractPostMedia(pv, false)
+ ld, _ := buildPostJSONLD(pv, nil, "https://bsky.app/profile/alice.bsky.social/post/abc123", hideEmbedLabels, hideReplyLabels)
+ html := renderTemplate(t, "post.html", pongo2.Context{
+ "postView": pv,
+ "requestURI": "https://bsky.app/profile/alice.bsky.social/post/abc123",
+ "canonicalURL": "https://bsky.app/profile/alice.bsky.social/post/abc123",
+ "postJSONLD": ld,
+ "imgThumbUrls": thumbs,
+ })
+
+ if !strings.Contains(html, ``) {
+ t.Errorf("og:image[0] not found in rendered HTML for gallery post")
+ }
+ if !strings.Contains(html, ``) {
+ t.Errorf("og:image[1] not found in rendered HTML for gallery post")
+ }
+ body := extractJSONLD(t, html)
+ var parsed map[string]any
+ _ = json.Unmarshal([]byte(body), &parsed)
+ main := parsed["mainEntity"].(map[string]any)
+ imgs := main["image"].([]any)
+ if len(imgs) != 2 || imgs[0] != thumb1 || main["thumbnailUrl"] != thumb1 {
+ t.Errorf("JSON-LD image strings drifted from og:image for gallery; image=%v thumbnailUrl=%v",
+ imgs, main["thumbnailUrl"])
+ }
+}
+
func TestRenderPost_FallsBackToCanonicalizeFilter(t *testing.T) {
// Without canonicalURL, the template falls back to requestURI|canonicalize_url.
pv := makePostView("alice.bsky.social", "did:plc:alice", "abc123", "hi")
diff --git a/bskyweb/go.mod b/bskyweb/go.mod
index 50ab9a162a..e25349e947 100644
--- a/bskyweb/go.mod
+++ b/bskyweb/go.mod
@@ -3,7 +3,7 @@ module github.com/bluesky-social/social-app/bskyweb
go 1.26
require (
- github.com/bluesky-social/indigo v0.0.0-20260529183052-5368f55344e0
+ github.com/bluesky-social/indigo v0.0.0-20260605210604-af2fec94f34c
github.com/flosch/pongo2/v6 v6.0.0
github.com/ipfs/go-log v1.0.5
github.com/joho/godotenv v1.5.1
diff --git a/bskyweb/go.sum b/bskyweb/go.sum
index 235e37f521..d9b4b0141a 100644
--- a/bskyweb/go.sum
+++ b/bskyweb/go.sum
@@ -2,8 +2,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
-github.com/bluesky-social/indigo v0.0.0-20260529183052-5368f55344e0 h1:eijBaF59A5c+kPqufH7YO1GOqDMkyUhtM9P9aAWtfJY=
-github.com/bluesky-social/indigo v0.0.0-20260529183052-5368f55344e0/go.mod h1:JqQkz8lrOI6YZivP38GHmtVOTtzsNToITKj1gMpU5Jo=
+github.com/bluesky-social/indigo v0.0.0-20260605210604-af2fec94f34c h1:Jr82+1HUmwwZzDpt/eeU4sieya27iXjuPMdXZkOXoBc=
+github.com/bluesky-social/indigo v0.0.0-20260605210604-af2fec94f34c/go.mod h1:JqQkz8lrOI6YZivP38GHmtVOTtzsNToITKj1gMpU5Jo=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
diff --git a/eslint-suppressions.json b/eslint-suppressions.json
index 304fd4a065..e1e981fd08 100644
--- a/eslint-suppressions.json
+++ b/eslint-suppressions.json
@@ -1,4 +1,113 @@
{
+ "modules/bottom-sheet/src/BottomSheetNativeComponent.tsx": {
+ "@typescript-eslint/no-explicit-any": {
+ "count": 2
+ },
+ "@typescript-eslint/no-unsafe-call": {
+ "count": 2
+ },
+ "@typescript-eslint/no-unsafe-member-access": {
+ "count": 2
+ }
+ },
+ "modules/bottom-sheet/src/BottomSheetPortal.tsx": {
+ "no-restricted-imports": {
+ "count": 1
+ }
+ },
+ "modules/bottom-sheet/src/lib/Portal.tsx": {
+ "no-restricted-imports": {
+ "count": 1
+ }
+ },
+ "modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider.tsx": {
+ "@typescript-eslint/no-floating-promises": {
+ "count": 1
+ },
+ "no-restricted-imports": {
+ "count": 1
+ }
+ },
+ "modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandlerModule.web.ts": {
+ "@typescript-eslint/require-await": {
+ "count": 4
+ }
+ },
+ "modules/expo-bluesky-gif-view/src/GifView.tsx": {
+ "@typescript-eslint/no-explicit-any": {
+ "count": 2
+ },
+ "@typescript-eslint/no-unsafe-call": {
+ "count": 4
+ },
+ "@typescript-eslint/no-unsafe-member-access": {
+ "count": 4
+ },
+ "no-restricted-imports": {
+ "count": 1
+ }
+ },
+ "modules/expo-bluesky-gif-view/src/GifView.web.tsx": {
+ "@typescript-eslint/no-floating-promises": {
+ "count": 2
+ },
+ "@typescript-eslint/require-await": {
+ "count": 2
+ }
+ },
+ "modules/expo-bluesky-swiss-army/src/PlatformInfo/index.native.ts": {
+ "@typescript-eslint/no-unsafe-call": {
+ "count": 3
+ },
+ "@typescript-eslint/no-unsafe-member-access": {
+ "count": 3
+ }
+ },
+ "modules/expo-bluesky-swiss-army/src/Referrer/index.android.ts": {
+ "@typescript-eslint/no-unsafe-call": {
+ "count": 2
+ },
+ "@typescript-eslint/no-unsafe-member-access": {
+ "count": 2
+ }
+ },
+ "modules/expo-bluesky-swiss-army/src/SharedPrefs/index.native.ts": {
+ "@typescript-eslint/no-explicit-any": {
+ "count": 1
+ },
+ "@typescript-eslint/no-unsafe-call": {
+ "count": 9
+ },
+ "@typescript-eslint/no-unsafe-member-access": {
+ "count": 9
+ }
+ },
+ "modules/expo-bluesky-swiss-army/src/VisibilityView/index.native.tsx": {
+ "@typescript-eslint/no-unsafe-call": {
+ "count": 1
+ },
+ "@typescript-eslint/no-unsafe-member-access": {
+ "count": 1
+ },
+ "no-restricted-imports": {
+ "count": 1
+ }
+ },
+ "modules/expo-bluesky-swiss-army/src/VisibilityView/index.tsx": {
+ "@typescript-eslint/require-await": {
+ "count": 1
+ }
+ },
+ "modules/expo-bluesky-swiss-army/src/VisibilityView/types.ts": {
+ "no-restricted-imports": {
+ "count": 1
+ }
+ },
+ "modules/expo-emoji-picker/src/EmojiPickerView.tsx": {
+ "no-restricted-imports": {
+ "count": 1
+ }
+ },
"src/Navigation.tsx": {
"@typescript-eslint/no-floating-promises": {
"count": 1
@@ -19,11 +128,6 @@
"count": 1
}
},
- "src/ageAssurance/util.ts": {
- "@typescript-eslint/no-floating-promises": {
- "count": 1
- }
- },
"src/alf/util/flatten.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 1
@@ -603,11 +707,6 @@
"count": 1
}
},
- "src/components/dms/MessageItem.tsx": {
- "@typescript-eslint/no-misused-promises": {
- "count": 1
- }
- },
"src/components/forms/DateField/index.web.tsx": {
"@typescript-eslint/no-explicit-any": {
"count": 1
@@ -650,14 +749,6 @@
"count": 2
}
},
- "src/components/hooks/useFullscreen.ts": {
- "@typescript-eslint/no-floating-promises": {
- "count": 2
- },
- "react-hooks/set-state-in-effect": {
- "count": 1
- }
- },
"src/components/hooks/useLandingEntry.native.ts": {
"react-hooks/set-state-in-effect": {
"count": 1
@@ -1903,12 +1994,6 @@
"src/state/session/agent.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 1
- },
- "@typescript-eslint/no-floating-promises": {
- "count": 1
- },
- "@typescript-eslint/require-await": {
- "count": 1
}
},
"src/state/shell/color-mode.tsx": {
@@ -2271,23 +2356,6 @@
"count": 2
}
},
- "src/view/com/profile/ProfileMenu.tsx": {
- "@typescript-eslint/no-explicit-any": {
- "count": 6
- },
- "@typescript-eslint/no-floating-promises": {
- "count": 4
- },
- "@typescript-eslint/no-misused-promises": {
- "count": 3
- },
- "@typescript-eslint/no-unsafe-call": {
- "count": 6
- },
- "@typescript-eslint/no-unsafe-member-access": {
- "count": 12
- }
- },
"src/view/com/testing/TestCtrls.e2e.tsx": {
"@typescript-eslint/no-explicit-any": {
"count": 1
diff --git a/modules/expo-bluesky-gif-view/src/GifView.web.tsx b/modules/expo-bluesky-gif-view/src/GifView.web.tsx
index c9f439bffe..e51b9bf9fc 100644
--- a/modules/expo-bluesky-gif-view/src/GifView.web.tsx
+++ b/modules/expo-bluesky-gif-view/src/GifView.web.tsx
@@ -67,7 +67,18 @@ export class GifView extends PureComponent {
}
async playAsync(): Promise {
- this.videoPlayerRef.current?.play()
+ try {
+ await this.videoPlayerRef.current?.play()
+ } catch (err) {
+ // `play()` rejects with a NotAllowedError when the browser blocks
+ // playback (e.g. Safari low-power mode or autoplay policy). This is
+ // expected and benign - the GIF simply stays paused - so swallow it
+ // rather than letting it surface as an unhandled rejection.
+ if (err instanceof DOMException && err.name === 'NotAllowedError') {
+ return
+ }
+ throw err
+ }
}
async pauseAsync(): Promise {
diff --git a/modules/expo-bluesky-swiss-army/src/VisibilityView/index.native.tsx b/modules/expo-bluesky-swiss-army/src/VisibilityView/index.native.tsx
index 9d0e8cf220..aba96c6d4b 100644
--- a/modules/expo-bluesky-swiss-army/src/VisibilityView/index.native.tsx
+++ b/modules/expo-bluesky-swiss-army/src/VisibilityView/index.native.tsx
@@ -1,12 +1,12 @@
import React from 'react'
-import {StyleProp, ViewStyle} from 'react-native'
+import {type StyleProp, type ViewStyle} from 'react-native'
import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core'
-import {VisibilityViewProps} from './types'
+import {type VisibilityViewProps} from './types'
const NativeView: React.ComponentType<{
onChangeStatus: (e: {nativeEvent: {isActive: boolean}}) => void
children: React.ReactNode
- enabled: Boolean
+ enabled: boolean
style: StyleProp
}> = requireNativeViewManager('ExpoBlueskyVisibilityView')
diff --git a/package.json b/package.json
index e054aca94a..84324a70de 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "bsky.app",
- "version": "1.123.0",
+ "version": "1.124.0",
"private": true,
"engines": {
"node": ">=24.15.0"
@@ -59,7 +59,7 @@
"test-watch": "NODE_ENV=test jest --watchAll",
"test-ci": "NODE_ENV=test jest --ci --forceExit --reporters=default --reporters=jest-junit",
"test-coverage": "NODE_ENV=test jest --coverage",
- "lint": "eslint --cache --quiet src",
+ "lint": "eslint --cache --quiet src modules",
"lint-native": "swiftlint ./modules && ktlint ./modules",
"lint-native:fix": "swiftlint --fix ./modules && ktlint --format ./modules",
"typecheck": "tsgo --project ./tsconfig.check.json",
@@ -93,7 +93,7 @@
"prettier": "prettier --check ."
},
"dependencies": {
- "@atproto/api": "0.20.9",
+ "@atproto/api": "0.20.11",
"@atproto/syntax": "0.6.1",
"@bitdrift/react-native": "^0.6.8",
"@braintree/sanitize-url": "^6.0.2",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b2f65bf409..9d21e055cd 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -242,8 +242,8 @@ importers:
.:
dependencies:
'@atproto/api':
- specifier: 0.20.9
- version: 0.20.9
+ specifier: 0.20.11
+ version: 0.20.11
'@atproto/syntax':
specifier: 0.6.1
version: 0.6.1
@@ -877,8 +877,8 @@ packages:
graphql:
optional: true
- '@atproto/api@0.20.9':
- resolution: {integrity: sha512-Yuw7Ewn+yMJZ8GskbuvI3lKPW65rsXic1xjFA2Dpq6H8WjVYs6xNZ31bkwtTYDDwjKIZcJmAVbAVgdfjo4T9iw==}
+ '@atproto/api@0.20.11':
+ resolution: {integrity: sha512-1NoVJpBDAdotxo1iMZdMd75JstpdKWgBYOnxfVD4m+52bRjgU4cFg3EOGNUognZntFgL/bIHyEgyN7SJWVf6Ig==}
engines: {node: '>=22'}
'@atproto/common-web@0.5.0':
@@ -9493,7 +9493,7 @@ snapshots:
'@0no-co/graphql.web@1.2.0': {}
- '@atproto/api@0.20.9':
+ '@atproto/api@0.20.11':
dependencies:
'@atproto/common-web': 0.5.0
'@atproto/lexicon': 0.7.1
diff --git a/src/ageAssurance/const.ts b/src/ageAssurance/const.ts
new file mode 100644
index 0000000000..bd79353b51
--- /dev/null
+++ b/src/ageAssurance/const.ts
@@ -0,0 +1,28 @@
+import {
+ ageAssuranceRuleIDs as ids,
+ type AppBskyAgeassuranceDefs,
+} from '@atproto/api'
+
+import {AgeAssuranceAccess} from '#/ageAssurance/types'
+
+/**
+ * Minimum age required to access the app at all.
+ */
+export const MIN_ACCESS_AGE = 13
+
+export const FALLBACK_REGION_CONFIG: AppBskyAgeassuranceDefs.ConfigRegion = {
+ countryCode: '*',
+ regionCode: undefined,
+ minAccessAge: MIN_ACCESS_AGE,
+ rules: [
+ {
+ $type: ids.IfDeclaredOverAge,
+ age: MIN_ACCESS_AGE,
+ access: AgeAssuranceAccess.Full,
+ },
+ {
+ $type: ids.Default,
+ access: AgeAssuranceAccess.None,
+ },
+ ],
+}
diff --git a/src/ageAssurance/index.tsx b/src/ageAssurance/index.tsx
index 2b10c93a5d..20bcc2bdb8 100644
--- a/src/ageAssurance/index.tsx
+++ b/src/ageAssurance/index.tsx
@@ -1,6 +1,7 @@
import {createContext, useCallback, useContext, useMemo} from 'react'
import {useGetAndRegisterPushToken} from '#/lib/notifications/notifications'
+import {restrictChatSettings} from '#/state/queries/messages/restrictChatSettings'
import {useAgent} from '#/state/session'
import {Provider as RedirectOverlayProvider} from '#/ageAssurance/components/RedirectOverlay'
import {
@@ -20,7 +21,6 @@ import {
} from '#/ageAssurance/types'
import {
computeAgeAssuranceFlags,
- maybeRestrictChatSettings,
useAgeAssuranceRegionConfigWithFallback,
} from '#/ageAssurance/util'
@@ -32,7 +32,6 @@ export {
usePatchServerState as usePatchAgeAssuranceServerState,
} from '#/ageAssurance/data'
export {logger} from '#/ageAssurance/logger'
-export {MIN_ACCESS_AGE} from '#/ageAssurance/util'
const AgeAssuranceStateContext = createContext<{
Access: typeof AgeAssuranceAccess
@@ -48,8 +47,10 @@ const AgeAssuranceStateContext = createContext<{
access: AgeAssuranceAccess.Full,
},
flags: {
+ isAgeRestricted: false,
adultContentDisabled: false,
chatDisabled: false,
+ groupChatDisabled: false,
isDeclaredUnderAdultAge: false,
isOverRegionMinAccessAge: false,
isOverAppMinAccessAge: false,
@@ -84,13 +85,25 @@ function InnerProvider({children}: {children: React.ReactNode}) {
const handleAccessUpdate = useCallback(
(s: AgeAssuranceState) => {
- const isAgeRestricted = s.access !== AgeAssuranceAccess.Full
- if (isAgeRestricted) {
- void getAndRegisterPushToken({isAgeRestricted})
- maybeRestrictChatSettings({agent})
+ const flags = computeAgeAssuranceFlags({
+ state: s,
+ regionConfig,
+ metadata,
+ })
+ if (flags.isAgeRestricted) {
+ void getAndRegisterPushToken({
+ isAgeRestricted: true,
+ })
+ }
+ if (flags.chatDisabled || flags.groupChatDisabled) {
+ void restrictChatSettings({
+ agent,
+ restrictIncoming: flags.chatDisabled,
+ restrictGroupInvites: flags.groupChatDisabled,
+ })
}
},
- [agent, getAndRegisterPushToken],
+ [agent, getAndRegisterPushToken, regionConfig, metadata],
)
useOnAgeAssuranceAccessUpdate(handleAccessUpdate)
diff --git a/src/ageAssurance/types.ts b/src/ageAssurance/types.ts
index 12473bc2e6..21abff89f0 100644
--- a/src/ageAssurance/types.ts
+++ b/src/ageAssurance/types.ts
@@ -30,8 +30,10 @@ export type AgeAssuranceState = {
}
export type AgeAssuranceFlags = {
+ isAgeRestricted: boolean
adultContentDisabled: boolean
chatDisabled: boolean
+ groupChatDisabled: boolean
isDeclaredUnderAdultAge: boolean
isOverRegionMinAccessAge: boolean
isOverAppMinAccessAge: boolean
diff --git a/src/ageAssurance/util.ts b/src/ageAssurance/util.ts
index b0e601d6c7..0d493f1130 100644
--- a/src/ageAssurance/util.ts
+++ b/src/ageAssurance/util.ts
@@ -1,20 +1,14 @@
import {useMemo} from 'react'
import {
- ageAssuranceRuleIDs as ids,
type AppBskyAgeassuranceDefs,
- type AtpAgent,
getAgeAssuranceRegionConfig,
type ModerationPrefs,
} from '@atproto/api'
import {getAge} from '#/lib/strings/time'
-import {restrictChatSettings} from '#/state/queries/messages/restrictChatSettings'
-import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/moderation'
-import {
- getDidFromAgentSession,
- getOtherRequiredDataFromCache,
- useAgeAssuranceServerDataContext,
-} from '#/ageAssurance/data'
+import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/const'
+import {FALLBACK_REGION_CONFIG, MIN_ACCESS_AGE} from '#/ageAssurance/const'
+import {useAgeAssuranceServerDataContext} from '#/ageAssurance/data'
import {
AgeAssuranceAccess,
type AgeAssuranceFlags,
@@ -23,24 +17,6 @@ import {
} from '#/ageAssurance/types'
import {type Geolocation, useGeolocation} from '#/geolocation'
-export const MIN_ACCESS_AGE = 13
-const FALLBACK_REGION_CONFIG: AppBskyAgeassuranceDefs.ConfigRegion = {
- countryCode: '*',
- regionCode: undefined,
- minAccessAge: MIN_ACCESS_AGE,
- rules: [
- {
- $type: ids.IfDeclaredOverAge,
- age: MIN_ACCESS_AGE,
- access: AgeAssuranceAccess.Full,
- },
- {
- $type: ids.Default,
- access: AgeAssuranceAccess.None,
- },
- ],
-}
-
/**
* Get age assurance region config based on geolocation, with fallback to
* app defaults if no region config is found.
@@ -121,19 +97,6 @@ export const makeAgeRestrictedModerationPrefs = (
labels: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES,
})
-/**
- * Checks our cache of the actor's chat declaration record, and if it's not
- * already restricted, restricts it.
- */
-export function maybeRestrictChatSettings({agent}: {agent: AtpAgent}) {
- const did = getDidFromAgentSession(agent)
- if (!did) return
- const data = getOtherRequiredDataFromCache({did})
- // ...update the chat setting record if allowIncoming is not already 'none'.
- if (data?.actorDeclaration?.allowIncoming === 'none') return
- restrictChatSettings({agent, did})
-}
-
export function computeAgeAssuranceFlags({
state,
regionConfig,
@@ -143,10 +106,12 @@ export function computeAgeAssuranceFlags({
regionConfig: AppBskyAgeassuranceDefs.ConfigRegion
metadata?: AgeAssuranceMetadata
}): AgeAssuranceFlags {
- const chatDisabled = state.access !== AgeAssuranceAccess.Full
+ const isAgeRestricted = state.access !== AgeAssuranceAccess.Full
+ const chatDisabled = isAgeRestricted
const isDeclaredUnderAdultAge = metadata?.declaredAge
? metadata.declaredAge < 18
: true
+ const groupChatDisabled = chatDisabled || isDeclaredUnderAdultAge
const isOverRegionMinAccessAge = metadata?.declaredAge
? metadata.declaredAge >= regionConfig.minAccessAge
: false
@@ -157,8 +122,10 @@ export function computeAgeAssuranceFlags({
state.access !== AgeAssuranceAccess.Full || isDeclaredUnderAdultAge
return {
+ isAgeRestricted,
adultContentDisabled,
chatDisabled,
+ groupChatDisabled,
isDeclaredUnderAdultAge,
isOverRegionMinAccessAge,
isOverAppMinAccessAge,
diff --git a/src/components/Lightbox/chrome/ImageMenu.tsx b/src/components/Lightbox/chrome/ImageMenu.tsx
index 8daa9a5b84..92b5c47749 100644
--- a/src/components/Lightbox/chrome/ImageMenu.tsx
+++ b/src/components/Lightbox/chrome/ImageMenu.tsx
@@ -10,7 +10,7 @@ import Animated, {
} from 'react-native-reanimated'
import {useLingui} from '@lingui/react/macro'
-import {android, atoms as a, ios} from '#/alf'
+import {atoms as a} from '#/alf'
import {ArrowShareRight_Stroke2_Corner2_Rounded as ShareIcon} from '#/components/icons/ArrowShareRight'
import {type Props as IconProps} from '#/components/icons/common'
import {DotGrid3x1_Stroke2_Corner0_Rounded as DotsIcon} from '#/components/icons/DotGrid'
@@ -25,7 +25,6 @@ type Props = {
type Anchor = {x: number; y: number; width: number; height: number}
-const MENU_WIDTH = 160
const GAP = 6
const CARD_BG = '#000000'
const CARD_BORDER = '#232e3e'
@@ -124,9 +123,8 @@ function MenuCard({
+
onOpen?: () => void
}) {
- const resolvedCode = code ?? preview?.code
+ const resolvedCode =
+ code ?? (isKnownJoinLinkPreview(preview) ? preview.code : undefined)
if (!resolvedCode) return null
return (
@@ -73,7 +78,7 @@ export function JoinRequestEmbedBody({
)
}
- if (!preview) {
+ if (!ChatBskyGroupDefs.isJoinLinkPreviewView(preview)) {
return (
{
+ setActive: useCallback(() => {
setActiveView(id)
- },
+ }, [setActiveView, id]),
currentActiveView: activeViewId,
- sendPosition: (y: number) => sendViewPosition(id, y),
+ sendPosition: useCallback(
+ (y: number) => {
+ sendViewPosition(id, y)
+ },
+ [sendViewPosition, id],
+ ),
}
}
diff --git a/src/components/PostControls/PostMenu/PostMenuItems.tsx b/src/components/PostControls/PostMenu/PostMenuItems.tsx
index 257c1dcb41..f34c9a473c 100644
--- a/src/components/PostControls/PostMenu/PostMenuItems.tsx
+++ b/src/components/PostControls/PostMenu/PostMenuItems.tsx
@@ -88,6 +88,7 @@ import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
import {Loader} from '#/components/Loader'
import * as Menu from '#/components/Menu'
+import {BlockDialog} from '#/components/moderation/BlockDialog'
import {
ReportDialog,
useReportDialogControl,
@@ -845,13 +846,10 @@ let PostMenuItems = ({
onConfirm={() => void onToggleReplyVisibility()}
confirmButtonCta={l`Yes, hide`}
/>
- void onBlockAuthor()}
- confirmButtonCta={l`Block`}
- confirmButtonColor="negative"
+ profile={postAuthor}
+ onBlock={onBlockAuthor}
/>
>
)
diff --git a/src/components/dms/ActionsWrapper.web.tsx b/src/components/dms/ActionsWrapper.web.tsx
index b89712bd1f..9ea12d7f9e 100644
--- a/src/components/dms/ActionsWrapper.web.tsx
+++ b/src/components/dms/ActionsWrapper.web.tsx
@@ -1,8 +1,10 @@
import {useCallback, useRef, useState} from 'react'
import {Pressable, View} from 'react-native'
import {type ChatBskyConvoDefs, type ModerationOpts} from '@atproto/api'
+import {plural} from '@lingui/core/macro'
import {useLingui} from '@lingui/react/macro'
+import {EMOJI_REACTION_LIMIT} from '#/lib/constants'
import {useMaybeProfileShadow} from '#/state/cache/profile-shadow'
import {useConvoActive} from '#/state/messages/convo'
import {useSession} from '#/state/session'
@@ -72,7 +74,18 @@ export function ActionsWrapper({
.removeReaction(message.id, emoji)
.catch(() => Toast.show(l`Failed to remove emoji reaction`))
} else {
- if (hasReachedReactionLimit(message, currentAccount?.did)) return
+ if (hasReachedReactionLimit(message, currentAccount?.did)) {
+ Toast.show(
+ l`You cannot add more than ${plural(EMOJI_REACTION_LIMIT, {
+ one: '# emoji reaction',
+ other: '# emoji reactions',
+ })}`,
+ {
+ type: 'info',
+ },
+ )
+ return
+ }
convo.addReaction(message.id, emoji).catch(() =>
Toast.show(l`Failed to add emoji reaction`, {
type: 'error',
diff --git a/src/components/dms/AddMembersFlow.tsx b/src/components/dms/AddMembersFlow.tsx
index ae2945d2ce..0c0b171e02 100644
--- a/src/components/dms/AddMembersFlow.tsx
+++ b/src/components/dms/AddMembersFlow.tsx
@@ -209,6 +209,7 @@ export function AddMembersFlow({
if (follows) {
for (const page of follows.pages) {
for (const profile of page.follows) {
+ if (!canBeAddedToGroup(profile)) continue
_items.push({
type: 'profile',
key: profile.did,
@@ -216,12 +217,6 @@ export function AddMembersFlow({
})
}
}
-
- _items.sort(item => {
- return item.type === 'profile' && canBeAddedToGroup(item.profile)
- ? -1
- : 1
- })
} else {
for (let i = 0; i < 10; i++) {
_items.push({type: 'placeholder', key: i + ''})
diff --git a/src/components/dms/ChatInvite/Card.tsx b/src/components/dms/ChatInvite/Card.tsx
index bccf23446c..33183f82cb 100644
--- a/src/components/dms/ChatInvite/Card.tsx
+++ b/src/components/dms/ChatInvite/Card.tsx
@@ -1,4 +1,5 @@
import {View} from 'react-native'
+import {ChatBskyGroupDefs} from '@atproto/api'
import {Plural, Trans} from '@lingui/react/macro'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
@@ -6,7 +7,7 @@ import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeHandle} from '#/lib/strings/handles'
import {atoms as a, useTheme} from '#/alf'
import {AvatarBubbles} from '#/components/AvatarBubbles'
-import {SimpleInlineLinkText} from '#/components/Link'
+import {InlineLinkText} from '#/components/Link'
import {ProfileBadges} from '#/components/ProfileBadges'
import {Text} from '#/components/Typography'
import {useChatInvite} from './Context'
@@ -20,7 +21,7 @@ export function Card({size}: {size: 'large' | 'small'}) {
const t = useTheme()
const {preview, hasFixedHeight} = useChatInvite()
- if (!preview) return null
+ if (!ChatBskyGroupDefs.isJoinLinkPreviewView(preview)) return null
const ownerDisplayName = createSanitizedDisplayName(preview.owner)
const ownerHandle = sanitizeHandle(preview.owner.handle, '@')
@@ -77,12 +78,12 @@ export function Card({size}: {size: 'large' | 'small'}) {
allowFontScaling={!hasFixedHeight}>
By{' '}
-
{ownerDisplayName}
-
+
= JoinIcon
let label = preview.requireApproval ? l`Request to join` : l`Join`
let color: ButtonColor = 'primary'
- if (preview.enabledStatus !== 'enabled') {
- canJoin = false
- icon = WarningIcon
- label = l`Chat invite link no longer available`
- color = 'secondary'
- } else if (preview.memberCount >= preview.memberLimit) {
+ if (preview.memberCount >= preview.memberLimit) {
canJoin = false
icon = HandIcon
label = l`This chat is full`
diff --git a/src/components/dms/InitiateChatFlow.tsx b/src/components/dms/InitiateChatFlow.tsx
index 161b0cf722..5eb2ec3705 100644
--- a/src/components/dms/InitiateChatFlow.tsx
+++ b/src/components/dms/InitiateChatFlow.tsx
@@ -36,6 +36,7 @@ import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Ti
import * as ProfileCard from '#/components/ProfileCard'
import * as Prompt from '#/components/Prompt'
import {Text} from '#/components/Typography'
+import {useAgeAssurance} from '#/ageAssurance'
import {IS_NATIVE, IS_WEB} from '#/env'
import type * as bsky from '#/types/bsky'
import {ChatProfileTabs} from './ChatProfileTabs'
@@ -209,6 +210,7 @@ export function InitiateChatFlow({
const [footerHeight, setFooterHeight] = useState(0)
const listRef = useRef(null)
const {currentAccount} = useSession()
+ const aa = useAgeAssurance()
const inputRef = useRef(null)
const accountTooNewPromptControl = Dialog.useDialogControl()
@@ -301,6 +303,7 @@ export function InitiateChatFlow({
if (follows) {
for (const page of follows.pages) {
for (const profile of page.follows) {
+ if (!checker(profile)) continue
_items.push({
type: 'profile',
key: profile.did,
@@ -308,10 +311,6 @@ export function InitiateChatFlow({
})
}
}
-
- _items = _items.sort(item => {
- return item.type === 'profile' && checker(item.profile) ? -1 : 1
- })
} else {
_items.push(...placeholders)
}
@@ -330,7 +329,11 @@ export function InitiateChatFlow({
})
}
- if (chatState === ChatState.NEW_CHAT && searchText === '') {
+ if (
+ chatState === ChatState.NEW_CHAT &&
+ searchText === '' &&
+ !aa.flags.groupChatDisabled
+ ) {
_items.unshift({type: 'newGroupChat', key: 'newGroupChat'})
}
@@ -344,6 +347,7 @@ export function InitiateChatFlow({
results,
currentAccount?.did,
follows,
+ aa.flags.groupChatDisabled,
])
if (searchText && !isFetching && !items.length && !isError) {
@@ -510,7 +514,6 @@ export function InitiateChatFlow({
a.relative,
a.align_center,
a.justify_between,
- web(a.pb_lg),
]}>
{IS_NATIVE ? (
) : showButton ? (
-
{statusNudgeActive && }
>
)
@@ -278,9 +282,7 @@ let ProfileMenu = ({
{
if (showLoggedOutWarning) {
loggedOutWarningPromptControl.open()
@@ -301,7 +303,7 @@ let ProfileMenu = ({
Search posts
@@ -320,14 +322,12 @@ let ProfileMenu = ({
void onPressUnfollowAccount()
+ : () => void onPressFollowAccount()
}>
{isFollowing ? (
@@ -343,7 +343,7 @@ let ProfileMenu = ({
)}
Add to starter packs
@@ -352,7 +352,7 @@ let ProfileMenu = ({
Add to lists
@@ -364,10 +364,10 @@ let ProfileMenu = ({
testID="profileHeaderDropdownListAddRemoveBtn"
label={
status.isDisabled
- ? _(msg`Go live (disabled)`)
+ ? l`Go live (disabled)`
: status.isActive
- ? _(msg`Edit live status`)
- : _(msg`Go live`)
+ ? l`Edit live status`
+ : l`Go live`
}
onPress={() => {
if (status.isDisabled) {
@@ -418,7 +418,7 @@ let ProfileMenu = ({
(verification.viewer.hasIssuedVerification ? (
verificationRemovePromptControl.open()}>
Remove verification
@@ -428,7 +428,7 @@ let ProfileMenu = ({
) : (
verificationCreatePromptControl.open()}>
Verify account
@@ -444,10 +444,10 @@ let ProfileMenu = ({
testID="profileHeaderDropdownMuteBtn"
label={
profile.viewer?.muted
- ? _(msg`Unmute account`)
- : _(msg`Mute account`)
+ ? l`Unmute account`
+ : l`Mute account`
}
- onPress={onPressMuteAccount}>
+ onPress={() => void onPressMuteAccount()}>
{profile.viewer?.muted ? (
Unmute account
@@ -464,9 +464,9 @@ let ProfileMenu = ({
blockPromptControl.open()}>
@@ -485,7 +485,7 @@ let ProfileMenu = ({
)}
Report account
@@ -503,7 +503,7 @@ let ProfileMenu = ({
Copy at:// URI
@@ -512,7 +512,7 @@ let ProfileMenu = ({
Copy DID
@@ -524,12 +524,10 @@ let ProfileMenu = ({
) : null}
-
-
-
-
-
-
-
{status.isDisabled ? (