Compare commits
25 Commits
d157199dd1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
7518cd7d06
|
|||
|
f130c8d731
|
|||
|
511527abfd
|
|||
|
31ee74bb66
|
|||
|
acd41f8ece
|
|||
|
34a51a2198
|
|||
|
966bfc7c2f
|
|||
|
25e7927c70
|
|||
|
6d89ea4481
|
|||
|
1690aa4c12
|
|||
|
c9dd10d6f3
|
|||
|
b6f7d8bed1
|
|||
|
f4180e0c54
|
|||
|
9157948d93
|
|||
|
8db064d8c0
|
|||
|
c09738f342
|
|||
|
0ef8f22dcb
|
|||
|
0506a32fd8
|
|||
|
a49a2ff61f
|
|||
|
9644e9151b
|
|||
|
8b26cf9452
|
|||
|
718820daec
|
|||
|
88cb429951
|
|||
|
f27cae0669
|
|||
|
bfa82b1880
|
7
.github/workflows/cf-pages.yml
vendored
@@ -33,11 +33,8 @@ jobs:
|
||||
working-directory: web
|
||||
|
||||
- name: Deploy to Cloudflare Pages
|
||||
uses: cloudflare/pages-action@v1
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
projectName: ${{ secrets.CLOUDFLARE_PROJECT_NAME }}
|
||||
directory: web/dist/aiat
|
||||
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
wranglerVersion: '3'
|
||||
command: pages deploy web/dist/aiat --project-name=${{ secrets.CLOUDFLARE_PROJECT_NAME }}
|
||||
|
||||
3
.gitignore
vendored
@@ -4,6 +4,9 @@ deploy.yml
|
||||
claude.md
|
||||
embedded.mobileprovision
|
||||
.env
|
||||
k8s/secrets.env
|
||||
k8s/deploy.yml
|
||||
web/dist
|
||||
node_modules
|
||||
package-lock.json
|
||||
/tmp
|
||||
|
||||
21
README.md
@@ -1,18 +1,15 @@
|
||||
# at
|
||||
|
||||
- https://github.com/bluesky-social/atproto
|
||||
- https://github.com/bluesky-social/atproto/discussions/2026
|
||||
- https://github.com/bluesky-social/social-app
|
||||
|
||||
|word|name|example|
|
||||
|name|type|example|
|
||||
|---|---|---|
|
||||
|at|uri|at://example.com|
|
||||
|@|user|@example.com|
|
||||
|[at]proto|repo|`git@github.com:bluesky-social/atproto`|
|
||||
|[at]mosphere|system|pds, bsky(appview), ozone, bgs, plc|
|
||||
|[a]uthenticated [t]ransfer|protocol|[did](https://www.w3.org/TR/did-core/)|
|
||||
|
||||
- https://atproto.com/guides/glossary
|
||||
|
||||
## account
|
||||
|
||||
- [ai@syu.is](https://syu.is/profile/did:plc:6qyecktefllvenje24fcxnie)
|
||||
@@ -22,19 +19,6 @@
|
||||
|
||||
```sh
|
||||
$ curl -sL syu.is/xrpc/_health
|
||||
|
||||
# latest
|
||||
# https://github.com/bluesky-social/atproto/blob/main/packages/pds/package.json
|
||||
$ curl -sL https://raw.githubusercontent.com/bluesky-social/atproto/refs/heads/main/packages/pds/package.json |jq -r .version
|
||||
```
|
||||
|
||||
```sh
|
||||
$ handle=ai.syui.ai
|
||||
$ curl -sL "syu.is/xrpc/com.atproto.repo.describeRepo?repo=${handle}" |jq -r .did
|
||||
did:plc:6qyecktefllvenje24fcxnie
|
||||
|
||||
$ curl -sL "syu.is/xrpc/com.atproto.repo.listRecords?repo=${handle}&collection=app.bsky.feed.post&reverse=true&limit=1"
|
||||
{"records":[{"uri":"at://did:plc:6qyecktefllvenje24fcxnie/app.bsky.feed.post/3l6s2riuouk2j","cid":"bafyreibjohl7va4upkibw5twaxdd4jg3l6rmfatu4dpjjfd5xkb2ijtlx4","value":{"text":"hello","$type":"app.bsky.feed.post","langs":["ja"],"createdAt":"2024-10-18T13:21:39.809Z"}}],"cursor":"3l6s2riuouk2j"}
|
||||
```
|
||||
|
||||
## feed
|
||||
@@ -85,4 +69,3 @@ $ curl -sL "syu.is/xrpc/com.atproto.repo.listRecords?repo=${handle}&collection=a
|
||||
./ios/build.zsh
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -155,3 +155,5 @@ services:
|
||||
- ./envs/feed
|
||||
volumes:
|
||||
- ./data/feed:/data/
|
||||
depends_on:
|
||||
- jetstream
|
||||
|
||||
20
docker/feed/Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies for better-sqlite3
|
||||
RUN apk add --no-cache python3 make g++
|
||||
|
||||
# Copy package files and install
|
||||
COPY package.json yarn.lock ./
|
||||
RUN yarn install
|
||||
|
||||
# Copy source
|
||||
COPY . .
|
||||
|
||||
# Build TypeScript
|
||||
RUN yarn build
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "dist/index.js"]
|
||||
32
icon.svg
@@ -1,32 +0,0 @@
|
||||
<svg
|
||||
viewBox="0 0 2821.6379 794.29016"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<g
|
||||
transform="matrix(0.1,0,0,-0.1,-282.80153,1445)"
|
||||
fill="#000000"
|
||||
stroke="none"
|
||||
>
|
||||
<path
|
||||
d="m 24787,14443 c -4,-3 -7,-224 -7,-490 v -483 h 545 545 v 490 490 h -538 c -296,0 -542,-3 -545,-7 z"
|
||||
/>
|
||||
<path
|
||||
d="m 5190,13285 c -8,-3 -96,-12 -195,-20 -199,-16 -296,-32 -430,-70 -49,-14 -115,-32 -145,-40 -153,-39 -504,-198 -662,-301 -21,-13 -57,-36 -80,-51 -24,-16 -72,-50 -109,-78 -241,-182 -377,-315 -528,-517 -119,-158 -120,-160 -106,-188 23,-45 140,-140 560,-457 110,-84 319,-242 465,-353 146,-110 369,-279 495,-375 791,-600 723,-549 1049,-799 269,-207 398,-307 524,-403 l 114,-86 -49,-49 c -128,-131 -378,-258 -588,-299 -66,-13 -357,-13 -420,0 -115,23 -172,39 -202,54 -18,10 -37,17 -43,17 -24,0 -171,81 -255,141 -50,35 -146,121 -215,192 -69,70 -133,127 -142,127 -19,0 -153,-63 -177,-83 -9,-7 -54,-35 -101,-62 -47,-26 -110,-64 -140,-85 -30,-20 -97,-61 -148,-91 -51,-30 -107,-62 -124,-72 -18,-10 -57,-38 -87,-61 -70,-53 -252,-168 -446,-281 -82,-49 -158,-95 -168,-104 -16,-16 -14,-21 34,-106 165,-289 544,-666 867,-860 9,-6 35,-22 58,-37 36,-23 267,-138 349,-173 108,-47 160,-67 240,-93 150,-48 201,-62 228,-62 14,0 64,-9 109,-19 197,-46 302,-56 573,-56 197,1 281,5 345,17 47,9 110,19 141,22 31,3 75,13 99,21 23,8 56,15 72,15 17,0 51,7 77,15 25,8 80,24 121,36 41,12 125,41 185,66 61,25 124,50 140,56 17,7 89,42 160,78 113,58 177,98 395,246 82,56 273,232 403,371 127,136 237,271 237,291 0,12 -208,179 -425,342 -186,140 -1121,843 -1720,1294 -264,199 -589,444 -723,546 -134,101 -274,208 -312,237 -39,29 -70,58 -70,66 0,21 107,115 203,179 95,64 237,133 295,143 20,4 49,12 63,20 96,48 519,48 619,-1 14,-7 41,-16 60,-20 65,-13 262,-118 360,-191 99,-74 250,-230 372,-384 34,-44 70,-80 80,-80 9,0 39,15 67,33 28,17 70,44 94,58 23,15 121,76 217,136 96,60 254,156 350,213 96,56 272,160 390,230 118,70 230,135 248,144 41,21 41,45 -3,116 -18,30 -46,75 -61,100 -64,106 -252,348 -352,454 -106,112 -169,171 -293,274 -197,164 -310,235 -594,376 -215,106 -519,205 -735,240 -137,22 -574,51 -610,41 z"
|
||||
/>
|
||||
<path
|
||||
d="m 29095,12736 c -421,-44 -744,-157 -975,-342 -259,-207 -396,-446 -465,-809 -16,-84 -23,-301 -14,-425 36,-518 257,-859 694,-1070 166,-80 284,-116 716,-215 449,-103 514,-121 646,-186 218,-107 308,-253 306,-494 -2,-303 -188,-477 -588,-551 -140,-26 -640,-27 -825,-1 -275,38 -486,94 -776,203 -94,35 -174,64 -178,64 -3,0 -6,-175 -6,-389 v -388 l 88,-41 c 404,-187 905,-282 1486,-282 675,0 1154,150 1465,459 196,194 311,434 362,756 20,121 17,476 -5,600 -89,517 -358,800 -945,994 -130,43 -241,71 -616,156 -137,31 -299,73 -360,92 -331,106 -455,246 -455,511 1,249 127,412 387,501 272,92 801,76 1269,-39 116,-28 326,-96 432,-138 l 52,-22 -2,406 -3,406 -66,28 c -154,66 -413,140 -604,174 -279,49 -756,69 -1020,42 z"
|
||||
/>
|
||||
<path
|
||||
d="m 9630,12676 c 0,-2 403,-996 896,-2208 493,-1211 898,-2211 901,-2220 6,-21 -135,-386 -193,-499 -104,-202 -256,-324 -471,-380 -118,-30 -340,-43 -516,-29 -84,6 -185,17 -225,24 -40,7 -75,10 -77,7 -3,-2 -5,-163 -5,-357 v -353 l 63,-30 c 194,-93 493,-138 801,-120 414,23 683,115 937,319 174,140 357,402 474,680 26,62 1945,5159 1945,5167 0,2 -232,2 -516,1 l -517,-3 -556,-1550 c -485,-1350 -560,-1550 -578,-1553 -18,-3 -26,11 -62,105 -23,59 -295,758 -605,1553 l -562,1445 -567,3 c -312,1 -567,0 -567,-2 z"
|
||||
/>
|
||||
<path
|
||||
d="m 15690,10867 c 0,-1970 -1,-1936 56,-2153 128,-497 461,-787 1014,-885 147,-26 440,-36 605,-20 413,40 834,200 1181,447 35,25 73,44 88,44 14,0 26,-1 26,-3 0,-2 20,-94 45,-205 25,-111 45,-204 45,-207 0,-3 209,-5 465,-5 h 465 v 2400 2400 h -535 -535 l -2,-1866 -3,-1866 -115,-50 c -425,-185 -743,-252 -1088,-227 -302,21 -472,109 -562,293 -79,161 -74,11 -77,1969 l -3,1747 h -535 -535 z"
|
||||
/>
|
||||
<path
|
||||
d="m 24785,12668 c -3,-7 -4,-1086 -3,-2398 l 3,-2385 538,-3 537,-2 v 2400 2400 h -535 c -419,0 -537,-3 -540,-12 z"
|
||||
/>
|
||||
<path
|
||||
d="m 21660,8275 v -545 h 565 565 v 545 545 h -565 -565 z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.0 KiB |
311
install.zsh
@@ -120,6 +120,10 @@ function at-repos-pull() {
|
||||
cd ..
|
||||
fi
|
||||
done
|
||||
# Copy feed-generator Dockerfile if missing (removed by git checkout)
|
||||
if [ ! -f $d/repos/feed-generator/Dockerfile ] && [ -f $d/docker/feed/Dockerfile ];then
|
||||
cp -rf $d/docker/feed/Dockerfile $d/repos/feed-generator/
|
||||
fi
|
||||
cd $d
|
||||
}
|
||||
|
||||
@@ -330,6 +334,41 @@ export const SOCIAL_APP_DOMAIN =\
|
||||
}' lib/constants.ts 2>/dev/null || true
|
||||
# Fix parseInt() to handle undefined by adding || ''
|
||||
sediment "s/parseInt(env('\([^']*\)'))/parseInt(env('\1') || '0')/g" lib/constants.ts 2>/dev/null || true
|
||||
|
||||
# Add next-runtime-env to package.json if missing (used by lib/constants.ts)
|
||||
if ! grep -q 'next-runtime-env' package.json 2>/dev/null; then
|
||||
echo "📦 Adding next-runtime-env to package.json..."
|
||||
python3 -c "
|
||||
import json
|
||||
with open('package.json') as f:
|
||||
d = json.load(f)
|
||||
d.setdefault('dependencies', {})['next-runtime-env'] = '^1.6.2'
|
||||
with open('package.json', 'w') as f:
|
||||
json.dump(d, f, indent=2)
|
||||
f.write('\n')
|
||||
"
|
||||
echo "✅ Added next-runtime-env"
|
||||
fi
|
||||
|
||||
# Update @atproto/ozone in service/package.json to latest
|
||||
echo "📦 Updating @atproto/ozone in service/package.json..."
|
||||
local latest_ozone_ver
|
||||
latest_ozone_ver=$(npm view @atproto/ozone version 2>/dev/null)
|
||||
if [ -n "$latest_ozone_ver" ] && [ -f service/package.json ]; then
|
||||
python3 -c "
|
||||
import json, sys
|
||||
ver = sys.argv[1]
|
||||
with open('service/package.json') as f:
|
||||
d = json.load(f)
|
||||
old = d.get('dependencies', {}).get('@atproto/ozone', '')
|
||||
d.setdefault('dependencies', {})['@atproto/ozone'] = ver
|
||||
with open('service/package.json', 'w') as f:
|
||||
json.dump(d, f, indent=2)
|
||||
f.write('\n')
|
||||
print(f'✅ @atproto/ozone: {old} -> {ver}')
|
||||
" "$latest_ozone_ver"
|
||||
fi
|
||||
|
||||
popd > /dev/null
|
||||
}
|
||||
|
||||
@@ -378,23 +417,86 @@ export const handler = async (ctx: AppContext, params: QueryParams) => {
|
||||
EOF
|
||||
|
||||
echo "✅ Created src/algos/app.ts"
|
||||
|
||||
# Restore Dockerfile (removed during patch apply to avoid conflicts)
|
||||
if [ ! -f $d/repos/feed-generator/Dockerfile ] && [ -f $d/docker/feed/Dockerfile ];then
|
||||
cp -rf $d/docker/feed/Dockerfile $d/repos/feed-generator/
|
||||
echo "✅ Restored Dockerfile"
|
||||
fi
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
}
|
||||
|
||||
function at-repos-docker-verify() {
|
||||
local service=$1
|
||||
local image="at-${service}:latest"
|
||||
local check_file=""
|
||||
case $service in
|
||||
pds) check_file="/app/services/pds/index.js" ;;
|
||||
bsky) check_file="/app/services/bsky/api.js" ;;
|
||||
ozone) check_file="/app/services/ozone/api.js" ;;
|
||||
bgs) check_file="/relay" ;;
|
||||
plc) check_file="/app/packages/server/dist/index.js" ;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
local cid
|
||||
cid=$(docker create --entrypoint "" "$image" true 2>&1)
|
||||
if [ $? -ne 0 ]; then
|
||||
echo " ❌ FAILED: cannot create container from $image"
|
||||
echo " $cid"
|
||||
return 1
|
||||
fi
|
||||
if docker cp "$cid:$check_file" /tmp/.docker-verify-tmp 2>/dev/null; then
|
||||
rm -f /tmp/.docker-verify-tmp
|
||||
docker rm "$cid" > /dev/null 2>&1
|
||||
echo " ✅ Verified: $check_file exists"
|
||||
return 0
|
||||
else
|
||||
rm -f /tmp/.docker-verify-tmp
|
||||
docker rm "$cid" > /dev/null 2>&1
|
||||
echo " ❌ FAILED: $check_file not found in $image"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
function at-repos-build-docker-atproto() {
|
||||
cd $d
|
||||
docker image prune -a
|
||||
local failed=()
|
||||
if [ -z "$1" ];then
|
||||
for ((i=1; i<=${#services}; i++)); do
|
||||
service=${services[$i]}
|
||||
docker compose build --no-cache $service
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "🔨 Building: $service"
|
||||
if ! docker compose build --no-cache $service; then
|
||||
echo " ❌ Build failed: $service"
|
||||
failed+=($service)
|
||||
continue
|
||||
fi
|
||||
if ! at-repos-docker-verify $service; then
|
||||
failed+=($service)
|
||||
continue
|
||||
fi
|
||||
if [ "$service" = "ozone" ]; then
|
||||
docker compose build --no-cache ${service}-web
|
||||
fi
|
||||
done
|
||||
else
|
||||
docker compose build --no-cache $1
|
||||
echo "🔨 Building: $1"
|
||||
if ! docker compose build --no-cache $1; then
|
||||
echo "❌ Build failed: $1"
|
||||
return 1
|
||||
fi
|
||||
if ! at-repos-docker-verify $1; then
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
if [ ${#failed[@]} -gt 0 ]; then
|
||||
echo ""
|
||||
echo "❌ Failed builds: ${failed[*]}"
|
||||
echo "⚠️ Do NOT push these images."
|
||||
return 1
|
||||
fi
|
||||
echo ""
|
||||
echo "✅ All builds verified successfully."
|
||||
}
|
||||
|
||||
function at-repos-push-reset() {
|
||||
@@ -412,25 +514,41 @@ function at-repos-push-reset() {
|
||||
}
|
||||
|
||||
function at-repos-push-docker() {
|
||||
local dtag=$(date +%Y%m%d)
|
||||
if [ -z "$1" ] || [ "$1" = "push" ]; then
|
||||
for service in "${services[@]}"; do
|
||||
if ! at-repos-docker-verify $service; then
|
||||
echo "⚠️ Skipping push: $service (verification failed)"
|
||||
continue
|
||||
fi
|
||||
docker tag at-${service}:latest localhost:${dport}/${service}:latest
|
||||
docker tag at-${service}:latest localhost:${dport}/${service}:${dtag}
|
||||
docker push localhost:${dport}/${service}:latest
|
||||
docker push localhost:${dport}/${service}:${dtag}
|
||||
if [ "$service" = "ozone" ]; then
|
||||
docker tag at-${service}-web:latest localhost:${dport}/${service}-web:latest
|
||||
docker tag at-${service}-web:latest localhost:${dport}/${service}-web:${dtag}
|
||||
docker push localhost:${dport}/${service}-web:latest
|
||||
docker push localhost:${dport}/${service}-web:${dtag}
|
||||
fi
|
||||
done
|
||||
else
|
||||
if ! at-repos-docker-verify $1; then
|
||||
echo "❌ Push aborted: $1 (verification failed)"
|
||||
return 1
|
||||
fi
|
||||
docker tag at-${1}:latest localhost:${dport}/${1}:latest
|
||||
docker tag at-${1}:latest localhost:${dport}/${1}:${dtag}
|
||||
docker push localhost:${dport}/${1}:latest
|
||||
docker push localhost:${dport}/${1}:${dtag}
|
||||
fi
|
||||
echo "📦 Pushed with tags: latest, ${dtag}"
|
||||
}
|
||||
|
||||
function at-repos-pull-docker() {
|
||||
cd $d
|
||||
docker image prune -a
|
||||
docker compose up -d --pull always
|
||||
echo "💡 Run 'docker image prune' manually to clean up old images."
|
||||
}
|
||||
|
||||
function at-repos-reset-bgs-db() {
|
||||
@@ -572,8 +690,179 @@ curl -sL -X POST -H "Content-Type: application/json" -H "Authorization: Bearer $
|
||||
https://${host}/xrpc/com.atproto.repo.putRecord
|
||||
}
|
||||
|
||||
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
# Patch creation helpers
|
||||
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
# Find existing patches that modify the same file
|
||||
# Usage: at-patch-find-conflicts <filepath> <patch-dir>
|
||||
function at-patch-find-conflicts() {
|
||||
local filepath="$1"
|
||||
local patch_dir="$2"
|
||||
local conflicts=()
|
||||
|
||||
if [ ! -d "$patch_dir" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
for pf in "$patch_dir"/*.patch(N) "$patch_dir"/*.diff(N); do
|
||||
[ -f "$pf" ] || continue
|
||||
if grep -q "^--- a/$filepath" "$pf" 2>/dev/null || grep -q "^+++ b/$filepath" "$pf" 2>/dev/null; then
|
||||
conflicts+=("$(basename "$pf")")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#conflicts[@]} -gt 0 ]; then
|
||||
echo "⚠️ This file is also modified by:"
|
||||
for c in "${conflicts[@]}"; do
|
||||
echo " - $c"
|
||||
done
|
||||
echo ""
|
||||
echo " Ensure patches are applied in order before patch-begin."
|
||||
echo " Baseline must reflect the post-earlier-patches state."
|
||||
fi
|
||||
}
|
||||
|
||||
# Save current file state as baseline for patch creation
|
||||
# Usage: ./install.zsh patch-begin <repo> <file-path> [--ios]
|
||||
# Example: ./install.zsh patch-begin social-app "src/screens/Profile/Header/ProfileHeaderStandard.tsx" --ios
|
||||
function at-patch-begin() {
|
||||
local repo="$1"
|
||||
local filepath="$2"
|
||||
local flag="$3"
|
||||
|
||||
if [ -z "$repo" ] || [ -z "$filepath" ]; then
|
||||
echo "Usage: ./install.zsh patch-begin <repo> <file-path> [--ios]"
|
||||
echo "Example: ./install.zsh patch-begin social-app \"src/screens/Profile/Header/ProfileHeaderStandard.tsx\" --ios"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local full_path="$d/repos/$repo/$filepath"
|
||||
if [ ! -f "$full_path" ]; then
|
||||
echo "❌ File not found: $full_path"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local tmp_file="/tmp/patch-base--$(echo "$filepath" | tr '/' '-')"
|
||||
cp "$full_path" "$tmp_file"
|
||||
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "✅ Baseline saved: $tmp_file"
|
||||
echo " File: $full_path"
|
||||
echo ""
|
||||
|
||||
# Check for conflicting patches
|
||||
if [ "$flag" = "--ios" ]; then
|
||||
at-patch-find-conflicts "$filepath" "$d/ios/patching"
|
||||
else
|
||||
at-patch-find-conflicts "$filepath" "$d/patching"
|
||||
fi
|
||||
|
||||
echo "Next steps:"
|
||||
echo " 1. Edit: $full_path"
|
||||
echo " 2. Save: ./install.zsh patch-save <NNN-name.patch> $repo \"$filepath\" ${flag:---ios}"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
}
|
||||
|
||||
# Generate patch from baseline diff
|
||||
# Usage: ./install.zsh patch-save <patch-filename> <repo> <file-path> [--ios]
|
||||
# Example: ./install.zsh patch-save 042-social-app-ios-feature.patch social-app "src/path/to/file.tsx" --ios
|
||||
function at-patch-save() {
|
||||
local patch_filename="$1"
|
||||
local repo="$2"
|
||||
local filepath="$3"
|
||||
local flag="$4"
|
||||
|
||||
if [ -z "$patch_filename" ] || [ -z "$repo" ] || [ -z "$filepath" ]; then
|
||||
echo "Usage: ./install.zsh patch-save <patch-filename> <repo> <file-path> [--ios]"
|
||||
echo "Example: ./install.zsh patch-save 042-social-app-ios-feature.patch social-app \"src/file.tsx\" --ios"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local full_path="$d/repos/$repo/$filepath"
|
||||
local tmp_file="/tmp/patch-base--$(echo "$filepath" | tr '/' '-')"
|
||||
|
||||
if [ ! -f "$tmp_file" ]; then
|
||||
echo "❌ No baseline found. Run 'patch-begin' first."
|
||||
return 1
|
||||
fi
|
||||
if [ ! -f "$full_path" ]; then
|
||||
echo "❌ File not found: $full_path"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Determine output directory
|
||||
local patch_dir="$d/patching"
|
||||
if [ "$flag" = "--ios" ]; then
|
||||
patch_dir="$d/ios/patching"
|
||||
fi
|
||||
|
||||
# Generate diff with proper a/b paths
|
||||
diff -u "$tmp_file" "$full_path" \
|
||||
| sed "1s|--- .*|--- a/$filepath|" \
|
||||
| sed "2s|+++ .*|+++ b/$filepath|" \
|
||||
> "$patch_dir/$patch_filename"
|
||||
|
||||
local line_count
|
||||
line_count=$(wc -l < "$patch_dir/$patch_filename" | tr -d ' ')
|
||||
if [ "$line_count" -eq 0 ]; then
|
||||
echo "⚠️ No differences found. Patch file is empty."
|
||||
rm "$patch_dir/$patch_filename"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "📝 Patch: $patch_dir/$patch_filename ($line_count lines)"
|
||||
|
||||
# Dry-run verify: restore baseline, test patch, restore edit
|
||||
pushd "$d/repos/$repo" > /dev/null
|
||||
cp "$full_path" /tmp/patch-edited-tmp
|
||||
cp "$tmp_file" "$full_path"
|
||||
|
||||
if patch --dry-run -p1 < "$patch_dir/$patch_filename" > /dev/null 2>&1; then
|
||||
echo "✅ Dry-run: OK"
|
||||
else
|
||||
echo "❌ Dry-run: FAILED"
|
||||
patch --dry-run -p1 < "$patch_dir/$patch_filename" 2>&1 | head -5
|
||||
fi
|
||||
|
||||
cp /tmp/patch-edited-tmp "$full_path"
|
||||
rm -f /tmp/patch-edited-tmp
|
||||
popd > /dev/null
|
||||
|
||||
rm -f "$tmp_file"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
}
|
||||
|
||||
# Verify all patches can be applied (full dry-run)
|
||||
# Usage: ./install.zsh patch-check [--ios]
|
||||
function at-patch-check() {
|
||||
local flag="$1"
|
||||
|
||||
if [ "$flag" = "--ios" ]; then
|
||||
echo "Checking iOS patches against: $d/repos/social-app"
|
||||
pushd "$d/repos/social-app" > /dev/null
|
||||
for pf in "$d/ios/patching"/*.patch; do
|
||||
[ -f "$pf" ] || continue
|
||||
local name="$(basename "$pf")"
|
||||
if patch --dry-run -p1 < "$pf" > /dev/null 2>&1; then
|
||||
echo " ✅ $name"
|
||||
else
|
||||
echo " ❌ $name"
|
||||
fi
|
||||
done
|
||||
popd > /dev/null
|
||||
else
|
||||
echo "Checking server patches..."
|
||||
for pf in "$d/patching"/*.patch "$d/patching"/*.diff; do
|
||||
[ -f "$pf" ] || continue
|
||||
echo " $(basename "$pf")"
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
at-repos-env
|
||||
case "$1" in
|
||||
case "$1" in
|
||||
pull)
|
||||
at-repos-clone
|
||||
at-repos-pull
|
||||
@@ -587,6 +876,18 @@ case "$1" in
|
||||
show-failed-patches
|
||||
exit
|
||||
;;
|
||||
patch-begin)
|
||||
at-patch-begin "$2" "$3" "$4"
|
||||
exit
|
||||
;;
|
||||
patch-save)
|
||||
at-patch-save "$2" "$3" "$4" "$5"
|
||||
exit
|
||||
;;
|
||||
patch-check)
|
||||
at-patch-check "$2"
|
||||
exit
|
||||
;;
|
||||
build)
|
||||
at-repos-build-docker-atproto $2
|
||||
exit
|
||||
|
||||
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 821 B |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 821 B |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 6.3 KiB |
BIN
ios/assets/favicons/atproto.com.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
ios/assets/favicons/bsky.app.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
ios/assets/favicons/bsky.chat.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 574 B After Width: | Height: | Size: 536 B |
|
Before Width: | Height: | Size: 1005 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1005 B After Width: | Height: | Size: 1.1 KiB |
BIN
ios/assets/favicons/syui.ai.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
1
ios/assets/icons/github.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M237.9 461.4C237.9 463.4 235.6 465 232.7 465C229.4 465.3 227.1 463.7 227.1 461.4C227.1 459.4 229.4 457.8 232.3 457.8C235.3 457.5 237.9 459.1 237.9 461.4zM206.8 456.9C206.1 458.9 208.1 461.2 211.1 461.8C213.7 462.8 216.7 461.8 217.3 459.8C217.9 457.8 216 455.5 213 454.6C210.4 453.9 207.5 454.9 206.8 456.9zM251 455.2C248.1 455.9 246.1 457.8 246.4 460.1C246.7 462.1 249.3 463.4 252.3 462.7C255.2 462 257.2 460.1 256.9 458.1C256.6 456.2 253.9 454.9 251 455.2zM316.8 72C178.1 72 72 177.3 72 316C72 426.9 141.8 521.8 241.5 555.2C254.3 557.5 258.8 549.6 258.8 543.1C258.8 536.9 258.5 502.7 258.5 481.7C258.5 481.7 188.5 496.7 173.8 451.9C173.8 451.9 162.4 422.8 146 415.3C146 415.3 123.1 399.6 147.6 399.9C147.6 399.9 172.5 401.9 186.2 425.7C208.1 464.3 244.8 453.2 259.1 446.6C261.4 430.6 267.9 419.5 275.1 412.9C219.2 406.7 162.8 398.6 162.8 302.4C162.8 274.9 170.4 261.1 186.4 243.5C183.8 237 175.3 210.2 189 175.6C209.9 169.1 258 202.6 258 202.6C278 197 299.5 194.1 320.8 194.1C342.1 194.1 363.6 197 383.6 202.6C383.6 202.6 431.7 169 452.6 175.6C466.3 210.3 457.8 237 455.2 243.5C471.2 261.2 481 275 481 302.4C481 398.9 422.1 406.6 366.2 412.9C375.4 420.8 383.2 435.8 383.2 459.3C383.2 493 382.9 534.7 382.9 542.9C382.9 549.4 387.5 557.3 400.2 555C500.2 521.8 568 426.9 568 316C568 177.3 455.5 72 316.8 72zM169.2 416.9C167.9 417.9 168.2 420.2 169.9 422.1C171.5 423.7 173.8 424.4 175.1 423.1C176.4 422.1 176.1 419.8 174.4 417.9C172.8 416.3 170.5 415.6 169.2 416.9zM158.4 408.8C157.7 410.1 158.7 411.7 160.7 412.7C162.3 413.7 164.3 413.4 165 412C165.7 410.7 164.7 409.1 162.7 408.1C160.7 407.5 159.1 407.8 158.4 408.8zM190.8 444.4C189.2 445.7 189.8 448.7 192.1 450.6C194.4 452.9 197.3 453.2 198.6 451.6C199.9 450.3 199.3 447.3 197.3 445.4C195.1 443.1 192.1 442.8 190.8 444.4zM179.4 429.7C177.8 430.7 177.8 433.3 179.4 435.6C181 437.9 183.7 438.9 185 437.9C186.6 436.6 186.6 434 185 431.7C183.6 429.4 181 428.4 179.4 429.7z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
1
ios/assets/icons/x.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M453.2 112L523.8 112L369.6 288.2L551 528L409 528L297.7 382.6L170.5 528L99.8 528L264.7 339.5L90.8 112L236.4 112L336.9 244.9L453.2 112zM428.4 485.8L467.5 485.8L215.1 152L173.1 152L428.4 485.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 421 B |
1
ios/assets/icons/youtube.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M581.7 188.1C575.5 164.4 556.9 145.8 533.4 139.5C490.9 128 320.1 128 320.1 128C320.1 128 149.3 128 106.7 139.5C83.2 145.8 64.7 164.4 58.4 188.1C47 231 47 320.4 47 320.4C47 320.4 47 409.8 58.4 452.7C64.7 476.3 83.2 494.2 106.7 500.5C149.3 512 320.1 512 320.1 512C320.1 512 490.9 512 533.5 500.5C557 494.2 575.5 476.3 581.8 452.7C593.2 409.8 593.2 320.4 593.2 320.4C593.2 320.4 593.2 231 581.8 188.1zM264.2 401.6L264.2 239.2L406.9 320.4L264.2 401.6z"/></svg>
|
||||
|
After Width: | Height: | Size: 678 B |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 26 KiB |
BIN
ios/assets/logo_old.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
ios/assets/logo_white.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
ios/assets/splash/illustration-mobile-dark.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
ios/assets/splash/illustration-mobile.mp4
Normal file
BIN
ios/assets/splash/illustration-mobile.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
ios/icon.png
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 26 KiB |
@@ -1,9 +1,7 @@
|
||||
diff --git a/app.config.js b/app.config.js
|
||||
index ca417206b..0807d7cd3 100644
|
||||
--- a/app.config.js
|
||||
+++ b/app.config.js
|
||||
@@ -18,10 +18,7 @@ module.exports = function (_config) {
|
||||
const IS_DEV = !IS_TESTFLIGHT || !IS_PRODUCTION
|
||||
@@ -23,10 +23,7 @@
|
||||
const IS_DEV = !IS_TESTFLIGHT && !IS_PRODUCTION
|
||||
|
||||
const ASSOCIATED_DOMAINS = [
|
||||
- 'applinks:bsky.app',
|
||||
@@ -14,7 +12,7 @@ index ca417206b..0807d7cd3 100644
|
||||
// When testing local services, enter an ngrok (et al) domain here. It must use a standard HTTP/HTTPS port.
|
||||
...(IS_DEV || IS_TESTFLIGHT ? [] : []),
|
||||
]
|
||||
@@ -33,27 +30,25 @@ module.exports = function (_config) {
|
||||
@@ -38,27 +35,25 @@
|
||||
return {
|
||||
expo: {
|
||||
version: VERSION,
|
||||
@@ -29,7 +27,7 @@ index ca417206b..0807d7cd3 100644
|
||||
policy: 'appVersion',
|
||||
},
|
||||
- icon: './assets/app-icons/ios_icon_default_next.png',
|
||||
+ icon: './assets/logo.png',
|
||||
+ icon: './assets/app-icon.png',
|
||||
userInterfaceStyle: 'automatic',
|
||||
primaryColor: '#1083fe',
|
||||
newArchEnabled: false,
|
||||
@@ -45,25 +43,25 @@ index ca417206b..0807d7cd3 100644
|
||||
- PLATFORM === 'web' // web build doesn't like .icon files
|
||||
- ? './assets/app-icons/ios_icon_default_next.png'
|
||||
- : './assets/app-icons/ios_icon_default.icon',
|
||||
+ icon: './assets/logo.png',
|
||||
+ icon: './assets/app-icon.png',
|
||||
infoPlist: {
|
||||
UIBackgroundModes: ['remote-notification'],
|
||||
NSCameraUsageDescription:
|
||||
@@ -113,7 +108,7 @@ module.exports = function (_config) {
|
||||
@@ -118,7 +113,7 @@
|
||||
entitlements: {
|
||||
'com.apple.developer.kernel.increased-memory-limit': true,
|
||||
'com.apple.developer.kernel.extended-virtual-addressing': true,
|
||||
- 'com.apple.security.application-groups': 'group.app.bsky',
|
||||
+ 'com.apple.security.application-groups': 'group.ai.syui.at',
|
||||
// 'com.apple.developer.device-information.user-assigned-device-name': true,
|
||||
},
|
||||
privacyManifests: {
|
||||
NSPrivacyCollectedDataTypes: [
|
||||
@@ -175,14 +170,14 @@ module.exports = function (_config) {
|
||||
@@ -181,14 +176,14 @@
|
||||
barStyle: 'light-content',
|
||||
},
|
||||
android: {
|
||||
- icon: './assets/app-icons/android_icon_default_next.png',
|
||||
+ icon: './assets/logo.png',
|
||||
+ icon: './assets/app-icon.png',
|
||||
adaptiveIcon: {
|
||||
foregroundImage: './assets/icon-android-foreground.png',
|
||||
monochromeImage: './assets/icon-android-monochrome.png',
|
||||
@@ -75,38 +73,16 @@ index ca417206b..0807d7cd3 100644
|
||||
intentFilters: [
|
||||
{
|
||||
action: 'VIEW',
|
||||
@@ -190,7 +185,7 @@ module.exports = function (_config) {
|
||||
@@ -196,7 +191,7 @@
|
||||
data: [
|
||||
{
|
||||
scheme: 'https',
|
||||
- host: 'bsky.app',
|
||||
+ host: 'syu.is',
|
||||
},
|
||||
IS_DEV && {
|
||||
scheme: 'http',
|
||||
@@ -213,9 +208,9 @@ module.exports = function (_config) {
|
||||
: undefined,
|
||||
codeSigningMetadata: UPDATES_ENABLED
|
||||
? {
|
||||
- keyid: 'main',
|
||||
- alg: 'rsa-v1_5-sha256',
|
||||
- }
|
||||
+ keyid: 'main',
|
||||
+ alg: 'rsa-v1_5-sha256',
|
||||
+ }
|
||||
: undefined,
|
||||
checkAutomatically: 'NEVER',
|
||||
},
|
||||
@@ -225,7 +220,7 @@ module.exports = function (_config) {
|
||||
'expo-web-browser',
|
||||
[
|
||||
'react-native-edge-to-edge',
|
||||
- {android: {enforceNavigationBarContrast: false}},
|
||||
+ { android: { enforceNavigationBarContrast: false } },
|
||||
],
|
||||
USE_SENTRY && [
|
||||
'@sentry/react-native/expo',
|
||||
@@ -265,7 +260,6 @@ module.exports = function (_config) {
|
||||
...(IS_DEV
|
||||
? [
|
||||
@@ -280,7 +275,6 @@
|
||||
networkInstrumentation: true,
|
||||
},
|
||||
],
|
||||
@@ -114,7 +90,7 @@ index ca417206b..0807d7cd3 100644
|
||||
'./plugins/withGradleJVMHeapSizeIncrease.js',
|
||||
'./plugins/withAndroidManifestLargeHeapPlugin.js',
|
||||
'./plugins/withAndroidManifestFCMIconPlugin.js',
|
||||
@@ -273,8 +267,6 @@ module.exports = function (_config) {
|
||||
@@ -288,8 +282,6 @@
|
||||
'./plugins/withAndroidStylesAccentColorPlugin.js',
|
||||
'./plugins/withAndroidDayNightThemePlugin.js',
|
||||
'./plugins/withAndroidNoJitpackPlugin.js',
|
||||
@@ -123,16 +99,7 @@ index ca417206b..0807d7cd3 100644
|
||||
[
|
||||
'expo-font',
|
||||
{
|
||||
@@ -387,7 +379,7 @@ module.exports = function (_config) {
|
||||
},
|
||||
},
|
||||
],
|
||||
- ['expo-screen-orientation', {initialOrientation: 'PORTRAIT_UP'}],
|
||||
+ ['expo-screen-orientation', { initialOrientation: 'PORTRAIT_UP' }],
|
||||
['expo-location'],
|
||||
[
|
||||
'expo-contacts',
|
||||
@@ -402,30 +394,7 @@ module.exports = function (_config) {
|
||||
@@ -417,30 +409,7 @@
|
||||
build: {
|
||||
experimental: {
|
||||
ios: {
|
||||
|
||||
@@ -38,7 +38,7 @@ index 231447b4f..a44b3da05 100644
|
||||
-const HELP_DESK_LANG = 'en-us'
|
||||
-export const HELP_DESK_URL = `https://blueskyweb.zendesk.com/hc/${HELP_DESK_LANG}`
|
||||
+const HELP_DESK_LANG = 'ja-jp'
|
||||
+export const HELP_DESK_URL = 'https://syu.is/about/support/help'
|
||||
+export const HELP_DESK_URL = '/support/license'
|
||||
export const EMBED_SERVICE = 'https://embed.bsky.app'
|
||||
export const EMBED_SCRIPT = `${EMBED_SERVICE}/static/embed.js`
|
||||
export const BSKY_DOWNLOAD_URL = 'https://bsky.app/download'
|
||||
@@ -102,8 +102,8 @@ index 231447b4f..a44b3da05 100644
|
||||
export const webLinks = {
|
||||
- tos: `https://bsky.social/about/support/tos`,
|
||||
- privacy: `https://bsky.social/about/support/privacy-policy`,
|
||||
+ tos: `https://syu.is/about/support/tos`,
|
||||
+ privacy: `https://syu.is/about/support/privacy-policy`,
|
||||
+ tos: `/support/tos`,
|
||||
+ privacy: `/support/privacy-policy`,
|
||||
community: `https://bsky.social/about/support/community-guidelines`,
|
||||
communityDeprecated: `https://bsky.social/about/support/community-guidelines-deprecated`,
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ index 2c4d6fa41..b69e2b18d 100644
|
||||
-prefetchAgeAssuranceConfig()
|
||||
+// // // prefetchAgeAssuranceConfig()
|
||||
prefetchLiveEvents()
|
||||
|
||||
|
||||
function InnerApp() {
|
||||
diff --git a/src/routes.ts b/src/routes.ts
|
||||
index f325539c7..3e2c7b3eb 100644
|
||||
@@ -21,11 +21,11 @@ index f325539c7..3e2c7b3eb 100644
|
||||
Support: '/support',
|
||||
- PrivacyPolicy: '/support/privacy',
|
||||
- TermsOfService: '/support/tos',
|
||||
+ PrivacyPolicy: 'https://syu.is/about/support/privacy-policy',
|
||||
+ TermsOfService: 'https://syu.is/about/support/tos',
|
||||
+ PrivacyPolicy: ['/support/privacy-policy', '/about/support/privacy-policy'],
|
||||
+ TermsOfService: ['/support/tos', '/about/support/tos'],
|
||||
CommunityGuidelines: '/support/community-guidelines',
|
||||
+ License: 'https://syu.is/about/support/license',
|
||||
+ AppInfo: 'https://syu.is/about/support/app',
|
||||
+ License: ['/support/license', '/about/support/license'],
|
||||
+ AppInfo: '/support/app',
|
||||
CopyrightPolicy: '/support/copyright',
|
||||
// hashtags
|
||||
Hashtag: '/hashtag/:tag',
|
||||
@@ -35,41 +35,41 @@ index 5c8ce3b97..ee85beb08 100644
|
||||
+++ b/src/state/session/agent.ts
|
||||
@@ -47,7 +47,8 @@ export function createPublicAgent() {
|
||||
configureModerationForGuest() // Side effect but only relevant for tests
|
||||
|
||||
|
||||
const agent = new BskyAppAgent({service: PUBLIC_BSKY_SERVICE})
|
||||
- agent.configureProxy(BLUESKY_PROXY_HEADER.get())
|
||||
+ // Disable proxy for self-hosted environments
|
||||
+ // agent.configureProxy(BLUESKY_PROXY_HEADER.get())
|
||||
return agent
|
||||
}
|
||||
|
||||
|
||||
@@ -88,7 +89,8 @@ export async function createAgentAndResume(
|
||||
// after session is attached
|
||||
const aa = prefetchAgeAssuranceData({agent})
|
||||
|
||||
|
||||
- agent.configureProxy(BLUESKY_PROXY_HEADER.get())
|
||||
+ // Disable proxy for self-hosted environments
|
||||
+ // agent.configureProxy(BLUESKY_PROXY_HEADER.get())
|
||||
|
||||
|
||||
return agent.prepare({
|
||||
resolvers: [gates, moderation, aa],
|
||||
@@ -127,7 +129,8 @@ export async function createAgentAndLogin(
|
||||
const moderation = configureModerationForAccount(agent, account)
|
||||
const aa = prefetchAgeAssuranceData({agent})
|
||||
|
||||
|
||||
- agent.configureProxy(BLUESKY_PROXY_HEADER.get())
|
||||
+ // Disable proxy for self-hosted environments
|
||||
+ // agent.configureProxy(BLUESKY_PROXY_HEADER.get())
|
||||
|
||||
|
||||
return agent.prepare({
|
||||
resolvers: [gates, moderation, aa],
|
||||
@@ -299,7 +302,8 @@ export async function createAgentAndCreateAccount(
|
||||
logger.error(e, {message: `session: failed snoozeEmailConfirmationPrompt`})
|
||||
}
|
||||
|
||||
|
||||
- agent.configureProxy(BLUESKY_PROXY_HEADER.get())
|
||||
+ // Disable proxy for self-hosted environments
|
||||
+ // agent.configureProxy(BLUESKY_PROXY_HEADER.get())
|
||||
|
||||
|
||||
return agent.prepare({
|
||||
resolvers: [gates, moderation, aa],
|
||||
|
||||
@@ -1,483 +1,47 @@
|
||||
diff --git a/src/screens/Settings/AboutSettings.tsx b/src/screens/Settings/AboutSettings.tsx
|
||||
index 6b8257b91..48ba7909e 100644
|
||||
--- a/src/screens/Settings/AboutSettings.tsx
|
||||
+++ b/src/screens/Settings/AboutSettings.tsx
|
||||
@@ -80,7 +80,7 @@ export function AboutSettingsScreen({}: Props) {
|
||||
@@ -79,7 +79,7 @@
|
||||
<Layout.Content>
|
||||
<SettingsList.Container>
|
||||
<SettingsList.LinkItem
|
||||
- to="https://bsky.social/about/support/tos"
|
||||
+ to="https://syu.is/about/support/tos"
|
||||
+ to="/support/tos"
|
||||
label={_(msg`Terms of Service`)}>
|
||||
<SettingsList.ItemIcon icon={NewspaperIcon} />
|
||||
<SettingsList.ItemText>
|
||||
@@ -88,7 +88,7 @@ export function AboutSettingsScreen({}: Props) {
|
||||
@@ -87,7 +87,7 @@
|
||||
</SettingsList.ItemText>
|
||||
</SettingsList.LinkItem>
|
||||
<SettingsList.LinkItem
|
||||
- to="https://bsky.social/about/support/privacy-policy"
|
||||
+ to="https://syu.is/about/support/privacy-policy"
|
||||
+ to="/support/privacy-policy"
|
||||
label={_(msg`Privacy Policy`)}>
|
||||
<SettingsList.ItemIcon icon={NewspaperIcon} />
|
||||
<SettingsList.ItemText>
|
||||
diff --git a/src/screens/Takendown.tsx b/src/screens/Takendown.tsx
|
||||
index 77f219e55..53f5e0cc0 100644
|
||||
--- a/src/screens/Takendown.tsx
|
||||
+++ b/src/screens/Takendown.tsx
|
||||
@@ -217,10 +217,10 @@ export function Takendown() {
|
||||
@@ -210,10 +210,10 @@
|
||||
<Trans>
|
||||
Your account was found to be in violation of the{' '}
|
||||
<SimpleInlineLinkText
|
||||
- label={_(msg`Bluesky Social Terms of Service`)}
|
||||
- to="https://bsky.social/about/support/tos"
|
||||
+ label={_(msg`syu.is Terms of Service`)}
|
||||
+ to="https://syu.is/about/support/tos"
|
||||
+ to="/support/tos"
|
||||
style={[a.text_md, a.leading_snug]}>
|
||||
- Bluesky Social Terms of Service
|
||||
+ syu.is Terms of Service
|
||||
</SimpleInlineLinkText>
|
||||
. You have been sent an email outlining the specific violation
|
||||
and suspension period, if applicable. You can appeal this
|
||||
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
|
||||
index e058e2883..8daf41089 100644
|
||||
--- a/src/view/screens/Home.tsx
|
||||
+++ b/src/view/screens/Home.tsx
|
||||
@@ -1,23 +1,16 @@
|
||||
import React from 'react'
|
||||
import {ActivityIndicator, StyleSheet} from 'react-native'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
-
|
||||
import {PROD_DEFAULT_FEED} from '#/lib/constants'
|
||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||
import {useOTAUpdates} from '#/lib/hooks/useOTAUpdates'
|
||||
import {useSetTitle} from '#/lib/hooks/useSetTitle'
|
||||
import {useRequestNotificationsPermission} from '#/lib/notifications/notifications'
|
||||
-import {
|
||||
- type HomeTabNavigatorParams,
|
||||
- type NativeStackScreenProps,
|
||||
-} from '#/lib/routes/types'
|
||||
+import {type HomeTabNavigatorParams, type NativeStackScreenProps} from '#/lib/routes/types'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {emitSoftReset} from '#/state/events'
|
||||
-import {
|
||||
- type SavedFeedSourceInfo,
|
||||
- usePinnedFeedsInfos,
|
||||
-} from '#/state/queries/feed'
|
||||
+import {type SavedFeedSourceInfo, usePinnedFeedsInfos} from '#/state/queries/feed'
|
||||
import {type FeedDescriptor, type FeedParams} from '#/state/queries/post-feed'
|
||||
import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||
import {type UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
|
||||
@@ -27,11 +20,7 @@ import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
||||
import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed'
|
||||
import {FeedPage} from '#/view/com/feeds/FeedPage'
|
||||
import {HomeHeader} from '#/view/com/home/HomeHeader'
|
||||
-import {
|
||||
- Pager,
|
||||
- type PagerRef,
|
||||
- type RenderTabBarFnProps,
|
||||
-} from '#/view/com/pager/Pager'
|
||||
+import {Pager, type PagerRef, type RenderTabBarFnProps} from '#/view/com/pager/Pager'
|
||||
import {CustomFeedEmptyState} from '#/view/com/posts/CustomFeedEmptyState'
|
||||
import {FollowingEmptyState} from '#/view/com/posts/FollowingEmptyState'
|
||||
import {FollowingEndOfFeed} from '#/view/com/posts/FollowingEndOfFeed'
|
||||
@@ -39,97 +28,90 @@ import {NoFeedsPinned} from '#/screens/Home/NoFeedsPinned'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {useDemoMode} from '#/storage/hooks/demo-mode'
|
||||
|
||||
+const SYU_IS_FEED_URI = 'at://did:plc:6qyecktefllvenje24fcxnie/app.bsky.feed.generator/app'
|
||||
+
|
||||
+const DEFAULT_PINNED_FEEDS: any[] = [{
|
||||
+ feedDescriptor: 'following',
|
||||
+ displayName: 'Following',
|
||||
+ id: 'following',
|
||||
+ uri: 'following',
|
||||
+ type: 'feed',
|
||||
+ savedFeed: undefined,
|
||||
+ pinned: true,
|
||||
+ route: { href: '/', name: 'Home', params: {} },
|
||||
+ cid: '',
|
||||
+ avatar: '',
|
||||
+ creatorDid: '',
|
||||
+ creatorHandle: '',
|
||||
+}, {
|
||||
+ feedDescriptor: `feedgen|${SYU_IS_FEED_URI}`,
|
||||
+ displayName: 'Feeds',
|
||||
+ id: SYU_IS_FEED_URI,
|
||||
+ uri: SYU_IS_FEED_URI,
|
||||
+ type: 'feed',
|
||||
+ savedFeed: {
|
||||
+ type: 'feed',
|
||||
+ value: SYU_IS_FEED_URI,
|
||||
+ pinned: true,
|
||||
+ },
|
||||
+ pinned: true,
|
||||
+ route: { href: '/', name: 'Home', params: {} },
|
||||
+ cid: '',
|
||||
+ avatar: '',
|
||||
+ creatorDid: '',
|
||||
+ creatorHandle: '',
|
||||
+}]
|
||||
+
|
||||
type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home' | 'Start'>
|
||||
export function HomeScreen(props: Props) {
|
||||
const {setShowLoggedOut} = useLoggedOutViewControls()
|
||||
const {data: preferences} = usePreferencesQuery()
|
||||
const {currentAccount} = useSession()
|
||||
- const {data: pinnedFeedInfos, isLoading: isPinnedFeedsLoading} =
|
||||
- usePinnedFeedsInfos()
|
||||
+ const {data: pinnedFeedInfos} = usePinnedFeedsInfos()
|
||||
+
|
||||
+ const safePreferences = preferences || { feedViewPrefs: { lab_mergeFeedEnabled: false }, savedFeeds: [] } as any
|
||||
+ // Use user's pinned feeds when logged in and available, otherwise use defaults
|
||||
+ const safePinnedFeedInfos = !currentAccount
|
||||
+ ? DEFAULT_PINNED_FEEDS.filter(f => f.feedDescriptor !== 'following')
|
||||
+ : (pinnedFeedInfos && pinnedFeedInfos.length > 0)
|
||||
+ ? pinnedFeedInfos
|
||||
+ : DEFAULT_PINNED_FEEDS
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isWeb && !currentAccount) {
|
||||
const getParams = new URLSearchParams(window.location.search)
|
||||
const splash = getParams.get('splash')
|
||||
- if (splash === 'true') {
|
||||
- setShowLoggedOut(true)
|
||||
- return
|
||||
- }
|
||||
+ if (splash === 'true') { setShowLoggedOut(true); return }
|
||||
}
|
||||
-
|
||||
const params = props.route.params
|
||||
- if (
|
||||
- currentAccount &&
|
||||
- props.route.name === 'Start' &&
|
||||
- params?.name &&
|
||||
- params?.rkey
|
||||
- ) {
|
||||
- props.navigation.navigate('StarterPack', {
|
||||
- rkey: params.rkey,
|
||||
- name: params.name,
|
||||
- })
|
||||
+ if (currentAccount && props.route.name === 'Start' && params?.name && params?.rkey) {
|
||||
+ props.navigation.navigate('StarterPack', { rkey: params.rkey, name: params.name })
|
||||
}
|
||||
- }, [
|
||||
- currentAccount,
|
||||
- props.navigation,
|
||||
- props.route.name,
|
||||
- props.route.params,
|
||||
- setShowLoggedOut,
|
||||
- ])
|
||||
+ }, [currentAccount, props.navigation, props.route.name, props.route.params, setShowLoggedOut])
|
||||
|
||||
- if (preferences && pinnedFeedInfos && !isPinnedFeedsLoading) {
|
||||
- return (
|
||||
- <Layout.Screen testID="HomeScreen">
|
||||
- <HomeScreenReady
|
||||
- {...props}
|
||||
- preferences={preferences}
|
||||
- pinnedFeedInfos={pinnedFeedInfos}
|
||||
- />
|
||||
- </Layout.Screen>
|
||||
- )
|
||||
- } else {
|
||||
- return (
|
||||
- <Layout.Screen>
|
||||
- <Layout.Center style={styles.loading}>
|
||||
- <ActivityIndicator size="large" />
|
||||
- </Layout.Center>
|
||||
- </Layout.Screen>
|
||||
- )
|
||||
- }
|
||||
+ return (
|
||||
+ <Layout.Screen testID="HomeScreen">
|
||||
+ <HomeScreenReady {...props} preferences={safePreferences} pinnedFeedInfos={safePinnedFeedInfos as any} />
|
||||
+ </Layout.Screen>
|
||||
+ )
|
||||
}
|
||||
|
||||
-function HomeScreenReady({
|
||||
- preferences,
|
||||
- pinnedFeedInfos,
|
||||
-}: Props & {
|
||||
- preferences: UsePreferencesQueryResponse
|
||||
- pinnedFeedInfos: SavedFeedSourceInfo[]
|
||||
-}) {
|
||||
- const allFeeds = React.useMemo(
|
||||
- () => pinnedFeedInfos.map(f => f.feedDescriptor),
|
||||
- [pinnedFeedInfos],
|
||||
- )
|
||||
- const maybeRawSelectedFeed: FeedDescriptor | undefined =
|
||||
- useSelectedFeed() ?? allFeeds[0]
|
||||
+function HomeScreenReady({preferences, pinnedFeedInfos}: any) {
|
||||
+ const allFeeds = React.useMemo(() => pinnedFeedInfos.map(f => f.feedDescriptor), [pinnedFeedInfos])
|
||||
+ const maybeRawSelectedFeed = useSelectedFeed() ?? allFeeds[0]
|
||||
const setSelectedFeed = useSetSelectedFeed()
|
||||
const maybeFoundIndex = allFeeds.indexOf(maybeRawSelectedFeed)
|
||||
const selectedIndex = Math.max(0, maybeFoundIndex)
|
||||
- const maybeSelectedFeed: FeedDescriptor | undefined = allFeeds[selectedIndex]
|
||||
+ const maybeSelectedFeed = allFeeds[selectedIndex]
|
||||
const requestNotificationsPermission = useRequestNotificationsPermission()
|
||||
|
||||
useSetTitle(pinnedFeedInfos[selectedIndex]?.displayName)
|
||||
useOTAUpdates()
|
||||
-
|
||||
- React.useEffect(() => {
|
||||
- requestNotificationsPermission('Home')
|
||||
- }, [requestNotificationsPermission])
|
||||
+ React.useEffect(() => { requestNotificationsPermission('Home') }, [requestNotificationsPermission])
|
||||
|
||||
const pagerRef = React.useRef<PagerRef>(null)
|
||||
const lastPagerReportedIndexRef = React.useRef(selectedIndex)
|
||||
React.useLayoutEffect(() => {
|
||||
- // Since the pager is not a controlled component, adjust it imperatively
|
||||
- // if the selected index gets out of sync with what it last reported.
|
||||
- // This is supposed to only happen on the web when you use the right nav.
|
||||
if (selectedIndex !== lastPagerReportedIndexRef.current) {
|
||||
lastPagerReportedIndexRef.current = selectedIndex
|
||||
pagerRef.current?.setPage(selectedIndex)
|
||||
@@ -138,205 +120,43 @@ function HomeScreenReady({
|
||||
|
||||
const {hasSession} = useSession()
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
- useFocusEffect(
|
||||
- React.useCallback(() => {
|
||||
- setMinimalShellMode(false)
|
||||
- }, [setMinimalShellMode]),
|
||||
- )
|
||||
+ useFocusEffect(React.useCallback(() => { setMinimalShellMode(false) }, [setMinimalShellMode]))
|
||||
|
||||
- useFocusEffect(
|
||||
- useNonReactiveCallback(() => {
|
||||
- if (maybeSelectedFeed) {
|
||||
- logEvent('home:feedDisplayed', {
|
||||
- index: selectedIndex,
|
||||
- feedType: maybeSelectedFeed.split('|')[0],
|
||||
- feedUrl: maybeSelectedFeed,
|
||||
- reason: 'focus',
|
||||
- })
|
||||
- }
|
||||
- }),
|
||||
- )
|
||||
-
|
||||
- const onPageSelected = React.useCallback(
|
||||
- (index: number) => {
|
||||
- setMinimalShellMode(false)
|
||||
- const maybeFeed = allFeeds[index]
|
||||
+ const onPageSelected = React.useCallback((index) => {
|
||||
+ setMinimalShellMode(false)
|
||||
+ const maybeFeed = allFeeds[index]
|
||||
+ lastPagerReportedIndexRef.current = index
|
||||
+ setSelectedFeed(maybeFeed)
|
||||
+ }, [setSelectedFeed, setMinimalShellMode, allFeeds])
|
||||
|
||||
- // Mutate the ref before setting state to avoid the imperative syncing effect
|
||||
- // above from starting a loop on Android when swiping back and forth.
|
||||
- lastPagerReportedIndexRef.current = index
|
||||
- setSelectedFeed(maybeFeed)
|
||||
-
|
||||
- if (maybeFeed) {
|
||||
- logEvent('home:feedDisplayed', {
|
||||
- index,
|
||||
- feedType: maybeFeed.split('|')[0],
|
||||
- feedUrl: maybeFeed,
|
||||
- })
|
||||
- }
|
||||
- },
|
||||
- [setSelectedFeed, setMinimalShellMode, allFeeds],
|
||||
- )
|
||||
-
|
||||
- const onPressSelected = React.useCallback(() => {
|
||||
- emitSoftReset()
|
||||
- }, [])
|
||||
-
|
||||
- const onPageScrollStateChanged = React.useCallback(
|
||||
- (state: 'idle' | 'dragging' | 'settling') => {
|
||||
- 'worklet'
|
||||
- if (state === 'dragging') {
|
||||
- setMinimalShellMode(false)
|
||||
- }
|
||||
- },
|
||||
- [setMinimalShellMode],
|
||||
- )
|
||||
+ const onPressSelected = React.useCallback(() => { emitSoftReset() }, [])
|
||||
+ const onPageScrollStateChanged = React.useCallback((state) => {
|
||||
+ 'worklet'
|
||||
+ if (state === 'dragging') setMinimalShellMode(false)
|
||||
+ }, [setMinimalShellMode])
|
||||
|
||||
const [demoMode] = useDemoMode()
|
||||
+ const renderTabBar = React.useCallback((props) => {
|
||||
+ return <HomeHeader key="FEEDS_TAB_BAR" {...props} testID="homeScreenFeedTabs" onPressSelected={onPressSelected} feeds={pinnedFeedInfos} />
|
||||
+ }, [onPressSelected, pinnedFeedInfos])
|
||||
|
||||
- const renderTabBar = React.useCallback(
|
||||
- (props: RenderTabBarFnProps) => {
|
||||
- if (demoMode) {
|
||||
- return (
|
||||
- <HomeHeader
|
||||
- key="FEEDS_TAB_BAR"
|
||||
- {...props}
|
||||
- testID="homeScreenFeedTabs"
|
||||
- onPressSelected={onPressSelected}
|
||||
- // @ts-ignore
|
||||
- feeds={[{displayName: 'Following'}, {displayName: 'Discover'}]}
|
||||
- />
|
||||
- )
|
||||
- }
|
||||
- return (
|
||||
- <HomeHeader
|
||||
- key="FEEDS_TAB_BAR"
|
||||
- {...props}
|
||||
- testID="homeScreenFeedTabs"
|
||||
- onPressSelected={onPressSelected}
|
||||
- feeds={pinnedFeedInfos}
|
||||
- />
|
||||
- )
|
||||
- },
|
||||
- [onPressSelected, pinnedFeedInfos, demoMode],
|
||||
- )
|
||||
-
|
||||
- const renderFollowingEmptyState = React.useCallback(() => {
|
||||
- return <FollowingEmptyState />
|
||||
- }, [])
|
||||
+ const renderFollowingEmptyState = React.useCallback(() => <FollowingEmptyState />, [])
|
||||
+ const renderCustomFeedEmptyState = React.useCallback(() => <CustomFeedEmptyState />, [])
|
||||
|
||||
- const renderCustomFeedEmptyState = React.useCallback(() => {
|
||||
- return <CustomFeedEmptyState />
|
||||
- }, [])
|
||||
+ const homeFeedParams = React.useMemo(() => ({
|
||||
+ mergeFeedEnabled: false, mergeFeedSources: []
|
||||
+ }), [preferences])
|
||||
|
||||
- const homeFeedParams = React.useMemo<FeedParams>(() => {
|
||||
- return {
|
||||
- mergeFeedEnabled: Boolean(preferences.feedViewPrefs.lab_mergeFeedEnabled),
|
||||
- mergeFeedSources: preferences.feedViewPrefs.lab_mergeFeedEnabled
|
||||
- ? preferences.savedFeeds
|
||||
- .filter(f => f.type === 'feed' || f.type === 'list')
|
||||
- .map(f => f.value)
|
||||
- : [],
|
||||
- }
|
||||
- }, [preferences])
|
||||
-
|
||||
- if (demoMode) {
|
||||
- return (
|
||||
- <Pager
|
||||
- ref={pagerRef}
|
||||
- testID="homeScreen"
|
||||
- onPageSelected={onPageSelected}
|
||||
- onPageScrollStateChanged={onPageScrollStateChanged}
|
||||
- renderTabBar={renderTabBar}
|
||||
- initialPage={selectedIndex}>
|
||||
- <FeedPage
|
||||
- testID="demoFeedPage"
|
||||
- isPageFocused
|
||||
- isPageAdjacent={false}
|
||||
- feed="demo"
|
||||
- renderEmptyState={renderCustomFeedEmptyState}
|
||||
- feedInfo={pinnedFeedInfos[0]}
|
||||
- />
|
||||
- <FeedPage
|
||||
- testID="customFeedPage"
|
||||
- isPageFocused
|
||||
- isPageAdjacent={false}
|
||||
- feed={`feedgen|${PROD_DEFAULT_FEED('whats-hot')}`}
|
||||
- renderEmptyState={renderCustomFeedEmptyState}
|
||||
- feedInfo={pinnedFeedInfos[0]}
|
||||
- />
|
||||
- </Pager>
|
||||
- )
|
||||
- }
|
||||
-
|
||||
- return hasSession ? (
|
||||
- <Pager
|
||||
- key={allFeeds.join(',')}
|
||||
- ref={pagerRef}
|
||||
- testID="homeScreen"
|
||||
- initialPage={selectedIndex}
|
||||
- onPageSelected={onPageSelected}
|
||||
- onPageScrollStateChanged={onPageScrollStateChanged}
|
||||
- renderTabBar={renderTabBar}>
|
||||
- {pinnedFeedInfos.length ? (
|
||||
- pinnedFeedInfos.map((feedInfo, index) => {
|
||||
+ return (
|
||||
+ <Pager ref={pagerRef} testID="homeScreen" initialPage={selectedIndex} onPageSelected={onPageSelected} onPageScrollStateChanged={onPageScrollStateChanged} renderTabBar={renderTabBar}>
|
||||
+ {pinnedFeedInfos.map((feedInfo, index) => {
|
||||
const feed = feedInfo.feedDescriptor
|
||||
if (feed === 'following') {
|
||||
- return (
|
||||
- <FeedPage
|
||||
- key={feed}
|
||||
- testID="followingFeedPage"
|
||||
- isPageFocused={maybeSelectedFeed === feed}
|
||||
- isPageAdjacent={Math.abs(selectedIndex - index) === 1}
|
||||
- feed={feed}
|
||||
- feedParams={homeFeedParams}
|
||||
- renderEmptyState={renderFollowingEmptyState}
|
||||
- renderEndOfFeed={FollowingEndOfFeed}
|
||||
- feedInfo={feedInfo}
|
||||
- />
|
||||
- )
|
||||
+ return <FeedPage key={feed} testID="followingFeedPage" isPageFocused={maybeSelectedFeed === feed} isPageAdjacent={Math.abs(selectedIndex - index) === 1} feed={feed} feedParams={homeFeedParams} renderEmptyState={renderFollowingEmptyState} renderEndOfFeed={FollowingEndOfFeed} feedInfo={feedInfo} />
|
||||
}
|
||||
- const savedFeedConfig = feedInfo.savedFeed
|
||||
- return (
|
||||
- <FeedPage
|
||||
- key={feed}
|
||||
- testID="customFeedPage"
|
||||
- isPageFocused={maybeSelectedFeed === feed}
|
||||
- isPageAdjacent={Math.abs(selectedIndex - index) === 1}
|
||||
- feed={feed}
|
||||
- renderEmptyState={renderCustomFeedEmptyState}
|
||||
- savedFeedConfig={savedFeedConfig}
|
||||
- feedInfo={feedInfo}
|
||||
- />
|
||||
- )
|
||||
- })
|
||||
- ) : (
|
||||
- <NoFeedsPinned preferences={preferences} />
|
||||
- )}
|
||||
- </Pager>
|
||||
- ) : (
|
||||
- <Pager
|
||||
- testID="homeScreen"
|
||||
- onPageSelected={onPageSelected}
|
||||
- onPageScrollStateChanged={onPageScrollStateChanged}
|
||||
- renderTabBar={renderTabBar}>
|
||||
- <FeedPage
|
||||
- testID="customFeedPage"
|
||||
- isPageFocused
|
||||
- isPageAdjacent={false}
|
||||
- feed={`feedgen|${PROD_DEFAULT_FEED('whats-hot')}`}
|
||||
- renderEmptyState={renderCustomFeedEmptyState}
|
||||
- feedInfo={pinnedFeedInfos[0]}
|
||||
- />
|
||||
+ return <FeedPage key={feed} testID="customFeedPage" isPageFocused={maybeSelectedFeed === feed} isPageAdjacent={Math.abs(selectedIndex - index) === 1} feed={feed} renderEmptyState={renderCustomFeedEmptyState} savedFeedConfig={feedInfo.savedFeed} feedInfo={feedInfo} />
|
||||
+ })}
|
||||
</Pager>
|
||||
)
|
||||
}
|
||||
-
|
||||
-const styles = StyleSheet.create({
|
||||
- loading: {
|
||||
- height: '100%',
|
||||
- alignContent: 'center',
|
||||
- justifyContent: 'center',
|
||||
- paddingBottom: 100,
|
||||
- },
|
||||
-})
|
||||
+const styles = StyleSheet.create({ loading: { height: '100%', alignContent: 'center', justifyContent: 'center', paddingBottom: 100 } })
|
||||
diff --git a/src/view/screens/PrivacyPolicy.tsx b/src/view/screens/PrivacyPolicy.tsx
|
||||
index a89eaadc4..1da393f03 100644
|
||||
--- a/src/view/screens/PrivacyPolicy.tsx
|
||||
+++ b/src/view/screens/PrivacyPolicy.tsx
|
||||
@@ -1,52 +1,13 @@
|
||||
@@ -1,52 +1,49 @@
|
||||
import React from 'react'
|
||||
-import {View} from 'react-native'
|
||||
-import {msg, Trans} from '@lingui/macro'
|
||||
-import {msg} from '@lingui/core/macro'
|
||||
-import {useLingui} from '@lingui/react'
|
||||
-import {Trans} from '@lingui/react/macro'
|
||||
-import {useFocusEffect} from '@react-navigation/native'
|
||||
-
|
||||
-import {usePalette} from '#/lib/hooks/usePalette'
|
||||
@@ -490,25 +54,28 @@ index a89eaadc4..1da393f03 100644
|
||||
-import {TextLink} from '#/view/com/util/Link'
|
||||
-import {Text} from '#/view/com/util/text/Text'
|
||||
-import {ScrollView} from '#/view/com/util/Views'
|
||||
+import { WebView } from 'react-native-webview'
|
||||
+import {ScrollView} from 'react-native'
|
||||
import * as Layout from '#/components/Layout'
|
||||
-import {ViewHeader} from '../com/util/ViewHeader'
|
||||
-
|
||||
+import {useSetTitle} from '#/lib/hooks/useSetTitle'
|
||||
+import {atoms as a, useTheme} from '#/alf'
|
||||
+import {Text} from '#/components/Typography'
|
||||
|
||||
-type Props = NativeStackScreenProps<CommonNavigatorParams, 'PrivacyPolicy'>
|
||||
-export const PrivacyPolicyScreen = (_props: Props) => {
|
||||
- const pal = usePalette('default')
|
||||
- const {_} = useLingui()
|
||||
- const setMinimalShellMode = useSetMinimalShellMode()
|
||||
-
|
||||
+export function PrivacyPolicyScreen() {
|
||||
+ useSetTitle('Privacy Policy')
|
||||
+ const t = useTheme()
|
||||
|
||||
- useFocusEffect(
|
||||
- React.useCallback(() => {
|
||||
- setMinimalShellMode(false)
|
||||
- }, [setMinimalShellMode]),
|
||||
- )
|
||||
+import {useSetTitle} from '#/lib/hooks/useSetTitle'
|
||||
|
||||
+export function PrivacyPolicyScreen() {
|
||||
+ useSetTitle('Privacy Policy')
|
||||
-
|
||||
return (
|
||||
<Layout.Screen>
|
||||
- <ViewHeader title={_(msg`Privacy Policy`)} />
|
||||
@@ -526,20 +93,50 @@ index a89eaadc4..1da393f03 100644
|
||||
- </Text>
|
||||
- </View>
|
||||
- <View style={s.footerSpacer} />
|
||||
- </ScrollView>
|
||||
+ <WebView source={{ uri: 'https://syu.is/about/support/privacy-policy' }} style={{ flex: 1 }} />
|
||||
+ <ScrollView
|
||||
+ style={[a.flex_1, {backgroundColor: t.palette.white}]}
|
||||
+ contentContainerStyle={[a.p_lg, a.pt_5xl, a.pb_5xl]}>
|
||||
+ <Text style={[a.text_2xl, a.font_bold, a.mb_lg]}>Privacy Policy</Text>
|
||||
+
|
||||
+ <Text style={[a.text_lg, a.font_bold, a.mt_lg, a.mb_md]}>Data Collection</Text>
|
||||
+ <Text style={[a.mb_md]}>
|
||||
+ syu.is collects minimal data necessary to provide the service. This includes your account information, posts, and interactions on the AT Protocol network.
|
||||
+ </Text>
|
||||
+
|
||||
+ <Text style={[a.text_lg, a.font_bold, a.mt_lg, a.mb_md]}>Data Storage</Text>
|
||||
+ <Text style={[a.mb_md]}>
|
||||
+ Your data is stored on the AT Protocol network. Posts and profile information are public by default as part of the decentralized social network.
|
||||
+ </Text>
|
||||
+
|
||||
+ <Text style={[a.text_lg, a.font_bold, a.mt_lg, a.mb_md]}>Third Parties</Text>
|
||||
+ <Text style={[a.mb_md]}>
|
||||
+ We do not sell your personal information to third parties. Your data may be visible to other users and services on the AT Protocol network.
|
||||
+ </Text>
|
||||
+
|
||||
+ <Text style={[a.text_lg, a.font_bold, a.mt_lg, a.mb_md]}>Contact</Text>
|
||||
+ <Text style={[a.mb_md]}>
|
||||
+ For privacy-related questions, please contact the administrator.
|
||||
+ </Text>
|
||||
+
|
||||
+ <Text style={[a.text_lg, a.font_bold, a.mt_xl, a.mb_md]}>日本語</Text>
|
||||
+ <Text style={[a.mb_md]}>
|
||||
+ syu.isはサービス提供に必要な最小限のデータのみを収集します。投稿やプロフィール情報はAT Protocolネットワーク上で公開されます。個人情報を第三者に販売することはありません。
|
||||
+ </Text>
|
||||
+
|
||||
+ <Text style={[a.text_sm, a.mt_xl, {color: t.palette.contrast_500}]}>
|
||||
+ Last updated: 2026
|
||||
+ </Text>
|
||||
</ScrollView>
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
diff --git a/src/view/screens/TermsOfService.tsx b/src/view/screens/TermsOfService.tsx
|
||||
index d843c713c..b81767bd5 100644
|
||||
--- a/src/view/screens/TermsOfService.tsx
|
||||
+++ b/src/view/screens/TermsOfService.tsx
|
||||
@@ -1,50 +1,13 @@
|
||||
@@ -1,50 +1,49 @@
|
||||
import React from 'react'
|
||||
-import {View} from 'react-native'
|
||||
-import {msg, Trans} from '@lingui/macro'
|
||||
-import {msg} from '@lingui/core/macro'
|
||||
-import {useLingui} from '@lingui/react'
|
||||
-import {Trans} from '@lingui/react/macro'
|
||||
-import {useFocusEffect} from '@react-navigation/native'
|
||||
-
|
||||
-import {usePalette} from '#/lib/hooks/usePalette'
|
||||
@@ -552,25 +149,28 @@ index d843c713c..b81767bd5 100644
|
||||
-import {TextLink} from '#/view/com/util/Link'
|
||||
-import {Text} from '#/view/com/util/text/Text'
|
||||
-import {ScrollView} from '#/view/com/util/Views'
|
||||
+import { WebView } from 'react-native-webview'
|
||||
+import {ScrollView} from 'react-native'
|
||||
import * as Layout from '#/components/Layout'
|
||||
-import {ViewHeader} from '../com/util/ViewHeader'
|
||||
-
|
||||
+import {useSetTitle} from '#/lib/hooks/useSetTitle'
|
||||
+import {atoms as a, useTheme} from '#/alf'
|
||||
+import {Text} from '#/components/Typography'
|
||||
|
||||
-type Props = NativeStackScreenProps<CommonNavigatorParams, 'TermsOfService'>
|
||||
-export const TermsOfServiceScreen = (_props: Props) => {
|
||||
- const pal = usePalette('default')
|
||||
- const setMinimalShellMode = useSetMinimalShellMode()
|
||||
- const {_} = useLingui()
|
||||
-
|
||||
+export function TermsOfServiceScreen() {
|
||||
+ useSetTitle('Terms of Service')
|
||||
+ const t = useTheme()
|
||||
|
||||
- useFocusEffect(
|
||||
- React.useCallback(() => {
|
||||
- setMinimalShellMode(false)
|
||||
- }, [setMinimalShellMode]),
|
||||
- )
|
||||
+import {useSetTitle} from '#/lib/hooks/useSetTitle'
|
||||
|
||||
+export function TermsOfServiceScreen() {
|
||||
+ useSetTitle('Terms of Service')
|
||||
-
|
||||
return (
|
||||
<Layout.Screen>
|
||||
- <ViewHeader title={_(msg`Terms of Service`)} />
|
||||
@@ -586,8 +186,39 @@ index d843c713c..b81767bd5 100644
|
||||
- </Text>
|
||||
- </View>
|
||||
- <View style={s.footerSpacer} />
|
||||
- </ScrollView>
|
||||
+ <WebView source={{ uri: 'https://syu.is/about/support/tos' }} style={{ flex: 1 }} />
|
||||
+ <ScrollView
|
||||
+ style={[a.flex_1, {backgroundColor: t.palette.white}]}
|
||||
+ contentContainerStyle={[a.p_lg, a.pt_5xl, a.pb_5xl]}>
|
||||
+ <Text style={[a.text_2xl, a.font_bold, a.mb_lg]}>Terms of Service</Text>
|
||||
+
|
||||
+ <Text style={[a.text_lg, a.font_bold, a.mt_lg, a.mb_md]}>Acceptance</Text>
|
||||
+ <Text style={[a.mb_md]}>
|
||||
+ By using syu.is, you agree to these terms. If you do not agree, please do not use the service.
|
||||
+ </Text>
|
||||
+
|
||||
+ <Text style={[a.text_lg, a.font_bold, a.mt_lg, a.mb_md]}>Prohibited Content</Text>
|
||||
+ <Text style={[a.mb_md]}>
|
||||
+ Do not post illegal content, spam, or harass others. Do not impersonate others or spread misinformation.
|
||||
+ </Text>
|
||||
+
|
||||
+ <Text style={[a.text_lg, a.font_bold, a.mt_lg, a.mb_md]}>Account Termination</Text>
|
||||
+ <Text style={[a.mb_md]}>
|
||||
+ The administrator reserves the right to suspend or terminate accounts that violate these terms.
|
||||
+ </Text>
|
||||
+
|
||||
+ <Text style={[a.text_lg, a.font_bold, a.mt_lg, a.mb_md]}>Disclaimer</Text>
|
||||
+ <Text style={[a.mb_md]}>
|
||||
+ This service is provided "as is" without warranty of any kind.
|
||||
+ </Text>
|
||||
+
|
||||
+ <Text style={[a.text_lg, a.font_bold, a.mt_xl, a.mb_md]}>日本語</Text>
|
||||
+ <Text style={[a.mb_md]}>
|
||||
+ syu.isを利用することで、これらの利用規約に同意したものとみなします。違法なコンテンツの投稿、スパム、他者への嫌がらせは禁止です。管理者は規約違反のアカウントを停止する権利を有します。本サービスは現状のまま提供され、いかなる保証もありません。
|
||||
+ </Text>
|
||||
+
|
||||
+ <Text style={[a.text_sm, a.mt_xl, {color: t.palette.contrast_500}]}>
|
||||
+ Last updated: 2026
|
||||
+ </Text>
|
||||
</ScrollView>
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx
|
||||
index f76147ccf..36b4d7de1 100644
|
||||
index 42a5fe417..8e7963512 100644
|
||||
--- a/src/view/shell/Drawer.tsx
|
||||
+++ b/src/view/shell/Drawer.tsx
|
||||
@@ -292,17 +292,11 @@ let DrawerContent = ({}: React.PropsWithoutRef<{}>): React.ReactNode => {
|
||||
@@ -294,17 +294,11 @@ let DrawerContent = ({}: React.PropsWithoutRef<{}>): React.ReactNode => {
|
||||
<>
|
||||
<SearchMenuItem isActive={isAtSearch} onPress={onPressSearch} />
|
||||
<HomeMenuItem isActive={isAtHome} onPress={onPressHome} />
|
||||
@@ -20,7 +20,7 @@ index f76147ccf..36b4d7de1 100644
|
||||
<ProfileMenuItem
|
||||
isActive={isAtMyProfile}
|
||||
onPress={onPressProfile}
|
||||
@@ -357,17 +351,7 @@ let DrawerFooter = ({
|
||||
@@ -359,17 +353,7 @@ let DrawerFooter = ({
|
||||
),
|
||||
},
|
||||
]}>
|
||||
@@ -39,18 +39,27 @@ index f76147ccf..36b4d7de1 100644
|
||||
<Button
|
||||
label={_(msg`Get help`)}
|
||||
size="small"
|
||||
@@ -695,12 +679,12 @@ function ExtraLinks() {
|
||||
@@ -697,15 +681,21 @@ function ExtraLinks() {
|
||||
<InlineLinkText
|
||||
style={[a.text_md]}
|
||||
label={_(msg`Terms of Service`)}
|
||||
- to="https://bsky.social/about/support/tos">
|
||||
+ to="https://syu.is/about/support/tos">
|
||||
+ to="/support/tos">
|
||||
<Trans>Terms of Service</Trans>
|
||||
</InlineLinkText>
|
||||
<InlineLinkText
|
||||
style={[a.text_md]}
|
||||
- to="https://bsky.social/about/support/privacy-policy"
|
||||
+ to="https://syu.is/about/support/privacy-policy"
|
||||
+ to="/support/privacy-policy"
|
||||
label={_(msg`Privacy Policy`)}>
|
||||
<Trans>Privacy Policy</Trans>
|
||||
</InlineLinkText>
|
||||
+ <InlineLinkText
|
||||
+ style={[a.text_md]}
|
||||
+ to="/support/license"
|
||||
+ label="License">
|
||||
+ License
|
||||
+ </InlineLinkText>
|
||||
{kawaii && (
|
||||
<Text style={t.atoms.text_contrast_medium}>
|
||||
<Trans>
|
||||
|
||||
@@ -20,13 +20,13 @@ index 8365057e8..59c8506a2 100644
|
||||
terms: {
|
||||
overridePresentation: false,
|
||||
- to: `https://bsky.social/about/support/tos`,
|
||||
+ to: `https://syu.is/about/support/tos`,
|
||||
+ to: `/support/tos`,
|
||||
label: _(msg`Terms of Service`),
|
||||
},
|
||||
privacy: {
|
||||
overridePresentation: false,
|
||||
- to: `https://bsky.social/about/support/privacy-policy`,
|
||||
+ to: `https://syu.is/about/support/privacy-policy`,
|
||||
+ to: `/support/privacy-policy`,
|
||||
label: _(msg`Privacy Policy`),
|
||||
},
|
||||
copyright: {
|
||||
|
||||
@@ -1,60 +1,11 @@
|
||||
diff --git a/src/view/com/auth/SplashScreen.tsx b/src/view/com/auth/SplashScreen.tsx
|
||||
index 3442d1bdf..8ed9e3d0d 100644
|
||||
--- a/src/view/com/auth/SplashScreen.tsx
|
||||
+++ b/src/view/com/auth/SplashScreen.tsx
|
||||
@@ -1,4 +1,5 @@
|
||||
import {View} from 'react-native'
|
||||
+import {Pressable, Linking} from 'react-native'
|
||||
import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
@@ -40,16 +41,6 @@ export const SplashScreen = ({
|
||||
<View style={[a.pb_sm, a.pt_5xl]}>
|
||||
<Logotype width={161} fill={t.atoms.text.color} />
|
||||
</View>
|
||||
-
|
||||
- <Text
|
||||
- style={[
|
||||
- a.text_md,
|
||||
- a.font_semi_bold,
|
||||
- t.atoms.text_contrast_medium,
|
||||
- a.text_center,
|
||||
- ]}>
|
||||
- <Trans>What's up?</Trans>
|
||||
- </Text>
|
||||
</View>
|
||||
|
||||
<View
|
||||
@@ -102,6 +93,21 @@ export const SplashScreen = ({
|
||||
<AppLanguageDropdown />
|
||||
</View>
|
||||
</View>
|
||||
+ <View style={[a.pb_sm, a.justify_center, a.align_center]}>
|
||||
+ <Pressable onPress={() => Linking.openURL('https://syu.is/about/support/license')}>
|
||||
+ <Text
|
||||
+ style={[
|
||||
+ a.text_xs,
|
||||
+ t.atoms.text_contrast_low,
|
||||
+ {textDecorationLine: 'underline'},
|
||||
+ ]}>
|
||||
+ License
|
||||
+ </Text>
|
||||
+ </Pressable>
|
||||
+ </View>
|
||||
+ <View style={[a.pb_xl, a.justify_center, a.align_center]}>
|
||||
+ <Text style={[a.text_xs, t.atoms.text_contrast_low]}>© syui</Text>
|
||||
+ </View>
|
||||
<View style={{height: insets.bottom}} />
|
||||
</ErrorBoundary>
|
||||
</Animated.View>
|
||||
diff --git a/src/view/com/auth/SplashScreen.web.tsx b/src/view/com/auth/SplashScreen.web.tsx
|
||||
index 22dd23d7f..7ceb3800e 100644
|
||||
index d9185d778..504f521a4 100644
|
||||
--- a/src/view/com/auth/SplashScreen.web.tsx
|
||||
+++ b/src/view/com/auth/SplashScreen.web.tsx
|
||||
@@ -94,14 +94,6 @@ export const SplashScreen = ({
|
||||
</View>
|
||||
)}
|
||||
|
||||
|
||||
- <Text
|
||||
- style={[
|
||||
- a.text_md,
|
||||
@@ -64,5 +15,5 @@ index 22dd23d7f..7ceb3800e 100644
|
||||
- <Trans>What's up?</Trans>
|
||||
- </Text>
|
||||
</View>
|
||||
|
||||
|
||||
<View
|
||||
|
||||
@@ -2,7 +2,7 @@ diff --git a/src/screens/Settings/Settings.tsx b/src/screens/Settings/Settings.t
|
||||
index 2fa5aa7de..3faf6a7b3 100644
|
||||
--- a/src/screens/Settings/Settings.tsx
|
||||
+++ b/src/screens/Settings/Settings.tsx
|
||||
@@ -205,26 +205,8 @@ export function SettingsScreen({}: Props) {
|
||||
@@ -203,26 +203,8 @@ export function SettingsScreen({}: Props) {
|
||||
<Trans>Notifications</Trans>
|
||||
</SettingsList.ItemText>
|
||||
</SettingsList.LinkItem>
|
||||
@@ -14,9 +14,9 @@ index 2fa5aa7de..3faf6a7b3 100644
|
||||
- <Trans>Content and media</Trans>
|
||||
- </SettingsList.ItemText>
|
||||
- </SettingsList.LinkItem>
|
||||
- {isNative &&
|
||||
- {IS_NATIVE &&
|
||||
- findContactsEnabled &&
|
||||
- !gate('disable_settings_find_contacts') && (
|
||||
- !ax.features.enabled(ax.features.ImportContactsSettingsDisable) && (
|
||||
- <SettingsList.LinkItem
|
||||
- to="/settings/find-contacts"
|
||||
- label={_(msg`Find friends from contacts`)}>
|
||||
@@ -31,7 +31,7 @@ index 2fa5aa7de..3faf6a7b3 100644
|
||||
<SettingsList.LinkItem
|
||||
to="/settings/appearance"
|
||||
label={_(msg`Appearance`)}>
|
||||
@@ -249,16 +231,6 @@ export function SettingsScreen({}: Props) {
|
||||
@@ -247,16 +229,6 @@ export function SettingsScreen({}: Props) {
|
||||
<Trans>Languages</Trans>
|
||||
</SettingsList.ItemText>
|
||||
</SettingsList.LinkItem>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
diff --git a/src/view/com/posts/DiscoverFallbackHeader.tsx b/src/view/com/posts/DiscoverFallbackHeader.tsx
|
||||
index e35a33aaf..a36f84ae0 100644
|
||||
--- a/src/view/com/posts/DiscoverFallbackHeader.tsx
|
||||
+++ b/src/view/com/posts/DiscoverFallbackHeader.tsx
|
||||
@@ -7,37 +7,5 @@ import {TextLink} from '../util/Link'
|
||||
@@ -7,37 +7,5 @@
|
||||
import {Text} from '../util/text/Text'
|
||||
|
||||
export function DiscoverFallbackHeader() {
|
||||
@@ -41,25 +39,23 @@ index e35a33aaf..a36f84ae0 100644
|
||||
- )
|
||||
+ return null
|
||||
}
|
||||
diff --git a/src/view/com/posts/FollowingEmptyState.tsx b/src/view/com/posts/FollowingEmptyState.tsx
|
||||
index 352cc1dc0..f477521af 100644
|
||||
--- a/src/view/com/posts/FollowingEmptyState.tsx
|
||||
+++ b/src/view/com/posts/FollowingEmptyState.tsx
|
||||
@@ -1,37 +1,14 @@
|
||||
@@ -1,38 +1,15 @@
|
||||
import React from 'react'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
-import {
|
||||
- FontAwesomeIcon,
|
||||
- type FontAwesomeIconStyle,
|
||||
-} from '@fortawesome/react-native-fontawesome'
|
||||
import {Trans} from '@lingui/macro'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
-import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {MagnifyingGlassIcon} from '#/lib/icons'
|
||||
-import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {s} from '#/lib/styles'
|
||||
-import {isWeb} from '#/platform/detection'
|
||||
-import {IS_WEB} from '#/env'
|
||||
-import {Button} from '../util/forms/Button'
|
||||
import {Text} from '../util/text/Text'
|
||||
|
||||
@@ -67,9 +63,9 @@ index 352cc1dc0..f477521af 100644
|
||||
const pal = usePalette('default')
|
||||
- const palInverted = usePalette('inverted')
|
||||
- const navigation = useNavigation<NavigationProp>()
|
||||
-
|
||||
|
||||
- const onPressFindAccounts = React.useCallback(() => {
|
||||
- if (isWeb) {
|
||||
- if (IS_WEB) {
|
||||
- navigation.navigate('Search', {})
|
||||
- } else {
|
||||
- navigation.navigate('SearchTab')
|
||||
@@ -80,10 +76,11 @@ index 352cc1dc0..f477521af 100644
|
||||
- const onPressDiscoverFeeds = React.useCallback(() => {
|
||||
- navigation.navigate('Feeds')
|
||||
- }, [navigation])
|
||||
|
||||
-
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
@@ -45,36 +22,6 @@ export function FollowingEmptyState() {
|
||||
<View style={styles.inner}>
|
||||
@@ -45,36 +22,6 @@
|
||||
happening.
|
||||
</Trans>
|
||||
</Text>
|
||||
@@ -120,7 +117,7 @@ index 352cc1dc0..f477521af 100644
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
@@ -98,13 +45,4 @@ const styles = StyleSheet.create({
|
||||
@@ -98,13 +45,4 @@
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
},
|
||||
@@ -134,24 +131,22 @@ index 352cc1dc0..f477521af 100644
|
||||
- borderRadius: 30,
|
||||
- },
|
||||
})
|
||||
diff --git a/src/view/com/posts/FollowingEndOfFeed.tsx b/src/view/com/posts/FollowingEndOfFeed.tsx
|
||||
index e3c84d782..efb55d406 100644
|
||||
--- a/src/view/com/posts/FollowingEndOfFeed.tsx
|
||||
+++ b/src/view/com/posts/FollowingEndOfFeed.tsx
|
||||
@@ -1,36 +1,13 @@
|
||||
@@ -1,37 +1,14 @@
|
||||
import React from 'react'
|
||||
import {Dimensions, StyleSheet, View} from 'react-native'
|
||||
-import {
|
||||
- FontAwesomeIcon,
|
||||
- type FontAwesomeIconStyle,
|
||||
-} from '@fortawesome/react-native-fontawesome'
|
||||
import {Trans} from '@lingui/macro'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
-import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
-import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {s} from '#/lib/styles'
|
||||
-import {isWeb} from '#/platform/detection'
|
||||
-import {IS_WEB} from '#/env'
|
||||
-import {Button} from '../util/forms/Button'
|
||||
import {Text} from '../util/text/Text'
|
||||
|
||||
@@ -159,9 +154,9 @@ index e3c84d782..efb55d406 100644
|
||||
const pal = usePalette('default')
|
||||
- const palInverted = usePalette('inverted')
|
||||
- const navigation = useNavigation<NavigationProp>()
|
||||
-
|
||||
|
||||
- const onPressFindAccounts = React.useCallback(() => {
|
||||
- if (isWeb) {
|
||||
- if (IS_WEB) {
|
||||
- navigation.navigate('Search', {})
|
||||
- } else {
|
||||
- navigation.navigate('SearchTab')
|
||||
@@ -172,10 +167,11 @@ index e3c84d782..efb55d406 100644
|
||||
- const onPressDiscoverFeeds = React.useCallback(() => {
|
||||
- navigation.navigate('Feeds')
|
||||
- }, [navigation])
|
||||
|
||||
-
|
||||
return (
|
||||
<View
|
||||
@@ -41,41 +18,8 @@ export function FollowingEndOfFeed() {
|
||||
style={[
|
||||
@@ -41,41 +18,8 @@
|
||||
]}>
|
||||
<View style={styles.inner}>
|
||||
<Text type="xl-medium" style={[s.textCenter, pal.text]}>
|
||||
@@ -183,7 +179,8 @@ index e3c84d782..efb55d406 100644
|
||||
- You've reached the end of your feed! Find some more accounts to
|
||||
- follow.
|
||||
- </Trans>
|
||||
- </Text>
|
||||
+ <Trans>You've reached the end of your feed!</Trans>
|
||||
</Text>
|
||||
- <Button
|
||||
- type="inverted"
|
||||
- style={styles.emptyBtn}
|
||||
@@ -200,8 +197,7 @@ index e3c84d782..efb55d406 100644
|
||||
-
|
||||
- <Text type="xl-medium" style={[s.textCenter, pal.text, s.mt20]}>
|
||||
- <Trans>You can also discover new Custom Feeds to follow.</Trans>
|
||||
+ <Trans>You've reached the end of your feed!</Trans>
|
||||
</Text>
|
||||
- </Text>
|
||||
- <Button
|
||||
- type="inverted"
|
||||
- style={[styles.emptyBtn, s.mt10]}
|
||||
@@ -218,7 +214,7 @@ index e3c84d782..efb55d406 100644
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
@@ -93,13 +37,4 @@ const styles = StyleSheet.create({
|
||||
@@ -93,13 +37,4 @@
|
||||
width: '100%',
|
||||
maxWidth: 460,
|
||||
},
|
||||
@@ -232,11 +228,9 @@ index e3c84d782..efb55d406 100644
|
||||
- borderRadius: 30,
|
||||
- },
|
||||
})
|
||||
diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx
|
||||
index 4f25468c9..a72a10b80 100644
|
||||
--- a/src/view/com/posts/PostFeed.tsx
|
||||
+++ b/src/view/com/posts/PostFeed.tsx
|
||||
@@ -766,7 +766,7 @@ let PostFeed = ({
|
||||
@@ -765,7 +765,7 @@
|
||||
} else if (row.type === 'feedShutdownMsg') {
|
||||
return <FeedShutdownMsg feedUri={feedUriOrActorDid} />
|
||||
} else if (row.type === 'interstitialFollows') {
|
||||
|
||||
@@ -1,51 +1,17 @@
|
||||
diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go
|
||||
index 790f211ee..ec05a8bcd 100644
|
||||
--- a/bskyweb/cmd/bskyweb/server.go
|
||||
+++ b/bskyweb/cmd/bskyweb/server.go
|
||||
@@ -317,6 +317,12 @@ func serve(cctx *cli.Context) error {
|
||||
@@ -317,6 +317,14 @@ func serve(cctx *cli.Context) error {
|
||||
e.GET("/support/tos", server.WebGeneric)
|
||||
e.GET("/support/community-guidelines", server.WebGeneric)
|
||||
e.GET("/support/copyright", server.WebGeneric)
|
||||
+ // about/support pages (syu.is specific)
|
||||
+ e.GET("/about/support/tos", server.WebAboutTOS)
|
||||
+ e.GET("/about/support/privacy-policy", server.WebAboutPrivacy)
|
||||
+ e.GET("/about/support/help", server.WebAboutHelp)
|
||||
+ e.GET("/about/support/license", server.WebAboutLicense)
|
||||
+ e.GET("/about/support/app", server.WebAboutApp)
|
||||
+ e.GET("/support/privacy-policy", server.WebGeneric)
|
||||
+ e.GET("/support/license", server.WebGeneric)
|
||||
+ e.GET("/support/app", server.WebGeneric)
|
||||
+ e.GET("/support/help", server.WebGeneric)
|
||||
+ // /about/support/ paths (for web compatibility)
|
||||
+ e.GET("/about/support/tos", server.WebGeneric)
|
||||
+ e.GET("/about/support/privacy-policy", server.WebGeneric)
|
||||
+ e.GET("/about/support/license", server.WebGeneric)
|
||||
e.GET("/intent/compose", server.WebGeneric)
|
||||
e.GET("/intent/verify-email", server.WebGeneric)
|
||||
e.GET("/intent/age-assurance", server.WebGeneric)
|
||||
@@ -825,3 +831,33 @@ func (srv *Server) serveSitemapRequest(c echo.Context, url, sitemapType string)
|
||||
|
||||
return nil
|
||||
}
|
||||
+
|
||||
+// Handler for About TOS page (syu.is specific)
|
||||
+func (srv *Server) WebAboutTOS(c echo.Context) error {
|
||||
+ data := srv.NewTemplateContext()
|
||||
+ return c.Render(http.StatusOK, "about-tos.html", data)
|
||||
+}
|
||||
+
|
||||
+// Handler for About Privacy Policy page (syu.is specific)
|
||||
+func (srv *Server) WebAboutPrivacy(c echo.Context) error {
|
||||
+ data := srv.NewTemplateContext()
|
||||
+ return c.Render(http.StatusOK, "about-privacy.html", data)
|
||||
+}
|
||||
+
|
||||
+// Handler for About Help page (syu.is specific)
|
||||
+func (srv *Server) WebAboutHelp(c echo.Context) error {
|
||||
+ data := srv.NewTemplateContext()
|
||||
+ return c.Render(http.StatusOK, "about-help.html", data)
|
||||
+}
|
||||
+
|
||||
+// Handler for About License page (syu.is specific)
|
||||
+func (srv *Server) WebAboutLicense(c echo.Context) error {
|
||||
+ data := srv.NewTemplateContext()
|
||||
+ return c.Render(http.StatusOK, "about-license.html", data)
|
||||
+}
|
||||
+
|
||||
+// Handler for About App page (syu.is specific)
|
||||
+func (srv *Server) WebAboutApp(c echo.Context) error {
|
||||
+ data := srv.NewTemplateContext()
|
||||
+ return c.Render(http.StatusOK, "about-app.html", data)
|
||||
+}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
--- a/src/view/shell/bottom-bar/BottomBar.tsx
|
||||
+++ b/src/view/shell/bottom-bar/BottomBar.tsx
|
||||
@@ -198,38 +198,40 @@
|
||||
@@ -196,38 +196,40 @@
|
||||
accessibilityLabel={_(msg`Search`)}
|
||||
accessibilityHint=""
|
||||
/>
|
||||
@@ -10,12 +10,12 @@
|
||||
- isAtMessages ? (
|
||||
- <MessageFilled
|
||||
- width={iconWidth - 1}
|
||||
- style={[styles.ctrlIcon, pal.text, styles.feedsIcon]}
|
||||
- style={[styles.ctrlIcon, t.atoms.text, styles.feedsIcon]}
|
||||
- />
|
||||
- ) : (
|
||||
- <Message
|
||||
- width={iconWidth - 1}
|
||||
- style={[styles.ctrlIcon, pal.text, styles.feedsIcon]}
|
||||
- style={[styles.ctrlIcon, t.atoms.text, styles.feedsIcon]}
|
||||
- />
|
||||
- )
|
||||
- }
|
||||
@@ -28,10 +28,10 @@
|
||||
- accessibilityHint={
|
||||
- numUnreadMessages.count > 0
|
||||
- ? _(
|
||||
- msg`${plural(numUnreadMessages.numUnread ?? 0, {
|
||||
- plural(numUnreadMessages.numUnread ?? 0, {
|
||||
- one: '# unread item',
|
||||
- other: '# unread items',
|
||||
- })}` || '',
|
||||
- }),
|
||||
- )
|
||||
- : ''
|
||||
- }
|
||||
@@ -43,12 +43,12 @@
|
||||
+ isAtMessages ? (
|
||||
+ <MessageFilled
|
||||
+ width={iconWidth - 1}
|
||||
+ style={[styles.ctrlIcon, pal.text, styles.feedsIcon]}
|
||||
+ style={[styles.ctrlIcon, t.atoms.text, styles.feedsIcon]}
|
||||
+ />
|
||||
+ ) : (
|
||||
+ <Message
|
||||
+ width={iconWidth - 1}
|
||||
+ style={[styles.ctrlIcon, pal.text, styles.feedsIcon]}
|
||||
+ style={[styles.ctrlIcon, t.atoms.text, styles.feedsIcon]}
|
||||
+ />
|
||||
+ )
|
||||
+ }
|
||||
@@ -61,10 +61,10 @@
|
||||
+ accessibilityHint={
|
||||
+ numUnreadMessages.count > 0
|
||||
+ ? _(
|
||||
+ msg`${plural(numUnreadMessages.numUnread ?? 0, {
|
||||
+ plural(numUnreadMessages.numUnread ?? 0, {
|
||||
+ one: '# unread item',
|
||||
+ other: '# unread items',
|
||||
+ })}` || '',
|
||||
+ }),
|
||||
+ )
|
||||
+ : ''
|
||||
+ }
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
diff --git a/src/env/common.ts b/src/env/common.ts
|
||||
index 04e98c49c..a4ee47932 100644
|
||||
--- a/src/env/common.ts
|
||||
+++ b/src/env/common.ts
|
||||
@@ -107,19 +107,17 @@ export const GCP_PROJECT_ID: number =
|
||||
@@ -88,7 +88,7 @@
|
||||
* Metrics API host
|
||||
*/
|
||||
export const METRICS_API_HOST: string =
|
||||
- process.env.EXPO_PUBLIC_METRICS_API_HOST || 'https://events.bsky.app'
|
||||
+ process.env.EXPO_PUBLIC_METRICS_API_HOST || ''
|
||||
|
||||
/**
|
||||
* URLs for the app config web worker. Can be a
|
||||
* locally running server, see `env.example` for more.
|
||||
+ * Disabled for self-hosted environment to avoid CORS errors
|
||||
* Growthbook API host
|
||||
@@ -128,9 +128,7 @@
|
||||
*/
|
||||
export const GEOLOCATION_DEV_URL = process.env.GEOLOCATION_DEV_URL
|
||||
export const GEOLOCATION_PROD_URL = `https://ip.bsky.app`
|
||||
@@ -17,8 +20,7 @@ index 04e98c49c..a4ee47932 100644
|
||||
|
||||
/**
|
||||
* URLs for the live-event config web worker. Can be a
|
||||
* locally running server, see `env.example` for more.
|
||||
+ * Disabled for self-hosted environment
|
||||
@@ -138,9 +136,7 @@
|
||||
*/
|
||||
export const LIVE_EVENTS_DEV_URL = process.env.LIVE_EVENTS_DEV_URL
|
||||
export const LIVE_EVENTS_PROD_URL = `https://live-events.workers.bsky.app`
|
||||
@@ -26,3 +28,14 @@ index 04e98c49c..a4ee47932 100644
|
||||
- ? (LIVE_EVENTS_DEV_URL ?? LIVE_EVENTS_PROD_URL)
|
||||
- : LIVE_EVENTS_PROD_URL
|
||||
+export const LIVE_EVENTS_URL = null
|
||||
|
||||
/**
|
||||
* URLs for the app-config web worker. Can be a
|
||||
@@ -148,6 +144,4 @@
|
||||
*/
|
||||
export const APP_CONFIG_DEV_URL = process.env.APP_CONFIG_DEV_URL
|
||||
export const APP_CONFIG_PROD_URL = `https://app-config.workers.bsky.app`
|
||||
-export const APP_CONFIG_URL = IS_DEV
|
||||
- ? (APP_CONFIG_DEV_URL ?? APP_CONFIG_PROD_URL)
|
||||
- : APP_CONFIG_PROD_URL
|
||||
+export const APP_CONFIG_URL = null
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
--- a/src/screens/Signup/StepInfo/index.tsx
|
||||
+++ b/src/screens/Signup/StepInfo/index.tsx
|
||||
@@ -9,44 +9,17 @@
|
||||
@@ -8,46 +8,18 @@
|
||||
import {isEmailMaybeInvalid} from '#/lib/strings/email'
|
||||
import {logger} from '#/logger'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {useSignupContext} from '#/screens/Signup/state'
|
||||
-import {Policies} from '#/screens/Signup/StepInfo/Policies'
|
||||
import {atoms as a, native} from '#/alf'
|
||||
@@ -26,6 +26,8 @@
|
||||
- MIN_ACCESS_AGE,
|
||||
- useAgeAssuranceRegionConfigWithFallback,
|
||||
-} from '#/ageAssurance/util'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
-import {IS_NATIVE} from '#/env'
|
||||
-import {
|
||||
- useDeviceGeolocationApi,
|
||||
- useIsDeviceGeolocationGranted,
|
||||
@@ -45,12 +47,12 @@
|
||||
export function StepInfo({
|
||||
onPressBack,
|
||||
isServerError,
|
||||
@@ -70,21 +43,6 @@
|
||||
@@ -72,22 +44,7 @@
|
||||
|
||||
const emailInputRef = useRef<TextInput>(null)
|
||||
const passwordInputRef = useRef<TextInput>(null)
|
||||
- const birthdateInputRef = useRef<DateFieldRef>(null)
|
||||
-
|
||||
|
||||
- const aaRegionConfig = useAgeAssuranceRegionConfigWithFallback()
|
||||
- const {setDeviceGeolocation} = useDeviceGeolocationApi()
|
||||
- const locationControl = Dialog.useDialogControl()
|
||||
@@ -64,10 +66,11 @@
|
||||
- ? !isUnderAge(state.dateOfBirth.toISOString(), 18)
|
||||
- : true
|
||||
- const isDeviceGeolocationGranted = useIsDeviceGeolocationGranted()
|
||||
|
||||
-
|
||||
const [hasWarnedEmail, setHasWarnedEmail] = React.useState<boolean>(false)
|
||||
|
||||
@@ -105,10 +63,6 @@
|
||||
const tldtsRef = React.useRef<typeof tldts>(undefined)
|
||||
@@ -107,10 +64,6 @@
|
||||
const emailChanged = prevEmailValueRef.current !== email
|
||||
const password = passwordValueRef.current
|
||||
|
||||
@@ -78,7 +81,7 @@
|
||||
if (state.serviceDescription?.inviteCodeRequired && !inviteCode) {
|
||||
return dispatch({
|
||||
type: 'setError',
|
||||
@@ -275,107 +229,16 @@
|
||||
@@ -273,107 +226,16 @@
|
||||
secureTextEntry
|
||||
autoComplete="new-password"
|
||||
autoCapitalize="none"
|
||||
@@ -91,7 +94,7 @@
|
||||
passwordRules="minlength: 8;"
|
||||
/>
|
||||
</TextField.Root>
|
||||
</View>
|
||||
- </View>
|
||||
- <View>
|
||||
- <DateField.LabelText>
|
||||
- <Trans>Your birth date</Trans>
|
||||
@@ -110,7 +113,7 @@
|
||||
- accessibilityHint={_(msg`Select your date of birth`)}
|
||||
- maximumDate={new Date()}
|
||||
- />
|
||||
- </View>
|
||||
</View>
|
||||
-
|
||||
- <View style={[a.gap_sm]}>
|
||||
- <Policies serviceDescription={state.serviceDescription} />
|
||||
@@ -122,18 +125,18 @@
|
||||
- <Admonition.Content>
|
||||
- <Admonition.Text>
|
||||
- {!isOverAppMinAccessAge ? (
|
||||
- <Trans>
|
||||
- You must be {MIN_ACCESS_AGE} years of age or older
|
||||
- to create an account.
|
||||
- </Trans>
|
||||
- <Plural
|
||||
- value={MIN_ACCESS_AGE}
|
||||
- other="You must be # years of age or older to create an account."
|
||||
- />
|
||||
- ) : (
|
||||
- <Trans>
|
||||
- You must be {aaRegionConfig.minAccessAge} years of
|
||||
- age or older to create an account in your region.
|
||||
- </Trans>
|
||||
- <Plural
|
||||
- value={aaRegionConfig.minAccessAge}
|
||||
- other="You must be # years of age or older to create an account in your region."
|
||||
- />
|
||||
- )}
|
||||
- </Admonition.Text>
|
||||
- {isNative &&
|
||||
- {IS_NATIVE &&
|
||||
- !isDeviceGeolocationGranted &&
|
||||
- isOverAppMinAccessAge && (
|
||||
- <Admonition.Text>
|
||||
@@ -165,7 +168,7 @@
|
||||
- ) : undefined}
|
||||
- </View>
|
||||
-
|
||||
- {isNative && (
|
||||
- {IS_NATIVE && (
|
||||
- <DeviceLocationRequestDialog
|
||||
- control={locationControl}
|
||||
- onLocationAcquired={props => {
|
||||
|
||||
132
ios/patching/038-social-app-ios-profile-services.patch
Normal file
@@ -0,0 +1,132 @@
|
||||
--- a/src/screens/Profile/Header/ProfileHeaderStandard.tsx
|
||||
+++ b/src/screens/Profile/Header/ProfileHeaderStandard.tsx
|
||||
@@ -1,5 +1,5 @@
|
||||
import {memo, useMemo, useState} from 'react'
|
||||
-import {View} from 'react-native'
|
||||
+import {Image, Pressable, View} from 'react-native'
|
||||
import {
|
||||
type AppBskyActorDefs,
|
||||
moderateProfile,
|
||||
@@ -11,7 +11,10 @@
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
|
||||
+import {useQuery} from '@tanstack/react-query'
|
||||
+
|
||||
import {useHaptics} from '#/lib/haptics'
|
||||
+import {useOpenLink} from '#/lib/hooks/useOpenLink'
|
||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {logger} from '#/logger'
|
||||
@@ -47,6 +50,103 @@
|
||||
import {ProfileHeaderShell} from './Shell'
|
||||
import {ProfileHeaderSuggestedFollows} from './SuggestedFollows'
|
||||
|
||||
+const SERVICE_FAVICONS: Record<string, any> = {
|
||||
+ 'syui.ai': require('../../../../assets/favicons/syui.ai.png'),
|
||||
+ 'bsky.app': require('../../../../assets/favicons/bsky.app.png'),
|
||||
+ 'atproto.com': require('../../../../assets/favicons/atproto.com.png'),
|
||||
+}
|
||||
+
|
||||
+async function resolvePds(did: string): Promise<string> {
|
||||
+ if (did.startsWith('did:web:')) {
|
||||
+ const host = did.split(':').slice(2).join(':')
|
||||
+ const res = await fetch(`https://${host}/.well-known/did.json`)
|
||||
+ if (!res.ok) throw new Error('failed to resolve did:web')
|
||||
+ const doc = await res.json()
|
||||
+ const pds = doc.service?.find((s: any) => s.id === '#atproto_pds')?.serviceEndpoint
|
||||
+ if (pds) return pds
|
||||
+ return `https://${host}`
|
||||
+ }
|
||||
+ const res = await fetch(`https://plc.directory/${did}`)
|
||||
+ if (!res.ok) throw new Error('failed to resolve DID')
|
||||
+ const doc = await res.json()
|
||||
+ const pds = doc.service?.find((s: any) => s.id === '#atproto_pds')?.serviceEndpoint
|
||||
+ if (!pds) throw new Error('no PDS found')
|
||||
+ return pds
|
||||
+}
|
||||
+
|
||||
+function ProfileServiceLinks({
|
||||
+ profile,
|
||||
+}: {
|
||||
+ profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
+}) {
|
||||
+ const t = useTheme()
|
||||
+ const openLink = useOpenLink()
|
||||
+
|
||||
+ const {data: services} = useQuery({
|
||||
+ queryKey: ['profile-services', profile.did],
|
||||
+ queryFn: async () => {
|
||||
+ const pds = await resolvePds(profile.did)
|
||||
+ const res = await fetch(
|
||||
+ `${pds}/xrpc/com.atproto.repo.describeRepo?repo=${encodeURIComponent(profile.did)}`,
|
||||
+ )
|
||||
+ if (!res.ok) throw new Error('failed')
|
||||
+ const data = await res.json()
|
||||
+ const collections: string[] = data.collections || []
|
||||
+ const serviceSet = new Set<string>()
|
||||
+ for (const nsid of collections) {
|
||||
+ const parts = nsid.split('.')
|
||||
+ if (parts.length >= 2) {
|
||||
+ const domain = parts.slice(0, 2).reverse().join('.')
|
||||
+ serviceSet.add(domain)
|
||||
+ }
|
||||
+ }
|
||||
+ return Array.from(serviceSet)
|
||||
+ },
|
||||
+ })
|
||||
+
|
||||
+ if (!services || services.length === 0) return null
|
||||
+
|
||||
+ return (
|
||||
+ <View style={[a.flex_row, a.flex_wrap, a.gap_sm, a.pt_xs]}>
|
||||
+ {services.map(service => (
|
||||
+ <Pressable
|
||||
+ key={service}
|
||||
+ onPress={() =>
|
||||
+ openLink(
|
||||
+ `https://at.syu.is/@${profile.handle}/at/service/${service}`,
|
||||
+ )
|
||||
+ }
|
||||
+ style={[
|
||||
+ a.flex_row,
|
||||
+ a.align_center,
|
||||
+ a.gap_xs,
|
||||
+ a.rounded_full,
|
||||
+ t.atoms.bg_contrast_50,
|
||||
+ {paddingVertical: 6, paddingHorizontal: 10},
|
||||
+ ]}>
|
||||
+ <Image
|
||||
+ source={
|
||||
+ SERVICE_FAVICONS[service] || {
|
||||
+ uri: `https://www.google.com/s2/favicons?domain=${service}&sz=32`,
|
||||
+ }
|
||||
+ }
|
||||
+ style={{width: 14, height: 14, borderRadius: 3}}
|
||||
+ accessibilityIgnoresInvertColors
|
||||
+ />
|
||||
+ <Text
|
||||
+ style={[
|
||||
+ a.text_xs,
|
||||
+ a.font_medium,
|
||||
+ t.atoms.text_contrast_medium,
|
||||
+ ]}>
|
||||
+ {service}
|
||||
+ </Text>
|
||||
+ </Pressable>
|
||||
+ ))}
|
||||
+ </View>
|
||||
+ )
|
||||
+}
|
||||
+
|
||||
interface Props {
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
descriptionRT: RichTextAPI | null
|
||||
@@ -152,6 +252,7 @@
|
||||
{!isPlaceholderProfile && !isBlockedUser && (
|
||||
<View style={a.gap_md}>
|
||||
<ProfileHeaderMetrics profile={profile} />
|
||||
+ <ProfileServiceLinks profile={profile} />
|
||||
{descriptionRT && !moderation.ui('profileView').blur ? (
|
||||
<View pointerEvents="auto">
|
||||
<RichText
|
||||
35
ios/patching/039-social-app-ios-hide-feed-controls.patch
Normal file
@@ -0,0 +1,35 @@
|
||||
diff --git a/src/view/com/posts/PostFeedItem.tsx b/src/view/com/posts/PostFeedItem.tsx
|
||||
--- a/src/view/com/posts/PostFeedItem.tsx
|
||||
+++ b/src/view/com/posts/PostFeedItem.tsx
|
||||
@@ -385,17 +385,19 @@
|
||||
threadgateRecord={threadgateRecord}
|
||||
/>
|
||||
- <PostControls
|
||||
- post={post}
|
||||
- record={record}
|
||||
- richText={richText}
|
||||
- onPressReply={onPressReply}
|
||||
- logContext="FeedItem"
|
||||
- feedContext={feedContext}
|
||||
- reqId={reqId}
|
||||
- threadgateRecord={threadgateRecord}
|
||||
- onShowLess={onShowLess}
|
||||
- viaRepost={viaRepost}
|
||||
- />
|
||||
+ {false && (
|
||||
+ <PostControls
|
||||
+ post={post}
|
||||
+ record={record}
|
||||
+ richText={richText}
|
||||
+ onPressReply={onPressReply}
|
||||
+ logContext="FeedItem"
|
||||
+ feedContext={feedContext}
|
||||
+ reqId={reqId}
|
||||
+ threadgateRecord={threadgateRecord}
|
||||
+ onShowLess={onShowLess}
|
||||
+ viaRepost={viaRepost}
|
||||
+ />
|
||||
+ )}
|
||||
</View>
|
||||
|
||||
<DiscoverDebug feedContext={feedContext} />
|
||||
50
ios/patching/040-social-app-ios-hide-composer-prompt.patch
Normal file
@@ -0,0 +1,50 @@
|
||||
--- a/src/view/com/posts/PostFeed.tsx 2026-02-16 02:19:59
|
||||
+++ b/src/view/com/posts/PostFeed.tsx 2026-02-16 02:20:13
|
||||
@@ -519,16 +519,17 @@
|
||||
key: 'liveEventFeedsAndTrendingBanner-' + sliceIndex,
|
||||
})
|
||||
// Show composer prompt for Discover and Following feeds
|
||||
- if (
|
||||
- hasSession &&
|
||||
- (feedUriOrActorDid === DISCOVER_FEED_URI ||
|
||||
- feed === 'following')
|
||||
- ) {
|
||||
- arr.push({
|
||||
- type: 'composerPrompt',
|
||||
- key: 'composerPrompt-' + sliceIndex,
|
||||
- })
|
||||
- }
|
||||
+ // Disabled: hide composer prompt
|
||||
+ // if (
|
||||
+ // hasSession &&
|
||||
+ // (feedUriOrActorDid === DISCOVER_FEED_URI ||
|
||||
+ // feed === 'following')
|
||||
+ // ) {
|
||||
+ // arr.push({
|
||||
+ // type: 'composerPrompt',
|
||||
+ // key: 'composerPrompt-' + sliceIndex,
|
||||
+ // })
|
||||
+ // }
|
||||
} else if (sliceIndex === 15) {
|
||||
if (areVideoFeedsEnabled && !trendingVideoDisabled) {
|
||||
arr.push({
|
||||
@@ -545,12 +546,13 @@
|
||||
} else if (feedKind === 'following') {
|
||||
if (sliceIndex === 0) {
|
||||
// Show composer prompt for Following feed
|
||||
- if (hasSession) {
|
||||
- arr.push({
|
||||
- type: 'composerPrompt',
|
||||
- key: 'composerPrompt-' + sliceIndex,
|
||||
- })
|
||||
- }
|
||||
+ // Disabled: hide composer prompt
|
||||
+ // if (hasSession) {
|
||||
+ // arr.push({
|
||||
+ // type: 'composerPrompt',
|
||||
+ // key: 'composerPrompt-' + sliceIndex,
|
||||
+ // })
|
||||
+ // }
|
||||
}
|
||||
} else if (feedKind === 'profile') {
|
||||
if (sliceIndex === 5) {
|
||||
14
ios/patching/041-social-app-ios-splash-signin-button.patch
Normal file
@@ -0,0 +1,14 @@
|
||||
--- a/src/view/com/auth/SplashScreen.tsx 2026-02-16 02:55:08
|
||||
+++ b/src/view/com/auth/SplashScreen.tsx 2026-02-16 02:55:16
|
||||
@@ -118,8 +118,9 @@
|
||||
accessibilityHint={_(
|
||||
msg`Opens flow to sign in to your existing Bluesky account`,
|
||||
)}
|
||||
- size="large">
|
||||
- <ButtonText style={{color: 'white'}}>
|
||||
+ size="large"
|
||||
+ color="primary">
|
||||
+ <ButtonText>
|
||||
<Trans>Sign in</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
18
ios/patching/042-social-app-ios-at-links.patch
Normal file
@@ -0,0 +1,18 @@
|
||||
--- a/src/screens/Profile/Header/ProfileHeaderStandard.tsx
|
||||
+++ b/src/screens/Profile/Header/ProfileHeaderStandard.tsx
|
||||
@@ -48,6 +48,7 @@
|
||||
import {ProfileHeaderHandle} from './Handle'
|
||||
import {ProfileHeaderMetrics} from './Metrics'
|
||||
import {ProfileHeaderShell} from './Shell'
|
||||
+import {ProfileAtLinks} from './ProfileAtLinks'
|
||||
import {ProfileHeaderSuggestedFollows} from './SuggestedFollows'
|
||||
|
||||
const SERVICE_FAVICONS: Record<string, any> = {
|
||||
@@ -253,6 +254,7 @@
|
||||
<View style={a.gap_md}>
|
||||
<ProfileHeaderMetrics profile={profile} />
|
||||
<ProfileServiceLinks profile={profile} />
|
||||
+ <ProfileAtLinks profile={profile} />
|
||||
{descriptionRT && !moderation.ui('profileView').blur ? (
|
||||
<View pointerEvents="auto">
|
||||
<RichText
|
||||
19
ios/patching/043-social-app-ios-rightnav-links.patch
Normal file
@@ -0,0 +1,19 @@
|
||||
--- a/src/view/shell/desktop/RightNav.tsx
|
||||
+++ b/src/view/shell/desktop/RightNav.tsx
|
||||
@@ -111,14 +111,14 @@
|
||||
</>
|
||||
)}
|
||||
<InlineLinkText
|
||||
- to="https://bsky.social/about/support/privacy-policy"
|
||||
+ to="/support/privacy-policy"
|
||||
style={[t.atoms.text_contrast_medium]}
|
||||
label={_(msg`Privacy`)}>
|
||||
{_(msg`Privacy`)}
|
||||
</InlineLinkText>
|
||||
<Text style={[t.atoms.text_contrast_low]}>{' ∙ '}</Text>
|
||||
<InlineLinkText
|
||||
- to="https://bsky.social/about/support/tos"
|
||||
+ to="/support/tos"
|
||||
style={[t.atoms.text_contrast_medium]}
|
||||
label={_(msg`Terms`)}>
|
||||
{_(msg`Terms`)}
|
||||
60
ios/patching/044-social-app-ios-splash-video.patch
Normal file
@@ -0,0 +1,60 @@
|
||||
--- a/src/view/com/auth/SplashScreen.tsx
|
||||
+++ b/src/view/com/auth/SplashScreen.tsx
|
||||
@@ -2,6 +2,7 @@
|
||||
import {Image as RNImage, View} from 'react-native'
|
||||
import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'
|
||||
import {Image} from 'expo-image'
|
||||
+import {useVideoPlayer, VideoView} from 'expo-video'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
@@ -15,10 +16,13 @@
|
||||
import splashImagePointer from '../../../../assets/splash/illustration-mobile.png'
|
||||
// @ts-ignore
|
||||
import darkSplashImagePointer from '../../../../assets/splash/illustration-mobile-dark.png'
|
||||
+// @ts-ignore
|
||||
+import splashVideoPointer from '../../../../assets/splash/illustration-mobile.mp4'
|
||||
const splashImageUri = RNImage.resolveAssetSource(splashImagePointer).uri
|
||||
const darkSplashImageUri = RNImage.resolveAssetSource(
|
||||
darkSplashImagePointer,
|
||||
).uri
|
||||
+const splashVideoUri = RNImage.resolveAssetSource(splashVideoPointer).uri
|
||||
|
||||
export const SplashScreen = ({
|
||||
onPressSignin,
|
||||
@@ -53,13 +57,30 @@
|
||||
}
|
||||
}, [t, isDarkMode])
|
||||
|
||||
+ const player = useVideoPlayer(splashVideoUri, p => {
|
||||
+ p.loop = true
|
||||
+ p.muted = true
|
||||
+ p.play()
|
||||
+ })
|
||||
+
|
||||
return (
|
||||
<>
|
||||
- <Image
|
||||
- accessibilityIgnoresInvertColors
|
||||
- source={{uri: isDarkMode ? darkSplashImageUri : splashImageUri}}
|
||||
- style={[a.absolute, a.inset_0]}
|
||||
- />
|
||||
+ {isDarkMode ? (
|
||||
+ <Image
|
||||
+ accessibilityIgnoresInvertColors
|
||||
+ source={{uri: darkSplashImageUri}}
|
||||
+ style={[a.absolute, a.inset_0]}
|
||||
+ />
|
||||
+ ) : (
|
||||
+ <VideoView
|
||||
+ player={player}
|
||||
+ style={[a.absolute, a.inset_0]}
|
||||
+ contentFit="cover"
|
||||
+ nativeControls={false}
|
||||
+ allowsFullscreen={false}
|
||||
+ allowsPictureInPicture={false}
|
||||
+ />
|
||||
+ )}
|
||||
|
||||
<Animated.View
|
||||
entering={FadeIn.duration(90)}
|
||||
40
ios/patching/045-social-app-ios-composer-cancel.patch
Normal file
@@ -0,0 +1,40 @@
|
||||
--- a/src/view/com/composer/Composer.tsx
|
||||
+++ b/src/view/com/composer/Composer.tsx
|
||||
@@ -717,16 +717,7 @@
|
||||
post.shortenedGraphemeLength > 0 || post.embed.media || post.embed.link,
|
||||
)
|
||||
|
||||
- // Show discard prompt if there's content AND either:
|
||||
- // - No draft is loaded (new composition)
|
||||
- // - Draft is loaded but has been modified
|
||||
- if (hasContent && (!composerState.draftId || composerState.isDirty)) {
|
||||
- closeAllDialogs()
|
||||
- Keyboard.dismiss()
|
||||
- discardPromptControl.open()
|
||||
- } else {
|
||||
- onClose()
|
||||
- }
|
||||
+ onClose()
|
||||
}, [
|
||||
thread,
|
||||
composerState.draftId,
|
||||
@@ -1551,18 +1542,7 @@
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
- {!isReply && (
|
||||
- <DraftsButton
|
||||
- onSelectDraft={onSelectDraft}
|
||||
- onSaveDraft={onSaveDraft}
|
||||
- onDiscard={onDiscard}
|
||||
- isEmpty={isEmpty}
|
||||
- isDirty={isDirty}
|
||||
- isEditingDraft={isEditingDraft}
|
||||
- canSaveDraft={canSaveDraft}
|
||||
- textLength={textLength}
|
||||
- />
|
||||
- )}
|
||||
+ {/* DraftsButton removed */}
|
||||
<Button
|
||||
testID="composerPublishBtn"
|
||||
label={
|
||||
55
ios/patching/046-social-app-ios-null-url-guards.patch
Normal file
@@ -0,0 +1,55 @@
|
||||
diff --git a/src/features/liveEvents/context.tsx b/src/features/liveEvents/context.tsx
|
||||
index 3de2534a8..e11955f57 100644
|
||||
--- a/src/features/liveEvents/context.tsx
|
||||
+++ b/src/features/liveEvents/context.tsx
|
||||
@@ -24,6 +24,7 @@ export const DEFAULT_LIVE_EVENTS = {
|
||||
|
||||
async function fetchLiveEvents(): Promise<LiveEventsWorkerResponse | null> {
|
||||
try {
|
||||
+ if (!LIVE_EVENTS_URL) return null
|
||||
const res = await fetch(`${LIVE_EVENTS_URL}/config`)
|
||||
if (!res.ok) return null
|
||||
const data = await res.json()
|
||||
diff --git a/src/geolocation/const.ts b/src/geolocation/const.ts
|
||||
index 653e829ba..4992cc870 100644
|
||||
--- a/src/geolocation/const.ts
|
||||
+++ b/src/geolocation/const.ts
|
||||
@@ -1,7 +1,9 @@
|
||||
import {GEOLOCATION_URL} from '#/env'
|
||||
import {type Geolocation} from '#/geolocation/types'
|
||||
|
||||
-export const GEOLOCATION_SERVICE_URL = `${GEOLOCATION_URL}/geolocation`
|
||||
+export const GEOLOCATION_SERVICE_URL = GEOLOCATION_URL
|
||||
+ ? `${GEOLOCATION_URL}/geolocation`
|
||||
+ : null
|
||||
|
||||
/**
|
||||
* Default geolocation config.
|
||||
diff --git a/src/geolocation/service.ts b/src/geolocation/service.ts
|
||||
index 2d9285b67..c7c3d02c0 100644
|
||||
--- a/src/geolocation/service.ts
|
||||
+++ b/src/geolocation/service.ts
|
||||
@@ -26,9 +26,10 @@ const onGeolocationServiceResponseUpdate = (
|
||||
}
|
||||
|
||||
async function fetchGeolocationServiceData(
|
||||
- url: string,
|
||||
+ url: string | null,
|
||||
): Promise<Geolocation | undefined> {
|
||||
if (debug.enabled) return debug.resolve(debug.geolocation)
|
||||
+ if (!url) throw new Error('geolocation service disabled')
|
||||
const res = await fetch(url)
|
||||
if (!res.ok) {
|
||||
throw new Error(`fetchGeolocationServiceData failed ${res.status}`)
|
||||
diff --git a/src/state/appConfig.tsx b/src/state/appConfig.tsx
|
||||
index 67b0e553e..9eacf7ead 100644
|
||||
--- a/src/state/appConfig.tsx
|
||||
+++ b/src/state/appConfig.tsx
|
||||
@@ -30,6 +30,7 @@ let fetchAppConfigPromise: Promise<AppConfigResponse> | undefined
|
||||
|
||||
async function fetchAppConfig(): Promise<AppConfigResponse | null> {
|
||||
try {
|
||||
+ if (!APP_CONFIG_URL) return null
|
||||
if (!fetchAppConfigPromise) {
|
||||
fetchAppConfigPromise = (async () => {
|
||||
const r = await fetch(`${APP_CONFIG_URL}/config`)
|
||||
@@ -27,7 +27,7 @@ export function LicenseScreen() {
|
||||
<Text style={[a.text_lg, a.font_bold, a.mt_lg, a.mb_md]}>MIT License</Text>
|
||||
|
||||
<Text style={[a.mb_md, {fontFamily: 'monospace'}]}>
|
||||
Copyright (c) 2022-2025 Bluesky PBC
|
||||
Copyright (c) 2022-2026 Bluesky PBC
|
||||
</Text>
|
||||
|
||||
<Text style={[a.mb_md]}>
|
||||
|
||||
150
ios/patching/ProfileAtLinks.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import React from 'react'
|
||||
import {Pressable, View} from 'react-native'
|
||||
import {type AppBskyActorDefs} from '@atproto/api'
|
||||
import {useQuery} from '@tanstack/react-query'
|
||||
|
||||
import {useOpenLink} from '#/lib/hooks/useOpenLink'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {createSinglePathSVG} from '#/components/icons/TEMPLATE'
|
||||
|
||||
// --- SVG Icons (viewBox 0 0 640 640, Font Awesome) ---
|
||||
|
||||
const GithubIcon = createSinglePathSVG({
|
||||
path: 'M237.9 461.4C237.9 463.4 235.6 465 232.7 465C229.4 465.3 227.1 463.7 227.1 461.4C227.1 459.4 229.4 457.8 232.3 457.8C235.3 457.5 237.9 459.1 237.9 461.4zM206.8 456.9C206.1 458.9 208.1 461.2 211.1 461.8C213.7 462.8 216.7 461.8 217.3 459.8C217.9 457.8 216 455.5 213 454.6C210.4 453.9 207.5 454.9 206.8 456.9zM251 455.2C248.1 455.9 246.1 457.8 246.4 460.1C246.7 462.1 249.3 463.4 252.3 462.7C255.2 462 257.2 460.1 256.9 458.1C256.6 456.2 253.9 454.9 251 455.2zM316.8 72C178.1 72 72 177.3 72 316C72 426.9 141.8 521.8 241.5 555.2C254.3 557.5 258.8 549.6 258.8 543.1C258.8 536.9 258.5 502.7 258.5 481.7C258.5 481.7 188.5 496.7 173.8 451.9C173.8 451.9 162.4 422.8 146 415.3C146 415.3 123.1 399.6 147.6 399.9C147.6 399.9 172.5 401.9 186.2 425.7C208.1 464.3 244.8 453.2 259.1 446.6C261.4 430.6 267.9 419.5 275.1 412.9C219.2 406.7 162.8 398.6 162.8 302.4C162.8 274.9 170.4 261.1 186.4 243.5C183.8 237 175.3 210.2 189 175.6C209.9 169.1 258 202.6 258 202.6C278 197 299.5 194.1 320.8 194.1C342.1 194.1 363.6 197 383.6 202.6C383.6 202.6 431.7 169 452.6 175.6C466.3 210.3 457.8 237 455.2 243.5C471.2 261.2 481 275 481 302.4C481 398.9 422.1 406.6 366.2 412.9C375.4 420.8 383.2 435.8 383.2 459.3C383.2 493 382.9 534.7 382.9 542.9C382.9 549.4 387.5 557.3 400.2 555C500.2 521.8 568 426.9 568 316C568 177.3 455.5 72 316.8 72zM169.2 416.9C167.9 417.9 168.2 420.2 169.9 422.1C171.5 423.7 173.8 424.4 175.1 423.1C176.4 422.1 176.1 419.8 174.4 417.9C172.8 416.3 170.5 415.6 169.2 416.9zM158.4 408.8C157.7 410.1 158.7 411.7 160.7 412.7C162.3 413.7 164.3 413.4 165 412C165.7 410.7 164.7 409.1 162.7 408.1C160.7 407.5 159.1 407.8 158.4 408.8zM190.8 444.4C189.2 445.7 189.8 448.7 192.1 450.6C194.4 452.9 197.3 453.2 198.6 451.6C199.9 450.3 199.3 447.3 197.3 445.4C195.1 443.1 192.1 442.8 190.8 444.4zM179.4 429.7C177.8 430.7 177.8 433.3 179.4 435.6C181 437.9 183.7 438.9 185 437.9C186.6 436.6 186.6 434 185 431.7C183.6 429.4 181 428.4 179.4 429.7z',
|
||||
viewBox: '0 0 640 640',
|
||||
})
|
||||
|
||||
const XIcon = createSinglePathSVG({
|
||||
path: 'M453.2 112L523.8 112L369.6 288.2L551 528L409 528L297.7 382.6L170.5 528L99.8 528L264.7 339.5L90.8 112L236.4 112L336.9 244.9L453.2 112zM428.4 485.8L467.5 485.8L215.1 152L173.1 152L428.4 485.8z',
|
||||
viewBox: '0 0 640 640',
|
||||
})
|
||||
|
||||
const YoutubeIcon = createSinglePathSVG({
|
||||
path: 'M581.7 188.1C575.5 164.4 556.9 145.8 533.4 139.5C490.9 128 320.1 128 320.1 128C320.1 128 149.3 128 106.7 139.5C83.2 145.8 64.7 164.4 58.4 188.1C47 231 47 320.4 47 320.4C47 320.4 47 409.8 58.4 452.7C64.7 476.3 83.2 494.2 106.7 500.5C149.3 512 320.1 512 320.1 512C320.1 512 490.9 512 533.5 500.5C557 494.2 575.5 476.3 581.8 452.7C593.2 409.8 593.2 320.4 593.2 320.4C593.2 320.4 593.2 231 581.8 188.1zM264.2 401.6L264.2 239.2L406.9 320.4L264.2 401.6z',
|
||||
viewBox: '0 0 640 640',
|
||||
})
|
||||
|
||||
// --- Types ---
|
||||
|
||||
interface LinkItem {
|
||||
service: string
|
||||
username: string
|
||||
}
|
||||
|
||||
interface LinkCollection {
|
||||
links: LinkItem[]
|
||||
createdAt: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
// --- Service Config ---
|
||||
|
||||
const SERVICE_CONFIG: Record<
|
||||
string,
|
||||
{
|
||||
name: string
|
||||
urlTemplate: string
|
||||
icon: React.ComponentType<{size?: 'xs' | 'sm' | 'md' | 'lg'; fill?: string}>
|
||||
}
|
||||
> = {
|
||||
github: {
|
||||
name: 'GitHub',
|
||||
urlTemplate: 'https://github.com/{username}',
|
||||
icon: GithubIcon,
|
||||
},
|
||||
x: {
|
||||
name: 'X',
|
||||
urlTemplate: 'https://x.com/{username}',
|
||||
icon: XIcon,
|
||||
},
|
||||
youtube: {
|
||||
name: 'YouTube',
|
||||
urlTemplate: 'https://youtube.com/@{username}',
|
||||
icon: YoutubeIcon,
|
||||
},
|
||||
}
|
||||
|
||||
// --- PDS Resolution ---
|
||||
|
||||
async function resolvePds(did: string): Promise<string> {
|
||||
if (did.startsWith('did:web:')) {
|
||||
const host = did.split(':').slice(2).join(':')
|
||||
const res = await fetch(`https://${host}/.well-known/did.json`)
|
||||
if (!res.ok) throw new Error('failed to resolve did:web')
|
||||
const doc = await res.json()
|
||||
const pds = doc.service?.find((s: any) => s.id === '#atproto_pds')?.serviceEndpoint
|
||||
if (pds) return pds
|
||||
return `https://${host}`
|
||||
}
|
||||
const res = await fetch(`https://plc.directory/${did}`)
|
||||
if (!res.ok) throw new Error('failed to resolve DID')
|
||||
const doc = await res.json()
|
||||
const pds = doc.service?.find((s: any) => s.id === '#atproto_pds')?.serviceEndpoint
|
||||
if (!pds) throw new Error('no PDS found')
|
||||
return pds
|
||||
}
|
||||
|
||||
// --- Component ---
|
||||
|
||||
export function ProfileAtLinks({
|
||||
profile,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const openLink = useOpenLink()
|
||||
|
||||
const {data: linkData} = useQuery({
|
||||
queryKey: ['at-links', profile.did],
|
||||
queryFn: async () => {
|
||||
const pds = await resolvePds(profile.did)
|
||||
const res = await fetch(
|
||||
`${pds}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(profile.did)}&collection=ai.syui.at.link&rkey=self`,
|
||||
)
|
||||
if (!res.ok) throw new Error('not found')
|
||||
const json = await res.json()
|
||||
return json.value as LinkCollection
|
||||
},
|
||||
retry: false,
|
||||
staleTime: 1000 * 60 * 5,
|
||||
})
|
||||
|
||||
if (!linkData?.links?.length) return null
|
||||
|
||||
return (
|
||||
<View style={[a.flex_row, a.flex_wrap, a.gap_sm, a.pt_xs]}>
|
||||
{linkData.links.map(link => {
|
||||
const config = SERVICE_CONFIG[link.service]
|
||||
if (!config) return null
|
||||
const url = config.urlTemplate.replace('{username}', link.username)
|
||||
const Icon = config.icon
|
||||
return (
|
||||
<Pressable
|
||||
key={link.service}
|
||||
onPress={() => openLink(url)}
|
||||
accessibilityRole="link"
|
||||
accessibilityLabel={`${config.name}: ${link.username}`}
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.gap_xs,
|
||||
a.rounded_full,
|
||||
t.atoms.bg_contrast_50,
|
||||
{paddingVertical: 6, paddingHorizontal: 10},
|
||||
]}>
|
||||
<Icon size="xs" fill={t.atoms.text_contrast_medium.color} />
|
||||
<Text
|
||||
style={[
|
||||
a.text_xs,
|
||||
a.font_medium,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
{link.username}
|
||||
</Text>
|
||||
</Pressable>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
12162
ios/patching/ja-messages.po
Normal file
@@ -45,6 +45,15 @@ PATCH_FILES_IOS=(
|
||||
"033-social-app-ios-hide-profile-tabs.patch"
|
||||
"036-social-app-ios-homeheader-loggedout.patch"
|
||||
"037-social-app-ios-disable-contacts-nux.patch"
|
||||
"038-social-app-ios-profile-services.patch"
|
||||
"039-social-app-ios-hide-feed-controls.patch"
|
||||
"040-social-app-ios-hide-composer-prompt.patch"
|
||||
"041-social-app-ios-splash-signin-button.patch"
|
||||
"042-social-app-ios-at-links.patch"
|
||||
"043-social-app-ios-rightnav-links.patch"
|
||||
"044-social-app-ios-splash-video.patch"
|
||||
"045-social-app-ios-composer-cancel.patch"
|
||||
"046-social-app-ios-null-url-guards.patch"
|
||||
)
|
||||
|
||||
function ios-env() {
|
||||
@@ -166,6 +175,17 @@ function ios-copy-new-files() {
|
||||
echo "✅ Copied all assets (including logo.png, app-icons)"
|
||||
fi
|
||||
|
||||
# Generate app-icon.png (logo on #333 background) for app icon
|
||||
if [ -f "$d/ios/assets/logo.png" ]; then
|
||||
if command -v magick >/dev/null 2>&1; then
|
||||
magick -size 1024x1024 "xc:#333333" "$d/ios/assets/logo.png" -gravity center -composite "$target_dir/assets/app-icon.png"
|
||||
echo "✅ Generated app-icon.png (logo + #333 bg)"
|
||||
else
|
||||
echo "⚠️ ImageMagick not found, copying logo.png as app-icon.png fallback"
|
||||
cp "$d/ios/assets/logo.png" "$target_dir/assets/app-icon.png"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Copy License.tsx
|
||||
if [ -f "$patching_dir/License.tsx" ]; then
|
||||
mkdir -p "$target_dir/src/view/screens"
|
||||
@@ -180,6 +200,24 @@ function ios-copy-new-files() {
|
||||
echo "✅ Copied AppInfo.tsx"
|
||||
fi
|
||||
|
||||
# Copy Japanese locale (translation overrides)
|
||||
if [ -f "$patching_dir/ja-messages.po" ]; then
|
||||
cp "$patching_dir/ja-messages.po" "$target_dir/src/locale/locales/ja/messages.po"
|
||||
echo "✅ Copied ja-messages.po"
|
||||
fi
|
||||
|
||||
# Copy splash video
|
||||
if [ -f "$d/ios/assets/splash/illustration-mobile.mp4" ]; then
|
||||
cp "$d/ios/assets/splash/illustration-mobile.mp4" "$target_dir/assets/splash/illustration-mobile.mp4"
|
||||
echo "✅ Copied splash video (illustration-mobile.mp4)"
|
||||
fi
|
||||
|
||||
# Copy ProfileAtLinks.tsx
|
||||
if [ -f "$patching_dir/ProfileAtLinks.tsx" ]; then
|
||||
cp "$patching_dir/ProfileAtLinks.tsx" "$target_dir/src/screens/Profile/Header/ProfileAtLinks.tsx"
|
||||
echo "✅ Copied ProfileAtLinks.tsx"
|
||||
fi
|
||||
|
||||
# Copy pre-generated favicons for bskyweb
|
||||
local favicon_src="$d/ios/assets/favicons"
|
||||
local bskyweb_static="$target_dir/bskyweb/static"
|
||||
@@ -192,6 +230,14 @@ function ios-copy-new-files() {
|
||||
echo "✅ Copied favicons to bskyweb/static"
|
||||
fi
|
||||
|
||||
# Compile locale if messages.po was copied
|
||||
if [ -f "$patching_dir/ja-messages.po" ]; then
|
||||
echo "🌐 Compiling locale..."
|
||||
pushd "$target_dir" > /dev/null
|
||||
yarn intl:compile 2>/dev/null && echo "✅ Compiled locale" || echo "⚠️ Locale compile skipped (yarn not ready)"
|
||||
popd > /dev/null
|
||||
fi
|
||||
|
||||
echo ""
|
||||
}
|
||||
|
||||
|
||||
78
k8s/bgs.yaml
Normal file
@@ -0,0 +1,78 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: bgs
|
||||
namespace: atproto
|
||||
spec:
|
||||
selector:
|
||||
app: bgs
|
||||
ports:
|
||||
- port: 2470
|
||||
targetPort: 2470
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: bgs-data
|
||||
namespace: atproto
|
||||
spec:
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: bgs
|
||||
namespace: atproto
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: bgs
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: bgs
|
||||
spec:
|
||||
containers:
|
||||
- name: bgs
|
||||
image: registry/bgs
|
||||
ports:
|
||||
- containerPort: 2470
|
||||
env:
|
||||
- name: DATABASE_URL
|
||||
value: "postgres://postgres:postgres@database/bgs"
|
||||
- name: CARSTORE_DATABASE_URL
|
||||
value: "postgres://postgres:postgres@database/carstore"
|
||||
- name: DATA_DIR
|
||||
value: "/data"
|
||||
- name: ATP_PLC_HOST
|
||||
value: "https://plc.syu.is"
|
||||
- name: BGS_NEW_PDS_PER_DAY_LIMIT
|
||||
value: "1000"
|
||||
- name: BGS_ADMIN_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: atproto-secrets
|
||||
key: bgs-admin-key
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /xrpc/_health
|
||||
port: 2470
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /xrpc/_health
|
||||
port: 2470
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: bgs-data
|
||||
199
k8s/bsky.yaml
Normal file
@@ -0,0 +1,199 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: bsky
|
||||
namespace: atproto
|
||||
spec:
|
||||
selector:
|
||||
app: bsky
|
||||
ports:
|
||||
- name: api
|
||||
port: 2584
|
||||
targetPort: 2584
|
||||
- name: dataplane
|
||||
port: 3001
|
||||
targetPort: 3001
|
||||
- name: bsync
|
||||
port: 3002
|
||||
targetPort: 3002
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: bsky-data
|
||||
namespace: atproto
|
||||
spec:
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
resources:
|
||||
requests:
|
||||
storage: 5Gi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: bsky
|
||||
namespace: atproto
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: bsky
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: bsky
|
||||
spec:
|
||||
securityContext:
|
||||
runAsUser: 0
|
||||
containers:
|
||||
- name: bsky
|
||||
image: registry/bsky
|
||||
command: ["node", "--enable-source-maps", "api.js"]
|
||||
ports:
|
||||
- containerPort: 2584
|
||||
- containerPort: 3001
|
||||
- containerPort: 3002
|
||||
env:
|
||||
- name: BSKY_PORT
|
||||
value: "2584"
|
||||
- name: BSKY_BLOB_CACHE_LOC
|
||||
value: "/data/"
|
||||
- name: BSKY_BSYNC_HTTP_VERSION
|
||||
value: "1.1"
|
||||
- name: BSKY_BSYNC_PORT
|
||||
value: "3002"
|
||||
- name: BSKY_BSYNC_URL
|
||||
value: "http://localhost:3002"
|
||||
- name: BSKY_COURIER_URL
|
||||
value: "http://fake-courier.example.invalid/"
|
||||
- name: BSKY_DATAPLANE_HTTP_VERSION
|
||||
value: "1.1"
|
||||
- name: BSKY_DATAPLANE_PORT
|
||||
value: "3001"
|
||||
- name: BSKY_DATAPLANE_URLS
|
||||
value: "http://localhost:3001"
|
||||
- name: BSKY_DB_POSTGRES_URL
|
||||
value: "postgres://postgres:postgres@database/bsky"
|
||||
- name: BSKY_DID_PLC_URL
|
||||
value: "https://plc.syu.is"
|
||||
- name: BSKY_PUBLIC_URL
|
||||
value: "https://bsky.syu.is"
|
||||
- name: BSKY_REPO_PROVIDER
|
||||
value: "ws://bgs:2470"
|
||||
- name: BSKY_SERVER_DID
|
||||
value: "did:web:bsky.syu.is"
|
||||
- name: MOD_SERVICE_DID
|
||||
value: "did:web:ozone.syu.is"
|
||||
- name: BSKY_ADMIN_PASSWORDS
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: atproto-secrets
|
||||
key: bsky-admin-passwords
|
||||
- name: BSKY_SERVICE_SIGNING_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: atproto-secrets
|
||||
key: bsky-service-signing-key
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /xrpc/_health
|
||||
port: 2584
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 10
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /xrpc/_health
|
||||
port: 2584
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: bsky-data
|
||||
---
|
||||
## bsky subscription monitor
|
||||
## subscriptionが停止していたらPodを再起動する
|
||||
apiVersion: batch/v1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: bsky-subscription-watchdog
|
||||
namespace: atproto
|
||||
spec:
|
||||
schedule: "*/5 * * * *"
|
||||
jobTemplate:
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: watchdog
|
||||
image: postgres:16-alpine
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
# BGSの最新seqを取得
|
||||
LATEST_SEQ=$(psql -t -A "$DB_URL" -c "SELECT COALESCE(MAX(seq),0) FROM repo_event_records")
|
||||
# bskyのsubscription cursorを取得
|
||||
BSKY_CURSOR=$(psql -t -A "$BSKY_DB_URL" -c "SELECT COALESCE(state,0) FROM subscription WHERE service='ws://bgs:2470' LIMIT 1")
|
||||
LAG=$((LATEST_SEQ - BSKY_CURSOR))
|
||||
echo "BGS seq=$LATEST_SEQ, bsky cursor=$BSKY_CURSOR, lag=$LAG"
|
||||
if [ "$LAG" -gt 50 ]; then
|
||||
echo "WARN: bsky subscription lag=$LAG, restarting bsky pod"
|
||||
# Podを削除すればDeploymentが再作成する
|
||||
apk add --no-cache curl > /dev/null 2>&1
|
||||
APISERVER=https://kubernetes.default.svc
|
||||
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
|
||||
NAMESPACE=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)
|
||||
# bsky podを取得して削除
|
||||
POD=$(curl -s -k -H "Authorization: Bearer $TOKEN" \
|
||||
"$APISERVER/api/v1/namespaces/$NAMESPACE/pods?labelSelector=app=bsky" \
|
||||
| grep -o '"name":"bsky-[^"]*"' | head -1 | cut -d'"' -f4)
|
||||
if [ -n "$POD" ]; then
|
||||
curl -s -k -X DELETE -H "Authorization: Bearer $TOKEN" \
|
||||
"$APISERVER/api/v1/namespaces/$NAMESPACE/pods/$POD"
|
||||
echo "Deleted pod $POD"
|
||||
fi
|
||||
else
|
||||
echo "OK: subscription is healthy"
|
||||
fi
|
||||
env:
|
||||
- name: DB_URL
|
||||
value: "postgres://postgres:postgres@database/bgs"
|
||||
- name: BSKY_DB_URL
|
||||
value: "postgres://postgres:postgres@database/bsky"
|
||||
restartPolicy: OnFailure
|
||||
serviceAccountName: bsky-watchdog
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: bsky-watchdog
|
||||
namespace: atproto
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: bsky-watchdog
|
||||
namespace: atproto
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["pods"]
|
||||
verbs: ["get", "list", "delete"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: bsky-watchdog
|
||||
namespace: atproto
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: bsky-watchdog
|
||||
namespace: atproto
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: bsky-watchdog
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
71
k8s/feed.yaml
Normal file
@@ -0,0 +1,71 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: feed
|
||||
namespace: atproto
|
||||
spec:
|
||||
selector:
|
||||
app: feed
|
||||
ports:
|
||||
- port: 3000
|
||||
targetPort: 3000
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: feed-data
|
||||
namespace: atproto
|
||||
spec:
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
resources:
|
||||
requests:
|
||||
storage: 2Gi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: feed
|
||||
namespace: atproto
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: feed
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: feed
|
||||
spec:
|
||||
containers:
|
||||
- name: feed
|
||||
image: registry/feed
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
env:
|
||||
- name: FEEDGEN_PORT
|
||||
value: "3000"
|
||||
- name: FEEDGEN_LISTENHOST
|
||||
value: "0.0.0.0"
|
||||
- name: FEEDGEN_SQLITE_LOCATION
|
||||
value: "/data/db.sqlite"
|
||||
- name: FEEDGEN_HOSTNAME
|
||||
value: "feed.syu.is"
|
||||
- name: FEEDGEN_PUBLISHER_DID
|
||||
value: "did:plc:6qyecktefllvenje24fcxnie"
|
||||
- name: FEEDGEN_SERVICE_DID
|
||||
value: "did:web:feed.syu.is"
|
||||
- name: FEEDGEN_JETSTREAM_URL
|
||||
value: "ws://jetstream:6008/subscribe"
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 3000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: feed-data
|
||||
26
k8s/gen-secrets.sh
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
## envs/ から k8s/secrets.env を生成する
|
||||
## usage: cd k8s && bash gen-secrets.sh
|
||||
|
||||
ENVS_DIR="${1:-../envs}"
|
||||
OUT="secrets.env"
|
||||
|
||||
get_val() {
|
||||
local file="$1" key="$2"
|
||||
grep "^${key}=" "$file" 2>/dev/null | head -1 | cut -d'=' -f2-
|
||||
}
|
||||
|
||||
cat > "$OUT" <<EOF
|
||||
pds-admin-password=$(get_val "$ENVS_DIR/pds" PDS_ADMIN_PASSWORD)
|
||||
pds-plc-rotation-key=$(get_val "$ENVS_DIR/pds" PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX)
|
||||
pds-repo-signing-key=$(get_val "$ENVS_DIR/pds" PDS_REPO_SIGNING_KEY_K256_PRIVATE_KEY_HEX)
|
||||
pds-jwt-secret=$(get_val "$ENVS_DIR/pds" PDS_JWT_SECRET)
|
||||
pds-email-smtp-url=$(get_val "$ENVS_DIR/pds" PDS_EMAIL_SMTP_URL)
|
||||
bsky-admin-passwords=$(get_val "$ENVS_DIR/bsky" BSKY_ADMIN_PASSWORDS)
|
||||
bsky-service-signing-key=$(get_val "$ENVS_DIR/bsky" BSKY_SERVICE_SIGNING_KEY)
|
||||
bgs-admin-key=$(get_val "$ENVS_DIR/bgs" BGS_ADMIN_KEY)
|
||||
ozone-admin-password=$(get_val "$ENVS_DIR/ozone" OZONE_ADMIN_PASSWORD)
|
||||
ozone-signing-key-hex=$(get_val "$ENVS_DIR/ozone" OZONE_SIGNING_KEY_HEX)
|
||||
EOF
|
||||
|
||||
echo "generated $OUT"
|
||||
67
k8s/jetstream.yaml
Normal file
@@ -0,0 +1,67 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: jetstream
|
||||
namespace: atproto
|
||||
spec:
|
||||
selector:
|
||||
app: jetstream
|
||||
ports:
|
||||
- port: 6008
|
||||
targetPort: 6008
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: jetstream-data
|
||||
namespace: atproto
|
||||
spec:
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
resources:
|
||||
requests:
|
||||
storage: 2Gi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: jetstream
|
||||
namespace: atproto
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: jetstream
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: jetstream
|
||||
spec:
|
||||
containers:
|
||||
- name: jetstream
|
||||
image: registry/jetstream
|
||||
ports:
|
||||
- containerPort: 6008
|
||||
env:
|
||||
- name: JETSTREAM_WS_URL
|
||||
value: "ws://bgs:2470/xrpc/com.atproto.sync.subscribeRepos"
|
||||
- name: JETSTREAM_DATA_DIR
|
||||
value: "/data"
|
||||
- name: JETSTREAM_LISTEN_ADDR
|
||||
value: ":6008"
|
||||
- name: JETSTREAM_METRICS_LISTEN_ADDR
|
||||
value: ":6009"
|
||||
- name: JETSTREAM_LIVENESS_TTL
|
||||
value: "96h"
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 6009
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: jetstream-data
|
||||
27
k8s/kustomization.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
namespace: atproto
|
||||
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- postgres.yaml
|
||||
- redis.yaml
|
||||
- plc.yaml
|
||||
- pds.yaml
|
||||
- bgs.yaml
|
||||
- bsky.yaml
|
||||
- social-app.yaml
|
||||
- ozone.yaml
|
||||
- ozone-web.yaml
|
||||
- jetstream.yaml
|
||||
- feed.yaml
|
||||
|
||||
## deploy.yml に images / secretGenerator を設定
|
||||
## デプロイ: cp deploy.yml kustomization.yaml && kubectl apply -k .
|
||||
secretGenerator:
|
||||
- name: atproto-secrets
|
||||
envs:
|
||||
- secrets.env
|
||||
options:
|
||||
disableNameSuffixHash: true
|
||||
4
k8s/namespace.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: atproto
|
||||
47
k8s/ozone-web.yaml
Normal file
@@ -0,0 +1,47 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: ozone-web
|
||||
namespace: atproto
|
||||
spec:
|
||||
selector:
|
||||
app: ozone-web
|
||||
ports:
|
||||
- port: 3000
|
||||
targetPort: 3000
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: ozone-web
|
||||
namespace: atproto
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: ozone-web
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: ozone-web
|
||||
spec:
|
||||
containers:
|
||||
- name: ozone-web
|
||||
image: registry/ozone-web
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
env:
|
||||
- name: NEXT_PUBLIC_PLC_DIRECTORY_URL
|
||||
value: "https://plc.syu.is"
|
||||
- name: NEXT_PUBLIC_OZONE_SERVICE_DID
|
||||
value: "did:web:ozone.syu.is"
|
||||
- name: NEXT_PUBLIC_SOCIAL_APP_DOMAIN
|
||||
value: "syu.is"
|
||||
- name: NEXT_PUBLIC_SOCIAL_APP_URL
|
||||
value: "https://syu.is"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 3000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
96
k8s/ozone.yaml
Normal file
@@ -0,0 +1,96 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: ozone
|
||||
namespace: atproto
|
||||
spec:
|
||||
selector:
|
||||
app: ozone
|
||||
ports:
|
||||
- port: 3000
|
||||
targetPort: 3000
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: ozone-data
|
||||
namespace: atproto
|
||||
spec:
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
resources:
|
||||
requests:
|
||||
storage: 2Gi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: ozone
|
||||
namespace: atproto
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: ozone
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: ozone
|
||||
spec:
|
||||
containers:
|
||||
- name: ozone
|
||||
image: registry/ozone
|
||||
command: ["node", "--enable-source-maps", "api.js"]
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
env:
|
||||
- name: OZONE_SERVER_DID
|
||||
value: "did:web:ozone.syu.is"
|
||||
- name: OZONE_PUBLIC_URL
|
||||
value: "https://ozone.syu.is"
|
||||
- name: OZONE_DB_POSTGRES_URL
|
||||
value: "postgres://postgres:postgres@database/ozone"
|
||||
- name: OZONE_DID_PLC_URL
|
||||
value: "https://plc.syu.is"
|
||||
- name: OZONE_APPVIEW_DID
|
||||
value: "did:web:bsky.syu.is"
|
||||
- name: OZONE_APPVIEW_URL
|
||||
value: "https://bsky.syu.is"
|
||||
- name: OZONE_APPVIEW_PUSH_EVENTS
|
||||
value: "true"
|
||||
- name: OZONE_PDS_DID
|
||||
value: "did:web:syu.is"
|
||||
- name: OZONE_PDS_URL
|
||||
value: "https://syu.is"
|
||||
- name: OZONE_DEV_MODE
|
||||
value: "true"
|
||||
- name: OZONE_DB_MIGRATE
|
||||
value: "1"
|
||||
- name: OZONE_ADMIN_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: atproto-secrets
|
||||
key: ozone-admin-password
|
||||
- name: OZONE_SIGNING_KEY_HEX
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: atproto-secrets
|
||||
key: ozone-signing-key-hex
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /xrpc/_health
|
||||
port: 3000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /xrpc/_health
|
||||
port: 3000
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: ozone-data
|
||||
121
k8s/pds.yaml
Normal file
@@ -0,0 +1,121 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: pds
|
||||
namespace: atproto
|
||||
spec:
|
||||
selector:
|
||||
app: pds
|
||||
ports:
|
||||
- port: 3000
|
||||
targetPort: 3000
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: pds-data
|
||||
namespace: atproto
|
||||
spec:
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: pds
|
||||
namespace: atproto
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: pds
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: pds
|
||||
spec:
|
||||
containers:
|
||||
- name: pds
|
||||
image: registry/pds
|
||||
command: ["node", "--enable-source-maps", "index.js"]
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
env:
|
||||
- name: PDS_HOSTNAME
|
||||
value: "syu.is"
|
||||
- name: PDS_DB_POSTGRES_URL
|
||||
value: "postgres://postgres:postgres@database/pds"
|
||||
- name: PDS_DATA_DIRECTORY
|
||||
value: "/data"
|
||||
- name: PDS_BLOBSTORE_DISK_LOCATION
|
||||
value: "/data/img/static"
|
||||
- name: PDS_BSKY_APP_VIEW_DID
|
||||
value: "did:web:bsky.syu.is"
|
||||
- name: PDS_BSKY_APP_VIEW_URL
|
||||
value: "https://bsky.syu.is"
|
||||
- name: PDS_CRAWLERS
|
||||
value: "https://bgs.syu.is"
|
||||
- name: PDS_SEQUENCER_ENABLED
|
||||
value: "true"
|
||||
- name: PDS_SEQUENCER_DB_LOCATION
|
||||
value: "/data/sequencer.sqlite"
|
||||
- name: PDS_DEV_MODE
|
||||
value: "true"
|
||||
- name: PDS_DID_PLC_URL
|
||||
value: "https://plc.syu.is"
|
||||
- name: PDS_ENABLE_DID_DOC_WITH_SESSION
|
||||
value: "true"
|
||||
- name: PDS_INVITE_INTERVAL
|
||||
value: "604800000"
|
||||
- name: PDS_SERVICE_DID
|
||||
value: "did:web:syu.is"
|
||||
- name: PDS_EMAIL_FROM_ADDRESS
|
||||
value: "no-reply@syu.is"
|
||||
- name: PDS_INVITE_REQUIRED
|
||||
value: "true"
|
||||
- name: PDS_ADMIN_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: atproto-secrets
|
||||
key: pds-admin-password
|
||||
- name: PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: atproto-secrets
|
||||
key: pds-plc-rotation-key
|
||||
- name: PDS_REPO_SIGNING_KEY_K256_PRIVATE_KEY_HEX
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: atproto-secrets
|
||||
key: pds-repo-signing-key
|
||||
- name: PDS_JWT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: atproto-secrets
|
||||
key: pds-jwt-secret
|
||||
- name: PDS_EMAIL_SMTP_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: atproto-secrets
|
||||
key: pds-email-smtp-url
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /xrpc/_health
|
||||
port: 3000
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /xrpc/_health
|
||||
port: 3000
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: pds-data
|
||||
53
k8s/plc.yaml
Normal file
@@ -0,0 +1,53 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: plc
|
||||
namespace: atproto
|
||||
spec:
|
||||
selector:
|
||||
app: plc
|
||||
ports:
|
||||
- port: 3000
|
||||
targetPort: 3000
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: plc
|
||||
namespace: atproto
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: plc
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: plc
|
||||
spec:
|
||||
containers:
|
||||
- name: plc
|
||||
image: registry/plc
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
env:
|
||||
- name: DATABASE_URL
|
||||
value: "postgres://postgres:postgres@database/plc"
|
||||
- name: DB_CREDS_JSON
|
||||
value: '{"username":"postgres","password":"postgres","host":"database","port":"5432","database":"plc"}'
|
||||
- name: ENABLE_MIGRATIONS
|
||||
value: "true"
|
||||
- name: DB_MIGRATE_CREDS_JSON
|
||||
value: '{"username":"postgres","password":"postgres","host":"database","port":"5432","database":"plc"}'
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /_health
|
||||
port: 3000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /_health
|
||||
port: 3000
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
89
k8s/postgres.yaml
Normal file
@@ -0,0 +1,89 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: postgres-init
|
||||
namespace: atproto
|
||||
data:
|
||||
init.sql: |
|
||||
CREATE DATABASE plc;
|
||||
GRANT ALL PRIVILEGES ON DATABASE plc TO postgres;
|
||||
CREATE DATABASE bgs;
|
||||
CREATE DATABASE carstore;
|
||||
GRANT ALL PRIVILEGES ON DATABASE bgs TO postgres;
|
||||
GRANT ALL PRIVILEGES ON DATABASE carstore TO postgres;
|
||||
CREATE DATABASE bsky;
|
||||
GRANT ALL PRIVILEGES ON DATABASE bsky TO postgres;
|
||||
CREATE DATABASE ozone;
|
||||
GRANT ALL PRIVILEGES ON DATABASE ozone TO postgres;
|
||||
CREATE DATABASE pds;
|
||||
GRANT ALL PRIVILEGES ON DATABASE pds TO postgres;
|
||||
CREATE DATABASE bsync;
|
||||
GRANT ALL PRIVILEGES ON DATABASE bsync TO postgres;
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: database
|
||||
namespace: atproto
|
||||
spec:
|
||||
selector:
|
||||
app: postgres
|
||||
ports:
|
||||
- port: 5432
|
||||
targetPort: 5432
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: postgres
|
||||
namespace: atproto
|
||||
spec:
|
||||
serviceName: database
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: postgres
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: postgres
|
||||
spec:
|
||||
containers:
|
||||
- name: postgres
|
||||
image: postgres:16-alpine
|
||||
env:
|
||||
- name: POSTGRES_USER
|
||||
value: postgres
|
||||
- name: POSTGRES_PASSWORD
|
||||
value: postgres
|
||||
- name: POSTGRES_DB
|
||||
value: healthcheck
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /var/lib/postgresql/data
|
||||
- name: init
|
||||
mountPath: /docker-entrypoint-initdb.d
|
||||
livenessProbe:
|
||||
exec:
|
||||
command: ["pg_isready", "-U", "postgres", "-d", "healthcheck"]
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 5
|
||||
readinessProbe:
|
||||
exec:
|
||||
command: ["pg_isready", "-U", "postgres", "-d", "healthcheck"]
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
volumes:
|
||||
- name: init
|
||||
configMap:
|
||||
name: postgres-init
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: data
|
||||
spec:
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
resources:
|
||||
requests:
|
||||
storage: 20Gi
|
||||
58
k8s/redis.yaml
Normal file
@@ -0,0 +1,58 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: redis
|
||||
namespace: atproto
|
||||
spec:
|
||||
selector:
|
||||
app: redis
|
||||
ports:
|
||||
- port: 6379
|
||||
targetPort: 6379
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: redis
|
||||
namespace: atproto
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: redis
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: redis
|
||||
spec:
|
||||
containers:
|
||||
- name: redis
|
||||
image: redis:alpine
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
livenessProbe:
|
||||
exec:
|
||||
command: ["redis-cli", "ping"]
|
||||
periodSeconds: 5
|
||||
readinessProbe:
|
||||
exec:
|
||||
command: ["redis-cli", "ping"]
|
||||
periodSeconds: 5
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: redis-data
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: redis-data
|
||||
namespace: atproto
|
||||
spec:
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
10
k8s/secrets.env.example
Normal file
@@ -0,0 +1,10 @@
|
||||
pds-admin-password=
|
||||
pds-plc-rotation-key=
|
||||
pds-repo-signing-key=
|
||||
pds-jwt-secret=
|
||||
pds-email-smtp-url=
|
||||
bsky-admin-passwords=
|
||||
bsky-service-signing-key=
|
||||
bgs-admin-key=
|
||||
ozone-admin-password=
|
||||
ozone-signing-key-hex=
|
||||
52
k8s/social-app.yaml
Normal file
@@ -0,0 +1,52 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: social-app
|
||||
namespace: atproto
|
||||
spec:
|
||||
selector:
|
||||
app: social-app
|
||||
ports:
|
||||
- port: 8100
|
||||
targetPort: 8100
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: social-app
|
||||
namespace: atproto
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: social-app
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: social-app
|
||||
spec:
|
||||
containers:
|
||||
- name: social-app
|
||||
image: registry/social-app
|
||||
command: ["/usr/bin/bskyweb", "serve"]
|
||||
ports:
|
||||
- containerPort: 8100
|
||||
env:
|
||||
- name: ATP_APPVIEW_HOST
|
||||
value: "https://public.api.bsky.app"
|
||||
- name: EXPO_PUBLIC_BLUESKY_PROXY_DID
|
||||
value: "did:web:api.bsky.app"
|
||||
- name: EXPO_PUBLIC_ENV
|
||||
value: "production"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 8100
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 8100
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
@@ -1,13 +1,11 @@
|
||||
diff --git a/packages/oauth/oauth-provider/src/router/create-authorization-page-middleware.ts b/packages/oauth/oauth-provider/src/router/create-authorization-page-middleware.ts
|
||||
index f653b0353..45c45fac1 100644
|
||||
--- a/packages/oauth/oauth-provider/src/router/create-authorization-page-middleware.ts
|
||||
+++ b/packages/oauth/oauth-provider/src/router/create-authorization-page-middleware.ts
|
||||
@@ -53,7 +53,7 @@ export function createAuthorizationPageMiddleware<
|
||||
res.setHeader('Cache-Control', 'no-store')
|
||||
res.setHeader('Pragma', 'no-cache')
|
||||
|
||||
- validateFetchSite(req, ['cross-site', 'none'])
|
||||
+ validateFetchSite(req, ['cross-site', 'same-site', 'none'])
|
||||
@@ -74,7 +74,7 @@
|
||||
// @TODO Consider removing this altogether to allow hosting PDS and app on
|
||||
// the same site but different origins (different subdomains).
|
||||
- validateFetchSite(req, ['same-origin', 'cross-site', 'none'])
|
||||
+ validateFetchSite(req, ['same-origin', 'same-site', 'cross-site', 'none'])
|
||||
validateFetchMode(req, ['navigate'])
|
||||
validateFetchDest(req, ['document'])
|
||||
validateOrigin(req, issuerOrigin)
|
||||
|
||||
@@ -7,12 +7,12 @@ path: help/index.html
|
||||
## Getting Started
|
||||
|
||||
1. Download the app from the App Store
|
||||
2. Sign in with your AT Protocol handle
|
||||
2. Sign in with your handle
|
||||
3. Start connecting with your community
|
||||
|
||||
## Authentication
|
||||
|
||||
This app uses AT Protocol OAuth for secure authentication. Your credentials are never stored on our servers.
|
||||
This app uses OAuth for secure authentication. Your credentials are never stored on our servers.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -36,4 +36,4 @@ This app uses AT Protocol OAuth for secure authentication. Your credentials are
|
||||
|
||||
## Contact Support
|
||||
|
||||
For additional help, please visit our Git repository or contact us through AT Protocol.
|
||||
For additional help, please visit our Git repository or contact us through our social account.
|
||||
|
||||
@@ -12,7 +12,7 @@ This application is open source software.
|
||||
|
||||
This app uses the following open source libraries:
|
||||
|
||||
- **AT Protocol** - MIT License
|
||||
- **Decentralized Social Protocol** - MIT License
|
||||
- **React Native** - MIT License
|
||||
- **Expo** - MIT License
|
||||
|
||||
@@ -22,7 +22,7 @@ App icons and graphics are proprietary unless otherwise noted.
|
||||
|
||||
## Attribution
|
||||
|
||||
Built with AT Protocol by syui.
|
||||
Built by syui.
|
||||
|
||||
## Contact
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ path: privacy/index.html
|
||||
|
||||
This application collects minimal information necessary for its operation:
|
||||
|
||||
- **Account Information**: Your AT Protocol handle and DID for authentication
|
||||
- **Account Information**: Your handle and DID for authentication
|
||||
- **Usage Data**: Basic app usage statistics to improve the service
|
||||
|
||||
## How We Use Your Information
|
||||
@@ -19,7 +19,7 @@ This application collects minimal information necessary for its operation:
|
||||
|
||||
## Data Storage
|
||||
|
||||
Your data is stored securely on AT Protocol compatible servers. We do not sell or share your personal information with third parties.
|
||||
Your data is stored securely on decentralized social servers. We do not sell or share your personal information with third parties.
|
||||
|
||||
## Your Rights
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ By using this application, you agree to these Terms of Service.
|
||||
|
||||
You agree to use this service in accordance with:
|
||||
- Applicable laws and regulations
|
||||
- AT Protocol community guidelines
|
||||
- Community guidelines
|
||||
- Respectful behavior towards other users
|
||||
|
||||
## User Content
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"scheme": "aiat",
|
||||
"version": "1.111.0",
|
||||
"category": "Social",
|
||||
"description": "Aiat is a social networking application based on AT Protocol. Connect with your community on syu.is.",
|
||||
"description": "Aiat is a social networking application. Connect with your community on syu.is.",
|
||||
"backLink": "syu.is",
|
||||
"icon": "/static/app.png",
|
||||
"os": "iOS 26.0+",
|
||||
|
||||
@@ -61,7 +61,7 @@ export function appLayout(site: SiteConfig, content: string): string {
|
||||
<span class="link-arrow">→</span>
|
||||
</div>
|
||||
<div class="link-row">
|
||||
<span class="link-icon">ATProto</span>
|
||||
<span class="link-icon">Social</span>
|
||||
<a href="https://syui.ai" class="link-value" target="_blank">syui.ai</a>
|
||||
<span class="link-arrow">→</span>
|
||||
</div>
|
||||
|
||||