Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6cfdeb6f0 | ||
|
|
a13108b840 | ||
|
|
27b4fd7698 | ||
|
|
fb3e138ea2 | ||
|
|
a7c5d1a2aa | ||
|
|
3313738990 | ||
|
|
ce86f56d37 | ||
|
|
3858b97104 | ||
|
|
5be66dbaa7 | ||
|
|
4501ee8f3d | ||
|
|
6c96c50d99 | ||
|
|
d5a885f42e | ||
|
|
bfed056a0e | ||
|
|
4b685df489 | ||
|
|
70dfa85d22 | ||
|
|
19698658b1 | ||
|
|
5a847b64cc | ||
|
|
c6bacbb23c | ||
|
|
e89ccbbeca | ||
|
|
6a6e911ef8 | ||
|
|
553d526862 | ||
|
|
44f412fffa | ||
|
|
b5e16bfa6e | ||
|
|
235c73dda8 | ||
|
|
f22441371b | ||
|
|
ea40700fd3 | ||
|
|
e6ae9d6b1e | ||
|
|
103ca2c5e4 | ||
|
|
799d3d591f | ||
|
|
86bfbcdb6d | ||
|
|
ab5243abff | ||
|
|
ca230066f8 | ||
|
|
347fa75534 | ||
|
|
1c9e04cf86 | ||
|
|
9c814dcc60 |
@@ -227,6 +227,8 @@ See [server-infra IMPLEMENTATION.md](https://git.aimloperations.com/ai_ml_operat
|
|||||||
- Script: `https://tianji.aimloperations.com/tracker.js`
|
- Script: `https://tianji.aimloperations.com/tracker.js`
|
||||||
- **Page views** always load on prod/beta (no consent gate)
|
- **Page views** always load on prod/beta (no consent gate)
|
||||||
- **Custom events / identify** require analytics consent (`AnalyticsConsentBanner` + `trackEvent` / `identifyUser`)
|
- **Custom events / identify** require analytics consent (`AnalyticsConsentBanner` + `trackEvent` / `identifyUser`)
|
||||||
|
- Consent choice stored in `localStorage` key `hesychia_analytics_consent_v1` (`granted` / `denied`)
|
||||||
|
- Banner links to `/terms_of_service/#analytics` for the always-on vs optional split
|
||||||
- Event catalog: [`llm-fe/ANALYTICS.md`](llm-fe/ANALYTICS.md)
|
- Event catalog: [`llm-fe/ANALYTICS.md`](llm-fe/ANALYTICS.md)
|
||||||
|
|
||||||
## Related repos
|
## Related repos
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
# Beta API hosts (current). Target public product domain: hesychia.ai
|
# Beta API hosts — public product domain hesychia.ai
|
||||||
# Flip these to hesychia.ai API hosts when server-infra / DNS cut over.
|
|
||||||
REACT_APP_DEPLOY_ENV=beta
|
REACT_APP_DEPLOY_ENV=beta
|
||||||
REACT_APP_BACKEND_REST_API_BASE_URL=https://beta.chatbackend.aimloperations.com/api/
|
REACT_APP_BACKEND_REST_API_BASE_URL=https://beta.backend.hesychia.ai/api/
|
||||||
REACT_APP_BACKEND_WS_API_BASE_URL=wss://beta.chatbackend.aimloperations.com/ws/chat_again/
|
REACT_APP_BACKEND_WS_API_BASE_URL=wss://beta.backend.hesychia.ai/ws/chat_again/
|
||||||
# Tianji website id for Hesychia beta (distinct from prod so analytics stay separate).
|
# Tianji website id for Hesychia beta (distinct from prod so analytics stay separate).
|
||||||
# Create "Hesychia Beta" in Tianji UI (workspace cm7w8087y020lddswyhamadj2), then paste id here.
|
# Create "Hesychia Beta" in Tianji UI (workspace cm7w8087y020lddswyhamadj2), then paste id here.
|
||||||
REACT_APP_TIANJI_WEBSITE_ID=cms38bw671mf9n5jjw3xp1j3q
|
REACT_APP_TIANJI_WEBSITE_ID=cms38bw671mf9n5jjw3xp1j3q
|
||||||
|
|||||||
@@ -4,3 +4,9 @@
|
|||||||
# REACT_APP_BACKEND_WS_API_BASE_URL=wss://beta.chatbackend.aimloperations.com/ws/chat_again/
|
# REACT_APP_BACKEND_WS_API_BASE_URL=wss://beta.chatbackend.aimloperations.com/ws/chat_again/
|
||||||
REACT_APP_BACKEND_REST_API_BASE_URL=https://chatbackend.aimloperations.com/api/
|
REACT_APP_BACKEND_REST_API_BASE_URL=https://chatbackend.aimloperations.com/api/
|
||||||
REACT_APP_BACKEND_WS_API_BASE_URL=wss://chatbackend.aimloperations.com/ws/chat_again/
|
REACT_APP_BACKEND_WS_API_BASE_URL=wss://chatbackend.aimloperations.com/ws/chat_again/
|
||||||
|
|
||||||
|
# RevenueCat public SDK keys (native IAP). Leave empty until store apps are wired.
|
||||||
|
REACT_APP_REVENUECAT_APPLE_API_KEY=
|
||||||
|
REACT_APP_REVENUECAT_GOOGLE_API_KEY=
|
||||||
|
# Optional: pin a specific offering identifier (defaults to current offering).
|
||||||
|
# REACT_APP_REVENUECAT_OFFERING_ID=
|
||||||
|
|||||||
@@ -23,6 +23,18 @@
|
|||||||
*.keystore
|
*.keystore
|
||||||
*.jks
|
*.jks
|
||||||
|
|
||||||
|
# Capacitor / iOS CocoaPods + build artifacts (keep ios/ source; ignore Pods / DerivedData)
|
||||||
|
/ios/App/Pods/
|
||||||
|
/ios/App/build/
|
||||||
|
/ios/App/output/
|
||||||
|
/ios/App/App/public/
|
||||||
|
/ios/DerivedData/
|
||||||
|
/ios/**/xcuserdata/
|
||||||
|
*.mobileprovision
|
||||||
|
*.p12
|
||||||
|
*.cer
|
||||||
|
ExportOptions.plist
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.env.local
|
.env.local
|
||||||
|
|||||||
@@ -20,7 +20,18 @@ Page views: always via `Tracker` / `tracker.js` on prod + beta.
|
|||||||
| Conversation Created | `CONVERSATION_CREATED` | MessageContext | `{ conversationId }` |
|
| Conversation Created | `CONVERSATION_CREATED` | MessageContext | `{ conversationId }` |
|
||||||
| Message Sent | `MESSAGE_SENT` | AsyncDashboard2 | `{ hasConversation, hasAttachment }` |
|
| Message Sent | `MESSAGE_SENT` | AsyncDashboard2 | `{ hasConversation, hasAttachment }` |
|
||||||
| ToS Acknowledged | `TOS_ACKNOWLEDGED` | TermsOfService | — |
|
| ToS Acknowledged | `TOS_ACKNOWLEDGED` | TermsOfService | — |
|
||||||
| Billing Portal Opened | `BILLING_PORTAL_OPENED` | *(pending #33 Account portal CTA)* | — |
|
| Billing Portal Opened | `BILLING_PORTAL_OPENED` | BillingSection (Account) | `{ intent? }` |
|
||||||
|
| Subscription Upgrade Started | `SUBSCRIPTION_UPGRADE_STARTED` | BillingSection, UsageSummaryCard | `{ source, plan_slug? }` |
|
||||||
|
| Plan Change Started | `PLAN_CHANGE_STARTED` | BillingSection | `{ source, plan_slug? }` |
|
||||||
|
| Subscription Cancel Started | `SUBSCRIPTION_CANCEL_STARTED` | BillingSection | `{ source: 'portal' }` |
|
||||||
|
| Account Delete Started | `ACCOUNT_DELETE_STARTED` | DeleteAccountSection | — |
|
||||||
|
| Account Delete Success | `ACCOUNT_DELETE_SUCCESS` | DeleteAccountSection | — |
|
||||||
|
| Account Delete Failed | `ACCOUNT_DELETE_FAILED` | DeleteAccountSection | — |
|
||||||
|
| Message Copied | `MESSAGE_COPIED` | MessageActions | `{ role }` |
|
||||||
|
| Message Rated | `MESSAGE_RATED` | MessageActions | `{ rating: 'up' \| 'down' \| 'cleared' }` |
|
||||||
|
| Message Rating Reason | `MESSAGE_RATING_REASON` | MessageActions | `{ reason, hasComment }` |
|
||||||
|
| Message Exported | `MESSAGE_EXPORTED` | MessageActions, AsyncDashboard2 | `{ format, scope }` |
|
||||||
|
| Activity Stage Completed | `ACTIVITY_STAGE_COMPLETED` | MessageContext | `{ stage, durationMs }` (no label/detail text) |
|
||||||
|
|
||||||
## Identify
|
## Identify
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
# Android (Capacitor)
|
# Android (Capacitor)
|
||||||
|
|
||||||
Ship the CRA web build as an Android app via [Capacitor](https://capacitorjs.com/).
|
Ship the CRA web build as an Android app via [Capacitor](https://capacitorjs.com/).
|
||||||
One codebase: `llm-fe/` → web + Android WebView.
|
One codebase: `llm-fe/` → web + Android + iOS WebView.
|
||||||
|
|
||||||
**Build & Play deploy steps:** [`android/README.md`](android/README.md).
|
**Build & Play deploy steps:** [`android/README.md`](android/README.md).
|
||||||
|
**iOS sibling:** [`IOS.md`](IOS.md) / [`ios/README.md`](ios/README.md).
|
||||||
|
|
||||||
Prerequisites for blockers already merged: JWT-only auth (#22), WebSocket lifecycle (#23), HashRouter (#24).
|
Prerequisites for blockers already merged: JWT-only auth (#22), WebSocket lifecycle (#23), HashRouter (#24).
|
||||||
|
|
||||||
@@ -39,6 +40,20 @@ npm run android:sync
|
|||||||
|
|
||||||
Default `.env.mobile` matches production (`chatbackend.aimloperations.com`). Point it at beta to flip the shell without touching web deploys. Optional gitignored override: `.env.mobile.local`.
|
Default `.env.mobile` matches production (`chatbackend.aimloperations.com`). Point it at beta to flip the shell without touching web deploys. Optional gitignored override: `.env.mobile.local`.
|
||||||
|
|
||||||
|
### RevenueCat IAP (#100)
|
||||||
|
|
||||||
|
Native billing uses `@revenuecat/purchases-capacitor` (Capacitor 7 → package **11.x**). Web keeps Stripe Checkout / Customer Portal.
|
||||||
|
|
||||||
|
Set public SDK keys in `.env.mobile` (empty placeholders OK until store apps ship):
|
||||||
|
|
||||||
|
| Variable | Purpose |
|
||||||
|
|----------|---------|
|
||||||
|
| `REACT_APP_REVENUECAT_APPLE_API_KEY` | iOS public SDK key |
|
||||||
|
| `REACT_APP_REVENUECAT_GOOGLE_API_KEY` | Android public SDK key |
|
||||||
|
| `REACT_APP_REVENUECAT_OFFERING_ID` | Optional offering pin (else current) |
|
||||||
|
|
||||||
|
After install / key changes: `npm run build:mobile` (runs `cap sync`). More detail: [`MONETIZATION.md`](MONETIZATION.md).
|
||||||
|
|
||||||
## Versioning
|
## Versioning
|
||||||
|
|
||||||
In `android/app/build.gradle`:
|
In `android/app/build.gradle`:
|
||||||
@@ -50,12 +65,21 @@ Bump both before each store release.
|
|||||||
|
|
||||||
## Icons / splash
|
## Icons / splash
|
||||||
|
|
||||||
Source art lives in `llm-fe/assets/` (`icon.png`, `splash.png`, `splash-dark.png`). Regenerate Android densities:
|
Source art lives in `llm-fe/assets/`:
|
||||||
|
- `icon.png` / `splash.png` / `splash-dark.png` — Capacitor inputs (Hesychia meander mark)
|
||||||
|
- `logo-source.png` — cropped square master
|
||||||
|
- `logo-mark-transparent.png` — circular mark with transparent corners (web / in-app)
|
||||||
|
|
||||||
|
In-app UI also imports `src/llm-fe/assets/brand/hesychia-mark.png`.
|
||||||
|
|
||||||
|
Regenerate Android densities after changing source art:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run assets:generate
|
npm run assets:generate
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Same command also regenerates iOS `Assets.xcassets` (see [`IOS.md`](IOS.md)).
|
||||||
|
|
||||||
## Signing (upload keystore)
|
## Signing (upload keystore)
|
||||||
|
|
||||||
**Never commit keystores.** Store outside the repo (e.g. host secrets / password manager).
|
**Never commit keystores.** Store outside the repo (e.g. host secrets / password manager).
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
# iOS (Capacitor)
|
||||||
|
|
||||||
|
Ship the CRA web build as an iOS app via [Capacitor](https://capacitorjs.com/)
|
||||||
|
WKWebView. One codebase: `llm-fe/` → web + Android + iOS.
|
||||||
|
|
||||||
|
**Build & Xcode steps:** [`ios/README.md`](ios/README.md).
|
||||||
|
|
||||||
|
Prerequisites already merged: JWT-only auth (#22), WebSocket lifecycle (#23),
|
||||||
|
HashRouter (#24), Android Capacitor scaffolding (#20).
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- **macOS** with **Xcode** 15+ (CI runners are Linux — iOS builds are **manual**
|
||||||
|
on a Mac until a hosted macOS runner is added)
|
||||||
|
- **Node.js 20** (matches CI; Capacitor **7.x**)
|
||||||
|
- **CocoaPods** (`sudo gem install cocoapods` or Homebrew `pod`)
|
||||||
|
- Apple Developer Program membership for device / TestFlight / App Store signing
|
||||||
|
(simulator debug runs without a paid membership using automatic signing)
|
||||||
|
|
||||||
|
## One-time / day-to-day
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd llm-fe
|
||||||
|
npm ci
|
||||||
|
npm run build:mobile # .env.mobile → CRA build → cap sync (android + ios)
|
||||||
|
npm run ios:open # opens ios/App/App.xcworkspace in Xcode
|
||||||
|
```
|
||||||
|
|
||||||
|
Or sync only after an existing `build/`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run ios:sync
|
||||||
|
```
|
||||||
|
|
||||||
|
**Always open the `.xcworkspace`**, not the `.xcodeproj` (CocoaPods).
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
Same as Android — see [`ANDROID.md`](ANDROID.md) § Environment.
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `.env.mobile` | Endpoints baked into the **mobile** bundle (`build:mobile`) |
|
||||||
|
| `.env.mobile.local` | Optional gitignored override |
|
||||||
|
|
||||||
|
Default `.env.mobile` points at production (`chatbackend.aimloperations.com`).
|
||||||
|
|
||||||
|
WebView origin is `capacitor://localhost`. Backend CORS/CSRF already trusts it
|
||||||
|
(`chat_backend` `CAPACITOR_WEBVIEW_ORIGINS`). Auth is header JWT — no cookies.
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
In Xcode target **App** → General, or `ios/App/App.xcodeproj/project.pbxproj`:
|
||||||
|
|
||||||
|
| Field | Xcode / build setting | Align with |
|
||||||
|
|-------|----------------------|------------|
|
||||||
|
| Version | `MARKETING_VERSION` (`CFBundleShortVersionString`) | `package.json` `version` + Android `versionName` (currently `0.1.0`) |
|
||||||
|
| Build | `CURRENT_PROJECT_VERSION` (`CFBundleVersion`) | Android `versionCode` (integer; **must increase** every TestFlight / App Store upload) |
|
||||||
|
|
||||||
|
## Icons / splash
|
||||||
|
|
||||||
|
Source art in `llm-fe/assets/` (same as Android):
|
||||||
|
|
||||||
|
- `icon.png` / `splash.png` / `splash-dark.png`
|
||||||
|
|
||||||
|
Regenerate iOS + Android densities:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run assets:generate
|
||||||
|
```
|
||||||
|
|
||||||
|
## Signing
|
||||||
|
|
||||||
|
**Never commit certificates, profiles, or `.p12` files.**
|
||||||
|
|
||||||
|
1. Xcode → Signing & Capabilities → Team (Automatic for debug / internal).
|
||||||
|
2. Bundle ID: **`ai.hesychia.chat`** (matches Android `applicationId` / Capacitor `appId`).
|
||||||
|
3. For distribution: App Store Connect app, distribution cert, provisioning profile
|
||||||
|
— keep credentials outside the repo (password manager / CI secrets later).
|
||||||
|
|
||||||
|
## Manual QA checklist
|
||||||
|
|
||||||
|
On **simulator and physical iPhone**:
|
||||||
|
|
||||||
|
- [ ] Login / logout / password reset
|
||||||
|
- [ ] Chat streaming + markdown / code blocks (selection + copy)
|
||||||
|
- [ ] Charts (`recharts`)
|
||||||
|
- [ ] Dark / light theme
|
||||||
|
- [ ] Keyboard does not cover compose input; notch / Dynamic Island / home indicator OK
|
||||||
|
- [ ] No rubber-band overscroll on chat shell
|
||||||
|
- [ ] Background → resume keeps or reconnects WebSocket (`appStateChange` + visibility)
|
||||||
|
- [ ] Cellular ↔ Wi‑Fi switch reconnects socket
|
||||||
|
- [ ] Airplane mode → restore reconnects
|
||||||
|
|
||||||
|
## TestFlight / App Store (follow-on)
|
||||||
|
|
||||||
|
Out of band (not automated here; separate deploy tickets):
|
||||||
|
|
||||||
|
1. Apple Developer Program + App ID `ai.hesychia.chat`
|
||||||
|
2. Privacy policy URL + App Privacy questionnaire
|
||||||
|
3. In-app account deletion if sign-up is offered (guideline)
|
||||||
|
4. Enough native shell value that review does not reject as 4.2 “repackaged website”
|
||||||
|
5. Archive → Upload → TestFlight internal → App Store
|
||||||
|
|
||||||
|
Push notifications, offline cache, Sign in with Apple — out of scope for this wrap.
|
||||||
|
|
||||||
|
## Related issues
|
||||||
|
|
||||||
|
- [#21](https://git.aimloperations.com/ai_ml_operations/chat_web_app/issues/21) — this iOS wrap
|
||||||
|
- [#20](https://git.aimloperations.com/ai_ml_operations/chat_web_app/issues/20) — Android scaffolding
|
||||||
|
- [#22](https://git.aimloperations.com/ai_ml_operations/chat_web_app/issues/22) / [#23](https://git.aimloperations.com/ai_ml_operations/chat_web_app/issues/23) / [#24](https://git.aimloperations.com/ai_ml_operations/chat_web_app/issues/24) — auth / WS / routing
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Monetization (web Stripe + native RevenueCat)
|
||||||
|
|
||||||
|
## Channels
|
||||||
|
|
||||||
|
| Runtime | Checkout / manage | Ledger |
|
||||||
|
|---------|-------------------|--------|
|
||||||
|
| Web (`!isNativePlatform`) | Stripe Checkout + Customer Portal via `/monetization/...` | Stripe webhooks |
|
||||||
|
| Native Capacitor | RevenueCat IAP (`@revenuecat/purchases-capacitor`) | RevenueCat webhooks → same invoice/payment tables |
|
||||||
|
|
||||||
|
FE displays whatever `/monetization/invoices/` (and subscription) returns after refresh. Provider badge uses `provider` + `revenuecat_store`.
|
||||||
|
|
||||||
|
## App user id
|
||||||
|
|
||||||
|
RevenueCat `appUserID` = Django user pk as string.
|
||||||
|
|
||||||
|
1. Prefer `id` from `/user/get/` (serializer returns all model fields).
|
||||||
|
2. Else JWT `user_id` claim (`SIMPLE_JWT.USER_ID_CLAIM`).
|
||||||
|
|
||||||
|
Backend webhook resolver accepts numeric pk (or email fallback).
|
||||||
|
|
||||||
|
## Mobile env keys
|
||||||
|
|
||||||
|
In `.env.mobile`:
|
||||||
|
|
||||||
|
- `REACT_APP_REVENUECAT_APPLE_API_KEY`
|
||||||
|
- `REACT_APP_REVENUECAT_GOOGLE_API_KEY`
|
||||||
|
- `REACT_APP_REVENUECAT_OFFERING_ID` (optional)
|
||||||
|
|
||||||
|
Package identifiers in the RC dashboard should include plan slugs (e.g. `founders`) so `purchasePlan(planSlug)` can match packages/products. Optional backend `REVENUECAT_PRODUCT_PLAN_MAP` maps product id → plan slug.
|
||||||
|
|
||||||
|
## Auth hooks
|
||||||
|
|
||||||
|
- `Purchases.configure` once on native (first purchase / logIn).
|
||||||
|
- `logIn(appUserID)` after SignIn / SignUp / AuthCallback / AccountContext load.
|
||||||
|
- `logOut` on Header2 sign-out and Delete account.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- Issue #100 (FE) + companion backend RevenueCat webhook PR
|
||||||
|
- [`ANDROID.md`](ANDROID.md) / [`IOS.md`](IOS.md) for store builds
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Hesychia — Android
|
# Hesychia — Android
|
||||||
|
|
||||||
Native Capacitor project for `com.aimloperations.chat`. Web UI comes from the CRA
|
Native Capacitor project for `ai.hesychia.chat`. Web UI comes from the CRA
|
||||||
`build/` output in the parent `llm-fe/` package (synced with `npx cap sync`).
|
`build/` output in the parent `llm-fe/` package (synced with `npx cap sync`).
|
||||||
|
|
||||||
Broader Capacitor notes (env files, icons, QA): [`../ANDROID.md`](../ANDROID.md).
|
Broader Capacitor notes (env files, icons, QA): [`../ANDROID.md`](../ANDROID.md).
|
||||||
@@ -105,7 +105,7 @@ Without `keystore.properties`, release builds are unsigned; debug builds do not
|
|||||||
|
|
||||||
## Deploy to Google Play
|
## Deploy to Google Play
|
||||||
|
|
||||||
1. [Play Console](https://play.google.com/console) → app **Hesychia** (`com.aimloperations.chat`).
|
1. [Play Console](https://play.google.com/console) → app **Hesychia** (`ai.hesychia.chat`).
|
||||||
2. Complete store listing: privacy policy URL, data-safety form, screenshots.
|
2. Complete store listing: privacy policy URL, data-safety form, screenshots.
|
||||||
3. **Testing → Internal testing** → create release → upload `app-release.aab`.
|
3. **Testing → Internal testing** → create release → upload `app-release.aab`.
|
||||||
4. Add testers, roll out internal track, verify on devices.
|
4. Add testers, roll out internal track, verify on devices.
|
||||||
@@ -126,6 +126,6 @@ Each new upload needs a higher `versionCode`.
|
|||||||
|
|
||||||
| | |
|
| | |
|
||||||
|--|--|
|
|--|--|
|
||||||
| Application ID | `com.aimloperations.chat` |
|
| Application ID | `ai.hesychia.chat` |
|
||||||
| Display name | Hesychia |
|
| Display name | Hesychia |
|
||||||
| Min / target SDK | 23 / 35 (see `variables.gradle`) |
|
| Min / target SDK | 23 / 35 (see `variables.gradle`) |
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ if (keystorePropertiesFile.exists()) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace "com.aimloperations.chat"
|
namespace "ai.hesychia.chat"
|
||||||
compileSdk rootProject.ext.compileSdkVersion
|
compileSdk rootProject.ext.compileSdkVersion
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.aimloperations.chat"
|
applicationId "ai.hesychia.chat"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 1
|
versionCode 1
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.aimloperations.chat;
|
package ai.hesychia.chat;
|
||||||
|
|
||||||
import com.getcapacitor.BridgeActivity;
|
import com.getcapacitor.BridgeActivity;
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 428 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 644 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 896 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 428 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 644 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 896 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 179 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 179 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 413 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 623 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 875 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 413 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 623 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 875 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 837 B After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 277 B After Width: | Height: | Size: 276 B |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 202 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 47 KiB |
@@ -2,6 +2,6 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Hesychia</string>
|
<string name="app_name">Hesychia</string>
|
||||||
<string name="title_activity_main">Hesychia</string>
|
<string name="title_activity_main">Hesychia</string>
|
||||||
<string name="package_name">com.aimloperations.chat</string>
|
<string name="package_name">ai.hesychia.chat</string>
|
||||||
<string name="custom_url_scheme">com.aimloperations.chat</string>
|
<string name="custom_url_scheme">ai.hesychia.chat</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 723 KiB |
|
After Width: | Height: | Size: 823 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 1.1 MiB |
@@ -1,16 +1,25 @@
|
|||||||
import type { CapacitorConfig } from '@capacitor/cli';
|
import type { CapacitorConfig } from '@capacitor/cli';
|
||||||
|
|
||||||
const config: CapacitorConfig = {
|
const config: CapacitorConfig = {
|
||||||
appId: 'com.aimloperations.chat',
|
appId: 'ai.hesychia.chat',
|
||||||
appName: 'Hesychia',
|
appName: 'Hesychia',
|
||||||
webDir: 'build',
|
webDir: 'build',
|
||||||
server: {
|
server: {
|
||||||
// Bundled assets; HashRouter (#24) handles deep links without a rewrite server.
|
// Bundled assets; HashRouter (#24) handles deep links without a rewrite server.
|
||||||
|
// Android: https://localhost — iOS: capacitor://localhost (CORS trusted in chat_backend).
|
||||||
androidScheme: 'https',
|
androidScheme: 'https',
|
||||||
|
iosScheme: 'capacitor',
|
||||||
},
|
},
|
||||||
android: {
|
android: {
|
||||||
allowMixedContent: false,
|
allowMixedContent: false,
|
||||||
},
|
},
|
||||||
|
ios: {
|
||||||
|
// Prefer content-driven insets; CSS env(safe-area-inset-*) handles notch / home indicator.
|
||||||
|
contentInset: 'automatic',
|
||||||
|
// Keep WebView scroll on; rubber-band bounce disabled in AppDelegate after load.
|
||||||
|
scrollEnabled: true,
|
||||||
|
preferredContentMode: 'mobile',
|
||||||
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
SplashScreen: {
|
SplashScreen: {
|
||||||
launchAutoHide: true,
|
launchAutoHide: true,
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
App/build
|
||||||
|
App/Pods
|
||||||
|
App/output
|
||||||
|
App/App/public
|
||||||
|
DerivedData
|
||||||
|
xcuserdata
|
||||||
|
|
||||||
|
# Cordova plugins for Capacitor
|
||||||
|
capacitor-cordova-ios-plugins
|
||||||
|
|
||||||
|
# Generated Config files
|
||||||
|
App/App/capacitor.config.json
|
||||||
|
App/App/config.xml
|
||||||
|
|
||||||
|
# Signing / export artifacts — never commit
|
||||||
|
*.mobileprovision
|
||||||
|
*.p12
|
||||||
|
*.cer
|
||||||
|
ExportOptions.plist
|
||||||
|
*.ipa
|
||||||
@@ -0,0 +1,408 @@
|
|||||||
|
// !$*UTF8*$!
|
||||||
|
{
|
||||||
|
archiveVersion = 1;
|
||||||
|
classes = {
|
||||||
|
};
|
||||||
|
objectVersion = 48;
|
||||||
|
objects = {
|
||||||
|
|
||||||
|
/* Begin PBXBuildFile section */
|
||||||
|
2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; };
|
||||||
|
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
|
||||||
|
504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; };
|
||||||
|
504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; };
|
||||||
|
504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; };
|
||||||
|
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
|
||||||
|
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
|
||||||
|
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */; };
|
||||||
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXFileReference section */
|
||||||
|
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };
|
||||||
|
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
|
||||||
|
504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
504EC30C1FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||||
|
504EC30E1FED79650016851F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
|
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
|
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
|
||||||
|
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
504EC3011FED79650016851F /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXGroup section */
|
||||||
|
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */,
|
||||||
|
);
|
||||||
|
name = Frameworks;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
504EC2FB1FED79650016851F = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
504EC3061FED79650016851F /* App */,
|
||||||
|
504EC3051FED79650016851F /* Products */,
|
||||||
|
7F8756D8B27F46E3366F6CEA /* Pods */,
|
||||||
|
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,
|
||||||
|
);
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
504EC3051FED79650016851F /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
504EC3041FED79650016851F /* App.app */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
504EC3061FED79650016851F /* App */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
50379B222058CBB4000EE86E /* capacitor.config.json */,
|
||||||
|
504EC3071FED79650016851F /* AppDelegate.swift */,
|
||||||
|
504EC30B1FED79650016851F /* Main.storyboard */,
|
||||||
|
504EC30E1FED79650016851F /* Assets.xcassets */,
|
||||||
|
504EC3101FED79650016851F /* LaunchScreen.storyboard */,
|
||||||
|
504EC3131FED79650016851F /* Info.plist */,
|
||||||
|
2FAD9762203C412B000D30F8 /* config.xml */,
|
||||||
|
50B271D01FEDC1A000F3C39B /* public */,
|
||||||
|
);
|
||||||
|
path = App;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
7F8756D8B27F46E3366F6CEA /* Pods */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */,
|
||||||
|
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */,
|
||||||
|
);
|
||||||
|
name = Pods;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXGroup section */
|
||||||
|
|
||||||
|
/* Begin PBXNativeTarget section */
|
||||||
|
504EC3031FED79650016851F /* App */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */;
|
||||||
|
buildPhases = (
|
||||||
|
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */,
|
||||||
|
504EC3001FED79650016851F /* Sources */,
|
||||||
|
504EC3011FED79650016851F /* Frameworks */,
|
||||||
|
504EC3021FED79650016851F /* Resources */,
|
||||||
|
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = App;
|
||||||
|
productName = App;
|
||||||
|
productReference = 504EC3041FED79650016851F /* App.app */;
|
||||||
|
productType = "com.apple.product-type.application";
|
||||||
|
};
|
||||||
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
|
/* Begin PBXProject section */
|
||||||
|
504EC2FC1FED79650016851F /* Project object */ = {
|
||||||
|
isa = PBXProject;
|
||||||
|
attributes = {
|
||||||
|
LastSwiftUpdateCheck = 920;
|
||||||
|
LastUpgradeCheck = 920;
|
||||||
|
TargetAttributes = {
|
||||||
|
504EC3031FED79650016851F = {
|
||||||
|
CreatedOnToolsVersion = 9.2;
|
||||||
|
LastSwiftMigration = 1100;
|
||||||
|
ProvisioningStyle = Automatic;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
buildConfigurationList = 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */;
|
||||||
|
compatibilityVersion = "Xcode 8.0";
|
||||||
|
developmentRegion = en;
|
||||||
|
hasScannedForEncodings = 0;
|
||||||
|
knownRegions = (
|
||||||
|
en,
|
||||||
|
Base,
|
||||||
|
);
|
||||||
|
mainGroup = 504EC2FB1FED79650016851F;
|
||||||
|
packageReferences = (
|
||||||
|
);
|
||||||
|
productRefGroup = 504EC3051FED79650016851F /* Products */;
|
||||||
|
projectDirPath = "";
|
||||||
|
projectRoot = "";
|
||||||
|
targets = (
|
||||||
|
504EC3031FED79650016851F /* App */,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
/* End PBXProject section */
|
||||||
|
|
||||||
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
504EC3021FED79650016851F /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */,
|
||||||
|
50B271D11FEDC1A000F3C39B /* public in Resources */,
|
||||||
|
504EC30F1FED79650016851F /* Assets.xcassets in Resources */,
|
||||||
|
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */,
|
||||||
|
504EC30D1FED79650016851F /* Main.storyboard in Resources */,
|
||||||
|
2FAD9763203C412B000D30F8 /* config.xml in Resources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
|
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||||
|
"${PODS_ROOT}/Manifest.lock",
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
|
outputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/Pods-App-checkManifestLockResult.txt",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
name = "[CP] Embed Pods Frameworks";
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App/Pods-App-frameworks.sh\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
504EC3001FED79650016851F /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXVariantGroup section */
|
||||||
|
504EC30B1FED79650016851F /* Main.storyboard */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
504EC30C1FED79650016851F /* Base */,
|
||||||
|
);
|
||||||
|
name = Main.storyboard;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
504EC3101FED79650016851F /* LaunchScreen.storyboard */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
504EC3111FED79650016851F /* Base */,
|
||||||
|
);
|
||||||
|
name = LaunchScreen.storyboard;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXVariantGroup section */
|
||||||
|
|
||||||
|
/* Begin XCBuildConfiguration section */
|
||||||
|
504EC3141FED79650016851F /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_TESTABILITY = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
|
GCC_DYNAMIC_NO_PIC = NO;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_OPTIMIZATION_LEVEL = 0;
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
|
"DEBUG=1",
|
||||||
|
"$(inherited)",
|
||||||
|
);
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
504EC3151FED79650016851F /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||||
|
VALIDATE_PRODUCT = YES;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
504EC3171FED79650016851F /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
|
MARKETING_VERSION = 0.1.0;
|
||||||
|
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = ai.hesychia.chat;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
504EC3181FED79650016851F /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
|
MARKETING_VERSION = 0.1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = ai.hesychia.chat;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
|
/* Begin XCConfigurationList section */
|
||||||
|
504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
504EC3141FED79650016851F /* Debug */,
|
||||||
|
504EC3151FED79650016851F /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
504EC3171FED79650016851F /* Debug */,
|
||||||
|
504EC3181FED79650016851F /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
/* End XCConfigurationList section */
|
||||||
|
};
|
||||||
|
rootObject = 504EC2FC1FED79650016851F /* Project object */;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "group:App.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
<FileRef
|
||||||
|
location = "group:Pods/Pods.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?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>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import UIKit
|
||||||
|
import Capacitor
|
||||||
|
|
||||||
|
@UIApplicationMain
|
||||||
|
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
|
|
||||||
|
var window: UIWindow?
|
||||||
|
|
||||||
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||||
|
// Disable WKWebView rubber-band overscroll so the MUI chat layout feels native.
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(disableWebViewBounce),
|
||||||
|
name: UIApplication.didBecomeActiveNotification,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func disableWebViewBounce() {
|
||||||
|
guard let root = window?.rootViewController else { return }
|
||||||
|
applyNoBounce(to: root)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyNoBounce(to controller: UIViewController) {
|
||||||
|
if let bridge = controller as? CAPBridgeViewController,
|
||||||
|
let scrollView = bridge.webView?.scrollView {
|
||||||
|
scrollView.bounces = false
|
||||||
|
scrollView.alwaysBounceVertical = false
|
||||||
|
scrollView.alwaysBounceHorizontal = false
|
||||||
|
}
|
||||||
|
for child in controller.children {
|
||||||
|
applyNoBounce(to: child)
|
||||||
|
}
|
||||||
|
if let presented = controller.presentedViewController {
|
||||||
|
applyNoBounce(to: presented)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func applicationWillResignActive(_ application: UIApplication) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func applicationDidEnterBackground(_ application: UIApplication) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func applicationWillEnterForeground(_ application: UIApplication) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||||
|
disableWebViewBounce()
|
||||||
|
}
|
||||||
|
|
||||||
|
func applicationWillTerminate(_ application: UIApplication) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
||||||
|
return ApplicationDelegateProxy.shared.application(app, open: url, options: options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
|
||||||
|
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 874 KiB |
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"idiom": "universal",
|
||||||
|
"size": "1024x1024",
|
||||||
|
"filename": "AppIcon-512@2x.png",
|
||||||
|
"platform": "ios"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info": {
|
||||||
|
"author": "xcode",
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"idiom": "universal",
|
||||||
|
"filename": "Default@1x~universal~anyany.png",
|
||||||
|
"scale": "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom": "universal",
|
||||||
|
"filename": "Default@2x~universal~anyany.png",
|
||||||
|
"scale": "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom": "universal",
|
||||||
|
"filename": "Default@3x~universal~anyany.png",
|
||||||
|
"scale": "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances": [
|
||||||
|
{
|
||||||
|
"appearance": "luminosity",
|
||||||
|
"value": "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom": "universal",
|
||||||
|
"scale": "1x",
|
||||||
|
"filename": "Default@1x~universal~anyany-dark.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances": [
|
||||||
|
{
|
||||||
|
"appearance": "luminosity",
|
||||||
|
"value": "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom": "universal",
|
||||||
|
"scale": "2x",
|
||||||
|
"filename": "Default@2x~universal~anyany-dark.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances": [
|
||||||
|
{
|
||||||
|
"appearance": "luminosity",
|
||||||
|
"value": "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom": "universal",
|
||||||
|
"scale": "3x",
|
||||||
|
"filename": "Default@3x~universal~anyany-dark.png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info": {
|
||||||
|
"version": 1,
|
||||||
|
"author": "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
@@ -0,0 +1,32 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17132" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||||
|
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17105"/>
|
||||||
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--View Controller-->
|
||||||
|
<scene sceneID="EHf-IW-A2E">
|
||||||
|
<objects>
|
||||||
|
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||||
|
<imageView key="view" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Splash" id="snD-IY-ifK">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||||
|
</imageView>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="53" y="375"/>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
<resources>
|
||||||
|
<image name="Splash" width="1366" height="1366"/>
|
||||||
|
<systemColor name="systemBackgroundColor">
|
||||||
|
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
</systemColor>
|
||||||
|
</resources>
|
||||||
|
</document>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14111" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||||
|
<device id="retina4_7" orientation="portrait">
|
||||||
|
<adaptation id="fullscreen"/>
|
||||||
|
</device>
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14088"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--Bridge View Controller-->
|
||||||
|
<scene sceneID="tne-QT-ifu">
|
||||||
|
<objects>
|
||||||
|
<viewController id="BYZ-38-t0r" customClass="CAPBridgeViewController" customModule="Capacitor" sceneMemberID="viewController"/>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
</document>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?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>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>en</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Hesychia</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>$(MARKETING_VERSION)</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
<true/>
|
||||||
|
<key>UILaunchStoryboardName</key>
|
||||||
|
<string>LaunchScreen</string>
|
||||||
|
<key>UIMainStoryboardFile</key>
|
||||||
|
<string>Main</string>
|
||||||
|
<key>UIRequiredDeviceCapabilities</key>
|
||||||
|
<array>
|
||||||
|
<string>armv7</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers'
|
||||||
|
|
||||||
|
platform :ios, '14.0'
|
||||||
|
use_frameworks!
|
||||||
|
|
||||||
|
# workaround to avoid Xcode caching of Pods that requires
|
||||||
|
# Product -> Clean Build Folder after new Cordova plugins installed
|
||||||
|
# Requires CocoaPods 1.6 or newer
|
||||||
|
install! 'cocoapods', :disable_input_output_paths => true
|
||||||
|
|
||||||
|
def capacitor_pods
|
||||||
|
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
||||||
|
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
||||||
|
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
|
||||||
|
pod 'CapacitorKeyboard', :path => '../../node_modules/@capacitor/keyboard'
|
||||||
|
pod 'CapacitorPreferences', :path => '../../node_modules/@capacitor/preferences'
|
||||||
|
pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
|
||||||
|
end
|
||||||
|
|
||||||
|
target 'App' do
|
||||||
|
capacitor_pods
|
||||||
|
# Add your Pods here
|
||||||
|
end
|
||||||
|
|
||||||
|
post_install do |installer|
|
||||||
|
assertDeploymentTarget(installer)
|
||||||
|
end
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
PODS:
|
||||||
|
- Capacitor (7.6.8):
|
||||||
|
- CapacitorCordova
|
||||||
|
- CapacitorApp (7.1.2):
|
||||||
|
- Capacitor
|
||||||
|
- CapacitorCordova (7.6.8)
|
||||||
|
- CapacitorKeyboard (7.0.6):
|
||||||
|
- Capacitor
|
||||||
|
- CapacitorPreferences (7.0.4):
|
||||||
|
- Capacitor
|
||||||
|
- CapacitorStatusBar (7.0.6):
|
||||||
|
- Capacitor
|
||||||
|
|
||||||
|
DEPENDENCIES:
|
||||||
|
- "Capacitor (from `../../node_modules/@capacitor/ios`)"
|
||||||
|
- "CapacitorApp (from `../../node_modules/@capacitor/app`)"
|
||||||
|
- "CapacitorCordova (from `../../node_modules/@capacitor/ios`)"
|
||||||
|
- "CapacitorKeyboard (from `../../node_modules/@capacitor/keyboard`)"
|
||||||
|
- "CapacitorPreferences (from `../../node_modules/@capacitor/preferences`)"
|
||||||
|
- "CapacitorStatusBar (from `../../node_modules/@capacitor/status-bar`)"
|
||||||
|
|
||||||
|
EXTERNAL SOURCES:
|
||||||
|
Capacitor:
|
||||||
|
:path: "../../node_modules/@capacitor/ios"
|
||||||
|
CapacitorApp:
|
||||||
|
:path: "../../node_modules/@capacitor/app"
|
||||||
|
CapacitorCordova:
|
||||||
|
:path: "../../node_modules/@capacitor/ios"
|
||||||
|
CapacitorKeyboard:
|
||||||
|
:path: "../../node_modules/@capacitor/keyboard"
|
||||||
|
CapacitorPreferences:
|
||||||
|
:path: "../../node_modules/@capacitor/preferences"
|
||||||
|
CapacitorStatusBar:
|
||||||
|
:path: "../../node_modules/@capacitor/status-bar"
|
||||||
|
|
||||||
|
SPEC CHECKSUMS:
|
||||||
|
Capacitor: ff6bf01336ac353098378828aff465095a89e459
|
||||||
|
CapacitorApp: f01a913211780e0718dae9750442c3e23f96e106
|
||||||
|
CapacitorCordova: e61ee8c40101b8cd011d0224261606a290c082cf
|
||||||
|
CapacitorKeyboard: a2e0869edd229490ce36aed2549c0d6b95e27ee8
|
||||||
|
CapacitorPreferences: 69d9991307507aeab8ef8019c10b9babfda0e9ca
|
||||||
|
CapacitorStatusBar: 416e9e53fd6397e668d4a181cd2131617d949bd6
|
||||||
|
|
||||||
|
PODFILE CHECKSUM: c2e7da979a3cb960e70090cd1956727cb3ca3413
|
||||||
|
|
||||||
|
COCOAPODS: 1.16.2
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# Hesychia — iOS
|
||||||
|
|
||||||
|
Native Capacitor project for `ai.hesychia.chat`. Web UI comes from the CRA
|
||||||
|
`build/` output in the parent `llm-fe/` package (synced with `npx cap sync`).
|
||||||
|
|
||||||
|
Broader Capacitor notes (env files, icons, QA, TestFlight): [`../IOS.md`](../IOS.md).
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- macOS + **Xcode** 15+
|
||||||
|
- Node.js **20**
|
||||||
|
- **CocoaPods** (`pod --version`)
|
||||||
|
- Open **`App.xcworkspace`** (not `.xcodeproj`)
|
||||||
|
|
||||||
|
## Build the web bundle and sync
|
||||||
|
|
||||||
|
From the **npm root** (`llm-fe/`, one level up):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ..
|
||||||
|
npm ci
|
||||||
|
npm run build:mobile # .env.mobile → react-scripts build → cap sync
|
||||||
|
```
|
||||||
|
|
||||||
|
Open in Xcode:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run ios:open
|
||||||
|
# or: npx cap open ios
|
||||||
|
```
|
||||||
|
|
||||||
|
Re-sync after changing web code (with an existing `build/`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run ios:sync
|
||||||
|
```
|
||||||
|
|
||||||
|
Backend URLs are baked in at web-build time via `../.env.mobile` (prod by default).
|
||||||
|
|
||||||
|
After cloning, if Pods are missing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd App
|
||||||
|
pod install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run on a simulator / device
|
||||||
|
|
||||||
|
1. Open `App/App.xcworkspace` in Xcode.
|
||||||
|
2. Pick a simulator or a paired iPhone (Signing & Capabilities → your Team).
|
||||||
|
3. Product → Run (⌘R).
|
||||||
|
|
||||||
|
CLI (optional):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
xcodebuild -workspace App/App.xcworkspace \
|
||||||
|
-scheme App \
|
||||||
|
-destination 'platform=iOS Simulator,name=iPhone 16' \
|
||||||
|
build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Version before a store release
|
||||||
|
|
||||||
|
Edit target **App** → General, or `App.xcodeproj` build settings:
|
||||||
|
|
||||||
|
| Field | Build setting | Rule |
|
||||||
|
|-------|---------------|------|
|
||||||
|
| Version | `MARKETING_VERSION` | Keep in sync with `../package.json` `version` + Android `versionName` |
|
||||||
|
| Build | `CURRENT_PROJECT_VERSION` | Integer; **must increase** for every TestFlight / App Store upload |
|
||||||
|
|
||||||
|
Currently: version **0.1.0**, build **1**.
|
||||||
|
|
||||||
|
## Signing
|
||||||
|
|
||||||
|
**Do not commit** `.p12`, `.mobileprovision`, or export option files with secrets.
|
||||||
|
|
||||||
|
Debug / simulator: Automatic signing with a personal team is enough.
|
||||||
|
|
||||||
|
Distribution: App Store Connect + distribution certificate + profile — credentials
|
||||||
|
live outside this repo. CI/TestFlight automation is a follow-on ticket.
|
||||||
|
|
||||||
|
## Useful paths
|
||||||
|
|
||||||
|
| Path | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `App/App/AppDelegate.swift` | Disables WKWebView rubber-band bounce |
|
||||||
|
| `App/App/Info.plist` | Display name **Hesychia**, version keys |
|
||||||
|
| `App/App/Assets.xcassets` | App icon + splash (regenerate via `npm run assets:generate`) |
|
||||||
|
| `App/Podfile` / `Podfile.lock` | CocoaPods (commit lockfile; Pods/ is gitignored) |
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
"@capacitor/app": "^7.1.2",
|
"@capacitor/app": "^7.1.2",
|
||||||
"@capacitor/cli": "^7.6.8",
|
"@capacitor/cli": "^7.6.8",
|
||||||
"@capacitor/core": "^7.6.8",
|
"@capacitor/core": "^7.6.8",
|
||||||
|
"@capacitor/ios": "^7.6.8",
|
||||||
"@capacitor/keyboard": "^7.0.6",
|
"@capacitor/keyboard": "^7.0.6",
|
||||||
"@capacitor/preferences": "^7.0.4",
|
"@capacitor/preferences": "^7.0.4",
|
||||||
"@capacitor/status-bar": "^7.0.6",
|
"@capacitor/status-bar": "^7.0.6",
|
||||||
@@ -15,32 +16,39 @@
|
|||||||
"@emotion/styled": "^11.14.0",
|
"@emotion/styled": "^11.14.0",
|
||||||
"@mui/icons-material": "^5.16.11",
|
"@mui/icons-material": "^5.16.11",
|
||||||
"@mui/material": "^5.16.11",
|
"@mui/material": "^5.16.11",
|
||||||
|
"@revenuecat/purchases-capacitor": "^11.3.2",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.1.0",
|
"@testing-library/react": "^16.1.0",
|
||||||
"@testing-library/user-event": "^14.5.2",
|
"@testing-library/user-event": "^14.5.2",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^22.10.2",
|
"@types/node": "^22.10.2",
|
||||||
"axios": "^1.7.9",
|
"@types/papaparse": "^5.5.2",
|
||||||
|
"@types/pdfmake": "^0.3.3",
|
||||||
|
"axios": "^1.13.2",
|
||||||
"babel-loader": "^9.2.1",
|
"babel-loader": "^9.2.1",
|
||||||
"bootstrap": "^5.3.3",
|
"bootstrap": "^5.3.3",
|
||||||
|
"brace-expansion": "file:vendor/brace-expansion-compat",
|
||||||
"chroma-js": "^3.1.2",
|
"chroma-js": "^3.1.2",
|
||||||
"formik": "^2.4.6",
|
"formik": "^2.4.6",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"markdown-to-jsx": "^7.7.2",
|
"markdown-to-jsx": "^7.7.2",
|
||||||
"mini.css": "^3.0.1",
|
"mini.css": "^3.0.1",
|
||||||
|
"papaparse": "^5.5.4",
|
||||||
|
"pdfmake": "^0.3.11",
|
||||||
"react-bootstrap": "^2.10.6",
|
"react-bootstrap": "^2.10.6",
|
||||||
"react-code-blocks": "^0.1.6",
|
"react-code-blocks": "^0.1.6",
|
||||||
"react-github-btn": "^1.4.0",
|
"react-github-btn": "^1.4.0",
|
||||||
"react-router-dom": "^6.28.0",
|
"react-router-dom": "^6.30.2",
|
||||||
"react-scripts": "^5.0.1",
|
"react-scripts": "^5.0.1",
|
||||||
"react-syntax-highlighter": "^15.6.1",
|
"react-syntax-highlighter": "^15.6.1",
|
||||||
"recharts": "^2.15.1",
|
"recharts": "^2.15.1",
|
||||||
"styled-components": "^6.1.14",
|
"styled-components": "6.1.15",
|
||||||
"stylis-plugin-rtl": "^2.1.1",
|
"stylis-plugin-rtl": "^2.1.1",
|
||||||
"web-vitals": "^4.2.4",
|
"web-vitals": "^4.2.4",
|
||||||
"webpack": "^5.97.1",
|
"webpack": "^5.97.1",
|
||||||
"webpack-cli": "^5.1.4",
|
"webpack-cli": "^5.1.4",
|
||||||
|
"xlsx": "^0.18.5",
|
||||||
"yup": "^1.5.0"
|
"yup": "^1.5.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -48,10 +56,12 @@
|
|||||||
"build": "NODE_ENV=production react-scripts build",
|
"build": "NODE_ENV=production react-scripts build",
|
||||||
"build:prod": "NODE_ENV=production react-scripts build && mkdir -p /var/www/prod.chat.aimloperations/html && cp -r ./build/* /var/www/prod.chat.aimloperations/html/",
|
"build:prod": "NODE_ENV=production react-scripts build && mkdir -p /var/www/prod.chat.aimloperations/html && cp -r ./build/* /var/www/prod.chat.aimloperations/html/",
|
||||||
"build:beta": "bash -c 'set -a; source .env.beta; set +a; NODE_ENV=production react-scripts build' && mkdir -p /var/www/beta.chat.aimloperations/html && cp -r ./build/* /var/www/beta.chat.aimloperations/html/",
|
"build:beta": "bash -c 'set -a; source .env.beta; set +a; NODE_ENV=production react-scripts build' && mkdir -p /var/www/beta.chat.aimloperations/html && cp -r ./build/* /var/www/beta.chat.aimloperations/html/",
|
||||||
"build:mobile": "bash -c 'set -a; source .env.mobile; set +a; NODE_ENV=production react-scripts build' && npx cap sync android",
|
"build:mobile": "bash -c 'set -a; source .env.mobile; set +a; NODE_ENV=production react-scripts build' && npx cap sync",
|
||||||
"android:open": "npx cap open android",
|
"android:open": "npx cap open android",
|
||||||
"android:sync": "npx cap sync android",
|
"android:sync": "npx cap sync android",
|
||||||
"assets:generate": "npx capacitor-assets generate --android",
|
"ios:open": "npx cap open ios",
|
||||||
|
"ios:sync": "npx cap sync ios",
|
||||||
|
"assets:generate": "npx capacitor-assets generate --ios --android",
|
||||||
"test": "NODE_ENV=development react-scripts test",
|
"test": "NODE_ENV=development react-scripts test",
|
||||||
"test:ci": "CI=true NODE_ENV=development react-scripts test --watchAll=false --coverage=false",
|
"test:ci": "CI=true NODE_ENV=development react-scripts test --watchAll=false --coverage=false",
|
||||||
"eject": "react-scripts eject"
|
"eject": "react-scripts eject"
|
||||||
@@ -76,6 +86,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@capacitor/assets": "^3.0.5",
|
"@capacitor/assets": "^3.0.5",
|
||||||
|
"@testing-library/dom": "^10.4.1",
|
||||||
"@types/bootstrap": "~5.2.10",
|
"@types/bootstrap": "~5.2.10",
|
||||||
"@types/lodash": "~4.17.13",
|
"@types/lodash": "~4.17.13",
|
||||||
"@types/react": "^18.3.16",
|
"@types/react": "^18.3.16",
|
||||||
@@ -86,5 +97,23 @@
|
|||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-google-recaptcha": "^3.1.0",
|
"react-google-recaptcha": "^3.1.0",
|
||||||
"typescript": "^4.9.5"
|
"typescript": "^4.9.5"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"tar": "7.5.22",
|
||||||
|
"underscore": "1.13.8",
|
||||||
|
"nth-check": "2.1.1",
|
||||||
|
"postcss": "8.5.25",
|
||||||
|
"serialize-javascript": "7.0.7",
|
||||||
|
"svgo": "2.8.3",
|
||||||
|
"sharp": "0.35.3",
|
||||||
|
"brace-expansion": "file:vendor/brace-expansion-compat",
|
||||||
|
"minimatch": "3.1.5",
|
||||||
|
"uuid": "11.1.1",
|
||||||
|
"prismjs": "1.30.0",
|
||||||
|
"bfj": "9.1.3",
|
||||||
|
"form-data": "4.0.6",
|
||||||
|
"ws": "8.21.1",
|
||||||
|
"flatted": "3.4.3",
|
||||||
|
"@tootallnate/once": "3.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 549 B After Width: | Height: | Size: 588 B |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 278 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 205 KiB |
@@ -23,6 +23,8 @@ import Tracker from './llm-fe/components/Tracker/Tracker';
|
|||||||
import { AnalyticsConsentProvider } from './llm-fe/contexts/AnalyticsConsentContext';
|
import { AnalyticsConsentProvider } from './llm-fe/contexts/AnalyticsConsentContext';
|
||||||
import AnalyticsConsentBanner from './llm-fe/components/AnalyticsConsentBanner/AnalyticsConsentBanner';
|
import AnalyticsConsentBanner from './llm-fe/components/AnalyticsConsentBanner/AnalyticsConsentBanner';
|
||||||
import AnalyticsSession from './llm-fe/components/AnalyticsSession/AnalyticsSession';
|
import AnalyticsSession from './llm-fe/components/AnalyticsSession/AnalyticsSession';
|
||||||
|
import AppErrorBoundary from './llm-fe/components/AppErrorBoundary/AppErrorBoundary';
|
||||||
|
import ToastHost from './llm-fe/components/ToastHost/ToastHost';
|
||||||
|
|
||||||
const ProtectedRoutes = () => {
|
const ProtectedRoutes = () => {
|
||||||
const { authenticated, loading } = useContext(AuthContext);
|
const { authenticated, loading } = useContext(AuthContext);
|
||||||
@@ -34,7 +36,7 @@ const ProtectedRoutes = () => {
|
|||||||
if (!authenticated) return <Navigate to='/signin/' replace />
|
if (!authenticated) return <Navigate to='/signin/' replace />
|
||||||
//if(!needsNewPassword) return <Navigate to='/password_reset/' replace/>
|
//if(!needsNewPassword) return <Navigate to='/password_reset/' replace/>
|
||||||
|
|
||||||
return <Outlet key={Date.now()} />
|
return <Outlet />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -47,12 +49,14 @@ class App extends Component {
|
|||||||
<AnalyticsConsentProvider>
|
<AnalyticsConsentProvider>
|
||||||
<Tracker />
|
<Tracker />
|
||||||
<AnalyticsSession />
|
<AnalyticsSession />
|
||||||
|
<ToastHost />
|
||||||
<AnalyticsConsentBanner />
|
<AnalyticsConsentBanner />
|
||||||
<div className='site'>
|
<div className='site'>
|
||||||
<main>
|
<main>
|
||||||
<div className="main-container">
|
<div className="main-container">
|
||||||
|
|
||||||
|
|
||||||
|
<AppErrorBoundary>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path='*' element={<NotFound />} />
|
<Route path='*' element={<NotFound />} />
|
||||||
|
|
||||||
@@ -78,6 +82,7 @@ class App extends Component {
|
|||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
</Routes>
|
</Routes>
|
||||||
|
</AppErrorBoundary>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 85 KiB |
@@ -6,7 +6,7 @@
|
|||||||
/**
|
/**
|
||||||
* Absolute backend OAuth start URL.
|
* Absolute backend OAuth start URL.
|
||||||
* @param {'google'|'microsoft'} provider
|
* @param {'google'|'microsoft'} provider
|
||||||
* @param {'login'|'signup'} intent
|
* @param {'login'|'signup'|'link_drive'|'link_company_drive'} intent
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
export function oauthStartUrl(provider, intent = 'login') {
|
export function oauthStartUrl(provider, intent = 'login') {
|
||||||
@@ -18,7 +18,7 @@ export function oauthStartUrl(provider, intent = 'login') {
|
|||||||
/**
|
/**
|
||||||
* Begin browser redirect to IdP via backend.
|
* Begin browser redirect to IdP via backend.
|
||||||
* @param {'google'|'microsoft'} provider
|
* @param {'google'|'microsoft'} provider
|
||||||
* @param {'login'|'signup'} intent
|
* @param {'login'|'signup'|'link_drive'|'link_company_drive'} intent
|
||||||
*/
|
*/
|
||||||
export function startOAuth(provider, intent = 'login') {
|
export function startOAuth(provider, intent = 'login') {
|
||||||
window.location.assign(oauthStartUrl(provider, intent));
|
window.location.assign(oauthStartUrl(provider, intent));
|
||||||
|
|||||||
@@ -9,6 +9,21 @@
|
|||||||
export const ACCESS_TOKEN_KEY = "access_token";
|
export const ACCESS_TOKEN_KEY = "access_token";
|
||||||
export const REFRESH_TOKEN_KEY = "refresh_token";
|
export const REFRESH_TOKEN_KEY = "refresh_token";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Browsers can expose localStorage but throw on access (privacy mode / blocked storage).
|
||||||
|
* @returns {Storage|null}
|
||||||
|
*/
|
||||||
|
function getLocalStorageSafe() {
|
||||||
|
try {
|
||||||
|
if (typeof window === "undefined" || !window.localStorage) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return window.localStorage;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {{ get?: Function, set?: Function, remove?: Function } | undefined}
|
* @returns {{ get?: Function, set?: Function, remove?: Function } | undefined}
|
||||||
*/
|
*/
|
||||||
@@ -24,10 +39,15 @@ function getPreferencesPlugin() {
|
|||||||
* @returns {string|null}
|
* @returns {string|null}
|
||||||
*/
|
*/
|
||||||
export function getToken(key) {
|
export function getToken(key) {
|
||||||
if (typeof localStorage === "undefined") {
|
const storage = getLocalStorageSafe();
|
||||||
|
if (!storage) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return storage.getItem(key);
|
||||||
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return localStorage.getItem(key);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,10 +71,15 @@ export function getRefreshToken() {
|
|||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
export async function setTokens(access, refresh) {
|
export async function setTokens(access, refresh) {
|
||||||
if (typeof localStorage !== "undefined") {
|
const storage = getLocalStorageSafe();
|
||||||
localStorage.setItem(ACCESS_TOKEN_KEY, access);
|
if (storage) {
|
||||||
|
try {
|
||||||
|
storage.setItem(ACCESS_TOKEN_KEY, access);
|
||||||
if (refresh != null) {
|
if (refresh != null) {
|
||||||
localStorage.setItem(REFRESH_TOKEN_KEY, refresh);
|
storage.setItem(REFRESH_TOKEN_KEY, refresh);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* localStorage unavailable */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,9 +100,14 @@ export async function setTokens(access, refresh) {
|
|||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
export async function clearTokens() {
|
export async function clearTokens() {
|
||||||
if (typeof localStorage !== "undefined") {
|
const storage = getLocalStorageSafe();
|
||||||
localStorage.removeItem(ACCESS_TOKEN_KEY);
|
if (storage) {
|
||||||
localStorage.removeItem(REFRESH_TOKEN_KEY);
|
try {
|
||||||
|
storage.removeItem(ACCESS_TOKEN_KEY);
|
||||||
|
storage.removeItem(REFRESH_TOKEN_KEY);
|
||||||
|
} catch {
|
||||||
|
/* localStorage unavailable */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const Preferences = getPreferencesPlugin();
|
const Preferences = getPreferencesPlugin();
|
||||||
@@ -110,7 +140,8 @@ export async function hydrateTokensFromNativeStorage() {
|
|||||||
try {
|
try {
|
||||||
const result = await Preferences.get({ key: ACCESS_TOKEN_KEY });
|
const result = await Preferences.get({ key: ACCESS_TOKEN_KEY });
|
||||||
if (result?.value) {
|
if (result?.value) {
|
||||||
localStorage.setItem(ACCESS_TOKEN_KEY, result.value);
|
const storage = getLocalStorageSafe();
|
||||||
|
storage?.setItem(ACCESS_TOKEN_KEY, result.value);
|
||||||
access = result.value;
|
access = result.value;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -122,7 +153,8 @@ export async function hydrateTokensFromNativeStorage() {
|
|||||||
try {
|
try {
|
||||||
const result = await Preferences.get({ key: REFRESH_TOKEN_KEY });
|
const result = await Preferences.get({ key: REFRESH_TOKEN_KEY });
|
||||||
if (result?.value) {
|
if (result?.value) {
|
||||||
localStorage.setItem(REFRESH_TOKEN_KEY, result.value);
|
const storage = getLocalStorageSafe();
|
||||||
|
storage?.setItem(REFRESH_TOKEN_KEY, result.value);
|
||||||
refresh = result.value;
|
refresh = result.value;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { act, render, screen } from '@testing-library/react';
|
||||||
|
import { ThemeProvider } from 'styled-components';
|
||||||
|
import ActivityIndicator from './ActivityIndicator';
|
||||||
|
import type { ActivityHistoryEntry } from '../../utils/wsFrames';
|
||||||
|
|
||||||
|
const theme = {
|
||||||
|
main: '#336699',
|
||||||
|
focus: '#224466',
|
||||||
|
darkMode: true,
|
||||||
|
colors: {
|
||||||
|
text: '#ffffff',
|
||||||
|
cardBackground: 'rgba(0,0,0,0.3)',
|
||||||
|
cardBorder: 'rgba(255,255,255,0.1)',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderIndicator = (
|
||||||
|
props: React.ComponentProps<typeof ActivityIndicator>,
|
||||||
|
) =>
|
||||||
|
render(
|
||||||
|
<ThemeProvider theme={theme as never}>
|
||||||
|
<ActivityIndicator {...props} />
|
||||||
|
</ThemeProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('ActivityIndicator (#96)', () => {
|
||||||
|
it('has status role and polite live region regardless of state', () => {
|
||||||
|
renderIndicator({ stage: null });
|
||||||
|
const status = screen.getByRole('status');
|
||||||
|
expect(status).toHaveAttribute('aria-live', 'polite');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to three dots when stage is null', () => {
|
||||||
|
renderIndicator({ stage: null });
|
||||||
|
expect(screen.getByTestId('activity-dots-fallback')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the label verbatim for unknown/future stage values', () => {
|
||||||
|
renderIndicator({ stage: 'some_future_stage', label: 'Doing something new' });
|
||||||
|
expect(screen.getByText('Doing something new')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('activity-dots-fallback')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows optional detail text', () => {
|
||||||
|
renderIndicator({
|
||||||
|
stage: 'searching',
|
||||||
|
label: 'Searching the web',
|
||||||
|
detail: 'query: best pizza in town',
|
||||||
|
});
|
||||||
|
expect(screen.getByText('Searching the web')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('query: best pizza in town')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders completed stages above with check marks', () => {
|
||||||
|
const history: ActivityHistoryEntry[] = [
|
||||||
|
{ stage: 'searching', label: 'Searched the web', startedAt: 0, finishedAt: 100 },
|
||||||
|
{ stage: 'reading', label: 'Read 3 sources', startedAt: 100, finishedAt: 400 },
|
||||||
|
];
|
||||||
|
renderIndicator({
|
||||||
|
stage: 'writing',
|
||||||
|
label: null,
|
||||||
|
history,
|
||||||
|
});
|
||||||
|
expect(screen.getByText('Searched the web')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Read 3 sources')).toBeInTheDocument();
|
||||||
|
expect(screen.getAllByText('✓')).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hides everything when interrupted', () => {
|
||||||
|
const { container } = renderIndicator({
|
||||||
|
stage: 'searching',
|
||||||
|
label: 'Searching the web',
|
||||||
|
interrupted: true,
|
||||||
|
});
|
||||||
|
expect(container).toBeEmptyDOMElement();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows a generic label when stage is present but label is cleared (writing stage)', () => {
|
||||||
|
renderIndicator({ stage: 'writing', label: null });
|
||||||
|
expect(screen.getByText('Working…')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('prefers-reduced-motion', () => {
|
||||||
|
const originalMatchMedia = window.matchMedia;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
Object.defineProperty(window, 'matchMedia', {
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: originalMatchMedia,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disables the spinner animation when the user prefers reduced motion', () => {
|
||||||
|
const matchMediaMock = jest.fn().mockImplementation((query: string) => ({
|
||||||
|
matches: query.includes('prefers-reduced-motion'),
|
||||||
|
media: query,
|
||||||
|
addEventListener: jest.fn(),
|
||||||
|
removeEventListener: jest.fn(),
|
||||||
|
addListener: jest.fn(),
|
||||||
|
removeListener: jest.fn(),
|
||||||
|
}));
|
||||||
|
Object.defineProperty(window, 'matchMedia', {
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: matchMediaMock,
|
||||||
|
});
|
||||||
|
|
||||||
|
renderIndicator({ stage: 'searching', label: 'Searching the web' });
|
||||||
|
const spinner = screen.getByTestId('activity-spinner');
|
||||||
|
expect(spinner).toHaveAttribute('data-reduced-motion', 'true');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows elapsed time after ~10s and "Still working…" after ~30s of silence', () => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
try {
|
||||||
|
renderIndicator({ stage: 'searching', label: 'Searching the web' });
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
jest.advanceTimersByTime(11_000);
|
||||||
|
});
|
||||||
|
expect(screen.getByText('11s')).toBeInTheDocument();
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
jest.advanceTimersByTime(20_000);
|
||||||
|
});
|
||||||
|
expect(screen.getByText('Still working…')).toBeInTheDocument();
|
||||||
|
} finally {
|
||||||
|
jest.useRealTimers();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,346 @@
|
|||||||
|
import React, { useEffect, useReducer, useRef, useState } from 'react';
|
||||||
|
import styled, { css, keyframes } from 'styled-components';
|
||||||
|
import type { ActivityHistoryEntry } from '../../utils/wsFrames';
|
||||||
|
|
||||||
|
/** Minimum time a single stage stays visible before advancing to a queued update (#96). */
|
||||||
|
const MIN_DISPLAY_MS = 400;
|
||||||
|
const ELAPSED_THRESHOLD_MS = 10_000;
|
||||||
|
const SILENCE_THRESHOLD_MS = 30_000;
|
||||||
|
|
||||||
|
function usePrefersReducedMotion(): boolean {
|
||||||
|
const [reduced, setReduced] = useState<boolean>(() => {
|
||||||
|
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false;
|
||||||
|
return window.matchMedia('(prefers-reduced-motion: reduce)')?.matches ?? false;
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return undefined;
|
||||||
|
const mql = window.matchMedia('(prefers-reduced-motion: reduce)');
|
||||||
|
if (!mql) return undefined;
|
||||||
|
const handler = (e: MediaQueryListEvent) => setReduced(e.matches);
|
||||||
|
if (typeof mql.addEventListener === 'function') {
|
||||||
|
mql.addEventListener('change', handler);
|
||||||
|
return () => mql.removeEventListener('change', handler);
|
||||||
|
}
|
||||||
|
// Safari < 14 fallback
|
||||||
|
mql.addListener(handler);
|
||||||
|
return () => mql.removeListener(handler);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return reduced;
|
||||||
|
}
|
||||||
|
|
||||||
|
type QueueItem = { stage: string; label: string | null; detail: string | null };
|
||||||
|
|
||||||
|
type IndicatorState = {
|
||||||
|
current: QueueItem | null;
|
||||||
|
queue: QueueItem[];
|
||||||
|
lastIncomingStage: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type IndicatorAction =
|
||||||
|
| { type: 'INCOMING'; item: QueueItem | null }
|
||||||
|
| { type: 'ADVANCE' }
|
||||||
|
| { type: 'RESET' };
|
||||||
|
|
||||||
|
const initialIndicatorState: IndicatorState = {
|
||||||
|
current: null,
|
||||||
|
queue: [],
|
||||||
|
lastIncomingStage: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
function indicatorReducer(state: IndicatorState, action: IndicatorAction): IndicatorState {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'RESET':
|
||||||
|
return initialIndicatorState;
|
||||||
|
case 'INCOMING': {
|
||||||
|
if (!action.item) return initialIndicatorState;
|
||||||
|
// Same stage as the last update seen — refresh content in place (label/detail tweak),
|
||||||
|
// no need to enforce the min-display queue since it isn't a stage transition.
|
||||||
|
if (action.item.stage === state.lastIncomingStage) {
|
||||||
|
if (state.queue.length > 0) {
|
||||||
|
const queue = [...state.queue];
|
||||||
|
queue[queue.length - 1] = action.item;
|
||||||
|
return { ...state, queue };
|
||||||
|
}
|
||||||
|
return { ...state, current: action.item };
|
||||||
|
}
|
||||||
|
if (!state.current) {
|
||||||
|
return { current: action.item, queue: [], lastIncomingStage: action.item.stage };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
queue: [...state.queue, action.item],
|
||||||
|
lastIncomingStage: action.item.stage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'ADVANCE': {
|
||||||
|
if (state.queue.length === 0) return state;
|
||||||
|
const [next, ...rest] = state.queue;
|
||||||
|
return { ...state, current: next, queue: rest };
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const spin = keyframes`
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
`;
|
||||||
|
|
||||||
|
const bounce = keyframes`
|
||||||
|
0%, 80%, 100% { transform: scale(0); }
|
||||||
|
40% { transform: scale(1); }
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Root = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.35rem;
|
||||||
|
max-width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
color: ${({ theme }) => theme.colors.text};
|
||||||
|
font-size: 0.9rem;
|
||||||
|
|
||||||
|
@media (max-width: 360px) {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const HistoryList = styled.ul`
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.2rem;
|
||||||
|
opacity: 0.6;
|
||||||
|
min-width: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const HistoryItem = styled.li`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
span {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CheckMark = styled.span`
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: ${({ theme }) => (theme.darkMode ? '#8ee6a0' : '#2e7d32')};
|
||||||
|
font-weight: 700;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CurrentRow = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
min-width: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Spinner = styled.span<{ $reducedMotion: boolean }>`
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid currentColor;
|
||||||
|
border-top-color: transparent;
|
||||||
|
opacity: 0.7;
|
||||||
|
${({ $reducedMotion }) =>
|
||||||
|
$reducedMotion
|
||||||
|
? css`
|
||||||
|
animation: none;
|
||||||
|
`
|
||||||
|
: css`
|
||||||
|
animation: ${spin} 0.8s linear infinite;
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TextColumn = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const LabelText = styled.span`
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-weight: 500;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const DetailText = styled.span`
|
||||||
|
display: block;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
opacity: 0.65;
|
||||||
|
margin-top: 0.1rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MetaText = styled.span`
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
opacity: 0.55;
|
||||||
|
white-space: nowrap;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const DotsRow = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.15rem 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Dot = styled.span<{ $delay: string; $reducedMotion: boolean }>`
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
margin: 0 4px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: currentColor;
|
||||||
|
${({ $reducedMotion, $delay }) =>
|
||||||
|
$reducedMotion
|
||||||
|
? css`
|
||||||
|
opacity: 0.6;
|
||||||
|
`
|
||||||
|
: css`
|
||||||
|
animation: ${bounce} 1.4s infinite ease-in-out both;
|
||||||
|
animation-delay: ${$delay};
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export type ActivityIndicatorProps = {
|
||||||
|
/** Current stage key from the latest "status" frame, or null before the first update. */
|
||||||
|
stage: string | null;
|
||||||
|
label?: string | null;
|
||||||
|
detail?: string | null;
|
||||||
|
/** Previously completed stages for this turn, oldest first. */
|
||||||
|
history?: ActivityHistoryEntry[];
|
||||||
|
/** Hides the indicator entirely once a stream is interrupted (#96). */
|
||||||
|
interrupted?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ActivityIndicator = ({
|
||||||
|
stage,
|
||||||
|
label = null,
|
||||||
|
detail = null,
|
||||||
|
history = [],
|
||||||
|
interrupted = false,
|
||||||
|
}: ActivityIndicatorProps): JSX.Element | null => {
|
||||||
|
const [state, dispatch] = useReducer(indicatorReducer, initialIndicatorState);
|
||||||
|
const reducedMotion = usePrefersReducedMotion();
|
||||||
|
|
||||||
|
const overallStartRef = useRef<number | null>(null);
|
||||||
|
const lastUpdateRef = useRef<number>(Date.now());
|
||||||
|
const currentSinceRef = useRef<number>(Date.now());
|
||||||
|
const advanceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
const [, forceTick] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (interrupted || stage == null) {
|
||||||
|
overallStartRef.current = null;
|
||||||
|
dispatch({ type: 'RESET' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (overallStartRef.current == null) {
|
||||||
|
overallStartRef.current = Date.now();
|
||||||
|
}
|
||||||
|
lastUpdateRef.current = Date.now();
|
||||||
|
dispatch({ type: 'INCOMING', item: { stage, label, detail } });
|
||||||
|
}, [stage, label, detail, interrupted]);
|
||||||
|
|
||||||
|
// Reset the per-stage clock whenever the displayed stage changes.
|
||||||
|
useEffect(() => {
|
||||||
|
currentSinceRef.current = Date.now();
|
||||||
|
}, [state.current?.stage]);
|
||||||
|
|
||||||
|
// Enforce the minimum display time per stage before advancing the queue.
|
||||||
|
useEffect(() => {
|
||||||
|
if (advanceTimerRef.current) {
|
||||||
|
clearTimeout(advanceTimerRef.current);
|
||||||
|
advanceTimerRef.current = null;
|
||||||
|
}
|
||||||
|
if (state.queue.length === 0) return undefined;
|
||||||
|
const elapsed = Date.now() - currentSinceRef.current;
|
||||||
|
const remaining = Math.max(MIN_DISPLAY_MS - elapsed, 0);
|
||||||
|
advanceTimerRef.current = setTimeout(() => {
|
||||||
|
dispatch({ type: 'ADVANCE' });
|
||||||
|
}, remaining);
|
||||||
|
return () => {
|
||||||
|
if (advanceTimerRef.current) clearTimeout(advanceTimerRef.current);
|
||||||
|
};
|
||||||
|
}, [state.queue.length]);
|
||||||
|
|
||||||
|
// Tick once a second while active so elapsed/"still working" copy stays fresh.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!state.current) return undefined;
|
||||||
|
const id = setInterval(() => forceTick((n) => n + 1), 1000);
|
||||||
|
return () => clearInterval(id);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [Boolean(state.current)]);
|
||||||
|
|
||||||
|
if (interrupted) return null;
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const elapsedMs = overallStartRef.current != null ? now - overallStartRef.current : 0;
|
||||||
|
const silentMs = now - lastUpdateRef.current;
|
||||||
|
const showElapsed = state.current != null && elapsedMs >= ELAPSED_THRESHOLD_MS;
|
||||||
|
const stillWorking = state.current != null && silentMs >= SILENCE_THRESHOLD_MS;
|
||||||
|
const elapsedLabel = showElapsed ? `${Math.floor(elapsedMs / 1000)}s` : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Root role="status" aria-live="polite">
|
||||||
|
{history.length > 0 && (
|
||||||
|
<HistoryList>
|
||||||
|
{history.map((entry, i) => (
|
||||||
|
<HistoryItem key={`${entry.stage}-${entry.startedAt}-${i}`}>
|
||||||
|
<CheckMark aria-hidden="true">✓</CheckMark>
|
||||||
|
<span>{entry.label}</span>
|
||||||
|
</HistoryItem>
|
||||||
|
))}
|
||||||
|
</HistoryList>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{state.current ? (
|
||||||
|
<CurrentRow>
|
||||||
|
<Spinner
|
||||||
|
aria-hidden="true"
|
||||||
|
$reducedMotion={reducedMotion}
|
||||||
|
data-testid="activity-spinner"
|
||||||
|
data-reduced-motion={reducedMotion}
|
||||||
|
/>
|
||||||
|
<TextColumn>
|
||||||
|
<LabelText>
|
||||||
|
{stillWorking ? 'Still working…' : state.current.label || 'Working…'}
|
||||||
|
</LabelText>
|
||||||
|
{state.current.detail && (
|
||||||
|
<DetailText title={state.current.detail}>{state.current.detail}</DetailText>
|
||||||
|
)}
|
||||||
|
</TextColumn>
|
||||||
|
{elapsedLabel && !stillWorking && <MetaText>{elapsedLabel}</MetaText>}
|
||||||
|
</CurrentRow>
|
||||||
|
) : (
|
||||||
|
<DotsRow data-testid="activity-dots-fallback">
|
||||||
|
<Dot $delay="-0.32s" $reducedMotion={reducedMotion} />
|
||||||
|
<Dot $delay="-0.16s" $reducedMotion={reducedMotion} />
|
||||||
|
<Dot $delay="0s" $reducedMotion={reducedMotion} />
|
||||||
|
</DotsRow>
|
||||||
|
)}
|
||||||
|
</Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ActivityIndicator;
|
||||||