diff --git a/.github/workflows/ios-release.yml b/.github/workflows/ios-release.yml
deleted file mode 100644
index 33b7e43..0000000
--- a/.github/workflows/ios-release.yml
+++ /dev/null
@@ -1,93 +0,0 @@
-name: iOS Release (preview)
-
-on:
- workflow_dispatch:
- inputs:
- tag:
- description: 'Tag for the pre-release (e.g. v0.15.0-ios.1)'
- required: true
- default: 'v0.0.0-ios.0'
- push:
- tags:
- - 'v*-ios.*'
- - 'v*-ios-*'
-
-permissions:
- contents: write
-
-jobs:
- build:
- runs-on: macos-14
- steps:
- - uses: actions/checkout@v4
-
- - uses: actions/setup-go@v5
- with:
- go-version: '1.26'
- cache: true
-
- - name: Select Xcode
- run: sudo xcode-select -s /Applications/Xcode.app
-
- - name: Install gomobile + gobind
- run: |
- go install golang.org/x/mobile/cmd/gomobile@latest
- go install golang.org/x/mobile/cmd/gobind@latest
- gomobile init
- go get golang.org/x/mobile/bind golang.org/x/mobile/bind/objc
- go mod tidy
-
- - name: Build Mobile.xcframework
- run: gomobile bind -iosversion=14.0 -target=ios,iossimulator -o ios/Mobile.xcframework ./mobile
-
- - name: Archive (unsigned)
- run: |
- xcodebuild \
- -project ios/Thefeed.xcodeproj \
- -scheme Thefeed \
- -configuration Release \
- -destination 'generic/platform=iOS' \
- -archivePath build/Thefeed.xcarchive \
- archive \
- CODE_SIGN_IDENTITY="" \
- CODE_SIGNING_REQUIRED=NO \
- CODE_SIGNING_ALLOWED=NO \
- DEVELOPMENT_TEAM=""
-
- - name: Pack unsigned IPA
- run: |
- mkdir -p build/Payload
- cp -r build/Thefeed.xcarchive/Products/Applications/Thefeed.app build/Payload/
- (cd build && zip -qry "Thefeed-${GITHUB_REF_NAME:-${{ github.event.inputs.tag }}}-unsigned.ipa" Payload)
- ls -lh build/*.ipa
-
- - name: Resolve tag
- id: tag
- run: |
- TAG="${GITHUB_REF_NAME}"
- if [ -z "$TAG" ] || [ "$TAG" = "main" ]; then TAG="${{ github.event.inputs.tag }}"; fi
- echo "tag=$TAG" >> "$GITHUB_OUTPUT"
-
- - name: Create pre-release
- uses: softprops/action-gh-release@v2
- with:
- tag_name: ${{ steps.tag.outputs.tag }}
- name: ${{ steps.tag.outputs.tag }} (iOS preview)
- prerelease: true
- generate_release_notes: false
- body: |
- ⚠️ **iOS-only preview release.**
-
- This release ships only the iOS build. Other platforms
- (server, Linux/macOS/Windows clients, Android APKs) are not
- included — pull the latest stable tag for those.
-
- The IPA is **unsigned**. To install:
- - Re-sign with your own provisioning profile and distribute via TestFlight, or
- - Use a sideloading tool such as AltStore / Sideloadly with your own Apple ID.
-
- iOS 14.0 or newer required. Universal binary (iPhone + iPad).
- files: |
- build/Thefeed-*.ipa
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/internal/web/static/index.html b/internal/web/static/index.html
index e401bea..bb74cc1 100644
--- a/internal/web/static/index.html
+++ b/internal/web/static/index.html
@@ -2940,6 +2940,51 @@
+
+
+
+
TheFeed
+
Choose your language · زبان خود را انتخاب کنید
+
+
+
+
+
+
+
@@ -4043,9 +4088,21 @@
// native value first survives an embedded server rebinding to a
// new loopback port on each launch.
var lang = (function () {
+ // Same precedence as the modal-init script: server first (so a
+ // wiped thefeeddata/ also wipes the language), then native bridge,
+ // then localStorage as a final cache.
+ try {
+ var xhr = new XMLHttpRequest();
+ xhr.open('GET', '/api/settings', false);
+ xhr.send();
+ if (xhr.status === 200) {
+ var d = JSON.parse(xhr.responseText);
+ if (d.lang) return d.lang;
+ }
+ } catch (e) { }
try { if (typeof IOS !== 'undefined' && IOS.getLang) { var v = IOS.getLang(); if (v) return v; } } catch (e) { }
try { if (typeof Android !== 'undefined' && Android.getLang) { var v2 = Android.getLang(); if (v2) return v2; } } catch (e) { }
- return localStorage.getItem('thefeed_lang') || 'fa';
+ return localStorage.getItem('thefeed_lang') || '';
})();
function t(k) { return (I18N[lang] && I18N[lang][k]) || I18N.en[k] || k }
function applyLang() {
@@ -4055,8 +4112,8 @@
document.querySelectorAll('[data-i18n]').forEach(function (el) { el.textContent = t(el.dataset.i18n) });
document.querySelectorAll('[data-i18n-ph]').forEach(function (el) { el.placeholder = t(el.dataset.i18nPh) });
document.querySelectorAll('[data-i18n-title]').forEach(function (el) { el.title = t(el.dataset.i18nTitle) });
- document.getElementById('langFa').classList.toggle('active-lang', lang === 'fa');
- document.getElementById('langEn').classList.toggle('active-lang', lang === 'en');
+ var lf = document.getElementById('langFa'); if (lf) lf.classList.toggle('active-lang', lang === 'fa');
+ var le = document.getElementById('langEn'); if (le) le.classList.toggle('active-lang', lang === 'en');
document.getElementById('sendInput').style.direction = isRtl ? 'rtl' : 'ltr';
applyThemeButtons();
// Re-render dynamic content
@@ -5351,9 +5408,15 @@
// pre-named profile instead of "thefeed.example.com" pulled
// from the domain. Capped at 32 chars on this side; the
// import side enforces the same cap defensively.
+ // Drop the default ":53" suffix from each resolver — the parser
+ // assumes 53 when no port is given, so omitting it shortens the
+ // URI considerably. Custom ports are kept and stay un-encoded.
+ var compact = resolvers.map(function (r) {
+ return r.replace(/:53$/, '');
+ }).join(',');
var uri = 'thefeed://' + encodeURIComponent(p.config.domain)
+ '/' + encodeURIComponent(p.config.key)
- + '?r=' + encodeURIComponent(resolvers.join(','));
+ + '?r=' + encodeURIComponent(compact).replace(/%3A/g, ':');
var nick = (p.nickname || '').trim().slice(0, 32);
if (nick && nick !== p.config.domain) {
uri += '&n=' + encodeURIComponent(nick);
diff --git a/ios/Thefeed/ContentView.swift b/ios/Thefeed/ContentView.swift
index c777442..9ab0327 100644
--- a/ios/Thefeed/ContentView.swift
+++ b/ios/Thefeed/ContentView.swift
@@ -2,12 +2,11 @@ import SwiftUI
struct ContentView: View {
@EnvironmentObject var server: ServerController
- @AppStorage("tf.lang") private var lang: String = ""
var body: some View {
ZStack {
Color.black.ignoresSafeArea()
- if !lang.isEmpty && server.port > 0 {
+ if server.port > 0 {
WebView(url: URL(string: "http://127.0.0.1:\(server.port)")!)
.ignoresSafeArea()
} else if let err = server.lastError {
diff --git a/ios/Thefeed/LanguagePicker.swift b/ios/Thefeed/LanguagePicker.swift
deleted file mode 100644
index 70b3f62..0000000
--- a/ios/Thefeed/LanguagePicker.swift
+++ /dev/null
@@ -1,68 +0,0 @@
-import SwiftUI
-
-/// First-launch language selection. Stored in UserDefaults under "tf.lang".
-struct LanguagePickerView: View {
- let onPick: (String) -> Void
-
- var body: some View {
- ZStack {
- Color(red: 0.07, green: 0.09, blue: 0.13).ignoresSafeArea()
-
- VStack(spacing: 40) {
- Spacer()
-
- VStack(spacing: 8) {
- Text("TheFeed")
- .font(.system(size: 40, weight: .bold, design: .rounded))
- .foregroundColor(.white)
- Text("Choose your language · زبان خود را انتخاب کنید")
- .font(.callout)
- .foregroundColor(.white.opacity(0.6))
- .multilineTextAlignment(.center)
- .padding(.horizontal, 24)
- }
-
- VStack(spacing: 16) {
- LanguageButton(label: "English", subtitle: "Continue in English") {
- onPick("en")
- }
- LanguageButton(label: "فارسی", subtitle: "ادامه به زبان فارسی") {
- onPick("fa")
- }
- }
- .padding(.horizontal, 32)
-
- Spacer()
- }
- }
- }
-}
-
-private struct LanguageButton: View {
- let label: String
- let subtitle: String
- let action: () -> Void
-
- var body: some View {
- Button(action: action) {
- VStack(spacing: 4) {
- Text(label)
- .font(.system(size: 22, weight: .semibold))
- .foregroundColor(.white)
- Text(subtitle)
- .font(.footnote)
- .foregroundColor(.white.opacity(0.7))
- }
- .frame(maxWidth: .infinity)
- .padding(.vertical, 18)
- .background(
- RoundedRectangle(cornerRadius: 16)
- .fill(Color.white.opacity(0.08))
- .overlay(
- RoundedRectangle(cornerRadius: 16)
- .stroke(Color.white.opacity(0.15), lineWidth: 1)
- )
- )
- }
- }
-}
diff --git a/ios/Thefeed/ThefeedApp.swift b/ios/Thefeed/ThefeedApp.swift
index a216c3b..5d5202c 100644
--- a/ios/Thefeed/ThefeedApp.swift
+++ b/ios/Thefeed/ThefeedApp.swift
@@ -3,7 +3,6 @@ import SwiftUI
@main
struct ThefeedApp: App {
@StateObject private var server = ServerController()
- @AppStorage("tf.lang") private var lang: String = ""
var body: some Scene {
WindowGroup {
@@ -11,14 +10,6 @@ struct ThefeedApp: App {
.environmentObject(server)
.onAppear { server.start() }
.onDisappear { server.stop() }
- .fullScreenCover(isPresented: Binding(
- get: { lang.isEmpty },
- set: { _ in }
- )) {
- LanguagePickerView { picked in
- lang = picked
- }
- }
}
}
}
diff --git a/ios/Thefeed/WebView.swift b/ios/Thefeed/WebView.swift
index 29f4f08..f3f0469 100644
--- a/ios/Thefeed/WebView.swift
+++ b/ios/Thefeed/WebView.swift
@@ -61,11 +61,10 @@ struct WebView: UIViewRepresentable {
"""
}
- /// Reads the language picked at first launch. Falls back to "en"
- /// for the brief window before the picker has been answered.
+ /// Returns the saved language ("" on first launch) so the WebView's
+ /// own language picker can detect first-launch and show its modal.
private func resolveLang() -> String {
- let saved = UserDefaults.standard.string(forKey: "tf.lang") ?? ""
- return saved.isEmpty ? "en" : saved
+ return UserDefaults.standard.string(forKey: "tf.lang") ?? ""
}
func updateUIView(_ view: WKWebView, context: Context) {