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