ai/at
1
0

add ios social-app

This commit is contained in:
2025-12-06 21:14:08 +09:00
parent 948551c185
commit 7c225b22fc
92 changed files with 5081 additions and 675 deletions

30
.github/workflows/cf-pages.yml vendored Normal file
View File

@@ -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'

3
.gitignore vendored
View File

@@ -2,3 +2,6 @@ repos
.claude .claude
deploy.yml deploy.yml
claude.md claude.md
embedded.mobileprovision
.env
html.zip

View File

@@ -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
```

View File

@@ -86,6 +86,7 @@ services:
depends_on: depends_on:
database: database:
condition: service_healthy condition: service_healthy
#command: ["/bigsky", "--crawl-insecure-ws"]
social-app: social-app:
ports: ports:

View File

@@ -3,5 +3,5 @@ FEEDGEN_LISTENHOST=0.0.0.0
FEEDGEN_SQLITE_LOCATION=/data/db.sqlite FEEDGEN_SQLITE_LOCATION=/data/db.sqlite
FEEDGEN_HOSTNAME=feed.syu.is FEEDGEN_HOSTNAME=feed.syu.is
FEEDGEN_PUBLISHER_DID=did:plc:6qyecktefllvenje24fcxnie FEEDGEN_PUBLISHER_DID=did:plc:6qyecktefllvenje24fcxnie
FEEDGEN_SUBSCRIPTION_ENDPOINT=ws://bgs:2470
FEEDGEN_SERVICE_DID=did:web:feed.syu.is FEEDGEN_SERVICE_DID=did:web:feed.syu.is
FEEDGEN_JETSTREAM_URL=ws://jetstream:6008/subscribe

View File

@@ -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_DATA_DIR=/data
JETSTREAM_LISTEN_ADDR=:6008 JETSTREAM_LISTEN_ADDR=:6008
JETSTREAM_METRICS_LISTEN_ADDR=:6009 JETSTREAM_METRICS_LISTEN_ADDR=:6009

135
html/about/support/app.html Normal file
View File

@@ -0,0 +1,135 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<title>App Info - Aiat</title>
<link rel="icon" type="image/png" href="/static/favicon.png">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
line-height: 1.6;
color: #1a1a1a;
background: #fff;
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
@media (prefers-color-scheme: dark) {
body { background: #000; color: #e0e0e0; }
a { color: #6bb3ff; }
h1, h2, h3 { color: #fff; }
.section { background: #1a1a1a; }
.info-item { background: #2a2a2a; }
}
.header { margin-bottom: 32px; }
.back-link { display: inline-block; margin-bottom: 16px; font-size: 14px; color: #0066cc; text-decoration: none; }
.back-link:hover { text-decoration: underline; }
.app-header { text-align: center; margin-bottom: 32px; }
.app-icon { width: 80px; height: 80px; border-radius: 18px; margin-bottom: 12px; }
.app-name { font-size: 24px; font-weight: bold; margin-bottom: 4px; }
.app-version { font-size: 14px; color: #666; }
.section { background: #f5f5f5; border-radius: 16px; padding: 20px; margin-bottom: 16px; }
.section-title { font-size: 13px; font-weight: 600; color: #999; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 12px; }
.description { font-size: 15px; line-height: 22px; }
.info-grid { display: flex; flex-wrap: wrap; gap: 8px; }
.info-item { flex: 1; min-width: 45%; text-align: center; background: #e8e8e8; border-radius: 12px; padding: 12px; }
.info-label { font-size: 11px; color: #999; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; }
.info-value { font-size: 16px; font-weight: 600; }
.developer-name { font-size: 18px; font-weight: 600; margin-bottom: 12px; }
.link-row { display: flex; align-items: center; padding: 12px 0; border-top: 1px solid rgba(0,0,0,0.1); }
.link-icon { font-size: 14px; font-weight: 600; color: #666; width: 70px; }
.link-value { flex: 1; font-size: 14px; color: #0084ff; text-decoration: none; }
.link-value:hover { text-decoration: underline; }
.link-arrow { font-size: 16px; color: #ccc; }
.bitcoin-row { display: flex; align-items: center; background: rgba(247, 147, 26, 0.08); border-radius: 12px; padding: 14px; gap: 10px; }
.bitcoin-label { font-size: 18px; font-weight: 600; color: #f7931a; }
.bitcoin-address { flex: 1; font-size: 11px; font-family: monospace; color: #666; word-break: break-all; }
.copy-btn { font-size: 12px; color: #999; cursor: pointer; min-width: 50px; text-align: right; }
.copy-btn:hover { color: #0084ff; }
.footer { text-align: center; margin-top: 32px; padding-top: 20px; border-top: 1px solid #ddd; }
.copyright { font-size: 12px; color: #999; }
</style>
</head>
<body>
<div class="header">
<a href="/" class="back-link">&larr; Back to syu.is</a>
</div>
<div class="app-header">
<img src="/static/app.png" alt="Aiat" class="app-icon">
<div class="app-name">Aiat</div>
<div class="app-version">v1.111.0</div>
</div>
<div class="section">
<p class="description">Aiat is a social networking application based on AT Protocol. Connect with your community on syu.is.</p>
</div>
<div class="section">
<div class="section-title">App Information</div>
<div class="info-grid">
<div class="info-item">
<div class="info-label">Version</div>
<div class="info-value">1.111.0</div>
</div>
<div class="info-item">
<div class="info-label">Category</div>
<div class="info-value">Social</div>
</div>
<div class="info-item">
<div class="info-label">Supported OS</div>
<div class="info-value">iOS 26.0+</div>
</div>
<div class="info-item">
<div class="info-label">Price</div>
<div class="info-value">Free</div>
</div>
</div>
</div>
<div class="section">
<div class="section-title">Developer</div>
<div class="developer-name">syui</div>
<div class="link-row">
<span class="link-icon">Git</span>
<a href="https://git.syui.ai/syui" class="link-value" target="_blank">git.syui.ai/syui</a>
<span class="link-arrow">&rarr;</span>
</div>
<div class="link-row">
<span class="link-icon">ATProto</span>
<a href="https://syu.is/syui" class="link-value" target="_blank">syu.is/syui</a>
<span class="link-arrow">&rarr;</span>
</div>
</div>
<div class="section">
<div class="section-title">Bitcoin</div>
<div class="bitcoin-row">
<span class="bitcoin-label">&#8383;</span>
<span class="bitcoin-address" id="btc-address">3BqHXxraZyBapyNpJmniJDh9zqzuB8aoRr</span>
<span class="copy-btn" onclick="copyBTC()">copy</span>
</div>
</div>
<div class="footer">
<p class="copyright">&copy; syui</p>
</div>
<script>
function copyBTC() {
const addr = document.getElementById('btc-address').textContent;
navigator.clipboard.writeText(addr).then(() => {
const btn = document.querySelector('.copy-btn');
btn.textContent = 'copied!';
btn.style.color = '#4CAF50';
setTimeout(() => {
btn.textContent = 'copy';
btn.style.color = '';
}, 2000);
});
}
</script>
</body>
</html>

View File

@@ -0,0 +1,100 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<title>Help - syu.is</title>
<link rel="icon" type="image/png" href="/static/favicon.png">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
line-height: 1.6;
color: #1a1a1a;
background: #fff;
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
@media (prefers-color-scheme: dark) {
body { background: #000; color: #e0e0e0; }
a { color: #6bb3ff; }
h1, h2, h3 { color: #fff; }
}
h1 { font-size: 28px; margin-bottom: 24px; padding-bottom: 12px; border-bottom: 1px solid #ddd; }
h2 { font-size: 20px; margin: 24px 0 12px; }
h3 { font-size: 16px; margin: 16px 0 8px; }
p { margin-bottom: 16px; }
ul { margin: 0 0 16px 24px; }
li { margin-bottom: 8px; }
a { color: #0066cc; text-decoration: none; }
a:hover { text-decoration: underline; }
.header { margin-bottom: 32px; }
.back-link { display: inline-block; margin-bottom: 16px; font-size: 14px; }
.footer { margin-top: 40px; padding-top: 20px; border-top: 1px solid #ddd; font-size: 14px; color: #666; }
.faq-item { margin-bottom: 24px; }
.contact-box { background: #f5f5f5; padding: 20px; border-radius: 8px; margin: 20px 0; }
@media (prefers-color-scheme: dark) {
.contact-box { background: #1a1a1a; }
}
</style>
</head>
<body>
<div class="header">
<a href="/" class="back-link">&larr; Back to syu.is</a>
<h1>Help Center</h1>
</div>
<h2>About syu.is</h2>
<p>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.</p>
<h2>Frequently Asked Questions</h2>
<div class="faq-item">
<h3>What is the AT Protocol?</h3>
<p>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.</p>
</div>
<div class="faq-item">
<h3>How do I create an account?</h3>
<p>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.</p>
</div>
<div class="faq-item">
<h3>How do I reset my password?</h3>
<p>You can reset your password through the login screen by selecting "Forgot Password" and following the instructions sent to your email.</p>
</div>
<div class="faq-item">
<h3>How do I delete my account?</h3>
<p>You can delete your account through Settings &gt; Account. Please note that account deletion is permanent and cannot be undone.</p>
</div>
<div class="faq-item">
<h3>How do I report abuse or inappropriate content?</h3>
<p>You can report content by using the report function available on each post. Our moderation team will review reports and take appropriate action.</p>
</div>
<h2>Contact</h2>
<div class="contact-box">
<p>For additional support or questions:</p>
<ul>
<li>GitHub: <a href="https://github.com/syui" target="_blank">github.com/syui</a></li>
</ul>
</div>
<h2>Related Links</h2>
<ul>
<li><a href="/about/support/tos">Terms of Service</a></li>
<li><a href="/about/support/privacy-policy">Privacy Policy</a></li>
<li><a href="/about/support/license">License</a></li>
<li><a href="/about/support/app">App Info</a></li>
<li><a href="https://atproto.com" target="_blank">AT Protocol Documentation</a></li>
</ul>
<div class="footer">
<p>Last updated: 2025</p>
<p>&copy; syui</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,66 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<title>License - syu.is</title>
<link rel="icon" type="image/png" href="/static/favicon.png">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
line-height: 1.6;
color: #1a1a1a;
background: #fff;
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
@media (prefers-color-scheme: dark) {
body { background: #000; color: #e0e0e0; }
a { color: #6bb3ff; }
h1, h2, h3 { color: #fff; }
}
h1 { font-size: 28px; margin-bottom: 24px; padding-bottom: 12px; border-bottom: 1px solid #ddd; }
h2 { font-size: 20px; margin: 24px 0 12px; }
p { margin-bottom: 16px; }
ul { margin: 0 0 16px 24px; }
li { margin-bottom: 8px; }
a { color: #0066cc; text-decoration: none; }
a:hover { text-decoration: underline; }
.header { margin-bottom: 32px; }
.back-link { display: inline-block; margin-bottom: 16px; font-size: 14px; }
.footer { margin-top: 40px; padding-top: 20px; border-top: 1px solid #ddd; font-size: 14px; color: #666; }
pre { background: #f5f5f5; padding: 16px; border-radius: 8px; overflow-x: auto; font-size: 13px; }
@media (prefers-color-scheme: dark) { pre { background: #1a1a1a; } }
</style>
</head>
<body>
<div class="header">
<a href="/" class="back-link">&larr; Back to syu.is</a>
<h1>License</h1>
</div>
<h2>Aiat (iOS/Android App)</h2>
<p>This application is based on the Bluesky Social App, which is open source software.</p>
<h2>Open Source Licenses</h2>
<p>This app uses the following open source software:</p>
<h3>Bluesky Social App</h3>
<p>Licensed under the MIT License</p>
<p><a href="https://github.com/bluesky-social/social-app" target="_blank">https://github.com/bluesky-social/social-app</a></p>
<h3>AT Protocol</h3>
<p>Licensed under the MIT License / Apache 2.0</p>
<p><a href="https://github.com/bluesky-social/atproto" target="_blank">https://github.com/bluesky-social/atproto</a></p>
<h2>Third Party Libraries</h2>
<p>This application includes various third-party libraries, each with their own licenses. For a complete list, please see the application's source code repository.</p>
<div class="footer">
<p>Last updated: 2025</p>
<p>&copy; syui</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,92 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<title>Privacy Policy - syu.is</title>
<link rel="icon" type="image/png" href="/static/favicon.png">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
line-height: 1.6;
color: #1a1a1a;
background: #fff;
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
@media (prefers-color-scheme: dark) {
body { background: #000; color: #e0e0e0; }
a { color: #6bb3ff; }
h1, h2, h3 { color: #fff; }
}
h1 { font-size: 28px; margin-bottom: 24px; padding-bottom: 12px; border-bottom: 1px solid #ddd; }
h2 { font-size: 20px; margin: 24px 0 12px; }
p { margin-bottom: 16px; }
ul { margin: 0 0 16px 24px; }
li { margin-bottom: 8px; }
a { color: #0066cc; text-decoration: none; }
a:hover { text-decoration: underline; }
.header { margin-bottom: 32px; }
.back-link { display: inline-block; margin-bottom: 16px; font-size: 14px; }
.footer { margin-top: 40px; padding-top: 20px; border-top: 1px solid #ddd; font-size: 14px; color: #666; }
</style>
</head>
<body>
<div class="header">
<a href="/" class="back-link">&larr; Back to syu.is</a>
<h1>Privacy Policy</h1>
</div>
<h2>1. Introduction</h2>
<p>This Privacy Policy explains how syu.is collects, uses, and protects your personal information when you use our service.</p>
<h2>2. Information We Collect</h2>
<p>We collect the following types of information:</p>
<ul>
<li><strong>Account Information:</strong> Email address, username, and profile information you provide</li>
<li><strong>Content:</strong> Posts, messages, and other content you create on the platform</li>
<li><strong>Usage Data:</strong> Information about how you interact with our service</li>
<li><strong>Device Information:</strong> Browser type, operating system, and device identifiers</li>
</ul>
<h2>3. How We Use Your Information</h2>
<p>We use your information to:</p>
<ul>
<li>Provide and maintain our service</li>
<li>Improve and personalize your experience</li>
<li>Communicate with you about the service</li>
<li>Ensure security and prevent abuse</li>
</ul>
<h2>4. Data Sharing</h2>
<p>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.</p>
<h2>5. Data Security</h2>
<p>We implement appropriate security measures to protect your personal information. However, no method of transmission over the Internet is 100% secure.</p>
<h2>6. Your Rights</h2>
<p>You have the right to:</p>
<ul>
<li>Access your personal data</li>
<li>Request correction of your data</li>
<li>Request deletion of your account</li>
<li>Export your data</li>
</ul>
<h2>7. Cookies</h2>
<p>We use cookies and similar technologies to maintain your session and improve your experience.</p>
<h2>8. Changes to This Policy</h2>
<p>We may update this Privacy Policy from time to time. We will notify you of any significant changes.</p>
<h2>9. Contact</h2>
<p>For privacy-related questions, please visit our <a href="/about/support/help">Help page</a>.</p>
<div class="footer">
<p>Last updated: 2025</p>
<p>&copy; syui</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,84 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<title>Terms of Service - syu.is</title>
<link rel="icon" type="image/png" href="/static/favicon.png">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
line-height: 1.6;
color: #1a1a1a;
background: #fff;
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
@media (prefers-color-scheme: dark) {
body { background: #000; color: #e0e0e0; }
a { color: #6bb3ff; }
h1, h2, h3 { color: #fff; }
}
h1 { font-size: 28px; margin-bottom: 24px; padding-bottom: 12px; border-bottom: 1px solid #ddd; }
h2 { font-size: 20px; margin: 24px 0 12px; }
p { margin-bottom: 16px; }
ul { margin: 0 0 16px 24px; }
li { margin-bottom: 8px; }
a { color: #0066cc; text-decoration: none; }
a:hover { text-decoration: underline; }
.header { margin-bottom: 32px; }
.back-link { display: inline-block; margin-bottom: 16px; font-size: 14px; }
.footer { margin-top: 40px; padding-top: 20px; border-top: 1px solid #ddd; font-size: 14px; color: #666; }
</style>
</head>
<body>
<div class="header">
<a href="/" class="back-link">&larr; Back to syu.is</a>
<h1>Terms of Service</h1>
</div>
<h2>1. Introduction</h2>
<p>Welcome to syu.is. By using our service, you agree to these terms. Please read them carefully.</p>
<h2>2. Service Description</h2>
<p>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.</p>
<h2>3. User Responsibilities</h2>
<p>As a user of syu.is, you agree to:</p>
<ul>
<li>Provide accurate information when creating an account</li>
<li>Keep your account credentials secure</li>
<li>Not use the service for illegal activities</li>
<li>Respect other users and their content</li>
<li>Comply with applicable laws and regulations</li>
</ul>
<h2>4. Content Guidelines</h2>
<p>Users are responsible for the content they post. Prohibited content includes:</p>
<ul>
<li>Illegal content</li>
<li>Harassment or abuse</li>
<li>Spam or misleading information</li>
<li>Content that violates others' rights</li>
</ul>
<h2>5. Privacy</h2>
<p>Your privacy is important to us. Please review our <a href="/about/support/privacy-policy">Privacy Policy</a> to understand how we handle your data.</p>
<h2>6. Disclaimer</h2>
<p>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.</p>
<h2>7. Changes to Terms</h2>
<p>We may update these terms from time to time. Continued use of the service after changes constitutes acceptance of the new terms.</p>
<h2>8. Contact</h2>
<p>For questions about these terms, please visit our <a href="/about/support/help">Help page</a>.</p>
<div class="footer">
<p>Last updated: 2025</p>
<p>&copy; syui</p>
</div>
</body>
</html>

135
html/index.html Normal file
View File

@@ -0,0 +1,135 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<title>App Info - Aiat</title>
<link rel="icon" type="image/png" href="/static/favicon.png">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
line-height: 1.6;
color: #1a1a1a;
background: #fff;
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
@media (prefers-color-scheme: dark) {
body { background: #000; color: #e0e0e0; }
a { color: #6bb3ff; }
h1, h2, h3 { color: #fff; }
.section { background: #1a1a1a; }
.info-item { background: #2a2a2a; }
}
.header { margin-bottom: 32px; }
.back-link { display: inline-block; margin-bottom: 16px; font-size: 14px; color: #0066cc; text-decoration: none; }
.back-link:hover { text-decoration: underline; }
.app-header { text-align: center; margin-bottom: 32px; }
.app-icon { width: 80px; height: 80px; border-radius: 18px; margin-bottom: 12px; }
.app-name { font-size: 24px; font-weight: bold; margin-bottom: 4px; }
.app-version { font-size: 14px; color: #666; }
.section { background: #f5f5f5; border-radius: 16px; padding: 20px; margin-bottom: 16px; }
.section-title { font-size: 13px; font-weight: 600; color: #999; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 12px; }
.description { font-size: 15px; line-height: 22px; }
.info-grid { display: flex; flex-wrap: wrap; gap: 8px; }
.info-item { flex: 1; min-width: 45%; text-align: center; background: #e8e8e8; border-radius: 12px; padding: 12px; }
.info-label { font-size: 11px; color: #999; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; }
.info-value { font-size: 16px; font-weight: 600; }
.developer-name { font-size: 18px; font-weight: 600; margin-bottom: 12px; }
.link-row { display: flex; align-items: center; padding: 12px 0; border-top: 1px solid rgba(0,0,0,0.1); }
.link-icon { font-size: 14px; font-weight: 600; color: #666; width: 70px; }
.link-value { flex: 1; font-size: 14px; color: #0084ff; text-decoration: none; }
.link-value:hover { text-decoration: underline; }
.link-arrow { font-size: 16px; color: #ccc; }
.bitcoin-row { display: flex; align-items: center; background: rgba(247, 147, 26, 0.08); border-radius: 12px; padding: 14px; gap: 10px; }
.bitcoin-label { font-size: 18px; font-weight: 600; color: #f7931a; }
.bitcoin-address { flex: 1; font-size: 11px; font-family: monospace; color: #666; word-break: break-all; }
.copy-btn { font-size: 12px; color: #999; cursor: pointer; min-width: 50px; text-align: right; }
.copy-btn:hover { color: #0084ff; }
.footer { text-align: center; margin-top: 32px; padding-top: 20px; border-top: 1px solid #ddd; }
.copyright { font-size: 12px; color: #999; }
</style>
</head>
<body>
<div class="header">
<a href="/" class="back-link">&larr; Back to syu.is</a>
</div>
<div class="app-header">
<img src="/static/app.png" alt="Aiat" class="app-icon">
<div class="app-name">Aiat</div>
<div class="app-version">v1.111.0</div>
</div>
<div class="section">
<p class="description">Aiat is a social networking application based on AT Protocol. Connect with your community on syu.is.</p>
</div>
<div class="section">
<div class="section-title">App Information</div>
<div class="info-grid">
<div class="info-item">
<div class="info-label">Version</div>
<div class="info-value">1.111.2</div>
</div>
<div class="info-item">
<div class="info-label">Category</div>
<div class="info-value">Social</div>
</div>
<div class="info-item">
<div class="info-label">Supported OS</div>
<div class="info-value">iOS 26.0+</div>
</div>
<div class="info-item">
<div class="info-label">Price</div>
<div class="info-value">Free</div>
</div>
</div>
</div>
<div class="section">
<div class="section-title">Developer</div>
<div class="developer-name">syui</div>
<div class="link-row">
<span class="link-icon">Git</span>
<a href="https://git.syui.ai/syui" class="link-value" target="_blank">git.syui.ai/syui</a>
<span class="link-arrow">&rarr;</span>
</div>
<div class="link-row">
<span class="link-icon">ATProto</span>
<a href="https://syu.is/syui" class="link-value" target="_blank">syu.is/syui</a>
<span class="link-arrow">&rarr;</span>
</div>
</div>
<div class="section">
<div class="section-title">Bitcoin</div>
<div class="bitcoin-row">
<span class="bitcoin-label">&#8383;</span>
<span class="bitcoin-address" id="btc-address">3BqHXxraZyBapyNpJmniJDh9zqzuB8aoRr</span>
<span class="copy-btn" onclick="copyBTC()">copy</span>
</div>
</div>
<div class="footer">
<p class="copyright">&copy; syui</p>
</div>
<script>
function copyBTC() {
const addr = document.getElementById('btc-address').textContent;
navigator.clipboard.writeText(addr).then(() => {
const btn = document.querySelector('.copy-btn');
btn.textContent = 'copied!';
btn.style.color = '#4CAF50';
setTimeout(() => {
btn.textContent = 'copy';
btn.style.color = '';
}, 2000);
});
}
</script>
</body>
</html>

BIN
html/static/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
html/static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1005 B

View File

@@ -1,5 +1,23 @@
#!/bin/zsh #!/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() { function at-repos-env() {
APP_PASSWORD=xxx APP_PASSWORD=xxx
host=syu.is host=syu.is
@@ -34,6 +52,12 @@ function at-repos-env() {
name=${host%%.*} name=${host%%.*}
domain=${host##*.} domain=${host##*.}
dport=5000 dport=5000
typeset -A PINNED_COMMITS
PINNED_COMMITS=(
[indigo]="d49b454196351c988ceb5ce1f5e21b689487b5ab"
[atproto]="104e6ed37b0589cc000109dc76316be35b2257e1"
)
} }
# Arrays for patch management # Arrays for patch management
@@ -82,6 +106,10 @@ function at-repos-pull() {
echo $repo echo $repo
if [ -d $d/repos/${repo##*/} ];then if [ -d $d/repos/${repo##*/} ];then
cd $d/repos/${repo##*/} 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 git stash -u
if ! git pull;then if ! git pull;then
rm -rf $d/repos/${repo##*/} rm -rf $d/repos/${repo##*/}
@@ -98,32 +126,24 @@ function at-repos-pull() {
cd $d cd $d
} }
function at-repos-social-app-avatar-write() { function at-repos-checkout-pinned() {
did_admin=did:plc:6qyecktefllvenje24fcxnie echo "🔒 Checking out pinned commits..."
dt=$d/repos/social-app/src cd $d/repos
cd $dt for repo_name pinned_commit in ${(kv)PINNED_COMMITS}; do
grep -R syu.is .|cut -d : -f 1|sort -u|xargs sed -i "s/syu.is/${host}/g" if [ -n "$pinned_commit" ] && [ -d "$d/repos/$repo_name" ]; then
grep -R web.syu.is .|cut -d : -f 1|sort -u|xargs sed -i "s/web.syu.is/web.${host}/g" echo " 📌 $repo_name -> $pinned_commit"
f=$dt/lib/constants.ts cd $d/repos/$repo_name
sed -i "s#export const BSKY_SERVICE = 'https://bsky.social'#export const BSKY_SERVICE = 'https://${host}'#g" $f git fetch origin
sed -i "s#export const BSKY_SERVICE_DID = 'did:web:bsky.social'#export const BSKY_SERVICE_DID = 'did:web:${host}'#g" $f git checkout $pinned_commit
sed -i "s#export const PUBLIC_BSKY_SERVICE = 'https://public.api.bsky.app'#export const PUBLIC_BSKY_SERVICE = 'https://bsky.${host}'#g" $f cd $d/repos
sed -i "s#export const PUBLIC_APPVIEW = 'https://api.bsky.app'#export const PUBLIC_APPVIEW = 'https://bsky.${host}'#g" $f fi
sed -i "s#export const PUBLIC_APPVIEW_DID = 'did:web:api.bsky.app'#export const PUBLIC_APPVIEW_DID = 'did:web:bsky.${host}'#g" $f done
cd $d
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-social-app-ios-patch() {
$d/ios/setup.zsh
}
# Common patch function with status detection # Common patch function with status detection
function apply-patch() { function apply-patch() {
@@ -139,7 +159,8 @@ function apply-patch() {
pushd ${target_dir} > /dev/null pushd ${target_dir} > /dev/null
# Check if patch is already applied (reverse dry-run succeeds) # 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" echo "✅ Already applied - skipping"
popd > /dev/null popd > /dev/null
echo "" echo ""
@@ -147,9 +168,9 @@ function apply-patch() {
fi fi
# Check if patch can be applied (forward dry-run succeeds) # 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..." echo "🔧 Applying patch..."
if patch -p1 < ${patch_file}; then if patch -f -p1 < ${patch_file}; then
echo "✅ Applied successfully" echo "✅ Applied successfully"
popd > /dev/null popd > /dev/null
echo "" echo ""
@@ -284,29 +305,29 @@ function at-repos-ozone-patch() {
fi fi
# Replace process.env with env() # Replace process.env with env()
sed -i 's/process\.env\.\(NEXT_PUBLIC_[A-Z_]*\)/env('\''\1'\'')/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
sed -i 's/process\.env\.NODE_ENV/env('\''NODE_ENV'\'')/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 # 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 =\ export const SOCIAL_APP_DOMAIN =\
env('\''NEXT_PUBLIC_SOCIAL_APP_DOMAIN'\'') || '\''bsky.app'\''\ env('\''NEXT_PUBLIC_SOCIAL_APP_DOMAIN'\'') || '\''bsky.app'\''\
}' lib/constants.ts 2>/dev/null || true }' lib/constants.ts 2>/dev/null || true
# Fix multiline process.env patterns # 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'\'')/ 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 /^ \.NEXT_PUBLIC_NEW_ACCOUNT_MARKER_THRESHOLD_IN_DAYS$/d
}' lib/constants.ts 2>/dev/null || true }' 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'\'')/ 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 /^ \.NEXT_PUBLIC_YOUNG_ACCOUNT_MARKER_THRESHOLD_IN_DAYS$/d
}' lib/constants.ts 2>/dev/null || true }' 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'\'')/ 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 /^ \.NEXT_PUBLIC_HIGH_PROFILE_FOLLOWER_THRESHOLD$/d
}' lib/constants.ts 2>/dev/null || true }' lib/constants.ts 2>/dev/null || true
# Fix parseInt() to handle undefined by adding || '' # 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 popd > /dev/null
} }
@@ -317,6 +338,9 @@ function at-repos-build-docker-atproto() {
for ((i=1; i<=${#services}; i++)); do for ((i=1; i<=${#services}; i++)); do
service=${services[$i]} service=${services[$i]}
docker compose build --no-cache $service docker compose build --no-cache $service
if [ "$service" = "ozone" ]; then
docker compose build --no-cache ${service}-web
fi
done done
else else
docker compose build --no-cache $1 docker compose build --no-cache $1
@@ -338,12 +362,11 @@ function at-repos-push-reset() {
} }
function at-repos-push-docker() { function at-repos-push-docker() {
if [ -z "$1" ];then if [ -z "$1" ] || [ "$1" = "push" ]; then
for ((i=1; i<=${#services}; i++)); do for service in "${services[@]}"; do
service=${services[$i]}
docker tag at-${service}:latest localhost:${dport}/${service}:latest docker tag at-${service}:latest localhost:${dport}/${service}:latest
docker push 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 tag at-${service}-web:latest localhost:${dport}/${service}-web:latest
docker push localhost:${dport}/${service}-web:latest docker push localhost:${dport}/${service}-web:latest
fi fi
@@ -388,28 +411,55 @@ function at-repos-reset-bgs-db() {
echo "⚙️ Updating Slurp Config..." 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;" 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 # Retry loop for addTrustedDomain as BGS might still be warming up
for i in {1..5}; do 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 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" echo "✅ Trusted domain registered"
break break
fi fi
echo "Bot failed to contact BGS (attempt $i/5)... waiting 5s" echo "Failed to contact BGS (attempt $i/5)... waiting 5s"
sleep 5 sleep 5
done 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 for ((i=1; i<=${#handles}; i++)); do
handle=${handles[$i]} handle=${handles[$i]}
did=`curl -sL "https://${host}/xrpc/com.atproto.repo.describeRepo?repo=${handle}" |jq -r .did` did=$(curl -sL "https://${host}/xrpc/com.atproto.repo.describeRepo?repo=${handle}" | jq -r .did)
if [ ! -z "$did" ] && [ "$did" != "null" ]; then if [ -n "$did" ] && [ "$did" != "null" ]; then
echo "Resetting repo: $handle ($did)" echo " Syncing repo: $handle ($did)"
curl -X POST "https://bgs.${host}/admin/repo/reset?did=${did}" \ # Use takedown=false to trigger a resync without actually taking down
-H "Authorization: Bearer ${BGS_ADMIN_KEY}" curl -s -X POST "https://bgs.${host}/admin/repo/takedown?did=${did}&takedown=false" \
-H "Authorization: Bearer ${BGS_ADMIN_KEY}" || true
else else
echo "Skipping reset for $handle (DID not found)" echo " Skipping $handle (DID not found)"
fi fi
done done
echo ""
echo "✅ BGS reset complete!"
echo " PDS should now be subscribed and syncing repos."
} }
function at-repos-feed-generator-start-push() { function at-repos-feed-generator-start-push() {
@@ -480,7 +530,7 @@ case "$1" in
exit exit
;; ;;
patch) patch)
at-repos-social-app-avatar-write at-repos-social-app-ios-patch
at-repos-patch-apply-all at-repos-patch-apply-all
at-repos-ozone-patch at-repos-ozone-patch
show-failed-patches show-failed-patches
@@ -520,7 +570,8 @@ case "`cat /etc/hostname`" in
*) *)
at-repos-clone at-repos-clone
at-repos-pull 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-patch-apply-all
at-repos-ozone-patch at-repos-ozone-patch
show-failed-patches show-failed-patches

17
ios/.env.example Normal file
View File

@@ -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

View File

View File

@@ -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 (
<View style={styles.container}>
<View style={styles.section}>
<Text style={styles.sectionTitle}>About This App</Text>
<Text style={styles.paragraph}>
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.
</Text>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Key Features</Text>
<View style={styles.list}>
<Text style={styles.listItem}> Connect to any AT Protocol PDS</Text>
<Text style={styles.listItem}> Post text, images, and videos</Text>
<Text style={styles.listItem}> Follow users and view timelines</Text>
<Text style={styles.listItem}> Customize feeds and moderation settings</Text>
<Text style={styles.listItem}> Direct messaging support</Text>
</View>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Open Source</Text>
<Text style={styles.paragraph}>
This application is based on the Bluesky social-app, licensed under the MIT
License. The original source code is available at:
</Text>
<Pressable
onPress={() =>
handleLinkPress('https://github.com/bluesky-social/social-app')
}>
<Text style={styles.link}>github.com/bluesky-social/social-app</Text>
</Pressable>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>AT Protocol</Text>
<Text style={styles.paragraph}>
This app uses the AT Protocol (Authenticated Transfer Protocol), an open and
decentralized standard for social applications.
</Text>
<Pressable onPress={() => handleLinkPress('https://atproto.com')}>
<Text style={styles.link}>atproto.com</Text>
</Pressable>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>License</Text>
<Text style={styles.paragraph}>
Copyright 20232025 Bluesky Social PBC
</Text>
<Text style={styles.paragraph}>
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.
</Text>
<Text style={styles.paragraph}>
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
</Text>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Contact</Text>
<Pressable onPress={() => handleLinkPress('https://syu.is')}>
<Text style={styles.link}>https://syu.is</Text>
</Pressable>
</View>
<View style={styles.section}>
<Text style={styles.versionText}>Version 1.0.0</Text>
</View>
</View>
)
}
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',
},
})

View File

@@ -1,95 +0,0 @@
import React from 'react'
import {View, Text, StyleSheet, Pressable, Linking} from 'react-native'
export default function LicenseNotice() {
return (
<View style={styles.container}>
<Text style={styles.title}>Open Source Licenses</Text>
<View style={styles.section}>
<Text style={styles.projectName}>Bluesky Social App</Text>
<Text style={styles.license}>MIT License</Text>
<Text style={styles.copyright}>Copyright 20232025 Bluesky Social PBC</Text>
<Text style={styles.licenseText}>
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:
</Text>
<Text style={styles.licenseText}>
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
</Text>
<Text style={styles.licenseText}>
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.
</Text>
<Pressable
onPress={() =>
Linking.openURL('https://github.com/bluesky-social/social-app')
}>
<Text style={styles.link}>View Source Code</Text>
</Pressable>
</View>
</View>
)
}
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,
},
})

View File

@@ -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 (
<View style={styles.container}>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Introduction</Text>
<Text style={styles.paragraph}>
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.
</Text>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Information We Collect</Text>
<Text style={styles.paragraph}>
The App may collect and use the following information:
</Text>
<Text style={styles.subTitle}>1. Information Collected Automatically</Text>
<View style={styles.list}>
<Text style={styles.listItem}> Device information (model, OS version)</Text>
<Text style={styles.listItem}> App usage data (sessions, features used)</Text>
<Text style={styles.listItem}> Crash logs and performance data</Text>
</View>
<Text style={styles.subTitle}>2. Information Provided by Users</Text>
<View style={styles.list}>
<Text style={styles.listItem}>
DID (Decentralized Identifier) and handle for authentication
</Text>
<Text style={styles.listItem}> Posts, media, and social interactions</Text>
<Text style={styles.listItem}> Profile information (avatar, display name, bio)</Text>
</View>
<View style={styles.highlight}>
<Text style={styles.highlightText}>
Important: Your data is stored on your chosen PDS (Personal Data Server).
This app does not store your content on our servers.
</Text>
</View>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>How We Use Your Information</Text>
<View style={styles.list}>
<Text style={styles.listItem}>
To provide AT Protocol social networking features
</Text>
<Text style={styles.listItem}> To improve app performance and user experience</Text>
<Text style={styles.listItem}> To diagnose and fix technical issues</Text>
</View>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Data Sharing</Text>
<Text style={styles.paragraph}>
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.
</Text>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Your Rights</Text>
<Text style={styles.paragraph}>
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.
</Text>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Contact</Text>
<Text style={styles.paragraph}>
For questions about this Privacy Policy, please contact:
</Text>
<Pressable onPress={() => handleLinkPress('https://syu.is')}>
<Text style={styles.link}>https://syu.is</Text>
</Pressable>
</View>
<View style={styles.section}>
<Text style={styles.lastUpdated}>Last Updated: December 3, 2025</Text>
</View>
</View>
)
}
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',
},
})

View File

@@ -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<CommonNavigatorParams, 'PrivacyPolicy'>
export const PrivacyPolicyScreen = (_props: Props) => {
const pal = usePalette('default')
const {_} = useLingui()
const setMinimalShellMode = useSetMinimalShellMode()
useFocusEffect(
React.useCallback(() => {
setMinimalShellMode(false)
}, [setMinimalShellMode]),
)
return (
<Layout.Screen>
<ViewHeader title={_(msg`Privacy Policy`)} />
<ScrollView style={[s.hContentRegion, pal.view]}>
<View style={[s.p20]}>
<PrivacyContent />
</View>
<View style={s.footerSpacer} />
</ScrollView>
</Layout.Screen>
)
}

9
ios/README.md Normal file
View File

@@ -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で実現しています。

View File

@@ -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<CommonNavigatorParams, 'Support'>
export const SupportScreen = (_props: Props) => {
const pal = usePalette('default')
const setMinimalShellMode = useSetMinimalShellMode()
const {_} = useLingui()
useFocusEffect(
React.useCallback(() => {
setMinimalShellMode(false)
}, [setMinimalShellMode]),
)
return (
<Layout.Screen>
<ViewHeader title={_(msg`App Info`)} />
<ScrollView style={[s.hContentRegion, pal.view]}>
<AppInfo />
</ScrollView>
</Layout.Screen>
)
}

View File

@@ -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
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1005 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1005 B

BIN
ios/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -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"

View File

@@ -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"
}

202
ios/build.zsh Executable file
View File

@@ -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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>production</string>
<key>com.apple.security.application-groups</key>
<array>
<string>${APP_GROUP}</string>
</array>
</dict>
</plist>
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"

BIN
ios/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -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: [],
},
},
},

View File

@@ -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{' '}
<InlineLinkText
label={_(msg`The Discover feed`)}
- to="/profile/bsky.app/feed/whats-hot"
+ to="/profile/did:plc:6qyecktefllvenje24fcxnie/feed/app"
style={[a.text_md]}>
Discover
</InlineLinkText>{' '}

View File

@@ -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 (
- <Svg
- fill="none"
- // @ts-ignore it's fiiiiine
+ <Image
+ // @ts-ignore
ref={ref}
- viewBox="0 0 64 66"
- style={[{width, height}, props.style]}>
- <Path
- fill={props.fill || '#fff'}
- d="M13.873 3.77C21.21 9.243 29.103 20.342 32 26.3v15.732c0-.335-.13.043-.41.858-1.512 4.414-7.418 21.642-20.923 7.87-7.111-7.252-3.819-14.503 9.125-16.692-7.405 1.252-15.73-.817-18.014-8.93C1.12 22.804 0 8.431 0 6.488 0-3.237 8.579-.18 13.873 3.77ZM50.127 3.77C42.79 9.243 34.897 20.342 32 26.3v15.732c0-.335.13.043.41.858 1.512 4.414 7.418 21.642 20.923 7.87 7.111-7.252 3.819-14.503-9.125-16.692 7.405 1.252 15.73-.817 18.014-8.93C62.88 22.804 64 8.431 64 6.488 64-3.237 55.422-.18 50.127 3.77Z"
- />
- </Svg>
+ 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 = ({
<HighPriorityImage
testID="userAvatarImage"
style={aviStyle}
- source={{uri: avatar}}
+ source={{ uri: hackModifyThumbnailPath(avatar, 1 > 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<SvgProps, 'style'>
-
-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 (
- <Image
- source={
- size > 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 (
- <Svg
- fill="none"
- // @ts-ignore it's fiiiiine
- ref={ref}
- viewBox="0 0 64 57"
- {...rest}
- style={[{width: size, height: size * ratio}, styles]}>
- {gradient && (
- <Defs>
- <LinearGradient id="sky" x1="0" y1="0" x2="0" y2="1">
- <Stop offset="0" stopColor="#0A7AFF" stopOpacity="1" />
- <Stop offset="1" stopColor="#59B9FF" stopOpacity="1" />
- </LinearGradient>
- </Defs>
- )}
-
- <Path
- fill={_fill}
- d="M13.873 3.805C21.21 9.332 29.103 20.537 32 26.55v15.882c0-.338-.13.044-.41.867-1.512 4.456-7.418 21.847-20.923 7.944-7.111-7.32-3.819-14.64 9.125-16.85-7.405 1.264-15.73-.825-18.014-9.015C1.12 23.022 0 8.51 0 6.55 0-3.268 8.579-.182 13.873 3.805ZM50.127 3.805C42.79 9.332 34.897 20.537 32 26.55v15.882c0-.338.13.044.41.867 1.512 4.456 7.418 21.847 20.923 7.944 7.111-7.32 3.819-14.64-9.125-16.85 7.405 1.264 15.73-.825 18.014-9.015C62.88 23.022 64 8.51 64 6.55c0-9.818-8.578-6.732-13.873-2.745Z"
- />
- </Svg>
+ <Image
+ source={require('../../../assets/logo.png')}
+ style={[{width: size, height: size}, flatten(style)]}
+ contentFit="contain"
+ accessibilityLabel="Logo"
+ />
)
})
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 (
- <Svg
- fill="none"
- viewBox="0 0 64 17"
- {...rest}
- width={size}
- height={Number(size) * ratio}>
- <Path
- fill={fill || pal.text.color}
- d="M8.478 6.252c1.503.538 2.3 1.78 2.3 3.172 0 2.356-1.576 3.785-4.6 3.785H0V0h5.974c2.875 0 4.267 1.466 4.267 3.413 0 1.3-.594 2.245-1.763 2.839Zm-2.69-4.193H2.504v3.45h3.284c1.28 0 1.967-.667 1.967-1.78 0-1.02-.705-1.67-1.967-1.67Zm-3.284 9.072h3.544c1.41 0 2.17-.65 2.17-1.818 0-1.224-.723-1.837-2.17-1.837H2.504v3.655ZM14.251 13.209h-2.337V0h2.337v13.209ZM22.001 8.998V3.636h2.338v9.573h-2.263v-1.392c-.724 1.076-1.726 1.614-3.006 1.614-2.022 0-3.34-1.224-3.34-3.45V3.636h2.338v5.955c0 1.206.594 1.818 1.8 1.818 1.132 0 2.133-.835 2.133-2.411ZM34.979 8.59v.556h-7.161c.167 1.651 1.076 2.467 2.486 2.467 1.076 0 1.8-.463 2.189-1.372h2.244c-.5 1.947-2.17 3.19-4.452 3.19-1.428 0-2.579-.463-3.45-1.372-.872-.91-1.318-2.115-1.318-3.637 0-1.502.427-2.708 1.299-3.636.872-.909 2.004-1.372 3.432-1.372 1.447 0 2.597.482 3.45 1.428.854.946 1.28 2.208 1.28 3.747Zm-4.75-3.358c-1.28 0-2.17.742-2.393 2.281h4.805c-.204-1.391-1.057-2.281-2.411-2.281ZM40.16 13.469c-2.783 0-4.249-1.095-4.379-3.303h2.282c.13 1.188.724 1.633 2.134 1.633 1.261 0 1.892-.39 1.892-1.15 0-.687-.445-1.02-1.874-1.262l-1.094-.185c-2.097-.353-3.136-1.318-3.136-2.894 0-1.8 1.429-2.894 3.97-2.894 2.728 0 4.138 1.075 4.23 3.246h-2.207c-.056-1.169-.742-1.577-2.023-1.577-1.113 0-1.67.371-1.67 1.113 0 .668.483.965 1.596 1.169l1.206.186c2.32.426 3.32 1.28 3.32 2.912 0 1.93-1.557 3.006-4.247 3.006ZM54.667 13.209h-2.671l-2.783-4.453-1.447 1.447v3.006h-2.3V0h2.3v7.606l3.896-3.97h2.783l-3.618 3.618 3.84 5.955ZM60.772 6.048l.78-2.412H64l-3.692 10.352c-.39 1.057-.872 1.818-1.484 2.245-.612.426-1.484.63-2.634.63-.39 0-.724-.018-1.02-.055V14.97h.89c1.057 0 1.577-.65 1.577-1.54 0-.445-.149-1.094-.446-1.929l-2.746-7.866h2.487l.779 2.393c.575 1.8 1.076 3.58 1.521 5.343.408-1.521.928-3.302 1.54-5.324Z"
- />
- </Svg>
+ <Text style={[
+ a.font_bold,
+ {
+ fontSize,
+ color: fill || t.palette.primary_500,
+ letterSpacing: -0.5
+ },
+ style
+ ]}>
+ Aiat
+ </Text>
)
}

View File

@@ -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<AllNavigatableRoutes>({
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],

View File

@@ -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) {
<Layout.Content>
<SettingsList.Container>
<SettingsList.LinkItem
- to="https://bsky.social/about/support/tos"
+ to="https://syu.is/about/support/tos"
label={_(msg`Terms of Service`)}>
<SettingsList.ItemIcon icon={NewspaperIcon} />
<SettingsList.ItemText>
@@ -88,7 +88,7 @@ export function AboutSettingsScreen({}: Props) {
</SettingsList.ItemText>
</SettingsList.LinkItem>
<SettingsList.LinkItem
- to="https://bsky.social/about/support/privacy-policy"
+ to="https://syu.is/about/support/privacy-policy"
label={_(msg`Privacy Policy`)}>
<SettingsList.ItemIcon icon={NewspaperIcon} />
<SettingsList.ItemText>
diff --git a/src/screens/Takendown.tsx b/src/screens/Takendown.tsx
index 77f219e55..53f5e0cc0 100644
--- a/src/screens/Takendown.tsx
+++ b/src/screens/Takendown.tsx
@@ -217,10 +217,10 @@ export function Takendown() {
<Trans>
Your account was found to be in violation of the{' '}
<SimpleInlineLinkText
- label={_(msg`Bluesky Social Terms of Service`)}
- to="https://bsky.social/about/support/tos"
+ label={_(msg`syu.is Terms of Service`)}
+ to="https://syu.is/about/support/tos"
style={[a.text_md, a.leading_normal]}>
- Bluesky Social Terms of Service
+ syu.is Terms of Service
</SimpleInlineLinkText>
. You have been sent an email outlining the specific violation
and suspension period, if applicable. You can appeal this
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index e058e2883..8daf41089 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -1,23 +1,16 @@
import React from 'react'
import {ActivityIndicator, StyleSheet} from 'react-native'
import {useFocusEffect} from '@react-navigation/native'
-
import {PROD_DEFAULT_FEED} from '#/lib/constants'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {useOTAUpdates} from '#/lib/hooks/useOTAUpdates'
import {useSetTitle} from '#/lib/hooks/useSetTitle'
import {useRequestNotificationsPermission} from '#/lib/notifications/notifications'
-import {
- type HomeTabNavigatorParams,
- type NativeStackScreenProps,
-} from '#/lib/routes/types'
+import {type HomeTabNavigatorParams, type NativeStackScreenProps} from '#/lib/routes/types'
import {logEvent} from '#/lib/statsig/statsig'
import {isWeb} from '#/platform/detection'
import {emitSoftReset} from '#/state/events'
-import {
- type SavedFeedSourceInfo,
- usePinnedFeedsInfos,
-} from '#/state/queries/feed'
+import {type SavedFeedSourceInfo, usePinnedFeedsInfos} from '#/state/queries/feed'
import {type FeedDescriptor, type FeedParams} from '#/state/queries/post-feed'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {type UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
@@ -27,11 +20,7 @@ import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed'
import {FeedPage} from '#/view/com/feeds/FeedPage'
import {HomeHeader} from '#/view/com/home/HomeHeader'
-import {
- Pager,
- type PagerRef,
- type RenderTabBarFnProps,
-} from '#/view/com/pager/Pager'
+import {Pager, type PagerRef, type RenderTabBarFnProps} from '#/view/com/pager/Pager'
import {CustomFeedEmptyState} from '#/view/com/posts/CustomFeedEmptyState'
import {FollowingEmptyState} from '#/view/com/posts/FollowingEmptyState'
import {FollowingEndOfFeed} from '#/view/com/posts/FollowingEndOfFeed'
@@ -39,97 +28,90 @@ import {NoFeedsPinned} from '#/screens/Home/NoFeedsPinned'
import * as Layout from '#/components/Layout'
import {useDemoMode} from '#/storage/hooks/demo-mode'
+const SYU_IS_FEED_URI = 'at://did:plc:6qyecktefllvenje24fcxnie/app.bsky.feed.generator/app'
+
+const DEFAULT_PINNED_FEEDS: any[] = [{
+ feedDescriptor: 'following',
+ displayName: 'Following',
+ id: 'following',
+ uri: 'following',
+ type: 'feed',
+ savedFeed: undefined,
+ pinned: true,
+ route: { href: '/', name: 'Home', params: {} },
+ cid: '',
+ avatar: '',
+ creatorDid: '',
+ creatorHandle: '',
+}, {
+ feedDescriptor: `feedgen|${SYU_IS_FEED_URI}`,
+ displayName: 'Feeds',
+ id: SYU_IS_FEED_URI,
+ uri: SYU_IS_FEED_URI,
+ type: 'feed',
+ savedFeed: {
+ type: 'feed',
+ value: SYU_IS_FEED_URI,
+ pinned: true,
+ },
+ pinned: true,
+ route: { href: '/', name: 'Home', params: {} },
+ cid: '',
+ avatar: '',
+ creatorDid: '',
+ creatorHandle: '',
+}]
+
type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home' | 'Start'>
export function HomeScreen(props: Props) {
const {setShowLoggedOut} = useLoggedOutViewControls()
const {data: preferences} = usePreferencesQuery()
const {currentAccount} = useSession()
- const {data: pinnedFeedInfos, isLoading: isPinnedFeedsLoading} =
- usePinnedFeedsInfos()
+ const {data: pinnedFeedInfos} = usePinnedFeedsInfos()
+
+ const safePreferences = preferences || { feedViewPrefs: { lab_mergeFeedEnabled: false }, savedFeeds: [] } as any
+ // Use user's pinned feeds when logged in and available, otherwise use defaults
+ const safePinnedFeedInfos = !currentAccount
+ ? DEFAULT_PINNED_FEEDS.filter(f => f.feedDescriptor !== 'following')
+ : (pinnedFeedInfos && pinnedFeedInfos.length > 0)
+ ? pinnedFeedInfos
+ : DEFAULT_PINNED_FEEDS
React.useEffect(() => {
if (isWeb && !currentAccount) {
const getParams = new URLSearchParams(window.location.search)
const splash = getParams.get('splash')
- if (splash === 'true') {
- setShowLoggedOut(true)
- return
- }
+ if (splash === 'true') { setShowLoggedOut(true); return }
}
-
const params = props.route.params
- if (
- currentAccount &&
- props.route.name === 'Start' &&
- params?.name &&
- params?.rkey
- ) {
- props.navigation.navigate('StarterPack', {
- rkey: params.rkey,
- name: params.name,
- })
+ if (currentAccount && props.route.name === 'Start' && params?.name && params?.rkey) {
+ props.navigation.navigate('StarterPack', { rkey: params.rkey, name: params.name })
}
- }, [
- currentAccount,
- props.navigation,
- props.route.name,
- props.route.params,
- setShowLoggedOut,
- ])
+ }, [currentAccount, props.navigation, props.route.name, props.route.params, setShowLoggedOut])
- if (preferences && pinnedFeedInfos && !isPinnedFeedsLoading) {
- return (
- <Layout.Screen testID="HomeScreen">
- <HomeScreenReady
- {...props}
- preferences={preferences}
- pinnedFeedInfos={pinnedFeedInfos}
- />
- </Layout.Screen>
- )
- } else {
- return (
- <Layout.Screen>
- <Layout.Center style={styles.loading}>
- <ActivityIndicator size="large" />
- </Layout.Center>
- </Layout.Screen>
- )
- }
+ return (
+ <Layout.Screen testID="HomeScreen">
+ <HomeScreenReady {...props} preferences={safePreferences} pinnedFeedInfos={safePinnedFeedInfos as any} />
+ </Layout.Screen>
+ )
}
-function HomeScreenReady({
- preferences,
- pinnedFeedInfos,
-}: Props & {
- preferences: UsePreferencesQueryResponse
- pinnedFeedInfos: SavedFeedSourceInfo[]
-}) {
- const allFeeds = React.useMemo(
- () => pinnedFeedInfos.map(f => f.feedDescriptor),
- [pinnedFeedInfos],
- )
- const maybeRawSelectedFeed: FeedDescriptor | undefined =
- useSelectedFeed() ?? allFeeds[0]
+function HomeScreenReady({preferences, pinnedFeedInfos}: any) {
+ const allFeeds = React.useMemo(() => pinnedFeedInfos.map(f => f.feedDescriptor), [pinnedFeedInfos])
+ const maybeRawSelectedFeed = useSelectedFeed() ?? allFeeds[0]
const setSelectedFeed = useSetSelectedFeed()
const maybeFoundIndex = allFeeds.indexOf(maybeRawSelectedFeed)
const selectedIndex = Math.max(0, maybeFoundIndex)
- const maybeSelectedFeed: FeedDescriptor | undefined = allFeeds[selectedIndex]
+ const maybeSelectedFeed = allFeeds[selectedIndex]
const requestNotificationsPermission = useRequestNotificationsPermission()
useSetTitle(pinnedFeedInfos[selectedIndex]?.displayName)
useOTAUpdates()
-
- React.useEffect(() => {
- requestNotificationsPermission('Home')
- }, [requestNotificationsPermission])
+ React.useEffect(() => { requestNotificationsPermission('Home') }, [requestNotificationsPermission])
const pagerRef = React.useRef<PagerRef>(null)
const lastPagerReportedIndexRef = React.useRef(selectedIndex)
React.useLayoutEffect(() => {
- // Since the pager is not a controlled component, adjust it imperatively
- // if the selected index gets out of sync with what it last reported.
- // This is supposed to only happen on the web when you use the right nav.
if (selectedIndex !== lastPagerReportedIndexRef.current) {
lastPagerReportedIndexRef.current = selectedIndex
pagerRef.current?.setPage(selectedIndex)
@@ -138,205 +120,43 @@ function HomeScreenReady({
const {hasSession} = useSession()
const setMinimalShellMode = useSetMinimalShellMode()
- useFocusEffect(
- React.useCallback(() => {
- setMinimalShellMode(false)
- }, [setMinimalShellMode]),
- )
+ useFocusEffect(React.useCallback(() => { setMinimalShellMode(false) }, [setMinimalShellMode]))
- useFocusEffect(
- useNonReactiveCallback(() => {
- if (maybeSelectedFeed) {
- logEvent('home:feedDisplayed', {
- index: selectedIndex,
- feedType: maybeSelectedFeed.split('|')[0],
- feedUrl: maybeSelectedFeed,
- reason: 'focus',
- })
- }
- }),
- )
-
- const onPageSelected = React.useCallback(
- (index: number) => {
- setMinimalShellMode(false)
- const maybeFeed = allFeeds[index]
+ const onPageSelected = React.useCallback((index) => {
+ setMinimalShellMode(false)
+ const maybeFeed = allFeeds[index]
+ lastPagerReportedIndexRef.current = index
+ setSelectedFeed(maybeFeed)
+ }, [setSelectedFeed, setMinimalShellMode, allFeeds])
- // Mutate the ref before setting state to avoid the imperative syncing effect
- // above from starting a loop on Android when swiping back and forth.
- lastPagerReportedIndexRef.current = index
- setSelectedFeed(maybeFeed)
-
- if (maybeFeed) {
- logEvent('home:feedDisplayed', {
- index,
- feedType: maybeFeed.split('|')[0],
- feedUrl: maybeFeed,
- })
- }
- },
- [setSelectedFeed, setMinimalShellMode, allFeeds],
- )
-
- const onPressSelected = React.useCallback(() => {
- emitSoftReset()
- }, [])
-
- const onPageScrollStateChanged = React.useCallback(
- (state: 'idle' | 'dragging' | 'settling') => {
- 'worklet'
- if (state === 'dragging') {
- setMinimalShellMode(false)
- }
- },
- [setMinimalShellMode],
- )
+ const onPressSelected = React.useCallback(() => { emitSoftReset() }, [])
+ const onPageScrollStateChanged = React.useCallback((state) => {
+ 'worklet'
+ if (state === 'dragging') setMinimalShellMode(false)
+ }, [setMinimalShellMode])
const [demoMode] = useDemoMode()
+ const renderTabBar = React.useCallback((props) => {
+ return <HomeHeader key="FEEDS_TAB_BAR" {...props} testID="homeScreenFeedTabs" onPressSelected={onPressSelected} feeds={pinnedFeedInfos} />
+ }, [onPressSelected, pinnedFeedInfos])
- const renderTabBar = React.useCallback(
- (props: RenderTabBarFnProps) => {
- if (demoMode) {
- return (
- <HomeHeader
- key="FEEDS_TAB_BAR"
- {...props}
- testID="homeScreenFeedTabs"
- onPressSelected={onPressSelected}
- // @ts-ignore
- feeds={[{displayName: 'Following'}, {displayName: 'Discover'}]}
- />
- )
- }
- return (
- <HomeHeader
- key="FEEDS_TAB_BAR"
- {...props}
- testID="homeScreenFeedTabs"
- onPressSelected={onPressSelected}
- feeds={pinnedFeedInfos}
- />
- )
- },
- [onPressSelected, pinnedFeedInfos, demoMode],
- )
-
- const renderFollowingEmptyState = React.useCallback(() => {
- return <FollowingEmptyState />
- }, [])
+ const renderFollowingEmptyState = React.useCallback(() => <FollowingEmptyState />, [])
+ const renderCustomFeedEmptyState = React.useCallback(() => <CustomFeedEmptyState />, [])
- const renderCustomFeedEmptyState = React.useCallback(() => {
- return <CustomFeedEmptyState />
- }, [])
+ const homeFeedParams = React.useMemo(() => ({
+ mergeFeedEnabled: false, mergeFeedSources: []
+ }), [preferences])
- const homeFeedParams = React.useMemo<FeedParams>(() => {
- return {
- mergeFeedEnabled: Boolean(preferences.feedViewPrefs.lab_mergeFeedEnabled),
- mergeFeedSources: preferences.feedViewPrefs.lab_mergeFeedEnabled
- ? preferences.savedFeeds
- .filter(f => f.type === 'feed' || f.type === 'list')
- .map(f => f.value)
- : [],
- }
- }, [preferences])
-
- if (demoMode) {
- return (
- <Pager
- ref={pagerRef}
- testID="homeScreen"
- onPageSelected={onPageSelected}
- onPageScrollStateChanged={onPageScrollStateChanged}
- renderTabBar={renderTabBar}
- initialPage={selectedIndex}>
- <FeedPage
- testID="demoFeedPage"
- isPageFocused
- isPageAdjacent={false}
- feed="demo"
- renderEmptyState={renderCustomFeedEmptyState}
- feedInfo={pinnedFeedInfos[0]}
- />
- <FeedPage
- testID="customFeedPage"
- isPageFocused
- isPageAdjacent={false}
- feed={`feedgen|${PROD_DEFAULT_FEED('whats-hot')}`}
- renderEmptyState={renderCustomFeedEmptyState}
- feedInfo={pinnedFeedInfos[0]}
- />
- </Pager>
- )
- }
-
- return hasSession ? (
- <Pager
- key={allFeeds.join(',')}
- ref={pagerRef}
- testID="homeScreen"
- initialPage={selectedIndex}
- onPageSelected={onPageSelected}
- onPageScrollStateChanged={onPageScrollStateChanged}
- renderTabBar={renderTabBar}>
- {pinnedFeedInfos.length ? (
- pinnedFeedInfos.map((feedInfo, index) => {
+ return (
+ <Pager ref={pagerRef} testID="homeScreen" initialPage={selectedIndex} onPageSelected={onPageSelected} onPageScrollStateChanged={onPageScrollStateChanged} renderTabBar={renderTabBar}>
+ {pinnedFeedInfos.map((feedInfo, index) => {
const feed = feedInfo.feedDescriptor
if (feed === 'following') {
- return (
- <FeedPage
- key={feed}
- testID="followingFeedPage"
- isPageFocused={maybeSelectedFeed === feed}
- isPageAdjacent={Math.abs(selectedIndex - index) === 1}
- feed={feed}
- feedParams={homeFeedParams}
- renderEmptyState={renderFollowingEmptyState}
- renderEndOfFeed={FollowingEndOfFeed}
- feedInfo={feedInfo}
- />
- )
+ return <FeedPage key={feed} testID="followingFeedPage" isPageFocused={maybeSelectedFeed === feed} isPageAdjacent={Math.abs(selectedIndex - index) === 1} feed={feed} feedParams={homeFeedParams} renderEmptyState={renderFollowingEmptyState} renderEndOfFeed={FollowingEndOfFeed} feedInfo={feedInfo} />
}
- const savedFeedConfig = feedInfo.savedFeed
- return (
- <FeedPage
- key={feed}
- testID="customFeedPage"
- isPageFocused={maybeSelectedFeed === feed}
- isPageAdjacent={Math.abs(selectedIndex - index) === 1}
- feed={feed}
- renderEmptyState={renderCustomFeedEmptyState}
- savedFeedConfig={savedFeedConfig}
- feedInfo={feedInfo}
- />
- )
- })
- ) : (
- <NoFeedsPinned preferences={preferences} />
- )}
- </Pager>
- ) : (
- <Pager
- testID="homeScreen"
- onPageSelected={onPageSelected}
- onPageScrollStateChanged={onPageScrollStateChanged}
- renderTabBar={renderTabBar}>
- <FeedPage
- testID="customFeedPage"
- isPageFocused
- isPageAdjacent={false}
- feed={`feedgen|${PROD_DEFAULT_FEED('whats-hot')}`}
- renderEmptyState={renderCustomFeedEmptyState}
- feedInfo={pinnedFeedInfos[0]}
- />
+ return <FeedPage key={feed} testID="customFeedPage" isPageFocused={maybeSelectedFeed === feed} isPageAdjacent={Math.abs(selectedIndex - index) === 1} feed={feed} renderEmptyState={renderCustomFeedEmptyState} savedFeedConfig={feedInfo.savedFeed} feedInfo={feedInfo} />
+ })}
</Pager>
)
}
-
-const styles = StyleSheet.create({
- loading: {
- height: '100%',
- alignContent: 'center',
- justifyContent: 'center',
- paddingBottom: 100,
- },
-})
+const styles = StyleSheet.create({ loading: { height: '100%', alignContent: 'center', justifyContent: 'center', paddingBottom: 100 } })
diff --git a/src/view/screens/PrivacyPolicy.tsx b/src/view/screens/PrivacyPolicy.tsx
index a89eaadc4..1da393f03 100644
--- a/src/view/screens/PrivacyPolicy.tsx
+++ b/src/view/screens/PrivacyPolicy.tsx
@@ -1,52 +1,13 @@
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<CommonNavigatorParams, 'PrivacyPolicy'>
-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 (
<Layout.Screen>
- <ViewHeader title={_(msg`Privacy Policy`)} />
- <ScrollView style={[s.hContentRegion, pal.view]}>
- <View style={[s.p20]}>
- <Text style={pal.text}>
- <Trans>
- The Privacy Policy has been moved to{' '}
- <TextLink
- style={pal.link}
- href="https://bsky.social/about/support/privacy-policy"
- text="bsky.social/about/support/privacy-policy"
- />
- </Trans>
- </Text>
- </View>
- <View style={s.footerSpacer} />
- </ScrollView>
+ <WebView source={{ uri: 'https://syu.is/about/support/privacy-policy' }} style={{ flex: 1 }} />
</Layout.Screen>
)
}
diff --git a/src/view/screens/TermsOfService.tsx b/src/view/screens/TermsOfService.tsx
index d843c713c..b81767bd5 100644
--- a/src/view/screens/TermsOfService.tsx
+++ b/src/view/screens/TermsOfService.tsx
@@ -1,50 +1,13 @@
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<CommonNavigatorParams, 'TermsOfService'>
-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 (
<Layout.Screen>
- <ViewHeader title={_(msg`Terms of Service`)} />
- <ScrollView style={[s.hContentRegion, pal.view]}>
- <View style={[s.p20]}>
- <Text style={pal.text}>
- <Trans>The Terms of Service have been moved to</Trans>{' '}
- <TextLink
- style={pal.link}
- href="https://bsky.social/about/support/tos"
- text="bsky.social/about/support/tos"
- />
- </Text>
- </View>
- <View style={s.footerSpacer} />
- </ScrollView>
+ <WebView source={{ uri: 'https://syu.is/about/support/tos' }} style={{ flex: 1 }} />
</Layout.Screen>
)
}

View File

@@ -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 => {
<>
<SearchMenuItem isActive={isAtSearch} onPress={onPressSearch} />
<HomeMenuItem isActive={isAtHome} onPress={onPressHome} />
- <ChatMenuItem isActive={isAtMessages} onPress={onPressMessages} />
<NotificationsMenuItem
isActive={isAtNotifications}
onPress={onPressNotifications}
/>
<FeedsMenuItem isActive={isAtFeeds} onPress={onPressMyFeeds} />
- <ListsMenuItem onPress={onPressLists} />
- <BookmarksMenuItem
- isActive={isAtBookmarks}
- onPress={onPressBookmarks}
- />
<ProfileMenuItem
isActive={isAtMyProfile}
onPress={onPressProfile}
@@ -357,17 +351,7 @@ let DrawerFooter = ({
),
},
]}>
- <Button
- label={_(msg`Send feedback`)}
- size="small"
- variant="solid"
- color="secondary"
- onPress={onPressFeedback}>
- <ButtonIcon icon={Message} position="left" />
- <ButtonText>
- <Trans>Feedback</Trans>
- </ButtonText>
- </Button>
+{/* Feedback button removed for syu.is */}
<Button
label={_(msg`Get help`)}
size="small"
@@ -695,12 +679,12 @@ function ExtraLinks() {
<InlineLinkText
style={[a.text_md]}
label={_(msg`Terms of Service`)}
- to="https://bsky.social/about/support/tos">
+ to="https://syu.is/about/support/tos">
<Trans>Terms of Service</Trans>
</InlineLinkText>
<InlineLinkText
style={[a.text_md]}
- to="https://bsky.social/about/support/privacy-policy"
+ to="https://syu.is/about/support/privacy-policy"
label={_(msg`Privacy Policy`)}>
<Trans>Privacy Policy</Trans>
</InlineLinkText>

View File

@@ -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: {

View File

@@ -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`)}}
/>
+ <Stack.Screen
+ name="License"
+ getComponent={() => LicenseScreen}
+ options={{title: title(msg`License`)}}
+ />
<Stack.Screen
name="CommunityGuidelines"
getComponent={() => 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 (
+ <Layout.Screen>
+ <Layout.Header.Outer>
+ <Layout.Header.BackButton />
+ <Layout.Header.Content>
+ <Layout.Header.TitleText>License</Layout.Header.TitleText>
+ </Layout.Header.Content>
+ <Layout.Header.Slot />
+ </Layout.Header.Outer>
+ <Layout.Content>
+ <ScrollView
+ style={[a.flex_1]}
+ contentContainerStyle={[a.p_lg, a.pt_xl, a.pb_5xl]}>
+ <RNText style={styles.text}>
+ This application is based on Bluesky Social App.
+ </RNText>
+
+ <RNText style={styles.link}>
+ https://github.com/bluesky-social/social-app
+ </RNText>
+
+ <RNText style={styles.sectionTitle}>MIT License</RNText>
+
+ <RNText style={styles.mono}>
+ Copyright (c) 2022-2025 Bluesky PBC
+ </RNText>
+
+ <RNText style={styles.text}>
+ 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:
+ </RNText>
+
+ <RNText style={styles.text}>
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+ </RNText>
+
+ <RNText style={styles.text}>
+ 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.
+ </RNText>
+
+ <RNText style={styles.sectionTitle2}>日本語訳(参考)</RNText>
+
+ <RNText style={styles.text}>
+ 本ソフトウェアおよび関連文書ファイル(以下「ソフトウェア」)のコピーを取得する
+ すべての人に対し、ソフトウェアを無制限に扱うことを無償で許可します。これには、
+ ソフトウェアのコピーを使用、複製、変更、結合、公開、配布、サブライセンス、
+ および/または販売する権利、ならびにソフトウェアを提供する相手にそうした行為を
+ 許可する権利が含まれますが、これらに限定されません。
+ </RNText>
+
+ <RNText style={styles.text}>
+ 上記の著作権表示および本許諾表示を、ソフトウェアのすべてのコピーまたは
+ 重要な部分に記載するものとします。
+ </RNText>
+
+ <RNText style={styles.text}>
+ ソフトウェアは「現状のまま」で提供され、明示黙示を問わず、商品性、特定目的への
+ 適合性、および権利非侵害についての保証を含む、いかなる種類の保証もなされません。
+ いかなる場合においても、作者または著作権者は、契約行為、不法行為、またはそれ以外で
+ あろうと、ソフトウェアに起因または関連し、あるいはソフトウェアの使用または
+ その他の扱いによって生じる一切の請求、損害、その他の義務について責任を負わないものとします。
+ </RNText>
+
+ <RNText style={styles.footer}>
+ Original License: https://github.com/bluesky-social/social-app/blob/main/LICENSE
+ </RNText>
+ </ScrollView>
+ </Layout.Content>
+ </Layout.Screen>
+ )
+}
+
+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',
+ },
+})

