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 0efae0e..daca568 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,6 @@ repos
.claude
deploy.yml
claude.md
+embedded.mobileprovision
+.env
+html.zip
diff --git a/README.md b/README.md
index 6b1798c..e11bdb2 100644
--- a/README.md
+++ b/README.md
@@ -65,3 +65,24 @@ $ curl -sL "syu.is/xrpc/com.atproto.repo.listRecords?repo=${handle}&collection=a
}
}
```
+
+## build
+
+```sh
+# build
+./install.zsh
+
+# build social-app
+./install.zsh pull;./install.zsh patch;./install.zsh build social-app;./install.zsh push social-app
+---
+# server
+./install.zsh
+---
+# social-app ios
+# https://appstoreconnect.apple.com/
+# https://developer.apple.com/account/resources/profiles/list
+./install.zsh pull;./ios/setup.zsh
+./ios/build.zsh
+```
+
+
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 58cdca6..d5afe8e 100755
--- a/install.zsh
+++ b/install.zsh
@@ -1,5 +1,23 @@
#!/bin/zsh
+# Sed compatibility wrapper
+function sediment() {
+ if [[ "$OSTYPE" == "darwin"* ]]; then
+ sed -i '' "$@"
+ else
+ sed -i "$@"
+ fi
+}
+
+
+# Patch compatibility wrapper
+function patchment() {
+ # -f : Force. Do not ask questions. (Standard in GNU and BSD patch)
+ # -N : Ignore patches that seem to be reversed or already applied (Forward)
+ # But we control these flags in the caller.
+ patch "$@"
+}
+
function at-repos-env() {
APP_PASSWORD=xxx
host=syu.is
@@ -34,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
@@ -82,6 +106,10 @@ function at-repos-pull() {
echo $repo
if [ -d $d/repos/${repo##*/} ];then
cd $d/repos/${repo##*/}
+ # Clean up before pull: reset changes, remove .orig files and untracked patch-created files
+ git checkout -- .
+ find . -name "*.orig" -type f -delete 2>/dev/null
+ git clean -fd 2>/dev/null
git stash -u
if ! git pull;then
rm -rf $d/repos/${repo##*/}
@@ -98,32 +126,24 @@ function at-repos-pull() {
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 sed -i "s/syu.is/${host}/g"
- grep -R web.syu.is .|cut -d : -f 1|sort -u|xargs sed -i "s/web.syu.is/web.${host}/g"
- f=$dt/lib/constants.ts
- sed -i "s#export const BSKY_SERVICE = 'https://bsky.social'#export const BSKY_SERVICE = 'https://${host}'#g" $f
- sed -i "s#export const BSKY_SERVICE_DID = 'did:web:bsky.social'#export const BSKY_SERVICE_DID = 'did:web:${host}'#g" $f
- sed -i "s#export const PUBLIC_BSKY_SERVICE = 'https://public.api.bsky.app'#export const PUBLIC_BSKY_SERVICE = 'https://bsky.${host}'#g" $f
- sed -i "s#export const PUBLIC_APPVIEW = 'https://api.bsky.app'#export const PUBLIC_APPVIEW = 'https://bsky.${host}'#g" $f
- sed -i "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/icons/Logotype.tsx
- o=$d/icons/Logotype.tsx
- cp -rf $o $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
- sed -i "s#/img/avatar/plain/#https://cdn.web.syu.is/img/avatar/plain/#g" $f
- sed -i "s#/img/avatar_thumbnail/plain/#https://bsky.${host}/img/avatar/plain/#g" $f
- sed -i "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
- sed -i "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 sed -i "s/${did_admin}/${did}/g"
+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-ios-patch() {
+ $d/ios/setup.zsh
+}
# Common patch function with status detection
function apply-patch() {
@@ -139,7 +159,8 @@ function apply-patch() {
pushd ${target_dir} > /dev/null
# Check if patch is already applied (reverse dry-run succeeds)
- if patch --dry-run -p1 -R < ${patch_file} > /dev/null 2>&1; then
+ # Use -f to force dry-run to fail instead of asking questions if unapplied
+ if patch -f --dry-run -p1 -R < ${patch_file} > /dev/null 2>&1; then
echo "โ
Already applied - skipping"
popd > /dev/null
echo ""
@@ -147,9 +168,9 @@ function apply-patch() {
fi
# Check if patch can be applied (forward dry-run succeeds)
- if patch --dry-run -p1 < ${patch_file} > /dev/null 2>&1; then
+ if patch -f --dry-run -p1 < ${patch_file} > /dev/null 2>&1; then
echo "๐ง Applying patch..."
- if patch -p1 < ${patch_file}; then
+ if patch -f -p1 < ${patch_file}; then
echo "โ
Applied successfully"
popd > /dev/null
echo ""
@@ -284,29 +305,29 @@ function at-repos-ozone-patch() {
fi
# Replace process.env with env()
- sed -i 's/process\.env\.\(NEXT_PUBLIC_[A-Z_]*\)/env('\''\1'\'')/g' lib/constants.ts 2>/dev/null || true
- sed -i 's/process\.env\.NODE_ENV/env('\''NODE_ENV'\'')/g' lib/constants.ts 2>/dev/null || true
+ sediment 's/process\.env\.\(NEXT_PUBLIC_[A-Z_]*\)/env('\''\1'\'')/g' lib/constants.ts 2>/dev/null || true
+ sediment 's/process\.env\.NODE_ENV/env('\''NODE_ENV'\'')/g' lib/constants.ts 2>/dev/null || true
# Add missing SOCIAL_APP_DOMAIN constant after SOCIAL_APP_URL
- sed -i '/^export const SOCIAL_APP_URL =/,/^$/{ /^$/a\
+ sediment '/^export const SOCIAL_APP_URL =/,/^$/{ /^$/a\
export const SOCIAL_APP_DOMAIN =\
env('\''NEXT_PUBLIC_SOCIAL_APP_DOMAIN'\'') || '\''bsky.app'\''\
}' lib/constants.ts 2>/dev/null || true
# Fix multiline process.env patterns
- sed -i '/^export const NEW_ACCOUNT_MARKER_THRESHOLD_IN_DAYS = process\.env$/,/^ : 7$/ {
+ sediment '/^export const NEW_ACCOUNT_MARKER_THRESHOLD_IN_DAYS = process\.env$/,/^ : 7$/ {
s/^export const NEW_ACCOUNT_MARKER_THRESHOLD_IN_DAYS = process\.env$/export const NEW_ACCOUNT_MARKER_THRESHOLD_IN_DAYS = env('\''NEXT_PUBLIC_NEW_ACCOUNT_MARKER_THRESHOLD_IN_DAYS'\'')/
/^ \.NEXT_PUBLIC_NEW_ACCOUNT_MARKER_THRESHOLD_IN_DAYS$/d
}' lib/constants.ts 2>/dev/null || true
- sed -i '/^export const YOUNG_ACCOUNT_MARKER_THRESHOLD_IN_DAYS = process\.env$/,/^ : 30$/ {
+ sediment '/^export const YOUNG_ACCOUNT_MARKER_THRESHOLD_IN_DAYS = process\.env$/,/^ : 30$/ {
s/^export const YOUNG_ACCOUNT_MARKER_THRESHOLD_IN_DAYS = process\.env$/export const YOUNG_ACCOUNT_MARKER_THRESHOLD_IN_DAYS = env('\''NEXT_PUBLIC_YOUNG_ACCOUNT_MARKER_THRESHOLD_IN_DAYS'\'')/
/^ \.NEXT_PUBLIC_YOUNG_ACCOUNT_MARKER_THRESHOLD_IN_DAYS$/d
}' lib/constants.ts 2>/dev/null || true
- sed -i '/^export const HIGH_PROFILE_FOLLOWER_THRESHOLD = process\.env$/,/^ : Infinity$/ {
+ sediment '/^export const HIGH_PROFILE_FOLLOWER_THRESHOLD = process\.env$/,/^ : Infinity$/ {
s/^export const HIGH_PROFILE_FOLLOWER_THRESHOLD = process\.env$/export const HIGH_PROFILE_FOLLOWER_THRESHOLD = env('\''NEXT_PUBLIC_HIGH_PROFILE_FOLLOWER_THRESHOLD'\'')/
/^ \.NEXT_PUBLIC_HIGH_PROFILE_FOLLOWER_THRESHOLD$/d
}' lib/constants.ts 2>/dev/null || true
# Fix parseInt() to handle undefined by adding || ''
- sed -i "s/parseInt(env('\([^']*\)'))/parseInt(env('\1') || '0')/g" lib/constants.ts 2>/dev/null || true
+ sediment "s/parseInt(env('\([^']*\)'))/parseInt(env('\1') || '0')/g" lib/constants.ts 2>/dev/null || true
popd > /dev/null
}
@@ -317,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
@@ -338,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
@@ -388,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() {
@@ -480,7 +530,7 @@ case "$1" in
exit
;;
patch)
- at-repos-social-app-avatar-write
+ at-repos-social-app-ios-patch
at-repos-patch-apply-all
at-repos-ozone-patch
show-failed-patches
@@ -520,7 +570,8 @@ case "`cat /etc/hostname`" in
*)
at-repos-clone
at-repos-pull
- at-repos-social-app-avatar-write
+ at-repos-checkout-pinned
+ at-repos-social-app-ios-patch
at-repos-patch-apply-all
at-repos-ozone-patch
show-failed-patches
diff --git a/ios/.env.example b/ios/.env.example
new file mode 100644
index 0000000..c0a09ce
--- /dev/null
+++ b/ios/.env.example
@@ -0,0 +1,17 @@
+APP_NAME="Aiat"
+REPO_DIR="../repos/social-app"
+APP_SLUG="aiat"
+APP_SCHEME="syui"
+APP_GROUP="group.ai.syui.at"
+APP_MAIL=user@example.com
+APP_KEYCHAIN=@keychain:KEYCHAIN_NAME
+BUNDLE_ID="ai.syui.at"
+SERVICE_URL="https://syu.is"
+HELP_URL="https://syu.is/about/support/help"
+PRIVACY_URL="https://syu.is/about/support/privacy-policy"
+TERMS_URL="https://syu.is/about/support/tos"
+REPO_DIR="../repos/social-app"
+CONFIG_FILE="$REPO_DIR/app.config.js"
+CONSTANTS_FILE="$REPO_DIR/src/lib/constants.ts"
+IOS_CERTIFICATE_NAME="Apple Distribution: $TEAM($TEAM_ID)"
+PDS_HOST=syu.is
diff --git a/ios/.keep b/ios/.keep
deleted file mode 100644
index e69de29..0000000
diff --git a/ios/AppInfo.tsx b/ios/AppInfo.tsx
deleted file mode 100644
index 6242b12..0000000
--- a/ios/AppInfo.tsx
+++ /dev/null
@@ -1,134 +0,0 @@
-import React from 'react'
-import {View, Text, StyleSheet, Pressable, Linking} from 'react-native'
-
-interface AppInfoProps {
- onLinkPress?: (url: string) => void
-}
-
-export default function AppInfo({onLinkPress}: AppInfoProps) {
- const handleLinkPress = (url: string) => {
- if (onLinkPress) {
- onLinkPress(url)
- } else {
- Linking.openURL(url)
- }
- }
-
- return (
-
-
- About This App
-
- This is a customized AT Protocol social networking client. It allows you to
- connect to any Personal Data Server (PDS) and participate in the decentralized
- social network.
-
-
-
-
- Key Features
-
- โข Connect to any AT Protocol PDS
- โข Post text, images, and videos
- โข Follow users and view timelines
- โข Customize feeds and moderation settings
- โข Direct messaging support
-
-
-
-
- Open Source
-
- This application is based on the Bluesky social-app, licensed under the MIT
- License. The original source code is available at:
-
-
- handleLinkPress('https://github.com/bluesky-social/social-app')
- }>
- github.com/bluesky-social/social-app
-
-
-
-
- AT Protocol
-
- This app uses the AT Protocol (Authenticated Transfer Protocol), an open and
- decentralized standard for social applications.
-
- handleLinkPress('https://atproto.com')}>
- atproto.com
-
-
-
-
- License
-
- Copyright 2023โ2025 Bluesky Social PBC
-
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software.
-
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
-
-
-
-
- Contact
- handleLinkPress('https://syu.is')}>
- https://syu.is
-
-
-
-
- Version 1.0.0
-
-
- )
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- },
- section: {
- marginBottom: 24,
- },
- sectionTitle: {
- fontSize: 20,
- fontWeight: '600',
- color: '#1d1d1f',
- marginBottom: 12,
- },
- paragraph: {
- fontSize: 15,
- lineHeight: 22,
- color: '#3a3a3c',
- marginBottom: 8,
- },
- list: {
- marginLeft: 8,
- marginTop: 8,
- },
- listItem: {
- fontSize: 15,
- lineHeight: 24,
- color: '#3a3a3c',
- },
- link: {
- fontSize: 15,
- color: '#007aff',
- textDecorationLine: 'underline',
- marginTop: 8,
- },
- versionText: {
- fontSize: 13,
- color: '#8e8e93',
- fontStyle: 'italic',
- },
-})
diff --git a/ios/LicenseNotice.tsx b/ios/LicenseNotice.tsx
deleted file mode 100644
index 496d367..0000000
--- a/ios/LicenseNotice.tsx
+++ /dev/null
@@ -1,95 +0,0 @@
-import React from 'react'
-import {View, Text, StyleSheet, Pressable, Linking} from 'react-native'
-
-export default function LicenseNotice() {
- return (
-
- Open Source Licenses
-
-
- Bluesky Social App
- MIT License
- Copyright 2023โ2025 Bluesky Social PBC
-
-
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
-
-
-
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
-
-
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
-
-
-
- Linking.openURL('https://github.com/bluesky-social/social-app')
- }>
- View Source Code
-
-
-
- )
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- padding: 16,
- },
- title: {
- fontSize: 24,
- fontWeight: 'bold',
- marginBottom: 20,
- color: '#1d1d1f',
- },
- section: {
- marginBottom: 24,
- padding: 16,
- backgroundColor: '#f5f5f7',
- borderRadius: 8,
- },
- projectName: {
- fontSize: 18,
- fontWeight: '600',
- marginBottom: 8,
- color: '#1d1d1f',
- },
- license: {
- fontSize: 14,
- fontWeight: '500',
- color: '#007aff',
- marginBottom: 4,
- },
- copyright: {
- fontSize: 13,
- color: '#3a3a3c',
- marginBottom: 12,
- },
- licenseText: {
- fontSize: 12,
- lineHeight: 18,
- color: '#3a3a3c',
- marginBottom: 12,
- },
- link: {
- fontSize: 14,
- color: '#007aff',
- textDecorationLine: 'underline',
- marginTop: 8,
- },
-})
diff --git a/ios/PrivacyContent.tsx b/ios/PrivacyContent.tsx
deleted file mode 100644
index d780104..0000000
--- a/ios/PrivacyContent.tsx
+++ /dev/null
@@ -1,163 +0,0 @@
-import React from 'react'
-import {View, Text, StyleSheet, Pressable, Linking} from 'react-native'
-
-interface PrivacyContentProps {
- onLinkPress?: (url: string) => void
-}
-
-export default function PrivacyContent({onLinkPress}: PrivacyContentProps) {
- const handleLinkPress = (url: string) => {
- if (onLinkPress) {
- onLinkPress(url)
- } else {
- Linking.openURL(url)
- }
- }
-
- return (
-
-
- Introduction
-
- This Privacy Policy explains how this AT Protocol client application
- (hereinafter referred to as "the App") handles personal information.
- Please read this policy carefully before using the App.
-
-
-
-
- Information We Collect
-
- The App may collect and use the following information:
-
-
- 1. Information Collected Automatically
-
- โข Device information (model, OS version)
- โข App usage data (sessions, features used)
- โข Crash logs and performance data
-
-
- 2. Information Provided by Users
-
-
- โข DID (Decentralized Identifier) and handle for authentication
-
- โข Posts, media, and social interactions
- โข Profile information (avatar, display name, bio)
-
-
-
-
- Important: Your data is stored on your chosen PDS (Personal Data Server).
- This app does not store your content on our servers.
-
-
-
-
-
- How We Use Your Information
-
-
- โข To provide AT Protocol social networking features
-
- โข To improve app performance and user experience
- โข To diagnose and fix technical issues
-
-
-
-
- Data Sharing
-
- The App interacts with your chosen PDS and AppView services. Your posts and
- profile information are shared according to the AT Protocol specification and
- your privacy settings.
-
-
-
-
- Your Rights
-
- You have the right to access, modify, or delete your data through your PDS.
- You can also switch to a different PDS at any time while maintaining your
- identity.
-
-
-
-
- Contact
-
- For questions about this Privacy Policy, please contact:
-
- handleLinkPress('https://syu.is')}>
- https://syu.is
-
-
-
-
- Last Updated: December 3, 2025
-
-
- )
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- },
- section: {
- marginBottom: 24,
- },
- sectionTitle: {
- fontSize: 20,
- fontWeight: '600',
- color: '#1d1d1f',
- marginBottom: 12,
- },
- subTitle: {
- fontSize: 16,
- fontWeight: '500',
- color: '#1d1d1f',
- marginTop: 12,
- marginBottom: 8,
- },
- paragraph: {
- fontSize: 15,
- lineHeight: 22,
- color: '#3a3a3c',
- marginBottom: 8,
- },
- list: {
- marginLeft: 8,
- marginTop: 8,
- },
- listItem: {
- fontSize: 15,
- lineHeight: 24,
- color: '#3a3a3c',
- },
- highlight: {
- backgroundColor: '#fff3cd',
- borderLeftWidth: 4,
- borderLeftColor: '#ffc107',
- padding: 12,
- marginTop: 12,
- borderRadius: 4,
- },
- highlightText: {
- fontSize: 14,
- lineHeight: 20,
- color: '#856404',
- },
- link: {
- fontSize: 15,
- color: '#007aff',
- textDecorationLine: 'underline',
- marginTop: 8,
- },
- lastUpdated: {
- fontSize: 13,
- color: '#8e8e93',
- fontStyle: 'italic',
- },
-})
diff --git a/ios/PrivacyPolicy.screen.tsx b/ios/PrivacyPolicy.screen.tsx
deleted file mode 100644
index df5d5c8..0000000
--- a/ios/PrivacyPolicy.screen.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import React from 'react'
-import {View} from 'react-native'
-import {msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useFocusEffect} from '@react-navigation/native'
-
-import {usePalette} from '#/lib/hooks/usePalette'
-import {
- type CommonNavigatorParams,
- type NativeStackScreenProps,
-} from '#/lib/routes/types'
-import {s} from '#/lib/styles'
-import {useSetMinimalShellMode} from '#/state/shell'
-import {ScrollView} from '#/view/com/util/Views'
-import * as Layout from '#/components/Layout'
-import {ViewHeader} from '../com/util/ViewHeader'
-import PrivacyContent from '#/components/custom/PrivacyContent'
-
-type Props = NativeStackScreenProps
-export const PrivacyPolicyScreen = (_props: Props) => {
- const pal = usePalette('default')
- const {_} = useLingui()
- const setMinimalShellMode = useSetMinimalShellMode()
-
- useFocusEffect(
- React.useCallback(() => {
- setMinimalShellMode(false)
- }, [setMinimalShellMode]),
- )
-
- return (
-
-
-
-
-
-
-
-
-
- )
-}
diff --git a/ios/README.md b/ios/README.md
new file mode 100644
index 0000000..c08d5da
--- /dev/null
+++ b/ios/README.md
@@ -0,0 +1,9 @@
+ไปๅใฎ./ios (social-app)้็บใฎ่ฆ็นใใพใจใใพใใ
+
+1. MITใฎใฉใคใปใณในใ้ตๅฎใใใใจใiosใขใใชใจใใฆๅบๅใใฆใๅ้กใชใใใใซใใใใจ
+https://raw.githubusercontent.com/bluesky-social/social-app/refs/heads/main/LICENSE
+
+2. "Bluesky"ใจใใๅ็งฐใไฝฟ็จใใชใใใจใใขใคใณใณใฎๅคๆดใใชใณใฏใฎๅคๆด
+
+3. selfhostใงใๅใใใจใใใใฏใใงใซpatchใงๅฎ็พใใฆใใพใใ
+
diff --git a/ios/Support.screen.tsx b/ios/Support.screen.tsx
deleted file mode 100644
index 3fddf92..0000000
--- a/ios/Support.screen.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import React from 'react'
-import {msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useFocusEffect} from '@react-navigation/native'
-
-import {usePalette} from '#/lib/hooks/usePalette'
-import {
- type CommonNavigatorParams,
- type NativeStackScreenProps,
-} from '#/lib/routes/types'
-import {s} from '#/lib/styles'
-import {useSetMinimalShellMode} from '#/state/shell'
-import {ViewHeader} from '#/view/com/util/ViewHeader'
-import {ScrollView} from '#/view/com/util/Views'
-import * as Layout from '#/components/Layout'
-import AppInfo from '#/components/custom/AppInfo'
-
-type Props = NativeStackScreenProps
-export const SupportScreen = (_props: Props) => {
- const pal = usePalette('default')
- const setMinimalShellMode = useSetMinimalShellMode()
- const {_} = useLingui()
-
- useFocusEffect(
- React.useCallback(() => {
- setMinimalShellMode(false)
- }, [setMinimalShellMode]),
- )
-
- return (
-
-
-
-
-
-
- )
-}
diff --git a/ios/app.config.patch.js b/ios/app.config.patch.js
deleted file mode 100644
index 136a4d9..0000000
--- a/ios/app.config.patch.js
+++ /dev/null
@@ -1,9 +0,0 @@
-// Aiat app configuration overrides
-module.exports = {
- name: 'Aiat',
- slug: 'aiat',
- scheme: 'aiat',
- owner: 'syui', // Your Expo account
- bundleIdentifier: 'ai.syui.at',
- // Icon will be set separately
-}
diff --git a/ios/assets/app-icons/android_icon_core_aurora.png b/ios/assets/app-icons/android_icon_core_aurora.png
new file mode 100644
index 0000000..f8f45fb
Binary files /dev/null and b/ios/assets/app-icons/android_icon_core_aurora.png differ
diff --git a/ios/assets/app-icons/android_icon_core_bonfire.png b/ios/assets/app-icons/android_icon_core_bonfire.png
new file mode 100644
index 0000000..f8f45fb
Binary files /dev/null and b/ios/assets/app-icons/android_icon_core_bonfire.png differ
diff --git a/ios/assets/app-icons/android_icon_core_classic.png b/ios/assets/app-icons/android_icon_core_classic.png
new file mode 100644
index 0000000..f8f45fb
Binary files /dev/null and b/ios/assets/app-icons/android_icon_core_classic.png differ
diff --git a/ios/assets/app-icons/android_icon_core_flat_black.png b/ios/assets/app-icons/android_icon_core_flat_black.png
new file mode 100644
index 0000000..f8f45fb
Binary files /dev/null and b/ios/assets/app-icons/android_icon_core_flat_black.png differ
diff --git a/ios/assets/app-icons/android_icon_core_flat_blue.png b/ios/assets/app-icons/android_icon_core_flat_blue.png
new file mode 100644
index 0000000..f8f45fb
Binary files /dev/null and b/ios/assets/app-icons/android_icon_core_flat_blue.png differ
diff --git a/ios/assets/app-icons/android_icon_core_flat_white.png b/ios/assets/app-icons/android_icon_core_flat_white.png
new file mode 100644
index 0000000..f8f45fb
Binary files /dev/null and b/ios/assets/app-icons/android_icon_core_flat_white.png differ
diff --git a/ios/assets/app-icons/android_icon_core_midnight.png b/ios/assets/app-icons/android_icon_core_midnight.png
new file mode 100644
index 0000000..f8f45fb
Binary files /dev/null and b/ios/assets/app-icons/android_icon_core_midnight.png differ
diff --git a/ios/assets/app-icons/android_icon_core_sunrise.png b/ios/assets/app-icons/android_icon_core_sunrise.png
new file mode 100644
index 0000000..f8f45fb
Binary files /dev/null and b/ios/assets/app-icons/android_icon_core_sunrise.png differ
diff --git a/ios/assets/app-icons/android_icon_core_sunset.png b/ios/assets/app-icons/android_icon_core_sunset.png
new file mode 100644
index 0000000..f8f45fb
Binary files /dev/null and b/ios/assets/app-icons/android_icon_core_sunset.png differ
diff --git a/ios/assets/app-icons/android_icon_default_next.png b/ios/assets/app-icons/android_icon_default_next.png
new file mode 100644
index 0000000..f8f45fb
Binary files /dev/null and b/ios/assets/app-icons/android_icon_default_next.png differ
diff --git a/ios/assets/app-icons/android_icon_legacy_dark.png b/ios/assets/app-icons/android_icon_legacy_dark.png
new file mode 100644
index 0000000..f8f45fb
Binary files /dev/null and b/ios/assets/app-icons/android_icon_legacy_dark.png differ
diff --git a/ios/assets/app-icons/android_icon_legacy_light.png b/ios/assets/app-icons/android_icon_legacy_light.png
new file mode 100644
index 0000000..f8f45fb
Binary files /dev/null and b/ios/assets/app-icons/android_icon_legacy_light.png differ
diff --git a/ios/assets/app-icons/ios_icon_core_aurora.png b/ios/assets/app-icons/ios_icon_core_aurora.png
new file mode 100644
index 0000000..f8f45fb
Binary files /dev/null and b/ios/assets/app-icons/ios_icon_core_aurora.png differ
diff --git a/ios/assets/app-icons/ios_icon_core_bonfire.png b/ios/assets/app-icons/ios_icon_core_bonfire.png
new file mode 100644
index 0000000..f8f45fb
Binary files /dev/null and b/ios/assets/app-icons/ios_icon_core_bonfire.png differ
diff --git a/ios/assets/app-icons/ios_icon_core_classic.png b/ios/assets/app-icons/ios_icon_core_classic.png
new file mode 100644
index 0000000..f8f45fb
Binary files /dev/null and b/ios/assets/app-icons/ios_icon_core_classic.png differ
diff --git a/ios/assets/app-icons/ios_icon_core_flat_black.png b/ios/assets/app-icons/ios_icon_core_flat_black.png
new file mode 100644
index 0000000..f8f45fb
Binary files /dev/null and b/ios/assets/app-icons/ios_icon_core_flat_black.png differ
diff --git a/ios/assets/app-icons/ios_icon_core_flat_blue.png b/ios/assets/app-icons/ios_icon_core_flat_blue.png
new file mode 100644
index 0000000..f8f45fb
Binary files /dev/null and b/ios/assets/app-icons/ios_icon_core_flat_blue.png differ
diff --git a/ios/assets/app-icons/ios_icon_core_flat_white.png b/ios/assets/app-icons/ios_icon_core_flat_white.png
new file mode 100644
index 0000000..f8f45fb
Binary files /dev/null and b/ios/assets/app-icons/ios_icon_core_flat_white.png differ
diff --git a/ios/assets/app-icons/ios_icon_core_midnight.png b/ios/assets/app-icons/ios_icon_core_midnight.png
new file mode 100644
index 0000000..f8f45fb
Binary files /dev/null and b/ios/assets/app-icons/ios_icon_core_midnight.png differ
diff --git a/ios/assets/app-icons/ios_icon_core_sunrise.png b/ios/assets/app-icons/ios_icon_core_sunrise.png
new file mode 100644
index 0000000..f8f45fb
Binary files /dev/null and b/ios/assets/app-icons/ios_icon_core_sunrise.png differ
diff --git a/ios/assets/app-icons/ios_icon_core_sunset.png b/ios/assets/app-icons/ios_icon_core_sunset.png
new file mode 100644
index 0000000..f8f45fb
Binary files /dev/null and b/ios/assets/app-icons/ios_icon_core_sunset.png differ
diff --git a/ios/assets/app-icons/ios_icon_default.icon/Assets/iOS transparent.png b/ios/assets/app-icons/ios_icon_default.icon/Assets/iOS transparent.png
new file mode 100644
index 0000000..f8f45fb
Binary files /dev/null and b/ios/assets/app-icons/ios_icon_default.icon/Assets/iOS transparent.png differ
diff --git a/ios/assets/app-icons/ios_icon_default.icon/icon.json b/ios/assets/app-icons/ios_icon_default.icon/icon.json
new file mode 100644
index 0000000..8a681cb
--- /dev/null
+++ b/ios/assets/app-icons/ios_icon_default.icon/icon.json
@@ -0,0 +1,31 @@
+{
+ "fill" : {
+ "automatic-gradient" : "srgb:0.00000,0.41569,1.00000,1.00000"
+ },
+ "groups" : [
+ {
+ "layers" : [
+ {
+ "fill" : "none",
+ "glass" : false,
+ "image-name" : "iOS transparent.png",
+ "name" : "iOS transparent"
+ }
+ ],
+ "shadow" : {
+ "kind" : "neutral",
+ "opacity" : 0.5
+ },
+ "translucency" : {
+ "enabled" : true,
+ "value" : 0.5
+ }
+ }
+ ],
+ "supported-platforms" : {
+ "circles" : [
+ "watchOS"
+ ],
+ "squares" : "shared"
+ }
+}
\ No newline at end of file
diff --git a/ios/assets/app-icons/ios_icon_default.icon/icon.png b/ios/assets/app-icons/ios_icon_default.icon/icon.png
new file mode 100644
index 0000000..4c67271
Binary files /dev/null and b/ios/assets/app-icons/ios_icon_default.icon/icon.png differ
diff --git a/ios/assets/app-icons/ios_icon_default_next.png b/ios/assets/app-icons/ios_icon_default_next.png
new file mode 100644
index 0000000..f8f45fb
Binary files /dev/null and b/ios/assets/app-icons/ios_icon_default_next.png differ
diff --git a/ios/assets/app-icons/ios_icon_legacy_dark.png b/ios/assets/app-icons/ios_icon_legacy_dark.png
new file mode 100644
index 0000000..f8f45fb
Binary files /dev/null and b/ios/assets/app-icons/ios_icon_legacy_dark.png differ
diff --git a/ios/assets/app-icons/ios_icon_legacy_light.png b/ios/assets/app-icons/ios_icon_legacy_light.png
new file mode 100644
index 0000000..f8f45fb
Binary files /dev/null and b/ios/assets/app-icons/ios_icon_legacy_light.png differ
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/assets/logo.png b/ios/assets/logo.png
new file mode 100644
index 0000000..f8f45fb
Binary files /dev/null and b/ios/assets/logo.png differ
diff --git a/ios/bin/build.zsh b/ios/bin/build.zsh
deleted file mode 100755
index d1fe8ea..0000000
--- a/ios/bin/build.zsh
+++ /dev/null
@@ -1,44 +0,0 @@
-#!/bin/zsh
-set -e
-
-d=~/ai/at/repos/social-app
-APP_NAME=Aiat
-PKG=aiat
-TEAM_NAME=
-TEAM_ID=
-CERT="Apple Distribution: ${TEAM_NAME} (${TEAM_ID})"
-MAIL=user@example.com
-KEY_CHAIN=EXAMPLE
-
-cd $d
-# npx expo prebuild --clean
-# cd ios && pod install && cd ..
-
-## ใขใผใซใคใ
-xcodebuild -workspace ios/${PKG}.xcworkspace \
- -scheme ${PKG} \
- -configuration Release \
- -archivePath build/${APP_NAME}.xcarchive \
- -allowProvisioningUpdates \
- archive
-
-cd build
-
-# IPAไฝๆ
-rm -rf Payload ${APP_NAME}.ipa
-mkdir -p Payload
-cp -R ${APP_NAME}.xcarchive/Products/Applications/${PKG}.app Payload/
-cp ../store.mobileprovision Payload/${PKG}.app/embedded.mobileprovision
-
-# entitlementsๆฝๅบ
-security cms -D -i Payload/${PKG}.app/embedded.mobileprovision > /tmp/profile.plist
-/usr/libexec/PlistBuddy -x -c "Print :Entitlements" /tmp/profile.plist > /tmp/entitlements.plist
-
-codesign -f -s "$CERT" Payload/${PKG}.app/Frameworks/*.framework 2>/dev/null || true
-codesign -f -s "$CERT" --entitlements /tmp/entitlements.plist Payload/${PKG}.app
-
-zip -r ${APP_NAME}.ipa Payload
-
-xcrun altool --upload-app -f ${APP_NAME}.ipa -t ios -u "${MAIL}" -p "@keychain:${KEY_CHAIN}"
-
-echo "Upload complete"
diff --git a/ios/bin/install.zsh b/ios/bin/install.zsh
deleted file mode 100644
index 2e372c7..0000000
--- a/ios/bin/install.zsh
+++ /dev/null
@@ -1,86 +0,0 @@
-#!/bin/zsh
-
-if [ "$1" = "social-app-custom" ];then
- at-social-app-custom-pages
- at-social-app-custom-screens
- at-social-app-aiat-config
- at-social-app-aiat-logo
- at-origin-social-app
- exit
-fi
-
-function at-social-app-custom-pages() {
- d_=$d/repos/social-app
- custom=$d/social-app-custom
-
- echo "copying custom components to social-app"
-
- # Create components directory if not exists
- mkdir -p ${d_}/src/components/custom
-
- # Copy custom components
- cp ${custom}/PrivacyContent.tsx ${d_}/src/components/custom/
- cp ${custom}/AppInfo.tsx ${d_}/src/components/custom/
-
- echo "custom components copied successfully"
-}
-
-function at-social-app-aiat-config() {
- d_=$d/repos/social-app
- custom=$d/social-app-custom
-
- echo "applying Aiat configuration"
-
- # Update app.config.js
- cd ${d_}
-
- # Backup original
- cp app.config.js app.config.js.orig
-
- # Apply changes using sed
- sed -i "s/name: 'Bluesky'/name: 'Aiat'/g" app.config.js
- sed -i "s/slug: 'bluesky'/slug: 'aiat'/g" app.config.js
- sed -i "s/scheme: 'bluesky'/scheme: 'aiat'/g" app.config.js
- sed -i "s/owner: 'blueskysocial'/owner: 'syui'/g" app.config.js
- sed -i "s/bundleIdentifier: 'xyz.blueskyweb.app'/bundleIdentifier: 'ai.syui.at'/g" app.config.js
-
- # Update package.json name
- sed -i 's/"name": "bsky.app"/"name": "aiat"/g' package.json
-
- echo "Aiat configuration applied"
-}
-
-function at-social-app-aiat-logo() {
- d_=$d/repos/social-app
- custom=$d/social-app-custom
-
- echo "applying Aiat logo"
-
- # Create logo directory if not exists
- mkdir -p ${custom}/assets
-
- # Copy logo if exists in custom folder
- if [ -f ${custom}/assets/icon.png ]; then
- cp ${custom}/assets/icon.png ${d_}/assets/app-icons/ios_icon_default_next.png
- echo "Aiat logo applied"
- else
- echo "Warning: Logo file not found at ${custom}/assets/icon.png"
- echo "Please add your logo file there"
- fi
-}
-
-function at-social-app-custom-screens() {
- d_=$d/repos/social-app
- custom=$d/social-app-custom
-
- echo "applying custom screens"
-
- # Copy custom screen files
- cp ${custom}/PrivacyPolicy.screen.tsx ${d_}/src/view/screens/PrivacyPolicy.tsx
- cp ${custom}/Support.screen.tsx ${d_}/src/view/screens/Support.tsx
- cp ${custom}/LicenseNotice.tsx ${d_}/src/components/custom/
-
- echo "custom screens applied"
-}
-
-
diff --git a/ios/build.zsh b/ios/build.zsh
new file mode 100755
index 0000000..a379cff
--- /dev/null
+++ b/ios/build.zsh
@@ -0,0 +1,202 @@
+#!/bin/zsh
+set -e
+
+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="$REPO_DIR/embedded.mobileprovision"
+ASSETS_DIR="$SCRIPT_DIR/assets"
+
+echo "Running iOS preview workflow..."
+cd "$REPO_DIR"
+
+# 0. Environment Setup (Fix Node Version)
+export NVM_DIR="$HOME/.nvm"
+[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
+echo "Checking Node version..."
+if command -v nvm >/dev/null; then
+ nvm use 22 || nvm use 20 || echo "Warning: Could not switch to Node 22/20. Current: $(node -v)"
+else
+ echo "nvm not found, using system node: $(node -v)"
+fi
+
+# 1. Install dependencies
+echo "1. Installing dependencies (yarn)..."
+yarn install
+
+# 1.5. Copy assets
+echo "1.5. Copying assets..."
+if [ -d "$ASSETS_DIR" ]; then
+ cp -rf "$ASSETS_DIR/"* "$REPO_DIR/assets/"
+ echo "โ
Copied all assets (including logo.png, logo-1024.png)"
+else
+ echo "โ ๏ธ Warning: $ASSETS_DIR not found"
+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
+ rm -rf ios
+ npx expo prebuild --platform ios --clean
+
+ # 3. CocoaPods
+ echo "3. Installing CocoaPods..."
+ # Ensure PATH includes Homebrew ruby gems if needed
+ export PATH="/opt/homebrew/lib/ruby/gems/3.4.0/bin:$PATH"
+ cd ios
+ pod install
+ cd ..
+
+ # 4. Signing (Automated)
+ echo "4. Configuring Xcode Signing..."
+ XCODE_PROJ="ios/${APP_NAME}.xcodeproj"
+ if [ ! -d "$XCODE_PROJ" ]; then
+ XCODE_PROJ=$(find ios -name "*.xcodeproj" | head -n 1)
+ fi
+ PBXPROJ="$XCODE_PROJ/project.pbxproj"
+
+ # 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
+ i)
+ cleanup_build
+ ;;
+esac
+
+echo "Building $APP_NAME for App Store upload..."
+
+# ใใซใใใฃใฌใฏใใชไฝๆ
+mkdir -p "$BUILD_DIR"
+
+# ใขใผใซใคใ๏ผ่ฉณ็ดฐใญใฐๅบๅ๏ผ
+xcodebuild -workspace "$WORKSPACE" \
+ -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
+ echo "Error: Archive failed. Check $BUILD_DIR/build.log for details"
+ exit 1
+fi
+
+cd "$BUILD_DIR"
+
+
+# IPAไฝๆ
+rm -rf Payload ${APP_NAME}.ipa
+mkdir -p Payload
+cp -R ${APP_NAME}.xcarchive/Products/Applications/${APP_NAME}.app Payload/
+
+# store.mobileprovisionใฎๅญๅจ็ขบ่ชใจใณใใผ
+# https://developer.apple.com/account/resources/profiles/list
+if [ ! -f "$MOBILEPROVISION" ]; then
+ echo "Error: store.mobileprovision not found at $MOBILEPROVISION"
+ exit 1
+fi
+
+cp "$MOBILEPROVISION" Payload/${APP_NAME}.app/embedded.mobileprovision
+
+# entitlementsๆฝๅบ
+security cms -D -i Payload/${APP_NAME}.app/embedded.mobileprovision > /tmp/profile.plist
+/usr/libexec/PlistBuddy -x -c "Print :Entitlements" /tmp/profile.plist > /tmp/entitlements.plist
+
+# ็ฝฒๅ
+CERT="$IOS_CERTIFICATE_NAME"
+
+# Frameworksใใฃใฌใฏใใชใๅญๅจใใๅ ดๅใฎใฟ็ฝฒๅ
+if [ -d "Payload/${APP_NAME}.app/Frameworks" ]; then
+ for framework in Payload/${APP_NAME}.app/Frameworks/*.framework; do
+ if [ -e "$framework" ]; then
+ echo "Signing $framework"
+ codesign -f -s "$CERT" "$framework"
+ fi
+ done
+fi
+
+# ใขใใชๆฌไฝใซ็ฝฒๅ
+codesign -f -s "$CERT" --entitlements /tmp/entitlements.plist Payload/${APP_NAME}.app
+
+# IPAไฝๆ
+zip -r ${APP_NAME}.ipa Payload
+
+# ใขใใใญใผใ
+xcrun altool --upload-app -f ${APP_NAME}.ipa -t ios -u "${APP_MAIL}" -p "${APP_KEYCHAIN}"
+
+echo "Upload complete: ${APP_NAME}.ipa"
diff --git a/ios/icon.png b/ios/icon.png
new file mode 100644
index 0000000..f8f45fb
Binary files /dev/null and b/ios/icon.png differ
diff --git a/ios/patching/001-social-app-ios-config.patch b/ios/patching/001-social-app-ios-config.patch
new file mode 100644
index 0000000..b132480
--- /dev/null
+++ b/ios/patching/001-social-app-ios-config.patch
@@ -0,0 +1,166 @@
+diff --git a/app.config.js b/app.config.js
+index 246d8abd3..ed8f7b2b2 100644
+--- a/app.config.js
++++ b/app.config.js
+@@ -18,10 +18,7 @@ module.exports = function (_config) {
+ const IS_DEV = !IS_TESTFLIGHT || !IS_PRODUCTION
+
+ const ASSOCIATED_DOMAINS = [
+- 'applinks:bsky.app',
+- 'applinks:staging.bsky.app',
+- 'appclips:bsky.app',
+- 'appclips:go.bsky.app', // Allows App Clip to work when scanning QR codes
++ 'applinks:syu.is',
+ // 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) {
+ return {
+ expo: {
+ version: VERSION,
+- name: 'Bluesky',
+- slug: 'bluesky',
+- scheme: 'bluesky',
++ name: 'Aiat',
++ slug: 'aiat',
++ scheme: 'syui',
+ owner: 'blueskysocial',
+ runtimeVersion: {
+ policy: 'appVersion',
+ },
+- icon: './assets/app-icons/ios_icon_default_next.png',
++ icon: './assets/logo.png',
+ userInterfaceStyle: 'automatic',
+ primaryColor: '#1083fe',
+ newArchEnabled: false,
+ ios: {
+ supportsTablet: false,
+- bundleIdentifier: 'xyz.blueskyweb.app',
++ bundleIdentifier: 'ai.syui.at',
++ buildNumber: '__BUILD_NUMBER__',
+ config: {
+ usesNonExemptEncryption: false,
+ },
+- icon:
+- 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',
+ infoPlist: {
+ UIBackgroundModes: ['remote-notification'],
+ NSCameraUsageDescription:
+@@ -113,7 +107,7 @@ module.exports = function (_config) {
+ 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',
+ },
+ privacyManifests: {
+ NSPrivacyCollectedDataTypes: [
+@@ -175,14 +169,14 @@ module.exports = function (_config) {
+ barStyle: 'light-content',
+ },
+ android: {
+- icon: './assets/app-icons/android_icon_default_next.png',
++ icon: './assets/logo.png',
+ adaptiveIcon: {
+ foregroundImage: './assets/icon-android-foreground.png',
+ monochromeImage: './assets/icon-android-monochrome.png',
+ backgroundColor: '#006AFF',
+ },
+ googleServicesFile: './google-services.json',
+- package: 'xyz.blueskyweb.app',
++ package: 'ai.syui.at',
+ intentFilters: [
+ {
+ action: 'VIEW',
+@@ -190,7 +184,7 @@ module.exports = function (_config) {
+ data: [
+ {
+ scheme: 'https',
+- host: 'bsky.app',
++ host: 'syu.is',
+ },
+ IS_DEV && {
+ scheme: 'http',
+@@ -213,9 +207,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 +219,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',
+@@ -264,7 +258,6 @@ module.exports = function (_config) {
+ networkInstrumentation: true,
+ },
+ ],
+- './plugins/starterPackAppClipExtension/withStarterPackAppClip.js',
+ './plugins/withGradleJVMHeapSizeIncrease.js',
+ './plugins/withAndroidManifestLargeHeapPlugin.js',
+ './plugins/withAndroidManifestFCMIconPlugin.js',
+@@ -272,8 +265,6 @@ module.exports = function (_config) {
+ './plugins/withAndroidStylesAccentColorPlugin.js',
+ './plugins/withAndroidDayNightThemePlugin.js',
+ './plugins/withAndroidNoJitpackPlugin.js',
+- './plugins/shareExtension/withShareExtensions.js',
+- './plugins/notificationsExtension/withNotificationsExtension.js',
+ [
+ 'expo-font',
+ {
+@@ -386,7 +377,7 @@ module.exports = function (_config) {
+ },
+ },
+ ],
+- ['expo-screen-orientation', {initialOrientation: 'PORTRAIT_UP'}],
++ ['expo-screen-orientation', { initialOrientation: 'PORTRAIT_UP' }],
+ ['expo-location'],
+ ].filter(Boolean),
+ extra: {
+@@ -394,30 +385,7 @@ module.exports = function (_config) {
+ build: {
+ experimental: {
+ ios: {
+- appExtensions: [
+- {
+- targetName: 'Share-with-Bluesky',
+- bundleIdentifier: 'xyz.blueskyweb.app.Share-with-Bluesky',
+- entitlements: {
+- 'com.apple.security.application-groups': [
+- 'group.app.bsky',
+- ],
+- },
+- },
+- {
+- targetName: 'BlueskyNSE',
+- bundleIdentifier: 'xyz.blueskyweb.app.BlueskyNSE',
+- entitlements: {
+- 'com.apple.security.application-groups': [
+- 'group.app.bsky',
+- ],
+- },
+- },
+- {
+- targetName: 'BlueskyClip',
+- bundleIdentifier: 'xyz.blueskyweb.app.AppClip',
+- },
+- ],
++ appExtensions: [],
+ },
+ },
+ },
diff --git a/ios/patching/002-social-app-ios-lib.patch b/ios/patching/002-social-app-ios-lib.patch
new file mode 100644
index 0000000..ee04f7b
--- /dev/null
+++ b/ios/patching/002-social-app-ios-lib.patch
@@ -0,0 +1,217 @@
+diff --git a/src/lib/api/feed/home.ts b/src/lib/api/feed/home.ts
+index 7a0d72d91..93554dc3e 100644
+--- a/src/lib/api/feed/home.ts
++++ b/src/lib/api/feed/home.ts
+@@ -45,7 +45,7 @@ export class HomeFeedAPI implements FeedAPI {
+ this.following = new FollowingFeedAPI({agent})
+ this.discover = new CustomFeedAPI({
+ agent,
+- feedParams: {feed: PROD_DEFAULT_FEED('whats-hot')},
++ feedParams: {feed: PROD_DEFAULT_FEED('app')},
+ })
+ this.userInterests = userInterests
+ }
+@@ -54,7 +54,7 @@ export class HomeFeedAPI implements FeedAPI {
+ this.following = new FollowingFeedAPI({agent: this.agent})
+ this.discover = new CustomFeedAPI({
+ agent: this.agent,
+- feedParams: {feed: PROD_DEFAULT_FEED('whats-hot')},
++ feedParams: {feed: PROD_DEFAULT_FEED('app')},
+ userInterests: this.userInterests,
+ })
+ this.usingDiscover = false
+diff --git a/src/lib/constants.ts b/src/lib/constants.ts
+index 231447b4f..a44b3da05 100644
+--- a/src/lib/constants.ts
++++ b/src/lib/constants.ts
+@@ -7,12 +7,12 @@ import {BLUESKY_PROXY_DID, CHAT_PROXY_DID} from '#/env'
+ export const LOCAL_DEV_SERVICE =
+ Platform.OS === 'android' ? 'http://10.0.2.2:2583' : 'http://localhost:2583'
+ export const STAGING_SERVICE = 'https://staging.bsky.dev'
+-export const BSKY_SERVICE = 'https://bsky.social'
+-export const BSKY_SERVICE_DID = 'did:web:bsky.social'
+-export const PUBLIC_BSKY_SERVICE = 'https://public.api.bsky.app'
++export const BSKY_SERVICE = 'https://syu.is'
++export const BSKY_SERVICE_DID = 'did:web:syu.is'
++export const PUBLIC_BSKY_SERVICE = 'https://bsky.syu.is'
+ export const DEFAULT_SERVICE = BSKY_SERVICE
+-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 EMBED_SERVICE = 'https://embed.bsky.app'
+ export const EMBED_SCRIPT = `${EMBED_SERVICE}/static/embed.js`
+ export const BSKY_DOWNLOAD_URL = 'https://bsky.app/download'
+@@ -79,19 +79,17 @@ export function IS_PROD_SERVICE(url?: string) {
+ }
+
+ export const PROD_DEFAULT_FEED = (rkey: string) =>
+- `at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/${rkey}`
++ `at://did:plc:6qyecktefllvenje24fcxnie/app.bsky.feed.generator/${rkey}`
+
+ export const STAGING_DEFAULT_FEED = (rkey: string) =>
+ `at://did:plc:yofh3kx63drvfljkibw5zuxo/app.bsky.feed.generator/${rkey}`
+
+ export const PROD_FEEDS = [
+- `feedgen|${PROD_DEFAULT_FEED('whats-hot')}`,
+- `feedgen|${PROD_DEFAULT_FEED('thevids')}`,
++ `feedgen|${PROD_DEFAULT_FEED('app')}`,
+ ]
+
+ export const STAGING_FEEDS = [
+- `feedgen|${STAGING_DEFAULT_FEED('whats-hot')}`,
+- `feedgen|${STAGING_DEFAULT_FEED('thevids')}`,
++ `feedgen|${STAGING_DEFAULT_FEED('app')}`,
+ ]
+
+ export const POST_IMG_MAX = {
+@@ -129,7 +127,7 @@ export const LANG_DROPDOWN_HITSLOP = {top: 10, bottom: 10, left: 4, right: 4}
+ export const BACK_HITSLOP = HITSLOP_30
+ export const MAX_POST_LINES = 25
+
+-export const BSKY_APP_ACCOUNT_DID = 'did:plc:z72i7hdynmk6r22z27h6tvur'
++export const BSKY_APP_ACCOUNT_DID = 'did:plc:6qyecktefllvenje24fcxnie'
+
+ export const BSKY_FEED_OWNER_DIDS = [
+ BSKY_APP_ACCOUNT_DID,
+@@ -138,9 +136,9 @@ export const BSKY_FEED_OWNER_DIDS = [
+ ]
+
+ export const DISCOVER_FEED_URI =
+- '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'
+ 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]
+@@ -209,8 +207,8 @@ export const urls = {
+ },
+ }
+
+-export const PUBLIC_APPVIEW = 'https://api.bsky.app'
+-export const PUBLIC_APPVIEW_DID = 'did:web:api.bsky.app'
++export const PUBLIC_APPVIEW = 'https://bsky.syu.is'
++export const PUBLIC_APPVIEW_DID = 'did:web:bsky.syu.is'
+ export const PUBLIC_STAGING_APPVIEW_DID = 'did:web:api.staging.bsky.dev'
+
+ export const DEV_ENV_APPVIEW = `http://localhost:2584` // always the same
+@@ -236,8 +234,8 @@ export const BLUESKY_MOD_SERVICE_HEADERS = {
+ }
+
+ 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`,
+ community: `https://bsky.social/about/support/community-guidelines`,
+ communityDeprecated: `https://bsky.social/about/support/community-guidelines-deprecated`,
+ }
+diff --git a/src/lib/demo.ts b/src/lib/demo.ts
+index 5ead62c9d..7c80dfe15 100644
+--- a/src/lib/demo.ts
++++ b/src/lib/demo.ts
+@@ -1,7 +1,7 @@
+ import {type AppBskyFeedGetFeed} from '@atproto/api'
+ import {subDays, subMinutes} from 'date-fns'
+
+-const DID = `did:plc:z72i7hdynmk6r22z27h6tvur`
++const DID = `did:plc:6qyecktefllvenje24fcxnie`
+ const NOW = new Date()
+ const POST_1_DATE = subMinutes(NOW, 2).toISOString()
+ const POST_2_DATE = subMinutes(NOW, 4).toISOString()
+diff --git a/src/lib/strings/url-helpers.ts b/src/lib/strings/url-helpers.ts
+index 6088e2806..0f6787a4d 100644
+--- a/src/lib/strings/url-helpers.ts
++++ b/src/lib/strings/url-helpers.ts
+@@ -53,7 +53,7 @@ export function toNiceDomain(url: string): string {
+ try {
+ const urlp = new URL(url)
+ if (`https://${urlp.host}` === BSKY_SERVICE) {
+- return 'Bluesky Social'
++ return 'syu.is'
+ }
+ return urlp.host ? urlp.host : url
+ } catch (e) {
+@@ -338,7 +338,7 @@ export function createProxiedUrl(url: string): string {
+ return url
+ }
+
+- return `https://go.bsky.app/redirect?u=${encodeURIComponent(url)}`
++ return url
+ }
+
+ export function isShortLink(url: string): boolean {
+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,6 @@ export function useFeedSourceInfoQuery({uri}: {uri: string}) {
+ // for the ones we know need it
+ // -prf
+ export const KNOWN_AUTHED_ONLY_FEEDS = [
+- 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/with-friends', // popular with friends, by bsky.app
+- 'at://did:plc:tenurhgjptubkk5zf5qhi3og/app.bsky.feed.generator/mutuals', // mutuals, by skyfeed
+- 'at://did:plc:tenurhgjptubkk5zf5qhi3og/app.bsky.feed.generator/only-posts', // only posts, by skyfeed
+- 'at://did:plc:wzsilnxf24ehtmmc3gssy5bu/app.bsky.feed.generator/mentions', // mentions, by flicknow
+- 'at://did:plc:q6gjnaw2blty4crticxkmujt/app.bsky.feed.generator/bangers', // my bangers, by jaz
+- '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
+ ]
+
+ 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
+--- a/src/state/queries/preferences/index.ts
++++ b/src/state/queries/preferences/index.ts
+@@ -270,7 +270,7 @@ export function useReplaceForYouWithDiscoverFeedMutation() {
+ await agent.addSavedFeeds([
+ {
+ type: 'feed',
+- value: PROD_DEFAULT_FEED('whats-hot'),
++ value: PROD_DEFAULT_FEED('app'),
+ pinned: true,
+ },
+ ])
+diff --git a/src/view/com/posts/FeedShutdownMsg.tsx b/src/view/com/posts/FeedShutdownMsg.tsx
+index 620382175..928480da2 100644
+--- a/src/view/com/posts/FeedShutdownMsg.tsx
++++ b/src/view/com/posts/FeedShutdownMsg.tsx
+@@ -32,7 +32,7 @@ export function FeedShutdownMsg({feedUri}: {feedUri: string}) {
+ f => f.value === feedUri && f.pinned,
+ )
+ const discoverFeedConfig = preferences?.savedFeeds?.find(
+- f => f.value === PROD_DEFAULT_FEED('whats-hot'),
++ f => f.value === PROD_DEFAULT_FEED('app'),
+ )
+ const hasFeedPinned = Boolean(feedConfig)
+ const hasDiscoverPinned = Boolean(discoverFeedConfig?.pinned)
+@@ -44,7 +44,7 @@ export function FeedShutdownMsg({feedUri}: {feedUri: string}) {
+ Toast.show(_(msg`Removed from your feeds`))
+ }
+ if (hasDiscoverPinned) {
+- setSelectedFeed(`feedgen|${PROD_DEFAULT_FEED('whats-hot')}`)
++ setSelectedFeed(`feedgen|${PROD_DEFAULT_FEED('app')}`)
+ }
+ } catch (err: any) {
+ Toast.show(
+@@ -63,7 +63,7 @@ export function FeedShutdownMsg({feedUri}: {feedUri: string}) {
+ forYouFeedConfig: feedConfig,
+ discoverFeedConfig,
+ })
+- setSelectedFeed(`feedgen|${PROD_DEFAULT_FEED('whats-hot')}`)
++ setSelectedFeed(`feedgen|${PROD_DEFAULT_FEED('app')}`)
+ Toast.show(_(msg`The feed has been replaced with Discover.`))
+ } catch (err: any) {
+ Toast.show(
+@@ -100,7 +100,7 @@ export function FeedShutdownMsg({feedUri}: {feedUri: string}) {
+ This feed is no longer online. We are showing{' '}
+
+ Discover
+ {' '}
+
diff --git a/ios/patching/003-social-app-ios-view.patch b/ios/patching/003-social-app-ios-view.patch
new file mode 100644
index 0000000..aa0cc29
--- /dev/null
+++ b/ios/patching/003-social-app-ios-view.patch
@@ -0,0 +1,213 @@
+diff --git a/src/Splash.tsx b/src/Splash.tsx
+index 47e70b375..616f351ed 100644
+--- a/src/Splash.tsx
++++ b/src/Splash.tsx
+@@ -15,8 +15,8 @@ import Animated, {
+ withTiming,
+ } from 'react-native-reanimated'
+ import {useSafeAreaInsets} from 'react-native-safe-area-context'
+-import Svg, {Path, type SvgProps} from 'react-native-svg'
+ import {Image} from 'expo-image'
++import {type SvgProps} from 'react-native-svg'
+ import * as SplashScreen from 'expo-splash-screen'
+
+ import {Logotype} from '#/view/icons/Logotype'
+@@ -29,21 +29,18 @@ const darkSplashImageUri = RNImage.resolveAssetSource(
+ darkSplashImagePointer,
+ ).uri
+
+-export const Logo = React.forwardRef(function LogoImpl(props: SvgProps, ref) {
+- const width = 1000
+- const height = width * (67 / 64)
++export const Logo = React.forwardRef(function LogoImpl(props: SvgProps & {fill?: string}, ref) {
++ const size = 1000
++ // @ts-ignore
+ return (
+-
+-
+-
++ source={require('../assets/logo.png')}
++ style={[{width: size, height: size}, props.style]}
++ contentFit="contain"
++ accessibilityLabel="Logo"
++ />
+ )
+ })
+
+diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx
+index 8a9e51a33..65d643b89 100644
+--- a/src/view/com/util/UserAvatar.tsx
++++ b/src/view/com/util/UserAvatar.tsx
+@@ -444,7 +444,7 @@ let EditableUserAvatar = ({
+ 0), }}
+ accessibilityRole="image"
+ />
+ ) : (
+@@ -618,9 +618,8 @@ export {PreviewableUserAvatar}
+ // manually string-replace to use the smaller ones
+ // -prf
+ function hackModifyThumbnailPath(uri: string, isEnabled: boolean): string {
+- return isEnabled
+- ? uri.replace('/img/avatar/plain/', '/img/avatar_thumbnail/plain/')
+- : uri
++ // syu.is: avatars are served directly from bsky.syu.is, no CDN transformation needed
++ return uri
+ }
+
+ const styles = StyleSheet.create({
+diff --git a/src/view/icons/Logo.tsx b/src/view/icons/Logo.tsx
+index d7208df13..2763800ac 100644
+--- a/src/view/icons/Logo.tsx
++++ b/src/view/icons/Logo.tsx
+@@ -1,75 +1,17 @@
+ import React from 'react'
+-import {type TextProps} from 'react-native'
+-import Svg, {
+- Defs,
+- LinearGradient,
+- Path,
+- type PathProps,
+- Stop,
+- type SvgProps,
+-} from 'react-native-svg'
+ import {Image} from 'expo-image'
++import {flatten} from '#/alf'
+
+-import {useKawaiiMode} from '#/state/preferences/kawaii'
+-import {flatten, useTheme} from '#/alf'
+-
+-const ratio = 57 / 64
+-
+-type Props = {
+- fill?: PathProps['fill']
+- style?: TextProps['style']
+-} & Omit
+-
+-export const Logo = React.forwardRef(function LogoImpl(props: Props, ref) {
+- const t = useTheme()
+- const {fill, ...rest} = props
+- const gradient = fill === 'sky'
+- const styles = flatten(props.style)
+- const _fill = gradient
+- ? 'url(#sky)'
+- : fill || styles?.color || t.palette.primary_500
+- // @ts-ignore it's fiiiiine
+- const size = parseInt(rest.width || 32, 10)
+-
+- const isKawaii = useKawaiiMode()
+-
+- if (isKawaii) {
+- return (
+- 100
+- ? require('../../../assets/kawaii.png')
+- : require('../../../assets/kawaii_smol.png')
+- }
+- accessibilityLabel="Bluesky"
+- accessibilityHint=""
+- accessibilityIgnoresInvertColors
+- style={[{height: size, aspectRatio: 1.4}]}
+- />
+- )
+- }
+-
++export const Logo = React.forwardRef(function LogoImpl(props: any, ref) {
++ const {width, style} = props
++ // @ts-ignore
++ const size = parseInt(width || 32, 10)
+ return (
+-
+- {gradient && (
+-
+-
+-
+-
+-
+-
+- )}
+-
+-
+-
++
+ )
+ })
+diff --git a/src/view/icons/Logotype.tsx b/src/view/icons/Logotype.tsx
+index 270c913fc..a60ffe07c 100644
+--- a/src/view/icons/Logotype.tsx
++++ b/src/view/icons/Logotype.tsx
+@@ -1,28 +1,22 @@
+-import Svg, {Path, type PathProps, type SvgProps} from 'react-native-svg'
+-
+-import {usePalette} from '#/lib/hooks/usePalette'
+-
+-const ratio = 17 / 64
+-
+-export function Logotype({
+- fill,
+- ...rest
+-}: {fill?: PathProps['fill']} & SvgProps) {
+- const pal = usePalette('default')
+- // @ts-ignore it's fiiiiine
+- const size = parseInt(rest.width || 32)
++import React from 'react'
++import {Text} from 'react-native'
++import {useTheme, atoms as a} from '#/alf'
+
++export function Logotype({width, fill, style}: any) {
++ const t = useTheme()
++ const fontSize = width ? parseInt(width) / 3.5 : 22
++
+ return (
+-
+-
+-
++
++ Aiat
++
+ )
+ }
diff --git a/ios/patching/004-social-app-ios-core.patch b/ios/patching/004-social-app-ios-core.patch
new file mode 100644
index 0000000..1f61904
--- /dev/null
+++ b/ios/patching/004-social-app-ios-core.patch
@@ -0,0 +1,72 @@
+diff --git a/src/App.native.tsx b/src/App.native.tsx
+index fb3008627..539ebc055 100644
+--- a/src/App.native.tsx
++++ b/src/App.native.tsx
+@@ -92,7 +92,7 @@ if (isAndroid) {
+ * Begin geolocation ASAP
+ */
+ Geo.resolve()
+-prefetchAgeAssuranceConfig()
++// // // prefetchAgeAssuranceConfig()
+
+ function InnerApp() {
+ const [isReady, setIsReady] = React.useState(false)
+diff --git a/src/routes.ts b/src/routes.ts
+index 1ed913bb2..c80340edb 100644
+--- a/src/routes.ts
++++ b/src/routes.ts
+@@ -71,8 +71,8 @@ export const router = new Router({
+ MiscellaneousNotificationSettings: '/settings/notifications/miscellaneous',
+ // support
+ Support: '/support',
+- PrivacyPolicy: '/support/privacy',
+- TermsOfService: '/support/tos',
++ PrivacyPolicy: 'https://syu.is/about/support/privacy-policy',
++ TermsOfService: 'https://syu.is/about/support/tos',
+ CommunityGuidelines: '/support/community-guidelines',
+ CopyrightPolicy: '/support/copyright',
+ // hashtags
+diff --git a/src/state/session/agent.ts b/src/state/session/agent.ts
+index 5c8ce3b97..ee85beb08 100644
+--- a/src/state/session/agent.ts
++++ 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],
diff --git a/ios/patching/005-social-app-ios-screens.patch b/ios/patching/005-social-app-ios-screens.patch
new file mode 100644
index 0000000..492e263
--- /dev/null
+++ b/ios/patching/005-social-app-ios-screens.patch
@@ -0,0 +1,593 @@
+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) {
+
+
+
+
+
+@@ -88,7 +88,7 @@ export function AboutSettingsScreen({}: Props) {
+
+
+
+
+
+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() {
+
+ Your account was found to be in violation of the{' '}
+
+- 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..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
+ 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 (
+-
+-
+-
+- )
+- } else {
+- return (
+-
+-
+-
+-
+-
+- )
+- }
++ return (
++
++
++
++ )
+ }
+
+-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(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
++ }, [onPressSelected, pinnedFeedInfos])
+
+- const renderTabBar = React.useCallback(
+- (props: RenderTabBarFnProps) => {
+- if (demoMode) {
+- return (
+-
+- )
+- }
+- return (
+-
+- )
+- },
+- [onPressSelected, pinnedFeedInfos, demoMode],
+- )
+-
+- 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),
+- mergeFeedSources: preferences.feedViewPrefs.lab_mergeFeedEnabled
+- ? preferences.savedFeeds
+- .filter(f => f.type === 'feed' || f.type === 'list')
+- .map(f => f.value)
+- : [],
+- }
+- }, [preferences])
+-
+- if (demoMode) {
+- return (
+-
+-
+-
+-
+- )
+- }
+-
+- return hasSession ? (
+-
+- {pinnedFeedInfos.length ? (
+- pinnedFeedInfos.map((feedInfo, index) => {
++ return (
++
++ {pinnedFeedInfos.map((feedInfo, index) => {
+ const feed = feedInfo.feedDescriptor
+ if (feed === 'following') {
+- return (
+-
+- )
++ return
+ }
+- const savedFeedConfig = feedInfo.savedFeed
+- return (
+-
+- )
+- })
+- ) : (
+-
+- )}
+-
+- ) : (
+-
+-
++ return
++ })}
+
+ )
+ }
+-
+-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 @@
+ import React from 'react'
+-import {View} from 'react-native'
+-import {msg, Trans} from '@lingui/macro'
+-import {useLingui} from '@lingui/react'
+-import {useFocusEffect} from '@react-navigation/native'
+-
+-import {usePalette} from '#/lib/hooks/usePalette'
+-import {
+- type CommonNavigatorParams,
+- type NativeStackScreenProps,
+-} from '#/lib/routes/types'
+-import {s} from '#/lib/styles'
+-import {useSetMinimalShellMode} from '#/state/shell'
+-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 * as Layout from '#/components/Layout'
+-import {ViewHeader} from '../com/util/ViewHeader'
+-
+-type Props = NativeStackScreenProps
+-export const PrivacyPolicyScreen = (_props: Props) => {
+- const pal = usePalette('default')
+- const {_} = useLingui()
+- const setMinimalShellMode = useSetMinimalShellMode()
+-
+- useFocusEffect(
+- React.useCallback(() => {
+- setMinimalShellMode(false)
+- }, [setMinimalShellMode]),
+- )
++import {useSetTitle} from '#/lib/hooks/useSetTitle'
+
++export function PrivacyPolicyScreen() {
++ useSetTitle('Privacy Policy')
+ return (
+
+-
+-
+-
+-
+-
+- The Privacy Policy has been moved to{' '}
+-
+-
+-
+-
+-
+-
++
+
+ )
+ }
+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 @@
+ import React from 'react'
+-import {View} from 'react-native'
+-import {msg, Trans} from '@lingui/macro'
+-import {useLingui} from '@lingui/react'
+-import {useFocusEffect} from '@react-navigation/native'
+-
+-import {usePalette} from '#/lib/hooks/usePalette'
+-import {
+- type CommonNavigatorParams,
+- type NativeStackScreenProps,
+-} from '#/lib/routes/types'
+-import {s} from '#/lib/styles'
+-import {useSetMinimalShellMode} from '#/state/shell'
+-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 * as Layout from '#/components/Layout'
+-import {ViewHeader} from '../com/util/ViewHeader'
+-
+-type Props = NativeStackScreenProps
+-export const TermsOfServiceScreen = (_props: Props) => {
+- const pal = usePalette('default')
+- const setMinimalShellMode = useSetMinimalShellMode()
+- const {_} = useLingui()
+-
+- useFocusEffect(
+- React.useCallback(() => {
+- setMinimalShellMode(false)
+- }, [setMinimalShellMode]),
+- )
++import {useSetTitle} from '#/lib/hooks/useSetTitle'
+
++export function TermsOfServiceScreen() {
++ useSetTitle('Terms of Service')
+ return (
+
+-
+-
+-
+-
+- The Terms of Service have been moved to {' '}
+-
+-
+-
+-
+-
++
+
+ )
+ }
diff --git a/ios/patching/006-social-app-ios-shell.patch b/ios/patching/006-social-app-ios-shell.patch
new file mode 100644
index 0000000..399df6f
--- /dev/null
+++ b/ios/patching/006-social-app-ios-shell.patch
@@ -0,0 +1,56 @@
+diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx
+index f76147ccf..36b4d7de1 100644
+--- a/src/view/shell/Drawer.tsx
++++ b/src/view/shell/Drawer.tsx
+@@ -292,17 +292,11 @@ let DrawerContent = ({}: React.PropsWithoutRef<{}>): React.ReactNode => {
+ <>
+
+
+-
+
+
+-
+-
+
+-
+-
+-
+- Feedback
+-
+-
++{/* Feedback button removed for syu.is */}
+
++ to="https://syu.is/about/support/tos">
+ Terms of Service
+
+
+ Privacy Policy
+
diff --git a/ios/patching/007-social-app-ios-misc.patch b/ios/patching/007-social-app-ios-misc.patch
new file mode 100644
index 0000000..7dc2352
--- /dev/null
+++ b/ios/patching/007-social-app-ios-misc.patch
@@ -0,0 +1,32 @@
+diff --git a/plugins/notificationsExtension/withNotificationsExtension.js b/plugins/notificationsExtension/withNotificationsExtension.js
+index 6a00cfd23..f91decc08 100644
+--- a/plugins/notificationsExtension/withNotificationsExtension.js
++++ 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/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
++++ b/src/components/PolicyUpdateOverlay/updates/202508/index.tsx
+@@ -26,12 +26,12 @@ export function Content({state}: {state: PolicyUpdateState}) {
+ const links = {
+ terms: {
+ overridePresentation: false,
+- to: `https://bsky.social/about/support/tos`,
++ to: `https://syu.is/about/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`,
+ label: _(msg`Privacy Policy`),
+ },
+ copyright: {
diff --git a/ios/patching/009-social-app-ios-license.patch b/ios/patching/009-social-app-ios-license.patch
new file mode 100644
index 0000000..ae4e3ee
--- /dev/null
+++ b/ios/patching/009-social-app-ios-license.patch
@@ -0,0 +1,174 @@
+diff --git a/src/Navigation.tsx b/src/Navigation.tsx
+index fa33a9d56..13af087c2 100644
+--- a/src/Navigation.tsx
++++ b/src/Navigation.tsx
+@@ -62,6 +62,7 @@ import {NotFoundScreen} from '#/view/screens/NotFound'
+ import {NotificationsScreen} from '#/view/screens/Notifications'
+ import {PostThreadScreen} from '#/view/screens/PostThread'
+ import {PrivacyPolicyScreen} from '#/view/screens/PrivacyPolicy'
++import {LicenseScreen} from '#/view/screens/License'
+ import {ProfileScreen} from '#/view/screens/Profile'
+ import {ProfileFeedLikedByScreen} from '#/view/screens/ProfileFeedLikedBy'
+ import {Storybook} from '#/view/screens/Storybook'
+@@ -335,6 +336,11 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) {
+ getComponent={() => TermsOfServiceScreen}
+ options={{title: title(msg`Terms of Service`)}}
+ />
++ LicenseScreen}
++ options={{title: title(msg`License`)}}
++ />
+ CommunityGuidelinesScreen}
+diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts
+index c315a8341..9b2f50a83 100644
+--- a/src/lib/routes/types.ts
++++ b/src/lib/routes/types.ts
+@@ -39,6 +39,7 @@ export type CommonNavigatorParams = {
+ Support: undefined
+ PrivacyPolicy: undefined
+ TermsOfService: undefined
++ License: undefined
+ CommunityGuidelines: undefined
+ CopyrightPolicy: undefined
+ LanguageSettings: undefined
+diff --git a/src/view/screens/License.tsx b/src/view/screens/License.tsx
+new file mode 100644
+index 000000000..87f52a972
+--- /dev/null
++++ b/src/view/screens/License.tsx
+@@ -0,0 +1,132 @@
++import React from 'react'
++import { ScrollView, Text as RNText, StyleSheet } from 'react-native'
++import * as Layout from '#/components/Layout'
++import {useSetTitle} from '#/lib/hooks/useSetTitle'
++import {atoms as a, useTheme} from '#/alf'
++
++export function LicenseScreen() {
++ useSetTitle('License')
++ const t = useTheme()
++
++ return (
++
++
++
++
++ License
++
++
++
++
++
++
++ This application is based on Bluesky Social App.
++
++
++
++ https://github.com/bluesky-social/social-app
++
++
++ MIT License
++
++
++ Copyright (c) 2022-2025 Bluesky PBC
++
++
++
++ Permission is hereby granted, free of charge, to any person obtaining a copy
++ of this software and associated documentation files (the "Software"), to deal
++ in the Software without restriction, including without limitation the rights
++ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
++ copies of the Software, and to permit persons to whom the Software is
++ furnished to do so, subject to the following conditions:
++
++
++
++ The above copyright notice and this permission notice shall be included in all
++ copies or substantial portions of the Software.
++
++
++
++ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
++ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
++ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
++ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
++ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
++ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
++ SOFTWARE.
++
++
++ ๆฅๆฌ่ช่จณ๏ผๅ่๏ผ
++
++
++ ๆฌใฝใใใฆใงใขใใใณ้ข้ฃๆๆธใใกใคใซ๏ผไปฅไธใใฝใใใฆใงใขใ๏ผใฎใณใใผใๅๅพใใ
++ ใในใฆใฎไบบใซๅฏพใใใฝใใใฆใงใขใ็กๅถ้ใซๆฑใใใจใ็กๅใง่จฑๅฏใใพใใใใใซใฏใ
++ ใฝใใใฆใงใขใฎใณใใผใไฝฟ็จใ่ค่ฃฝใๅคๆดใ็ตๅใๅ
ฌ้ใ้
ๅธใใตใใฉใคใปใณในใ
++ ใใใณ/ใพใใฏ่ฒฉๅฃฒใใๆจฉๅฉใใชใใณใซใฝใใใฆใงใขใๆไพใใ็ธๆใซใใใใ่ก็บใ
++ ่จฑๅฏใใๆจฉๅฉใๅซใพใใพใใใใใใใซ้ๅฎใใใพใใใ
++
++
++
++ ไธ่จใฎ่ไฝๆจฉ่กจ็คบใใใณๆฌ่จฑ่ซพ่กจ็คบใใใฝใใใฆใงใขใฎใในใฆใฎใณใใผใพใใฏ
++ ้่ฆใช้จๅใซ่จ่ผใใใใฎใจใใพใใ
++
++
++
++ ใฝใใใฆใงใขใฏใ็พ็ถใฎใพใพใใงๆไพใใใๆ็คบ้ป็คบใๅใใใๅๅๆงใ็นๅฎ็ฎ็ใธใฎ
++ ้ฉๅๆงใใใใณๆจฉๅฉ้ไพตๅฎณใซใคใใฆใฎไฟ่จผใๅซใใใใใชใ็จฎ้กใฎไฟ่จผใใชใใใพใใใ
++ ใใใชใๅ ดๅใซใใใฆใใไฝ่
ใพใใฏ่ไฝๆจฉ่
ใฏใๅฅ็ด่ก็บใไธๆณ่ก็บใใพใใฏใใไปฅๅคใง
++ ใใใใจใใฝใใใฆใงใขใซ่ตทๅ ใพใใฏ้ข้ฃใใใใใใฏใฝใใใฆใงใขใฎไฝฟ็จใพใใฏ
++ ใใฎไปใฎๆฑใใซใใฃใฆ็ใใไธๅใฎ่ซๆฑใๆๅฎณใใใฎไปใฎ็พฉๅใซใคใใฆ่ฒฌไปปใ่ฒ ใใชใใใฎใจใใพใใ
++
++
++
++ Original License: https://github.com/bluesky-social/social-app/blob/main/LICENSE
++
++
++
++
++ )
++}
++
++const styles = StyleSheet.create({
++ title: {
++ fontSize: 24,
++ fontWeight: 'bold',
++ marginBottom: 16,
++ },
++ text: {
++ fontSize: 14,
++ marginBottom: 12,
++ lineHeight: 20,
++ },
++ link: {
++ fontSize: 14,
++ marginBottom: 12,
++ color: '#0066cc',
++ },
++ sectionTitle: {
++ fontSize: 18,
++ fontWeight: 'bold',
++ marginTop: 16,
++ marginBottom: 12,
++ },
++ sectionTitle2: {
++ fontSize: 18,
++ fontWeight: 'bold',
++ marginTop: 24,
++ marginBottom: 12,
++ },
++ mono: {
++ fontSize: 14,
++ marginBottom: 12,
++ fontFamily: 'monospace',
++ },
++ footer: {
++ fontSize: 12,
++ marginTop: 24,
++ color: '#666666',
++ },
++})
diff --git a/ios/patching/010-social-app-ios-remove-contact-support.patch b/ios/patching/010-social-app-ios-remove-contact-support.patch
new file mode 100644
index 0000000..6392d9f
--- /dev/null
+++ b/ios/patching/010-social-app-ios-remove-contact-support.patch
@@ -0,0 +1,25 @@
+diff --git a/src/screens/Signup/index.tsx b/src/screens/Signup/index.tsx
+index aa6cd4156..37c7a38b0 100644
+--- a/src/screens/Signup/index.tsx
++++ b/src/screens/Signup/index.tsx
+@@ -211,20 +211,6 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
+ a.align_center,
+ ]}>
+
+-
+- Having trouble? {' '}
+-
+- Contact support
+-
+-
+
+
+
diff --git a/ios/patching/011-social-app-ios-splash-license-footer.patch b/ios/patching/011-social-app-ios-splash-license-footer.patch
new file mode 100644
index 0000000..a127fa0
--- /dev/null
+++ b/ios/patching/011-social-app-ios-splash-license-footer.patch
@@ -0,0 +1,77 @@
+diff --git a/src/routes.ts b/src/routes.ts
+--- a/src/routes.ts
++++ b/src/routes.ts
+@@ -74,6 +74,7 @@ export const router = new Router({
+ PrivacyPolicy: 'https://syu.is/about/support/privacy-policy',
+ TermsOfService: 'https://syu.is/about/support/tos',
+ CommunityGuidelines: '/support/community-guidelines',
++ License: 'https://syu.is/about/support/license',
+ CopyrightPolicy: '/support/copyright',
+ // hashtags
+ Hashtag: '/hashtag/:tag',
+diff --git a/src/view/com/auth/SplashScreen.tsx b/src/view/com/auth/SplashScreen.tsx
+--- 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 = ({
+
+
+
+-
+-
+- What's up?
+-
+
+
+
+
+
++
++ Linking.openURL('https://syu.is/about/support/license')}>
++
++ License
++
++
++
++
++ ยฉ syui
++
+
+
+
+diff --git a/src/view/com/auth/SplashScreen.web.tsx b/src/view/com/auth/SplashScreen.web.tsx
+--- a/src/view/com/auth/SplashScreen.web.tsx
++++ b/src/view/com/auth/SplashScreen.web.tsx
+@@ -94,14 +94,6 @@ export const SplashScreen = ({
+
+ )}
+
+-
+- What's up?
+-
+
+
+ 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
+
+
+- Linking.openURL(HELP_DESK_URL)}
+- label={_(msg`Help`)}
+- accessibilityHint={_(msg`Opens helpdesk in browser`)}>
+-
+-
+- Help
+-
+-
+-
+
+
+
diff --git a/ios/patching/019-social-app-ios-entitlements-plugin.patch b/ios/patching/019-social-app-ios-entitlements-plugin.patch
new file mode 100644
index 0000000..bc2cbf6
--- /dev/null
+++ b/ios/patching/019-social-app-ios-entitlements-plugin.patch
@@ -0,0 +1,31 @@
+diff --git a/plugins/withCodeSignEntitlements.js b/plugins/withCodeSignEntitlements.js
+new file mode 100644
+index 000000000..b03b6bd68
+--- /dev/null
++++ b/plugins/withCodeSignEntitlements.js
+@@ -0,0 +1,25 @@
++/* eslint-disable @typescript-eslint/no-var-requires */
++const { withXcodeProject } = require('@expo/config-plugins')
++
++const withCodeSignEntitlements = (config) => {
++ return withXcodeProject(config, (config) => {
++ const xcodeProject = config.modResults
++ const configurations = xcodeProject.pbxXCBuildConfigurationSection()
++
++ for (const key in configurations) {
++ const configuration = configurations[key]
++ if (
++ configuration.buildSettings &&
++ configuration.comment &&
++ !configuration.comment.includes('TEST')
++ ) {
++ configuration.buildSettings.CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION =
++ 'YES'
++ }
++ }
++
++ return config
++ })
++}
++
++module.exports = withCodeSignEntitlements
diff --git a/ios/patching/020-social-app-ios-bypass-age-assurance.patch b/ios/patching/020-social-app-ios-bypass-age-assurance.patch
new file mode 100644
index 0000000..9eca21d
--- /dev/null
+++ b/ios/patching/020-social-app-ios-bypass-age-assurance.patch
@@ -0,0 +1,30 @@
+diff --git a/src/ageAssurance/index.tsx b/src/ageAssurance/index.tsx
+index 9a0a9c9d5..5a6563e52 100644
+--- a/src/ageAssurance/index.tsx
++++ b/src/ageAssurance/index.tsx
+@@ -88,19 +88,16 @@ function InnerProvider({children}: {children: React.ReactNode}) {
+ return (
+ {
+- const chatDisabled = state.access !== AgeAssuranceAccess.Full
+- const isUnderage = data?.birthdate
+- ? isUserUnderAdultAge(data.birthdate)
+- : true
+- const adultContentDisabled =
+- state.access !== AgeAssuranceAccess.Full || isUnderage
+ return {
+ Access: AgeAssuranceAccess,
+ Status: AgeAssuranceStatus,
+- state,
++ state: {
++ ...state,
++ access: AgeAssuranceAccess.Full,
++ },
+ flags: {
+- adultContentDisabled,
+- chatDisabled,
++ adultContentDisabled: false,
++ chatDisabled: false,
+ },
+ }
+ }, [state, data])}>
diff --git a/ios/patching/021-social-app-ios-clean-feed.patch b/ios/patching/021-social-app-ios-clean-feed.patch
new file mode 100644
index 0000000..155b0a2
--- /dev/null
+++ b/ios/patching/021-social-app-ios-clean-feed.patch
@@ -0,0 +1,247 @@
+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'
+ import {Text} from '../util/text/Text'
+
+ export function DiscoverFallbackHeader() {
+- const pal = usePalette('default')
+- return (
+-
+-
+-
+-
+-
+-
+-
+- We ran out of posts from your follows. Here's the latest from{' '}
+-
+- .
+-
+-
+-
+-
+- )
++ 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 @@
+ 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 {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 {Button} from '../util/forms/Button'
+ import {Text} from '../util/text/Text'
+
+ export function FollowingEmptyState() {
+ const pal = usePalette('default')
+- const palInverted = usePalette('inverted')
+- const navigation = useNavigation()
+-
+- const onPressFindAccounts = React.useCallback(() => {
+- if (isWeb) {
+- navigation.navigate('Search', {})
+- } else {
+- navigation.navigate('SearchTab')
+- navigation.popToTop()
+- }
+- }, [navigation])
+-
+- const onPressDiscoverFeeds = React.useCallback(() => {
+- navigation.navigate('Feeds')
+- }, [navigation])
+
+ return (
+
+@@ -45,36 +22,6 @@ export function FollowingEmptyState() {
+ happening.
+
+
+-
+-
+- Find accounts to follow
+-
+-
+-
+-
+-
+- You can also discover new Custom Feeds to follow.
+-
+-
+-
+- Discover new custom feeds
+-
+-
+-
+
+
+ )
+@@ -98,13 +45,4 @@ const styles = StyleSheet.create({
+ marginLeft: 'auto',
+ marginRight: 'auto',
+ },
+- emptyBtn: {
+- marginVertical: 20,
+- flexDirection: 'row',
+- alignItems: 'center',
+- justifyContent: 'space-between',
+- paddingVertical: 18,
+- paddingHorizontal: 24,
+- 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 @@
+ 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 {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 {Button} from '../util/forms/Button'
+ import {Text} from '../util/text/Text'
+
+ export function FollowingEndOfFeed() {
+ const pal = usePalette('default')
+- const palInverted = usePalette('inverted')
+- const navigation = useNavigation()
+-
+- const onPressFindAccounts = React.useCallback(() => {
+- if (isWeb) {
+- navigation.navigate('Search', {})
+- } else {
+- navigation.navigate('SearchTab')
+- navigation.popToTop()
+- }
+- }, [navigation])
+-
+- const onPressDiscoverFeeds = React.useCallback(() => {
+- navigation.navigate('Feeds')
+- }, [navigation])
+
+ return (
+
+
+
+-
+- You've reached the end of your feed! Find some more accounts to
+- follow.
+-
+-
+-
+-
+- Find accounts to follow
+-
+-
+-
+-
+-
+- You can also discover new Custom Feeds to follow.
++ You've reached the end of your feed!
+
+-
+-
+- Discover new custom feeds
+-
+-
+-
+
+
+ )
+@@ -93,13 +37,4 @@ const styles = StyleSheet.create({
+ width: '100%',
+ maxWidth: 460,
+ },
+- emptyBtn: {
+- marginVertical: 20,
+- flexDirection: 'row',
+- alignItems: 'center',
+- justifyContent: 'space-between',
+- paddingVertical: 18,
+- paddingHorizontal: 24,
+- 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 = ({
+ } else if (row.type === 'feedShutdownMsg') {
+ return
+ } else if (row.type === 'interstitialFollows') {
+- return
++ return null
+ } else if (row.type === 'interstitialProgressGuide') {
+ return
+ } else if (row.type === 'ageAssuranceBanner') {
diff --git a/ios/patching/022-social-app-ios-bskyweb-support-pages.patch b/ios/patching/022-social-app-ios-bskyweb-support-pages.patch
new file mode 100644
index 0000000..0c983ac
--- /dev/null
+++ b/ios/patching/022-social-app-ios-bskyweb-support-pages.patch
@@ -0,0 +1,51 @@
+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 {
+ 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("/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)
++}
diff --git a/ios/patching/023-social-app-ios-disable-dm.patch b/ios/patching/023-social-app-ios-disable-dm.patch
new file mode 100644
index 0000000..2f2236e
--- /dev/null
+++ b/ios/patching/023-social-app-ios-disable-dm.patch
@@ -0,0 +1,70 @@
+diff --git a/src/state/messages/events/index.tsx b/src/state/messages/events/index.tsx
+index 2ff0784ae..dc314ecc5 100644
+--- a/src/state/messages/events/index.tsx
++++ b/src/state/messages/events/index.tsx
+@@ -10,13 +10,7 @@ const MessagesEventBusContext = React.createContext(
+ MessagesEventBusContext.displayName = 'MessagesEventBusContext'
+
+ export function useMessagesEventBus() {
+- const ctx = React.useContext(MessagesEventBusContext)
+- if (!ctx) {
+- throw new Error(
+- 'useMessagesEventBus must be used within a MessagesEventBusProvider',
+- )
+- }
+- return ctx
++ return React.useContext(MessagesEventBusContext)
+ }
+
+ export function MessagesEventBusProvider({
+@@ -24,18 +18,11 @@ export function MessagesEventBusProvider({
+ }: {
+ children: React.ReactNode
+ }) {
+- const {currentAccount} = useSession()
+-
+- if (!currentAccount) {
+- return (
+-
+- {children}
+-
+- )
+- }
+-
++ // DM functionality is disabled for syu.is
+ return (
+- {children}
++
++ {children}
++
+ )
+ }
+
+diff --git a/src/state/queries/messages/list-conversations.tsx b/src/state/queries/messages/list-conversations.tsx
+index c5457d1cb..5bc37bdce 100644
+--- a/src/state/queries/messages/list-conversations.tsx
++++ b/src/state/queries/messages/list-conversations.tsx
+@@ -74,17 +74,12 @@ export function useListConvos() {
+
+ const empty = {accepted: [], request: []}
+ export function ListConvosProvider({children}: {children: React.ReactNode}) {
+- const {hasSession} = useSession()
+-
+- if (!hasSession) {
+- return (
+-
+- {children}
+-
+- )
+- }
+-
+- return {children}
++ // DM functionality is disabled for syu.is - always return empty
++ return (
++
++ {children}
++
++ )
+ }
+
+ export function ListConvosProviderInner({
diff --git a/ios/patching/024-social-app-ios-disable-external-services.patch b/ios/patching/024-social-app-ios-disable-external-services.patch
new file mode 100644
index 0000000..66bd786
--- /dev/null
+++ b/ios/patching/024-social-app-ios-disable-external-services.patch
@@ -0,0 +1,15 @@
+diff --git a/src/env/common.ts b/src/env/common.ts
+--- a/src/env/common.ts
++++ b/src/env/common.ts
+@@ -107,9 +107,8 @@ export const GCP_PROJECT_ID: number =
+ /**
+ * 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
+ */
+ export const BAPP_CONFIG_DEV_URL = process.env.BAPP_CONFIG_DEV_URL
+ export const BAPP_CONFIG_PROD_URL = `https://ip.bsky.app`
+-export const BAPP_CONFIG_URL = IS_DEV
+- ? (BAPP_CONFIG_DEV_URL ?? BAPP_CONFIG_PROD_URL)
+- : BAPP_CONFIG_PROD_URL
++export const BAPP_CONFIG_URL = null
diff --git a/ios/patching/025-social-app-ios-bskyweb-title.patch b/ios/patching/025-social-app-ios-bskyweb-title.patch
new file mode 100644
index 0000000..9b674e6
--- /dev/null
+++ b/ios/patching/025-social-app-ios-bskyweb-title.patch
@@ -0,0 +1,91 @@
+diff --git a/bskyweb/templates/base.html b/bskyweb/templates/base.html
+--- a/bskyweb/templates/base.html
++++ b/bskyweb/templates/base.html
+@@ -7,9 +7,9 @@
+
+-
+-
+- {%- block head_title -%}Bluesky{%- endblock -%}
++
++
++ {%- block head_title -%}syu.is{%- endblock -%}
+
+
+
+@@ -121,7 +121,7 @@
+
+ JavaScript Required
+ This is a heavily interactive web application, and JavaScript is required. Simple HTML interfaces are possible, but that is not what this is.
+-
Learn more about Bluesky at bsky.social and atproto.com .
++
Learn more at syu.is and atproto.com .
+ {% block noscript_extra %}{% endblock %}
+
+ {% endblock -%}
+diff --git a/bskyweb/templates/home.html b/bskyweb/templates/home.html
+--- a/bskyweb/templates/home.html
++++ b/bskyweb/templates/home.html
+@@ -1,17 +1,17 @@
+ {% extends "base.html" %}
+
+-{% block head_title %}Bluesky{% endblock %}
++{% block head_title %}syu.is{% endblock %}
+
+ {% block html_head_extra -%}
+-
+-
++
++
+
+
+
+
+
+-
+-
+-
++
++
++
+
+
+diff --git a/bskyweb/templates/error.html b/bskyweb/templates/error.html
+--- a/bskyweb/templates/error.html
++++ b/bskyweb/templates/error.html
+@@ -1,6 +1,6 @@
+ {% extends "base.html" %}
+
+-{% block head_title %}Error {{ statusCode }} - Bluesky{% endblock %}
++{% block head_title %}Error {{ statusCode }} - syu.is{% endblock %}
+
+ {% block noscript_extra %}
+ {%- if statusCode == 404 %}
+diff --git a/bskyweb/templates/starterpack.html b/bskyweb/templates/starterpack.html
+--- a/bskyweb/templates/starterpack.html
++++ b/bskyweb/templates/starterpack.html
+@@ -17,8 +17,8 @@
+
+
+ {%- else -%}
+-
+-
++
++
+ {% endif -%}
+
+
+diff --git a/web/index.html b/web/index.html
+--- a/web/index.html
++++ b/web/index.html
+@@ -14,8 +14,8 @@
+
+-
+-
++
++
+ %WEB_TITLE%
+
+
diff --git a/ios/patching/027-social-app-ios-remove-birthdate.patch b/ios/patching/027-social-app-ios-remove-birthdate.patch
new file mode 100644
index 0000000..4e2fc17
--- /dev/null
+++ b/ios/patching/027-social-app-ios-remove-birthdate.patch
@@ -0,0 +1,101 @@
+diff --git a/src/screens/Signup/StepInfo/index.tsx b/src/screens/Signup/StepInfo/index.tsx
+--- a/src/screens/Signup/StepInfo/index.tsx
++++ b/src/screens/Signup/StepInfo/index.tsx
+@@ -7,11 +7,9 @@
+
+ import {isEmailMaybeInvalid} from '#/lib/strings/email'
+ import {logger} from '#/logger'
+-import {is13, is18, useSignupContext} from '#/screens/Signup/state'
++import {useSignupContext} from '#/screens/Signup/state'
+ import {Policies} from '#/screens/Signup/StepInfo/Policies'
+ import {atoms as a, native} from '#/alf'
+-import * as DateField from '#/components/forms/DateField'
+-import {type DateFieldRef} from '#/components/forms/DateField/types'
+ import {FormError} from '#/components/forms/FormError'
+ import {HostingProvider} from '#/components/forms/HostingProvider'
+ import * as TextField from '#/components/forms/TextField'
+@@ -22,16 +20,6 @@
+ import {usePreemptivelyCompleteActivePolicyUpdate} from '#/components/PolicyUpdateOverlay/usePreemptivelyCompleteActivePolicyUpdate'
+ import {BackNextButtons} from '../BackNextButtons'
+
+-function sanitizeDate(date: Date): Date {
+- if (!date || date.toString() === 'Invalid Date') {
+- logger.error(`Create account: handled invalid date for birthDate`, {
+- hasDate: !!date,
+- })
+- return new Date()
+- }
+- return date
+-}
+-
+ export function StepInfo({
+ onPressBack,
+ isServerError,
+@@ -55,7 +43,6 @@
+
+ const emailInputRef = useRef(null)
+ const passwordInputRef = useRef(null)
+- const birthdateInputRef = useRef(null)
+
+ const [hasWarnedEmail, setHasWarnedEmail] = React.useState(false)
+
+@@ -76,10 +63,6 @@
+ const emailChanged = prevEmailValueRef.current !== email
+ const password = passwordValueRef.current
+
+- if (!is13(state.dateOfBirth)) {
+- return
+- }
+-
+ if (state.serviceDescription?.inviteCodeRequired && !inviteCode) {
+ return dispatch({
+ type: 'setError',
+@@ -246,44 +229,21 @@
+ secureTextEntry
+ autoComplete="new-password"
+ autoCapitalize="none"
+- returnKeyType="next"
+- submitBehavior={native('blurAndSubmit')}
+- onSubmitEditing={native(() =>
+- birthdateInputRef.current?.focus(),
+- )}
++ returnKeyType="done"
+ passwordRules="minlength: 8;"
+ />
+
+
+-
+-
+- Your birth date
+-
+- {
+- dispatch({
+- type: 'setDateOfBirth',
+- value: sanitizeDate(new Date(date)),
+- })
+- }}
+- label={_(msg`Date of birth`)}
+- accessibilityHint={_(msg`Select your date of birth`)}
+- maximumDate={new Date()}
+- />
+-
+
+ >
+ ) : undefined}
+
+ ({
+- key: `popularFeed:${feed.uri}`,
+- type: 'popularFeed',
+- feedUri: feed.uri,
+- feed,
+- })),
+- )
+- }
+- }
+- } else {
+- if (isPopularFeedsFetching && !popularFeeds?.pages) {
+- slices.push({
+- key: 'popularFeedsLoading',
+- type: 'popularFeedsLoading',
+- })
+- } else {
+- if (!popularFeeds?.pages) {
+- slices.push({
+- key: 'popularFeedsNoResults',
+- type: 'popularFeedsNoResults',
+- })
+- } else {
+- for (const page of popularFeeds.pages || []) {
+- slices = slices.concat(
+- page.feeds.map(feed => ({
+- key: `popularFeed:${feed.uri}`,
+- type: 'popularFeed',
+- feedUri: feed.uri,
+- feed,
+- })),
+- )
+- }
+-
+- if (isPopularFeedsFetchingNextPage) {
+- slices.push({
+- key: 'popularFeedsLoadingMore',
+- type: 'popularFeedsLoadingMore',
+- })
+- }
+- }
+- }
+- }
+- }
+- }
+-
+ return slices
+ }, [
+ hasSession,
diff --git a/ios/patching/030-social-app-ios-appinfo.patch b/ios/patching/030-social-app-ios-appinfo.patch
new file mode 100644
index 0000000..f9f62c2
--- /dev/null
+++ b/ios/patching/030-social-app-ios-appinfo.patch
@@ -0,0 +1,361 @@
+diff --git a/src/Navigation.tsx b/src/Navigation.tsx
+--- a/src/Navigation.tsx
++++ b/src/Navigation.tsx
+@@ -63,6 +63,7 @@ import {NotificationsScreen} from '#/view/screens/Notifications'
+ import {PostThreadScreen} from '#/view/screens/PostThread'
+ import {PrivacyPolicyScreen} from '#/view/screens/PrivacyPolicy'
+ import {LicenseScreen} from '#/view/screens/License'
++import {AppInfoScreen} from '#/view/screens/AppInfo'
+ import {ProfileScreen} from '#/view/screens/Profile'
+ import {ProfileFeedLikedByScreen} from '#/view/screens/ProfileFeedLikedBy'
+ import {Storybook} from '#/view/screens/Storybook'
+@@ -341,6 +342,11 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) {
+ getComponent={() => LicenseScreen}
+ options={{title: title(msg`License`)}}
+ />
++ AppInfoScreen}
++ options={{title: title(msg`App Info`)}}
++ />
+ CommunityGuidelinesScreen}
+diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts
+--- a/src/lib/routes/types.ts
++++ b/src/lib/routes/types.ts
+@@ -40,6 +40,7 @@ export type CommonNavigatorParams = {
+ PrivacyPolicy: undefined
+ TermsOfService: undefined
+ License: undefined
++ AppInfo: undefined
+ CommunityGuidelines: undefined
+ CopyrightPolicy: undefined
+ LanguageSettings: undefined
+diff --git a/src/routes.ts b/src/routes.ts
+--- a/src/routes.ts
++++ b/src/routes.ts
+@@ -75,6 +75,7 @@ export const router = new Router({
+ TermsOfService: 'https://syu.is/about/support/tos',
+ CommunityGuidelines: '/support/community-guidelines',
+ License: 'https://syu.is/about/support/license',
++ AppInfo: 'https://syu.is/about/support/app',
+ CopyrightPolicy: '/support/copyright',
+ // hashtags
+ Hashtag: '/hashtag/:tag',
+diff --git a/src/view/screens/AppInfo.tsx b/src/view/screens/AppInfo.tsx
+new file mode 100644
+index 000000000..000000001
+--- /dev/null
++++ b/src/view/screens/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://github.com/syui')}
++ style={[styles.linkRow, t.atoms.border_contrast_low]}>
++
++ GitHub
++
++
++ github.com/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/patching/032-social-app-ios-feed-loggedout.patch b/ios/patching/032-social-app-ios-feed-loggedout.patch
new file mode 100644
index 0000000..1e63085
--- /dev/null
+++ b/ios/patching/032-social-app-ios-feed-loggedout.patch
@@ -0,0 +1,71 @@
+diff --git a/src/lib/api/feed/custom.ts b/src/lib/api/feed/custom.ts
+index 18bb8c8f0..bab286d7a 100644
+--- a/src/lib/api/feed/custom.ts
++++ b/src/lib/api/feed/custom.ts
+@@ -5,6 +5,7 @@ import {
+ jsonStringToLex,
+ } from '@atproto/api'
+
++import {PUBLIC_APPVIEW} from '#/lib/constants'
+ import {
+ getAppLanguageAsContentLanguage,
+ getContentLanguages,
+@@ -12,6 +13,17 @@ import {
+ import {type FeedAPI, type FeedAPIResponse} from './types'
+ import {createBskyTopicsHeader, isBlueskyOwnedFeed} from './utils'
+
++// Check if the feed is hosted on syu.is network
++function isSyuIsFeed(feedUri: string): boolean {
++ return feedUri.includes('did:plc:6qyecktefllvenje24fcxnie') || feedUri.includes('syu.is')
++}
++
++// Check if the agent is connected to syu.is
++function isAgentOnSyuIs(agent: BskyAgent): boolean {
++ const serviceUrl = agent.service?.toString() || ''
++ return serviceUrl.includes('syu.is')
++}
++
+ export class CustomFeedAPI implements FeedAPI {
+ agent: BskyAgent
+ params: GetCustomFeed.QueryParams
+@@ -54,8 +66,12 @@ export class CustomFeedAPI implements FeedAPI {
+ const agent = this.agent
+ const isBlueskyOwned = isBlueskyOwnedFeed(this.params.feed)
+
+- const res = agent.did
+- ? await this.agent.app.bsky.feed.getFeed(
++ // For syu.is feeds accessed from non-syu.is accounts, use PUBLIC_APPVIEW
++ const needsPublicAppView = isSyuIsFeed(this.params.feed) && !isAgentOnSyuIs(agent)
++
++ const res = !agent.did || needsPublicAppView
++ ? await loggedOutFetch({...this.params, cursor, limit})
++ : await this.agent.app.bsky.feed.getFeed(
+ {
+ ...this.params,
+ cursor,
+@@ -70,7 +86,6 @@ export class CustomFeedAPI implements FeedAPI {
+ },
+ },
+ )
+- : await loggedOutFetch({...this.params, cursor, limit})
+ if (res.success) {
+ // NOTE
+ // some custom feeds fail to enforce the pagination limit
+@@ -120,7 +135,7 @@ async function loggedOutFetch({
+
+ // manually construct fetch call so we can add the `lang` cache-busting param
+ let res = await fetch(
+- `https://api.bsky.app/xrpc/app.bsky.feed.getFeed?feed=${feed}${
++ `${PUBLIC_APPVIEW}/xrpc/app.bsky.feed.getFeed?feed=${encodeURIComponent(feed)}${
+ cursor ? `&cursor=${cursor}` : ''
+ }&limit=${limit}&lang=${contentLangs}`,
+ {
+@@ -140,7 +155,7 @@ async function loggedOutFetch({
+
+ // no data, try again with language headers removed
+ res = await fetch(
+- `https://api.bsky.app/xrpc/app.bsky.feed.getFeed?feed=${feed}${
++ `${PUBLIC_APPVIEW}/xrpc/app.bsky.feed.getFeed?feed=${encodeURIComponent(feed)}${
+ cursor ? `&cursor=${cursor}` : ''
+ }&limit=${limit}`,
+ {method: 'GET', headers: {'Accept-Language': '', ...labelersHeader}},
diff --git a/ios/patching/033-social-app-ios-hide-profile-tabs.patch b/ios/patching/033-social-app-ios-hide-profile-tabs.patch
new file mode 100644
index 0000000..aba88e3
--- /dev/null
+++ b/ios/patching/033-social-app-ios-hide-profile-tabs.patch
@@ -0,0 +1,23 @@
+diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
+index 123456789..987654321 100644
+--- a/src/view/screens/Profile.tsx
++++ b/src/view/screens/Profile.tsx
+@@ -218,13 +218,13 @@ function ProfileScreenLoaded({
+ const showPostsTab = true
+ const showRepliesTab = hasSession
+ const showMediaTab = !hasLabeler
+- const showVideosTab = !hasLabeler
++ const showVideosTab = false
+ const showLikesTab = isMe
+ const feedGenCount = profile.associated?.feedgens || 0
+- const showFeedsTab = isMe || feedGenCount > 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/patching/License.tsx b/ios/patching/License.tsx
new file mode 100644
index 0000000..f98cd6a
--- /dev/null
+++ b/ios/patching/License.tsx
@@ -0,0 +1,86 @@
+import React from 'react'
+import { ScrollView } from 'react-native'
+import * as Layout from '#/components/Layout'
+import {useSetTitle} from '#/lib/hooks/useSetTitle'
+import {atoms as a, useTheme} from '#/alf'
+import {Text} from '#/components/Typography'
+
+export function LicenseScreen() {
+ useSetTitle('License')
+ const t = useTheme()
+
+ return (
+
+
+ License
+
+
+ This application is based on Bluesky Social App.
+
+
+
+ https://github.com/bluesky-social/social-app
+
+
+ MIT License
+
+
+ Copyright (c) 2022-2025 Bluesky PBC
+
+
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+
+
+ ๆฅๆฌ่ช่จณ๏ผๅ่๏ผ
+
+
+ ๆฌใฝใใใฆใงใขใใใณ้ข้ฃๆๆธใใกใคใซ๏ผไปฅไธใใฝใใใฆใงใขใ๏ผใฎใณใใผใๅๅพใใ
+ ใในใฆใฎไบบใซๅฏพใใใฝใใใฆใงใขใ็กๅถ้ใซๆฑใใใจใ็กๅใง่จฑๅฏใใพใใใใใซใฏใ
+ ใฝใใใฆใงใขใฎใณใใผใไฝฟ็จใ่ค่ฃฝใๅคๆดใ็ตๅใๅ
ฌ้ใ้
ๅธใใตใใฉใคใปใณในใ
+ ใใใณ/ใพใใฏ่ฒฉๅฃฒใใๆจฉๅฉใใชใใณใซใฝใใใฆใงใขใๆไพใใ็ธๆใซใใใใ่ก็บใ
+ ่จฑๅฏใใๆจฉๅฉใๅซใพใใพใใใใใใใซ้ๅฎใใใพใใใ
+
+
+
+ ไธ่จใฎ่ไฝๆจฉ่กจ็คบใใใณๆฌ่จฑ่ซพ่กจ็คบใใใฝใใใฆใงใขใฎใในใฆใฎใณใใผใพใใฏ
+ ้่ฆใช้จๅใซ่จ่ผใใใใฎใจใใพใใ
+
+
+
+ ใฝใใใฆใงใขใฏใ็พ็ถใฎใพใพใใงๆไพใใใๆ็คบ้ป็คบใๅใใใๅๅๆงใ็นๅฎ็ฎ็ใธใฎ
+ ้ฉๅๆงใใใใณๆจฉๅฉ้ไพตๅฎณใซใคใใฆใฎไฟ่จผใๅซใใใใใชใ็จฎ้กใฎไฟ่จผใใชใใใพใใใ
+ ใใใชใๅ ดๅใซใใใฆใใไฝ่
ใพใใฏ่ไฝๆจฉ่
ใฏใๅฅ็ด่ก็บใไธๆณ่ก็บใใพใใฏใใไปฅๅคใง
+ ใใใใจใใฝใใใฆใงใขใซ่ตทๅ ใพใใฏ้ข้ฃใใใใใใฏใฝใใใฆใงใขใฎไฝฟ็จใพใใฏ
+ ใใฎไปใฎๆฑใใซใใฃใฆ็ใใไธๅใฎ่ซๆฑใๆๅฎณใใใฎไปใฎ็พฉๅใซใคใใฆ่ฒฌไปปใ่ฒ ใใชใใใฎใจใใพใใ
+
+
+
+ Original License: https://github.com/bluesky-social/social-app/blob/main/LICENSE
+
+
+
+ )
+}
diff --git a/ios/patching/README.md b/ios/patching/README.md
new file mode 100644
index 0000000..a7c6ba3
--- /dev/null
+++ b/ios/patching/README.md
@@ -0,0 +1,62 @@
+# iOS Social App Patches
+
+ใใฎใใฃใฌใฏใใชใซใฏใiOS็social-appใฎใซในใฟใใคใบใใใใๅซใพใใฆใใพใใ
+
+## ใใใใใกใคใซไธ่ฆง
+
+- `001-social-app-ios-config.patch` - app.config.js ใฎ่จญๅฎๅคๆด๏ผใขใใชๅใBundle IDใใขใคใณใณใในใใใกใคใณ็ญ๏ผ
+- `002-social-app-ios-lib.patch` - lib/constants.ts, lib/statsig, lib/url-helpers ใฎๅคๆด
+- `003-social-app-ios-view.patch` - Logo, Logotype, UserAvatar, Splash.tsx ใฎ UI ๅคๆด๏ผBluesky ่ถใญใดใ logo.png ใซๅคๆด๏ผ
+- `004-social-app-ios-core.patch` - agent.ts, App.native.tsx, routes.ts ใฎใณใขๅคๆด
+- `005-social-app-ios-screens.patch` - Settings, Home, Privacy, TOS ็ป้ขใฎๅคๆด
+- `006-social-app-ios-shell.patch` - Drawer, BottomBar, RightNav, ServerInput ใชใฉใทใงใซๅคๆด
+- `007-social-app-ios-misc.patch` - notifications, ageAssurance, PolicyUpdate ใชใฉใใฎไปๅคๆด
+- `008-social-app-ios-policy-tos-error.patch` - ใใฉใคใใทใผใใชใทใผใปๅฉ็จ่ฆ็ดใใใคใใฃใใณใณใใผใใณใใง่กจ็คบ๏ผWebView ใใ ScrollView + Text ใธๅคๆด๏ผ
+- `009-social-app-ios-license.patch` - ใฉใคใปใณในใใผใธใฎ่ฟฝๅ ๏ผDrawer, Navigation, routes, types๏ผ
+- `010-social-app-ios-remove-contact-support.patch` - ใขใซใฆใณใไฝๆๆใฎใContact supportใใชใณใฏใๅ้ค
+- `011-social-app-ios-splash-license-footer.patch` - ในใใฉใใทใฅ็ป้ขใฎใWhat's up?ใๅ้คใยฉ syui ใใใฟใผ่ฟฝๅ
+- `012-social-app-ios-settings-about-help.patch` - About ่จญๅฎใจ routes.ts ใฎใชใณใฏใๅ
้จใซใผใใซๅคๆด๏ผ/support/tos, /support/privacy-policy, /support/license๏ผ
+- `013-social-app-ios-settings-remove-help.patch` - Settings ใใ Help ้
็ฎใๅ้ค
+- `019-social-app-ios-entitlements-plugin.patch` - iOS entitlements ใใฉใฐใคใณ่จญๅฎ
+- `020-social-app-ios-bypass-age-assurance.patch` - ๅนด้ฝข็ขบ่ชใๅฎๅ
จใซ็กๅนๅ๏ผaccess ใ Full ใซๅบๅฎใchatDisabled ใจ adultContentDisabled ใ false ใซๅบๅฎ๏ผ
+- `021-social-app-ios-clean-feed.patch` - Following ใใฃใผใใฎใทใณใใซๅ๏ผDiscoverFallbackHeader ใฎ (i) ใขใคใณใณใจ Discover ใชใณใฏๅ้คใSuggestedFollows ใคใณใฟใผในใใฃใทใฃใซ็กๅนๅใใใใใใใฟใณๅ้ค๏ผ
+- `License.tsx` - ใฉใคใปใณใน่กจ็คบ็ป้ข๏ผๆฐ่ฆใใกใคใซ๏ผ
+
+## ไฝฟ็จๆนๆณ
+
+### ใใใใฎ้ฉ็จ
+
+```bash
+cd /Users/syui/ai/at/ios
+./setup.zsh patch
+```
+
+**ๆณจๆ**: setup.zsh ใ่ชๅ็ใซไปฅไธใๅฎ่กใใพใ๏ผ
+- ใใใใใกใคใซใฎ้ฉ็จ
+- License.tsx ใฎใณใใผ
+- Xcode AppIcon ใ logo.png ใใ 1024x1024 ใซใชใตใคใบใใฆ้
็ฝฎ๏ผGraphicsMagick ใพใใฏ sips ใไฝฟ็จ๏ผ
+
+### ใชใใธใใชใฎใชใปใใ
+
+```bash
+cd /Users/syui/ai/at/ios
+./setup.zsh reset
+```
+
+### ใในใฆใฎใใใใ้ฉ็จ๏ผใใใฉใซใ๏ผ
+
+```bash
+cd /Users/syui/ai/at/ios
+./setup.zsh
+```
+
+## ใใใใฎๆดๆฐๆนๆณ
+
+repos/social-app ใงๅคๆดใๅ ใใๅพ:
+
+```bash
+cd /Users/syui/ai/at/repos/social-app
+git diff [ใใกใคใซๅ] > /Users/syui/ai/at/ios/patching/ๆฐใใใใใ.patch
+```
+
+ใใฎๅพใ`setup.zsh` ใฎ `PATCH_FILES_IOS` ้
ๅใซๆฐใใใใใใใกใคใซๅใ่ฟฝๅ ใใฆใใ ใใใ
diff --git a/ios/preview.zsh b/ios/preview.zsh
new file mode 100755
index 0000000..3644670
--- /dev/null
+++ b/ios/preview.zsh
@@ -0,0 +1,147 @@
+#!/bin/zsh
+set -e
+d=${0:a:h}
+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..."
+cd "$REPO_DIR"
+
+# 0. Environment Setup (Fix Node Version)
+export NVM_DIR="$HOME/.nvm"
+[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
+echo "Checking Node version..."
+if command -v nvm >/dev/null; then
+ nvm use 22 || nvm use 20 || echo "Warning: Could not switch to Node 22/20. Current: $(node -v)"
+else
+ echo "nvm not found, using system node: $(node -v)"
+fi
+
+# 1. Install dependencies
+echo "1. Installing dependencies (yarn)..."
+yarn install
+
+# 1.5. Copy assets
+echo "1.5. Copying assets..."
+ASSETS_DIR="${0:a:h}/assets"
+if [ -d "$ASSETS_DIR" ]; then
+ cp -rf "$ASSETS_DIR/"* "$REPO_DIR/assets/"
+ echo "โ
Copied all assets (including logo.png, logo-1024.png)"
+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
+rm -rf ios
+npx expo prebuild --platform ios --clean
+
+# 3. CocoaPods
+echo "3. Installing CocoaPods..."
+# Ensure PATH includes Homebrew ruby gems if needed
+export PATH="/opt/homebrew/lib/ruby/gems/3.4.0/bin:$PATH"
+cd ios
+pod install
+cd ..
+
+# 4. Signing (Automated)
+echo "4. Configuring Xcode Signing..."
+XCODE_PROJ="ios/${APP_NAME}.xcodeproj"
+if [ ! -d "$XCODE_PROJ" ]; then
+ XCODE_PROJ=$(find ios -name "*.xcodeproj" | head -n 1)
+fi
+PBXPROJ="$XCODE_PROJ/project.pbxproj"
+
+# 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..."
+# If user wants specific device ID, uncomment below, otherwise let Expo ask/pick boot simulator
+
+case $1 in
+ d|devicei)
+ npx expo run:ios --device "$DEVICE_ID" --configuration Release
+ ;;
+ *)
+ npx expo run:ios
+ ;;
+esac
diff --git a/ios/setup.zsh b/ios/setup.zsh
new file mode 100755
index 0000000..cf3b058
--- /dev/null
+++ b/ios/setup.zsh
@@ -0,0 +1,295 @@
+#!/bin/zsh
+
+cd ${0:a:h}
+
+# iOS Social App Patch Setup Script
+# Usage: ./ios/setup.zsh [patch|reset]
+
+# Cross-platform sed (macOS vs Linux)
+function sediment() {
+ if [[ "$OSTYPE" == "darwin"* ]]; then
+ sed -i '' "$@"
+ else
+ sed -i "$@"
+ fi
+}
+
+# Arrays for patch management
+typeset -a FAILED_PATCHES
+
+# Patch file lists for iOS
+typeset -a PATCH_FILES_IOS
+PATCH_FILES_IOS=(
+ "001-social-app-ios-config.patch"
+ "002-social-app-ios-lib.patch"
+ "003-social-app-ios-view.patch"
+ "004-social-app-ios-core.patch"
+ "005-social-app-ios-screens.patch"
+ "006-social-app-ios-shell.patch"
+ "007-social-app-ios-misc.patch"
+ "009-social-app-ios-license.patch"
+ "010-social-app-ios-remove-contact-support.patch"
+ "011-social-app-ios-splash-license-footer.patch"
+ "013-social-app-ios-settings-remove-help.patch"
+ "019-social-app-ios-entitlements-plugin.patch"
+ "020-social-app-ios-bypass-age-assurance.patch"
+ "021-social-app-ios-clean-feed.patch"
+ "022-social-app-ios-bskyweb-support-pages.patch"
+ "023-social-app-ios-disable-dm.patch"
+ "024-social-app-ios-disable-external-services.patch"
+ "025-social-app-ios-bskyweb-title.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() {
+ d=${0:a:h:h}
+ patching_dir=${0:a:h}/patching
+ target_dir=$d/repos/social-app
+}
+
+# Common patch function with status detection
+function apply-patch() {
+ local patch_name=$1
+ local target_dir=$2
+ local patch_file=$3
+
+ echo "โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ"
+ echo "๐ Patch: ${patch_name}"
+ echo " Target: ${target_dir}"
+ echo " File: ${patch_file}"
+
+ pushd ${target_dir} > /dev/null
+
+ # 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..."
+ if patch -p1 < ${patch_file}; then
+ echo "โ
Applied successfully"
+ popd > /dev/null
+ echo ""
+ return 0
+ else
+ echo "โ Failed to apply"
+ FAILED_PATCHES+=("${patch_name} (${patch_file})")
+ popd > /dev/null
+ echo ""
+ return 1
+ fi
+ else
+ echo "โ ๏ธ Cannot apply - file may have been modified"
+ echo " Please check manually"
+ FAILED_PATCHES+=("${patch_name} (${patch_file}) - file modified")
+ popd > /dev/null
+ echo ""
+ return 1
+ fi
+}
+
+function show-failed-patches() {
+ if [ ${#FAILED_PATCHES[@]} -eq 0 ]; then
+ echo ""
+ echo "โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ"
+ echo "โ
All patches applied successfully!"
+ echo "โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ"
+ echo ""
+ return 0
+ fi
+
+ echo ""
+ echo "โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ"
+ echo "โ ๏ธ FAILED PATCHES SUMMARY"
+ echo "โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ"
+ echo ""
+ echo "The following patches could not be applied:"
+ echo ""
+ for failed_patch in "${FAILED_PATCHES[@]}"; do
+ echo " โ ${failed_patch}"
+ done
+ echo ""
+ echo "โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ"
+ echo ""
+}
+
+# Helper function for applying patches
+function patch-apply() {
+ local name=$1
+ local patch_file=$2
+ apply-patch "${name}" "$target_dir" "$patching_dir/${patch_file}"
+}
+
+# Generate build number from timestamp (YYMMDDHHMMSS)
+function ios-generate-build-number() {
+ local build_number=$(date +%y%m%d%H%M%S)
+ local config_patch="$patching_dir/001-social-app-ios-config.patch"
+
+ if [ -f "$config_patch" ]; then
+ # Replace placeholder with timestamp
+ sediment "s/__BUILD_NUMBER__/${build_number}/" "$config_patch"
+ echo "โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ"
+ echo "๐ข Build number: ${build_number}"
+ echo ""
+ fi
+}
+
+# Restore placeholder after patching (for git cleanliness)
+function ios-restore-build-placeholder() {
+ local config_patch="$patching_dir/001-social-app-ios-config.patch"
+
+ if [ -f "$config_patch" ]; then
+ # Restore placeholder for next build
+ sediment "s/buildNumber: '[0-9]*'/buildNumber: '__BUILD_NUMBER__'/" "$config_patch"
+ fi
+}
+
+# Auto-apply patches from list
+function ios-patch-apply-all() {
+ for filename in "${PATCH_FILES_IOS[@]}"; do
+ local title="${filename%.*}"
+ patch-apply "$title" "$filename"
+ done
+}
+
+# Copy new files that aren't in patches
+function ios-copy-new-files() {
+ echo "โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ"
+ echo "๐ Copying new files..."
+
+ # Copy all assets from ios/assets/ to repos/social-app/assets/
+ if [ -d "$d/ios/assets" ]; then
+ cp -rf "$d/ios/assets/"* "$target_dir/assets/"
+ echo "โ
Copied all assets (including logo.png, app-icons)"
+ fi
+
+ # Copy License.tsx
+ if [ -f "$patching_dir/License.tsx" ]; then
+ mkdir -p "$target_dir/src/view/screens"
+ cp "$patching_dir/License.tsx" "$target_dir/src/view/screens/License.tsx"
+ 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 ""
+}
+
+function ios-setup-clone() {
+ if [ ! -d $target_dir ]; then
+ echo "Error: social-app repository not found at $target_dir"
+ echo "Please run install.zsh first to clone repositories"
+ return 1
+ fi
+ 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
+ git stash
+ git checkout .
+ echo "Reset complete"
+}
+
+# Main execution
+ios-env
+
+case "$1" in
+ patch)
+ 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
+ exit
+ ;;
+ reset)
+ 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()
+ }
+ }