diff --git a/.env.example b/.env.example
index e18cda4e55..4aefa33202 100644
--- a/.env.example
+++ b/.env.example
@@ -16,6 +16,9 @@ EXPO_PUBLIC_LOG_LEVEL=debug
# Enable debug logs for specific logger instances
EXPO_PUBLIC_LOG_DEBUG=session
+# Bluesky appview DID
+EXPO_PUBLIC_BLUESKY_PROXY_DID=
+
# Chat service DID
EXPO_PUBLIC_CHAT_PROXY_DID=
diff --git a/.eslintrc.js b/.eslintrc.js
index 59c1b4eb4f..04914bde64 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -34,6 +34,7 @@ module.exports = {
'P',
'Admonition',
'Admonition.Admonition',
+ 'Toast.Action',
'AgeAssuranceAdmonition',
'Span',
],
@@ -110,6 +111,8 @@ module.exports = {
'bskyembed',
'src/locale/locales/_build/',
'src/locale/locales/**/*.js',
+ '*.e2e.ts',
+ '*.e2e.tsx',
],
settings: {
componentWrapperFunctions: ['observer'],
diff --git a/__e2e__/flows/composer-self-label.yml b/__e2e__/flows/composer-self-label.yml
index b91ff7507f..4539e1e04e 100644
--- a/__e2e__/flows/composer-self-label.yml
+++ b/__e2e__/flows/composer-self-label.yml
@@ -3,20 +3,26 @@ appId: xyz.blueskyweb.app
- runScript:
file: ../setupServer.js
env:
- SERVER_PATH: ?users
+ SERVER_PATH: ?users
- runFlow:
file: ../setupApp.yml
- tapOn:
id: "e2eSignInAlice"
+- extendedWaitUntil:
+ visible:
+ id: "viewHeaderHomeFeedPrefsBtn"
# Post an image with the porn label
- assertVisible:
- id: "composeFAB"
+ id: "composeFAB"
- tapOn:
id: "composeFAB"
- inputText: "Post with an image"
- tapOn:
- id: "openGalleryBtn"
+ id: "openMediaBtn"
+- extendedWaitUntil:
+ visible:
+ id: "selectedPhotosView"
- tapOn: "Content warnings"
- tapOn: "Porn"
- tapOn:
diff --git a/__e2e__/flows/composer.yml b/__e2e__/flows/composer.yml
index d6cf7dff57..62f195de75 100644
--- a/__e2e__/flows/composer.yml
+++ b/__e2e__/flows/composer.yml
@@ -3,13 +3,17 @@ appId: xyz.blueskyweb.app
- runScript:
file: ../setupServer.js
env:
- SERVER_PATH: ?users
+ SERVER_PATH: ?users
- runFlow:
file: ../setupApp.yml
- tapOn:
id: "e2eSignInAlice"
+- extendedWaitUntil:
+ visible:
+ id: "viewHeaderHomeFeedPrefsBtn"
+
- assertVisible:
- id: "composeFAB"
+ id: "composeFAB"
- tapOn:
id: "composeFAB"
- inputText: "Post text only"
@@ -21,7 +25,10 @@ appId: xyz.blueskyweb.app
id: "composeFAB"
- inputText: "Post with an image"
- tapOn:
- id: "openGalleryBtn"
+ id: "openMediaBtn"
+- extendedWaitUntil:
+ visible:
+ id: "selectedPhotosView"
- tapOn:
id: "composerPublishBtn"
- assertVisible:
@@ -46,7 +53,10 @@ appId: xyz.blueskyweb.app
id: "replyBtn"
- inputText: "Reply with an image"
- tapOn:
- id: "openGalleryBtn"
+ id: "openMediaBtn"
+- extendedWaitUntil:
+ visible:
+ id: "selectedPhotosView"
- tapOn:
id: "composerPublishBtn"
- assertVisible:
@@ -73,7 +83,10 @@ appId: xyz.blueskyweb.app
id: "quoteBtn"
- inputText: "QP with an image"
- tapOn:
- id: "openGalleryBtn"
+ id: "openMediaBtn"
+- extendedWaitUntil:
+ visible:
+ id: "selectedPhotosView"
- tapOn:
id: "composerPublishBtn"
- assertVisible:
diff --git a/__e2e__/flows/create-account.yml b/__e2e__/flows/create-account.yml
index 4d49e9b6a8..1290c94bf0 100644
--- a/__e2e__/flows/create-account.yml
+++ b/__e2e__/flows/create-account.yml
@@ -32,6 +32,9 @@ appId: xyz.blueskyweb.app
text: "Not Now"
optional: true
- inputText: "e2e-test"
+- extendedWaitUntil:
+ visible:
+ id: "handleAvailableCheck"
- tapOn:
id: "nextBtn"
-
+- assertVisible: "Give your profile a face"
diff --git a/__e2e__/flows/curate-lists.yml b/__e2e__/flows/curate-lists.yml
index fdb71ae7eb..8fe6a2c44c 100644
--- a/__e2e__/flows/curate-lists.yml
+++ b/__e2e__/flows/curate-lists.yml
@@ -3,11 +3,14 @@ appId: xyz.blueskyweb.app
- runScript:
file: ../setupServer.js
env:
- SERVER_PATH: "?users&follows&posts"
+ SERVER_PATH: "?users&follows&posts"
- runFlow:
file: ../setupApp.yml
- tapOn:
id: "e2eSignInAlice"
+- extendedWaitUntil:
+ visible:
+ id: "viewHeaderHomeFeedPrefsBtn"
- tapOn:
label: "Create a curate list"
@@ -75,9 +78,8 @@ appId: xyz.blueskyweb.app
- tapOn:
id: "confirmBtn"
-- tapOn:
- label: "Create a new curatelist"
- id: "e2eGotoLists"
+- assertVisible:
+ id: "newUserListBtn"
- tapOn:
id: "newUserListBtn"
- assertVisible:
@@ -146,7 +148,7 @@ appId: xyz.blueskyweb.app
- tapOn:
id: "bottomBarSearchBtn"
-- tapOn: "Search for posts, users, or feeds"
+- tapOn: "Search for posts, users[,]? or feeds"
- inputText: "bob"
- tapOn:
id: "searchAutoCompleteResult-bob.test"
diff --git a/__e2e__/flows/mod-lists.yml b/__e2e__/flows/mod-lists.yml
index ef757c5b11..02d76b83ed 100644
--- a/__e2e__/flows/mod-lists.yml
+++ b/__e2e__/flows/mod-lists.yml
@@ -3,11 +3,14 @@ appId: xyz.blueskyweb.app
- runScript:
file: ../setupServer.js
env:
- SERVER_PATH: "?users&follows&labels"
+ SERVER_PATH: "?users&follows&labels"
- runFlow:
file: ../setupApp.yml
- tapOn:
id: "e2eSignInAlice"
+- extendedWaitUntil:
+ visible:
+ id: "viewHeaderHomeFeedPrefsBtn"
# create a modlist
- tapOn:
@@ -28,20 +31,17 @@ appId: xyz.blueskyweb.app
- assertVisible: "Muted Users"
- assertVisible: "Shhh"
-- tapOn:
- label: "Dropdown"
- point: "71%,9%"
-
+# DOES NOT WORK - THE BUTTON IS NOT ACCESSIBLE
+# IGNORING FOR NOW, FIX THE COMPONENTS IN THE NEXT RELEASE
+# BECAUSE THIS IS A LEGIT A11Y PROBLEM -sfn
+- tapOn: "Subscribe to this list"
- tapOn: "Mute accounts"
- tapOn: "Mute list"
- tapOn: "Unmute"
-- tapOn:
- label: "Dropdown"
- point: "71%,9%"
-
+- tapOn: "Subscribe to this list"
- tapOn: "Block accounts"
- tapOn: "Block list"
- tapOn: "Unblock"
- # the rest of the behaviors are tested in curate-lists.yml
+ # the rest of the behaviors are tested in curate-lists.yml
diff --git a/__e2e__/flows/onboarding-avatar-creator.yml b/__e2e__/flows/onboarding-avatar-creator.yml
index 3a20053ba5..b8f65b9c38 100644
--- a/__e2e__/flows/onboarding-avatar-creator.yml
+++ b/__e2e__/flows/onboarding-avatar-creator.yml
@@ -3,11 +3,15 @@ appId: xyz.blueskyweb.app
- runScript:
file: ../setupServer.js
env:
- SERVER_PATH: "?users"
+ SERVER_PATH: "?users"
- runFlow:
file: ../setupApp.yml
- tapOn:
id: "e2eSignInAlice"
+- extendedWaitUntil:
+ visible:
+ id: "viewHeaderHomeFeedPrefsBtn"
+
- tapOn:
id: "e2eStartOnboarding"
- tapOn: "Open avatar creator"
@@ -21,12 +25,12 @@ appId: xyz.blueskyweb.app
- tapOn: "Select the atom emoji as your avatar"
- tapOn: "Done"
- waitForAnimationToEnd
-- tapOn: "Continue to next step"
+- tapOn:
+ id: "onboardingContinue"
- assertVisible: "What are your interests?"
- tapOn:
- label: "Tap on continue"
- point: "50%,92%"
+ id: "onboardingContinue"
- assertVisible: "You're ready to go!"
- tapOn:
- label: "Tap on Lets go"
- point: "50%,92%"
\ No newline at end of file
+ id: "onboardingFinish"
+- assertVisible: "Following"
diff --git a/__e2e__/flows/onboarding.yml b/__e2e__/flows/onboarding.yml
index c8d9d57233..81aeef6ccb 100644
--- a/__e2e__/flows/onboarding.yml
+++ b/__e2e__/flows/onboarding.yml
@@ -3,11 +3,15 @@ appId: xyz.blueskyweb.app
- runScript:
file: ../setupServer.js
env:
- SERVER_PATH: "?users"
+ SERVER_PATH: "?users"
- runFlow:
file: ../setupApp.yml
- tapOn:
id: "e2eSignInAlice"
+- extendedWaitUntil:
+ visible:
+ id: "viewHeaderHomeFeedPrefsBtn"
+
- tapOn:
id: "e2eStartOnboarding"
- tapOn: "Select an avatar"
@@ -18,12 +22,12 @@ appId: xyz.blueskyweb.app
- waitForAnimationToEnd
- tapOn: "Done"
- waitForAnimationToEnd
-- tapOn: "Continue to next step"
+- tapOn:
+ id: "onboardingContinue"
- assertVisible: "What are your interests?"
- tapOn:
- label: "Tap on continue"
- point: "50%,92%"
+ id: "onboardingContinue"
- assertVisible: "You're ready to go!"
- tapOn:
- label: "Tap on Lets go"
- point: "50%,92%"
\ No newline at end of file
+ id: "onboardingFinish"
+- assertVisible: "Following"
diff --git a/__e2e__/flows/post-report-flow.yml b/__e2e__/flows/post-report-flow.yml
index 3925bef897..a509fcc024 100644
--- a/__e2e__/flows/post-report-flow.yml
+++ b/__e2e__/flows/post-report-flow.yml
@@ -3,11 +3,14 @@ appId: xyz.blueskyweb.app
- runScript:
file: ../setupServer.js
env:
- SERVER_PATH: "?users&follows&posts"
+ SERVER_PATH: "?users&follows&posts"
- runFlow:
file: ../setupApp.yml
- tapOn:
id: "e2eSignInAlice"
+- extendedWaitUntil:
+ visible:
+ id: "viewHeaderHomeFeedPrefsBtn"
- tapOn:
id: "postDropdownBtn"
diff --git a/__e2e__/flows/profile-screen.yml b/__e2e__/flows/profile-screen.yml
index 858448b445..a20302dacc 100644
--- a/__e2e__/flows/profile-screen.yml
+++ b/__e2e__/flows/profile-screen.yml
@@ -3,7 +3,7 @@ appId: xyz.blueskyweb.app
- runScript:
file: ../setupServer.js
env:
- SERVER_PATH: "?users&posts&feeds"
+ SERVER_PATH: "?users&posts&feeds"
- runFlow:
file: ../setupApp.yml
- tapOn:
@@ -12,10 +12,10 @@ appId: xyz.blueskyweb.app
# Navigate to another user profile
- extendedWaitUntil:
visible:
- id: "bottomBarSearchBtn"
+ id: "bottomBarSearchBtn"
- tapOn:
id: "bottomBarSearchBtn"
-- tapOn: "Search for posts, users, or feeds"
+- tapOn: "Search for posts, users[,]? or feeds"
- inputText: "b"
- tapOn:
id: "searchAutoCompleteResult-bob.test"
@@ -38,4 +38,4 @@ appId: xyz.blueskyweb.app
- tapOn:
id: "profileHeaderDropdownBtn"
- tapOn: "Unmute Account"
-- assertNotVisible: "Account Muted"
\ No newline at end of file
+- assertNotVisible: "Account Muted"
diff --git a/__e2e__/flows/search-screen.yml b/__e2e__/flows/search-screen.yml
index 78ebbee369..13a1b06fa2 100644
--- a/__e2e__/flows/search-screen.yml
+++ b/__e2e__/flows/search-screen.yml
@@ -3,20 +3,22 @@ appId: xyz.blueskyweb.app
- runScript:
file: ../setupServer.js
env:
- SERVER_PATH: "?users"
+ SERVER_PATH: "?users"
- runFlow:
file: ../setupApp.yml
- tapOn:
id: "e2eSignInAlice"
# Navigate to another user profile via autocomplete
+- extendedWaitUntil:
+ visible:
+ id: "bottomBarSearchBtn"
- tapOn:
id: "bottomBarSearchBtn"
-- assertVisible: "Search for posts, users, or feeds"
-- tapOn: "Search for posts, users, or feeds"
+- assertVisible: "Search for posts, users[,]? or feeds"
+- tapOn: "Search for posts, users[,]? or feeds"
- inputText: "b"
- tapOn:
id: "searchAutoCompleteResult-bob.test"
- assertVisible:
id: "profileView"
-
diff --git a/__e2e__/flows/shared-prefs.yml b/__e2e__/flows/shared-prefs.yml
index 9cc7707be1..8b773a7665 100644
--- a/__e2e__/flows/shared-prefs.yml
+++ b/__e2e__/flows/shared-prefs.yml
@@ -8,8 +8,12 @@ appId: xyz.blueskyweb.app
file: ../setupApp.yml
- tapOn:
id: "e2eSignInAlice"
+- extendedWaitUntil:
+ visible:
+ id: "viewHeaderHomeFeedPrefsBtn"
+
- assertVisible:
- id: "storybookBtn"
+ id: "storybookBtn"
- tapOn:
id: "storybookBtn"
- tapOn:
diff --git a/__e2e__/flows/thread-muting.yml b/__e2e__/flows/thread-muting.yml
index e588805c2d..2724833feb 100644
--- a/__e2e__/flows/thread-muting.yml
+++ b/__e2e__/flows/thread-muting.yml
@@ -3,13 +3,16 @@ appId: xyz.blueskyweb.app
- runScript:
file: ../setupServer.js
env:
- SERVER_PATH: "?users&follows"
+ SERVER_PATH: "?users&follows"
- runFlow:
file: ../setupApp.yml
# Login, create a thread, and log out
- tapOn:
id: "e2eSignInAlice"
+- extendedWaitUntil:
+ visible:
+ id: "viewHeaderHomeFeedPrefsBtn"
- assertVisible:
id: "composeFAB"
- tapOn:
@@ -19,8 +22,13 @@ appId: xyz.blueskyweb.app
id: "composerPublishBtn"
# Login, reply to the thread, and log out
+- tapOn:
+ id: "e2eSignOut"
- tapOn:
id: "e2eSignInBob"
+- extendedWaitUntil:
+ visible:
+ id: "viewHeaderHomeFeedPrefsBtn"
- tapOn:
id: "replyBtn"
- inputText: "Reply 1"
@@ -28,8 +36,13 @@ appId: xyz.blueskyweb.app
id: "composerPublishBtn"
# Login, confirm notification exists, mute thread, and log out
+- tapOn:
+ id: "e2eSignOut"
- tapOn:
id: "e2eSignInAlice"
+- extendedWaitUntil:
+ visible:
+ id: "viewHeaderHomeFeedPrefsBtn"
- tapOn:
id: "bottomBarNotificationsBtn"
- assertVisible:
@@ -39,12 +52,17 @@ appId: xyz.blueskyweb.app
- tapOn:
id: "postDropdownBtn"
childOf:
- id: "postThreadItem-by-bob.test"
+ id: "postThreadItem-by-bob.test"
- tapOn: "Mute thread"
# Login, reply to the thread twice, and log out
+- tapOn:
+ id: "e2eSignOut"
- tapOn:
id: "e2eSignInBob"
+- extendedWaitUntil:
+ visible:
+ id: "viewHeaderHomeFeedPrefsBtn"
- tapOn:
id: "bottomBarProfileBtn"
- tapOn:
@@ -60,24 +78,35 @@ appId: xyz.blueskyweb.app
- tapOn:
id: "composerPublishBtn"
-
-# Login, confirm notifications dont exist, unmute the thread, confirm notifications exist
+# Login, confirm notifications dont exist, unmute the thread, ~~confirm notifications exist~~
+# Mute thread behaviour no longer change old notifications after muting/unmuting a thread -sfn
+- tapOn:
+ id: "e2eSignOut"
- tapOn:
id: "e2eSignInAlice"
+- extendedWaitUntil:
+ visible:
+ id: "viewHeaderHomeFeedPrefsBtn"
- tapOn:
id: "bottomBarNotificationsBtn"
-- assertNotVisible:
+- assertVisible: ".*Reply 1.*"
+- assertNotVisible: ".*Reply 2.*"
+- assertNotVisible: ".*Reply 3.*"
+- assertVisible:
id: "feedItem-by-bob.test"
- tapOn:
- id: "bottomBarHomeBtn"
+ id: "feedItem-by-bob.test"
- tapOn:
id: "postDropdownBtn"
+ childOf:
+ id: "postThreadItem-by-bob.test"
- tapOn: "Unmute thread"
- tapOn:
id: "bottomBarNotificationsBtn"
- swipe:
from:
- id: "notifsFeed"
+ id: "notifsFeed"
direction: DOWN
-- assertVisible:
- id: "feedItem-by-bob.test"
+- assertVisible: ".*Reply 1.*"
+- assertNotVisible: ".*Reply 2.*"
+- assertNotVisible: ".*Reply 3.*"
diff --git a/__e2e__/mock-server.ts b/__e2e__/mock-server.ts
index b5f13a87fd..1f78848446 100644
--- a/__e2e__/mock-server.ts
+++ b/__e2e__/mock-server.ts
@@ -1,7 +1,7 @@
import {createServer as createHTTPServer} from 'node:http'
import {parse} from 'node:url'
-import {createServer, TestPDS} from '../jest/test-pds'
+import {createServer, type TestPDS} from '../jest/test-pds'
async function main() {
let server: TestPDS
@@ -181,10 +181,10 @@ async function main() {
'warn-profile',
'warn-posts',
'muted-account',
- 'muted-by-list-account',
+ 'muted-by-list-acc',
'blocking-account',
'blockedby-account',
- 'mutual-block-account',
+ 'mutual-block-acc',
]) {
await server.mocker.createUser(user)
await server.mocker.follow('alice', user)
@@ -411,16 +411,16 @@ async function main() {
await server.mocker.addToMuteList(
'alice',
list,
- server.mocker.users['muted-by-list-account'].did,
+ server.mocker.users['muted-by-list-acc'].did,
)
- await server.mocker.createPost('muted-by-list-account', 'muted post')
+ await server.mocker.createPost('muted-by-list-acc', 'muted post')
await server.mocker.createQuotePost(
- 'muted-by-list-account',
+ 'muted-by-list-acc',
'account quote post',
anchorPost,
)
await server.mocker.createReply(
- 'muted-by-list-account',
+ 'muted-by-list-acc',
'account reply',
anchorPost,
)
@@ -470,16 +470,16 @@ async function main() {
)
await server.mocker.createPost(
- 'mutual-block-account',
+ 'mutual-block-acc',
'mutual-block post',
)
await server.mocker.createQuotePost(
- 'mutual-block-account',
+ 'mutual-block-acc',
'mutual-block quote post',
anchorPost,
)
await server.mocker.createReply(
- 'mutual-block-account',
+ 'mutual-block-acc',
'mutual-block reply',
anchorPost,
)
@@ -488,15 +488,15 @@ async function main() {
repo: server.mocker.users.alice.did,
},
{
- subject: server.mocker.users['mutual-block-account'].did,
+ subject: server.mocker.users['mutual-block-acc'].did,
createdAt: new Date().toISOString(),
},
)
await server.mocker.users[
- 'mutual-block-account'
+ 'mutual-block-acc'
].agent.app.bsky.graph.block.create(
{
- repo: server.mocker.users['mutual-block-account'].did,
+ repo: server.mocker.users['mutual-block-acc'].did,
},
{
subject: server.mocker.users.alice.did,
@@ -509,7 +509,16 @@ async function main() {
}
}
console.log('Ready')
- return res.writeHead(200).end(server.pdsUrl)
+ return res
+ .writeHead(200, {
+ 'content-type': 'application/json',
+ })
+ .end(
+ JSON.stringify({
+ pdsUrl: server.pdsUrl,
+ appviewDid: server.appviewDid,
+ }),
+ )
} catch (e) {
console.error('Error!', e)
return res.writeHead(500).end()
diff --git a/__e2e__/setupApp.yml b/__e2e__/setupApp.yml
index 25f9aa8470..728d548fb3 100644
--- a/__e2e__/setupApp.yml
+++ b/__e2e__/setupApp.yml
@@ -11,3 +11,8 @@ appId: xyz.blueskyweb.app
- swipe:
from: "Bluesky"
direction: DOWN
+- tapOn:
+ id: e2eProxyHeaderInput
+- inputText: ${output.result}
+- pressKey: Enter
+- hideKeyboard
diff --git a/__e2e__/setupServer.js b/__e2e__/setupServer.js
index 7b1fb95745..5038197b0a 100644
--- a/__e2e__/setupServer.js
+++ b/__e2e__/setupServer.js
@@ -1,5 +1,8 @@
-// eslint-disable-next-line
-http.post('http://localhost:1986/' + SERVER_PATH, {
+// eslint-disable-next-line no-undef
+var res = http.post('http://localhost:1986/' + SERVER_PATH, {
headers: {'Content-Type': 'text/plain'},
body: '',
})
+
+// eslint-disable-next-line no-undef
+output.result = json(res.body).appviewDid
diff --git a/app.config.js b/app.config.js
index b30cdf0fea..8a0e6e48c1 100644
--- a/app.config.js
+++ b/app.config.js
@@ -303,6 +303,11 @@ module.exports = function (_config) {
android: './assets/app-icons/android_icon_default_dark.png',
prerendered: true,
},
+ next: {
+ ios: './assets/app-icons/icon_default_next.png',
+ android: './assets/app-icons/icon_default_next.png',
+ prerendered: true,
+ },
/**
* Bluesky+ core set
diff --git a/assets/app-icons/icon_default_next.png b/assets/app-icons/icon_default_next.png
new file mode 100644
index 0000000000..8c17c0ea6e
Binary files /dev/null and b/assets/app-icons/icon_default_next.png differ
diff --git a/assets/images/onboarding/value_prop_1_dark.webp b/assets/images/onboarding/value_prop_1_dark.webp
new file mode 100644
index 0000000000..259e6155b2
Binary files /dev/null and b/assets/images/onboarding/value_prop_1_dark.webp differ
diff --git a/assets/images/onboarding/value_prop_1_dark_borderless.webp b/assets/images/onboarding/value_prop_1_dark_borderless.webp
new file mode 100644
index 0000000000..5c44735f9b
Binary files /dev/null and b/assets/images/onboarding/value_prop_1_dark_borderless.webp differ
diff --git a/assets/images/onboarding/value_prop_1_dim.webp b/assets/images/onboarding/value_prop_1_dim.webp
new file mode 100644
index 0000000000..2fea1b9916
Binary files /dev/null and b/assets/images/onboarding/value_prop_1_dim.webp differ
diff --git a/assets/images/onboarding/value_prop_1_dim_borderless.webp b/assets/images/onboarding/value_prop_1_dim_borderless.webp
new file mode 100644
index 0000000000..98c7542b44
Binary files /dev/null and b/assets/images/onboarding/value_prop_1_dim_borderless.webp differ
diff --git a/assets/images/onboarding/value_prop_1_light.webp b/assets/images/onboarding/value_prop_1_light.webp
new file mode 100644
index 0000000000..13ffbf02b3
Binary files /dev/null and b/assets/images/onboarding/value_prop_1_light.webp differ
diff --git a/assets/images/onboarding/value_prop_1_light_borderless.webp b/assets/images/onboarding/value_prop_1_light_borderless.webp
new file mode 100644
index 0000000000..1cf0cd032e
Binary files /dev/null and b/assets/images/onboarding/value_prop_1_light_borderless.webp differ
diff --git a/assets/images/onboarding/value_prop_2_dark.webp b/assets/images/onboarding/value_prop_2_dark.webp
new file mode 100644
index 0000000000..7a60d554eb
Binary files /dev/null and b/assets/images/onboarding/value_prop_2_dark.webp differ
diff --git a/assets/images/onboarding/value_prop_2_dim.webp b/assets/images/onboarding/value_prop_2_dim.webp
new file mode 100644
index 0000000000..86f2c1f893
Binary files /dev/null and b/assets/images/onboarding/value_prop_2_dim.webp differ
diff --git a/assets/images/onboarding/value_prop_2_light.webp b/assets/images/onboarding/value_prop_2_light.webp
new file mode 100644
index 0000000000..2b385a3f2a
Binary files /dev/null and b/assets/images/onboarding/value_prop_2_light.webp differ
diff --git a/assets/images/onboarding/value_prop_3_dark.webp b/assets/images/onboarding/value_prop_3_dark.webp
new file mode 100644
index 0000000000..0ea0dba1c1
Binary files /dev/null and b/assets/images/onboarding/value_prop_3_dark.webp differ
diff --git a/assets/images/onboarding/value_prop_3_dim.webp b/assets/images/onboarding/value_prop_3_dim.webp
new file mode 100644
index 0000000000..a0986e94be
Binary files /dev/null and b/assets/images/onboarding/value_prop_3_dim.webp differ
diff --git a/assets/images/onboarding/value_prop_3_light.webp b/assets/images/onboarding/value_prop_3_light.webp
new file mode 100644
index 0000000000..cd8bcbb457
Binary files /dev/null and b/assets/images/onboarding/value_prop_3_light.webp differ
diff --git a/bskyembed/package.json b/bskyembed/package.json
index e269b1c953..c47c4218da 100644
--- a/bskyembed/package.json
+++ b/bskyembed/package.json
@@ -6,24 +6,25 @@
"dev": "vite",
"build": "tsc && vite build",
"build-snippet": "tsc --project tsconfig.snippet.json",
- "lint": "eslint --cache --ext .js,.jsx,.ts,.tsx src"
+ "lint": "eslint --cache --ext .js,.jsx,.ts,.tsx src",
+ "typecheck": "tsc --noEmit"
},
"dependencies": {
- "@atproto/api": "0.13.6",
- "@preact/preset-vite": "^2.8.2",
- "@vitejs/plugin-legacy": "^5.3.2",
- "preact": "^10.4.8",
- "terser": "^5.30.3"
+ "@atproto/api": "^0.15.25",
+ "preact": "^10.4.8"
},
"devDependencies": {
+ "@preact/preset-vite": "^2.10.2",
+ "@vitejs/plugin-legacy": "^7.0.0",
"autoprefixer": "^10.4.19",
"eslint": "^8.19.0",
"eslint-config-preact": "^1.3.0",
"eslint-plugin-simple-import-sort": "^12.0.0",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.3",
- "typescript": "^5.5.4",
- "vite": "^5.2.8",
- "vite-tsconfig-paths": "^4.3.2"
+ "terser": "^5.43.1",
+ "typescript": "^5.8.3",
+ "vite": "^7.0.4",
+ "vite-tsconfig-paths": "^5.1.4"
}
}
diff --git a/bskyembed/src/components/container.tsx b/bskyembed/src/components/container.tsx
index 8e142a25be..bafc497aea 100644
--- a/bskyembed/src/components/container.tsx
+++ b/bskyembed/src/components/container.tsx
@@ -49,7 +49,7 @@ export function Container({
}
}}>
{href && }
-
{children}
+ {children}
)
}
diff --git a/bskyembed/src/components/embed.tsx b/bskyembed/src/components/embed.tsx
index 428782b642..52618a89da 100644
--- a/bskyembed/src/components/embed.tsx
+++ b/bskyembed/src/components/embed.tsx
@@ -17,8 +17,11 @@ import infoIcon from '../../assets/circleInfo_stroke2_corner0_rounded.svg'
import playIcon from '../../assets/play_filled_corner2_rounded.svg'
import starterPackIcon from '../../assets/starterPack.svg'
import {CONTENT_LABELS, labelsToInfo} from '../labels'
-import {getRkey} from '../utils'
+import * as bsky from '../types/bsky'
+import {getRkey} from '../util/rkey'
+import {getVerificationState} from '../util/verification-state'
import {Link} from './link'
+import {VerificationCheck} from './verification-check'
export function Embed({
content,
@@ -75,23 +78,35 @@ export function Embed({
CONTENT_LABELS.includes(label.val),
)
+ const verification = getVerificationState({profile: record.author})
+
return (
-
+
-
- {record.author.displayName}
-
+
+
+ {record.author.displayName?.trim() || record.author.handle}
+
+ {verification.isVerified && (
+
+ )}
+
@{record.author.handle}
-
-
+
+
{text &&
{text}
}
{record.embeds?.map(embed => (
@@ -404,7 +419,12 @@ function StarterPackEmbed({
}: {
content: AppBskyGraphDefs.StarterPackViewBasic
}) {
- if (!AppBskyGraphStarterpack.isRecord(content.record)) {
+ if (
+ !bsky.dangerousIsType
(
+ content.record,
+ AppBskyGraphStarterpack.isRecord,
+ )
+ ) {
return null
}
@@ -443,7 +463,9 @@ function StarterPackEmbed({
}
// from #/lib/strings/starter-pack.ts
-function getStarterPackImage(starterPack: AppBskyGraphDefs.StarterPackView) {
+function getStarterPackImage(
+ starterPack: AppBskyGraphDefs.StarterPackViewBasic,
+) {
const rkey = getRkey({uri: starterPack.uri})
return `https://ogcard.cdn.bsky.app/start/${starterPack.creator.did}/${rkey}`
}
diff --git a/bskyembed/src/components/post.tsx b/bskyembed/src/components/post.tsx
index 6ecac57965..d216ce0e54 100644
--- a/bskyembed/src/components/post.tsx
+++ b/bskyembed/src/components/post.tsx
@@ -11,10 +11,15 @@ import likeIcon from '../../assets/heart2_filled_stroke2_corner0_rounded.svg'
import logo from '../../assets/logo.svg'
import repostIcon from '../../assets/repost_stroke2_corner2_rounded.svg'
import {CONTENT_LABELS} from '../labels'
-import {getRkey, niceDate, prettyNumber} from '../utils'
+import * as bsky from '../types/bsky'
+import {niceDate} from '../util/nice-date'
+import {prettyNumber} from '../util/pretty-number'
+import {getRkey} from '../util/rkey'
+import {getVerificationState} from '../util/verification-state'
import {Container} from './container'
import {Embed} from './embed'
import {Link} from './link'
+import {VerificationCheck} from './verification-check'
interface Props {
thread: AppBskyFeedDefs.ThreadViewPost
@@ -28,16 +33,25 @@ export function Post({thread}: Props) {
)
let record: AppBskyFeedPost.Record | null = null
- if (AppBskyFeedPost.isRecord(post.record)) {
+ if (
+ bsky.dangerousIsType(
+ post.record,
+ AppBskyFeedPost.isRecord,
+ )
+ ) {
record = post.record
}
+ const verification = getVerificationState({profile: post.author})
+
const href = `/profile/${post.author.did}/post/${getRkey(post)}`
return (
-
-
+
+
-
+
+
+
+ {post.author.displayName?.trim() || post.author.handle}
+
+ {verification.isVerified && (
+
+ )}
+
-
{post.author.displayName}
-
-
-
@{post.author.handle}
+ className="block text-[15px] text-textLight dark:text-textDimmed hover:underline line-clamp-1">
+ @{post.author.handle}
-
@@ -133,7 +155,7 @@ function PostContent({record}: {record: AppBskyFeedPost.Record | null}) {
:
+}
+
+export function VerifiedCheck({size, className}: IconProps) {
+ return (
+
+ )
+}
+
+export function VerifierCheck({size, className}: IconProps) {
+ return (
+
+ )
+}
diff --git a/bskyembed/src/screens/landing.tsx b/bskyembed/src/screens/landing.tsx
index 880b713372..bb650241f4 100644
--- a/bskyembed/src/screens/landing.tsx
+++ b/bskyembed/src/screens/landing.tsx
@@ -14,9 +14,11 @@ import {
import {Container} from '../components/container'
import {Link} from '../components/link'
import {Post} from '../components/post'
-import {niceDate} from '../utils'
+import * as bsky from '../types/bsky'
+import {niceDate} from '../util/nice-date'
-const DEFAULT_POST = 'https://bsky.app/profile/emilyliu.me/post/3jzn6g7ixgq2y'
+const DEFAULT_POST =
+ 'https://bsky.app/profile/did:plc:vjug55kidv6sye7ykr5faxxn/post/3jzn6g7ixgq2y'
const DEFAULT_URI =
'at://did:plc:vjug55kidv6sye7ykr5faxxn/app.bsky.feed.post/3jzn6g7ixgq2y'
@@ -222,7 +224,12 @@ function Snippet({
const snippet = useMemo(() => {
const record = thread.post.record
- if (!AppBskyFeedPost.isRecord(record)) {
+ if (
+ !bsky.dangerousIsType
(
+ record,
+ AppBskyFeedPost.isRecord,
+ )
+ ) {
return ''
}
diff --git a/bskyembed/src/screens/post.tsx b/bskyembed/src/screens/post.tsx
index 4cd72b69bc..83914f66f3 100644
--- a/bskyembed/src/screens/post.tsx
+++ b/bskyembed/src/screens/post.tsx
@@ -8,7 +8,7 @@ import {applyTheme, initSystemColorMode} from '../color-mode'
import {Container} from '../components/container'
import {Link} from '../components/link'
import {Post} from '../components/post'
-import {getRkey} from '../utils'
+import {getRkey} from '../util/rkey'
const root = document.getElementById('app')
if (!root) throw new Error('No root element')
diff --git a/bskyembed/src/types/bsky/index.ts b/bskyembed/src/types/bsky/index.ts
new file mode 100644
index 0000000000..c462cdf654
--- /dev/null
+++ b/bskyembed/src/types/bsky/index.ts
@@ -0,0 +1,49 @@
+import {ValidationResult} from '@atproto/lexicon'
+
+export * as profile from './profile'
+
+/**
+ * Fast type checking without full schema validation, for use with data we
+ * trust, or for non-critical path use cases. Why? Our SDK's `is*` identity
+ * utils do not assert the type of the entire object, only the `$type` string.
+ *
+ * For full validation of the object schema, use the `validate` export from
+ * this file.
+ *
+ * Usage:
+ * ```ts
+ * import * as bsky from '#/types/bsky'
+ *
+ * if (bsky.dangerousIsType(item, AppBskyFeedPost.isRecord)) {
+ * // `item` has type `$Typed` here
+ * }
+ * ```
+ */
+export function dangerousIsType(
+ record: unknown,
+ identity: (v: V) => v is V & {$type: NonNullable},
+): record is R {
+ return identity(record)
+}
+
+/**
+ * Fully validates the object schema, which has a performance cost.
+ *
+ * For faster checks with data we trust, like that from our app view, use the
+ * `dangerousIsType` export from this same file.
+ *
+ * Usage:
+ * ```ts
+ * import * as bsky from '#/types/bsky'
+ *
+ * if (bsky.validate(item, AppBskyFeedPost.validateRecord)) {
+ * // `item` has type `$Typed` here
+ * }
+ * ```
+ */
+export function validate(
+ record: unknown,
+ validator: (v: unknown) => ValidationResult,
+): record is R {
+ return validator(record).success
+}
diff --git a/bskyembed/src/types/bsky/profile.ts b/bskyembed/src/types/bsky/profile.ts
new file mode 100644
index 0000000000..12c8146ae1
--- /dev/null
+++ b/bskyembed/src/types/bsky/profile.ts
@@ -0,0 +1,10 @@
+import {type AppBskyActorDefs, type ChatBskyActorDefs} from '@atproto/api'
+
+/**
+ * Matches any profile view exported by our SDK
+ */
+export type AnyProfileView =
+ | AppBskyActorDefs.ProfileViewBasic
+ | AppBskyActorDefs.ProfileView
+ | AppBskyActorDefs.ProfileViewDetailed
+ | ChatBskyActorDefs.ProfileViewBasic
diff --git a/bskyembed/src/util/nice-date.ts b/bskyembed/src/util/nice-date.ts
new file mode 100644
index 0000000000..016c97a694
--- /dev/null
+++ b/bskyembed/src/util/nice-date.ts
@@ -0,0 +1,11 @@
+export function niceDate(date: number | string | Date) {
+ const d = new Date(date)
+ return `${d.toLocaleDateString('en-us', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ })} at ${d.toLocaleTimeString(undefined, {
+ hour: 'numeric',
+ minute: '2-digit',
+ })}`
+}
diff --git a/bskyembed/src/util/parse-embed.ts b/bskyembed/src/util/parse-embed.ts
new file mode 100644
index 0000000000..97c3dc33e7
--- /dev/null
+++ b/bskyembed/src/util/parse-embed.ts
@@ -0,0 +1,152 @@
+/**
+ * This file is a copy of what exists in the social-app
+ */
+
+import {
+ AppBskyEmbedExternal,
+ AppBskyEmbedImages,
+ AppBskyEmbedRecord,
+ AppBskyEmbedRecordWithMedia,
+ AppBskyEmbedVideo,
+ AppBskyFeedDefs,
+ AppBskyGraphDefs,
+ AppBskyLabelerDefs,
+} from '@atproto/api'
+
+export type Embed =
+ | {
+ type: 'post'
+ view: AppBskyEmbedRecord.ViewRecord
+ }
+ | {
+ type: 'post_not_found'
+ view: AppBskyEmbedRecord.ViewNotFound
+ }
+ | {
+ type: 'post_blocked'
+ view: AppBskyEmbedRecord.ViewBlocked
+ }
+ | {
+ type: 'post_detached'
+ view: AppBskyEmbedRecord.ViewDetached
+ }
+ | {
+ type: 'feed'
+ view: AppBskyFeedDefs.GeneratorView
+ }
+ | {
+ type: 'list'
+ view: AppBskyGraphDefs.ListView
+ }
+ | {
+ type: 'labeler'
+ view: AppBskyLabelerDefs.LabelerView
+ }
+ | {
+ type: 'starter_pack'
+ view: AppBskyGraphDefs.StarterPackViewBasic
+ }
+ | {
+ type: 'images'
+ view: AppBskyEmbedImages.View
+ }
+ | {
+ type: 'link'
+ view: AppBskyEmbedExternal.View
+ }
+ | {
+ type: 'video'
+ view: AppBskyEmbedVideo.View
+ }
+ | {
+ type: 'post_with_media'
+ view: Embed
+ media: Embed
+ }
+ | {
+ type: 'unknown'
+ view: null
+ }
+
+export type EmbedType = Extract