View File

@@ -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,
]}>
<AppLanguageDropdown />
- <Text
- style={[
- a.flex_1,
- t.atoms.text_contrast_medium,
- !gtMobile && a.text_md,
- ]}>
- <Trans>Having trouble?</Trans>{' '}
- <InlineLinkText
- label={_(msg`Contact support`)}
- to={FEEDBACK_FORM_URL({email: state.email})}
- style={[!gtMobile && a.text_md]}>
- <Trans>Contact support</Trans>
- </InlineLinkText>
- </Text>
</View>
</View>
</ScreenTransition>

View File

@@ -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<AllNavigatableRoutes>({
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 = ({
<View style={[a.pb_sm, a.pt_5xl]}>
<Logotype width={161} fill={t.atoms.text.color} />
</View>
-
- <Text
- style={[
- a.text_md,
- a.font_semi_bold,
- t.atoms.text_contrast_medium,
- a.text_center,
- ]}>
- <Trans>What's up?</Trans>
- </Text>
</View>
<View
@@ -102,6 +93,21 @@ export const SplashScreen = ({
<AppLanguageDropdown />
</View>
</View>
+ <View style={[a.pb_sm, a.justify_center, a.align_center]}>
+ <Pressable onPress={() => Linking.openURL('https://syu.is/about/support/license')}>
+ <Text
+ style={[
+ a.text_xs,
+ t.atoms.text_contrast_low,
+ {textDecorationLine: 'underline'},
+ ]}>
+ License
+ </Text>
+ </Pressable>
+ </View>
+ <View style={[a.pb_xl, a.justify_center, a.align_center]}>
+ <Text style={[a.text_xs, t.atoms.text_contrast_low]}>© syui</Text>
+ </View>
<View style={{height: insets.bottom}} />
</ErrorBoundary>
</Animated.View>
diff --git a/src/view/com/auth/SplashScreen.web.tsx b/src/view/com/auth/SplashScreen.web.tsx
--- a/src/view/com/auth/SplashScreen.web.tsx
+++ b/src/view/com/auth/SplashScreen.web.tsx
@@ -94,14 +94,6 @@ export const SplashScreen = ({
</View>
)}
- <Text
- style={[
- a.text_md,
- a.font_semi_bold,
- t.atoms.text_contrast_medium,
- ]}>
- <Trans>What's up?</Trans>
- </Text>
</View>
<View

View File

@@ -0,0 +1,48 @@
diff --git a/src/screens/Settings/Settings.tsx b/src/screens/Settings/Settings.tsx
index 6b0e184c0..42b609c9e 100644
--- a/src/screens/Settings/Settings.tsx
+++ b/src/screens/Settings/Settings.tsx
@@ -203,24 +203,8 @@ export function SettingsScreen({}: Props) {
<Trans>Notifications</Trans>
</SettingsList.ItemText>
</SettingsList.LinkItem>
- <SettingsList.LinkItem
- to="/settings/content-and-media"
- label={_(msg`Content and media`)}>
- <SettingsList.ItemIcon icon={WindowIcon} />
- <SettingsList.ItemText>
- <Trans>Content and media</Trans>
- </SettingsList.ItemText>
- </SettingsList.LinkItem>
- {isNative && findContactsEnabled && (
- <SettingsList.LinkItem
- to="/settings/find-contacts"
- label={_(msg`Find friends from contacts`)}>
- <SettingsList.ItemIcon icon={ContactsIcon} />
- <SettingsList.ItemText>
- <Trans>Find friends from contacts</Trans>
- </SettingsList.ItemText>
- </SettingsList.LinkItem>
- )}
+{/* Content and media removed for syu.is */}
+{/* Find friends from contacts removed for syu.is */}
<SettingsList.LinkItem
to="/settings/appearance"
label={_(msg`Appearance`)}>
@@ -245,16 +229,6 @@ export function SettingsScreen({}: Props) {
<Trans>Languages</Trans>
</SettingsList.ItemText>
</SettingsList.LinkItem>
- <SettingsList.PressableItem
- onPress={() => Linking.openURL(HELP_DESK_URL)}
- label={_(msg`Help`)}
- accessibilityHint={_(msg`Opens helpdesk in browser`)}>
- <SettingsList.ItemIcon icon={CircleQuestionIcon} />
- <SettingsList.ItemText>
- <Trans>Help</Trans>
- </SettingsList.ItemText>
- <SettingsList.Chevron />
- </SettingsList.PressableItem>
<SettingsList.LinkItem to="/settings/about" label={_(msg`About`)}>
<SettingsList.ItemIcon icon={BubbleInfoIcon} />
<SettingsList.ItemText>

View File

@@ -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

View File

@@ -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 (
<AgeAssuranceStateContext.Provider
value={useMemo(() => {
- 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])}>

View File

@@ -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 (
- <View
- style={[
- {
- flexDirection: 'row',
- alignItems: 'center',
- paddingVertical: 12,
- paddingHorizontal: 12,
- borderTopWidth: 1,
- },
- pal.border,
- pal.viewLight,
- ]}>
- <View style={{width: 68, paddingLeft: 12}}>
- <InfoCircleIcon size={36} style={pal.textLight} strokeWidth={1.5} />
- </View>
- <View style={{flex: 1}}>
- <Text type="md" style={pal.text}>
- <Trans>
- We ran out of posts from your follows. Here's the latest from{' '}
- <TextLink
- type="md-medium"
- href="/profile/bsky.app/feed/whats-hot"
- text="Discover"
- style={pal.link}
- />
- .
- </Trans>
- </Text>
- </View>
- </View>
- )
+ 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<NavigationProp>()
-
- 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 (
<View style={styles.container}>
@@ -45,36 +22,6 @@ export function FollowingEmptyState() {
happening.
</Trans>
</Text>
- <Button
- type="inverted"
- style={styles.emptyBtn}
- onPress={onPressFindAccounts}>
- <Text type="lg-medium" style={palInverted.text}>
- <Trans>Find accounts to follow</Trans>
- </Text>
- <FontAwesomeIcon
- icon="angle-right"
- style={palInverted.text as FontAwesomeIconStyle}
- size={14}
- />
- </Button>
-
- <Text type="xl-medium" style={[s.textCenter, pal.text, s.mt20]}>
- <Trans>You can also discover new Custom Feeds to follow.</Trans>
- </Text>
- <Button
- type="inverted"
- style={[styles.emptyBtn, s.mt10]}
- onPress={onPressDiscoverFeeds}>
- <Text type="lg-medium" style={palInverted.text}>
- <Trans>Discover new custom feeds</Trans>
- </Text>
- <FontAwesomeIcon
- icon="angle-right"
- style={palInverted.text as FontAwesomeIconStyle}
- size={14}
- />
- </Button>
</View>
</View>
)
@@ -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<NavigationProp>()
-
- 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 (
<View
@@ -41,41 +18,8 @@ export function FollowingEndOfFeed() {
]}>
<View style={styles.inner}>
<Text type="xl-medium" style={[s.textCenter, pal.text]}>
- <Trans>
- You've reached the end of your feed! Find some more accounts to
- follow.
- </Trans>
- </Text>
- <Button
- type="inverted"
- style={styles.emptyBtn}
- onPress={onPressFindAccounts}>
- <Text type="lg-medium" style={palInverted.text}>
- <Trans>Find accounts to follow</Trans>
- </Text>
- <FontAwesomeIcon
- icon="angle-right"
- style={palInverted.text as FontAwesomeIconStyle}
- size={14}
- />
- </Button>
-
- <Text type="xl-medium" style={[s.textCenter, pal.text, s.mt20]}>
- <Trans>You can also discover new Custom Feeds to follow.</Trans>
+ <Trans>You've reached the end of your feed!</Trans>
</Text>
- <Button
- type="inverted"
- style={[styles.emptyBtn, s.mt10]}
- onPress={onPressDiscoverFeeds}>
- <Text type="lg-medium" style={palInverted.text}>
- <Trans>Discover new custom feeds</Trans>
- </Text>
- <FontAwesomeIcon
- icon="angle-right"
- style={palInverted.text as FontAwesomeIconStyle}
- size={14}
- />
- </Button>
</View>
</View>
)
@@ -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 <FeedShutdownMsg feedUri={feedUriOrActorDid} />
} else if (row.type === 'interstitialFollows') {
- return <SuggestedFollows feed={feed} />
+ return null
} else if (row.type === 'interstitialProgressGuide') {
return <ProgressGuide />
} else if (row.type === 'ageAssuranceBanner') {

View File

@@ -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)
+}

View File

@@ -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<MessagesEventBus | null>(
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 (
- <MessagesEventBusContext.Provider value={null}>
- {children}
- </MessagesEventBusContext.Provider>
- )
- }
-
+ // DM functionality is disabled for syu.is
return (
- <MessagesEventBusProviderInner>{children}</MessagesEventBusProviderInner>
+ <MessagesEventBusContext.Provider value={null}>
+ {children}
+ </MessagesEventBusContext.Provider>
)
}
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 (
- <ListConvosContext.Provider value={empty}>
- {children}
- </ListConvosContext.Provider>
- )
- }
-
- return <ListConvosProviderInner>{children}</ListConvosProviderInner>
+ // DM functionality is disabled for syu.is - always return empty
+ return (
+ <ListConvosContext.Provider value={empty}>
+ {children}
+ </ListConvosContext.Provider>
+ )
}
export function ListConvosProviderInner({

View File

@@ -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

View File

@@ -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 @@
<!--
Preconnect to essential domains
-->
- <link rel="preconnect" href="https://bsky.social">
- <link rel="preconnect" href="https://go.bsky.app">
- <title>{%- block head_title -%}Bluesky{%- endblock -%}</title>
+ <link rel="preconnect" href="https://syu.is">
+ <link rel="preconnect" href="https://bsky.syu.is">
+ <title>{%- block head_title -%}syu.is{%- endblock -%}</title>
<!-- Hello Humans! API docs at https://atproto.com -->
@@ -121,7 +121,7 @@
<noscript>
<h1 lang="en">JavaScript Required</h1>
<p lang="en">This is a heavily interactive web application, and JavaScript is required. Simple HTML interfaces are possible, but that is not what this is.
- <p lang="en">Learn more about Bluesky at <a href="https://bsky.social">bsky.social</a> and <a href="https://atproto.com">atproto.com</a>.
+ <p lang="en">Learn more at <a href="https://syu.is">syu.is</a> and <a href="https://atproto.com">atproto.com</a>.
{% block noscript_extra %}{% endblock %}
</noscript>
{% 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 -%}
- <meta property="og:title" content="Bluesky" />
- <meta name="twitter:title" content="Bluesky" />
+ <meta property="og:title" content="syu.is" />
+ <meta name="twitter:title" content="syu.is" />
<meta name="description" content="Social media as it should be. Find your community among millions of users, unleash your creativity, and have some fun again." />
<meta name="og:description" content="Social media as it should be. Find your community among millions of users, unleash your creativity, and have some fun again." />
<meta name="twitter:description" content="Social media as it should be. Find your community among millions of users, unleash your creativity, and have some fun again." />
- <meta property="og:url" content="https://bsky.app" />
- <meta name="twitter:url" content="https://bsky.app" />
- <link rel="canonical" href="https://bsky.app" />
+ <meta property="og:url" content="https://syu.is" />
+ <meta name="twitter:url" content="https://syu.is" />
+ <link rel="canonical" href="https://syu.is" />
<meta property="og:image" content="https://bsky.app/static/social-card-default-gradient.png" />
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 @@
<meta property="og:title" content="{{ title }}" />
<meta name="twitter:title" content="{{ title }}" />
{%- else -%}
- <meta property="og:title" content="Bluesky" />
- <meta name="twitter:title" content="Bluesky" />
+ <meta property="og:title" content="syu.is" />
+ <meta name="twitter:title" content="syu.is" />
{% endif -%}
<meta name="description" content="Join the conversation" />
<meta name="og:description" content="Join the conversation" />
diff --git a/web/index.html b/web/index.html
--- a/web/index.html
+++ b/web/index.html
@@ -14,8 +14,8 @@
<!--
Preconnect to essential domains
-->
- <link rel="preconnect" href="https://bsky.social">
- <link rel="preconnect" href="https://go.bsky.app">
+ <link rel="preconnect" href="https://syu.is">
+ <link rel="preconnect" href="https://bsky.syu.is">
<title>%WEB_TITLE%</title>
<link rel="preload" as="font" type="font/woff2" href="/static/media/InterVariable.c504db5c06caaf7cdfba.woff2" crossorigin>

View File

@@ -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<TextInput>(null)
const passwordInputRef = useRef<TextInput>(null)
- const birthdateInputRef = useRef<DateFieldRef>(null)
const [hasWarnedEmail, setHasWarnedEmail] = React.useState<boolean>(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;"
/>
</TextField.Root>
</View>
- <View>
- <DateField.LabelText>
- <Trans>Your birth date</Trans>
- </DateField.LabelText>
- <DateField.DateField
- testID="date"
- inputRef={birthdateInputRef}
- value={state.dateOfBirth}
- onChangeDate={date => {
- dispatch({
- type: 'setDateOfBirth',
- value: sanitizeDate(new Date(date)),
- })
- }}
- label={_(msg`Date of birth`)}
- accessibilityHint={_(msg`Select your date of birth`)}
- maximumDate={new Date()}
- />
- </View>
<Policies
serviceDescription={state.serviceDescription}
- needsGuardian={!is18(state.dateOfBirth)}
- under13={!is13(state.dateOfBirth)}
+ needsGuardian={false}
+ under13={false}
/>
</>
) : undefined}
</View>
<BackNextButtons
- hideNext={!is13(state.dateOfBirth)}
+ hideNext={false}
showRetry={isServerError}
isLoading={state.isLoading}
onBackPress={onPressBack}

View File

@@ -0,0 +1,16 @@
diff --git a/src/screens/Search/Explore.tsx b/src/screens/Search/Explore.tsx
--- a/src/screens/Search/Explore.tsx
+++ b/src/screens/Search/Explore.tsx
@@ -687,12 +687,7 @@ export function Explore({
if (useFullExperience) {
i.push(trendingTopicsModule)
- i.push(...suggestedFeedsModule)
- i.push(...suggestedFollowsModule)
- i.push(...suggestedStarterPacksModule)
i.push(...feedPreviewsModule)
- } else {
- i.push(...suggestedFollowsModule)
}
return i

View File

@@ -0,0 +1,84 @@
diff --git a/src/view/screens/Feeds.tsx b/src/view/screens/Feeds.tsx
--- a/src/view/screens/Feeds.tsx
+++ b/src/view/screens/Feeds.tsx
@@ -288,80 +288,7 @@ export function FeedsScreen(_props: Props) {
}
}
- if (!hasSession || (hasSession && canShowDiscoverSection)) {
- slices.push({
- key: 'popularFeedsHeader',
- type: 'popularFeedsHeader',
- })
- if (popularFeedsError || searchError) {
- slices.push({
- key: 'popularFeedsError',
- type: 'error',
- error: cleanError(
- popularFeedsError?.toString() ?? searchError?.toString() ?? '',
- ),
- })
- } else {
- if (isUserSearching) {
- if (isSearchPending || !searchResults) {
- slices.push({
- key: 'popularFeedsLoading',
- type: 'popularFeedsLoading',
- })
- } else {
- if (!searchResults || searchResults?.length === 0) {
- slices.push({
- key: 'popularFeedsNoResults',
- type: 'popularFeedsNoResults',
- })
- } else {
- slices = slices.concat(
- searchResults.map(feed => ({
- 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,

View File

@@ -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`)}}
/>
+ <Stack.Screen
+ name="AppInfo"
+ getComponent={() => AppInfoScreen}
+ options={{title: title(msg`App Info`)}}
+ />
<Stack.Screen
name="CommunityGuidelines"
getComponent={() => 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<AllNavigatableRoutes>({
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 (
+ <Layout.Screen>
+ <Layout.Header.Outer>
+ <Layout.Header.BackButton />
+ <Layout.Header.Content>
+ <Layout.Header.TitleText>
+ <Trans>App Info</Trans>
+ </Layout.Header.TitleText>
+ </Layout.Header.Content>
+ <Layout.Header.Slot />
+ </Layout.Header.Outer>
+ <Layout.Content>
+ <ScrollView
+ style={[a.flex_1]}
+ contentContainerStyle={[a.p_lg, a.pt_xl, a.pb_5xl]}>
+ {/* App Header */}
+ <View style={styles.appHeader}>
+ <View style={[styles.appIconContainer, t.atoms.bg_contrast_25]}>
+ <Image
+ source={require('../../../assets/logo.png')}
+ style={styles.appIcon}
+ resizeMode="cover"
+ />
+ </View>
+ <Text style={[styles.appName, t.atoms.text]}>
+ {APP_NAME}
+ </Text>
+ <Text style={[styles.appVersion, t.atoms.text_contrast_medium]}>
+ v{APP_VERSION}
+ </Text>
+ </View>
+
+ {/* Description Section */}
+ <View style={[styles.section, t.atoms.bg_contrast_25]}>
+ <Text style={[styles.description, t.atoms.text]}>
+ {APP_NAME} is a social networking application based on AT Protocol.
+ Connect with your community on syu.is.
+ </Text>
+ </View>
+
+ {/* App Information Section */}
+ <View style={[styles.section, t.atoms.bg_contrast_25]}>
+ <Text style={[styles.sectionTitle, t.atoms.text_contrast_medium]}>
+ App Information
+ </Text>
+ <View style={styles.infoGrid}>
+ <View style={[styles.infoItem, t.atoms.bg_contrast_50]}>
+ <Text style={[styles.infoLabel, t.atoms.text_contrast_medium]}>
+ Version
+ </Text>
+ <Text style={[styles.infoValue, t.atoms.text]}>
+ {APP_VERSION}
+ </Text>
+ </View>
+ <View style={[styles.infoItem, t.atoms.bg_contrast_50]}>
+ <Text style={[styles.infoLabel, t.atoms.text_contrast_medium]}>
+ Category
+ </Text>
+ <Text style={[styles.infoValue, t.atoms.text]}>
+ Social
+ </Text>
+ </View>
+ <View style={[styles.infoItem, t.atoms.bg_contrast_50]}>
+ <Text style={[styles.infoLabel, t.atoms.text_contrast_medium]}>
+ Supported OS
+ </Text>
+ <Text style={[styles.infoValue, t.atoms.text]}>
+ iOS 26.0+
+ </Text>
+ </View>
+ <View style={[styles.infoItem, t.atoms.bg_contrast_50]}>
+ <Text style={[styles.infoLabel, t.atoms.text_contrast_medium]}>
+ Price
+ </Text>
+ <Text style={[styles.infoValue, t.atoms.text]}>
+ Free
+ </Text>
+ </View>
+ </View>
+ </View>
+
+ {/* Developer Section */}
+ <View style={[styles.section, t.atoms.bg_contrast_25]}>
+ <Text style={[styles.sectionTitle, t.atoms.text_contrast_medium]}>
+ Developer
+ </Text>
+ <View style={styles.developerCard}>
+ <Text style={[styles.developerName, t.atoms.text]}>syui</Text>
+ </View>
+
+ <Pressable
+ onPress={() => openUrl('https://github.com/syui')}
+ style={[styles.linkRow, t.atoms.border_contrast_low]}>
+ <Text style={[styles.linkIcon, t.atoms.text_contrast_medium]}>
+ GitHub
+ </Text>
+ <Text style={[styles.linkValue, {color: '#0084ff'}]}>
+ github.com/syui
+ </Text>
+ <Text style={[styles.linkArrow, t.atoms.text_contrast_low]}>→</Text>
+ </Pressable>
+
+ <Pressable
+ onPress={() => openUrl('https://syu.is/syui')}
+ style={[styles.linkRow, t.atoms.border_contrast_low]}>
+ <Text style={[styles.linkIcon, t.atoms.text_contrast_medium]}>
+ ATProto
+ </Text>
+ <Text style={[styles.linkValue, {color: '#0084ff'}]}>
+ syu.is/syui
+ </Text>
+ <Text style={[styles.linkArrow, t.atoms.text_contrast_low]}>→</Text>
+ </Pressable>
+ </View>
+
+ {/* Bitcoin Section */}
+ <View style={[styles.section, t.atoms.bg_contrast_25]}>
+ <Text style={[styles.sectionTitle, t.atoms.text_contrast_medium]}>
+ Bitcoin
+ </Text>
+ <Pressable
+ onPress={() => copyToClipboard(BITCOIN_ADDRESS)}
+ style={styles.bitcoinRow}>
+ <Text style={styles.bitcoinLabel}>₿</Text>
+ <Text style={[styles.bitcoinAddress, t.atoms.text_contrast_medium]}>
+ {BITCOIN_ADDRESS}
+ </Text>
+ <Text style={[styles.copyHint, copied && styles.copiedHint]}>
+ {copied ? 'copied!' : 'copy'}
+ </Text>
+ </Pressable>
+ </View>
+
+ {/* Copyright */}
+ <View style={styles.copyright}>
+ <Text style={[styles.copyrightText, t.atoms.text_contrast_low]}>
+ © syui
+ </Text>
+ </View>
+ </ScrollView>
+ </Layout.Content>
+ </Layout.Screen>
+ )
+}
+
+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,
+ },
+})

View File

@@ -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}},

View File

@@ -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 = [

View File

@@ -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<NavigationProp>()
const hasPinnedCustom = React.useMemo<boolean>(() => {
- 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)

View File

@@ -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)
)
},
},

310
ios/patching/AppInfo.tsx Normal file
View File

@@ -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 (
<Layout.Screen>
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>App Info</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content>
<ScrollView
style={[a.flex_1]}
contentContainerStyle={[a.p_lg, a.pt_xl, a.pb_5xl]}>
{/* App Header */}
<View style={styles.appHeader}>
<View style={[styles.appIconContainer, t.atoms.bg_contrast_25]}>
<Image
source={require('../../../assets/logo.png')}
style={styles.appIcon}
resizeMode="cover"
/>
</View>
<Text style={[styles.appName, t.atoms.text]}>
{APP_NAME}
</Text>
<Text style={[styles.appVersion, t.atoms.text_contrast_medium]}>
v{APP_VERSION}
</Text>
</View>
{/* Description Section */}
<View style={[styles.section, t.atoms.bg_contrast_25]}>
<Text style={[styles.description, t.atoms.text]}>
{APP_NAME} is a social networking application based on AT Protocol.
Connect with your community on syu.is.
</Text>
</View>
{/* App Information Section */}
<View style={[styles.section, t.atoms.bg_contrast_25]}>
<Text style={[styles.sectionTitle, t.atoms.text_contrast_medium]}>
App Information
</Text>
<View style={styles.infoGrid}>
<View style={[styles.infoItem, t.atoms.bg_contrast_50]}>
<Text style={[styles.infoLabel, t.atoms.text_contrast_medium]}>
Version
</Text>
<Text style={[styles.infoValue, t.atoms.text]}>
{APP_VERSION}
</Text>
</View>
<View style={[styles.infoItem, t.atoms.bg_contrast_50]}>
<Text style={[styles.infoLabel, t.atoms.text_contrast_medium]}>
Category
</Text>
<Text style={[styles.infoValue, t.atoms.text]}>
Social
</Text>
</View>
<View style={[styles.infoItem, t.atoms.bg_contrast_50]}>
<Text style={[styles.infoLabel, t.atoms.text_contrast_medium]}>
Supported OS
</Text>
<Text style={[styles.infoValue, t.atoms.text]}>
iOS 26.0+
</Text>
</View>
<View style={[styles.infoItem, t.atoms.bg_contrast_50]}>
<Text style={[styles.infoLabel, t.atoms.text_contrast_medium]}>
Price
</Text>
<Text style={[styles.infoValue, t.atoms.text]}>
Free
</Text>
</View>
</View>
</View>
{/* Developer Section */}
<View style={[styles.section, t.atoms.bg_contrast_25]}>
<Text style={[styles.sectionTitle, t.atoms.text_contrast_medium]}>
Developer
</Text>
<View style={styles.developerCard}>
<Text style={[styles.developerName, t.atoms.text]}>syui</Text>
</View>
<Pressable
onPress={() => openUrl('https://git.syui.ai/syui')}
style={[styles.linkRow, t.atoms.border_contrast_low]}>
<Text style={[styles.linkIcon, t.atoms.text_contrast_medium]}>
Git
</Text>
<Text style={[styles.linkValue, {color: '#0084ff'}]}>
git.syui.ai/syui
</Text>
<Text style={[styles.linkArrow, t.atoms.text_contrast_low]}></Text>
</Pressable>
<Pressable
onPress={() => openUrl('https://syu.is/syui')}
style={[styles.linkRow, t.atoms.border_contrast_low]}>
<Text style={[styles.linkIcon, t.atoms.text_contrast_medium]}>
ATProto
</Text>
<Text style={[styles.linkValue, {color: '#0084ff'}]}>
syu.is/syui
</Text>
<Text style={[styles.linkArrow, t.atoms.text_contrast_low]}></Text>
</Pressable>
</View>
{/* Bitcoin Section */}
<View style={[styles.section, t.atoms.bg_contrast_25]}>
<Text style={[styles.sectionTitle, t.atoms.text_contrast_medium]}>
Bitcoin
</Text>
<Pressable
onPress={() => copyToClipboard(BITCOIN_ADDRESS)}
style={styles.bitcoinRow}>
<Text style={styles.bitcoinLabel}></Text>
<Text style={[styles.bitcoinAddress, t.atoms.text_contrast_medium]}>
{BITCOIN_ADDRESS}
</Text>
<Text style={[styles.copyHint, copied && styles.copiedHint]}>
{copied ? 'copied!' : 'copy'}
</Text>
</Pressable>
</View>
{/* Copyright */}
<View style={styles.copyright}>
<Text style={[styles.copyrightText, t.atoms.text_contrast_low]}>
© syui
</Text>
</View>
</ScrollView>
</Layout.Content>
</Layout.Screen>
)
}
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,
},
})

86
ios/patching/License.tsx Normal file
View File

@@ -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 (
<Layout.Screen>
<ScrollView
style={[a.flex_1, {backgroundColor: t.palette.white}]}
contentContainerStyle={[a.p_lg, a.pt_5xl, a.pb_5xl]}>
<Text style={[a.text_2xl, a.font_bold, a.mb_lg]}>License</Text>
<Text style={[a.mb_md]}>
This application is based on Bluesky Social App.
</Text>
<Text style={[a.text_md, a.mb_md, {color: t.palette.primary_500}]}>
https://github.com/bluesky-social/social-app
</Text>
<Text style={[a.text_lg, a.font_bold, a.mt_lg, a.mb_md]}>MIT License</Text>
<Text style={[a.mb_md, {fontFamily: 'monospace'}]}>
Copyright (c) 2022-2025 Bluesky PBC
</Text>
<Text style={[a.mb_md]}>
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:
</Text>
<Text style={[a.mb_md]}>
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
</Text>
<Text style={[a.mb_md]}>
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.
</Text>
<Text style={[a.text_lg, a.font_bold, a.mt_xl, a.mb_md]}></Text>
<Text style={[a.mb_md]}>
使
/
</Text>
<Text style={[a.mb_md]}>
</Text>
<Text style={[a.mb_md]}>
使
</Text>
<Text style={[a.text_sm, a.mt_xl, {color: t.palette.contrast_500}]}>
Original License: https://github.com/bluesky-social/social-app/blob/main/LICENSE
</Text>
</ScrollView>
</Layout.Screen>
)
}

62
ios/patching/README.md Normal file
View File

@@ -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` 配列に新しいパッチファイル名を追加してください。

147
ios/preview.zsh Executable file
View File

@@ -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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.security.application-groups</key>
<array>
<string>${APP_GROUP}</string>
</array>
</dict>
</plist>
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

295
ios/setup.zsh Executable file
View File

@@ -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

View File

@@ -22,14 +22,31 @@ index 0000000..993c83d
+ +
+EXPOSE 3000 +EXPOSE 3000
+CMD ["yarn", "start"] +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 diff --git a/scripts/publish.ts b/scripts/publish.ts
new file mode 100644 new file mode 100644
index 0000000..966edcf index 0000000..044f1d9
--- /dev/null --- /dev/null
+++ b/scripts/publish.ts +++ b/scripts/publish.ts
@@ -0,0 +1,64 @@ @@ -0,0 +1,64 @@
+import dotenv from 'dotenv' +import dotenv from 'dotenv'
+import { AtpAgent, BlobRef, AppBskyFeedDefs } from '@atproto/api' +import { AtpAgent, BlobRef } from '@atproto/api'
+import fs from 'fs/promises' +import fs from 'fs/promises'
+import { ids } from '../src/lexicon/lexicons' +import { ids } from '../src/lexicon/lexicons'
+ +
@@ -88,7 +105,7 @@ index 0000000..966edcf
+ }, + },
+ }) + })
+ +
+ console.log('All done 🎉') + console.log('All done')
+} +}
+ +
+run() +run()
@@ -152,12 +169,15 @@ index b7ee48a..102cb93 100644
export default algos export default algos
diff --git a/src/index.ts b/src/index.ts diff --git a/src/index.ts b/src/index.ts
index c3bd006..1e7f0b5 100644 index 7128525..40d985c 100644
--- a/src/index.ts --- a/src/index.ts
+++ b/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( 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('Supported algos:', Object.keys(require('./algos').default))
+ console.log('Publisher DID:', server.cfg.publisherDid) + console.log('Publisher DID:', server.cfg.publisherDid)
@@ -165,7 +185,7 @@ index c3bd006..1e7f0b5 100644
const maybeStr = (val?: string) => { const maybeStr = (val?: string) => {
diff --git a/src/methods/feed-generation.ts b/src/methods/feed-generation.ts 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 --- a/src/methods/feed-generation.ts
+++ b/src/methods/feed-generation.ts +++ b/src/methods/feed-generation.ts
@@ -10,7 +10,7 @@ export default function (server: Server, ctx: AppContext) { @@ -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' || feedUri.collection !== 'app.bsky.feed.generator' ||
!algo !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<http.Server> {
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 diff --git a/src/subscription.ts b/src/subscription.ts
index 0422a03..d591ef9 100644 index 0422a03..7785982 100644
--- a/src/subscription.ts --- a/src/subscription.ts
+++ b/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 postsToDelete = ops.posts.deletes.map((del) => del.uri)
const postsToCreate = ops.posts.creates - const postsToCreate = ops.posts.creates
- .filter((create) => { - .filter((create) => {
- // only alf-related posts - // only alf-related posts
- return create.record.text.toLowerCase().includes('alf') - return create.record.text.toLowerCase().includes('alf')
- }) - })
.map((create) => { - .map((create) => {
// map alf-related posts to a db row - // map alf-related posts to a db row
return { - 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()
}
}