Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6aaccd4f15 | |||
| caf2f31d9a | |||
| f08afb7404 | |||
| e456fb4a0e | |||
| f62f9b5ca4 | |||
| df82678ebf | |||
| 209be23586 | |||
| 4491b3d15a | |||
| 2c6ff9bea4 | |||
| b37cdb135b | |||
| 19da52f1c7 | |||
| de25c4b676 | |||
| 921b13dbdc | |||
| ffe15a981a |
@@ -71,19 +71,6 @@ module.exports = {
|
||||
'simple-import-sort/exports': 'warn',
|
||||
// TODO: Reenable when we figure out why it gets stuck on CI.
|
||||
// 'react-compiler/react-compiler': 'error',
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: '@atproto/api',
|
||||
importNames: ['moderatePost'],
|
||||
message:
|
||||
'Please use `moderatePost_wrapped` from `#/lib/moderatePost_wrapped` instead.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
ignorePatterns: [
|
||||
'**/__mocks__/*.ts',
|
||||
|
||||
@@ -132,8 +132,7 @@ appId: xyz.blueskyweb.app
|
||||
id: "feedItem-by-bob.test"
|
||||
- tapOn:
|
||||
id: "e2eGotoFeeds"
|
||||
- tapOn:
|
||||
id: "saved-feed-Good Ppl"
|
||||
- tapOn: "Good Ppl"
|
||||
- assertVisible:
|
||||
id: "feedItem-by-bob.test"
|
||||
- tapOn:
|
||||
@@ -169,7 +168,7 @@ appId: xyz.blueskyweb.app
|
||||
id: "profilePager-selector"
|
||||
direction: LEFT
|
||||
- tapOn:
|
||||
id: "profilePager-selector-6"
|
||||
id: "profilePager-selector-5"
|
||||
- tapOn: "Good Ppl"
|
||||
|
||||
- tapOn:
|
||||
|
||||
@@ -340,14 +340,12 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
'https://youtube.com/watch?v=videoId',
|
||||
'https://youtube.com/watch?v=videoId&feature=share',
|
||||
'https://youtube.com/shorts/videoId',
|
||||
'https://youtube.com/live/videoId',
|
||||
'https://m.youtube.com/watch?v=videoId',
|
||||
'https://music.youtube.com/watch?v=videoId',
|
||||
|
||||
'https://youtube.com/shorts/',
|
||||
'https://youtube.com/',
|
||||
'https://youtube.com/random',
|
||||
'https://youtube.com/live/',
|
||||
|
||||
'https://twitch.tv/channelName',
|
||||
'https://www.twitch.tv/channelName',
|
||||
@@ -477,16 +475,10 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
source: 'youtube',
|
||||
playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=0',
|
||||
},
|
||||
{
|
||||
type: 'youtube_video',
|
||||
source: 'youtube',
|
||||
playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=0',
|
||||
},
|
||||
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
|
||||
{
|
||||
type: 'twitch_video',
|
||||
|
||||
@@ -191,7 +191,7 @@ module.exports = function (config) {
|
||||
'expo-build-properties',
|
||||
{
|
||||
ios: {
|
||||
deploymentTarget: '15.1',
|
||||
deploymentTarget: '14.0',
|
||||
newArchEnabled: false,
|
||||
},
|
||||
android: {
|
||||
@@ -221,7 +221,6 @@ module.exports = function (config) {
|
||||
'./plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js',
|
||||
'./plugins/shareExtension/withShareExtensions.js',
|
||||
'./plugins/notificationsExtension/withNotificationsExtension.js',
|
||||
'./plugins/withAppDelegateReferrer.js',
|
||||
].filter(Boolean),
|
||||
extra: {
|
||||
eas: {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M20.957 3.043a1 1 0 0 1 0 1.414L16.414 9H20a1 1 0 1 1 0 2h-6a1 1 0 0 1-1-1V4a1 1 0 1 1 2 0v3.586l4.543-4.543a1 1 0 0 1 1.414 0ZM3 14a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 1 1-2 0v-3.586l-4.543 4.543a1 1 0 0 1-1.414-1.414L7.586 15H4a1 1 0 0 1-1-1Z" fill="#000"/></svg>
|
||||
|
Before Width: | Height: | Size: 391 B |
@@ -1 +0,0 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M20.957 3.043a1 1 0 0 1 0 1.414L16.414 9H20a1 1 0 1 1 0 2h-5a2 2 0 0 1-2-2V4a1 1 0 1 1 2 0v3.586l4.543-4.543a1 1 0 0 1 1.414 0ZM3 14a1 1 0 0 1 1-1h5a2 2 0 0 1 2 2v5a1 1 0 1 1-2 0v-3.586l-4.543 4.543a1 1 0 0 1-1.414-1.414L7.586 15H4a1 1 0 0 1-1-1Z" fill="#000"/></svg>
|
||||
|
Before Width: | Height: | Size: 392 B |
@@ -1 +0,0 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M14 5a1 1 0 1 1 0-2h6a1 1 0 0 1 1 1v6a1 1 0 1 1-2 0V6.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L17.586 5H14ZM4 13a1 1 0 0 1 1 1v3.586l4.293-4.293a1 1 0 0 1 1.414 1.414L6.414 19H10a1 1 0 1 1 0 2H4a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1Z" fill="#000"/></svg>
|
||||
|
Before Width: | Height: | Size: 369 B |
@@ -1 +0,0 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M13 4a1 1 0 0 1 1-1h5a2 2 0 0 1 2 2v5a1 1 0 1 1-2 0V6.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L17.586 5H14a1 1 0 0 1-1-1Zm-9 9a1 1 0 0 1 1 1v3.586l4.293-4.293a1 1 0 0 1 1.414 1.414L6.414 19H10a1 1 0 1 1 0 2H5a2 2 0 0 1-2-2v-5a1 1 0 0 1 1-1Z" fill="#000"/></svg>
|
||||
|
Before Width: | Height: | Size: 384 B |
@@ -1 +0,0 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm11.543 7.293a1 1 0 0 1 1.414 0 1 1 0 0 0 1.414-1.414 3 3 0 1 0 0 4.242 1 1 0 0 0-1.414-1.414 1 1 0 0 1-1.414-1.414Zm-6 0a1 1 0 0 1 1.414 0 1 1 0 0 0 1.414-1.414 3 3 0 1 0 0 4.243 1 1 0 0 0-1.414-1.415 1 1 0 0 1-1.414-1.414Z" fill="#000"/></svg>
|
||||
|
Before Width: | Height: | Size: 440 B |
@@ -1 +0,0 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 1v14h14V5H5Zm10.957 6.293a1 1 0 1 0 0 1.414 1 1 0 0 1 1.414 1.414 3 3 0 1 1 0-4.242 1 1 0 0 1-1.414 1.414Zm-6.331-.22a1 1 0 1 0 .331 1.634 1 1 0 0 1 1.414 1.414 3 3 0 1 1 0-4.242 1 1 0 0 1-1.414 1.414.994.994 0 0 0-.331-.22Z" fill="#000"/></svg>
|
||||
|
Before Width: | Height: | Size: 443 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M6 2a1 1 0 0 1 1 1v2h11a1 1 0 0 1 1 1v11h2a1 1 0 1 1 0 2h-2v2a1 1 0 1 1-2 0v-2H6a1 1 0 0 1-1-1V7H3a1 1 0 0 1 0-2h2V3a1 1 0 0 1 1-1Zm1 5v10h10V7H7Z" clip-rule="evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 289 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M3.135 12C5.413 16.088 8.77 18 12 18s6.587-1.912 8.865-6C18.587 7.912 15.23 6 12 6c-3.228 0-6.587 1.912-8.865 6ZM12 4c4.24 0 8.339 2.611 10.888 7.54a1 1 0 0 1 0 .92C20.338 17.388 16.24 20 12 20c-4.24 0-8.339-2.611-10.888-7.54a1 1 0 0 1 0-.92C3.662 6.612 7.76 4 12 4Zm0 6a2 2 0 1 0 0 4 2 2 0 0 0 0-4Zm-4 2a4 4 0 1 1 8 0 4 4 0 0 1-8 0Z" clip-rule="evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 476 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12.097 2.53a1 1 0 0 1-.041 1.07 6 6 0 0 0 8.345 8.344 1 1 0 0 1 1.563.908c-.434 5.122-4.728 9.144-9.962 9.144-5.522 0-9.998-4.476-9.998-9.998 0-5.234 4.021-9.528 9.144-9.962a1 1 0 0 1 .949.494ZM9.424 4.424a7.998 7.998 0 1 0 10.152 10.152A8 8 0 0 1 9.424 4.424Z" clip-rule="evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 403 B |
@@ -1 +0,0 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M4 4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V4ZM14 4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1V4Z" fill="#000"/></svg>
|
||||
|
Before Width: | Height: | Size: 247 B |
@@ -1 +0,0 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M4 6a3 3 0 0 1 6 0v12a3 3 0 1 1-6 0V6ZM14 6a3 3 0 1 1 6 0v12a3 3 0 1 1-6 0V6Z" fill="#000"/></svg>
|
||||
|
Before Width: | Height: | Size: 183 B |
@@ -1 +0,0 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V4Zm2 1v14h2V5H6Zm8-1a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1V4Zm2 1v14h2V5h-2Z" fill="#000"/></svg>
|
||||
|
Before Width: | Height: | Size: 315 B |
@@ -1 +0,0 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 6a3 3 0 0 1 6 0v12a3 3 0 1 1-6 0V6Zm3-1a1 1 0 0 0-1 1v12a1 1 0 1 0 2 0V6a1 1 0 0 0-1-1Zm7 1a3 3 0 1 1 6 0v12a3 3 0 1 1-6 0V6Zm3-1a1 1 0 0 0-1 1v12a1 1 0 1 0 2 0V6a1 1 0 0 0-1-1Z" fill="#000"/></svg>
|
||||
|
Before Width: | Height: | Size: 326 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M5 4a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3v16a3 3 0 0 1-3 3H8a3 3 0 0 1-3-3V4Zm3-1a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H8Zm2 2a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2h-2a1 1 0 0 1-1-1Z" clip-rule="evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 334 B |
@@ -1 +0,0 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.514 2.143A1 1 0 0 0 5 3v18a1 1 0 0 0 1.514.858l15-9a1 1 0 0 0 0-1.716l-15-9Z" fill="#000"/></svg>
|
||||
|
Before Width: | Height: | Size: 184 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M9.576 2.534C7.578 1.299 5 2.737 5 5.086v13.828c0 2.35 2.578 3.787 4.576 2.552l11.194-6.914c1.899-1.172 1.899-3.932 0-5.104L9.576 2.534Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 239 B |
@@ -1 +0,0 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M5.507 2.13a1 1 0 0 1 1.008.013l15 9a1 1 0 0 1 0 1.714l-15 9A1 1 0 0 1 5 21V3a1 1 0 0 1 .507-.87ZM7 4.766v14.468L19.056 12 7 4.766Z" fill="#000"/></svg>
|
||||
|
Before Width: | Height: | Size: 276 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M5 5.086C5 2.736 7.578 1.3 9.576 2.534L20.77 9.448c1.899 1.172 1.899 3.932 0 5.104L9.576 21.466C7.578 22.701 5 21.263 5 18.914V5.086Zm3.525-.85A1 1 0 0 0 7 5.085v13.828a1 1 0 0 0 1.525.85l11.194-6.913a1 1 0 0 0 0-1.702L8.525 4.235Z" clip-rule="evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 374 B |
@@ -10,12 +10,11 @@
|
||||
],
|
||||
"rules": {
|
||||
"simple-import-sort/imports": "warn",
|
||||
"simple-import-sort/exports": "warn",
|
||||
'no-else-return': 'off'
|
||||
"simple-import-sort/exports": "warn"
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaVersion": "latest",
|
||||
"project": "./bskyembed/tsconfig.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#fff" d="M9.576 2.534C7.578 1.299 5 2.737 5 5.086v13.828c0 2.35 2.578 3.787 4.576 2.552l11.194-6.914c1.899-1.172 1.899-3.932 0-5.104L9.576 2.534Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 239 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><defs><linearGradient x1="0" y1="0" x2="100%" y2="0" gradientTransform="rotate(45)" id="sky_V5w1FF_xb91wVQ_1euhBX"><stop offset="0" stop-color="#0A7AFF"></stop><stop offset="1" stop-color="#59B9FF"></stop></linearGradient></defs><path fill="url(#sky_V5w1FF_xb91wVQ_1euhBX)" fill-rule="evenodd" clip-rule="evenodd" d="M11.26 5.227 5.02 6.899c-.734.197-1.17.95-.973 1.685l1.672 6.24c.197.734.951 1.17 1.685.973l6.24-1.672c.734-.197 1.17-.951.973-1.685L12.945 6.2a1.375 1.375 0 0 0-1.685-.973Zm-6.566.459a2.632 2.632 0 0 0-1.86 3.223l1.672 6.24a2.632 2.632 0 0 0 3.223 1.861l6.24-1.672a2.631 2.631 0 0 0 1.861-3.223l-1.672-6.24a2.632 2.632 0 0 0-3.223-1.861l-6.24 1.672Z"></path><path fill="url(#sky_V5w1FF_xb91wVQ_1euhBX)" fill-rule="evenodd" clip-rule="evenodd" d="M15.138 18.411a4.606 4.606 0 1 0 0-9.211 4.606 4.606 0 0 0 0 9.211Zm0 1.257a5.862 5.862 0 1 0 0-11.724 5.862 5.862 0 0 0 0 11.724Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 982 B |
@@ -9,7 +9,7 @@
|
||||
"lint": "eslint --cache --ext .js,.jsx,.ts,.tsx src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "0.13.6",
|
||||
"@atproto/api": "^0.12.2",
|
||||
"@preact/preset-vite": "^2.8.2",
|
||||
"@vitejs/plugin-legacy": "^5.3.2",
|
||||
"preact": "^10.4.8",
|
||||
@@ -22,7 +22,7 @@
|
||||
"eslint-plugin-simple-import-sort": "^12.0.0",
|
||||
"postcss": "^8.4.38",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"typescript": "^5.5.4",
|
||||
"typescript": "^4.0.5",
|
||||
"vite": "^5.2.8",
|
||||
"vite-tsconfig-paths": "^4.3.2"
|
||||
}
|
||||
|
||||
@@ -3,20 +3,15 @@ import {
|
||||
AppBskyEmbedImages,
|
||||
AppBskyEmbedRecord,
|
||||
AppBskyEmbedRecordWithMedia,
|
||||
AppBskyEmbedVideo,
|
||||
AppBskyFeedDefs,
|
||||
AppBskyFeedPost,
|
||||
AppBskyGraphDefs,
|
||||
AppBskyGraphStarterpack,
|
||||
AppBskyLabelerDefs,
|
||||
AtUri,
|
||||
} from '@atproto/api'
|
||||
import {ComponentChildren, h} from 'preact'
|
||||
import {useMemo} from 'preact/hooks'
|
||||
|
||||
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 {Link} from './link'
|
||||
@@ -110,7 +105,7 @@ export function Embed({
|
||||
// Case 3.2: List
|
||||
if (AppBskyGraphDefs.isListView(record)) {
|
||||
return (
|
||||
<GenericWithImageEmbed
|
||||
<GenericWithImage
|
||||
image={record.avatar}
|
||||
title={record.name}
|
||||
href={`/profile/${record.creator.did}/lists/${getRkey(record)}`}
|
||||
@@ -127,7 +122,7 @@ export function Embed({
|
||||
// Case 3.3: Feed
|
||||
if (AppBskyFeedDefs.isGeneratorView(record)) {
|
||||
return (
|
||||
<GenericWithImageEmbed
|
||||
<GenericWithImage
|
||||
image={record.avatar}
|
||||
title={record.displayName}
|
||||
href={`/profile/${record.creator.did}/feed/${getRkey(record)}`}
|
||||
@@ -139,35 +134,31 @@ export function Embed({
|
||||
|
||||
// Case 3.4: Labeler
|
||||
if (AppBskyLabelerDefs.isLabelerView(record)) {
|
||||
// Embed type does not exist in the app, so show nothing
|
||||
return null
|
||||
return (
|
||||
<GenericWithImage
|
||||
image={record.creator.avatar}
|
||||
title={record.creator.displayName || record.creator.handle}
|
||||
href={`/profile/${record.creator.did}`}
|
||||
subtitle="Labeler"
|
||||
description={`Liked by ${record.likeCount ?? 0} users`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// Case 3.5: Starter pack
|
||||
if (AppBskyGraphDefs.isStarterPackViewBasic(record)) {
|
||||
return <StarterPackEmbed content={record} />
|
||||
}
|
||||
|
||||
// Case 3.6: Post not found
|
||||
// Case 3.5: Post not found
|
||||
if (AppBskyEmbedRecord.isViewNotFound(record)) {
|
||||
return <Info>Quoted post not found, it may have been deleted.</Info>
|
||||
}
|
||||
|
||||
// Case 3.7: Post blocked
|
||||
// Case 3.6: Post blocked
|
||||
if (AppBskyEmbedRecord.isViewBlocked(record)) {
|
||||
return <Info>The quoted post is blocked.</Info>
|
||||
}
|
||||
|
||||
// Unknown embed type
|
||||
return null
|
||||
throw new Error('Unknown embed type')
|
||||
}
|
||||
|
||||
// Case 4: Video
|
||||
if (AppBskyEmbedVideo.isView(content)) {
|
||||
return <VideoEmbed content={content} />
|
||||
}
|
||||
|
||||
// Case 5: Record with media
|
||||
// Case 4: Record with media
|
||||
if (
|
||||
AppBskyEmbedRecordWithMedia.isView(content) &&
|
||||
AppBskyEmbedRecord.isViewRecord(content.record.record)
|
||||
@@ -191,8 +182,7 @@ export function Embed({
|
||||
)
|
||||
}
|
||||
|
||||
// Unknown embed type
|
||||
return null
|
||||
throw new Error('Unsupported embed type')
|
||||
} catch (err) {
|
||||
return (
|
||||
<Info>{err instanceof Error ? err.message : 'An error occurred'}</Info>
|
||||
@@ -324,7 +314,7 @@ function ExternalEmbed({
|
||||
)
|
||||
}
|
||||
|
||||
function GenericWithImageEmbed({
|
||||
function GenericWithImage({
|
||||
title,
|
||||
subtitle,
|
||||
href,
|
||||
@@ -360,89 +350,3 @@ function GenericWithImageEmbed({
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
// just the thumbnail and a play button
|
||||
function VideoEmbed({content}: {content: AppBskyEmbedVideo.View}) {
|
||||
let aspectRatio = 1
|
||||
|
||||
if (content.aspectRatio) {
|
||||
const {width, height} = content.aspectRatio
|
||||
aspectRatio = clamp(width / height, 1 / 1, 3 / 1)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full overflow-hidden rounded-lg aspect-square"
|
||||
style={{aspectRatio: `${aspectRatio} / 1`}}>
|
||||
<img
|
||||
src={content.thumbnail}
|
||||
alt={content.alt}
|
||||
className="object-cover size-full"
|
||||
/>
|
||||
<div className="size-24 absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-black/50 flex items-center justify-center">
|
||||
<img src={playIcon} className="object-cover size-3/5" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function StarterPackEmbed({
|
||||
content,
|
||||
}: {
|
||||
content: AppBskyGraphDefs.StarterPackViewBasic
|
||||
}) {
|
||||
if (!AppBskyGraphStarterpack.isRecord(content.record)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const starterPackHref = getStarterPackHref(content)
|
||||
const imageUri = getStarterPackImage(content)
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={starterPackHref}
|
||||
className="w-full rounded-lg overflow-hidden border flex flex-col items-stretch">
|
||||
<img src={imageUri} className="aspect-[1.91/1] object-cover" />
|
||||
<div className="py-3 px-4">
|
||||
<div className="flex space-x-2 items-center">
|
||||
<img src={starterPackIcon} className="w-10 h-10" />
|
||||
<div>
|
||||
<p className="font-semibold leading-[21px]">
|
||||
{content.record.name}
|
||||
</p>
|
||||
<p className="text-sm text-textLight line-clamp-2 leading-[18px]">
|
||||
Starter pack by{' '}
|
||||
{content.creator.displayName || `@${content.creator.handle}`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{content.record.description && (
|
||||
<p className="text-sm mt-1">{content.record.description}</p>
|
||||
)}
|
||||
{!!content.joinedAllTimeCount && content.joinedAllTimeCount > 50 && (
|
||||
<p className="text-sm font-semibold text-textLight mt-1">
|
||||
{content.joinedAllTimeCount} users have joined!
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
// from #/lib/strings/starter-pack.ts
|
||||
function getStarterPackImage(starterPack: AppBskyGraphDefs.StarterPackView) {
|
||||
const rkey = new AtUri(starterPack.uri).rkey
|
||||
return `https://ogcard.cdn.bsky.app/start/${starterPack.creator.did}/${rkey}`
|
||||
}
|
||||
|
||||
function getStarterPackHref(
|
||||
starterPack: AppBskyGraphDefs.StarterPackViewBasic,
|
||||
) {
|
||||
const rkey = new AtUri(starterPack.uri).rkey
|
||||
const handleOrDid = starterPack.creator.handle || starterPack.creator.did
|
||||
return `/starter-pack/${handleOrDid}/${rkey}`
|
||||
}
|
||||
|
||||
function clamp(num: number, min: number, max: number) {
|
||||
return Math.max(min, Math.min(num, max))
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ 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 {getRkey, niceDate} from '../utils'
|
||||
import {Container} from './container'
|
||||
import {Embed} from './embed'
|
||||
import {Link} from './link'
|
||||
@@ -78,7 +78,7 @@ export function Post({thread}: Props) {
|
||||
<div className="flex items-center gap-2 cursor-pointer">
|
||||
<img src={likeIcon} className="w-5 h-5" />
|
||||
<p className="font-bold text-neutral-500 mb-px">
|
||||
{prettyNumber(post.likeCount)}
|
||||
{post.likeCount}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -86,7 +86,7 @@ export function Post({thread}: Props) {
|
||||
<div className="flex items-center gap-2 cursor-pointer">
|
||||
<img src={repostIcon} className="w-5 h-5" />
|
||||
<p className="font-bold text-neutral-500 mb-px">
|
||||
{prettyNumber(post.repostCount)}
|
||||
{post.repostCount}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -97,7 +97,7 @@ export function Post({thread}: Props) {
|
||||
<div className="flex-1" />
|
||||
<p className="cursor-pointer text-brand font-bold hover:underline hidden min-[450px]:inline">
|
||||
{post.replyCount
|
||||
? `Read ${prettyNumber(post.replyCount)} ${
|
||||
? `Read ${post.replyCount} ${
|
||||
post.replyCount > 1 ? 'replies' : 'reply'
|
||||
} on Bluesky`
|
||||
: `View on Bluesky`}
|
||||
@@ -132,10 +132,7 @@ function PostContent({record}: {record: AppBskyFeedPost.Record | null}) {
|
||||
key={counter}
|
||||
href={segment.link.uri}
|
||||
className="text-blue-400 hover:underline"
|
||||
disableTracking={
|
||||
!segment.link.uri.startsWith('https://bsky.app') &&
|
||||
!segment.link.uri.startsWith('https://go.bsky.app')
|
||||
}>
|
||||
disableTracking={!segment.link.uri.startsWith('https://bsky.app')}>
|
||||
{segment.text}
|
||||
</Link>,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import '../index.css'
|
||||
|
||||
import {AppBskyFeedDefs, AtpAgent} from '@atproto/api'
|
||||
import {AppBskyFeedDefs, BskyAgent} from '@atproto/api'
|
||||
import {h, render} from 'preact'
|
||||
|
||||
import logo from '../../assets/logo.svg'
|
||||
@@ -12,7 +12,7 @@ import {getRkey} from '../utils'
|
||||
const root = document.getElementById('app')
|
||||
if (!root) throw new Error('No root element')
|
||||
|
||||
const agent = new AtpAgent({
|
||||
const agent = new BskyAgent({
|
||||
service: 'https://public.api.bsky.app',
|
||||
})
|
||||
|
||||
|
||||
@@ -16,13 +16,3 @@ export function getRkey({uri}: {uri: string}): string {
|
||||
const at = new AtUri(uri)
|
||||
return at.rkey
|
||||
}
|
||||
|
||||
const formatter = new Intl.NumberFormat('en-US', {
|
||||
notation: 'compact',
|
||||
maximumFractionDigits: 1,
|
||||
roundingMode: 'trunc',
|
||||
})
|
||||
|
||||
export function prettyNumber(number: number) {
|
||||
return formatter.format(number)
|
||||
}
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"jsxFragmentFactory": "Fragment",
|
||||
"downlevelIteration": true
|
||||
},
|
||||
"include": ["src", "vite.config.ts"]
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"strict": true,
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["snippet"]
|
||||
"include": ["snippet"],
|
||||
}
|
||||
|
||||
@@ -20,16 +20,15 @@
|
||||
"@jridgewell/gen-mapping" "^0.3.5"
|
||||
"@jridgewell/trace-mapping" "^0.3.24"
|
||||
|
||||
"@atproto/api@0.13.6":
|
||||
version "0.13.6"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.13.6.tgz#2500e9d7143e6718089632300c42ce50149f8cd5"
|
||||
integrity sha512-58emFFZhqY8nVWD3xFWK0yYqAmJ2un+NaTtZxBbRo00mGq1rz9VXTpVmfoHFcuXL1hoDQN3WyJfsub8r6xGOgg==
|
||||
"@atproto/api@^0.12.2":
|
||||
version "0.12.2"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.2.tgz#5df6d4f60dea0395c84fdebd9e81a7e853edf130"
|
||||
integrity sha512-UVzCiDZH2j0wrr/O8nb1edD5cYLVqB5iujueXUCbHS3rAwIxgmyLtA3Hzm2QYsGPo/+xsIg1fNvpq9rNT6KWUA==
|
||||
dependencies:
|
||||
"@atproto/common-web" "^0.3.0"
|
||||
"@atproto/lexicon" "^0.4.1"
|
||||
"@atproto/lexicon" "^0.4.0"
|
||||
"@atproto/syntax" "^0.3.0"
|
||||
"@atproto/xrpc" "^0.6.1"
|
||||
await-lock "^2.2.2"
|
||||
"@atproto/xrpc" "^0.5.0"
|
||||
multiformats "^9.9.0"
|
||||
tlds "^1.234.0"
|
||||
|
||||
@@ -43,29 +42,29 @@
|
||||
uint8arrays "3.0.0"
|
||||
zod "^3.21.4"
|
||||
|
||||
"@atproto/lexicon@^0.4.1":
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.1.tgz#19155210570a2fafbcc7d4f655d9b813948e72a0"
|
||||
integrity sha512-bzyr+/VHXLQWbumViX5L7h1NKQObfs8Z+XZJl43OUK8nYFUI4e/sW1IZKRNfw7Wvi5YVNK+J+yP3DWIBZhkCYA==
|
||||
"@atproto/lexicon@^0.4.0":
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.0.tgz#63e8829945d80c25524882caa8ed27b1151cc576"
|
||||
integrity sha512-RvCBKdSI4M8qWm5uTNz1z3R2yIvIhmOsMuleOj8YR6BwRD+QbtUBy3l+xQ7iXf4M5fdfJFxaUNa6Ty0iRwdKqQ==
|
||||
dependencies:
|
||||
"@atproto/common-web" "^0.3.0"
|
||||
"@atproto/syntax" "^0.3.0"
|
||||
iso-datestring-validator "^2.2.2"
|
||||
multiformats "^9.9.0"
|
||||
zod "^3.23.8"
|
||||
zod "^3.21.4"
|
||||
|
||||
"@atproto/syntax@^0.3.0":
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.3.0.tgz#fafa2dbea9add37253005cb663e7373e05e618b3"
|
||||
integrity sha512-Weq0ZBxffGHDXHl9U7BQc2BFJi/e23AL+k+i5+D9hUq/bzT4yjGsrCejkjq0xt82xXDjmhhvQSZ0LqxyZ5woxA==
|
||||
|
||||
"@atproto/xrpc@^0.6.1":
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.6.1.tgz#dcd1315c8c60eef5af2db7fa4e35a38ebc6d79d5"
|
||||
integrity sha512-Zy5ydXEdk6sY7FDUZcEVfCL1jvbL4tXu5CcdPqbEaW6LQtk9GLds/DK1bCX9kswTGaBC88EMuqQMfkxOhp2t4A==
|
||||
"@atproto/xrpc@^0.5.0":
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.5.0.tgz#dacbfd8f7b13f0ab5bd56f8fdd4b460e132a6032"
|
||||
integrity sha512-swu+wyOLvYW4l3n+VAuJbHcPcES+tin2Lsrp8Bw5aIXIICiuFn1YMFlwK9JwVUzTH21Py1s1nHEjr4CJeElJog==
|
||||
dependencies:
|
||||
"@atproto/lexicon" "^0.4.1"
|
||||
zod "^3.23.8"
|
||||
"@atproto/lexicon" "^0.4.0"
|
||||
zod "^3.21.4"
|
||||
|
||||
"@babel/code-frame@^7.23.5", "@babel/code-frame@^7.24.1", "@babel/code-frame@^7.24.2":
|
||||
version "7.24.2"
|
||||
@@ -1711,11 +1710,6 @@ available-typed-arrays@^1.0.7:
|
||||
dependencies:
|
||||
possible-typed-array-names "^1.0.0"
|
||||
|
||||
await-lock@^2.2.2:
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/await-lock/-/await-lock-2.2.2.tgz#a95a9b269bfd2f69d22b17a321686f551152bcef"
|
||||
integrity sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==
|
||||
|
||||
babel-plugin-polyfill-corejs2@^0.4.10:
|
||||
version "0.4.10"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.10.tgz#276f41710b03a64f6467433cab72cbc2653c38b1"
|
||||
@@ -3736,16 +3730,8 @@ stack-trace@^1.0.0-pre2:
|
||||
resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-1.0.0-pre2.tgz#46a83a79f1b287807e9aaafc6a5dd8bcde626f9c"
|
||||
integrity sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A==
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^4.1.0:
|
||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0:
|
||||
name string-width-cjs
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
@@ -3809,14 +3795,7 @@ string.prototype.trimstart@^1.0.8:
|
||||
define-properties "^1.2.1"
|
||||
es-object-atoms "^1.0.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
@@ -4024,10 +4003,10 @@ typed-array-length@^1.0.6:
|
||||
is-typed-array "^1.1.13"
|
||||
possible-typed-array-names "^1.0.0"
|
||||
|
||||
typescript@^5.5.4:
|
||||
version "5.5.4"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba"
|
||||
integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==
|
||||
typescript@^4.0.5:
|
||||
version "4.9.5"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"
|
||||
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
|
||||
|
||||
uint8arrays@3.0.0:
|
||||
version "3.0.0"
|
||||
@@ -4213,8 +4192,3 @@ zod@^3.21.4:
|
||||
version "3.22.4"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff"
|
||||
integrity sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==
|
||||
|
||||
zod@^3.23.8:
|
||||
version "3.23.8"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d"
|
||||
integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==
|
||||
|
||||
@@ -41,10 +41,10 @@ func run(args []string) {
|
||||
EnvVars: []string{"ATP_APPVIEW_HOST", "ATP_PDS_HOST"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "ogcard-host",
|
||||
Usage: "scheme, hostname, and port of ogcard service",
|
||||
Name: "ogcard-host",
|
||||
Usage: "scheme, hostname, and port of ogcard service",
|
||||
Required: false,
|
||||
EnvVars: []string{"OGCARD_HOST"},
|
||||
EnvVars: []string{"OGCARD_HOST"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "http-address",
|
||||
@@ -67,13 +67,6 @@ func run(args []string) {
|
||||
Required: false,
|
||||
EnvVars: []string{"DEBUG"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "basic-auth-password",
|
||||
Usage: "optional password to restrict access to web interface",
|
||||
Required: false,
|
||||
Value: "",
|
||||
EnvVars: []string{"BASIC_AUTH_PASSWORD"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
@@ -49,7 +48,6 @@ func serve(cctx *cli.Context) error {
|
||||
appviewHost := cctx.String("appview-host")
|
||||
ogcardHost := cctx.String("ogcard-host")
|
||||
linkHost := cctx.String("link-host")
|
||||
basicAuthPassword := cctx.String("basic-auth-password")
|
||||
|
||||
// Echo
|
||||
e := echo.New()
|
||||
@@ -142,18 +140,6 @@ func serve(cctx *cli.Context) error {
|
||||
},
|
||||
}))
|
||||
|
||||
// optional password gating of entire web interface
|
||||
if basicAuthPassword != "" {
|
||||
e.Use(middleware.BasicAuth(func(username, password string, c echo.Context) (bool, error) {
|
||||
// Be careful to use constant time comparison to prevent timing attacks
|
||||
if subtle.ConstantTimeCompare([]byte(username), []byte("admin")) == 1 &&
|
||||
subtle.ConstantTimeCompare([]byte(password), []byte(basicAuthPassword)) == 1 {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}))
|
||||
}
|
||||
|
||||
// redirect trailing slash to non-trailing slash.
|
||||
// all of our current endpoints have no trailing slash.
|
||||
e.Use(middleware.RemoveTrailingSlashWithConfig(middleware.TrailingSlashConfig{
|
||||
@@ -211,7 +197,6 @@ func serve(cctx *cli.Context) error {
|
||||
e.GET("/search", server.WebGeneric)
|
||||
e.GET("/feeds", server.WebGeneric)
|
||||
e.GET("/notifications", server.WebGeneric)
|
||||
e.GET("/notifications/settings", server.WebGeneric)
|
||||
e.GET("/lists", server.WebGeneric)
|
||||
e.GET("/moderation", server.WebGeneric)
|
||||
e.GET("/moderation/modlists", server.WebGeneric)
|
||||
@@ -225,7 +210,6 @@ func serve(cctx *cli.Context) error {
|
||||
e.GET("/settings/threads", server.WebGeneric)
|
||||
e.GET("/settings/external-embeds", server.WebGeneric)
|
||||
e.GET("/settings/accessibility", server.WebGeneric)
|
||||
e.GET("/settings/appearance", server.WebGeneric)
|
||||
e.GET("/sys/debug", server.WebGeneric)
|
||||
e.GET("/sys/debug-mod", server.WebGeneric)
|
||||
e.GET("/sys/log", server.WebGeneric)
|
||||
@@ -255,7 +239,6 @@ func serve(cctx *cli.Context) error {
|
||||
e.GET("/profile/:handleOrDID/post/:rkey", server.WebPost)
|
||||
e.GET("/profile/:handleOrDID/post/:rkey/liked-by", server.WebGeneric)
|
||||
e.GET("/profile/:handleOrDID/post/:rkey/reposted-by", server.WebGeneric)
|
||||
e.GET("/profile/:handleOrDID/post/:rkey/quotes", server.WebGeneric)
|
||||
|
||||
// starter packs
|
||||
e.GET("/starter-pack/:handleOrDID/:rkey", server.WebStarterPack)
|
||||
|
||||
@@ -253,11 +253,6 @@
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
|
||||
.force-no-clicks > *,
|
||||
.force-no-clicks * {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
{% include "scripts.html" %}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import 'react-native-gesture-handler' // must be first
|
||||
import '#/platform/polyfills'
|
||||
|
||||
import {LogBox} from 'react-native'
|
||||
|
||||
import '#/platform/polyfills'
|
||||
import {IS_TEST} from '#/env'
|
||||
import {registerRootComponent} from 'expo'
|
||||
import {doPolyfill} from '#/lib/api/api-polyfill'
|
||||
|
||||
import App from '#/App'
|
||||
import {IS_TEST} from '#/env'
|
||||
|
||||
doPolyfill()
|
||||
|
||||
if (IS_TEST) {
|
||||
LogBox.ignoreAllLogs() // suppress all logs in tests
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import '#/platform/markBundleStartTime'
|
||||
|
||||
import '#/platform/polyfills'
|
||||
|
||||
import {registerRootComponent} from 'expo'
|
||||
|
||||
import {doPolyfill} from '#/lib/api/api-polyfill'
|
||||
import App from '#/App'
|
||||
|
||||
doPolyfill()
|
||||
registerRootComponent(App)
|
||||
|
||||
@@ -95,16 +95,3 @@ jest.mock('expo-application', () => ({
|
||||
nativeApplicationVersion: '1.0.0',
|
||||
nativeBuildVersion: '1',
|
||||
}))
|
||||
|
||||
jest.mock('expo-modules-core', () => ({
|
||||
requireNativeModule: jest.fn().mockImplementation(moduleName => {
|
||||
if (moduleName === 'ExpoPlatformInfo') {
|
||||
return {
|
||||
getIsReducedMotionEnabled: () => false,
|
||||
}
|
||||
}
|
||||
}),
|
||||
requireNativeViewManager: jest.fn().mockImplementation(moduleName => {
|
||||
return () => null
|
||||
}),
|
||||
}))
|
||||
|
||||
@@ -79,33 +79,32 @@ export async function createServer(
|
||||
plc: {port: port2},
|
||||
})
|
||||
|
||||
// DISABLED - looks like dev-env added this and now it conflicts
|
||||
// add the test mod authority
|
||||
// const agent = new BskyAgent({service: pdsUrl})
|
||||
// const res = await agent.api.com.atproto.server.createAccount({
|
||||
// email: 'mod-authority@test.com',
|
||||
// handle: 'mod-authority.test',
|
||||
// password: 'hunter2',
|
||||
// })
|
||||
// agent.api.setHeader('Authorization', `Bearer ${res.data.accessJwt}`)
|
||||
// await agent.api.app.bsky.actor.profile.create(
|
||||
// {repo: res.data.did},
|
||||
// {
|
||||
// displayName: 'Dev-env Moderation',
|
||||
// description: `The pretend version of mod.bsky.app`,
|
||||
// },
|
||||
// )
|
||||
const agent = new BskyAgent({service: pdsUrl})
|
||||
const res = await agent.api.com.atproto.server.createAccount({
|
||||
email: 'mod-authority@test.com',
|
||||
handle: 'mod-authority.test',
|
||||
password: 'hunter2',
|
||||
})
|
||||
agent.api.setHeader('Authorization', `Bearer ${res.data.accessJwt}`)
|
||||
await agent.api.app.bsky.actor.profile.create(
|
||||
{repo: res.data.did},
|
||||
{
|
||||
displayName: 'Dev-env Moderation',
|
||||
description: `The pretend version of mod.bsky.app`,
|
||||
},
|
||||
)
|
||||
|
||||
// await agent.api.app.bsky.labeler.service.create(
|
||||
// {repo: res.data.did, rkey: 'self'},
|
||||
// {
|
||||
// policies: {
|
||||
// labelValues: ['!hide', '!warn'],
|
||||
// labelValueDefinitions: [],
|
||||
// },
|
||||
// createdAt: new Date().toISOString(),
|
||||
// },
|
||||
// )
|
||||
await agent.api.app.bsky.labeler.service.create(
|
||||
{repo: res.data.did, rkey: 'self'},
|
||||
{
|
||||
policies: {
|
||||
labelValues: ['!hide', '!warn'],
|
||||
labelValueDefinitions: [],
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
)
|
||||
|
||||
const pic = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'assets', 'default-avatar.png'),
|
||||
@@ -157,7 +156,7 @@ class Mocker {
|
||||
}
|
||||
|
||||
async createUser(name: string) {
|
||||
const agent = new BskyAgent({service: this.service})
|
||||
const agent = new BskyAgent({service: this.agent.service})
|
||||
|
||||
const inviteRes = await agent.api.com.atproto.server.createInviteCode(
|
||||
{useCount: 1},
|
||||
@@ -333,7 +332,7 @@ class Mocker {
|
||||
}
|
||||
|
||||
async createInvite(forAccount: string) {
|
||||
const agent = new BskyAgent({service: this.service})
|
||||
const agent = new BskyAgent({service: this.agent.service})
|
||||
await agent.api.com.atproto.server.createInviteCode(
|
||||
{useCount: 1, forAccount},
|
||||
{
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package expo.modules.blueskyswissarmy.platforminfo
|
||||
|
||||
import android.provider.Settings
|
||||
import expo.modules.kotlin.modules.Module
|
||||
import expo.modules.kotlin.modules.ModuleDefinition
|
||||
|
||||
class ExpoPlatformInfoModule : Module() {
|
||||
override fun definition() =
|
||||
ModuleDefinition {
|
||||
Name("ExpoPlatformInfo")
|
||||
|
||||
// See https://github.com/software-mansion/react-native-reanimated/blob/7df5fd57d608fe25724608835461cd925ff5151d/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/nativeProxy/NativeProxyCommon.java#L242
|
||||
Function("getIsReducedMotionEnabled") {
|
||||
val resolver = appContext.reactContext?.contentResolver ?: return@Function false
|
||||
val scale = Settings.Global.getString(resolver, Settings.Global.TRANSITION_ANIMATION_SCALE) ?: return@Function false
|
||||
|
||||
try {
|
||||
return@Function scale.toFloat() == 0f
|
||||
} catch (_: Error) {
|
||||
return@Function false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
package expo.modules.blueskyswissarmy.referrer
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import com.android.installreferrer.api.InstallReferrerClient
|
||||
import com.android.installreferrer.api.InstallReferrerStateListener
|
||||
@@ -11,53 +8,10 @@ import expo.modules.kotlin.modules.Module
|
||||
import expo.modules.kotlin.modules.ModuleDefinition
|
||||
|
||||
class ExpoBlueskyReferrerModule : Module() {
|
||||
private var intent: Intent? = null
|
||||
private var activityReferrer: Uri? = null
|
||||
|
||||
override fun definition() =
|
||||
ModuleDefinition {
|
||||
Name("ExpoBlueskyReferrer")
|
||||
|
||||
OnNewIntent {
|
||||
intent = it
|
||||
activityReferrer = appContext.currentActivity?.referrer
|
||||
}
|
||||
|
||||
Function("getReferrerInfo") {
|
||||
val intentReferrer =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
intent?.getParcelableExtra(Intent.EXTRA_REFERRER, Uri::class.java)
|
||||
} else {
|
||||
intent?.getParcelableExtra(Intent.EXTRA_REFERRER)
|
||||
}
|
||||
|
||||
// Some apps explicitly set a referrer, like Chrome. In these cases, we prefer this since
|
||||
// it's the actual website that the user came from rather than the app.
|
||||
if (intentReferrer is Uri) {
|
||||
val res =
|
||||
mapOf(
|
||||
"referrer" to intentReferrer.toString(),
|
||||
"hostname" to intentReferrer.host,
|
||||
)
|
||||
intent = null
|
||||
return@Function res
|
||||
}
|
||||
|
||||
// In all other cases, we'll just record the app that sent the intent.
|
||||
if (activityReferrer != null) {
|
||||
// referrer could become null here. `.toString()` though can be called on null
|
||||
val res =
|
||||
mapOf(
|
||||
"referrer" to activityReferrer.toString(),
|
||||
"hostname" to (activityReferrer?.host ?: ""),
|
||||
)
|
||||
activityReferrer = null
|
||||
return@Function res
|
||||
}
|
||||
|
||||
return@Function null
|
||||
}
|
||||
|
||||
AsyncFunction("getGooglePlayReferrerInfoAsync") { promise: Promise ->
|
||||
val referrerClient = InstallReferrerClient.newBuilder(appContext.reactContext).build()
|
||||
referrerClient.startConnection(
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
package expo.modules.blueskyswissarmy.visibilityview
|
||||
|
||||
import expo.modules.kotlin.modules.Module
|
||||
import expo.modules.kotlin.modules.ModuleDefinition
|
||||
|
||||
class ExpoBlueskyVisibilityViewModule : Module() {
|
||||
override fun definition() =
|
||||
ModuleDefinition {
|
||||
Name("ExpoBlueskyVisibilityView")
|
||||
|
||||
AsyncFunction("updateActiveViewAsync") {
|
||||
VisibilityViewManager.updateActiveView()
|
||||
}
|
||||
|
||||
View(VisibilityView::class) {
|
||||
Events(arrayOf("onChangeStatus"))
|
||||
|
||||
Prop("enabled") { view: VisibilityView, prop: Boolean ->
|
||||
view.isViewEnabled = prop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package expo.modules.blueskyswissarmy.visibilityview
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import expo.modules.kotlin.AppContext
|
||||
import expo.modules.kotlin.viewevent.EventDispatcher
|
||||
import expo.modules.kotlin.views.ExpoView
|
||||
|
||||
class VisibilityView(
|
||||
context: Context,
|
||||
appContext: AppContext,
|
||||
) : ExpoView(context, appContext) {
|
||||
var isViewEnabled: Boolean = false
|
||||
|
||||
private val onChangeStatus by EventDispatcher()
|
||||
|
||||
private var isCurrentlyActive = false
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
VisibilityViewManager.addView(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
VisibilityViewManager.removeView(this)
|
||||
}
|
||||
|
||||
fun setIsCurrentlyActive(isActive: Boolean) {
|
||||
if (isCurrentlyActive == isActive) {
|
||||
return
|
||||
}
|
||||
|
||||
this.isCurrentlyActive = isActive
|
||||
this.onChangeStatus(
|
||||
mapOf(
|
||||
"isActive" to isActive,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun getPositionOnScreen(): Rect? {
|
||||
if (!this.isShown) {
|
||||
return null
|
||||
}
|
||||
|
||||
val screenPosition = intArrayOf(0, 0)
|
||||
this.getLocationInWindow(screenPosition)
|
||||
return Rect(
|
||||
screenPosition[0],
|
||||
screenPosition[1],
|
||||
screenPosition[0] + this.width,
|
||||
screenPosition[1] + this.height,
|
||||
)
|
||||
}
|
||||
|
||||
fun isViewableEnough(): Boolean {
|
||||
val positionOnScreen = this.getPositionOnScreen() ?: return false
|
||||
val visibleArea = positionOnScreen.width() * positionOnScreen.height()
|
||||
val totalArea = this.width * this.height
|
||||
return visibleArea >= 0.5 * totalArea
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package expo.modules.blueskyswissarmy.visibilityview
|
||||
|
||||
import android.graphics.Rect
|
||||
|
||||
class VisibilityViewManager {
|
||||
companion object {
|
||||
private val views = HashMap<Int, VisibilityView>()
|
||||
private var currentlyActiveView: VisibilityView? = null
|
||||
private var prevCount = 0
|
||||
|
||||
fun addView(view: VisibilityView) {
|
||||
this.views[view.id] = view
|
||||
|
||||
if (this.prevCount == 0) {
|
||||
this.updateActiveView()
|
||||
}
|
||||
this.prevCount = this.views.count()
|
||||
}
|
||||
|
||||
fun removeView(view: VisibilityView) {
|
||||
this.views.remove(view.id)
|
||||
this.prevCount = this.views.count()
|
||||
}
|
||||
|
||||
fun updateActiveView() {
|
||||
var activeView: VisibilityView? = null
|
||||
val count = this.views.count()
|
||||
|
||||
if (count == 1) {
|
||||
val view = this.views.values.first()
|
||||
if (view.isViewableEnough()) {
|
||||
activeView = view
|
||||
}
|
||||
} else if (count > 1) {
|
||||
val views = this.views.values
|
||||
var mostVisibleView: VisibilityView? = null
|
||||
var mostVisiblePosition: Rect? = null
|
||||
|
||||
views.forEach { view ->
|
||||
if (!view.isViewableEnough()) {
|
||||
return
|
||||
}
|
||||
|
||||
val position = view.getPositionOnScreen() ?: return@forEach
|
||||
val topY = position.centerY() - (position.height() / 2)
|
||||
|
||||
if (topY >= 150) {
|
||||
if (mostVisiblePosition == null) {
|
||||
mostVisiblePosition = position
|
||||
}
|
||||
|
||||
if (position.centerY() <= mostVisiblePosition!!.centerY()) {
|
||||
mostVisibleView = view
|
||||
mostVisiblePosition = position
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
activeView = mostVisibleView
|
||||
}
|
||||
|
||||
if (activeView == this.currentlyActiveView) {
|
||||
return
|
||||
}
|
||||
|
||||
this.clearActiveView()
|
||||
if (activeView != null) {
|
||||
this.setActiveView(activeView)
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearActiveView() {
|
||||
this.currentlyActiveView?.setIsCurrentlyActive(false)
|
||||
this.currentlyActiveView = null
|
||||
}
|
||||
|
||||
private fun setActiveView(view: VisibilityView) {
|
||||
view.setIsCurrentlyActive(true)
|
||||
this.currentlyActiveView = view
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,12 @@
|
||||
{
|
||||
"platforms": ["ios", "tvos", "android", "web"],
|
||||
"ios": {
|
||||
"modules": [
|
||||
"ExpoBlueskySharedPrefsModule",
|
||||
"ExpoBlueskyReferrerModule",
|
||||
"ExpoBlueskyVisibilityViewModule",
|
||||
"ExpoPlatformInfoModule"
|
||||
]
|
||||
"modules": ["ExpoBlueskySharedPrefsModule", "ExpoBlueskyReferrerModule"]
|
||||
},
|
||||
"android": {
|
||||
"modules": [
|
||||
"expo.modules.blueskyswissarmy.sharedprefs.ExpoBlueskySharedPrefsModule",
|
||||
"expo.modules.blueskyswissarmy.referrer.ExpoBlueskyReferrerModule",
|
||||
"expo.modules.blueskyswissarmy.visibilityview.ExpoBlueskyVisibilityViewModule",
|
||||
"expo.modules.blueskyswissarmy.platforminfo.ExpoPlatformInfoModule"
|
||||
"expo.modules.blueskyswissarmy.referrer.ExpoBlueskyReferrerModule"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import * as PlatformInfo from './src/PlatformInfo'
|
||||
import {AudioCategory} from './src/PlatformInfo/types'
|
||||
import * as Referrer from './src/Referrer'
|
||||
import * as SharedPrefs from './src/SharedPrefs'
|
||||
import VisibilityView from './src/VisibilityView'
|
||||
|
||||
export {AudioCategory, PlatformInfo, Referrer, SharedPrefs, VisibilityView}
|
||||
export {Referrer, SharedPrefs}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import ExpoModulesCore
|
||||
|
||||
public class ExpoPlatformInfoModule: Module {
|
||||
private var prevAudioActive: Bool?
|
||||
private var prevAudioCategory: AVAudioSession.Category?
|
||||
|
||||
public func definition() -> ModuleDefinition {
|
||||
Name("ExpoPlatformInfo")
|
||||
|
||||
Function("getIsReducedMotionEnabled") {
|
||||
return UIAccessibility.isReduceMotionEnabled
|
||||
}
|
||||
|
||||
Function("setAudioCategory") { (audioCategoryString: String) in
|
||||
let audioCategory = AVAudioSession.Category(rawValue: audioCategoryString)
|
||||
if audioCategory == self.prevAudioCategory {
|
||||
return
|
||||
}
|
||||
self.prevAudioCategory = audioCategory
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
try? AVAudioSession.sharedInstance().setCategory(audioCategory)
|
||||
}
|
||||
}
|
||||
|
||||
Function("setAudioActive") { (active: Bool) in
|
||||
if active == self.prevAudioActive {
|
||||
return
|
||||
}
|
||||
self.prevAudioActive = active
|
||||
if active {
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
try? AVAudioSession.sharedInstance().setActive(true)
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
try? AVAudioSession
|
||||
.sharedInstance()
|
||||
.setActive(
|
||||
false,
|
||||
options: [.notifyOthersOnDeactivation]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import ExpoModulesCore
|
||||
|
||||
public class ExpoBlueskyVisibilityViewModule: Module {
|
||||
public func definition() -> ModuleDefinition {
|
||||
Name("ExpoBlueskyVisibilityView")
|
||||
|
||||
AsyncFunction("updateActiveViewAsync") {
|
||||
VisibilityViewManager.shared.updateActiveView()
|
||||
}
|
||||
|
||||
View(VisibilityView.self) {
|
||||
Events([
|
||||
"onChangeStatus"
|
||||
])
|
||||
|
||||
Prop("enabled") { (view: VisibilityView, prop: Bool) in
|
||||
view.enabled = prop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
class VisibilityViewManager {
|
||||
static let shared = VisibilityViewManager()
|
||||
|
||||
private let views = NSHashTable<VisibilityView>(options: .weakMemory)
|
||||
private var currentlyActiveView: VisibilityView?
|
||||
private var screenHeight: CGFloat = UIScreen.main.bounds.height
|
||||
private var prevCount = 0
|
||||
|
||||
func addView(_ view: VisibilityView) {
|
||||
self.views.add(view)
|
||||
|
||||
if self.prevCount == 0 {
|
||||
self.updateActiveView()
|
||||
}
|
||||
self.prevCount = self.views.count
|
||||
}
|
||||
|
||||
func removeView(_ view: VisibilityView) {
|
||||
self.views.remove(view)
|
||||
self.prevCount = self.views.count
|
||||
}
|
||||
|
||||
func updateActiveView() {
|
||||
DispatchQueue.main.async {
|
||||
var activeView: VisibilityView?
|
||||
|
||||
if self.views.count == 1 {
|
||||
let view = self.views.allObjects[0]
|
||||
if view.isViewableEnough() {
|
||||
activeView = view
|
||||
}
|
||||
} else if self.views.count > 1 {
|
||||
let views = self.views.allObjects
|
||||
var mostVisibleView: VisibilityView?
|
||||
var mostVisiblePosition: CGRect?
|
||||
|
||||
views.forEach { view in
|
||||
if !view.isViewableEnough() {
|
||||
return
|
||||
}
|
||||
|
||||
guard let position = view.getPositionOnScreen() else {
|
||||
return
|
||||
}
|
||||
|
||||
if position.minY >= 150 {
|
||||
if mostVisiblePosition == nil {
|
||||
mostVisiblePosition = position
|
||||
}
|
||||
|
||||
if let unwrapped = mostVisiblePosition,
|
||||
position.minY <= unwrapped.minY {
|
||||
mostVisibleView = view
|
||||
mostVisiblePosition = position
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
activeView = mostVisibleView
|
||||
}
|
||||
|
||||
if activeView == self.currentlyActiveView {
|
||||
return
|
||||
}
|
||||
|
||||
self.clearActiveView()
|
||||
if let view = activeView {
|
||||
self.setActiveView(view)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func clearActiveView() {
|
||||
if let currentlyActiveView = self.currentlyActiveView {
|
||||
currentlyActiveView.setIsCurrentlyActive(isActive: false)
|
||||
self.currentlyActiveView = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func setActiveView(_ view: VisibilityView) {
|
||||
view.setIsCurrentlyActive(isActive: true)
|
||||
self.currentlyActiveView = view
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import ExpoModulesCore
|
||||
|
||||
class VisibilityView: ExpoView {
|
||||
var enabled = false {
|
||||
didSet {
|
||||
if enabled {
|
||||
VisibilityViewManager.shared.removeView(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let onChangeStatus = EventDispatcher()
|
||||
private var isCurrentlyActiveView = false
|
||||
|
||||
required init(appContext: AppContext? = nil) {
|
||||
super.init(appContext: appContext)
|
||||
}
|
||||
|
||||
public override func willMove(toWindow newWindow: UIWindow?) {
|
||||
super.willMove(toWindow: newWindow)
|
||||
|
||||
if !self.enabled {
|
||||
return
|
||||
}
|
||||
|
||||
if newWindow == nil {
|
||||
VisibilityViewManager.shared.removeView(self)
|
||||
} else {
|
||||
VisibilityViewManager.shared.addView(self)
|
||||
}
|
||||
}
|
||||
|
||||
func setIsCurrentlyActive(isActive: Bool) {
|
||||
if isCurrentlyActiveView == isActive {
|
||||
return
|
||||
}
|
||||
self.isCurrentlyActiveView = isActive
|
||||
self.onChangeStatus([
|
||||
"isActive": isActive
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
// 🚨 DANGER 🚨
|
||||
// These functions need to be called from the main thread. Xcode will warn you if you call one of them
|
||||
// off the main thread, so pay attention!
|
||||
extension UIView {
|
||||
func getPositionOnScreen() -> CGRect? {
|
||||
if let window = self.window {
|
||||
return self.convert(self.bounds, to: window)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isViewableEnough() -> Bool {
|
||||
guard let window = self.window else {
|
||||
return false
|
||||
}
|
||||
|
||||
let viewFrameOnScreen = self.convert(self.bounds, to: window)
|
||||
let screenBounds = window.bounds
|
||||
let intersection = viewFrameOnScreen.intersection(screenBounds)
|
||||
|
||||
let viewHeight = viewFrameOnScreen.height
|
||||
let intersectionHeight = intersection.height
|
||||
|
||||
return intersectionHeight >= 0.5 * viewHeight
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import {Platform} from 'react-native'
|
||||
import {requireNativeModule} from 'expo-modules-core'
|
||||
|
||||
import {AudioCategory} from './types'
|
||||
|
||||
const NativeModule = requireNativeModule('ExpoPlatformInfo')
|
||||
|
||||
export function getIsReducedMotionEnabled(): boolean {
|
||||
return NativeModule.getIsReducedMotionEnabled()
|
||||
}
|
||||
|
||||
export function setAudioActive(active: boolean): void {
|
||||
if (Platform.OS !== 'ios') return
|
||||
NativeModule.setAudioActive(active)
|
||||
}
|
||||
|
||||
export function setAudioCategory(audioCategory: AudioCategory): void {
|
||||
if (Platform.OS !== 'ios') return
|
||||
NativeModule.setAudioCategory(audioCategory)
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import {NotImplementedError} from '../NotImplemented'
|
||||
import {AudioCategory} from './types'
|
||||
|
||||
export function getIsReducedMotionEnabled(): boolean {
|
||||
throw new NotImplementedError()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether the app's audio should mix with other apps' audio. Will also resume background music playback when `false`
|
||||
* if it was previously playing.
|
||||
* @param mixWithOthers
|
||||
* @see https://developer.apple.com/documentation/avfaudio/avaudiosession/setactiveoptions/1616603-notifyothersondeactivation
|
||||
*/
|
||||
export function setAudioActive(active: boolean): void {
|
||||
throw new NotImplementedError({active})
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the audio category for the app.
|
||||
* @param audioCategory
|
||||
* @platform ios
|
||||
*/
|
||||
export function setAudioCategory(audioCategory: AudioCategory): void {
|
||||
throw new NotImplementedError({audioCategory})
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import {NotImplementedError} from '../NotImplemented'
|
||||
import {AudioCategory} from './types'
|
||||
|
||||
export function getIsReducedMotionEnabled(): boolean {
|
||||
if (typeof window === 'undefined') {
|
||||
return false
|
||||
}
|
||||
return window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
}
|
||||
|
||||
export function setAudioActive(active: boolean): void {
|
||||
throw new NotImplementedError({active})
|
||||
}
|
||||
|
||||
export function setAudioCategory(audioCategory: AudioCategory): void {
|
||||
throw new NotImplementedError({audioCategory})
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
/**
|
||||
* Sets the audio session category on iOS. In general, we should only need to use this for the `playback` and `ambient`
|
||||
* categories. This enum however includes other categories that are available in the native API for clarity and
|
||||
* potential future use.
|
||||
* @see https://developer.apple.com/documentation/avfoundation/avaudiosession/category
|
||||
* @platform ios
|
||||
*/
|
||||
export enum AudioCategory {
|
||||
Ambient = 'AVAudioSessionCategoryAmbient',
|
||||
Playback = 'AVAudioSessionCategoryPlayback',
|
||||
_SoloAmbient = 'AVAudioSessionCategorySoloAmbient',
|
||||
_Record = 'AVAudioSessionCategoryRecord',
|
||||
_PlayAndRecord = 'AVAudioSessionCategoryPlayAndRecord',
|
||||
_MultiRoute = 'AVAudioSessionCategoryMultiRoute',
|
||||
}
|
||||
@@ -1,13 +1,9 @@
|
||||
import {requireNativeModule} from 'expo'
|
||||
|
||||
import {GooglePlayReferrerInfo, ReferrerInfo} from './types'
|
||||
import {GooglePlayReferrerInfo} from './types'
|
||||
|
||||
export const NativeModule = requireNativeModule('ExpoBlueskyReferrer')
|
||||
|
||||
export function getGooglePlayReferrerInfoAsync(): Promise<GooglePlayReferrerInfo | null> {
|
||||
export function getGooglePlayReferrerInfoAsync(): Promise<GooglePlayReferrerInfo> {
|
||||
return NativeModule.getGooglePlayReferrerInfoAsync()
|
||||
}
|
||||
|
||||
export function getReferrerInfo(): Promise<ReferrerInfo | null> {
|
||||
return NativeModule.getReferrerInfo()
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import {SharedPrefs} from '../../index'
|
||||
import {NotImplementedError} from '../NotImplemented'
|
||||
import {GooglePlayReferrerInfo, ReferrerInfo} from './types'
|
||||
|
||||
export function getGooglePlayReferrerInfoAsync(): Promise<GooglePlayReferrerInfo> {
|
||||
throw new NotImplementedError()
|
||||
}
|
||||
|
||||
export function getReferrerInfo(): ReferrerInfo | null {
|
||||
const referrer = SharedPrefs.getString('referrer')
|
||||
if (referrer) {
|
||||
SharedPrefs.removeValue('referrer')
|
||||
try {
|
||||
const url = new URL(referrer)
|
||||
return {
|
||||
referrer,
|
||||
hostname: url.hostname,
|
||||
}
|
||||
} catch (e) {
|
||||
return {
|
||||
referrer,
|
||||
hostname: referrer,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const referrerApp = SharedPrefs.getString('referrerApp')
|
||||
if (referrerApp) {
|
||||
SharedPrefs.removeValue('referrerApp')
|
||||
return {
|
||||
referrer: referrerApp,
|
||||
hostname: referrerApp,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
import {NotImplementedError} from '../NotImplemented'
|
||||
import {GooglePlayReferrerInfo, ReferrerInfo} from './types'
|
||||
import {GooglePlayReferrerInfo} from './types'
|
||||
|
||||
// @ts-ignore throws
|
||||
export function getGooglePlayReferrerInfoAsync(): Promise<GooglePlayReferrerInfo> {
|
||||
throw new NotImplementedError()
|
||||
}
|
||||
|
||||
export function getReferrerInfo(): ReferrerInfo | null {
|
||||
throw new NotImplementedError()
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import {Platform} from 'react-native'
|
||||
|
||||
import {NotImplementedError} from '../NotImplemented'
|
||||
import {GooglePlayReferrerInfo, ReferrerInfo} from './types'
|
||||
|
||||
export function getGooglePlayReferrerInfoAsync(): Promise<GooglePlayReferrerInfo> {
|
||||
throw new NotImplementedError()
|
||||
}
|
||||
|
||||
export function getReferrerInfo(): ReferrerInfo | null {
|
||||
if (
|
||||
Platform.OS === 'web' &&
|
||||
// for ssr
|
||||
typeof document !== 'undefined' &&
|
||||
document != null &&
|
||||
document.referrer
|
||||
) {
|
||||
try {
|
||||
const url = new URL(document.referrer)
|
||||
if (url.hostname !== 'bsky.app') {
|
||||
return {
|
||||
referrer: url.href,
|
||||
hostname: url.hostname,
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// If something happens to the URL parsing, we don't want to actually cause any problems for the user. Just
|
||||
// log the error so we might catch it
|
||||
console.error('Failed to parse referrer URL')
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
export type GooglePlayReferrerInfo = {
|
||||
installReferrer?: string
|
||||
clickTimestamp?: number
|
||||
installTimestamp?: number
|
||||
}
|
||||
|
||||
export type ReferrerInfo = {
|
||||
referrer: string
|
||||
hostname: string
|
||||
}
|
||||
export type GooglePlayReferrerInfo =
|
||||
| {
|
||||
installReferrer?: string
|
||||
clickTimestamp?: number
|
||||
installTimestamp?: number
|
||||
}
|
||||
| undefined
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import React from 'react'
|
||||
import {StyleProp, ViewStyle} from 'react-native'
|
||||
import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core'
|
||||
|
||||
import {VisibilityViewProps} from './types'
|
||||
const NativeView: React.ComponentType<{
|
||||
onChangeStatus: (e: {nativeEvent: {isActive: boolean}}) => void
|
||||
children: React.ReactNode
|
||||
enabled: Boolean
|
||||
style: StyleProp<ViewStyle>
|
||||
}> = requireNativeViewManager('ExpoBlueskyVisibilityView')
|
||||
|
||||
const NativeModule = requireNativeModule('ExpoBlueskyVisibilityView')
|
||||
|
||||
export async function updateActiveViewAsync() {
|
||||
await NativeModule.updateActiveViewAsync()
|
||||
}
|
||||
|
||||
export default function VisibilityView({
|
||||
children,
|
||||
onChangeStatus: onChangeStatusOuter,
|
||||
enabled,
|
||||
}: VisibilityViewProps) {
|
||||
const onChangeStatus = React.useCallback(
|
||||
(e: {nativeEvent: {isActive: boolean}}) => {
|
||||
onChangeStatusOuter(e.nativeEvent.isActive)
|
||||
},
|
||||
[onChangeStatusOuter],
|
||||
)
|
||||
|
||||
return (
|
||||
<NativeView
|
||||
onChangeStatus={onChangeStatus}
|
||||
enabled={enabled}
|
||||
style={{flex: 1}}>
|
||||
{children}
|
||||
</NativeView>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import {NotImplementedError} from '../NotImplemented'
|
||||
import {VisibilityViewProps} from './types'
|
||||
|
||||
export async function updateActiveViewAsync() {
|
||||
throw new NotImplementedError()
|
||||
}
|
||||
|
||||
export default function VisibilityView({children}: VisibilityViewProps) {
|
||||
return children
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import React from 'react'
|
||||
export interface VisibilityViewProps {
|
||||
children: React.ReactNode
|
||||
onChangeStatus: (isActive: boolean) => void
|
||||
enabled: boolean
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bsky.app",
|
||||
"version": "1.91.0",
|
||||
"version": "1.89.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -32,7 +32,7 @@
|
||||
"lint-native": "swiftlint ./modules && ktlint ./modules",
|
||||
"lint-native:fix": "swiftlint --fix ./modules && ktlint --format ./modules",
|
||||
"typecheck": "tsc --project ./tsconfig.check.json",
|
||||
"e2e:mock-server": "NODE_ENV=development ./jest/dev-infra/with-test-redis-and-db.sh ts-node --project tsconfig.e2e.json __e2e__/mock-server.ts",
|
||||
"e2e:mock-server": "./jest/dev-infra/with-test-redis-and-db.sh ts-node --project tsconfig.e2e.json __e2e__/mock-server.ts",
|
||||
"e2e:metro": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios",
|
||||
"e2e:metro-android": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:android",
|
||||
"e2e:run": "maestro test __e2e__",
|
||||
@@ -52,7 +52,7 @@
|
||||
"open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "0.13.5",
|
||||
"@atproto/api": "^0.12.23",
|
||||
"@bam.tech/react-native-image-resizer": "^3.0.4",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||
@@ -139,11 +139,10 @@
|
||||
"expo-system-ui": "~3.0.4",
|
||||
"expo-task-manager": "~11.8.1",
|
||||
"expo-updates": "~0.25.14",
|
||||
"expo-video": "https://github.com/bluesky-social/expo/raw/expo-video-1.2.4-patch/packages/expo-video/expo-video-v1.2.4-1.tgz",
|
||||
"expo-video": "^1.1.10",
|
||||
"expo-web-browser": "~13.0.3",
|
||||
"fast-text-encoding": "^1.0.6",
|
||||
"history": "^5.3.0",
|
||||
"hls.js": "^1.5.11",
|
||||
"js-sha256": "^0.9.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lande": "^1.0.10",
|
||||
@@ -180,7 +179,6 @@
|
||||
"react-native-image-crop-picker": "0.40.3",
|
||||
"react-native-ios-context-menu": "^1.15.3",
|
||||
"react-native-keyboard-controller": "^1.12.1",
|
||||
"react-native-mmkv": "^2.12.2",
|
||||
"react-native-pager-view": "6.2.3",
|
||||
"react-native-picker-select": "^9.1.3",
|
||||
"react-native-progress": "bluesky-social/react-native-progress",
|
||||
@@ -200,6 +198,7 @@
|
||||
"react-responsive": "^9.0.2",
|
||||
"react-textarea-autosize": "^8.5.3",
|
||||
"rn-fetch-blob": "^0.12.0",
|
||||
"rn-tourguide": "bluesky-social/rn-tourguide",
|
||||
"sentry-expo": "~7.0.1",
|
||||
"statsig-react-native-expo": "^4.6.1",
|
||||
"tippy.js": "^6.3.7",
|
||||
@@ -208,7 +207,7 @@
|
||||
"zod": "^3.20.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@atproto/dev-env": "^0.3.39",
|
||||
"@atproto/dev-env": "^0.3.5",
|
||||
"@babel/core": "^7.23.2",
|
||||
"@babel/preset-env": "^7.20.0",
|
||||
"@babel/runtime": "^7.20.0",
|
||||
@@ -270,7 +269,7 @@
|
||||
"react-scripts": "^5.0.1",
|
||||
"react-test-renderer": "18.2.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.5.4",
|
||||
"typescript": "^5.3.3",
|
||||
"url-loader": "^4.1.1",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-bundle-analyzer": "^4.10.1",
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
diff --git a/node_modules/@atproto/lexicon/dist/validators/complex.js b/node_modules/@atproto/lexicon/dist/validators/complex.js
|
||||
index 32d7798..9d688b7 100644
|
||||
--- a/node_modules/@atproto/lexicon/dist/validators/complex.js
|
||||
+++ b/node_modules/@atproto/lexicon/dist/validators/complex.js
|
||||
@@ -113,7 +113,22 @@ function object(lexicons, path, def, value) {
|
||||
if (value[key] === null && nullableProps.has(key)) {
|
||||
continue;
|
||||
}
|
||||
- const propDef = def.properties[key];
|
||||
+ const propDef = def.properties[key]
|
||||
+ if (typeof value[key] === 'undefined' && !requiredProps.has(key)) {
|
||||
+ // Fast path for non-required undefined props.
|
||||
+ if (
|
||||
+ propDef.type === 'integer' ||
|
||||
+ propDef.type === 'boolean' ||
|
||||
+ propDef.type === 'string'
|
||||
+ ) {
|
||||
+ if (typeof propDef.default === 'undefined') {
|
||||
+ continue
|
||||
+ }
|
||||
+ } else {
|
||||
+ // Other types have no defaults.
|
||||
+ continue
|
||||
+ }
|
||||
+ }
|
||||
const propPath = `${path}/${key}`;
|
||||
const validated = (0, util_1.validateOneOf)(lexicons, propPath, propDef, value[key]);
|
||||
const propValue = validated.success ? validated.value : value[key];
|
||||
@@ -12,28 +12,3 @@ index bb74e80..0aa0202 100644
|
||||
|
||||
Map<String, Object> constants = new HashMap<>(3);
|
||||
constants.put(MODULES_CONSTANTS_KEY, new HashMap<>());
|
||||
diff --git a/node_modules/expo-modules-core/build/uuid/uuid.js b/node_modules/expo-modules-core/build/uuid/uuid.js
|
||||
index 109d3fe..c421931 100644
|
||||
--- a/node_modules/expo-modules-core/build/uuid/uuid.js
|
||||
+++ b/node_modules/expo-modules-core/build/uuid/uuid.js
|
||||
@@ -1,5 +1,7 @@
|
||||
import bytesToUuid from './lib/bytesToUuid';
|
||||
import { Uuidv5Namespace } from './uuid.types';
|
||||
+import { ensureNativeModulesAreInstalled } from '../ensureNativeModulesAreInstalled';
|
||||
+ensureNativeModulesAreInstalled();
|
||||
const nativeUuidv4 = globalThis?.expo?.uuidv4;
|
||||
const nativeUuidv5 = globalThis?.expo?.uuidv5;
|
||||
function uuidv4() {
|
||||
diff --git a/node_modules/expo-modules-core/ios/Core/SharedObjects/SharedObjectRegistry.swift b/node_modules/expo-modules-core/ios/Core/SharedObjects/SharedObjectRegistry.swift
|
||||
index ee2268a..4851b67 100644
|
||||
--- a/node_modules/expo-modules-core/ios/Core/SharedObjects/SharedObjectRegistry.swift
|
||||
+++ b/node_modules/expo-modules-core/ios/Core/SharedObjects/SharedObjectRegistry.swift
|
||||
@@ -173,7 +173,7 @@ public final class SharedObjectRegistry {
|
||||
}
|
||||
|
||||
internal func clear() {
|
||||
- Self.lockQueue.async {
|
||||
+ Self.lockQueue.sync {
|
||||
self.pairs.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,3 +207,31 @@ index 88b3fdf..2488ebc 100644
|
||||
|
||||
const { layout, entering, exiting, sharedTransitionTag } = this.props;
|
||||
if (
|
||||
diff --git a/node_modules/react-native-reanimated/lib/module/reanimated2/index.js b/node_modules/react-native-reanimated/lib/module/reanimated2/index.js
|
||||
index ac9be5d..86d4605 100644
|
||||
--- a/node_modules/react-native-reanimated/lib/module/reanimated2/index.js
|
||||
+++ b/node_modules/react-native-reanimated/lib/module/reanimated2/index.js
|
||||
@@ -47,4 +47,5 @@ export { LayoutAnimationConfig } from './component/LayoutAnimationConfig';
|
||||
export { PerformanceMonitor } from './component/PerformanceMonitor';
|
||||
export { startMapper, stopMapper } from './mappers';
|
||||
export { startScreenTransition, finishScreenTransition, ScreenTransition } from './screenTransition';
|
||||
+export { isReducedMotion } from './PlatformChecker';
|
||||
//# sourceMappingURL=index.js.map
|
||||
diff --git a/node_modules/react-native-reanimated/lib/typescript/reanimated2/index.d.ts b/node_modules/react-native-reanimated/lib/typescript/reanimated2/index.d.ts
|
||||
index f01dc57..161ef22 100644
|
||||
--- a/node_modules/react-native-reanimated/lib/typescript/reanimated2/index.d.ts
|
||||
+++ b/node_modules/react-native-reanimated/lib/typescript/reanimated2/index.d.ts
|
||||
@@ -36,3 +36,4 @@ export type { FlatListPropsWithLayout } from './component/FlatList';
|
||||
export { startMapper, stopMapper } from './mappers';
|
||||
export { startScreenTransition, finishScreenTransition, ScreenTransition, } from './screenTransition';
|
||||
export type { AnimatedScreenTransition, GoBackGesture, ScreenTransitionConfig, } from './screenTransition';
|
||||
+export { isReducedMotion } from './PlatformChecker';
|
||||
diff --git a/node_modules/react-native-reanimated/src/reanimated2/index.ts b/node_modules/react-native-reanimated/src/reanimated2/index.ts
|
||||
index 5885fa1..a3c693f 100644
|
||||
--- a/node_modules/react-native-reanimated/src/reanimated2/index.ts
|
||||
+++ b/node_modules/react-native-reanimated/src/reanimated2/index.ts
|
||||
@@ -284,3 +284,4 @@ export type {
|
||||
GoBackGesture,
|
||||
ScreenTransitionConfig,
|
||||
} from './screenTransition';
|
||||
+export { isReducedMotion } from './PlatformChecker';
|
||||
|
||||
@@ -57,7 +57,7 @@ const withXcodeTarget = (config, {targetName}) => {
|
||||
buildSettingsObj.SWIFT_VERSION = '5.0'
|
||||
buildSettingsObj.TARGETED_DEVICE_FAMILY = `"1"`
|
||||
buildSettingsObj.DEVELOPMENT_TEAM = 'B3LX46C5HS'
|
||||
buildSettingsObj.IPHONEOS_DEPLOYMENT_TARGET = '15.1'
|
||||
buildSettingsObj.IPHONEOS_DEPLOYMENT_TARGET = '14.0'
|
||||
buildSettingsObj.ASSETCATALOG_COMPILER_APPICON_NAME = 'AppIcon'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
const {withAppDelegate} = require('@expo/config-plugins')
|
||||
const {mergeContents} = require('@expo/config-plugins/build/utils/generateCode')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
|
||||
module.exports = config => {
|
||||
// eslint-disable-next-line no-shadow
|
||||
return withAppDelegate(config, async config => {
|
||||
const delegatePath = path.join(
|
||||
config.modRequest.platformProjectRoot,
|
||||
'AppDelegate.mm',
|
||||
)
|
||||
|
||||
let newContents = config.modResults.contents
|
||||
newContents = mergeContents({
|
||||
src: newContents,
|
||||
anchor: '// Linking API',
|
||||
newSrc: `
|
||||
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
||||
[defaults setObject:options[UIApplicationOpenURLOptionsSourceApplicationKey] forKey:@"referrerApp"];\n`,
|
||||
offset: 2,
|
||||
tag: 'referrer info - deep links',
|
||||
comment: '//',
|
||||
}).contents
|
||||
|
||||
newContents = mergeContents({
|
||||
src: newContents,
|
||||
anchor: '// Universal Links',
|
||||
newSrc: `
|
||||
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
||||
[defaults setURL:userActivity.referrerURL forKey:@"referrer"];\n`,
|
||||
offset: 2,
|
||||
tag: 'referrer info - universal links',
|
||||
comment: '//',
|
||||
}).contents
|
||||
|
||||
config.modResults.contents = newContents
|
||||
|
||||
return config
|
||||
})
|
||||
}
|
||||
@@ -2,6 +2,7 @@ const path = require('path')
|
||||
const fs = require('fs')
|
||||
|
||||
const projectRoot = path.join(__dirname, '..')
|
||||
const webBuildJs = path.join(projectRoot, 'web-build', 'static', 'js')
|
||||
const templateFile = path.join(
|
||||
projectRoot,
|
||||
'bskyweb',
|
||||
@@ -9,18 +10,18 @@ const templateFile = path.join(
|
||||
'scripts.html',
|
||||
)
|
||||
|
||||
const {entrypoints} = require(path.join(
|
||||
projectRoot,
|
||||
'web-build/asset-manifest.json',
|
||||
))
|
||||
const jsFiles = fs.readdirSync(webBuildJs).filter(name => name.endsWith('.js'))
|
||||
jsFiles.sort((a, b) => {
|
||||
// make sure main is written last
|
||||
if (a.startsWith('main')) return 1
|
||||
if (b.startsWith('main')) return -1
|
||||
return a.localeCompare(b)
|
||||
})
|
||||
|
||||
console.log(`Found ${entrypoints.length} entrypoints`)
|
||||
console.log(`Found ${jsFiles.length} js files in web-build`)
|
||||
console.log(`Writing ${templateFile}`)
|
||||
|
||||
const outputFile = entrypoints
|
||||
.map(name => {
|
||||
const file = path.basename(name)
|
||||
return `<script defer="defer" src="/static/js/${file}"></script>`
|
||||
})
|
||||
const outputFile = jsFiles
|
||||
.map(name => `<script defer="defer" src="/static/js/${name}"></script>`)
|
||||
.join('\n')
|
||||
fs.writeFileSync(templateFile, outputFile)
|
||||
|
||||
@@ -23,12 +23,10 @@ import {
|
||||
} from '#/lib/statsig/statsig'
|
||||
import {s} from '#/lib/styles'
|
||||
import {ThemeProvider} from '#/lib/ThemeContext'
|
||||
import I18nProvider from '#/locale/i18nProvider'
|
||||
import {logger} from '#/logger'
|
||||
import {Provider as A11yProvider} from '#/state/a11y'
|
||||
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
|
||||
import {Provider as DialogStateProvider} from '#/state/dialogs'
|
||||
import {listenSessionDropped} from '#/state/events'
|
||||
import {Provider as InvitesStateProvider} from '#/state/invites'
|
||||
import {Provider as LightboxStateProvider} from '#/state/lightbox'
|
||||
import {MessagesProvider} from '#/state/messages'
|
||||
@@ -50,9 +48,7 @@ import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out'
|
||||
import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
|
||||
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
|
||||
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
||||
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
|
||||
import {TestCtrls} from '#/view/com/testing/TestCtrls'
|
||||
import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoNativeContext'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {Shell} from '#/view/shell'
|
||||
import {ThemeProvider as Alf} from '#/alf'
|
||||
@@ -60,8 +56,10 @@ import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
|
||||
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
||||
import {Provider as PortalProvider} from '#/components/Portal'
|
||||
import {Splash} from '#/Splash'
|
||||
import {Provider as TourProvider} from '#/tours'
|
||||
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||
import {AudioCategory, PlatformInfo} from '../modules/expo-bluesky-swiss-army'
|
||||
import I18nProvider from './locale/i18nProvider'
|
||||
import {listenSessionDropped} from './state/events'
|
||||
|
||||
SplashScreen.preventAutoHideAsync()
|
||||
|
||||
@@ -97,10 +95,7 @@ function InnerApp() {
|
||||
|
||||
useEffect(() => {
|
||||
return listenSessionDropped(() => {
|
||||
Toast.show(
|
||||
_(msg`Sorry! Your session expired. Please log in again.`),
|
||||
'info',
|
||||
)
|
||||
Toast.show(_(msg`Sorry! Your session expired. Please log in again.`))
|
||||
})
|
||||
}, [_])
|
||||
|
||||
@@ -109,44 +104,42 @@ function InnerApp() {
|
||||
<Alf theme={theme}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<Splash isReady={isReady && hasCheckedReferrer}>
|
||||
<ActiveVideoProvider>
|
||||
<RootSiblingParent>
|
||||
<React.Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<QueryProvider currentDid={currentAccount?.did}>
|
||||
<StatsigProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<HiddenRepliesProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<ProgressGuideProvider>
|
||||
<GestureHandlerRootView
|
||||
style={s.h100pct}>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
</GestureHandlerRootView>
|
||||
</ProgressGuideProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</HiddenRepliesProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</StatsigProvider>
|
||||
</QueryProvider>
|
||||
</React.Fragment>
|
||||
</RootSiblingParent>
|
||||
</ActiveVideoProvider>
|
||||
<RootSiblingParent>
|
||||
<React.Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<QueryProvider currentDid={currentAccount?.did}>
|
||||
<StatsigProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<TourProvider>
|
||||
<ProgressGuideProvider>
|
||||
<GestureHandlerRootView
|
||||
style={s.h100pct}>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
</GestureHandlerRootView>
|
||||
</ProgressGuideProvider>
|
||||
</TourProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</StatsigProvider>
|
||||
</QueryProvider>
|
||||
</React.Fragment>
|
||||
</RootSiblingParent>
|
||||
</Splash>
|
||||
</ThemeProvider>
|
||||
</Alf>
|
||||
@@ -158,8 +151,6 @@ function App() {
|
||||
const [isReady, setReady] = useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
PlatformInfo.setAudioCategory(AudioCategory.Ambient)
|
||||
PlatformInfo.setAudioActive(false)
|
||||
initPersistedState().then(() => setReady(true))
|
||||
}, [])
|
||||
|
||||
@@ -175,25 +166,25 @@ function App() {
|
||||
<A11yProvider>
|
||||
<KeyboardProvider enabled={false} statusBarTranslucent={true}>
|
||||
<SessionProvider>
|
||||
<PrefsStateProvider>
|
||||
<I18nProvider>
|
||||
<ShellStateProvider>
|
||||
<InvitesStateProvider>
|
||||
<ModalStateProvider>
|
||||
<DialogStateProvider>
|
||||
<LightboxStateProvider>
|
||||
<ShellStateProvider>
|
||||
<PrefsStateProvider>
|
||||
<InvitesStateProvider>
|
||||
<ModalStateProvider>
|
||||
<DialogStateProvider>
|
||||
<LightboxStateProvider>
|
||||
<I18nProvider>
|
||||
<PortalProvider>
|
||||
<StarterPackProvider>
|
||||
<InnerApp />
|
||||
</StarterPackProvider>
|
||||
</PortalProvider>
|
||||
</LightboxStateProvider>
|
||||
</DialogStateProvider>
|
||||
</ModalStateProvider>
|
||||
</InvitesStateProvider>
|
||||
</ShellStateProvider>
|
||||
</I18nProvider>
|
||||
</PrefsStateProvider>
|
||||
</I18nProvider>
|
||||
</LightboxStateProvider>
|
||||
</DialogStateProvider>
|
||||
</ModalStateProvider>
|
||||
</InvitesStateProvider>
|
||||
</PrefsStateProvider>
|
||||
</ShellStateProvider>
|
||||
</SessionProvider>
|
||||
</KeyboardProvider>
|
||||
</A11yProvider>
|
||||
|
||||
@@ -12,12 +12,10 @@ import {useIntentHandler} from '#/lib/hooks/useIntentHandler'
|
||||
import {QueryProvider} from '#/lib/react-query'
|
||||
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
|
||||
import {ThemeProvider} from '#/lib/ThemeContext'
|
||||
import I18nProvider from '#/locale/i18nProvider'
|
||||
import {logger} from '#/logger'
|
||||
import {Provider as A11yProvider} from '#/state/a11y'
|
||||
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
|
||||
import {Provider as DialogStateProvider} from '#/state/dialogs'
|
||||
import {listenSessionDropped} from '#/state/events'
|
||||
import {Provider as InvitesStateProvider} from '#/state/invites'
|
||||
import {Provider as LightboxStateProvider} from '#/state/lightbox'
|
||||
import {MessagesProvider} from '#/state/messages'
|
||||
@@ -39,8 +37,6 @@ import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out'
|
||||
import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
|
||||
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
|
||||
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
||||
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
|
||||
import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoWebContext'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {ToastContainer} from '#/view/com/util/Toast.web'
|
||||
import {Shell} from '#/view/shell/index'
|
||||
@@ -48,7 +44,10 @@ import {ThemeProvider as Alf} from '#/alf'
|
||||
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
|
||||
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
||||
import {Provider as PortalProvider} from '#/components/Portal'
|
||||
import {Provider as TourProvider} from '#/tours'
|
||||
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||
import I18nProvider from './locale/i18nProvider'
|
||||
import {listenSessionDropped} from './state/events'
|
||||
|
||||
function InnerApp() {
|
||||
const [isReady, setIsReady] = React.useState(false)
|
||||
@@ -78,10 +77,7 @@ function InnerApp() {
|
||||
|
||||
useEffect(() => {
|
||||
return listenSessionDropped(() => {
|
||||
Toast.show(
|
||||
_(msg`Sorry! Your session expired. Please log in again.`),
|
||||
'info',
|
||||
)
|
||||
Toast.show(_(msg`Sorry! Your session expired. Please log in again.`))
|
||||
})
|
||||
}, [_])
|
||||
|
||||
@@ -93,41 +89,39 @@ function InnerApp() {
|
||||
<Alf theme={theme}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<RootSiblingParent>
|
||||
<ActiveVideoProvider>
|
||||
<React.Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<QueryProvider currentDid={currentAccount?.did}>
|
||||
<StatsigProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<HiddenRepliesProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<SafeAreaProvider>
|
||||
<ProgressGuideProvider>
|
||||
<Shell />
|
||||
</ProgressGuideProvider>
|
||||
</SafeAreaProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</HiddenRepliesProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</StatsigProvider>
|
||||
</QueryProvider>
|
||||
</React.Fragment>
|
||||
<ToastContainer />
|
||||
</ActiveVideoProvider>
|
||||
<React.Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<QueryProvider currentDid={currentAccount?.did}>
|
||||
<StatsigProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<SafeAreaProvider>
|
||||
<TourProvider>
|
||||
<ProgressGuideProvider>
|
||||
<Shell />
|
||||
</ProgressGuideProvider>
|
||||
</TourProvider>
|
||||
</SafeAreaProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</StatsigProvider>
|
||||
</QueryProvider>
|
||||
</React.Fragment>
|
||||
<ToastContainer />
|
||||
</RootSiblingParent>
|
||||
</ThemeProvider>
|
||||
</Alf>
|
||||
@@ -153,25 +147,25 @@ function App() {
|
||||
return (
|
||||
<A11yProvider>
|
||||
<SessionProvider>
|
||||
<PrefsStateProvider>
|
||||
<I18nProvider>
|
||||
<ShellStateProvider>
|
||||
<InvitesStateProvider>
|
||||
<ModalStateProvider>
|
||||
<DialogStateProvider>
|
||||
<LightboxStateProvider>
|
||||
<ShellStateProvider>
|
||||
<PrefsStateProvider>
|
||||
<InvitesStateProvider>
|
||||
<ModalStateProvider>
|
||||
<DialogStateProvider>
|
||||
<LightboxStateProvider>
|
||||
<I18nProvider>
|
||||
<PortalProvider>
|
||||
<StarterPackProvider>
|
||||
<InnerApp />
|
||||
</StarterPackProvider>
|
||||
</PortalProvider>
|
||||
</LightboxStateProvider>
|
||||
</DialogStateProvider>
|
||||
</ModalStateProvider>
|
||||
</InvitesStateProvider>
|
||||
</ShellStateProvider>
|
||||
</I18nProvider>
|
||||
</PrefsStateProvider>
|
||||
</I18nProvider>
|
||||
</LightboxStateProvider>
|
||||
</DialogStateProvider>
|
||||
</ModalStateProvider>
|
||||
</InvitesStateProvider>
|
||||
</PrefsStateProvider>
|
||||
</ShellStateProvider>
|
||||
</SessionProvider>
|
||||
</A11yProvider>
|
||||
)
|
||||
|
||||
@@ -15,12 +15,10 @@ import {
|
||||
StackActions,
|
||||
} from '@react-navigation/native'
|
||||
|
||||
import {init as initAnalytics} from '#/lib/analytics/analytics'
|
||||
import {timeout} from '#/lib/async/timeout'
|
||||
import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle'
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {useWebScrollRestoration} from '#/lib/hooks/useWebScrollRestoration'
|
||||
import {buildStateObject} from '#/lib/routes/helpers'
|
||||
import {timeout} from 'lib/async/timeout'
|
||||
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {buildStateObject} from 'lib/routes/helpers'
|
||||
import {
|
||||
AllNavigatorParams,
|
||||
BottomTabNavigatorParams,
|
||||
@@ -30,72 +28,70 @@ import {
|
||||
MyProfileTabNavigatorParams,
|
||||
NotificationsTabNavigatorParams,
|
||||
SearchTabNavigatorParams,
|
||||
} from '#/lib/routes/types'
|
||||
import {RouteParams, State} from '#/lib/routes/types'
|
||||
import {attachRouteToLogEvents, logEvent} from '#/lib/statsig/statsig'
|
||||
import {bskyTitle} from '#/lib/strings/headings'
|
||||
import {isAndroid, isNative, isWeb} from '#/platform/detection'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {useUnreadNotifications} from '#/state/queries/notifications/unread'
|
||||
import {useSession} from '#/state/session'
|
||||
import {
|
||||
shouldRequestEmailConfirmation,
|
||||
snoozeEmailConfirmationPrompt,
|
||||
} from '#/state/shell/reminders'
|
||||
import {AccessibilitySettingsScreen} from '#/view/screens/AccessibilitySettings'
|
||||
import {AppPasswords} from '#/view/screens/AppPasswords'
|
||||
import {CommunityGuidelinesScreen} from '#/view/screens/CommunityGuidelines'
|
||||
import {CopyrightPolicyScreen} from '#/view/screens/CopyrightPolicy'
|
||||
import {DebugModScreen} from '#/view/screens/DebugMod'
|
||||
import {FeedsScreen} from '#/view/screens/Feeds'
|
||||
import {HomeScreen} from '#/view/screens/Home'
|
||||
import {LanguageSettingsScreen} from '#/view/screens/LanguageSettings'
|
||||
import {ListsScreen} from '#/view/screens/Lists'
|
||||
import {LogScreen} from '#/view/screens/Log'
|
||||
import {ModerationBlockedAccounts} from '#/view/screens/ModerationBlockedAccounts'
|
||||
import {ModerationModlistsScreen} from '#/view/screens/ModerationModlists'
|
||||
import {ModerationMutedAccounts} from '#/view/screens/ModerationMutedAccounts'
|
||||
import {NotFoundScreen} from '#/view/screens/NotFound'
|
||||
import {NotificationsScreen} from '#/view/screens/Notifications'
|
||||
import {NotificationsSettingsScreen} from '#/view/screens/NotificationsSettings'
|
||||
import {PostThreadScreen} from '#/view/screens/PostThread'
|
||||
} from 'lib/routes/types'
|
||||
import {RouteParams, State} from 'lib/routes/types'
|
||||
import {bskyTitle} from 'lib/strings/headings'
|
||||
import {isAndroid, isNative} from 'platform/detection'
|
||||
import {PreferencesExternalEmbeds} from '#/view/screens/PreferencesExternalEmbeds'
|
||||
import {PreferencesFollowingFeed} from '#/view/screens/PreferencesFollowingFeed'
|
||||
import {PreferencesThreads} from '#/view/screens/PreferencesThreads'
|
||||
import {PrivacyPolicyScreen} from '#/view/screens/PrivacyPolicy'
|
||||
import {ProfileScreen} from '#/view/screens/Profile'
|
||||
import {ProfileFeedScreen} from '#/view/screens/ProfileFeed'
|
||||
import {ProfileFeedLikedByScreen} from '#/view/screens/ProfileFeedLikedBy'
|
||||
import {ProfileFollowersScreen} from '#/view/screens/ProfileFollowers'
|
||||
import {ProfileFollowsScreen} from '#/view/screens/ProfileFollows'
|
||||
import {ProfileListScreen} from '#/view/screens/ProfileList'
|
||||
import {SavedFeeds} from '#/view/screens/SavedFeeds'
|
||||
import {SearchScreen} from '#/view/screens/Search'
|
||||
import {SettingsScreen} from '#/view/screens/Settings'
|
||||
import {Storybook} from '#/view/screens/Storybook'
|
||||
import {SupportScreen} from '#/view/screens/Support'
|
||||
import {TermsOfServiceScreen} from '#/view/screens/TermsOfService'
|
||||
import {BottomBar} from '#/view/shell/bottom-bar/BottomBar'
|
||||
import {createNativeStackNavigatorWithAuth} from '#/view/shell/createNativeStackNavigatorWithAuth'
|
||||
import {AppPasswords} from 'view/screens/AppPasswords'
|
||||
import {ModerationBlockedAccounts} from 'view/screens/ModerationBlockedAccounts'
|
||||
import {ModerationMutedAccounts} from 'view/screens/ModerationMutedAccounts'
|
||||
import {PreferencesFollowingFeed} from 'view/screens/PreferencesFollowingFeed'
|
||||
import {PreferencesThreads} from 'view/screens/PreferencesThreads'
|
||||
import {SavedFeeds} from 'view/screens/SavedFeeds'
|
||||
import {SharedPreferencesTesterScreen} from '#/screens/E2E/SharedPreferencesTesterScreen'
|
||||
import HashtagScreen from '#/screens/Hashtag'
|
||||
import {MessagesConversationScreen} from '#/screens/Messages/Conversation'
|
||||
import {MessagesScreen} from '#/screens/Messages/List'
|
||||
import {MessagesSettingsScreen} from '#/screens/Messages/Settings'
|
||||
import {ModerationScreen} from '#/screens/Moderation'
|
||||
import {PostLikedByScreen} from '#/screens/Post/PostLikedBy'
|
||||
import {PostQuotesScreen} from '#/screens/Post/PostQuotes'
|
||||
import {PostRepostedByScreen} from '#/screens/Post/PostRepostedBy'
|
||||
import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers'
|
||||
import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
|
||||
import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings'
|
||||
import {
|
||||
StarterPackScreen,
|
||||
StarterPackScreenShort,
|
||||
} from '#/screens/StarterPack/StarterPackScreen'
|
||||
import {Wizard} from '#/screens/StarterPack/Wizard'
|
||||
import {router} from '#/routes'
|
||||
import {Referrer} from '../modules/expo-bluesky-swiss-army'
|
||||
import {init as initAnalytics} from './lib/analytics/analytics'
|
||||
import {useWebScrollRestoration} from './lib/hooks/useWebScrollRestoration'
|
||||
import {attachRouteToLogEvents, logEvent} from './lib/statsig/statsig'
|
||||
import {router} from './routes'
|
||||
import {MessagesConversationScreen} from './screens/Messages/Conversation'
|
||||
import {MessagesScreen} from './screens/Messages/List'
|
||||
import {MessagesSettingsScreen} from './screens/Messages/Settings'
|
||||
import {useModalControls} from './state/modals'
|
||||
import {useUnreadNotifications} from './state/queries/notifications/unread'
|
||||
import {useSession} from './state/session'
|
||||
import {
|
||||
shouldRequestEmailConfirmation,
|
||||
snoozeEmailConfirmationPrompt,
|
||||
} from './state/shell/reminders'
|
||||
import {AccessibilitySettingsScreen} from './view/screens/AccessibilitySettings'
|
||||
import {CommunityGuidelinesScreen} from './view/screens/CommunityGuidelines'
|
||||
import {CopyrightPolicyScreen} from './view/screens/CopyrightPolicy'
|
||||
import {DebugModScreen} from './view/screens/DebugMod'
|
||||
import {FeedsScreen} from './view/screens/Feeds'
|
||||
import {HomeScreen} from './view/screens/Home'
|
||||
import {LanguageSettingsScreen} from './view/screens/LanguageSettings'
|
||||
import {ListsScreen} from './view/screens/Lists'
|
||||
import {LogScreen} from './view/screens/Log'
|
||||
import {ModerationModlistsScreen} from './view/screens/ModerationModlists'
|
||||
import {NotFoundScreen} from './view/screens/NotFound'
|
||||
import {NotificationsScreen} from './view/screens/Notifications'
|
||||
import {PostLikedByScreen} from './view/screens/PostLikedBy'
|
||||
import {PostRepostedByScreen} from './view/screens/PostRepostedBy'
|
||||
import {PostThreadScreen} from './view/screens/PostThread'
|
||||
import {PrivacyPolicyScreen} from './view/screens/PrivacyPolicy'
|
||||
import {ProfileScreen} from './view/screens/Profile'
|
||||
import {ProfileFeedScreen} from './view/screens/ProfileFeed'
|
||||
import {ProfileFeedLikedByScreen} from './view/screens/ProfileFeedLikedBy'
|
||||
import {ProfileFollowersScreen} from './view/screens/ProfileFollowers'
|
||||
import {ProfileFollowsScreen} from './view/screens/ProfileFollows'
|
||||
import {ProfileListScreen} from './view/screens/ProfileList'
|
||||
import {SearchScreen} from './view/screens/Search'
|
||||
import {SettingsScreen} from './view/screens/Settings'
|
||||
import {Storybook} from './view/screens/Storybook'
|
||||
import {SupportScreen} from './view/screens/Support'
|
||||
import {TermsOfServiceScreen} from './view/screens/TermsOfService'
|
||||
import {BottomBar} from './view/shell/bottom-bar/BottomBar'
|
||||
import {createNativeStackNavigatorWithAuth} from './view/shell/createNativeStackNavigatorWithAuth'
|
||||
|
||||
const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
|
||||
|
||||
@@ -213,13 +209,6 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
||||
title: title(msg`Post by @${route.params.name}`),
|
||||
})}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="PostQuotes"
|
||||
getComponent={() => PostQuotesScreen}
|
||||
options={({route}) => ({
|
||||
title: title(msg`Post by @${route.params.name}`),
|
||||
})}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="ProfileFeed"
|
||||
getComponent={() => ProfileFeedScreen}
|
||||
@@ -319,14 +308,6 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
||||
requireAuth: true,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="AppearanceSettings"
|
||||
getComponent={() => AppearanceSettingsScreen}
|
||||
options={{
|
||||
title: title(msg`Appearance Settings`),
|
||||
requireAuth: true,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Hashtag"
|
||||
getComponent={() => HashtagScreen}
|
||||
@@ -342,11 +323,6 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
||||
getComponent={() => MessagesSettingsScreen}
|
||||
options={{title: title(msg`Chat settings`), requireAuth: true}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="NotificationsSettings"
|
||||
getComponent={() => NotificationsSettingsScreen}
|
||||
options={{title: title(msg`Notification settings`), requireAuth: true}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Feeds"
|
||||
getComponent={() => FeedsScreen}
|
||||
@@ -490,7 +466,6 @@ function MyProfileTabNavigator() {
|
||||
getComponent={() => ProfileScreen}
|
||||
initialParams={{
|
||||
name: 'me',
|
||||
hideBackButton: true,
|
||||
}}
|
||||
/>
|
||||
{commonScreens(MyProfileTab as typeof HomeTab)}
|
||||
@@ -794,17 +769,6 @@ function logModuleInitTime() {
|
||||
initMs,
|
||||
})
|
||||
|
||||
if (isWeb) {
|
||||
const referrerInfo = Referrer.getReferrerInfo()
|
||||
if (referrerInfo && referrerInfo.hostname !== 'bsky.app') {
|
||||
logEvent('deepLink:referrerReceived', {
|
||||
to: window.location.href,
|
||||
referrer: referrerInfo?.referrer,
|
||||
hostname: referrerInfo?.hostname,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
// This log is noisy, so keep false committed
|
||||
const shouldLog = false
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import {Platform, StyleSheet, ViewStyle} from 'react-native'
|
||||
import {Platform, StyleSheet} from 'react-native'
|
||||
|
||||
import * as tokens from '#/alf/tokens'
|
||||
import {native, web} from '#/alf/util/platform'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
|
||||
export const atoms = {
|
||||
debug: {
|
||||
borderColor: 'red',
|
||||
borderWidth: 1,
|
||||
},
|
||||
|
||||
/*
|
||||
* Positioning
|
||||
*/
|
||||
@@ -60,19 +56,6 @@ export const atoms = {
|
||||
height: '100vh',
|
||||
}),
|
||||
|
||||
/**
|
||||
* Used for the outermost components on screens, to ensure that they can fill
|
||||
* the screen and extend beyond.
|
||||
*/
|
||||
util_screen_outer: [
|
||||
web({
|
||||
minHeight: '100vh',
|
||||
}),
|
||||
native({
|
||||
height: '100%',
|
||||
}),
|
||||
] as ViewStyle,
|
||||
|
||||
/*
|
||||
* Theme-independent bg colors
|
||||
*/
|
||||
@@ -162,9 +145,6 @@ export const atoms = {
|
||||
flex_shrink: {
|
||||
flexShrink: 1,
|
||||
},
|
||||
flex_shrink_0: {
|
||||
flexShrink: 0,
|
||||
},
|
||||
justify_start: {
|
||||
justifyContent: 'flex-start',
|
||||
},
|
||||
@@ -301,19 +281,19 @@ export const atoms = {
|
||||
borderWidth: 0,
|
||||
},
|
||||
border: {
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderWidth: hairlineWidth,
|
||||
},
|
||||
border_t: {
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
borderTopWidth: hairlineWidth,
|
||||
},
|
||||
border_b: {
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomWidth: hairlineWidth,
|
||||
},
|
||||
border_l: {
|
||||
borderLeftWidth: StyleSheet.hairlineWidth,
|
||||
borderLeftWidth: hairlineWidth,
|
||||
},
|
||||
border_r: {
|
||||
borderRightWidth: StyleSheet.hairlineWidth,
|
||||
borderRightWidth: hairlineWidth,
|
||||
},
|
||||
|
||||
/*
|
||||
@@ -871,7 +851,6 @@ export const atoms = {
|
||||
mr_auto: {
|
||||
marginRight: 'auto',
|
||||
},
|
||||
|
||||
/*
|
||||
* Pointer events & user select
|
||||
*/
|
||||
@@ -890,7 +869,6 @@ export const atoms = {
|
||||
user_select_all: {
|
||||
userSelect: 'all',
|
||||
},
|
||||
|
||||
/*
|
||||
* Text decoration
|
||||
*/
|
||||
@@ -900,11 +878,4 @@ export const atoms = {
|
||||
strike_through: {
|
||||
textDecorationLine: 'line-through',
|
||||
},
|
||||
|
||||
/*
|
||||
* Display
|
||||
*/
|
||||
hidden: {
|
||||
display: 'none',
|
||||
},
|
||||
} as const
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import {useMediaQuery} from 'react-responsive'
|
||||
import {Dimensions} from 'react-native'
|
||||
|
||||
import {createThemes, defaultTheme} from '#/alf/themes'
|
||||
import {Theme, ThemeName} from '#/alf/types'
|
||||
@@ -12,15 +12,52 @@ export * from '#/alf/util/flatten'
|
||||
export * from '#/alf/util/platform'
|
||||
export * from '#/alf/util/themeSelector'
|
||||
|
||||
type BreakpointName = keyof typeof breakpoints
|
||||
|
||||
/*
|
||||
* Breakpoints
|
||||
*/
|
||||
const breakpoints: {
|
||||
[key: string]: number
|
||||
} = {
|
||||
gtPhone: 500,
|
||||
gtMobile: 800,
|
||||
gtTablet: 1300,
|
||||
}
|
||||
function getActiveBreakpoints({width}: {width: number}) {
|
||||
const active: (keyof typeof breakpoints)[] = Object.keys(breakpoints).filter(
|
||||
breakpoint => width >= breakpoints[breakpoint],
|
||||
)
|
||||
|
||||
return {
|
||||
active: active[active.length - 1],
|
||||
gtPhone: active.includes('gtPhone'),
|
||||
gtMobile: active.includes('gtMobile'),
|
||||
gtTablet: active.includes('gtTablet'),
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Context
|
||||
*/
|
||||
export const Context = React.createContext<{
|
||||
themeName: ThemeName
|
||||
theme: Theme
|
||||
breakpoints: {
|
||||
active: BreakpointName | undefined
|
||||
gtPhone: boolean
|
||||
gtMobile: boolean
|
||||
gtTablet: boolean
|
||||
}
|
||||
}>({
|
||||
themeName: 'light',
|
||||
theme: defaultTheme,
|
||||
breakpoints: {
|
||||
active: undefined,
|
||||
gtPhone: false,
|
||||
gtMobile: false,
|
||||
gtTablet: false,
|
||||
},
|
||||
})
|
||||
|
||||
export function ThemeProvider({
|
||||
@@ -37,6 +74,18 @@ export function ThemeProvider({
|
||||
})
|
||||
}, [])
|
||||
const theme = themes[themeName]
|
||||
const [breakpoints, setBreakpoints] = React.useState(() =>
|
||||
getActiveBreakpoints({width: Dimensions.get('window').width}),
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
const listener = Dimensions.addEventListener('change', ({window}) => {
|
||||
const bp = getActiveBreakpoints({width: window.width})
|
||||
if (bp.active !== breakpoints.active) setBreakpoints(bp)
|
||||
})
|
||||
|
||||
return listener.remove
|
||||
}, [breakpoints, setBreakpoints])
|
||||
|
||||
return (
|
||||
<Context.Provider
|
||||
@@ -44,8 +93,9 @@ export function ThemeProvider({
|
||||
() => ({
|
||||
themeName: themeName,
|
||||
theme: theme,
|
||||
breakpoints,
|
||||
}),
|
||||
[theme, themeName],
|
||||
[theme, themeName, breakpoints],
|
||||
)}>
|
||||
{children}
|
||||
</Context.Provider>
|
||||
@@ -57,12 +107,5 @@ export function useTheme() {
|
||||
}
|
||||
|
||||
export function useBreakpoints() {
|
||||
const gtPhone = useMediaQuery({minWidth: 500})
|
||||
const gtMobile = useMediaQuery({minWidth: 800})
|
||||
const gtTablet = useMediaQuery({minWidth: 1300})
|
||||
return {
|
||||
gtPhone,
|
||||
gtMobile,
|
||||
gtTablet,
|
||||
}
|
||||
return React.useContext(Context).breakpoints
|
||||
}
|
||||
|
||||
@@ -186,19 +186,19 @@ export function createThemes({
|
||||
white: color.gray_0,
|
||||
black: color.trueBlack,
|
||||
|
||||
contrast_25: color.gray_975,
|
||||
contrast_50: color.gray_950,
|
||||
contrast_100: color.gray_900,
|
||||
contrast_200: color.gray_800,
|
||||
contrast_300: color.gray_700,
|
||||
contrast_400: color.gray_600,
|
||||
contrast_500: color.gray_500,
|
||||
contrast_600: color.gray_400,
|
||||
contrast_700: color.gray_300,
|
||||
contrast_800: color.gray_200,
|
||||
contrast_900: color.gray_100,
|
||||
contrast_950: color.gray_50,
|
||||
contrast_975: color.gray_25,
|
||||
contrast_25: color.gray_1000,
|
||||
contrast_50: color.gray_975,
|
||||
contrast_100: color.gray_950,
|
||||
contrast_200: color.gray_900,
|
||||
contrast_300: color.gray_800,
|
||||
contrast_400: color.gray_700,
|
||||
contrast_500: color.gray_600,
|
||||
contrast_600: color.gray_500,
|
||||
contrast_700: color.gray_400,
|
||||
contrast_800: color.gray_300,
|
||||
contrast_900: color.gray_200,
|
||||
contrast_950: color.gray_100,
|
||||
contrast_975: color.gray_50,
|
||||
|
||||
primary_25: color.primary_975,
|
||||
primary_50: color.primary_950,
|
||||
@@ -261,23 +261,23 @@ export function createThemes({
|
||||
contrast_950: `hsl(${hues.primary}, 20%, ${dimScale[12]}%)`,
|
||||
contrast_975: `hsl(${hues.primary}, 20%, ${dimScale[13]}%)`,
|
||||
|
||||
primary_25: `hsl(${hues.primary}, 50%, ${dimScale[1]}%)`,
|
||||
primary_50: `hsl(${hues.primary}, 60%, ${dimScale[2]}%)`,
|
||||
primary_100: `hsl(${hues.primary}, 70%, ${dimScale[3]}%)`,
|
||||
primary_200: `hsl(${hues.primary}, 82%, ${dimScale[4]}%)`,
|
||||
primary_300: `hsl(${hues.primary}, 90%, ${dimScale[5]}%)`,
|
||||
primary_400: `hsl(${hues.primary}, 95%, ${dimScale[6]}%)`,
|
||||
primary_25: `hsl(${hues.primary}, 99%, ${dimScale[1]}%)`,
|
||||
primary_50: `hsl(${hues.primary}, 99%, ${dimScale[2]}%)`,
|
||||
primary_100: `hsl(${hues.primary}, 99%, ${dimScale[3]}%)`,
|
||||
primary_200: `hsl(${hues.primary}, 99%, ${dimScale[4]}%)`,
|
||||
primary_300: `hsl(${hues.primary}, 99%, ${dimScale[5]}%)`,
|
||||
primary_400: `hsl(${hues.primary}, 99%, ${dimScale[6]}%)`,
|
||||
primary_500: `hsl(${hues.primary}, 99%, ${dimScale[7]}%)`,
|
||||
primary_600: `hsl(${hues.primary}, 99%, ${dimScale[8]}%)`,
|
||||
primary_700: `hsl(${hues.primary}, 99%, ${dimScale[9]}%)`,
|
||||
primary_800: `hsl(${hues.primary}, 99%, ${dimScale[10]}%)`,
|
||||
primary_900: `hsl(${hues.primary}, 99%, ${dimScale[11]}%)`,
|
||||
primary_950: `hsl(${hues.primary}, 99%, ${dimScale[12]}%)`,
|
||||
primary_975: `hsl(${hues.primary}, 99%, ${dimScale[13]}%)`,
|
||||
primary_600: `hsl(${hues.primary}, 95%, ${dimScale[8]}%)`,
|
||||
primary_700: `hsl(${hues.primary}, 90%, ${dimScale[9]}%)`,
|
||||
primary_800: `hsl(${hues.primary}, 82%, ${dimScale[10]}%)`,
|
||||
primary_900: `hsl(${hues.primary}, 70%, ${dimScale[11]}%)`,
|
||||
primary_950: `hsl(${hues.primary}, 60%, ${dimScale[12]}%)`,
|
||||
primary_975: `hsl(${hues.primary}, 50%, ${dimScale[13]}%)`,
|
||||
|
||||
positive_25: `hsl(${hues.positive}, 50%, ${dimScale[1]}%)`,
|
||||
positive_50: `hsl(${hues.positive}, 60%, ${dimScale[2]}%)`,
|
||||
positive_100: `hsl(${hues.positive}, 70%, ${dimScale[3]}%)`,
|
||||
positive_25: `hsl(${hues.positive}, 82%, ${dimScale[1]}%)`,
|
||||
positive_50: `hsl(${hues.positive}, 82%, ${dimScale[2]}%)`,
|
||||
positive_100: `hsl(${hues.positive}, 82%, ${dimScale[3]}%)`,
|
||||
positive_200: `hsl(${hues.positive}, 82%, ${dimScale[4]}%)`,
|
||||
positive_300: `hsl(${hues.positive}, 82%, ${dimScale[5]}%)`,
|
||||
positive_400: `hsl(${hues.positive}, 82%, ${dimScale[6]}%)`,
|
||||
@@ -285,23 +285,23 @@ export function createThemes({
|
||||
positive_600: `hsl(${hues.positive}, 82%, ${dimScale[8]}%)`,
|
||||
positive_700: `hsl(${hues.positive}, 82%, ${dimScale[9]}%)`,
|
||||
positive_800: `hsl(${hues.positive}, 82%, ${dimScale[10]}%)`,
|
||||
positive_900: `hsl(${hues.positive}, 82%, ${dimScale[11]}%)`,
|
||||
positive_950: `hsl(${hues.positive}, 82%, ${dimScale[12]}%)`,
|
||||
positive_975: `hsl(${hues.positive}, 82%, ${dimScale[13]}%)`,
|
||||
positive_900: `hsl(${hues.positive}, 70%, ${dimScale[11]}%)`,
|
||||
positive_950: `hsl(${hues.positive}, 60%, ${dimScale[12]}%)`,
|
||||
positive_975: `hsl(${hues.positive}, 50%, ${dimScale[13]}%)`,
|
||||
|
||||
negative_25: `hsl(${hues.negative}, 70%, ${dimScale[1]}%)`,
|
||||
negative_50: `hsl(${hues.negative}, 80%, ${dimScale[2]}%)`,
|
||||
negative_100: `hsl(${hues.negative}, 84%, ${dimScale[3]}%)`,
|
||||
negative_200: `hsl(${hues.negative}, 88%, ${dimScale[4]}%)`,
|
||||
negative_25: `hsl(${hues.negative}, 91%, ${dimScale[1]}%)`,
|
||||
negative_50: `hsl(${hues.negative}, 91%, ${dimScale[2]}%)`,
|
||||
negative_100: `hsl(${hues.negative}, 91%, ${dimScale[3]}%)`,
|
||||
negative_200: `hsl(${hues.negative}, 91%, ${dimScale[4]}%)`,
|
||||
negative_300: `hsl(${hues.negative}, 91%, ${dimScale[5]}%)`,
|
||||
negative_400: `hsl(${hues.negative}, 91%, ${dimScale[6]}%)`,
|
||||
negative_500: `hsl(${hues.negative}, 91%, ${dimScale[7]}%)`,
|
||||
negative_600: `hsl(${hues.negative}, 91%, ${dimScale[8]}%)`,
|
||||
negative_700: `hsl(${hues.negative}, 91%, ${dimScale[9]}%)`,
|
||||
negative_800: `hsl(${hues.negative}, 91%, ${dimScale[10]}%)`,
|
||||
negative_900: `hsl(${hues.negative}, 91%, ${dimScale[11]}%)`,
|
||||
negative_950: `hsl(${hues.negative}, 91%, ${dimScale[12]}%)`,
|
||||
negative_975: `hsl(${hues.negative}, 91%, ${dimScale[13]}%)`,
|
||||
negative_800: `hsl(${hues.negative}, 88%, ${dimScale[10]}%)`,
|
||||
negative_900: `hsl(${hues.negative}, 84%, ${dimScale[11]}%)`,
|
||||
negative_950: `hsl(${hues.negative}, 80%, ${dimScale[12]}%)`,
|
||||
negative_975: `hsl(${hues.negative}, 70%, ${dimScale[13]}%)`,
|
||||
} as const
|
||||
|
||||
const light: Theme = {
|
||||
@@ -400,7 +400,7 @@ export function createThemes({
|
||||
color: darkPalette.contrast_400,
|
||||
},
|
||||
text_contrast_medium: {
|
||||
color: darkPalette.contrast_600,
|
||||
color: darkPalette.contrast_700,
|
||||
},
|
||||
text_contrast_high: {
|
||||
color: darkPalette.contrast_900,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
AccessibilityProps,
|
||||
GestureResponderEvent,
|
||||
MouseEvent,
|
||||
Pressable,
|
||||
PressableProps,
|
||||
StyleProp,
|
||||
@@ -67,22 +65,11 @@ type NonTextElements =
|
||||
|
||||
export type ButtonProps = Pick<
|
||||
PressableProps,
|
||||
| 'disabled'
|
||||
| 'onPress'
|
||||
| 'testID'
|
||||
| 'onLongPress'
|
||||
| 'hitSlop'
|
||||
| 'onHoverIn'
|
||||
| 'onHoverOut'
|
||||
| 'onPressIn'
|
||||
| 'onPressOut'
|
||||
'disabled' | 'onPress' | 'testID' | 'onLongPress' | 'hitSlop'
|
||||
> &
|
||||
AccessibilityProps &
|
||||
VariantProps & {
|
||||
testID?: string
|
||||
/**
|
||||
* For a11y, try to make this descriptive and clear
|
||||
*/
|
||||
label: string
|
||||
style?: StyleProp<ViewStyle>
|
||||
hoverStyle?: StyleProp<ViewStyle>
|
||||
@@ -125,50 +112,30 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
focused: false,
|
||||
})
|
||||
|
||||
const onPressInOuter = rest.onPressIn
|
||||
const onPressIn = React.useCallback(
|
||||
(e: GestureResponderEvent) => {
|
||||
setState(s => ({
|
||||
...s,
|
||||
pressed: true,
|
||||
}))
|
||||
onPressInOuter?.(e)
|
||||
},
|
||||
[setState, onPressInOuter],
|
||||
)
|
||||
const onPressOutOuter = rest.onPressOut
|
||||
const onPressOut = React.useCallback(
|
||||
(e: GestureResponderEvent) => {
|
||||
setState(s => ({
|
||||
...s,
|
||||
pressed: false,
|
||||
}))
|
||||
onPressOutOuter?.(e)
|
||||
},
|
||||
[setState, onPressOutOuter],
|
||||
)
|
||||
const onHoverInOuter = rest.onHoverIn
|
||||
const onHoverIn = React.useCallback(
|
||||
(e: MouseEvent) => {
|
||||
setState(s => ({
|
||||
...s,
|
||||
hovered: true,
|
||||
}))
|
||||
onHoverInOuter?.(e)
|
||||
},
|
||||
[setState, onHoverInOuter],
|
||||
)
|
||||
const onHoverOutOuter = rest.onHoverOut
|
||||
const onHoverOut = React.useCallback(
|
||||
(e: MouseEvent) => {
|
||||
setState(s => ({
|
||||
...s,
|
||||
hovered: false,
|
||||
}))
|
||||
onHoverOutOuter?.(e)
|
||||
},
|
||||
[setState, onHoverOutOuter],
|
||||
)
|
||||
const onPressIn = React.useCallback(() => {
|
||||
setState(s => ({
|
||||
...s,
|
||||
pressed: true,
|
||||
}))
|
||||
}, [setState])
|
||||
const onPressOut = React.useCallback(() => {
|
||||
setState(s => ({
|
||||
...s,
|
||||
pressed: false,
|
||||
}))
|
||||
}, [setState])
|
||||
const onHoverIn = React.useCallback(() => {
|
||||
setState(s => ({
|
||||
...s,
|
||||
hovered: true,
|
||||
}))
|
||||
}, [setState])
|
||||
const onHoverOut = React.useCallback(() => {
|
||||
setState(s => ({
|
||||
...s,
|
||||
hovered: false,
|
||||
}))
|
||||
}, [setState])
|
||||
const onFocus = React.useCallback(() => {
|
||||
setState(s => ({
|
||||
...s,
|
||||
@@ -232,10 +199,28 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
} else if (color === 'secondary') {
|
||||
if (variant === 'solid') {
|
||||
if (!disabled) {
|
||||
baseStyles.push(t.atoms.bg_contrast_25)
|
||||
hoverStyles.push(t.atoms.bg_contrast_50)
|
||||
baseStyles.push({
|
||||
backgroundColor: select(t.name, {
|
||||
light: t.palette.contrast_25,
|
||||
dim: t.palette.contrast_100,
|
||||
dark: t.palette.contrast_100,
|
||||
}),
|
||||
})
|
||||
hoverStyles.push({
|
||||
backgroundColor: select(t.name, {
|
||||
light: t.palette.contrast_50,
|
||||
dim: t.palette.contrast_200,
|
||||
dark: t.palette.contrast_200,
|
||||
}),
|
||||
})
|
||||
} else {
|
||||
baseStyles.push(t.atoms.bg_contrast_100)
|
||||
baseStyles.push({
|
||||
backgroundColor: select(t.name, {
|
||||
light: t.palette.contrast_100,
|
||||
dim: t.palette.contrast_25,
|
||||
dark: t.palette.contrast_25,
|
||||
}),
|
||||
})
|
||||
}
|
||||
} else if (variant === 'outline') {
|
||||
baseStyles.push(a.border, t.atoms.bg, {
|
||||
|
||||
@@ -2,18 +2,21 @@ import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/core'
|
||||
import {StackActions} from '@react-navigation/native'
|
||||
|
||||
import {useGoBack} from 'lib/hooks/useGoBack'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
import {CenteredView} from 'view/com/util/Views'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {router} from '#/routes'
|
||||
|
||||
export function Error({
|
||||
title,
|
||||
message,
|
||||
onRetry,
|
||||
onGoBack,
|
||||
onGoBack: onGoBackProp,
|
||||
hideBackButton,
|
||||
sideBorders = true,
|
||||
}: {
|
||||
@@ -24,10 +27,31 @@ export function Error({
|
||||
hideBackButton?: boolean
|
||||
sideBorders?: boolean
|
||||
}) {
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const goBack = useGoBack(onGoBack)
|
||||
|
||||
const canGoBack = navigation.canGoBack()
|
||||
const onGoBack = React.useCallback(() => {
|
||||
if (onGoBackProp) {
|
||||
onGoBackProp()
|
||||
return
|
||||
}
|
||||
if (canGoBack) {
|
||||
navigation.goBack()
|
||||
} else {
|
||||
navigation.navigate('HomeTab')
|
||||
|
||||
// Checking the state for routes ensures that web doesn't encounter errors while going back
|
||||
if (navigation.getState()?.routes) {
|
||||
navigation.dispatch(StackActions.push(...router.matchPath('/')))
|
||||
} else {
|
||||
navigation.navigate('HomeTab')
|
||||
navigation.dispatch(StackActions.popToTop())
|
||||
}
|
||||
}
|
||||
}, [navigation, canGoBack, onGoBackProp])
|
||||
|
||||
return (
|
||||
<CenteredView
|
||||
@@ -72,7 +96,7 @@ export function Error({
|
||||
variant="solid"
|
||||
color={onRetry ? 'secondary' : 'primary'}
|
||||
label={_(msg`Return to previous page`)}
|
||||
onPress={goBack}
|
||||
onPress={onGoBack}
|
||||
size="large"
|
||||
style={[a.rounded_sm, a.overflow_hidden, {paddingVertical: 10}]}>
|
||||
<ButtonText>
|
||||
|
||||
@@ -40,7 +40,7 @@ type Props = {
|
||||
export function Default(props: Props) {
|
||||
const {view} = props
|
||||
return (
|
||||
<Link {...props}>
|
||||
<Link label={view.displayName} {...props}>
|
||||
<Outer>
|
||||
<Header>
|
||||
<Avatar src={view.avatar} />
|
||||
@@ -58,7 +58,7 @@ export function Link({
|
||||
view,
|
||||
children,
|
||||
...props
|
||||
}: Props & Omit<LinkProps, 'to' | 'label'>) {
|
||||
}: Props & Omit<LinkProps, 'to'>) {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const href = React.useMemo(() => {
|
||||
@@ -70,11 +70,7 @@ export function Link({
|
||||
}, [view, queryClient])
|
||||
|
||||
return (
|
||||
<InternalLink
|
||||
label={view.displayName}
|
||||
to={href}
|
||||
style={[a.flex_col]}
|
||||
{...props}>
|
||||
<InternalLink to={href} style={[a.flex_col]} {...props}>
|
||||
{children}
|
||||
</InternalLink>
|
||||
)
|
||||
@@ -268,9 +264,9 @@ function SaveButtonInner({
|
||||
])
|
||||
}
|
||||
Toast.show(_(msg`Feeds updated!`))
|
||||
} catch (err: any) {
|
||||
logger.error(err, {context: `FeedCard: failed to update feeds`, pin})
|
||||
Toast.show(_(msg`Failed to update feeds`), 'xmark')
|
||||
} catch (e: any) {
|
||||
logger.error(e, {context: `FeedCard: failed to update feeds`, pin})
|
||||
Toast.show(_(msg`Failed to update feeds`))
|
||||
}
|
||||
},
|
||||
[_, pin, saveFeeds, removeFeed, uri, savedFeedConfig, type],
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {ScrollView} from 'react-native-gesture-handler'
|
||||
import {AppBskyActorDefs, AppBskyFeedDefs, AtUri} from '@atproto/api'
|
||||
import {AppBskyFeedDefs, AtUri} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
import {useGate} from '#/lib/statsig/statsig'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {logger} from '#/logger'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useGetPopularFeedsQuery} from '#/state/queries/feed'
|
||||
import {FeedDescriptor} from '#/state/queries/post-feed'
|
||||
import {useProfilesQuery} from '#/state/queries/profile'
|
||||
import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useProgressGuide} from '#/state/shell/progress-guide'
|
||||
import * as userActionHistory from '#/state/userActionHistory'
|
||||
@@ -95,16 +92,14 @@ function getRank(seenPost: SeenPost): string {
|
||||
tier = 'a'
|
||||
} else if (seenPost.feedContext?.startsWith('cluster')) {
|
||||
tier = 'b'
|
||||
} else if (seenPost.feedContext === 'popcluster') {
|
||||
tier = 'c'
|
||||
} else if (seenPost.feedContext?.startsWith('ntpc')) {
|
||||
tier = 'd'
|
||||
tier = 'c'
|
||||
} else if (seenPost.feedContext?.startsWith('t-')) {
|
||||
tier = 'e'
|
||||
tier = 'd'
|
||||
} else if (seenPost.feedContext === 'nettop') {
|
||||
tier = 'f'
|
||||
tier = 'e'
|
||||
} else {
|
||||
tier = 'g'
|
||||
tier = 'f'
|
||||
}
|
||||
let score = Math.round(
|
||||
Math.log(
|
||||
@@ -136,26 +131,16 @@ function useExperimentalSuggestedUsersQuery() {
|
||||
const {currentAccount} = useSession()
|
||||
const userActionSnapshot = userActionHistory.useActionHistorySnapshot()
|
||||
const dids = React.useMemo(() => {
|
||||
const {likes, follows, followSuggestions, seen} = userActionSnapshot
|
||||
const {likes, follows, seen} = userActionSnapshot
|
||||
const likeDids = likes
|
||||
.map(l => new AtUri(l))
|
||||
.map(uri => uri.host)
|
||||
.filter(did => !follows.includes(did))
|
||||
let suggestedDids: string[] = []
|
||||
if (followSuggestions.length > 0) {
|
||||
suggestedDids = [
|
||||
// It's ok if these will pick the same item (weighed by its frequency)
|
||||
followSuggestions[Math.floor(Math.random() * followSuggestions.length)],
|
||||
followSuggestions[Math.floor(Math.random() * followSuggestions.length)],
|
||||
followSuggestions[Math.floor(Math.random() * followSuggestions.length)],
|
||||
followSuggestions[Math.floor(Math.random() * followSuggestions.length)],
|
||||
]
|
||||
}
|
||||
const seenDids = seen
|
||||
.sort(sortSeenPosts)
|
||||
.map(l => new AtUri(l.uri))
|
||||
.map(uri => uri.host)
|
||||
return [...new Set([...suggestedDids, ...likeDids, ...seenDids])].filter(
|
||||
return [...new Set([...likeDids, ...seenDids])].filter(
|
||||
did => did !== currentAccount?.did,
|
||||
)
|
||||
}, [userActionSnapshot, currentAccount])
|
||||
@@ -176,63 +161,14 @@ function useExperimentalSuggestedUsersQuery() {
|
||||
}
|
||||
}
|
||||
|
||||
export function SuggestedFollows({feed}: {feed: FeedDescriptor}) {
|
||||
const gate = useGate()
|
||||
const [feedType, feedUri] = feed.split('|')
|
||||
if (feedType === 'author') {
|
||||
if (gate('show_follow_suggestions_in_profile')) {
|
||||
return <SuggestedFollowsProfile did={feedUri} />
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
} else {
|
||||
return <SuggestedFollowsHome />
|
||||
}
|
||||
}
|
||||
|
||||
export function SuggestedFollowsProfile({did}: {did: string}) {
|
||||
const {
|
||||
isLoading: isSuggestionsLoading,
|
||||
data,
|
||||
error,
|
||||
} = useSuggestedFollowsByActorQuery({
|
||||
did,
|
||||
})
|
||||
return (
|
||||
<ProfileGrid
|
||||
isSuggestionsLoading={isSuggestionsLoading}
|
||||
profiles={data?.suggestions ?? []}
|
||||
error={error}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function SuggestedFollowsHome() {
|
||||
export function SuggestedFollows() {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {
|
||||
isLoading: isSuggestionsLoading,
|
||||
profiles,
|
||||
error,
|
||||
} = useExperimentalSuggestedUsersQuery()
|
||||
return (
|
||||
<ProfileGrid
|
||||
isSuggestionsLoading={isSuggestionsLoading}
|
||||
profiles={profiles}
|
||||
error={error}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function ProfileGrid({
|
||||
isSuggestionsLoading,
|
||||
error,
|
||||
profiles,
|
||||
}: {
|
||||
isSuggestionsLoading: boolean
|
||||
profiles: AppBskyActorDefs.ProfileViewDetailed[]
|
||||
error: Error | null
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const moderationOpts = useModerationOpts()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
@@ -254,7 +190,7 @@ export function ProfileGrid({
|
||||
{profiles.slice(0, maxLength).map(profile => (
|
||||
<ProfileCard.Link
|
||||
key={profile.did}
|
||||
profile={profile}
|
||||
did={profile.handle}
|
||||
onPress={() => {
|
||||
logEvent('feed:interstitial:profileCard:press', {})
|
||||
}}
|
||||
@@ -330,10 +266,7 @@ export function ProfileGrid({
|
||||
a.pt_xs,
|
||||
a.gap_md,
|
||||
]}>
|
||||
<InlineLinkText
|
||||
label={_(msg`Browse more suggestions`)}
|
||||
to="/search"
|
||||
style={[t.atoms.text_contrast_medium]}>
|
||||
<InlineLinkText to="/search" style={[t.atoms.text_contrast_medium]}>
|
||||
<Trans>Browse more suggestions</Trans>
|
||||
</InlineLinkText>
|
||||
<Arrow size="sm" fill={t.atoms.text_contrast_medium.color} />
|
||||
@@ -463,10 +396,7 @@ export function SuggestedFeeds() {
|
||||
a.pt_xs,
|
||||
a.gap_md,
|
||||
]}>
|
||||
<InlineLinkText
|
||||
label={_(msg`Browse more suggestions`)}
|
||||
to="/search"
|
||||
style={[t.atoms.text_contrast_medium]}>
|
||||
<InlineLinkText to="/search" style={[t.atoms.text_contrast_medium]}>
|
||||
<Trans>Browse more suggestions</Trans>
|
||||
</InlineLinkText>
|
||||
<Arrow size="sm" fill={t.atoms.text_contrast_medium.color} />
|
||||
|
||||
@@ -40,6 +40,11 @@ type BaseLinkProps = Pick<
|
||||
> & {
|
||||
testID?: string
|
||||
|
||||
/**
|
||||
* Label for a11y. Defaults to the href.
|
||||
*/
|
||||
label?: string
|
||||
|
||||
/**
|
||||
* The React Navigation `StackAction` to perform when the link is pressed.
|
||||
*/
|
||||
@@ -192,7 +197,7 @@ export function useLink({
|
||||
}
|
||||
|
||||
export type LinkProps = Omit<BaseLinkProps, 'disableMismatchWarning'> &
|
||||
Omit<ButtonProps, 'onPress' | 'disabled'>
|
||||
Omit<ButtonProps, 'onPress' | 'disabled' | 'label'>
|
||||
|
||||
/**
|
||||
* A interactive element that renders as a `<a>` tag on the web. On mobile it
|
||||
@@ -219,6 +224,7 @@ export function Link({
|
||||
|
||||
return (
|
||||
<Button
|
||||
label={href}
|
||||
{...rest}
|
||||
style={[a.justify_start, flatten(rest.style)]}
|
||||
role="link"
|
||||
@@ -243,8 +249,7 @@ export function Link({
|
||||
|
||||
export type InlineLinkProps = React.PropsWithChildren<
|
||||
BaseLinkProps & TextStyleProp & Pick<TextProps, 'selectable'>
|
||||
> &
|
||||
Pick<ButtonProps, 'label'>
|
||||
>
|
||||
|
||||
export function InlineLinkText({
|
||||
children,
|
||||
@@ -286,7 +291,7 @@ export function InlineLinkText({
|
||||
<Text
|
||||
selectable={selectable}
|
||||
accessibilityHint=""
|
||||
accessibilityLabel={label}
|
||||
accessibilityLabel={label || href}
|
||||
{...rest}
|
||||
style={[
|
||||
{color: t.palette.primary_500},
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {
|
||||
AppBskyActorDefs,
|
||||
AppBskyGraphDefs,
|
||||
AtUri,
|
||||
moderateUserList,
|
||||
ModerationUI,
|
||||
} from '@atproto/api'
|
||||
import {AppBskyActorDefs, AppBskyGraphDefs, AtUri} from '@atproto/api'
|
||||
import {Trans} from '@lingui/macro'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {sanitizeHandle} from 'lib/strings/handles'
|
||||
import {useModerationOpts} from 'state/preferences/moderation-opts'
|
||||
import {precacheList} from 'state/queries/feed'
|
||||
import {useSession} from 'state/session'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {useTheme} from '#/alf'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {
|
||||
Avatar,
|
||||
Description,
|
||||
@@ -23,7 +16,6 @@ import {
|
||||
SaveButton,
|
||||
} from '#/components/FeedCard'
|
||||
import {Link as InternalLink, LinkProps} from '#/components/Link'
|
||||
import * as Hider from '#/components/moderation/Hider'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
/*
|
||||
@@ -51,13 +43,8 @@ type Props = {
|
||||
|
||||
export function Default(props: Props) {
|
||||
const {view, showPinButton} = props
|
||||
const moderationOpts = useModerationOpts()
|
||||
const moderation = moderationOpts
|
||||
? moderateUserList(view, moderationOpts)
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<Link {...props}>
|
||||
<Link label={view.name} {...props}>
|
||||
<Outer>
|
||||
<Header>
|
||||
<Avatar src={view.avatar} />
|
||||
@@ -65,7 +52,6 @@ export function Default(props: Props) {
|
||||
title={view.name}
|
||||
creator={view.creator}
|
||||
purpose={view.purpose}
|
||||
modUi={moderation?.ui('contentView')}
|
||||
/>
|
||||
{showPinButton && view.purpose === CURATELIST && (
|
||||
<SaveButton view={view} pin />
|
||||
@@ -81,7 +67,7 @@ export function Link({
|
||||
view,
|
||||
children,
|
||||
...props
|
||||
}: Props & Omit<LinkProps, 'to' | 'label'>) {
|
||||
}: Props & Omit<LinkProps, 'to'>) {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const href = React.useMemo(() => {
|
||||
@@ -93,7 +79,7 @@ export function Link({
|
||||
}, [view, queryClient])
|
||||
|
||||
return (
|
||||
<InternalLink label={view.name} to={href} {...props}>
|
||||
<InternalLink to={href} {...props}>
|
||||
{children}
|
||||
</InternalLink>
|
||||
)
|
||||
@@ -103,40 +89,18 @@ export function TitleAndByline({
|
||||
title,
|
||||
creator,
|
||||
purpose = CURATELIST,
|
||||
modUi,
|
||||
}: {
|
||||
title: string
|
||||
creator?: AppBskyActorDefs.ProfileViewBasic
|
||||
purpose?: AppBskyGraphDefs.ListView['purpose']
|
||||
modUi?: ModerationUI
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {currentAccount} = useSession()
|
||||
|
||||
return (
|
||||
<View style={[a.flex_1]}>
|
||||
<Hider.Outer
|
||||
modui={modUi}
|
||||
isContentVisibleInitialState={
|
||||
creator && currentAccount?.did === creator.did
|
||||
}
|
||||
allowOverride={creator && currentAccount?.did === creator.did}>
|
||||
<Hider.Mask>
|
||||
<Text
|
||||
style={[a.text_md, a.font_bold, a.leading_snug, a.italic]}
|
||||
numberOfLines={1}>
|
||||
<Trans>Hidden list</Trans>
|
||||
</Text>
|
||||
</Hider.Mask>
|
||||
<Hider.Content>
|
||||
<Text
|
||||
style={[a.text_md, a.font_bold, a.leading_snug]}
|
||||
numberOfLines={1}>
|
||||
{title}
|
||||
</Text>
|
||||
</Hider.Content>
|
||||
</Hider.Outer>
|
||||
|
||||
<Text style={[a.text_md, a.font_bold, a.leading_snug]} numberOfLines={1}>
|
||||
{title}
|
||||
</Text>
|
||||
{creator && (
|
||||
<Text
|
||||
style={[a.leading_snug, t.atoms.text_contrast_medium]}
|
||||
|
||||
@@ -122,16 +122,8 @@ export function ListHeaderDesktop({
|
||||
if (!gtTablet) return null
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.py_sm,
|
||||
a.px_xl,
|
||||
a.gap_xs,
|
||||
a.justify_center,
|
||||
{minHeight: 50},
|
||||
]}>
|
||||
<Text style={[a.text_2xl, a.font_bold]}>{title}</Text>
|
||||
<View style={[a.w_full, a.py_lg, a.px_xl, a.gap_xs]}>
|
||||
<Text style={[a.text_3xl, a.font_bold]}>{title}</Text>
|
||||
{subtitle ? (
|
||||
<Text style={[a.text_md, t.atoms.text_contrast_medium]}>
|
||||
{subtitle}
|
||||
@@ -178,7 +170,7 @@ let ListMaybePlaceholder = ({
|
||||
return (
|
||||
<CenteredView
|
||||
style={[
|
||||
a.h_full_vh,
|
||||
a.flex_1,
|
||||
a.align_center,
|
||||
!gtMobile ? a.justify_between : a.gap_5xl,
|
||||
t.atoms.border_contrast_low,
|
||||
@@ -197,7 +189,7 @@ let ListMaybePlaceholder = ({
|
||||
return (
|
||||
<Error
|
||||
title={errorTitle ?? _(msg`Oops!`)}
|
||||
message={errorMessage ?? _(msg`Something went wrong!`)}
|
||||
message={errorMessage ?? _(`Something went wrong!`)}
|
||||
onRetry={onRetry}
|
||||
onGoBack={onGoBack}
|
||||
sideBorders={sideBorders}
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
import React from 'react'
|
||||
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
|
||||
import {Image} from 'expo-image'
|
||||
import {
|
||||
AppBskyEmbedExternal,
|
||||
AppBskyEmbedImages,
|
||||
AppBskyEmbedRecordWithMedia,
|
||||
AppBskyEmbedVideo,
|
||||
} from '@atproto/api'
|
||||
import {Trans} from '@lingui/macro'
|
||||
|
||||
import {parseTenorGif} from '#/lib/strings/embed-player'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
|
||||
|
||||
/**
|
||||
* Streamlined MediaPreview component which just handles images, gifs, and videos
|
||||
*/
|
||||
export function Embed({
|
||||
embed,
|
||||
style,
|
||||
}: {
|
||||
embed?:
|
||||
| AppBskyEmbedImages.View
|
||||
| AppBskyEmbedRecordWithMedia.View
|
||||
| AppBskyEmbedExternal.View
|
||||
| AppBskyEmbedVideo.View
|
||||
| {[k: string]: unknown}
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
let media = AppBskyEmbedRecordWithMedia.isView(embed) ? embed.media : embed
|
||||
|
||||
if (AppBskyEmbedImages.isView(media)) {
|
||||
return (
|
||||
<Outer style={style}>
|
||||
{media.images.map(image => (
|
||||
<ImageItem
|
||||
key={image.thumb}
|
||||
thumbnail={image.thumb}
|
||||
alt={image.alt}
|
||||
/>
|
||||
))}
|
||||
</Outer>
|
||||
)
|
||||
} else if (AppBskyEmbedExternal.isView(embed) && embed.external.thumb) {
|
||||
let url: URL | undefined
|
||||
try {
|
||||
url = new URL(embed.external.uri)
|
||||
} catch {}
|
||||
if (url) {
|
||||
const {success} = parseTenorGif(url)
|
||||
if (success) {
|
||||
return (
|
||||
<Outer style={style}>
|
||||
<GifItem
|
||||
thumbnail={embed.external.thumb}
|
||||
alt={embed.external.title}
|
||||
/>
|
||||
</Outer>
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (AppBskyEmbedVideo.isView(embed)) {
|
||||
return (
|
||||
<Outer style={style}>
|
||||
<VideoItem thumbnail={embed.thumbnail} alt={embed.alt} />
|
||||
</Outer>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function Outer({
|
||||
children,
|
||||
style,
|
||||
}: {
|
||||
children?: React.ReactNode
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
return <View style={[a.flex_row, a.gap_xs, style]}>{children}</View>
|
||||
}
|
||||
|
||||
export function ImageItem({
|
||||
thumbnail,
|
||||
alt,
|
||||
children,
|
||||
}: {
|
||||
thumbnail: string
|
||||
alt?: string
|
||||
children?: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<View style={[a.relative, a.flex_1, {aspectRatio: 1, maxWidth: 100}]}>
|
||||
<Image
|
||||
key={thumbnail}
|
||||
source={{uri: thumbnail}}
|
||||
style={[a.flex_1, a.rounded_xs]}
|
||||
contentFit="cover"
|
||||
accessible={true}
|
||||
accessibilityIgnoresInvertColors
|
||||
accessibilityHint={alt}
|
||||
accessibilityLabel=""
|
||||
/>
|
||||
{children}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function GifItem({thumbnail, alt}: {thumbnail: string; alt?: string}) {
|
||||
return (
|
||||
<ImageItem thumbnail={thumbnail} alt={alt}>
|
||||
<View style={[a.absolute, a.inset_0, a.justify_center, a.align_center]}>
|
||||
<PlayButtonIcon size={24} />
|
||||
</View>
|
||||
<View style={styles.altContainer}>
|
||||
<Text style={styles.alt}>
|
||||
<Trans>GIF</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</ImageItem>
|
||||
)
|
||||
}
|
||||
|
||||
export function VideoItem({
|
||||
thumbnail,
|
||||
alt,
|
||||
}: {
|
||||
thumbnail?: string
|
||||
alt?: string
|
||||
}) {
|
||||
if (!thumbnail) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
{backgroundColor: 'black'},
|
||||
a.flex_1,
|
||||
{aspectRatio: 1, maxWidth: 100},
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
]}>
|
||||
<PlayButtonIcon size={24} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<ImageItem thumbnail={thumbnail} alt={alt}>
|
||||
<View style={[a.absolute, a.inset_0, a.justify_center, a.align_center]}>
|
||||
<PlayButtonIcon size={24} />
|
||||
</View>
|
||||
</ImageItem>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
altContainer: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.75)',
|
||||
borderRadius: 6,
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 3,
|
||||
position: 'absolute',
|
||||
right: 5,
|
||||
bottom: 5,
|
||||
zIndex: 2,
|
||||
},
|
||||
alt: {
|
||||
color: 'white',
|
||||
fontSize: 7,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
})
|
||||
@@ -1,12 +1,8 @@
|
||||
import React from 'react'
|
||||
|
||||
import type {ContextType, ItemContextType} from '#/components/Menu/types'
|
||||
import type {ContextType} from '#/components/Menu/types'
|
||||
|
||||
export const Context = React.createContext<ContextType>({
|
||||
// @ts-ignore
|
||||
control: null,
|
||||
})
|
||||
|
||||
export const ItemContext = React.createContext<ItemContextType>({
|
||||
disabled: false,
|
||||
})
|
||||
|
||||
@@ -9,7 +9,7 @@ import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {Context, ItemContext} from '#/components/Menu/context'
|
||||
import {Context} from '#/components/Menu/context'
|
||||
import {
|
||||
ContextType,
|
||||
GroupProps,
|
||||
@@ -125,14 +125,8 @@ export function Item({children, label, style, onPress, ...rest}: ItemProps) {
|
||||
}}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
onPressIn={e => {
|
||||
onPressIn()
|
||||
rest.onPressIn?.(e)
|
||||
}}
|
||||
onPressOut={e => {
|
||||
onPressOut()
|
||||
rest.onPressOut?.(e)
|
||||
}}
|
||||
onPressIn={onPressIn}
|
||||
onPressOut={onPressOut}
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
@@ -144,18 +138,15 @@ export function Item({children, label, style, onPress, ...rest}: ItemProps) {
|
||||
t.atoms.border_contrast_low,
|
||||
{minHeight: 44, paddingVertical: 10},
|
||||
style,
|
||||
(focused || pressed) && !rest.disabled && [t.atoms.bg_contrast_50],
|
||||
(focused || pressed) && [t.atoms.bg_contrast_50],
|
||||
]}>
|
||||
<ItemContext.Provider value={{disabled: Boolean(rest.disabled)}}>
|
||||
{children}
|
||||
</ItemContext.Provider>
|
||||
{children}
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
export function ItemText({children, style}: ItemTextProps) {
|
||||
const t = useTheme()
|
||||
const {disabled} = React.useContext(ItemContext)
|
||||
return (
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
@@ -164,10 +155,9 @@ export function ItemText({children, style}: ItemTextProps) {
|
||||
a.flex_1,
|
||||
a.text_md,
|
||||
a.font_bold,
|
||||
t.atoms.text_contrast_high,
|
||||
t.atoms.text_contrast_medium,
|
||||
{paddingTop: 3},
|
||||
style,
|
||||
disabled && t.atoms.text_contrast_low,
|
||||
]}>
|
||||
{children}
|
||||
</Text>
|
||||
@@ -176,17 +166,7 @@ export function ItemText({children, style}: ItemTextProps) {
|
||||
|
||||
export function ItemIcon({icon: Comp}: ItemIconProps) {
|
||||
const t = useTheme()
|
||||
const {disabled} = React.useContext(ItemContext)
|
||||
return (
|
||||
<Comp
|
||||
size="lg"
|
||||
fill={
|
||||
disabled
|
||||
? t.atoms.text_contrast_low.color
|
||||
: t.atoms.text_contrast_medium.color
|
||||
}
|
||||
/>
|
||||
)
|
||||
return <Comp size="lg" fill={t.atoms.text_contrast_medium.color} />
|
||||
}
|
||||
|
||||
export function Group({children, style}: GroupProps) {
|
||||
|
||||
@@ -9,7 +9,7 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import {atoms as a, flatten, useTheme, web} from '#/alf'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {Context, ItemContext} from '#/components/Menu/context'
|
||||
import {Context} from '#/components/Menu/context'
|
||||
import {
|
||||
ContextType,
|
||||
GroupProps,
|
||||
@@ -239,21 +239,18 @@ export function Item({children, label, onPress, ...rest}: ItemProps) {
|
||||
a.rounded_xs,
|
||||
{minHeight: 32, paddingHorizontal: 10},
|
||||
web({outline: 0}),
|
||||
(hovered || focused) &&
|
||||
!rest.disabled && [
|
||||
web({outline: '0 !important'}),
|
||||
t.name === 'light'
|
||||
? t.atoms.bg_contrast_25
|
||||
: t.atoms.bg_contrast_50,
|
||||
],
|
||||
(hovered || focused) && [
|
||||
web({outline: '0 !important'}),
|
||||
t.name === 'light'
|
||||
? t.atoms.bg_contrast_25
|
||||
: t.atoms.bg_contrast_50,
|
||||
],
|
||||
])}
|
||||
{...web({
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
})}>
|
||||
<ItemContext.Provider value={{disabled: Boolean(rest.disabled)}}>
|
||||
{children}
|
||||
</ItemContext.Provider>
|
||||
{children}
|
||||
</Pressable>
|
||||
</DropdownMenu.Item>
|
||||
)
|
||||
@@ -261,16 +258,8 @@ export function Item({children, label, onPress, ...rest}: ItemProps) {
|
||||
|
||||
export function ItemText({children, style}: ItemTextProps) {
|
||||
const t = useTheme()
|
||||
const {disabled} = React.useContext(ItemContext)
|
||||
return (
|
||||
<Text
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.font_bold,
|
||||
t.atoms.text_contrast_high,
|
||||
style,
|
||||
disabled && t.atoms.text_contrast_low,
|
||||
]}>
|
||||
<Text style={[a.flex_1, a.font_bold, t.atoms.text_contrast_high, style]}>
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
@@ -278,9 +267,10 @@ export function ItemText({children, style}: ItemTextProps) {
|
||||
|
||||
export function ItemIcon({icon: Comp, position = 'left'}: ItemIconProps) {
|
||||
const t = useTheme()
|
||||
const {disabled} = React.useContext(ItemContext)
|
||||
return (
|
||||
<View
|
||||
<Comp
|
||||
size="md"
|
||||
fill={t.atoms.text_contrast_medium.color}
|
||||
style={[
|
||||
position === 'left' && {
|
||||
marginLeft: -2,
|
||||
@@ -289,16 +279,8 @@ export function ItemIcon({icon: Comp, position = 'left'}: ItemIconProps) {
|
||||
marginRight: -2,
|
||||
marginLeft: 12,
|
||||
},
|
||||
]}>
|
||||
<Comp
|
||||
size="md"
|
||||
fill={
|
||||
disabled
|
||||
? t.atoms.text_contrast_low.color
|
||||
: t.atoms.text_contrast_medium.color
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
AccessibilityProps,
|
||||
GestureResponderEvent,
|
||||
PressableProps,
|
||||
AccessibilityProps,
|
||||
} from 'react-native'
|
||||
|
||||
import {TextStyleProp, ViewStyleProp} from '#/alf'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {Props as SVGIconProps} from '#/components/icons/common'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {TextStyleProp, ViewStyleProp} from '#/alf'
|
||||
|
||||
export type ContextType = {
|
||||
control: Dialog.DialogOuterProps['control']
|
||||
}
|
||||
|
||||
export type ItemContextType = {
|
||||
disabled: boolean
|
||||
}
|
||||
|
||||
export type RadixPassThroughTriggerProps = {
|
||||
id: string
|
||||
type: 'button'
|
||||
|
||||
@@ -13,15 +13,6 @@ import {
|
||||
} from '#/components/moderation/ModerationDetailsDialog'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export type AppModerationCause =
|
||||
| ModerationCause
|
||||
| {
|
||||
type: 'reply-hidden'
|
||||
source: {type: 'user'; did: string}
|
||||
priority: 6
|
||||
downgraded?: boolean
|
||||
}
|
||||
|
||||
export type CommonProps = {
|
||||
size?: 'sm' | 'lg'
|
||||
}
|
||||
@@ -49,7 +40,7 @@ export function Row({
|
||||
}
|
||||
|
||||
export type LabelProps = {
|
||||
cause: AppModerationCause
|
||||
cause: ModerationCause
|
||||
disableDetailsDialog?: boolean
|
||||
noBg?: boolean
|
||||
} & CommonProps
|
||||
|
||||
@@ -36,7 +36,7 @@ export function Default({
|
||||
logContext?: 'ProfileCard' | 'StarterPackProfilesList'
|
||||
}) {
|
||||
return (
|
||||
<Link profile={profile}>
|
||||
<Link did={profile.did}>
|
||||
<Card
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
@@ -96,24 +96,16 @@ export function Header({
|
||||
}
|
||||
|
||||
export function Link({
|
||||
profile,
|
||||
did,
|
||||
children,
|
||||
style,
|
||||
...rest
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
} & Omit<LinkProps, 'to' | 'label'>) {
|
||||
const {_} = useLingui()
|
||||
}: {did: string} & Omit<LinkProps, 'to'>) {
|
||||
return (
|
||||
<InternalLink
|
||||
label={_(
|
||||
msg`View ${
|
||||
profile.displayName || sanitizeHandle(profile.handle)
|
||||
}'s profile`,
|
||||
)}
|
||||
to={{
|
||||
screen: 'Profile',
|
||||
params: {name: profile.did},
|
||||
params: {name: did},
|
||||
}}
|
||||
style={[a.flex_col, style]}
|
||||
{...rest}>
|
||||
@@ -314,9 +306,9 @@ export function FollowButtonInner({
|
||||
)}`,
|
||||
),
|
||||
)
|
||||
} catch (err: any) {
|
||||
if (err?.name !== 'AbortError') {
|
||||
Toast.show(_(msg`An issue occurred, please try again.`), 'xmark')
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') {
|
||||
Toast.show(_(msg`An issue occurred, please try again.`))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -334,9 +326,9 @@ export function FollowButtonInner({
|
||||
)}`,
|
||||
),
|
||||
)
|
||||
} catch (err: any) {
|
||||
if (err?.name !== 'AbortError') {
|
||||
Toast.show(_(msg`An issue occurred, please try again.`), 'xmark')
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') {
|
||||
Toast.show(_(msg`An issue occurred, please try again.`))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,7 +377,7 @@ function Inner({
|
||||
hide: () => void
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_, i18n} = useLingui()
|
||||
const {_} = useLingui()
|
||||
const {currentAccount} = useSession()
|
||||
const moderation = React.useMemo(
|
||||
() => moderateProfile(profile, moderationOpts),
|
||||
@@ -393,8 +393,8 @@ function Inner({
|
||||
profile.viewer?.blocking ||
|
||||
profile.viewer?.blockedBy ||
|
||||
profile.viewer?.blockingByList
|
||||
const following = formatCount(i18n, profile.followsCount || 0)
|
||||
const followers = formatCount(i18n, profile.followersCount || 0)
|
||||
const following = formatCount(profile.followsCount || 0)
|
||||
const followers = formatCount(profile.followersCount || 0)
|
||||
const pluralizedFollowers = plural(profile.followersCount || 0, {
|
||||
one: 'follower',
|
||||
other: 'followers',
|
||||
|
||||
@@ -8,10 +8,7 @@ import {Button, ButtonColor, ButtonProps, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export {
|
||||
type DialogControlProps as PromptControlProps,
|
||||
useDialogControl as usePromptControl,
|
||||
} from '#/components/Dialog'
|
||||
export {useDialogControl as usePromptControl} from '#/components/Dialog'
|
||||
|
||||
const Context = React.createContext<{
|
||||
titleId: string
|
||||
@@ -26,7 +23,7 @@ export function Outer({
|
||||
control,
|
||||
testID,
|
||||
}: React.PropsWithChildren<{
|
||||
control: Dialog.DialogControlProps
|
||||
control: Dialog.DialogOuterProps['control']
|
||||
testID?: string
|
||||
}>) {
|
||||
const {gtMobile} = useBreakpoints()
|
||||
|
||||
@@ -6,6 +6,7 @@ import {useLingui} from '@lingui/react'
|
||||
|
||||
import {getLabelingServiceTitle} from '#/lib/moderation'
|
||||
import {ReportOption} from '#/lib/moderation/useReportOptions'
|
||||
import {useGate} from '#/lib/statsig/statsig'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
@@ -36,6 +37,7 @@ export function SubmitView({
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const agent = useAgent()
|
||||
const gate = useGate()
|
||||
const [details, setDetails] = React.useState<string>('')
|
||||
const [submitting, setSubmitting] = React.useState<boolean>(false)
|
||||
const [selectedServices, setSelectedServices] = React.useState<string[]>([
|
||||
@@ -61,17 +63,27 @@ export function SubmitView({
|
||||
}
|
||||
const results = await Promise.all(
|
||||
selectedServices.map(did => {
|
||||
return agent
|
||||
.createModerationReport(report, {
|
||||
encoding: 'application/json',
|
||||
headers: {
|
||||
'atproto-proxy': `${did}#atproto_labeler`,
|
||||
},
|
||||
})
|
||||
.then(
|
||||
_ => true,
|
||||
_ => false,
|
||||
)
|
||||
if (gate('session_withproxy_fix')) {
|
||||
return agent
|
||||
.createModerationReport(report, {
|
||||
encoding: 'application/json',
|
||||
headers: {
|
||||
'atproto-proxy': `${did}#atproto_labeler`,
|
||||
},
|
||||
})
|
||||
.then(
|
||||
_ => true,
|
||||
_ => false,
|
||||
)
|
||||
} else {
|
||||
return agent
|
||||
.withProxy('atproto_labeler', did)
|
||||
.createModerationReport(report)
|
||||
.then(
|
||||
_ => true,
|
||||
_ => false,
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -96,6 +108,7 @@ export function SubmitView({
|
||||
onSubmitComplete,
|
||||
setError,
|
||||
agent,
|
||||
gate,
|
||||
])
|
||||
|
||||
return (
|
||||
|
||||
@@ -9,15 +9,14 @@ import {
|
||||
import {InfiniteData, UseInfiniteQueryResult} from '@tanstack/react-query'
|
||||
|
||||
import {useBottomBarOffset} from 'lib/hooks/useBottomBarOffset'
|
||||
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
|
||||
import {isBlockedOrBlocking} from 'lib/moderation/blocked-and-muted'
|
||||
import {isNative, isWeb} from 'platform/detection'
|
||||
import {useAllListMembersQuery} from 'state/queries/list-members'
|
||||
import {useListMembersQuery} from 'state/queries/list-members'
|
||||
import {useSession} from 'state/session'
|
||||
import {List, ListRef} from 'view/com/util/List'
|
||||
import {SectionRef} from '#/screens/Profile/Sections/types'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
|
||||
import {ListMaybePlaceholder} from '#/components/Lists'
|
||||
import {Default as ProfileCard} from '#/components/ProfileCard'
|
||||
|
||||
function keyExtractor(item: AppBskyActorDefs.ProfileViewBasic, index: number) {
|
||||
@@ -40,20 +39,17 @@ export const ProfilesList = React.forwardRef<SectionRef, ProfilesListProps>(
|
||||
ref,
|
||||
) {
|
||||
const t = useTheme()
|
||||
const bottomBarOffset = useBottomBarOffset(200)
|
||||
const initialNumToRender = useInitialNumToRender()
|
||||
const [initialHeaderHeight] = React.useState(headerHeight)
|
||||
const bottomBarOffset = useBottomBarOffset(20)
|
||||
const {currentAccount} = useSession()
|
||||
const {data, refetch, isError} = useAllListMembersQuery(listUri)
|
||||
const {data, refetch, isError} = useListMembersQuery(listUri, 50)
|
||||
|
||||
const [isPTRing, setIsPTRing] = React.useState(false)
|
||||
|
||||
// The server returns these sorted by descending creation date, so we want to invert
|
||||
|
||||
const profiles = data
|
||||
?.filter(
|
||||
p => !isBlockedOrBlocking(p.subject) && !p.subject.associated?.labeler,
|
||||
)
|
||||
.map(p => p.subject)
|
||||
const profiles = data?.pages
|
||||
.flatMap(p => p.items.map(i => i.subject))
|
||||
.filter(p => !isBlockedOrBlocking(p) && !p.associated?.labeler)
|
||||
.reverse()
|
||||
const isOwn = new AtUri(listUri).host === currentAccount?.did
|
||||
|
||||
@@ -103,11 +99,7 @@ export const ProfilesList = React.forwardRef<SectionRef, ProfilesListProps>(
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.h_full_vh,
|
||||
{marginTop: headerHeight, marginBottom: bottomBarOffset},
|
||||
]}>
|
||||
<View style={{marginTop: headerHeight, marginBottom: bottomBarOffset}}>
|
||||
<ListMaybePlaceholder
|
||||
isLoading={true}
|
||||
isError={isError}
|
||||
@@ -126,13 +118,10 @@ export const ProfilesList = React.forwardRef<SectionRef, ProfilesListProps>(
|
||||
ref={scrollElRef}
|
||||
headerOffset={headerHeight}
|
||||
ListFooterComponent={
|
||||
<ListFooter
|
||||
style={{paddingBottom: bottomBarOffset, borderTopWidth: 0}}
|
||||
/>
|
||||
<View style={[{height: initialHeaderHeight + bottomBarOffset}]} />
|
||||
}
|
||||
showsVerticalScrollIndicator={false}
|
||||
desktopFixedHeight
|
||||
initialNumToRender={initialNumToRender}
|
||||
refreshing={isPTRing}
|
||||
onRefresh={async () => {
|
||||
setIsPTRing(true)
|
||||
|
||||
@@ -59,24 +59,20 @@ export const QrCode = React.forwardRef<ViewShot, Props>(function QrCode(
|
||||
<QrCodeInner link={link} />
|
||||
</View>
|
||||
|
||||
<Text
|
||||
style={[
|
||||
a.flex,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.font_bold,
|
||||
{color: 'white', fontSize: 18, gap: 6},
|
||||
]}>
|
||||
<Trans>
|
||||
on
|
||||
<View style={[a.flex_row, a.align_center, {gap: 6}]}>
|
||||
<Logo width={25} fill="white" />
|
||||
<View style={[{marginTop: 3.5}]}>
|
||||
<Logotype width={72} fill="white" />
|
||||
</View>
|
||||
</View>
|
||||
</Trans>
|
||||
</Text>
|
||||
<View style={[a.flex_row, a.align_center, {gap: 5}]}>
|
||||
<Text
|
||||
style={[
|
||||
a.font_bold,
|
||||
a.text_center,
|
||||
{color: 'white', fontSize: 18},
|
||||
]}>
|
||||
<Trans>on</Trans>
|
||||
</Text>
|
||||
<Logo width={26} fill="white" />
|
||||
<View style={[{marginTop: 5, marginLeft: 2.5}]}>
|
||||
<Logotype width={68} fill="white" />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</LinearGradientBackground>
|
||||
</ViewShot>
|
||||
|
||||
@@ -67,10 +67,7 @@ export function QrCodeDialog({
|
||||
try {
|
||||
await createAssetAsync(`file://${uri}`)
|
||||
} catch (e: unknown) {
|
||||
Toast.show(
|
||||
_(msg`An error occurred while saving the QR code!`),
|
||||
'xmark',
|
||||
)
|
||||
Toast.show(_(msg`An error occurred while saving the QR code!`))
|
||||
logger.error('Failed to save QR code', {error: e})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -66,7 +66,6 @@ function ShareDialogInner({
|
||||
if (!res) {
|
||||
Toast.show(
|
||||
_(msg`You must grant access to your photo library to save the image.`),
|
||||
'xmark',
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -76,7 +75,7 @@ function ShareDialogInner({
|
||||
Toast.show(_(msg`Image saved to your camera roll!`))
|
||||
control.close()
|
||||
} catch (e: unknown) {
|
||||
Toast.show(_(msg`An error occurred while saving the QR code!`), 'xmark')
|
||||
Toast.show(_(msg`An error occurred while saving the QR code!`))
|
||||
logger.error('Failed to save QR code', {error: e})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {Image} from 'expo-image'
|
||||
import {AppBskyGraphDefs, AppBskyGraphStarterpack, AtUri} from '@atproto/api'
|
||||
import {AppBskyGraphStarterpack, AtUri} from '@atproto/api'
|
||||
import {StarterPackViewBasic} from '@atproto/api/dist/client/types/app/bsky/graph/defs'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
@@ -16,11 +17,7 @@ import {StarterPack} from '#/components/icons/StarterPack'
|
||||
import {BaseLink} from '#/components/Link'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function Default({
|
||||
starterPack,
|
||||
}: {
|
||||
starterPack?: AppBskyGraphDefs.StarterPackViewBasic
|
||||
}) {
|
||||
export function Default({starterPack}: {starterPack?: StarterPackViewBasic}) {
|
||||
if (!starterPack) return null
|
||||
return (
|
||||
<Link starterPack={starterPack}>
|
||||
@@ -32,7 +29,7 @@ export function Default({
|
||||
export function Notification({
|
||||
starterPack,
|
||||
}: {
|
||||
starterPack?: AppBskyGraphDefs.StarterPackViewBasic
|
||||
starterPack?: StarterPackViewBasic
|
||||
}) {
|
||||
if (!starterPack) return null
|
||||
return (
|
||||
@@ -47,7 +44,7 @@ export function Card({
|
||||
noIcon,
|
||||
noDescription,
|
||||
}: {
|
||||
starterPack: AppBskyGraphDefs.StarterPackViewBasic
|
||||
starterPack: StarterPackViewBasic
|
||||
noIcon?: boolean
|
||||
noDescription?: boolean
|
||||
}) {
|
||||
@@ -97,7 +94,7 @@ export function Link({
|
||||
starterPack,
|
||||
children,
|
||||
}: {
|
||||
starterPack: AppBskyGraphDefs.StarterPackViewBasic
|
||||
starterPack: StarterPackViewBasic
|
||||
onPress?: () => void
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
@@ -132,11 +129,7 @@ export function Link({
|
||||
)
|
||||
}
|
||||
|
||||
export function Embed({
|
||||
starterPack,
|
||||
}: {
|
||||
starterPack: AppBskyGraphDefs.StarterPackViewBasic
|
||||
}) {
|
||||
export function Embed({starterPack}: {starterPack: StarterPackViewBasic}) {
|
||||
const t = useTheme()
|
||||
const imageUri = getStarterPackOgCard(starterPack)
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import {BottomSheetFlatListMethods} from '@discord/bottom-sheet'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {useSession} from 'state/session'
|
||||
import {WizardAction, WizardState} from '#/screens/StarterPack/Wizard/State'
|
||||
@@ -43,7 +42,6 @@ export function WizardEditListDialog({
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
const {currentAccount} = useSession()
|
||||
const initialNumToRender = useInitialNumToRender()
|
||||
|
||||
const listRef = useRef<BottomSheetFlatListMethods>(null)
|
||||
|
||||
@@ -150,7 +148,6 @@ export function WizardEditListDialog({
|
||||
webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]}
|
||||
keyboardDismissMode="on-drag"
|
||||
removeClippedSubviews={true}
|
||||
initialNumToRender={initialNumToRender}
|
||||
/>
|
||||
</Dialog.Outer>
|
||||
)
|
||||
|
||||
@@ -12,7 +12,7 @@ import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {DISCOVER_FEED_URI, STARTER_PACK_MAX_SIZE} from 'lib/constants'
|
||||
import {DISCOVER_FEED_URI} from 'lib/constants'
|
||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||
import {sanitizeHandle} from 'lib/strings/handles'
|
||||
import {useSession} from 'state/session'
|
||||
@@ -130,8 +130,7 @@ export function WizardProfileCard({
|
||||
|
||||
const isMe = profile.did === currentAccount?.did
|
||||
const included = isMe || state.profiles.some(p => p.did === profile.did)
|
||||
const disabled =
|
||||
isMe || (!included && state.profiles.length >= STARTER_PACK_MAX_SIZE - 1)
|
||||
const disabled = isMe || (!included && state.profiles.length >= 49)
|
||||
const moderationUi = moderateProfile(profile, moderationOpts).ui('avatar')
|
||||
const displayName = profile.displayName
|
||||
? sanitizeDisplayName(profile.displayName)
|
||||
|
||||