diff --git a/.github/workflows/cf-pages.yml b/.github/workflows/cf-pages.yml
new file mode 100644
index 0000000..85694ae
--- /dev/null
+++ b/.github/workflows/cf-pages.yml
@@ -0,0 +1,30 @@
+name: Deploy to Cloudflare Pages
+
+on:
+ push:
+ branches:
+ - main
+ paths:
+ - 'html/**'
+ workflow_dispatch:
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ deployments: write
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Deploy to Cloudflare Pages
+ uses: cloudflare/pages-action@v1
+ with:
+ apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+ accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+ projectName: ${{ secrets.CLOUDFLARE_PROJECT_NAME }}
+ directory: html
+ gitHubToken: ${{ secrets.GITHUB_TOKEN }}
+ wranglerVersion: '3'
diff --git a/.gitignore b/.gitignore
index bd389f8..daca568 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,5 +2,6 @@ repos
.claude
deploy.yml
claude.md
-store.mobileprovision
+embedded.mobileprovision
.env
+html.zip
diff --git a/README.md b/README.md
index 4137d1f..e11bdb2 100644
--- a/README.md
+++ b/README.md
@@ -85,20 +85,4 @@ $ curl -sL "syu.is/xrpc/com.atproto.repo.listRecords?repo=${handle}&collection=a
./ios/build.zsh
```
-## social-app
-- https://github.com/bluesky-social/social-app/blob/main/LICENSE
-
-```js
-PrivacyPolicy: 'https://syu.is/about/support/privacy-policy',
-TermsOfService: 'https://syu.is/about/support/tos',
-License: 'https://syu.is/about/support/license',
-```
-
-```js
-CommunityGuidelines: '/support/community-guidelines',
-CopyrightPolicy: '/support/copyright',
-```
-
-- https://bsky.social/about/support/community-guidelines
-- https://bsky.social/about/support/copyright
diff --git a/compose.yml b/compose.yml
index ca1a873..26c975e 100644
--- a/compose.yml
+++ b/compose.yml
@@ -86,6 +86,7 @@ services:
depends_on:
database:
condition: service_healthy
+ #command: ["/bigsky", "--crawl-insecure-ws"]
social-app:
ports:
diff --git a/envs/feed b/envs/feed
index 60f82c3..1a2464e 100644
--- a/envs/feed
+++ b/envs/feed
@@ -3,5 +3,5 @@ FEEDGEN_LISTENHOST=0.0.0.0
FEEDGEN_SQLITE_LOCATION=/data/db.sqlite
FEEDGEN_HOSTNAME=feed.syu.is
FEEDGEN_PUBLISHER_DID=did:plc:6qyecktefllvenje24fcxnie
-FEEDGEN_SUBSCRIPTION_ENDPOINT=ws://bgs:2470
FEEDGEN_SERVICE_DID=did:web:feed.syu.is
+FEEDGEN_JETSTREAM_URL=ws://jetstream:6008/subscribe
diff --git a/envs/jetstream b/envs/jetstream
index 324bbd7..950a3c9 100644
--- a/envs/jetstream
+++ b/envs/jetstream
@@ -1,4 +1,4 @@
-JETSTREAM_WS_URL=wss://bgs.${host}/xrpc/com.atproto.sync.subscribeRepos
+JETSTREAM_WS_URL=ws://bgs.${host}/xrpc/com.atproto.sync.subscribeRepos
JETSTREAM_DATA_DIR=/data
JETSTREAM_LISTEN_ADDR=:6008
JETSTREAM_METRICS_LISTEN_ADDR=:6009
diff --git a/html/about/support/app.html b/html/about/support/app.html
new file mode 100644
index 0000000..241d905
--- /dev/null
+++ b/html/about/support/app.html
@@ -0,0 +1,135 @@
+
+
+
+
+
+ App Info - Aiat
+
+
+
+
+
+
+
+
+
+
Aiat is a social networking application based on AT Protocol. Connect with your community on syu.is.
+
+
+
+
App Information
+
+
+
+
+
Supported OS
+
iOS 26.0+
+
+
+
+
+
+
+
+
+
Bitcoin
+
+ ₿
+ 3BqHXxraZyBapyNpJmniJDh9zqzuB8aoRr
+ copy
+
+
+
+
+
+
+
+
diff --git a/html/about/support/help.html b/html/about/support/help.html
new file mode 100644
index 0000000..72da246
--- /dev/null
+++ b/html/about/support/help.html
@@ -0,0 +1,100 @@
+
+
+
+
+
+ Help - syu.is
+
+
+
+
+
+
+ About syu.is
+ syu.is is a social networking service built on the AT Protocol (Authenticated Transfer Protocol). It allows users to share content, connect with others, and participate in a decentralized social network.
+
+ Frequently Asked Questions
+
+
+
What is the AT Protocol?
+
The AT Protocol is a decentralized social networking protocol that allows users to own their data and identity. It enables federation between different services while maintaining user control.
+
+
+
+
How do I create an account?
+
You can create an account by downloading the app or visiting the website. You'll need to provide an email address and choose a username.
+
+
+
+
How do I reset my password?
+
You can reset your password through the login screen by selecting "Forgot Password" and following the instructions sent to your email.
+
+
+
+
How do I delete my account?
+
You can delete your account through Settings > Account. Please note that account deletion is permanent and cannot be undone.
+
+
+
+
How do I report abuse or inappropriate content?
+
You can report content by using the report function available on each post. Our moderation team will review reports and take appropriate action.
+
+
+ Contact
+
+
+ Related Links
+
+
+
+
+
diff --git a/html/about/support/license.html b/html/about/support/license.html
new file mode 100644
index 0000000..c61217a
--- /dev/null
+++ b/html/about/support/license.html
@@ -0,0 +1,66 @@
+
+
+
+
+
+ License - syu.is
+
+
+
+
+
+
+ Aiat (iOS/Android App)
+ This application is based on the Bluesky Social App, which is open source software.
+
+ Open Source Licenses
+ This app uses the following open source software:
+
+ Bluesky Social App
+ Licensed under the MIT License
+ https://github.com/bluesky-social/social-app
+
+ AT Protocol
+ Licensed under the MIT License / Apache 2.0
+ https://github.com/bluesky-social/atproto
+
+ Third Party Libraries
+ This application includes various third-party libraries, each with their own licenses. For a complete list, please see the application's source code repository.
+
+
+
+
diff --git a/html/about/support/privacy.html b/html/about/support/privacy.html
new file mode 100644
index 0000000..9ab6925
--- /dev/null
+++ b/html/about/support/privacy.html
@@ -0,0 +1,92 @@
+
+
+
+
+
+ Privacy Policy - syu.is
+
+
+
+
+
+
+ 1. Introduction
+ This Privacy Policy explains how syu.is collects, uses, and protects your personal information when you use our service.
+
+ 2. Information We Collect
+ We collect the following types of information:
+
+ Account Information: Email address, username, and profile information you provide
+ Content: Posts, messages, and other content you create on the platform
+ Usage Data: Information about how you interact with our service
+ Device Information: Browser type, operating system, and device identifiers
+
+
+ 3. How We Use Your Information
+ We use your information to:
+
+ Provide and maintain our service
+ Improve and personalize your experience
+ Communicate with you about the service
+ Ensure security and prevent abuse
+
+
+ 4. Data Sharing
+ As part of the AT Protocol federation, your public content may be shared with other servers in the network. We do not sell your personal information to third parties.
+
+ 5. Data Security
+ We implement appropriate security measures to protect your personal information. However, no method of transmission over the Internet is 100% secure.
+
+ 6. Your Rights
+ You have the right to:
+
+ Access your personal data
+ Request correction of your data
+ Request deletion of your account
+ Export your data
+
+
+ 7. Cookies
+ We use cookies and similar technologies to maintain your session and improve your experience.
+
+ 8. Changes to This Policy
+ We may update this Privacy Policy from time to time. We will notify you of any significant changes.
+
+ 9. Contact
+ For privacy-related questions, please visit our Help page .
+
+
+
+
diff --git a/html/about/support/tos.html b/html/about/support/tos.html
new file mode 100644
index 0000000..3784cb1
--- /dev/null
+++ b/html/about/support/tos.html
@@ -0,0 +1,84 @@
+
+
+
+
+
+ Terms of Service - syu.is
+
+
+
+
+
+
+ 1. Introduction
+ Welcome to syu.is. By using our service, you agree to these terms. Please read them carefully.
+
+ 2. Service Description
+ syu.is is a social networking service built on the AT Protocol. We provide a platform for users to share content and connect with others.
+
+ 3. User Responsibilities
+ As a user of syu.is, you agree to:
+
+ Provide accurate information when creating an account
+ Keep your account credentials secure
+ Not use the service for illegal activities
+ Respect other users and their content
+ Comply with applicable laws and regulations
+
+
+ 4. Content Guidelines
+ Users are responsible for the content they post. Prohibited content includes:
+
+ Illegal content
+ Harassment or abuse
+ Spam or misleading information
+ Content that violates others' rights
+
+
+ 5. Privacy
+ Your privacy is important to us. Please review our Privacy Policy to understand how we handle your data.
+
+ 6. Disclaimer
+ The service is provided "as is" without warranties of any kind. We are not liable for any damages arising from your use of the service.
+
+ 7. Changes to Terms
+ We may update these terms from time to time. Continued use of the service after changes constitutes acceptance of the new terms.
+
+ 8. Contact
+ For questions about these terms, please visit our Help page .
+
+
+
+
diff --git a/html/index.html b/html/index.html
new file mode 100644
index 0000000..a9f880a
--- /dev/null
+++ b/html/index.html
@@ -0,0 +1,135 @@
+
+
+
+
+
+ App Info - Aiat
+
+
+
+
+
+
+
+
+
+
Aiat is a social networking application based on AT Protocol. Connect with your community on syu.is.
+
+
+
+
App Information
+
+
+
+
+
Supported OS
+
iOS 26.0+
+
+
+
+
+
+
+
+
+
Bitcoin
+
+ ₿
+ 3BqHXxraZyBapyNpJmniJDh9zqzuB8aoRr
+ copy
+
+
+
+
+
+
+
+
diff --git a/html/static/app.png b/html/static/app.png
new file mode 100644
index 0000000..f8f45fb
Binary files /dev/null and b/html/static/app.png differ
diff --git a/html/static/favicon.png b/html/static/favicon.png
new file mode 100644
index 0000000..2227ba3
Binary files /dev/null and b/html/static/favicon.png differ
diff --git a/install.zsh b/install.zsh
index 1845aac..d5afe8e 100755
--- a/install.zsh
+++ b/install.zsh
@@ -52,6 +52,12 @@ function at-repos-env() {
name=${host%%.*}
domain=${host##*.}
dport=5000
+
+ typeset -A PINNED_COMMITS
+ PINNED_COMMITS=(
+ [indigo]="d49b454196351c988ceb5ce1f5e21b689487b5ab"
+ [atproto]="104e6ed37b0589cc000109dc76316be35b2257e1"
+ )
}
# Arrays for patch management
@@ -120,31 +126,23 @@ function at-repos-pull() {
cd $d
}
-function at-repos-social-app-ios-patch() {
- $d/ios/setup.zsh
+function at-repos-checkout-pinned() {
+ echo "🔒 Checking out pinned commits..."
+ cd $d/repos
+ for repo_name pinned_commit in ${(kv)PINNED_COMMITS}; do
+ if [ -n "$pinned_commit" ] && [ -d "$d/repos/$repo_name" ]; then
+ echo " 📌 $repo_name -> $pinned_commit"
+ cd $d/repos/$repo_name
+ git fetch origin
+ git checkout $pinned_commit
+ cd $d/repos
+ fi
+ done
+ cd $d
}
-function at-repos-social-app-avatar-write() {
- did_admin=did:plc:6qyecktefllvenje24fcxnie
- dt=$d/repos/social-app/src
- cd $dt
- grep -R syu.is .|cut -d : -f 1|sort -u|xargs sediment "s/syu.is/${host}/g"
- grep -R web.syu.is .|cut -d : -f 1|sort -u|xargs sediment "s/web.syu.is/web.${host}/g"
- f=$dt/lib/constants.ts
- sediment "s#export const BSKY_SERVICE = 'https://bsky.social'#export const BSKY_SERVICE = 'https://${host}'#g" $f
- sediment "s#export const BSKY_SERVICE_DID = 'did:web:bsky.social'#export const BSKY_SERVICE_DID = 'did:web:${host}'#g" $f
- sediment "s#export const PUBLIC_BSKY_SERVICE = 'https://public.api.bsky.app'#export const PUBLIC_BSKY_SERVICE = 'https://bsky.${host}'#g" $f
- sediment "s#export const PUBLIC_APPVIEW = 'https://api.bsky.app'#export const PUBLIC_APPVIEW = 'https://bsky.${host}'#g" $f
- sediment "s#export const PUBLIC_APPVIEW_DID = 'did:web:api.bsky.app'#export const PUBLIC_APPVIEW_DID = 'did:web:bsky.${host}'#g" $f
-
- f=$dt/view/com/util/UserAvatar.tsx
- curl -sL https://raw.githubusercontent.com/bluesky-social/social-app/refs/heads/main/src/view/com/util/UserAvatar.tsx -o $f
- sediment "s#/img/avatar/plain/#https://cdn.web.syu.is/img/avatar/plain/#g" $f
- sediment "s#/img/avatar_thumbnail/plain/#https://bsky.${host}/img/avatar/plain/#g" $f
- sediment "s#source={{uri: avatar}}#source={{ uri: hackModifyThumbnailPath(avatar, 1 > 0), }}#g" $f
- curl -sL https://raw.githubusercontent.com/bluesky-social/social-app/refs/heads/main/src/lib/strings/url-helpers.ts -o $dt/lib/strings/url-helpers.ts
- sediment "s#https://go.web.syu.is/redirect?u=\${encodeURIComponent(url)}#\${url}#g" $dt/lib/strings/url-helpers.ts
- grep -R $did_admin .|cut -d : -f 1|sort -u|xargs sediment "s/${did_admin}/${did}/g"
+function at-repos-social-app-ios-patch() {
+ $d/ios/setup.zsh
}
# Common patch function with status detection
@@ -340,6 +338,9 @@ function at-repos-build-docker-atproto() {
for ((i=1; i<=${#services}; i++)); do
service=${services[$i]}
docker compose build --no-cache $service
+ if [ "$service" = "ozone" ]; then
+ docker compose build --no-cache ${service}-web
+ fi
done
else
docker compose build --no-cache $1
@@ -361,12 +362,11 @@ function at-repos-push-reset() {
}
function at-repos-push-docker() {
- if [ -z "$1" ];then
- for ((i=1; i<=${#services}; i++)); do
- service=${services[$i]}
+ if [ -z "$1" ] || [ "$1" = "push" ]; then
+ for service in "${services[@]}"; do
docker tag at-${service}:latest localhost:${dport}/${service}:latest
docker push localhost:${dport}/${service}:latest
- if [ "$service" == "ozone" ];then
+ if [ "$service" = "ozone" ]; then
docker tag at-${service}-web:latest localhost:${dport}/${service}-web:latest
docker push localhost:${dport}/${service}-web:latest
fi
@@ -411,28 +411,55 @@ function at-repos-reset-bgs-db() {
echo "⚙️ Updating Slurp Config..."
docker exec -i $dp psql -U postgres -d bgs -c "UPDATE slurp_configs SET new_subs_disabled = false, new_pds_per_day_limit = 1000 WHERE id = 1;"
- echo "🔗 Registering Trusted Domain & Resetting Repos..."
+ # host=pds:3000
+ echo "🔗 Registering Trusted Domain..."
# Retry loop for addTrustedDomain as BGS might still be warming up
for i in {1..5}; do
if curl -f -X POST "https://bgs.${host}/admin/pds/addTrustedDomain?domain=${host}" -H "Authorization: Bearer ${BGS_ADMIN_KEY}"; then
+ echo ""
echo "✅ Trusted domain registered"
break
fi
- echo "Bot failed to contact BGS (attempt $i/5)... waiting 5s"
+ echo "Failed to contact BGS (attempt $i/5)... waiting 5s"
sleep 5
done
+ echo "🔗 Requesting PDS Crawl..."
+ # Request BGS to crawl the PDS - this registers the PDS and starts subscription
+ for i in {1..5}; do
+ result=$(curl -s -X POST "https://bgs.${host}/admin/pds/requestCrawl" \
+ -H "Authorization: Bearer ${BGS_ADMIN_KEY}" \
+ -H "Content-Type: application/json" \
+ -d "{\"hostname\":\"{$host}\"}" \
+ -w "%{http_code}" -o /dev/null)
+ if [ "$result" = "200" ]; then
+ echo "✅ PDS crawl requested successfully"
+ break
+ fi
+ echo "Failed to request crawl (attempt $i/5, status: $result)... waiting 5s"
+ sleep 5
+ done
+
+ echo "⏳ Waiting 5s for BGS to connect to PDS..."
+ sleep 5
+
+ echo "🔄 Triggering repo sync for existing users..."
for ((i=1; i<=${#handles}; i++)); do
handle=${handles[$i]}
- did=`curl -sL "https://${host}/xrpc/com.atproto.repo.describeRepo?repo=${handle}" |jq -r .did`
- if [ ! -z "$did" ] && [ "$did" != "null" ]; then
- echo "Resetting repo: $handle ($did)"
- curl -X POST "https://bgs.${host}/admin/repo/reset?did=${did}" \
- -H "Authorization: Bearer ${BGS_ADMIN_KEY}"
+ did=$(curl -sL "https://${host}/xrpc/com.atproto.repo.describeRepo?repo=${handle}" | jq -r .did)
+ if [ -n "$did" ] && [ "$did" != "null" ]; then
+ echo " Syncing repo: $handle ($did)"
+ # Use takedown=false to trigger a resync without actually taking down
+ curl -s -X POST "https://bgs.${host}/admin/repo/takedown?did=${did}&takedown=false" \
+ -H "Authorization: Bearer ${BGS_ADMIN_KEY}" || true
else
- echo "Skipping reset for $handle (DID not found)"
+ echo " Skipping $handle (DID not found)"
fi
done
+
+ echo ""
+ echo "✅ BGS reset complete!"
+ echo " PDS should now be subscribed and syncing repos."
}
function at-repos-feed-generator-start-push() {
@@ -543,8 +570,8 @@ case "`cat /etc/hostname`" in
*)
at-repos-clone
at-repos-pull
+ at-repos-checkout-pinned
at-repos-social-app-ios-patch
- #at-repos-social-app-avatar-write
at-repos-patch-apply-all
at-repos-ozone-patch
show-failed-patches
diff --git a/ios/.keep b/ios/.keep
deleted file mode 100644
index e69de29..0000000
diff --git a/ios/README.md b/ios/README.md
index dee5ce3..c08d5da 100644
--- a/ios/README.md
+++ b/ios/README.md
@@ -5,94 +5,5 @@ https://raw.githubusercontent.com/bluesky-social/social-app/refs/heads/main/LICE
2. "Bluesky"という名称を使用しないこと。アイコンの変更。リンクの変更
-3. selfhostでも動くこと。本来のsocial-appは動きませんので、これは不便なのでiosアプリに出品することにしました。なお、これはすでにpatchで実現しています。
-
-
-```sh
-$ ./install.zsh pull
-$ ./install.zsh patch
-$ ./ios/setup.zsh
-$ ./ios/preview.zsh
-```
-
-## App Storeへのアップロード
-
-```sh
-# 1. 事前にXcodeでサイニング設定を完了させる
-# 2. store.mobileprovision を repos/social-app/ に配置
-# 3. キーチェーンに AC_PASSWORD を登録
-
-$ cd /Users/syui/ai/at/ios
-$ ./build.sh
-```
-
-**必要な準備:**
-- Apple Distribution証明書: `Apple Distribution: syutaro inagaki (WN6KD5ZT49)`
-- App Store用Provisioning Profile: `store.mobileprovision`
-- App-Specific Password: キーチェーンに `AC_PASSWORD` として登録
-
-```sh
-# App-Specific Passwordの登録
-security add-generic-password -a "syui@syui.ai" -w "your-app-specific-password" -s "AC_PASSWORD"
-```
-
-## 実装済み
-
-1. 最初の画面で、webではちゃんと私のサイトのロゴが表示されていますが、ios モバイル版では、未だにBluesky (icon)です。アカウント作成、サインイン、が表示されています。
-
-2. 上のメニューバーにもBlueskyのロゴが表示されています。
-
-3. サインイン後のホスティングプロバイダーで中身はsyu.isですが、表示は"Bluesky Social"になっています。これをsyu.isに変更してください。ios/webでコードは異なります。
-
-4. チャット機能
-チャット機能は今回無効化するので、下メニューバーやプロフィール、設定画面に表示しないでください。
-
-5. 設定ボタン(左カラム)を押すと、フィードバック、ヘルプが表示されますが、非表示にしてください。
-
-6. 設定ボタン(左カラム)を押すと、フィード、リスト、保存済みの項目がありますが、これを削除してください。
-
-7. 設定ボタン(左カラム)を押すと、下に利用規約、プライバシーポリシーが表示されますが、リンクがbsky.socialです。
-- /about/support/privacy-policy
-- /about/support/tos
-このページを独自に作って表示してください。
-
-8. LOG 09:52:20 (logger) Poll latest failed {
- "feed": "following",
- "message": "Error: Could not find repo: did:plc:z72i7hdynmk6r22z27h6tvur"
-}
-
-9. LOG 10:24:03 (metric) router:navigate
- LOG 10:24:04 (dms-agent) init failed {
- "safeMessage": "could not resolve iss did"
-}
-
-9. 設定ボタン(左カラム)の一番下、利用規約やプライバシーポリシーが表示されいてるライセンスという項目を追加。ページを追加して、ライセンスの表示。
-https://github.com/bluesky-social/social-app
-https://raw.githubusercontent.com/bluesky-social/social-app/refs/heads/main/LICENSE
-
-10. アカウント作成時(create account)のページに"Having trouble?"で`Contact support`のリンクがありますが、これを削除してください。
-
-11. スタートページ、つまり、`Create account`, `Sign in`があるページの一番下にライセンスページへのリンクを追加してください。また、footerに`© syui`を表示してください。このページのタイトル下にある文字`What's up?`の項目は削除。
-
-12. スタートページのラインセンスリンクが機能しない。おそらくページ変遷に問題があるため。また、ライセンスページは上下が隠れて見えてしまうため、大きく上下に空間を開けること。
-
-13. 利用規約、プライバシーポリシーのページの言語が日本語で書かれています。ラインセンスと同様に、英語を基本とし、日本語訳をその下に表示してください。
-
-14. Settings/ 項目の非表示を追加。
-- Helpの非表示
-- Aboutのリンクを変更
-
-## 壊れた実装
-
-1. ログイン後のメイン画面、"Following"の項目(フィード)に表示されるものをシンプルにします。表示するのはFollowingのみで、以下のものを削除してください。
-- おすすめの削除
-- Discoverの削除
-- アカウントを探すの削除
-
-2. 年齢保証、年齢確認ページがでてくるのを削除。誕生日を入力する処理を削除。アプリ配布国は限定します。
-
-## 追加
-
-1. 生年月日をサインイン時に要求しないよう削除
-2. 検索で、Discoverを表示しない
+3. selfhostでも動くこと。これはすでにpatchで実現しています。
diff --git a/ios/assets/favicons/apple-touch-icon.png b/ios/assets/favicons/apple-touch-icon.png
new file mode 100644
index 0000000..143348f
Binary files /dev/null and b/ios/assets/favicons/apple-touch-icon.png differ
diff --git a/ios/assets/favicons/favicon-16x16.png b/ios/assets/favicons/favicon-16x16.png
new file mode 100644
index 0000000..07aa7a8
Binary files /dev/null and b/ios/assets/favicons/favicon-16x16.png differ
diff --git a/ios/assets/favicons/favicon-32x32.png b/ios/assets/favicons/favicon-32x32.png
new file mode 100644
index 0000000..2227ba3
Binary files /dev/null and b/ios/assets/favicons/favicon-32x32.png differ
diff --git a/ios/assets/favicons/favicon.png b/ios/assets/favicons/favicon.png
new file mode 100644
index 0000000..2227ba3
Binary files /dev/null and b/ios/assets/favicons/favicon.png differ
diff --git a/ios/build.sh b/ios/build.zsh
similarity index 55%
rename from ios/build.sh
rename to ios/build.zsh
index 59fbf46..a379cff 100755
--- a/ios/build.sh
+++ b/ios/build.zsh
@@ -5,13 +5,21 @@ SCRIPT_DIR=${0:a:h}
cd "$SCRIPT_DIR"
source .env
+function sediment() {
+ if [[ "$OSTYPE" == "darwin"* ]]; then
+ sed -i '' "$@"
+ else
+ sed -i "$@"
+ fi
+}
+
# 絶対パスに変換
REPO_DIR="$SCRIPT_DIR/../repos/social-app"
APP_NAME="Aiat"
WORKSPACE="$REPO_DIR/ios/${APP_NAME}.xcworkspace"
SCHEME="$APP_NAME"
BUILD_DIR="$REPO_DIR/build"
-MOBILEPROVISION="$SCRIPT_DIR/store.mobileprovision"
+MOBILEPROVISION="$REPO_DIR/embedded.mobileprovision"
ASSETS_DIR="$SCRIPT_DIR/assets"
echo "Running iOS preview workflow..."
@@ -41,6 +49,27 @@ else
fi
function cleanup_build {
+ # 1.8. Update package.json version (prevent App Store version conflict)
+ echo "1.8. Updating package.json version..."
+ if [ -n "$APP_VERSION" ]; then
+ # Use node to update version in package.json (already in REPO_DIR)
+ node -e "
+ const fs = require('fs');
+ const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
+ pkg.version = '$APP_VERSION';
+ fs.writeFileSync('./package.json', JSON.stringify(pkg, null, 2) + '\n');
+ "
+ echo " ✅ Set version to $APP_VERSION"
+ else
+ echo " ⚠️ APP_VERSION not set in .env"
+ fi
+
+ # 1.9. Update buildNumber (CFBundleVersion) with current timestamp
+ echo "1.9. Updating buildNumber..."
+ local build_number=$(date +%y%m%d%H%M%S)
+ sediment "s/buildNumber: '[0-9]*'/buildNumber: '${build_number}'/" "./app.config.js"
+ echo " ✅ Set buildNumber to $build_number"
+
# 2. Prebuild (Generate ios directory)
echo "2. Running Expo Prebuild..."
# Clean old ios folder to remove old entitlements/AppClip targets
@@ -55,16 +84,50 @@ function cleanup_build {
pod install
cd ..
- # 4. Signing (Manual Step)
- echo "4. Opening Xcode for Signing..."
+ # 4. Signing (Automated)
+ echo "4. Configuring Xcode Signing..."
XCODE_PROJ="ios/${APP_NAME}.xcodeproj"
- # Fallback search if variable name logic differs
if [ ! -d "$XCODE_PROJ" ]; then
XCODE_PROJ=$(find ios -name "*.xcodeproj" | head -n 1)
fi
+ PBXPROJ="$XCODE_PROJ/project.pbxproj"
- open "$XCODE_PROJ"
- read
+ # Set DEVELOPMENT_TEAM in pbxproj
+ if [ -n "$DEVELOPMENT_TEAM" ]; then
+ echo " Setting DEVELOPMENT_TEAM=$DEVELOPMENT_TEAM"
+ sediment "s/PRODUCT_BUNDLE_IDENTIFIER = /DEVELOPMENT_TEAM = $DEVELOPMENT_TEAM; PRODUCT_BUNDLE_IDENTIFIER = /g" "$PBXPROJ"
+ sediment "s/DEVELOPMENT_TEAM = \"\";/DEVELOPMENT_TEAM = $DEVELOPMENT_TEAM;/g" "$PBXPROJ"
+ sediment "s/DEVELOPMENT_TEAM = ;/DEVELOPMENT_TEAM = $DEVELOPMENT_TEAM;/g" "$PBXPROJ"
+ fi
+
+ # Create/Update entitlements file with App Group
+ ENTITLEMENTS_FILE="ios/${APP_NAME}/${APP_NAME}.entitlements"
+ if [ -n "$APP_GROUP" ]; then
+ echo " Setting APP_GROUP=$APP_GROUP"
+ cat > "$ENTITLEMENTS_FILE" << EOF
+
+
+
+
+ aps-environment
+ production
+ com.apple.security.application-groups
+
+ ${APP_GROUP}
+
+
+
+EOF
+ if ! grep -q "CODE_SIGN_ENTITLEMENTS" "$PBXPROJ"; then
+ sediment "s/DEVELOPMENT_TEAM = $DEVELOPMENT_TEAM;/DEVELOPMENT_TEAM = $DEVELOPMENT_TEAM; CODE_SIGN_ENTITLEMENTS = ${APP_NAME}\\/${APP_NAME}.entitlements;/g" "$PBXPROJ"
+ fi
+ fi
+
+ echo "✅ Signing configured automatically"
+
+ # (Old manual step - commented out)
+ # open "$XCODE_PROJ"
+ # read
}
case $1 in
@@ -80,11 +143,12 @@ mkdir -p "$BUILD_DIR"
# アーカイブ(詳細ログ出力)
xcodebuild -workspace "$WORKSPACE" \
- -scheme "$SCHEME" \
- -configuration Release \
- -archivePath "$BUILD_DIR/${APP_NAME}.xcarchive" \
- -allowProvisioningUpdates \
- archive 2>&1 | tee "$BUILD_DIR/build.log"
+ -scheme "$SCHEME" \
+ -configuration Release \
+ -archivePath "$BUILD_DIR/${APP_NAME}.xcarchive" \
+ -allowProvisioningUpdates \
+ DEVELOPMENT_TEAM="$DEVELOPMENT_TEAM" \
+ archive 2>&1 | tee "$BUILD_DIR/build.log"
# アーカイブ成功確認
if [ ! -d "$BUILD_DIR/${APP_NAME}.xcarchive" ]; then
diff --git a/ios/patching/002-social-app-ios-lib.patch b/ios/patching/002-social-app-ios-lib.patch
index cfa2931..ee04f7b 100644
--- a/ios/patching/002-social-app-ios-lib.patch
+++ b/ios/patching/002-social-app-ios-lib.patch
@@ -81,8 +81,7 @@ index 231447b4f..a44b3da05 100644
- 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot'
+ 'at://did:plc:6qyecktefllvenje24fcxnie/app.bsky.feed.generator/app'
export const VIDEO_FEED_URI =
-- 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/thevids'
-+ 'at://did:plc:6qyecktefllvenje24fcxnie/app.bsky.feed.generator/app'
+ 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/thevids'
export const STAGING_VIDEO_FEED_URI =
'at://did:plc:yofh3kx63drvfljkibw5zuxo/app.bsky.feed.generator/thevids'
export const VIDEO_FEED_URIS = [VIDEO_FEED_URI, STAGING_VIDEO_FEED_URI]
@@ -147,7 +146,7 @@ diff --git a/src/state/queries/feed.ts b/src/state/queries/feed.ts
index de1e92533..3d1566800 100644
--- a/src/state/queries/feed.ts
+++ b/src/state/queries/feed.ts
-@@ -201,14 +201,7 @@ export function useFeedSourceInfoQuery({uri}: {uri: string}) {
+@@ -201,14 +201,6 @@ export function useFeedSourceInfoQuery({uri}: {uri: string}) {
// for the ones we know need it
// -prf
export const KNOWN_AUTHED_ONLY_FEEDS = [
@@ -159,9 +158,8 @@ index de1e92533..3d1566800 100644
- 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/mutuals', // mutuals, by bluesky
- 'at://did:plc:q6gjnaw2blty4crticxkmujt/app.bsky.feed.generator/my-followers', // followers, by jaz
- 'at://did:plc:vpkhqolt662uhesyj6nxm7ys/app.bsky.feed.generator/followpics', // the gram, by why
-+ 'at://did:plc:6qyecktefllvenje24fcxnie/app.bsky.feed.generator/app', // app feed, by syu.is
]
-
+
type GetPopularFeedsOptions = {limit?: number; enabled?: boolean}
diff --git a/src/state/queries/preferences/index.ts b/src/state/queries/preferences/index.ts
index 0cf6ab546..399e592bc 100644
@@ -216,3 +214,4 @@ index 620382175..928480da2 100644
style={[a.text_md]}>
Discover
{' '}
+
diff --git a/ios/patching/005-social-app-ios-screens.patch b/ios/patching/005-social-app-ios-screens.patch
index 1853137..492e263 100644
--- a/ios/patching/005-social-app-ios-screens.patch
+++ b/ios/patching/005-social-app-ios-screens.patch
@@ -1,5 +1,5 @@
diff --git a/src/screens/Settings/AboutSettings.tsx b/src/screens/Settings/AboutSettings.tsx
-index 6b8257b91..35202224b 100644
+index 6b8257b91..48ba7909e 100644
--- a/src/screens/Settings/AboutSettings.tsx
+++ b/src/screens/Settings/AboutSettings.tsx
@@ -80,7 +80,7 @@ export function AboutSettingsScreen({}: Props) {
@@ -21,26 +21,25 @@ index 6b8257b91..35202224b 100644
diff --git a/src/screens/Takendown.tsx b/src/screens/Takendown.tsx
-index dd319a4c6..0e80f956a 100644
+index 77f219e55..53f5e0cc0 100644
--- a/src/screens/Takendown.tsx
+++ b/src/screens/Takendown.tsx
-@@ -223,11 +223,11 @@ export function Takendown() {
+@@ -217,10 +217,10 @@ export function Takendown() {
Your account was found to be in violation of the{' '}
-
+ style={[a.text_md, a.leading_normal]}>
- Bluesky Social Terms of Service
+ syu.is Terms of Service
-
+
. 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..0f583c915 100644
+index e058e2883..8daf41089 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -1,23 +1,16 @@
@@ -82,17 +81,42 @@ index e058e2883..0f583c915 100644
import {CustomFeedEmptyState} from '#/view/com/posts/CustomFeedEmptyState'
import {FollowingEmptyState} from '#/view/com/posts/FollowingEmptyState'
import {FollowingEndOfFeed} from '#/view/com/posts/FollowingEndOfFeed'
-@@ -39,97 +28,60 @@ import {NoFeedsPinned} from '#/screens/Home/NoFeedsPinned'
+@@ -39,97 +28,90 @@ import {NoFeedsPinned} from '#/screens/Home/NoFeedsPinned'
import * as Layout from '#/components/Layout'
import {useDemoMode} from '#/storage/hooks/demo-mode'
-+const DEFAULT_PINNED_FEEDS = [{
++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
@@ -105,7 +129,12 @@ index e058e2883..0f583c915 100644
+ const {data: pinnedFeedInfos} = usePinnedFeedsInfos()
+
+ const safePreferences = preferences || { feedViewPrefs: { lab_mergeFeedEnabled: false }, savedFeeds: [] } as any
-+ const safePinnedFeedInfos = pinnedFeedInfos || DEFAULT_PINNED_FEEDS
++ // 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) {
@@ -189,8 +218,7 @@ index e058e2883..0f583c915 100644
- const maybeSelectedFeed: FeedDescriptor | undefined = allFeeds[selectedIndex]
+ const maybeSelectedFeed = allFeeds[selectedIndex]
const requestNotificationsPermission = useRequestNotificationsPermission()
--
-+
+
useSetTitle(pinnedFeedInfos[selectedIndex]?.displayName)
useOTAUpdates()
-
@@ -208,7 +236,7 @@ index e058e2883..0f583c915 100644
if (selectedIndex !== lastPagerReportedIndexRef.current) {
lastPagerReportedIndexRef.current = selectedIndex
pagerRef.current?.setPage(selectedIndex)
-@@ -138,205 +90,43 @@ function HomeScreenReady({
+@@ -138,205 +120,43 @@ function HomeScreenReady({
const {hasSession} = useSession()
const setMinimalShellMode = useSetMinimalShellMode()
@@ -217,7 +245,8 @@ index e058e2883..0f583c915 100644
- setMinimalShellMode(false)
- }, [setMinimalShellMode]),
- )
--
++ useFocusEffect(React.useCallback(() => { setMinimalShellMode(false) }, [setMinimalShellMode]))
+
- useFocusEffect(
- useNonReactiveCallback(() => {
- if (maybeSelectedFeed) {
@@ -235,7 +264,13 @@ index e058e2883..0f583c915 100644
- (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
@@ -265,15 +300,6 @@ index e058e2883..0f583c915 100644
- },
- [setMinimalShellMode],
- )
-+ useFocusEffect(React.useCallback(() => { setMinimalShellMode(false) }, [setMinimalShellMode]))
-+
-+ const onPageSelected = React.useCallback((index) => {
-+ setMinimalShellMode(false)
-+ const maybeFeed = allFeeds[index]
-+ lastPagerReportedIndexRef.current = index
-+ setSelectedFeed(maybeFeed)
-+ }, [setSelectedFeed, setMinimalShellMode, allFeeds])
-+
+ const onPressSelected = React.useCallback(() => { emitSoftReset() }, [])
+ const onPageScrollStateChanged = React.useCallback((state) => {
+ 'worklet'
@@ -281,7 +307,10 @@ index e058e2883..0f583c915 100644
+ }, [setMinimalShellMode])
const [demoMode] = useDemoMode()
--
++ const renderTabBar = React.useCallback((props) => {
++ return
++ }, [onPressSelected, pinnedFeedInfos])
+
- const renderTabBar = React.useCallback(
- (props: RenderTabBarFnProps) => {
- if (demoMode) {
@@ -312,11 +341,16 @@ index e058e2883..0f583c915 100644
- const renderFollowingEmptyState = React.useCallback(() => {
- return
- }, [])
--
++ const renderFollowingEmptyState = React.useCallback(() => , [])
++ const renderCustomFeedEmptyState = React.useCallback(() => , [])
+
- const renderCustomFeedEmptyState = React.useCallback(() => {
- return
- }, [])
--
++ const homeFeedParams = React.useMemo(() => ({
++ mergeFeedEnabled: false, mergeFeedSources: []
++ }), [preferences])
+
- const homeFeedParams = React.useMemo(() => {
- return {
- mergeFeedEnabled: Boolean(preferences.feedViewPrefs.lab_mergeFeedEnabled),
@@ -368,17 +402,6 @@ index e058e2883..0f583c915 100644
- renderTabBar={renderTabBar}>
- {pinnedFeedInfos.length ? (
- pinnedFeedInfos.map((feedInfo, index) => {
-+ const renderTabBar = React.useCallback((props) => {
-+ return
-+ }, [onPressSelected, pinnedFeedInfos])
-+
-+ const renderFollowingEmptyState = React.useCallback(() => , [])
-+ const renderCustomFeedEmptyState = React.useCallback(() => , [])
-+
-+ const homeFeedParams = React.useMemo(() => ({
-+ mergeFeedEnabled: false, mergeFeedSources: []
-+ }), [preferences])
-+
+ return (
+
+ {pinnedFeedInfos.map((feedInfo, index) => {
@@ -447,7 +470,7 @@ index e058e2883..0f583c915 100644
-})
+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..228af4966 100644
+index a89eaadc4..1da393f03 100644
--- a/src/view/screens/PrivacyPolicy.tsx
+++ b/src/view/screens/PrivacyPolicy.tsx
@@ -1,52 +1,13 @@
@@ -509,7 +532,7 @@ index a89eaadc4..228af4966 100644
)
}
diff --git a/src/view/screens/TermsOfService.tsx b/src/view/screens/TermsOfService.tsx
-index d843c713c..c0b34c886 100644
+index d843c713c..b81767bd5 100644
--- a/src/view/screens/TermsOfService.tsx
+++ b/src/view/screens/TermsOfService.tsx
@@ -1,50 +1,13 @@
diff --git a/ios/patching/006-social-app-ios-shell.patch b/ios/patching/006-social-app-ios-shell.patch
index d2154d2..399df6f 100644
--- a/ios/patching/006-social-app-ios-shell.patch
+++ b/ios/patching/006-social-app-ios-shell.patch
@@ -1,311 +1,8 @@
-diff --git a/src/components/dialogs/BirthDateSettings.tsx b/src/components/dialogs/BirthDateSettings.tsx
-index 9915d0a2d..4ae51215d 100644
---- a/src/components/dialogs/BirthDateSettings.tsx
-+++ b/src/components/dialogs/BirthDateSettings.tsx
-@@ -163,7 +163,7 @@ function BirthdayInner({
-
- You must be at least 13 years old to use Bluesky. Read our{' '}
-
- Terms of Service
- {' '}
-diff --git a/src/components/dialogs/ServerInput.tsx b/src/components/dialogs/ServerInput.tsx
-index d7c02bb9f..fda1dfe4a 100644
---- a/src/components/dialogs/ServerInput.tsx
-+++ b/src/components/dialogs/ServerInput.tsx
-@@ -165,7 +165,7 @@ function DialogInner({
-
- Bluesky is an open network where you can choose your own
- provider. If you're new here, we recommend sticking with the
-- default Bluesky Social option.
-+ default syu.is option.
-
-
-
diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx
-index ed2a6cfb7..1dc7f9227 100644
+index f76147ccf..36b4d7de1 100644
--- a/src/view/shell/Drawer.tsx
+++ b/src/view/shell/Drawer.tsx
-@@ -1,60 +1,50 @@
--import React, {type ComponentProps, type JSX} from 'react'
--import {Linking, ScrollView, TouchableOpacity, View} from 'react-native'
--import {useSafeAreaInsets} from 'react-native-safe-area-context'
--import {msg, Plural, plural, Trans} from '@lingui/macro'
--import {useLingui} from '@lingui/react'
--import {StackActions, useNavigation} from '@react-navigation/native'
--
--import {useActorStatus} from '#/lib/actor-status'
--import {FEEDBACK_FORM_URL, HELP_DESK_URL} from '#/lib/constants'
--import {type PressableScale} from '#/lib/custom-animations/PressableScale'
--import {useNavigationTabState} from '#/lib/hooks/useNavigationTabState'
--import {getTabState, TabState} from '#/lib/routes/helpers'
--import {type NavigationProp} from '#/lib/routes/types'
--import {sanitizeHandle} from '#/lib/strings/handles'
--import {colors} from '#/lib/styles'
--import {isWeb} from '#/platform/detection'
--import {emitSoftReset} from '#/state/events'
--import {useKawaiiMode} from '#/state/preferences/kawaii'
--import {useUnreadNotifications} from '#/state/queries/notifications/unread'
--import {useProfileQuery} from '#/state/queries/profile'
--import {type SessionAccount, useSession} from '#/state/session'
--import {useSetDrawerOpen} from '#/state/shell'
--import {formatCount} from '#/view/com/util/numeric/format'
--import {UserAvatar} from '#/view/com/util/UserAvatar'
--import {NavSignupCard} from '#/view/shell/NavSignupCard'
--import {atoms as a, tokens, useTheme, web} from '#/alf'
--import {Button, ButtonIcon, ButtonText} from '#/components/Button'
--import {Divider} from '#/components/Divider'
-+import React, { type ComponentProps, type JSX } from 'react'
-+import { Linking, ScrollView, TouchableOpacity, View } from 'react-native'
-+import { useSafeAreaInsets } from 'react-native-safe-area-context'
-+import { msg, Plural, plural, Trans } from '@lingui/macro'
-+import { useLingui } from '@lingui/react'
-+import { StackActions, useNavigation } from '@react-navigation/native'
-+
-+import { useActorStatus } from '#/lib/actor-status'
-+import { FEEDBACK_FORM_URL, HELP_DESK_URL } from '#/lib/constants'
-+import { type PressableScale } from '#/lib/custom-animations/PressableScale'
-+import { useNavigationTabState } from '#/lib/hooks/useNavigationTabState'
-+import { getTabState, TabState } from '#/lib/routes/helpers'
-+import { type NavigationProp } from '#/lib/routes/types'
-+import { sanitizeHandle } from '#/lib/strings/handles'
-+import { colors } from '#/lib/styles'
-+import { isWeb } from '#/platform/detection'
-+import { emitSoftReset } from '#/state/events'
-+import { useKawaiiMode } from '#/state/preferences/kawaii'
-+import { useUnreadNotifications } from '#/state/queries/notifications/unread'
-+import { useProfileQuery } from '#/state/queries/profile'
-+import { type SessionAccount, useSession } from '#/state/session'
-+import { useSetDrawerOpen } from '#/state/shell'
-+import { formatCount } from '#/view/com/util/numeric/format'
-+import { UserAvatar } from '#/view/com/util/UserAvatar'
-+import { NavSignupCard } from '#/view/shell/NavSignupCard'
-+import { atoms as a, tokens, useTheme, web } from '#/alf'
-+import { Button } from '#/components/Button'
-+import { Divider } from '#/components/Divider'
- import {
- Bell_Filled_Corner0_Rounded as BellFilled,
- Bell_Stroke2_Corner0_Rounded as Bell,
- } from '#/components/icons/Bell'
--import {Bookmark, BookmarkFilled} from '#/components/icons/Bookmark'
--import {BulletList_Stroke2_Corner0_Rounded as List} from '#/components/icons/BulletList'
--import {
-- Hashtag_Filled_Corner0_Rounded as HashtagFilled,
-- Hashtag_Stroke2_Corner0_Rounded as Hashtag,
--} from '#/components/icons/Hashtag'
- import {
- HomeOpen_Filled_Corner0_Rounded as HomeFilled,
- HomeOpen_Stoke2_Corner0_Rounded as Home,
- } from '#/components/icons/HomeOpen'
--import {MagnifyingGlass_Filled_Stroke2_Corner0_Rounded as MagnifyingGlassFilled} from '#/components/icons/MagnifyingGlass'
--import {MagnifyingGlass2_Stroke2_Corner0_Rounded as MagnifyingGlass} from '#/components/icons/MagnifyingGlass2'
--import {
-- Message_Stroke2_Corner0_Rounded as Message,
-- Message_Stroke2_Corner0_Rounded_Filled as MessageFilled,
--} from '#/components/icons/Message'
--import {SettingsGear2_Stroke2_Corner0_Rounded as Settings} from '#/components/icons/SettingsGear2'
-+import { MagnifyingGlass_Filled_Stroke2_Corner0_Rounded as MagnifyingGlassFilled } from '#/components/icons/MagnifyingGlass'
-+import { MagnifyingGlass2_Stroke2_Corner0_Rounded as MagnifyingGlass } from '#/components/icons/MagnifyingGlass2'
-+import { SettingsGear2_Stroke2_Corner0_Rounded as Settings } from '#/components/icons/SettingsGear2'
- import {
- UserCircle_Filled_Corner0_Rounded as UserCircleFilled,
- UserCircle_Stroke2_Corner0_Rounded as UserCircle,
- } from '#/components/icons/UserCircle'
--import {InlineLinkText} from '#/components/Link'
--import {Text} from '#/components/Typography'
--import {useSimpleVerificationState} from '#/components/verification'
--import {VerificationCheck} from '#/components/verification/VerificationCheck'
-+import { InlineLinkText } from '#/components/Link'
-+import { Text } from '#/components/Typography'
-+import { useSimpleVerificationState } from '#/components/verification'
-+import { VerificationCheck } from '#/components/verification/VerificationCheck'
-
- const iconWidth = 26
-
-@@ -65,11 +55,11 @@ let DrawerProfileCard = ({
- account: SessionAccount
- onPressProfile: () => void
- }): React.ReactNode => {
-- const {_, i18n} = useLingui()
-+ const { _, i18n } = useLingui()
- const t = useTheme()
-- const {data: profile} = useProfileQuery({did: account.did})
-- const verification = useSimpleVerificationState({profile})
-- const {isActive: live} = useActorStatus(profile)
-+ const { data: profile } = useProfileQuery({ did: account.did })
-+ const verification = useSimpleVerificationState({ profile })
-+ const { isActive: live } = useActorStatus(profile)
-
- return (
- ): React.ReactNode => {
-+let DrawerContent = ({ }: React.PropsWithoutRef<{}>): React.ReactNode => {
- const t = useTheme()
- const insets = useSafeAreaInsets()
- const setDrawerOpen = useSetDrawerOpen()
-@@ -150,27 +139,20 @@ let DrawerContent = ({}: React.PropsWithoutRef<{}>): React.ReactNode => {
- const {
- isAtHome,
- isAtSearch,
-- isAtFeeds,
-- isAtBookmarks,
- isAtNotifications,
- isAtMyProfile,
-- isAtMessages,
- } = useNavigationTabState()
-- const {hasSession, currentAccount} = useSession()
--
-- // events
-- // =
-+ const { hasSession, currentAccount } = useSession()
-
- const onPressTab = React.useCallback(
- (tab: 'Home' | 'Search' | 'Messages' | 'Notifications' | 'MyProfile') => {
- const state = navigation.getState()
- setDrawerOpen(false)
- if (isWeb) {
-- // hack because we have flat navigator for web and MyProfile does not exist on the web navigator -ansh
- if (tab === 'MyProfile') {
-- navigation.navigate('Profile', {name: currentAccount!.handle})
-+ navigation.navigate('Profile', { name: currentAccount!.handle })
- } else {
-- // @ts-expect-error struggles with string unions, apparently
-+ // @ts-expect-error struggles with string unions
- navigation.navigate(tab)
- }
- } else {
-@@ -178,21 +160,11 @@ let DrawerContent = ({}: React.PropsWithoutRef<{}>): React.ReactNode => {
- if (tabState === TabState.InsideAtRoot) {
- emitSoftReset()
- } else if (tabState === TabState.Inside) {
-- // find the correct navigator in which to pop-to-top
-- const target = state.routes.find(route => route.name === `${tab}Tab`)
-- ?.state?.key
-+ const target = state.routes.find(route => route.name === `${tab}Tab`)?.state?.key
- if (target) {
-- // if we found it, trigger pop-to-top
-- navigation.dispatch({
-- ...StackActions.popToTop(),
-- target,
-- })
-+ navigation.dispatch({ ...StackActions.popToTop(), target })
- } else {
-- // fallback: reset navigation
-- navigation.reset({
-- index: 0,
-- routes: [{name: `${tab}Tab`}],
-- })
-+ navigation.reset({ index: 0, routes: [{ name: `${tab}Tab` }] })
- }
- } else {
- navigation.navigate(`${tab}Tab`)
-@@ -203,76 +175,21 @@ let DrawerContent = ({}: React.PropsWithoutRef<{}>): React.ReactNode => {
- )
-
- const onPressHome = React.useCallback(() => onPressTab('Home'), [onPressTab])
--
-- const onPressSearch = React.useCallback(
-- () => onPressTab('Search'),
-- [onPressTab],
-- )
--
-- const onPressMessages = React.useCallback(
-- () => onPressTab('Messages'),
-- [onPressTab],
-- )
--
-- const onPressNotifications = React.useCallback(
-- () => onPressTab('Notifications'),
-- [onPressTab],
-- )
--
-- const onPressProfile = React.useCallback(() => {
-- onPressTab('MyProfile')
-- }, [onPressTab])
--
-- const onPressMyFeeds = React.useCallback(() => {
-- navigation.navigate('Feeds')
-- setDrawerOpen(false)
-- }, [navigation, setDrawerOpen])
--
-- const onPressLists = React.useCallback(() => {
-- navigation.navigate('Lists')
-- setDrawerOpen(false)
-- }, [navigation, setDrawerOpen])
--
-- const onPressBookmarks = React.useCallback(() => {
-- navigation.navigate('Bookmarks')
-- setDrawerOpen(false)
-- }, [navigation, setDrawerOpen])
--
-+ const onPressSearch = React.useCallback(() => onPressTab('Search'), [onPressTab])
-+ const onPressNotifications = React.useCallback(() => onPressTab('Notifications'), [onPressTab])
-+ const onPressProfile = React.useCallback(() => { onPressTab('MyProfile') }, [onPressTab])
- const onPressSettings = React.useCallback(() => {
- navigation.navigate('Settings')
- setDrawerOpen(false)
- }, [navigation, setDrawerOpen])
-
-- const onPressFeedback = React.useCallback(() => {
-- Linking.openURL(
-- FEEDBACK_FORM_URL({
-- email: currentAccount?.email,
-- handle: currentAccount?.handle,
-- }),
-- )
-- }, [currentAccount])
--
-- const onPressHelp = React.useCallback(() => {
-- Linking.openURL(HELP_DESK_URL)
-- }, [])
--
-- // rendering
-- // =
--
- return (
-
-
-+ contentContainerStyle={[{ paddingTop: Math.max(insets.top + a.pt_xl.paddingTop, a.pt_xl.paddingTop) }]}>
-
- {hasSession && currentAccount ? (
- ): React.ReactNode => {
-
-
- )}
--
-
-
-
-@@ -292,17 +208,10 @@ let DrawerContent = ({}: React.PropsWithoutRef<{}>): React.ReactNode => {
+@@ -292,17 +292,11 @@ let DrawerContent = ({}: React.PropsWithoutRef<{}>): React.ReactNode => {
<>
@@ -314,7 +11,7 @@ index ed2a6cfb7..1dc7f9227 100644
isActive={isAtNotifications}
onPress={onPressNotifications}
/>
--
+
-
- ): React.ReactNode => {
- ) : (
- <>
-
--
-
- >
- )}
-@@ -322,69 +230,11 @@ let DrawerContent = ({}: React.PropsWithoutRef<{}>): React.ReactNode => {
-
-
-
--
--
-
- )
- }
- DrawerContent = React.memo(DrawerContent)
--export {DrawerContent}
--
--let DrawerFooter = ({
-- onPressFeedback,
-- onPressHelp,
--}: {
-- onPressFeedback: () => void
-- onPressHelp: () => void
--}): React.ReactNode => {
-- const {_} = useLingui()
-- const insets = useSafeAreaInsets()
-- return (
--
+@@ -357,17 +351,7 @@ let DrawerFooter = ({
+ ),
+ },
+ ]}>
- Feedback
-
-
--
--
-- Help
--
--
--
-- )
--}
--DrawerFooter = React.memo(DrawerFooter)
-+export { DrawerContent }
-
- interface MenuItemProps extends ComponentProps {
- icon: JSX.Element
-@@ -400,7 +250,7 @@ let SearchMenuItem = ({
- isActive: boolean
- onPress: () => void
- }): React.ReactNode => {
-- const {_} = useLingui()
-+ const { _ } = useLingui()
- const t = useTheme()
- return (
- void
- }): React.ReactNode => {
-- const {_} = useLingui()
-+ const { _ } = useLingui()
- const t = useTheme()
- return (
- void
--}): React.ReactNode => {
-- const {_} = useLingui()
-- const t = useTheme()
-- return (
--
-- ) : (
--
-- )
-- }
-- label={_(msg`Chat`)}
-- bold={isActive}
-- onPress={onPress}
-- />
-- )
--}
--ChatMenuItem = React.memo(ChatMenuItem)
--
- let NotificationsMenuItem = ({
- isActive,
- onPress,
-@@ -478,7 +302,7 @@ let NotificationsMenuItem = ({
- isActive: boolean
- onPress: () => void
- }): React.ReactNode => {
-- const {_} = useLingui()
-+ const { _ } = useLingui()
- const t = useTheme()
- const numUnreadNotifications = useUnreadNotifications()
- return (
-@@ -495,11 +319,11 @@ let NotificationsMenuItem = ({
- numUnreadNotifications === ''
- ? ''
- : _(
-- msg`${plural(numUnreadNotifications ?? 0, {
-- one: '# unread item',
-- other: '# unread items',
-- })}` || '',
-- )
-+ msg`${plural(numUnreadNotifications ?? 0, {
-+ one: '# unread item',
-+ other: '# unread items',
-+ })}` || '',
-+ )
- }
- count={numUnreadNotifications}
- bold={isActive}
-@@ -509,72 +333,6 @@ let NotificationsMenuItem = ({
- }
- NotificationsMenuItem = React.memo(NotificationsMenuItem)
-
--let FeedsMenuItem = ({
-- isActive,
-- onPress,
--}: {
-- isActive: boolean
-- onPress: () => void
--}): React.ReactNode => {
-- const {_} = useLingui()
-- const t = useTheme()
-- return (
--
-- ) : (
--
-- )
-- }
-- label={_(msg`Feeds`)}
-- bold={isActive}
-- onPress={onPress}
-- />
-- )
--}
--FeedsMenuItem = React.memo(FeedsMenuItem)
--
--let ListsMenuItem = ({onPress}: {onPress: () => void}): React.ReactNode => {
-- const {_} = useLingui()
-- const t = useTheme()
--
-- return (
-- }
-- label={_(msg`Lists`)}
-- onPress={onPress}
-- />
-- )
--}
--ListsMenuItem = React.memo(ListsMenuItem)
--
--let BookmarksMenuItem = ({
-- isActive,
-- onPress,
--}: {
-- isActive: boolean
-- onPress: () => void
--}): React.ReactNode => {
-- const {_} = useLingui()
-- const t = useTheme()
--
-- return (
--
-- ) : (
--
-- )
-- }
-- label={_(msg({message: 'Saved', context: 'link to bookmarks screen'}))}
-- onPress={onPress}
-- />
-- )
--}
--BookmarksMenuItem = React.memo(BookmarksMenuItem)
--
- let ProfileMenuItem = ({
- isActive,
- onPress,
-@@ -582,7 +340,7 @@ let ProfileMenuItem = ({
- isActive: boolean
- onPress: () => void
- }): React.ReactNode => {
-- const {_} = useLingui()
-+ const { _ } = useLingui()
- const t = useTheme()
- return (
- void}): React.ReactNode => {
-- const {_} = useLingui()
-+let SettingsMenuItem = ({ onPress }: { onPress: () => void }): React.ReactNode => {
-+ const { _ } = useLingui()
- const t = useTheme()
- return (
- void}): React.ReactNode => {
- }
- SettingsMenuItem = React.memo(SettingsMenuItem)
-
--function MenuItem({icon, label, count, bold, onPress}: MenuItemProps) {
-+function MenuItem({ icon, label, count, bold, onPress }: MenuItemProps) {
- const t = useTheme()
- return (
-
-- {({hovered, pressed}) => (
-+ {({ hovered, pressed }) => (
-
- ()
-
- return (
-
--
-- Terms of Service
--
--
+ Terms of Service
+
+
-- Privacy Policy
--
-- {kawaii && (
--
--
-- Logo by{' '}
--
-- @sawaratsuki.bsky.social
--
--
-+ Linking.openURL('https://syu.is/about/support/tos')}>
-+
-+ Terms of Service
-
-- )}
-+
-+ Linking.openURL('https://syu.is/about/support/privacy-policy')}>
-+
-+ Privacy Policy
-+
-+
-+ navigation.navigate('License')}>
-+
-+ License
-+
-+
-
- )
- }
-diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx
-index 779ebda68..bfd9b70fa 100644
---- a/src/view/shell/bottom-bar/BottomBar.tsx
-+++ b/src/view/shell/bottom-bar/BottomBar.tsx
-@@ -198,38 +198,6 @@ export function BottomBar({navigation}: BottomTabBarProps) {
- accessibilityLabel={_(msg`Search`)}
- accessibilityHint=""
- />
--
-- ) : (
--
-- )
-- }
-- onPress={onPressMessages}
-- notificationCount={numUnreadMessages.numUnread}
-- hasNew={numUnreadMessages.hasNew}
-- accessible={true}
-- accessibilityRole="tab"
-- accessibilityLabel={_(msg`Chat`)}
-- accessibilityHint={
-- numUnreadMessages.count > 0
-- ? _(
-- msg`${plural(numUnreadMessages.numUnread ?? 0, {
-- one: '# unread item',
-- other: '# unread items',
-- })}` || '',
-- )
-- : ''
-- }
-- />
-
- )}
-
- {_(msg`Privacy`)}
-
- {' • '}
-
- {_(msg`Terms`)}
-
++ to="https://syu.is/about/support/privacy-policy"
+ label={_(msg`Privacy Policy`)}>
+ Privacy Policy
+
diff --git a/ios/patching/007-social-app-ios-misc.patch b/ios/patching/007-social-app-ios-misc.patch
index 40e6be4..7dc2352 100644
--- a/ios/patching/007-social-app-ios-misc.patch
+++ b/ios/patching/007-social-app-ios-misc.patch
@@ -4,119 +4,13 @@ index 6a00cfd23..f91decc08 100644
+++ b/plugins/notificationsExtension/withNotificationsExtension.js
@@ -10,7 +10,7 @@ const EXTENSION_NAME = 'BlueskyNSE'
const EXTENSION_CONTROLLER_NAME = 'NotificationService'
-
+
const withNotificationsExtension = config => {
- const soundFiles = ['dm.aiff']
+ const soundFiles = []
-
+
return withPlugins(config, [
// IOS
-diff --git a/src/ageAssurance/util.ts b/src/ageAssurance/util.ts
-index 104328330..c992a21de 100644
---- a/src/ageAssurance/util.ts
-+++ b/src/ageAssurance/util.ts
-@@ -2,87 +2,32 @@ import {useMemo} from 'react'
- import {
- ageAssuranceRuleIDs as ids,
- type AppBskyAgeassuranceDefs,
-- getAgeAssuranceRegionConfig,
- } from '@atproto/api'
--
--import {getAge} from '#/lib/strings/time'
--import {useAgeAssuranceDataContext} from '#/ageAssurance/data'
- import {AgeAssuranceAccess} from '#/ageAssurance/types'
- import {type Geolocation, useGeolocation} from '#/geolocation'
-+import {useAgeAssuranceDataContext} from '#/ageAssurance/data'
-
--const DEFAULT_MIN_AGE = 13
--
--/**
-- * Get age assurance region config based on geolocation, with fallback to
-- * app defaults if no region config is found.
-- *
-- * See {@link getAgeAssuranceRegionConfig} for the generic option, which can
-- * return undefined if the geolocation does not match any AA region.
-- */
- export function getAgeAssuranceRegionConfigWithFallback(
- config: AppBskyAgeassuranceDefs.Config,
- geolocation: Geolocation,
- ): AppBskyAgeassuranceDefs.ConfigRegion {
-- const region = getAgeAssuranceRegionConfig(config, {
-- countryCode: geolocation.countryCode ?? '',
-- regionCode: geolocation.regionCode,
-- })
--
-- return (
-- region || {
-- countryCode: '*',
-- regionCode: undefined,
-- rules: [
-- {
-- $type: ids.IfDeclaredOverAge,
-- age: DEFAULT_MIN_AGE,
-- access: AgeAssuranceAccess.Full,
-- },
-- {
-- $type: ids.Default,
-- access: AgeAssuranceAccess.None,
-- },
-- ],
-- }
-- )
-+ return {
-+ countryCode: '*',
-+ regionCode: undefined,
-+ rules: [{ $type: ids.Default, access: AgeAssuranceAccess.Full }],
-+ }
- }
-
--/**
-- * Hook to get the age assurance region config based on current geolocation.
-- * Does not fall-back to our app defaults. If no config is found, returns
-- * undefined, which indicates no regional age assurance rules apply.
-- */
- export function useAgeAssuranceRegionConfig() {
- const geolocation = useGeolocation()
- const {config} = useAgeAssuranceDataContext()
-- return useMemo(() => {
-- if (!config) return
-- // use generic helper, we want to potentially return undefined
-- return getAgeAssuranceRegionConfig(config, {
-- countryCode: geolocation.countryCode ?? '',
-- regionCode: geolocation.regionCode,
-- })
-- }, [config, geolocation])
-+ return useMemo(() => ({
-+ countryCode: '*',
-+ regionCode: undefined,
-+ rules: [{ $type: ids.Default, access: AgeAssuranceAccess.Full }],
-+ }), [config, geolocation])
- }
-
--/**
-- * Some users may have erroneously set their birth date to the current date
-- * if one wasn't set on their account. We previously didn't do validation on
-- * the bday dialog, and it defaulted to the current date. This bug _has_ been
-- * seen in production, so we need to check for it where possible.
-- */
--export function isLegacyBirthdateBug(birthDate: string) {
-- return ['2025', '2024', '2023'].includes((birthDate || '').slice(0, 4))
--}
--
--/**
-- * Returns whether the user is under the minimum age required to use the app.
-- * This applies to all regions.
-- */
--export function isUserUnderMinimumAge(birthDate: string) {
-- return getAge(new Date(birthDate)) < DEFAULT_MIN_AGE
--}
--
--export function isUserUnderAdultAge(birthDate: string) {
-- return getAge(new Date(birthDate)) < 18
--}
-+export function isLegacyBirthdateBug(birthDate: string) { return false }
-+export function isUserUnderMinimumAge(birthDate: string) { return false }
-+export function isUserUnderAdultAge(birthDate: string) { return false }
diff --git a/src/components/PolicyUpdateOverlay/updates/202508/index.tsx b/src/components/PolicyUpdateOverlay/updates/202508/index.tsx
index 8365057e8..59c8506a2 100644
--- a/src/components/PolicyUpdateOverlay/updates/202508/index.tsx
diff --git a/ios/patching/013-social-app-ios-settings-remove-help.patch b/ios/patching/013-social-app-ios-settings-remove-help.patch
index b81aaa0..03ca178 100644
--- a/ios/patching/013-social-app-ios-settings-remove-help.patch
+++ b/ios/patching/013-social-app-ios-settings-remove-help.patch
@@ -1,8 +1,35 @@
diff --git a/src/screens/Settings/Settings.tsx b/src/screens/Settings/Settings.tsx
-index 1703036b5..42baa462c 100644
+index 6b0e184c0..42b609c9e 100644
--- a/src/screens/Settings/Settings.tsx
+++ b/src/screens/Settings/Settings.tsx
-@@ -231,16 +231,6 @@ export function SettingsScreen({}: Props) {
+@@ -203,24 +203,8 @@ export function SettingsScreen({}: Props) {
+ Notifications
+
+
+-
+-
+-
+- Content and media
+-
+-
+- {isNative && findContactsEnabled && (
+-
+-
+-
+- Find friends from contacts
+-
+-
+- )}
++{/* Content and media removed for syu.is */}
++{/* Find friends from contacts removed for syu.is */}
+
+@@ -245,16 +229,6 @@ export function SettingsScreen({}: Props) {
Languages
diff --git a/ios/patching/022-social-app-ios-bskyweb-support-pages.patch b/ios/patching/022-social-app-ios-bskyweb-support-pages.patch
index 812885c..0c983ac 100644
--- a/ios/patching/022-social-app-ios-bskyweb-support-pages.patch
+++ b/ios/patching/022-social-app-ios-bskyweb-support-pages.patch
@@ -1,8 +1,8 @@
diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go
-index ec5261dee..c670cf75a 100644
+index 790f211ee..ec05a8bcd 100644
--- a/bskyweb/cmd/bskyweb/server.go
+++ b/bskyweb/cmd/bskyweb/server.go
-@@ -302,6 +302,12 @@ func serve(cctx *cli.Context) error {
+@@ -317,6 +317,12 @@ 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)
@@ -15,9 +15,9 @@ index ec5261dee..c670cf75a 100644
e.GET("/intent/compose", server.WebGeneric)
e.GET("/intent/verify-email", server.WebGeneric)
e.GET("/intent/age-assurance", server.WebGeneric)
-@@ -755,3 +761,33 @@ func (srv *Server) WebIpCC(c echo.Context) error {
- }
- return c.JSON(200, outResponse)
+@@ -825,3 +831,33 @@ func (srv *Server) serveSitemapRequest(c echo.Context, url, sitemapType string)
+
+ return nil
}
+
+// Handler for About TOS page (syu.is specific)
@@ -49,510 +49,3 @@ index ec5261dee..c670cf75a 100644
+ data := srv.NewTemplateContext()
+ return c.Render(http.StatusOK, "about-app.html", data)
+}
-diff --git a/bskyweb/templates/about-app.html b/bskyweb/templates/about-app.html
-new file mode 100644
-index 000000000..000000002
---- /dev/null
-+++ b/bskyweb/templates/about-app.html
-@@ -0,0 +1,135 @@
-+
-+
-+
-+
-+
-+ App Info - Aiat
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
Aiat is a social networking application based on AT Protocol. Connect with your community on syu.is.
-+
-+
-+
-+
App Information
-+
-+
-+
-+
-+
Supported OS
-+
iOS 26.0+
-+
-+
-+
-+
-+
-+
-+
Developer
-+
syui
-+
-+
-+
-+
-+
-+
Bitcoin
-+
-+ ₿
-+ 3BqHXxraZyBapyNpJmniJDh9zqzuB8aoRr
-+ copy
-+
-+
-+
-+
-+
-+
-+
-+
-diff --git a/bskyweb/templates/about-license.html b/bskyweb/templates/about-license.html
-new file mode 100644
-index 000000000..000000003
---- /dev/null
-+++ b/bskyweb/templates/about-license.html
-@@ -0,0 +1,66 @@
-+
-+
-+
-+
-+
-+ License - syu.is
-+
-+
-+
-+
-+
-+
-+ Aiat (iOS/Android App)
-+ This application is based on the Bluesky Social App, which is open source software.
-+
-+ Open Source Licenses
-+ This app uses the following open source software:
-+
-+ Bluesky Social App
-+ Licensed under the MIT License
-+ https://github.com/bluesky-social/social-app
-+
-+ AT Protocol
-+ Licensed under the MIT License / Apache 2.0
-+ https://github.com/bluesky-social/atproto
-+
-+ Third Party Libraries
-+ This application includes various third-party libraries, each with their own licenses. For a complete list, please see the application's source code repository.
-+
-+
-+
-+
-diff --git a/bskyweb/templates/about-help.html b/bskyweb/templates/about-help.html
-new file mode 100644
-index 000000000..d37db25c5
---- /dev/null
-+++ b/bskyweb/templates/about-help.html
-@@ -0,0 +1,91 @@
-+
-+
-+
-+
-+
-+ Help - syu.is
-+
-+
-+
-+
-+
-+
-+ About syu.is
-+ syu.is is a social networking service built on the AT Protocol (Authenticated Transfer Protocol). It allows users to share content, connect with others, and participate in a decentralized social network.
-+
-+ Frequently Asked Questions
-+
-+
-+
What is the AT Protocol?
-+
The AT Protocol is a decentralized social networking protocol that allows users to own their data and identity. It enables federation between different services while maintaining user control.
-+
-+
-+
-+
How do I create an account?
-+
You can create an account by downloading the app or visiting the website. You'll need to provide an email address and choose a username.
-+
-+
-+
-+
How do I reset my password?
-+
You can reset your password through the login screen by selecting "Forgot Password" and following the instructions sent to your email.
-+
-+
-+
-+
How do I delete my account?
-+
You can delete your account through Settings > Account. Please note that account deletion is permanent and cannot be undone.
-+
-+
-+
-+
How do I report abuse or inappropriate content?
-+
You can report content by using the report function available on each post. Our moderation team will review reports and take appropriate action.
-+
-+
-+ Contact
-+
-+
-+ Related Links
-+
-+
-+
-+
-+
-diff --git a/bskyweb/templates/about-privacy.html b/bskyweb/templates/about-privacy.html
-new file mode 100644
-index 000000000..14a1168ad
---- /dev/null
-+++ b/bskyweb/templates/about-privacy.html
-@@ -0,0 +1,83 @@
-+
-+
-+
-+
-+
-+ Privacy Policy - syu.is
-+
-+
-+
-+
-+
-+
-+ 1. Introduction
-+ This Privacy Policy explains how syu.is collects, uses, and protects your personal information when you use our service.
-+
-+ 2. Information We Collect
-+ We collect the following types of information:
-+
-+ Account Information: Email address, username, and profile information you provide
-+ Content: Posts, messages, and other content you create on the platform
-+ Usage Data: Information about how you interact with our service
-+ Device Information: Browser type, operating system, and device identifiers
-+
-+
-+ 3. How We Use Your Information
-+ We use your information to:
-+
-+ Provide and maintain our service
-+ Improve and personalize your experience
-+ Communicate with you about the service
-+ Ensure security and prevent abuse
-+
-+
-+ 4. Data Sharing
-+ As part of the AT Protocol federation, your public content may be shared with other servers in the network. We do not sell your personal information to third parties.
-+
-+ 5. Data Security
-+ We implement appropriate security measures to protect your personal information. However, no method of transmission over the Internet is 100% secure.
-+
-+ 6. Your Rights
-+ You have the right to:
-+
-+ Access your personal data
-+ Request correction of your data
-+ Request deletion of your account
-+ Export your data
-+
-+
-+ 7. Cookies
-+ We use cookies and similar technologies to maintain your session and improve your experience.
-+
-+ 8. Changes to This Policy
-+ We may update this Privacy Policy from time to time. We will notify you of any significant changes.
-+
-+ 9. Contact
-+ For privacy-related questions, please visit our Help page .
-+
-+
-+
-+
-diff --git a/bskyweb/templates/about-tos.html b/bskyweb/templates/about-tos.html
-new file mode 100644
-index 000000000..db5d82f5c
---- /dev/null
-+++ b/bskyweb/templates/about-tos.html
-@@ -0,0 +1,76 @@
-+
-+
-+
-+
-+
-+ Terms of Service - syu.is
-+
-+
-+
-+
-+
-+
-+ 1. Introduction
-+ Welcome to syu.is. By using our service, you agree to these terms. Please read them carefully.
-+
-+ 2. Service Description
-+ syu.is is a social networking service built on the AT Protocol. We provide a platform for users to share content and connect with others.
-+
-+ 3. User Responsibilities
-+ As a user of syu.is, you agree to:
-+
-+ Provide accurate information when creating an account
-+ Keep your account credentials secure
-+ Not use the service for illegal activities
-+ Respect other users and their content
-+ Comply with applicable laws and regulations
-+
-+
-+ 4. Content Guidelines
-+ Users are responsible for the content they post. Prohibited content includes:
-+
-+ Illegal content
-+ Harassment or abuse
-+ Spam or misleading information
-+ Content that violates others' rights
-+
-+
-+ 5. Privacy
-+ Your privacy is important to us. Please review our Privacy Policy to understand how we handle your data.
-+
-+ 6. Disclaimer
-+ The service is provided "as is" without warranties of any kind. We are not liable for any damages arising from your use of the service.
-+
-+ 7. Changes to Terms
-+ We may update these terms from time to time. Continued use of the service after changes constitutes acceptance of the new terms.
-+
-+ 8. Contact
-+ For questions about these terms, please visit our Help page .
-+
-+
-+
-+
diff --git a/ios/patching/026-social-app-ios-serverinput-label.patch b/ios/patching/026-social-app-ios-serverinput-label.patch
deleted file mode 100644
index 08a3264..0000000
--- a/ios/patching/026-social-app-ios-serverinput-label.patch
+++ /dev/null
@@ -1,15 +0,0 @@
-diff --git a/src/components/dialogs/ServerInput.tsx b/src/components/dialogs/ServerInput.tsx
---- a/src/components/dialogs/ServerInput.tsx
-+++ b/src/components/dialogs/ServerInput.tsx
-@@ -144,9 +144,9 @@
-
-+ label={_(msg`syu.is`)}>
-
-- {_(msg`Bluesky`)}
-+ {_(msg`syu.is`)}
-
-
- 0
++ const showFeedsTab = feedGenCount > 0
+ const starterPackCount = profile.associated?.starterPacks || 0
+- const showStarterPacksTab = isMe || starterPackCount > 0
++ const showStarterPacksTab = false
+ // subtract starterpack count from list count, since starterpacks are a type of list
+ const listCount = (profile.associated?.lists || 0) - starterPackCount
+- const showListsTab = hasSession && (isMe || listCount > 0)
++ const showListsTab = false
+
+ const sectionTitles = [
diff --git a/ios/patching/036-social-app-ios-homeheader-loggedout.patch b/ios/patching/036-social-app-ios-homeheader-loggedout.patch
new file mode 100644
index 0000000..a978ef6
--- /dev/null
+++ b/ios/patching/036-social-app-ios-homeheader-loggedout.patch
@@ -0,0 +1,28 @@
+diff --git a/src/view/com/home/HomeHeader.tsx b/src/view/com/home/HomeHeader.tsx
+--- a/src/view/com/home/HomeHeader.tsx
++++ b/src/view/com/home/HomeHeader.tsx
+@@ -3,7 +3,6 @@ import {useNavigation} from '@react-navigation/native'
+
+ import {type NavigationProp} from '#/lib/routes/types'
+ import {type FeedSourceInfo} from '#/state/queries/feed'
+-import {useSession} from '#/state/session'
+ import {type RenderTabBarFnProps} from '#/view/com/pager/Pager'
+ import {TabBar} from '../pager/TabBar'
+ import {HomeHeaderLayout} from './HomeHeaderLayout'
+@@ -16,17 +15,15 @@ export function HomeHeader(
+ ) {
+ const {feeds, onSelect: onSelectProp} = props
+- const {hasSession} = useSession()
+ const navigation = useNavigation()
+
+ const hasPinnedCustom = React.useMemo(() => {
+- if (!hasSession) return false
+ return feeds.some(tab => {
+ const isFollowing = tab.uri === 'following'
+ return !isFollowing
+ })
+- }, [feeds, hasSession])
++ }, [feeds])
+
+ const items = React.useMemo(() => {
+ const pinnedNames = feeds.map(f => f.displayName)
diff --git a/ios/patching/037-social-app-ios-disable-contacts-nux.patch b/ios/patching/037-social-app-ios-disable-contacts-nux.patch
new file mode 100644
index 0000000..0fddde3
--- /dev/null
+++ b/ios/patching/037-social-app-ios-disable-contacts-nux.patch
@@ -0,0 +1,13 @@
+diff --git a/src/components/dialogs/nuxs/index.tsx b/src/components/dialogs/nuxs/index.tsx
+index 63e11a7f4..70fa993cf 100644
+--- a/src/components/dialogs/nuxs/index.tsx
++++ b/src/components/dialogs/nuxs/index.tsx
+@@ -46,7 +46,7 @@ const queuedNuxs: {
+ enabled: ({currentProfile}) => {
+ return (
+ isNative &&
+- isExistingUserAsOf('2025-12-16T00:00:00.000Z', currentProfile.createdAt)
++ isExistingUserAsOf('2099-12-16T00:00:00.000Z', currentProfile.createdAt)
+ )
+ },
+ },
diff --git a/ios/patching/AppInfo.tsx b/ios/patching/AppInfo.tsx
new file mode 100644
index 0000000..f762515
--- /dev/null
+++ b/ios/patching/AppInfo.tsx
@@ -0,0 +1,310 @@
+import React, {useState} from 'react'
+import {
+ View,
+ ScrollView,
+ StyleSheet,
+ Pressable,
+ Image,
+} from 'react-native'
+import * as Clipboard from 'expo-clipboard'
+import * as WebBrowser from 'expo-web-browser'
+import {Trans} from '@lingui/macro'
+
+import * as Layout from '#/components/Layout'
+import {Text} from '#/components/Typography'
+import {useSetTitle} from '#/lib/hooks/useSetTitle'
+import {atoms as a, useTheme} from '#/alf'
+
+const APP_VERSION = '1.111.0'
+const APP_NAME = 'Aiat'
+const BITCOIN_ADDRESS = '3BqHXxraZyBapyNpJmniJDh9zqzuB8aoRr'
+
+export function AppInfoScreen() {
+ useSetTitle('App Info')
+ const t = useTheme()
+ const [copied, setCopied] = useState(false)
+
+ const copyToClipboard = async (text: string) => {
+ try {
+ await Clipboard.setStringAsync(text)
+ setCopied(true)
+ setTimeout(() => setCopied(false), 2000)
+ } catch (e) {
+ console.log('Clipboard not available')
+ }
+ }
+
+ const openUrl = (url: string) => {
+ WebBrowser.openBrowserAsync(url)
+ }
+
+ return (
+
+
+
+
+
+ App Info
+
+
+
+
+
+
+ {/* App Header */}
+
+
+
+
+
+ {APP_NAME}
+
+
+ v{APP_VERSION}
+
+
+
+ {/* Description Section */}
+
+
+ {APP_NAME} is a social networking application based on AT Protocol.
+ Connect with your community on syu.is.
+
+
+
+ {/* App Information Section */}
+
+
+ App Information
+
+
+
+
+ Version
+
+
+ {APP_VERSION}
+
+
+
+
+ Category
+
+
+ Social
+
+
+
+
+ Supported OS
+
+
+ iOS 26.0+
+
+
+
+
+ Price
+
+
+ Free
+
+
+
+
+
+ {/* Developer Section */}
+
+
+ Developer
+
+
+ syui
+
+
+ openUrl('https://git.syui.ai/syui')}
+ style={[styles.linkRow, t.atoms.border_contrast_low]}>
+
+ Git
+
+
+ git.syui.ai/syui
+
+ →
+
+
+ openUrl('https://syu.is/syui')}
+ style={[styles.linkRow, t.atoms.border_contrast_low]}>
+
+ ATProto
+
+
+ syu.is/syui
+
+ →
+
+
+
+ {/* Bitcoin Section */}
+
+
+ Bitcoin
+
+ copyToClipboard(BITCOIN_ADDRESS)}
+ style={styles.bitcoinRow}>
+ ₿
+
+ {BITCOIN_ADDRESS}
+
+
+ {copied ? 'copied!' : 'copy'}
+
+
+
+
+ {/* Copyright */}
+
+
+ © syui
+
+
+
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ appHeader: {
+ alignItems: 'center',
+ marginBottom: 24,
+ },
+ appIconContainer: {
+ width: 80,
+ height: 80,
+ borderRadius: 18,
+ overflow: 'hidden',
+ marginBottom: 12,
+ },
+ appIcon: {
+ width: '100%',
+ height: '100%',
+ },
+ appName: {
+ fontSize: 24,
+ fontWeight: 'bold',
+ marginBottom: 4,
+ },
+ appVersion: {
+ fontSize: 14,
+ },
+ section: {
+ marginBottom: 16,
+ borderRadius: 16,
+ padding: 16,
+ },
+ sectionTitle: {
+ fontSize: 13,
+ fontWeight: '600',
+ textTransform: 'uppercase',
+ letterSpacing: 0.5,
+ marginBottom: 12,
+ },
+ description: {
+ fontSize: 15,
+ lineHeight: 22,
+ },
+ infoGrid: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ gap: 8,
+ },
+ infoItem: {
+ flex: 1,
+ minWidth: '45%',
+ alignItems: 'center',
+ borderRadius: 12,
+ padding: 12,
+ },
+ infoLabel: {
+ fontSize: 11,
+ textTransform: 'uppercase',
+ letterSpacing: 0.5,
+ marginBottom: 4,
+ },
+ infoValue: {
+ fontSize: 16,
+ fontWeight: '600',
+ textAlign: 'center',
+ },
+ developerCard: {
+ marginBottom: 12,
+ },
+ developerName: {
+ fontSize: 18,
+ fontWeight: '600',
+ },
+ linkRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingVertical: 12,
+ borderTopWidth: 1,
+ },
+ linkIcon: {
+ fontSize: 14,
+ fontWeight: '600',
+ width: 70,
+ },
+ linkValue: {
+ flex: 1,
+ fontSize: 14,
+ },
+ linkArrow: {
+ fontSize: 16,
+ },
+ bitcoinRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ backgroundColor: 'rgba(247, 147, 26, 0.08)',
+ borderRadius: 12,
+ padding: 14,
+ gap: 10,
+ },
+ bitcoinLabel: {
+ fontSize: 18,
+ fontWeight: '600',
+ color: '#f7931a',
+ },
+ bitcoinAddress: {
+ flex: 1,
+ fontSize: 11,
+ fontFamily: 'monospace',
+ },
+ copyHint: {
+ fontSize: 12,
+ color: '#999999',
+ minWidth: 50,
+ textAlign: 'right',
+ },
+ copiedHint: {
+ color: '#4CAF50',
+ fontWeight: '600',
+ },
+ copyright: {
+ alignItems: 'center',
+ marginTop: 20,
+ marginBottom: 20,
+ },
+ copyrightText: {
+ fontSize: 12,
+ },
+})
diff --git a/ios/preview.zsh b/ios/preview.zsh
index 070c575..3644670 100755
--- a/ios/preview.zsh
+++ b/ios/preview.zsh
@@ -5,6 +5,14 @@ cd $d
source $d/.env
+function sediment() {
+ if [[ "$OSTYPE" == "darwin"* ]]; then
+ sed -i '' "$@"
+ else
+ sed -i "$@"
+ fi
+}
+
#xcrun simctl uninstall booted $BUNDLE_ID
echo "Running iOS preview workflow..."
@@ -34,6 +42,26 @@ else
echo "⚠️ Warning: $ASSETS_DIR not found"
fi
+# 1.8. Update package.json version (prevent App Store version conflict)
+echo "1.8. Updating package.json version..."
+if [ -n "$APP_VERSION" ]; then
+ node -e "
+ const fs = require('fs');
+ const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
+ pkg.version = '$APP_VERSION';
+ fs.writeFileSync('./package.json', JSON.stringify(pkg, null, 2) + '\n');
+ "
+ echo " ✅ Set version to $APP_VERSION"
+else
+ echo " ⚠️ APP_VERSION not set in .env"
+fi
+
+# 1.9. Update buildNumber (CFBundleVersion) with current timestamp
+echo "1.9. Updating buildNumber..."
+build_number=$(date +%y%m%d%H%M%S)
+sediment "s/buildNumber: '[0-9]*'/buildNumber: '${build_number}'/" "./app.config.js"
+echo " ✅ Set buildNumber to $build_number"
+
# 2. Prebuild (Generate ios directory)
echo "2. Running Expo Prebuild..."
# Clean old ios folder to remove old entitlements/AppClip targets
@@ -48,25 +76,62 @@ cd ios
pod install
cd ..
-# 4. Signing (Manual Step)
-echo "4. Opening Xcode for Signing..."
+# 4. Signing (Automated)
+echo "4. Configuring Xcode Signing..."
XCODE_PROJ="ios/${APP_NAME}.xcodeproj"
-# Fallback search if variable name logic differs
if [ ! -d "$XCODE_PROJ" ]; then
XCODE_PROJ=$(find ios -name "*.xcodeproj" | head -n 1)
fi
+PBXPROJ="$XCODE_PROJ/project.pbxproj"
-open "$XCODE_PROJ"
-echo "========================================================"
-echo " [ACTION REQUIRED] "
-echo " Xcode opened ($XCODE_PROJ)."
-echo " 1. Go to 'Signing & Capabilities' tab."
-echo " 2. Select your Team."
-echo " 3. Verify 'App Clip' target is gone."
-echo " 4. Ensure no red errors exist."
-echo " Press ENTER here once you are done to continue building."
-echo "========================================================"
-read
+# Set DEVELOPMENT_TEAM in pbxproj
+if [ -n "$DEVELOPMENT_TEAM" ]; then
+ echo " Setting DEVELOPMENT_TEAM=$DEVELOPMENT_TEAM"
+ # Add DEVELOPMENT_TEAM to all build configurations
+ sediment "s/PRODUCT_BUNDLE_IDENTIFIER = /DEVELOPMENT_TEAM = $DEVELOPMENT_TEAM; PRODUCT_BUNDLE_IDENTIFIER = /g" "$PBXPROJ"
+ # Also set where it might already exist but be empty
+ sediment "s/DEVELOPMENT_TEAM = \"\";/DEVELOPMENT_TEAM = $DEVELOPMENT_TEAM;/g" "$PBXPROJ"
+ sediment "s/DEVELOPMENT_TEAM = ;/DEVELOPMENT_TEAM = $DEVELOPMENT_TEAM;/g" "$PBXPROJ"
+fi
+
+# Create/Update entitlements file with App Group
+ENTITLEMENTS_FILE="ios/${APP_NAME}/${APP_NAME}.entitlements"
+if [ -n "$APP_GROUP" ]; then
+ echo " Setting APP_GROUP=$APP_GROUP"
+ cat > "$ENTITLEMENTS_FILE" << EOF
+
+
+
+
+ aps-environment
+ development
+ com.apple.security.application-groups
+
+ ${APP_GROUP}
+
+
+
+EOF
+ # Add CODE_SIGN_ENTITLEMENTS to pbxproj if not present
+ if ! grep -q "CODE_SIGN_ENTITLEMENTS" "$PBXPROJ"; then
+ sediment "s/DEVELOPMENT_TEAM = $DEVELOPMENT_TEAM;/DEVELOPMENT_TEAM = $DEVELOPMENT_TEAM; CODE_SIGN_ENTITLEMENTS = ${APP_NAME}\\/${APP_NAME}.entitlements;/g" "$PBXPROJ"
+ fi
+fi
+
+echo "✅ Signing configured automatically"
+
+# (Old manual step - commented out)
+# open "$XCODE_PROJ"
+# echo "========================================================"
+# echo " [ACTION REQUIRED] "
+# echo " Xcode opened ($XCODE_PROJ)."
+# echo " 1. Go to 'Signing & Capabilities' tab."
+# echo " 2. Select your Team."
+# echo " 3. Verify 'App Clip' target is gone."
+# echo " 4. Ensure no red errors exist."
+# echo " Press ENTER here once you are done to continue building."
+# echo "========================================================"
+# read
# 5. Run
echo "5. Building and Running..."
diff --git a/ios/setup.zsh b/ios/setup.zsh
index a208933..cf3b058 100755
--- a/ios/setup.zsh
+++ b/ios/setup.zsh
@@ -38,11 +38,14 @@ PATCH_FILES_IOS=(
"023-social-app-ios-disable-dm.patch"
"024-social-app-ios-disable-external-services.patch"
"025-social-app-ios-bskyweb-title.patch"
- "026-social-app-ios-serverinput-label.patch"
"027-social-app-ios-remove-birthdate.patch"
"028-social-app-ios-remove-discover-feeds.patch"
"029-social-app-ios-remove-feeds-discover.patch"
"030-social-app-ios-appinfo.patch"
+ "032-social-app-ios-feed-loggedout.patch"
+ "033-social-app-ios-hide-profile-tabs.patch"
+ "036-social-app-ios-homeheader-loggedout.patch"
+ "037-social-app-ios-disable-contacts-nux.patch"
)
function ios-env() {
@@ -64,14 +67,6 @@ function apply-patch() {
pushd ${target_dir} > /dev/null
- # Check if patch is already applied (reverse dry-run succeeds)
- if patch -f --dry-run -p1 -R < ${patch_file} > /dev/null 2>&1; then
- echo "✅ Already applied - skipping"
- popd > /dev/null
- echo ""
- return 0
- fi
-
# Check if patch can be applied (forward dry-run succeeds)
if patch --dry-run -p1 < ${patch_file} > /dev/null 2>&1; then
echo "🔧 Applying patch..."
@@ -179,6 +174,25 @@ function ios-copy-new-files() {
echo "✅ Copied License.tsx"
fi
+ # Copy AppInfo.tsx
+ if [ -f "$patching_dir/AppInfo.tsx" ]; then
+ mkdir -p "$target_dir/src/view/screens"
+ cp "$patching_dir/AppInfo.tsx" "$target_dir/src/view/screens/AppInfo.tsx"
+ echo "✅ Copied AppInfo.tsx"
+ fi
+
+ # Copy pre-generated favicons for bskyweb
+ local favicon_src="$d/ios/assets/favicons"
+ local bskyweb_static="$target_dir/bskyweb/static"
+ if [ -d "$favicon_src" ] && [ -d "$bskyweb_static" ]; then
+ cp -f "$d/ios/assets/logo.png" "$bskyweb_static/app.png"
+ cp -f "$favicon_src/favicon.png" "$bskyweb_static/favicon.png"
+ cp -f "$favicon_src/favicon-16x16.png" "$bskyweb_static/favicon-16x16.png"
+ cp -f "$favicon_src/favicon-32x32.png" "$bskyweb_static/favicon-32x32.png"
+ cp -f "$favicon_src/apple-touch-icon.png" "$bskyweb_static/apple-touch-icon.png"
+ echo "✅ Copied favicons to bskyweb/static"
+ fi
+
echo ""
}
@@ -191,6 +205,53 @@ function ios-setup-clone() {
echo "Repository found: $target_dir"
}
+# Generate bskyweb templates from html/ source
+# html/ is the source of truth, bskyweb templates are generated
+function ios-generate-bskyweb-templates() {
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo "🌐 Generating bskyweb templates from html/..."
+
+ local html_src="$d/html/about/support"
+ local templates="$target_dir/bskyweb/templates"
+ local static_src="$d/html/static"
+ local static_out="$target_dir/bskyweb/static"
+
+ # Check if html source exists
+ if [ ! -d "$html_src" ]; then
+ echo "⚠️ html/about/support not found, skipping template generation"
+ return 1
+ fi
+
+ # Create output directory
+ mkdir -p "$templates"
+ mkdir -p "$static_out"
+
+ # Convert html/ to bskyweb templates
+ # Add {{ staticCDNHost }} prefix to /static/ paths
+ for html_file in privacy.html license.html tos.html help.html app.html; do
+ if [ -f "$html_src/$html_file" ]; then
+ local template_name="about-${html_file}"
+ sed 's|href="/static/|href="{{ staticCDNHost }}/static/|g; s|src="/static/|src="{{ staticCDNHost }}/static/|g' \
+ "$html_src/$html_file" > "$templates/$template_name"
+ fi
+ done
+
+ # Also generate about-app.html from index.html if exists
+ if [ -f "$d/html/index.html" ]; then
+ sed 's|href="/static/|href="{{ staticCDNHost }}/static/|g; s|src="/static/|src="{{ staticCDNHost }}/static/|g' \
+ "$d/html/index.html" > "$templates/about-app.html"
+ fi
+
+ # Copy static assets
+ if [ -d "$static_src" ]; then
+ cp -f "$static_src/"* "$static_out/" 2>/dev/null
+ fi
+
+ echo "✅ Generated bskyweb templates"
+ echo " - about-privacy.html, about-tos.html, etc."
+ echo ""
+}
+
function ios-setup-reset() {
echo "Resetting social-app repository..."
cd $target_dir
@@ -209,6 +270,7 @@ case "$1" in
ios-patch-apply-all
ios-restore-build-placeholder
ios-copy-new-files
+ ios-generate-bskyweb-templates
show-failed-patches
exit
;;
@@ -216,12 +278,18 @@ case "$1" in
ios-setup-reset
exit
;;
+ html)
+ # Generate bskyweb templates only (requires patches to be applied first)
+ ios-generate-bskyweb-templates
+ exit
+ ;;
*)
ios-setup-clone
ios-generate-build-number
ios-patch-apply-all
ios-restore-build-placeholder
ios-copy-new-files
+ ios-generate-bskyweb-templates
show-failed-patches
;;
esac
diff --git a/patching/200-feed-generator-custom.patch b/patching/200-feed-generator-custom.patch
index 8c68f4c..3ee2a6a 100644
--- a/patching/200-feed-generator-custom.patch
+++ b/patching/200-feed-generator-custom.patch
@@ -22,14 +22,31 @@ index 0000000..993c83d
+
+EXPOSE 3000
+CMD ["yarn", "start"]
+diff --git a/package.json b/package.json
+index 1431a9e..6a7c33c 100644
+--- a/package.json
++++ b/package.json
+@@ -23,9 +23,11 @@
+ "dotenv": "^16.0.3",
+ "express": "^4.18.2",
+ "kysely": "^0.27.4",
+- "multiformats": "^9.9.0"
++ "multiformats": "^9.9.0",
++ "ws": "^8.14.2"
+ },
+ "devDependencies": {
++ "@types/ws": "^8.5.10",
+ "@types/better-sqlite3": "^7.6.11",
+ "@types/express": "^4.17.17",
+ "@types/node": "^20.1.2",
diff --git a/scripts/publish.ts b/scripts/publish.ts
new file mode 100644
-index 0000000..966edcf
+index 0000000..044f1d9
--- /dev/null
+++ b/scripts/publish.ts
@@ -0,0 +1,64 @@
+import dotenv from 'dotenv'
-+import { AtpAgent, BlobRef, AppBskyFeedDefs } from '@atproto/api'
++import { AtpAgent, BlobRef } from '@atproto/api'
+import fs from 'fs/promises'
+import { ids } from '../src/lexicon/lexicons'
+
@@ -88,7 +105,7 @@ index 0000000..966edcf
+ },
+ })
+
-+ console.log('All done 🎉')
++ console.log('All done')
+}
+
+run()
@@ -152,12 +169,15 @@ index b7ee48a..102cb93 100644
export default algos
diff --git a/src/index.ts b/src/index.ts
-index c3bd006..1e7f0b5 100644
+index 7128525..40d985c 100644
--- a/src/index.ts
+++ b/src/index.ts
-@@ -24,6 +24,8 @@ const run = async () => {
+@@ -22,8 +22,10 @@ const run = async () => {
+ })
+ await server.start()
console.log(
- `🤖 running feed generator at http://${server.cfg.listenhost}:${server.cfg.port}`,
+- `🤖 running feed generator at http://${server.cfg.listenhost}:${server.cfg.port}`,
++ `running feed generator at http://${server.cfg.listenhost}:${server.cfg.port}`,
)
+ console.log('Supported algos:', Object.keys(require('./algos').default))
+ console.log('Publisher DID:', server.cfg.publisherDid)
@@ -165,7 +185,7 @@ index c3bd006..1e7f0b5 100644
const maybeStr = (val?: string) => {
diff --git a/src/methods/feed-generation.ts b/src/methods/feed-generation.ts
-index b887413..34c5148 100644
+index 0f4989e..17be062 100644
--- a/src/methods/feed-generation.ts
+++ b/src/methods/feed-generation.ts
@@ -10,7 +10,7 @@ export default function (server: Server, ctx: AppContext) {
@@ -177,18 +197,233 @@ index b887413..34c5148 100644
feedUri.collection !== 'app.bsky.feed.generator' ||
!algo
) {
+diff --git a/src/server.ts b/src/server.ts
+index c696749..9b9c382 100644
+--- a/src/server.ts
++++ b/src/server.ts
+@@ -6,7 +6,7 @@ import { createServer } from './lexicon'
+ import feedGeneration from './methods/feed-generation'
+ import describeGenerator from './methods/describe-generator'
+ import { createDb, Database, migrateToLatest } from './db'
+-import { FirehoseSubscription } from './subscription'
++import { JetstreamSubscription } from './subscription'
+ import { AppContext, Config } from './config'
+ import wellKnown from './well-known'
+
+@@ -14,25 +14,28 @@ export class FeedGenerator {
+ public app: express.Application
+ public server?: http.Server
+ public db: Database
+- public firehose: FirehoseSubscription
++ public jetstream: JetstreamSubscription
+ public cfg: Config
+
+ constructor(
+ app: express.Application,
+ db: Database,
+- firehose: FirehoseSubscription,
++ jetstream: JetstreamSubscription,
+ cfg: Config,
+ ) {
+ this.app = app
+ this.db = db
+- this.firehose = firehose
++ this.jetstream = jetstream
+ this.cfg = cfg
+ }
+
+ static create(cfg: Config) {
+ const app = express()
+ const db = createDb(cfg.sqliteLocation)
+- const firehose = new FirehoseSubscription(db, cfg.subscriptionEndpoint)
++
++ // Use Jetstream URL from env or default to internal jetstream service
++ const jetstreamUrl = process.env.FEEDGEN_JETSTREAM_URL || 'ws://jetstream:6008/subscribe'
++ const jetstream = new JetstreamSubscription(db, jetstreamUrl, cfg.subscriptionReconnectDelay)
+
+ const didCache = new MemoryCache()
+ const didResolver = new DidResolver({
+@@ -58,12 +61,12 @@ export class FeedGenerator {
+ app.use(server.xrpc.router)
+ app.use(wellKnown(ctx))
+
+- return new FeedGenerator(app, db, firehose, cfg)
++ return new FeedGenerator(app, db, jetstream, cfg)
+ }
+
+ async start(): Promise {
+ await migrateToLatest(this.db)
+- this.firehose.run(this.cfg.subscriptionReconnectDelay)
++ this.jetstream.run()
+ this.server = this.app.listen(this.cfg.port, this.cfg.listenhost)
+ await events.once(this.server, 'listening')
+ return this.server
diff --git a/src/subscription.ts b/src/subscription.ts
-index 0422a03..d591ef9 100644
+index 0422a03..7785982 100644
--- a/src/subscription.ts
+++ b/src/subscription.ts
-@@ -19,10 +19,6 @@ export class FirehoseSubscription extends FirehoseSubscriptionBase {
+@@ -1,49 +1,126 @@
+-import {
+- OutputSchema as RepoEvent,
+- isCommit,
+-} from './lexicon/types/com/atproto/sync/subscribeRepos'
+-import { FirehoseSubscriptionBase, getOpsByType } from './util/subscription'
+-
+-export class FirehoseSubscription extends FirehoseSubscriptionBase {
+- async handleEvent(evt: RepoEvent) {
+- if (!isCommit(evt)) return
+-
+- const ops = await getOpsByType(evt)
+-
+- // This logs the text of every post off the firehose.
+- // Just for fun :)
+- // Delete before actually using
+- for (const post of ops.posts.creates) {
+- console.log(post.record.text)
++import WebSocket from 'ws'
++import { Database } from './db'
++
++// Jetstream event types
++interface JetstreamEvent {
++ did: string
++ time_us: number
++ kind: 'commit' | 'identity' | 'account'
++ commit?: {
++ rev: string
++ operation: 'create' | 'update' | 'delete'
++ collection: string
++ rkey: string
++ record?: {
++ $type: string
++ text?: string
++ createdAt?: string
++ [key: string]: unknown
+ }
++ cid?: string
++ }
++}
- const postsToDelete = ops.posts.deletes.map((del) => del.uri)
- const postsToCreate = ops.posts.creates
+- const postsToDelete = ops.posts.deletes.map((del) => del.uri)
+- const postsToCreate = ops.posts.creates
- .filter((create) => {
- // only alf-related posts
- return create.record.text.toLowerCase().includes('alf')
- })
- .map((create) => {
- // map alf-related posts to a db row
- return {
+- .map((create) => {
+- // map alf-related posts to a db row
+- return {
+- uri: create.uri,
+- cid: create.cid,
+- indexedAt: new Date().toISOString(),
+- }
+- })
++export class JetstreamSubscription {
++ private ws: WebSocket | null = null
++ private cursor: number = 0
+
+- if (postsToDelete.length > 0) {
+- await this.db
+- .deleteFrom('post')
+- .where('uri', 'in', postsToDelete)
+- .execute()
++ constructor(
++ public db: Database,
++ public jetstreamUrl: string,
++ public reconnectDelay: number = 3000
++ ) {}
++
++ async run() {
++ await this.loadCursor()
++ this.connect()
++ }
++
++ private connect() {
++ const url = new URL(this.jetstreamUrl)
++ url.searchParams.set('wantedCollections', 'app.bsky.feed.post')
++ if (this.cursor > 0) {
++ url.searchParams.set('cursor', this.cursor.toString())
+ }
+- if (postsToCreate.length > 0) {
++
++ console.log(`Connecting to Jetstream: ${url.toString()}`)
++ this.ws = new WebSocket(url.toString())
++
++ this.ws.on('open', () => {
++ console.log('Connected to Jetstream')
++ })
++
++ this.ws.on('message', async (data: WebSocket.Data) => {
++ try {
++ const event: JetstreamEvent = JSON.parse(data.toString())
++ await this.handleEvent(event)
++ } catch (err) {
++ console.error('Failed to handle Jetstream message:', err)
++ }
++ })
++
++ this.ws.on('error', (err) => {
++ console.error('Jetstream WebSocket error:', err)
++ })
++
++ this.ws.on('close', () => {
++ console.log('Jetstream connection closed, reconnecting...')
++ setTimeout(() => this.connect(), this.reconnectDelay)
++ })
++ }
++
++ private async handleEvent(event: JetstreamEvent) {
++ if (event.kind !== 'commit' || !event.commit) return
++ if (event.commit.collection !== 'app.bsky.feed.post') return
++
++ const uri = `at://${event.did}/${event.commit.collection}/${event.commit.rkey}`
++
++ if (event.commit.operation === 'delete') {
++ await this.db.deleteFrom('post').where('uri', '=', uri).execute()
++ } else if (event.commit.operation === 'create' && event.commit.record) {
++ const text = event.commit.record.text || ''
++
++ // Filter: posts starting with / or @ai
++ if (!text.match(/^\/[a-z]/) && !text.match(/^@ai/)) {
++ return
++ }
++
++ console.log(`[post] ${event.did}: ${text.substring(0, 50)}...`)
++
+ await this.db
+ .insertInto('post')
+- .values(postsToCreate)
++ .values({
++ uri: uri,
++ cid: event.commit.cid || '',
++ indexedAt: new Date().toISOString(),
++ })
+ .onConflict((oc) => oc.doNothing())
+ .execute()
+ }
++
++ // Update cursor periodically
++ this.cursor = event.time_us
++ if (event.time_us % 20 === 0) {
++ await this.saveCursor()
++ }
++ }
++
++ private async loadCursor() {
++ const res = await this.db
++ .selectFrom('sub_state')
++ .selectAll()
++ .where('service', '=', 'jetstream')
++ .executeTakeFirst()
++ if (res) {
++ this.cursor = res.cursor
++ }
++ }
++
++ private async saveCursor() {
++ await this.db
++ .insertInto('sub_state')
++ .values({ service: 'jetstream', cursor: this.cursor })
++ .onConflict((oc) => oc.column('service').doUpdateSet({ cursor: this.cursor }))
++ .execute()
+ }
+ }
diff --git a/task.md b/task.md
deleted file mode 100644
index 3136801..0000000
--- a/task.md
+++ /dev/null
@@ -1,4 +0,0 @@
-## Rules & Constraints
-- Do not reformat existing code unless explicitly requested.
-- Maintain existing import styles (newlines, sorting).
-- Keep patches minimal (clean deltas).