Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7f915d7ad | ||
|
|
e96f501ec0 | ||
|
|
7af00aff6f | ||
|
|
adffef6a50 | ||
|
|
824d77e267 | ||
|
|
1d1801e9b5 |
+16
-16
@@ -10,7 +10,7 @@ This guide explains how to build TimeSafari for different platforms using the co
|
||||
# 🖥️ Web Development
|
||||
npm install # setup -- and pkgx.dev `dev` command before this will set environment with npm, etc
|
||||
npm run build:web:serve -- --test # Start with test endorser server
|
||||
npm run build:web:dev # Start development server with hot reload with local endorser server
|
||||
npm run dev # Start development server with hot reload with local endorser server
|
||||
npm run build:web:prod # Production build
|
||||
|
||||
# 📱 Mobile Development
|
||||
@@ -44,7 +44,7 @@ npm run test:web # Run web tests to verify setup
|
||||
|
||||
```bash
|
||||
# Start web development server
|
||||
npm run build:web:dev # Opens http://localhost:8080
|
||||
npm run dev # Opens http://localhost:8080
|
||||
|
||||
# For mobile development
|
||||
npm run build:ios # Opens Xcode with iOS project
|
||||
@@ -225,7 +225,7 @@ npm install
|
||||
|
||||
```bash
|
||||
# Local development
|
||||
npm run build:web:dev # Start development server with hot reload
|
||||
npm run dev # Start development server with hot reload (shortcut for `build:web:dev`)
|
||||
|
||||
# Web builds
|
||||
npm run build:web # Development build (starts dev server with hot reload)
|
||||
@@ -241,7 +241,7 @@ npm run build:web:docker:prod # Production build with Docker containerizat
|
||||
|
||||
### Local Development
|
||||
|
||||
Start the development server using `npm run build:web:dev` or `npm run build:web` (both start the development server with hot reload).
|
||||
Start the development server using `npm run dev` (a shortcut for `npm run build:web:dev`) or `npm run build:web` (both start the development server with hot reload).
|
||||
|
||||
### Web Build for Server
|
||||
|
||||
@@ -1434,8 +1434,8 @@ The recommended way to build for Android is using the automated build script:
|
||||
##### 1. Bump the version in package.json, then update these versions & run:
|
||||
|
||||
```bash
|
||||
perl -p -i -e 's/versionCode .*/versionCode 72/g' android/app/build.gradle
|
||||
perl -p -i -e 's/versionName .*/versionName "1.4.6"/g' android/app/build.gradle
|
||||
perl -p -i -e 's/versionCode .*/versionCode 73/g' android/app/build.gradle
|
||||
perl -p -i -e 's/versionName .*/versionName "1.4.7"/g' android/app/build.gradle
|
||||
```
|
||||
|
||||
##### 2. Build
|
||||
@@ -1446,13 +1446,13 @@ perl -p -i -e 's/versionName .*/versionName "1.4.6"/g' android/app/build.gradle
|
||||
npm run build:android:prod
|
||||
```
|
||||
|
||||
##### 3. Open the project in Android Studio
|
||||
##### 2.1. (Optional) Open the project in Android Studio
|
||||
|
||||
```bash
|
||||
npx cap open android
|
||||
```
|
||||
|
||||
##### 4. Use Android Studio to build and run on emulator or device
|
||||
##### 3. Use Android Studio to build and run on emulator or device
|
||||
|
||||
#### Android Build from the console
|
||||
|
||||
@@ -1460,13 +1460,7 @@ npx cap open android
|
||||
|
||||
* Note that this currently fails: `./gradlew build`
|
||||
|
||||
To create the `aab` file, `bundle` instead of `build` (but not signed and not usable in Play Store):
|
||||
|
||||
```bash
|
||||
./gradlew bundleDebug -Dlint.baselines.continue=true
|
||||
```
|
||||
|
||||
So, to create a signed `aab` release:
|
||||
To create a signed `aab` release:
|
||||
|
||||
- Setup by adding the app/gradle.properties.secrets file (see properties at top
|
||||
of app/build.gradle) and the app/time-safari-upload-key-pkcs12.jks file
|
||||
@@ -1479,6 +1473,12 @@ So, to create a signed `aab` release:
|
||||
|
||||
... and find your `aab` file at app/build/outputs/bundle/release
|
||||
|
||||
To create an unsigned debugging bundle (not usable in Play Store):
|
||||
|
||||
```bash
|
||||
./gradlew bundleDebug -Dlint.baselines.continue=true
|
||||
```
|
||||
|
||||
* Gradle packages whatever web bundle was last synced into
|
||||
`android/app/src/main/assets/public`, even one from a `--test` build. Before
|
||||
uploading, confirm it has production server defaults:
|
||||
@@ -2407,7 +2407,7 @@ npm run clean:electron # Electron only
|
||||
# Clean and rebuild
|
||||
npm run clean:all
|
||||
npm install
|
||||
npm run build:web:dev
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Appendix B: Vite Configuration Files Reference
|
||||
|
||||
@@ -20,7 +20,7 @@ npm install
|
||||
### Web
|
||||
|
||||
```bash
|
||||
npm run build:web:dev
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Then go to [the test page](http://localhost:8080/test) and click "Become User 0" to take action on the platform.
|
||||
@@ -33,6 +33,45 @@ npm run build:android:test:run
|
||||
|
||||
Assumes ADB is installed; see [Android Build](BUILDING.md#android-build) for SDK, emulator, and `PATH` setup.
|
||||
|
||||
#### Building an APK for Android 6 (API 23)
|
||||
|
||||
App versions up to 1.4.6 support Android 6.0 (API 23); later versions require Android 7.0 (API 24) or newer. To run the app on an Android 6 device, build your own APK from the 1.4.6 release commit (`6183afc2`, "bump version to 1.4.6").
|
||||
|
||||
Prerequisites: git, Node.js 20 (see `.nvmrc`), Java 21, and the Android SDK with `ANDROID_HOME` set (see [Android Emulator Without Android Studio](BUILDING.md#android-emulator-without-android-studio-command-line-only) for an SDK setup that skips Android Studio). With [pkgx](https://pkgx.dev), prefix the commands with `pkgx`, e.g. `pkgx npm ci`.
|
||||
|
||||
1. Get the 1.4.6 source:
|
||||
|
||||
```bash
|
||||
git clone https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa.git
|
||||
cd crowd-funder-for-time-pwa
|
||||
git checkout 6183afc2
|
||||
```
|
||||
|
||||
2. Confirm the minimum SDK is 23:
|
||||
|
||||
```bash
|
||||
grep minSdkVersion android/variables.gradle # expect: minSdkVersion = 23
|
||||
```
|
||||
|
||||
3. Install dependencies and build a production APK:
|
||||
|
||||
```bash
|
||||
npm ci
|
||||
npm run build:android:prod -- --apk
|
||||
```
|
||||
|
||||
`npm ci` installs the exact dependency versions locked for 1.4.6. `build:android:prod -- --apk` builds the web app with production server settings, syncs it into the Android project, and runs `./gradlew assembleDebug`. The result is signed with your local Android debug key, so it installs on any device without a release keystore.
|
||||
|
||||
4. Find the APK at `android/app/build/outputs/apk/debug/app-debug.apk` and install it, either with `adb install android/app/build/outputs/apk/debug/app-debug.apk` (USB debugging enabled on the phone) or by copying the file to the phone and opening it (allow "Install unknown apps" for the file manager).
|
||||
|
||||
Notes:
|
||||
|
||||
* **Back up your identity first.** A self-built APK has a different signature from the Play Store app, so Android refuses to install it over the store version. Uninstalling the store version deletes the app's local data, including your identifier keys. Back up your seed phrase (Profile page → "Backup Identifier Seed") before uninstalling, then import it into the self-built app.
|
||||
* This build has Firebase disabled, so push notifications through Google's FCM service are off.
|
||||
* The daily notification plugin calls `java.time`, which Android provides only from 8.0 (API 26). On Android 6, scheduling a daily reminder can crash the app; leave daily reminders off. See [doc/plugin-feedback-android-6-api23-zoneid-fix.md](doc/plugin-feedback-android-6-api23-zoneid-fix.md).
|
||||
* Barcode scanning uses Google Play Services; on devices without them, scanning fails while the rest of the app works.
|
||||
* A self-built APK does not auto-update.
|
||||
|
||||
### iOS
|
||||
|
||||
```bash
|
||||
@@ -98,16 +137,16 @@ TimeSafari supports configurable logging levels via the `VITE_LOG_LEVEL` environ
|
||||
|
||||
```bash
|
||||
# Show only errors
|
||||
VITE_LOG_LEVEL=error npm run build:web:dev
|
||||
VITE_LOG_LEVEL=error npm run dev
|
||||
|
||||
# Show warnings and errors
|
||||
VITE_LOG_LEVEL=warn npm run build:web:dev
|
||||
VITE_LOG_LEVEL=warn npm run dev
|
||||
|
||||
# Show info, warnings, and errors (default)
|
||||
VITE_LOG_LEVEL=info npm run build:web:dev
|
||||
VITE_LOG_LEVEL=info npm run dev
|
||||
|
||||
# Show all log levels including debug
|
||||
VITE_LOG_LEVEL=debug npm run build:web:dev
|
||||
VITE_LOG_LEVEL=debug npm run dev
|
||||
```
|
||||
|
||||
### Available Levels
|
||||
@@ -147,7 +186,7 @@ Platform-specific end-to-end guides:
|
||||
|
||||
# Then restart your development server
|
||||
npm run build:electron:dev # For Electron
|
||||
npm run build:web:dev # For Web
|
||||
npm run dev # For Web
|
||||
```
|
||||
|
||||
### What It Does
|
||||
|
||||
@@ -37,8 +37,8 @@ android {
|
||||
applicationId "app.timesafari.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 72
|
||||
versionName "1.4.6"
|
||||
versionCode 73
|
||||
versionName "1.4.7"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
ext {
|
||||
minSdkVersion = 23
|
||||
minSdkVersion = 24
|
||||
compileSdkVersion = 36
|
||||
targetSdkVersion = 36
|
||||
androidxActivityVersion = '1.8.0'
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "giftopia",
|
||||
"version": "1.4.7-beta",
|
||||
"version": "1.4.7",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "giftopia",
|
||||
"version": "1.4.7-beta",
|
||||
"version": "1.4.7",
|
||||
"dependencies": {
|
||||
"@capacitor-community/electron": "^5.0.1",
|
||||
"@capacitor-community/sqlite": "^7.0.3",
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "giftopia",
|
||||
"version": "1.4.7-beta",
|
||||
"version": "1.4.7",
|
||||
"description": "Giftopia App",
|
||||
"author": {
|
||||
"name": "Gift Economies Team"
|
||||
@@ -110,6 +110,7 @@
|
||||
"clean:electron": "./scripts/build-electron.sh --clean",
|
||||
"clean:ios": "rm -rf ios/App/build ios/App/Pods ios/App/output ios/App/App/public ios/DerivedData ios/capacitor-cordova-ios-plugins ios/App/App/capacitor.config.json ios/App/App/config.xml || true",
|
||||
"deps:update-daily-notification-plugin": "npm install @timesafari/daily-notification-plugin@git+https://gitea.anomalistdesign.com/trent_larson/daily-notification-plugin.git#master",
|
||||
"dev": "npm run build:web:dev",
|
||||
"docker:down": "docker-compose down",
|
||||
"docker:logs": "docker-compose logs -f",
|
||||
"docker:up": "docker-compose up",
|
||||
|
||||
@@ -854,19 +854,19 @@ export default class ImageMethodDialog extends Vue {
|
||||
formData.append("image", this.blob, this.fileName || "photo.jpg");
|
||||
formData.append("claimType", this.claimType);
|
||||
try {
|
||||
const imageServer =
|
||||
(await this.$accountSettings()).imageServer || DEFAULT_IMAGE_API_SERVER;
|
||||
if (
|
||||
window.location.hostname === "localhost" &&
|
||||
!DEFAULT_IMAGE_API_SERVER.includes("localhost")
|
||||
!imageServer.includes("localhost")
|
||||
) {
|
||||
logger.log(
|
||||
"Using shared image API server, so only users on that server can play with images.",
|
||||
);
|
||||
}
|
||||
const response = await axios.post(
|
||||
DEFAULT_IMAGE_API_SERVER + "/image",
|
||||
formData,
|
||||
{ headers },
|
||||
);
|
||||
const response = await axios.post(imageServer + "/image", formData, {
|
||||
headers,
|
||||
});
|
||||
this.uploading = false;
|
||||
|
||||
this.close();
|
||||
|
||||
@@ -556,19 +556,19 @@ export default class PhotoDialog extends Vue {
|
||||
formData.append("image", this.blob, this.fileName || "photo.jpg");
|
||||
formData.append("claimType", this.claimType);
|
||||
try {
|
||||
const imageServer =
|
||||
(await this.$accountSettings()).imageServer || DEFAULT_IMAGE_API_SERVER;
|
||||
if (
|
||||
window.location.hostname === "localhost" &&
|
||||
!DEFAULT_IMAGE_API_SERVER.includes("localhost")
|
||||
!imageServer.includes("localhost")
|
||||
) {
|
||||
logger.log(
|
||||
"Using shared image API server, so only users on that server can play with images.",
|
||||
);
|
||||
}
|
||||
const response = await axios.post(
|
||||
DEFAULT_IMAGE_API_SERVER + "/image",
|
||||
formData,
|
||||
{ headers },
|
||||
);
|
||||
const response = await axios.post(imageServer + "/image", formData, {
|
||||
headers,
|
||||
});
|
||||
this.uploading = false;
|
||||
|
||||
this.close();
|
||||
|
||||
@@ -1401,6 +1401,22 @@ export const NOTIFY_PROJECT_DELETE_IMAGE_GENERAL_ERROR = {
|
||||
message: "There was an error deleting the image.",
|
||||
};
|
||||
|
||||
// Used in: AccountViewView.vue, GiftedDetailsView.vue, NewEditProjectView.vue
|
||||
// (deleteImage - the image server returned 404). The server looks an image up
|
||||
// by URL and uploader, so this also happens when the image was uploaded to a
|
||||
// different image server than the one in settings.
|
||||
export const NOTIFY_IMAGE_DELETE_NOT_FOUND = {
|
||||
title: "Image Not Deleted",
|
||||
message:
|
||||
"The image server in your settings has no record of that image, so it was not removed from storage. It may already be gone, or it may have been uploaded through a different image server.",
|
||||
};
|
||||
|
||||
// Used in: GiftedDetailsView.vue (deleteImage method - image deletion error)
|
||||
export const NOTIFY_IMAGE_DELETE_ERROR = {
|
||||
title: "Image Deletion Error",
|
||||
message: "There was an error deleting the image from the image server.",
|
||||
};
|
||||
|
||||
// Used in: NewEditProjectView.vue (validateLocation method - invalid location error)
|
||||
export const NOTIFY_PROJECT_INVALID_LOCATION = {
|
||||
title: "Invalid Location",
|
||||
|
||||
@@ -287,6 +287,13 @@ const MIGRATIONS = [
|
||||
ALTER TABLE settings ADD COLUMN smsNotificationsEnabled BOOLEAN DEFAULT FALSE;
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "011_add_imageServer_to_settings",
|
||||
sql: `
|
||||
-- Image API server URL; blank means DEFAULT_IMAGE_API_SERVER
|
||||
ALTER TABLE settings ADD COLUMN imageServer TEXT;
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -38,7 +38,7 @@ export type Settings = {
|
||||
hasBackedUpSeed?: boolean; // tracks whether the user has backed up their seed phrase
|
||||
hideRegisterPromptOnNewContact?: boolean;
|
||||
isRegistered?: boolean;
|
||||
// imageServer?: string; // if we want to allow modification then we should make image functionality optional -- or at least customizable
|
||||
imageServer?: string; // image API server URL
|
||||
lastName?: string; // deprecated - put all names in firstName
|
||||
|
||||
lastAckedOfferToUserJwtId?: string; // the last JWT ID for offer-to-user that they've acknowledged seeing
|
||||
|
||||
@@ -37,6 +37,7 @@ export interface AccountSettings {
|
||||
notifyingReminderMessage?: string;
|
||||
notifyingReminderTime?: string;
|
||||
starredPlanHandleIds?: string[];
|
||||
imageServer?: string;
|
||||
reminderFastRolloverForTesting?: boolean;
|
||||
partnerApiServer?: string;
|
||||
profileImageUrl?: string;
|
||||
@@ -102,6 +103,8 @@ export interface SettingsState {
|
||||
activeDid: string;
|
||||
apiServer: string;
|
||||
apiServerInput: string;
|
||||
imageServer: string;
|
||||
imageServerInput: string;
|
||||
partnerApiServer: string;
|
||||
partnerApiServerInput: string;
|
||||
webPushServer: string;
|
||||
|
||||
+9
-14
@@ -10,7 +10,6 @@ import {
|
||||
retrieveAccountDids,
|
||||
generateSaveAndActivateIdentity,
|
||||
} from "../libs/util";
|
||||
import { includeDevToolkitRoutes } from "../utils/includeDevToolkitRoutes";
|
||||
|
||||
const routes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
@@ -291,19 +290,15 @@ const routes: Array<RouteRecordRaw> = [
|
||||
name: "user-profile",
|
||||
component: () => import("../views/UserProfileView.vue"),
|
||||
},
|
||||
...(includeDevToolkitRoutes
|
||||
? ([
|
||||
{
|
||||
path: "/dev/notifications",
|
||||
name: "dev-notifications",
|
||||
component: () => import("../views/dev/NotificationDebugView.vue"),
|
||||
meta: {
|
||||
title: "Notification Debug",
|
||||
requiresAuth: false,
|
||||
},
|
||||
},
|
||||
] satisfies Array<RouteRecordRaw>)
|
||||
: []),
|
||||
{
|
||||
path: "/dev/notifications",
|
||||
name: "dev-notifications",
|
||||
component: () => import("../views/dev/NotificationDebugView.vue"),
|
||||
meta: {
|
||||
title: "Notification Debug",
|
||||
requiresAuth: false,
|
||||
},
|
||||
},
|
||||
// Catch-all route for 404 errors - must be last
|
||||
{
|
||||
path: "/:pathMatch(.*)*",
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* True for `vite dev` and for `vite build` when mode is not `production`
|
||||
* (e.g. `--mode capacitor`, `--mode test`). Use for dev-only routes and UI.
|
||||
*/
|
||||
export const includeDevToolkitRoutes =
|
||||
import.meta.env.DEV || import.meta.env.MODE !== "production";
|
||||
@@ -168,9 +168,9 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Channel 1: In-App notification -->
|
||||
<!-- Channel 1: In-App notification (only with general advanced functions on) -->
|
||||
<div
|
||||
v-if="isDev"
|
||||
v-if="showGeneralAdvanced"
|
||||
class="flex items-center justify-between mt-3 mb-2 pl-1"
|
||||
>
|
||||
<!-- label -->
|
||||
@@ -205,12 +205,15 @@
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isDev" class="mb-2 pl-1">
|
||||
<div v-if="showGeneralAdvanced" class="mb-2 pl-1">
|
||||
<router-link class="text-sm text-blue-500" to="/help-notifications">
|
||||
Troubleshoot your in-app notifications…
|
||||
</router-link>
|
||||
</div>
|
||||
<div v-if="notifyingNewActivity && isDev" class="w-full pl-1">
|
||||
<div
|
||||
v-if="notifyingNewActivity && showGeneralAdvanced"
|
||||
class="w-full pl-1"
|
||||
>
|
||||
<div
|
||||
class="text-sm text-slate-500 mb-2 bg-white rounded px-3 py-2 border border-slate-200"
|
||||
>
|
||||
@@ -635,6 +638,7 @@
|
||||
color="white"
|
||||
aria-hidden="true"
|
||||
></font-awesome>
|
||||
<span class="text-white">Unsavedaroonosity</span>
|
||||
</button>
|
||||
<div class="mt-2" role="group" aria-label="Quick server selection">
|
||||
<button
|
||||
@@ -732,6 +736,7 @@
|
||||
class="fa-fw"
|
||||
color="white"
|
||||
></font-awesome>
|
||||
<span class="text-white">Unsaved</span>
|
||||
</button>
|
||||
<button
|
||||
class="px-3 rounded bg-slate-200 border border-slate-400"
|
||||
@@ -778,6 +783,7 @@
|
||||
class="fa-fw"
|
||||
color="white"
|
||||
></font-awesome>
|
||||
<span class="text-white">Unsaved</span>
|
||||
</button>
|
||||
<button
|
||||
class="px-3 rounded bg-slate-200 border border-slate-400"
|
||||
@@ -810,11 +816,49 @@
|
||||
{{ DEFAULT_PARTNER_API_SERVER }}
|
||||
</span>
|
||||
|
||||
<div class="mt-2">
|
||||
<span class="text-slate-500 text-sm font-bold">Image Server URL</span>
|
||||
|
||||
<span class="text-sm">{{ DEFAULT_IMAGE_API_SERVER }}</span>
|
||||
<h2 class="text-slate-500 text-sm font-bold mb-2">Image Server URL</h2>
|
||||
<div class="px-3 py-4">
|
||||
<input
|
||||
v-model="imageServerInput"
|
||||
type="text"
|
||||
class="block w-full rounded border border-slate-400 px-3 py-2"
|
||||
/>
|
||||
<button
|
||||
v-if="imageServerInput != imageServer"
|
||||
class="w-full px-4 rounded bg-yellow-500 border border-slate-400"
|
||||
@click="onClickSaveImageServer()"
|
||||
>
|
||||
<font-awesome
|
||||
icon="floppy-disk"
|
||||
class="fa-fw"
|
||||
color="white"
|
||||
></font-awesome>
|
||||
<span class="text-white">Unsaved</span>
|
||||
</button>
|
||||
<button
|
||||
class="px-3 rounded bg-slate-200 border border-slate-400"
|
||||
@click="imageServerInput = AppConstants.PROD_IMAGE_API_SERVER"
|
||||
>
|
||||
Use Prod
|
||||
</button>
|
||||
<button
|
||||
class="px-3 rounded bg-slate-200 border border-slate-400"
|
||||
@click="imageServerInput = AppConstants.TEST_IMAGE_API_SERVER"
|
||||
>
|
||||
Use Test
|
||||
</button>
|
||||
<button
|
||||
class="px-3 rounded bg-slate-200 border border-slate-400"
|
||||
@click="imageServerInput = AppConstants.LOCAL_IMAGE_API_SERVER"
|
||||
>
|
||||
Use Local
|
||||
</button>
|
||||
</div>
|
||||
<span v-if="!imageServerInput" class="px-4 text-sm">
|
||||
When that setting is blank, this app will use the default image server
|
||||
URL:
|
||||
{{ DEFAULT_IMAGE_API_SERVER }}
|
||||
</span>
|
||||
|
||||
<div class="mt-2">
|
||||
<span class="text-slate-500 text-sm font-bold"
|
||||
@@ -939,10 +983,7 @@
|
||||
Test Page
|
||||
</router-link>
|
||||
|
||||
<!-- Non-production bundles only; route `dev-notifications` must exist
|
||||
(see `includeDevToolkitRoutes`). -->
|
||||
<router-link
|
||||
v-if="isDev"
|
||||
:to="{ name: 'dev-notifications' }"
|
||||
class="block w-fit text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md mt-2"
|
||||
>
|
||||
@@ -1025,8 +1066,8 @@ import { logger } from "../utils/logger";
|
||||
import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
|
||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||
import { ACCOUNT_VIEW_CONSTANTS } from "@/constants/accountView";
|
||||
import { NOTIFY_IMAGE_DELETE_NOT_FOUND } from "@/constants/notifications";
|
||||
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
|
||||
import { includeDevToolkitRoutes } from "@/utils/includeDevToolkitRoutes";
|
||||
import { AccountSettings, isApiError } from "@/interfaces/accountView";
|
||||
import {
|
||||
NotificationService,
|
||||
@@ -1105,7 +1146,6 @@ export default class AccountViewView extends Vue {
|
||||
readonly DEFAULT_IMAGE_API_SERVER: string = DEFAULT_IMAGE_API_SERVER;
|
||||
readonly DEFAULT_PARTNER_API_SERVER: string = DEFAULT_PARTNER_API_SERVER;
|
||||
readonly PASSKEYS_ENABLED: boolean = PASSKEYS_ENABLED;
|
||||
readonly isDev: boolean = includeDevToolkitRoutes;
|
||||
|
||||
// Server URLs section starts collapsed
|
||||
showServerSettings: boolean = false;
|
||||
@@ -1124,6 +1164,8 @@ export default class AccountViewView extends Vue {
|
||||
searchBox: { name: string; bbox: BoundingBox } | null = null;
|
||||
partnerApiServer: string = DEFAULT_PARTNER_API_SERVER;
|
||||
partnerApiServerInput: string = DEFAULT_PARTNER_API_SERVER;
|
||||
imageServer: string = DEFAULT_IMAGE_API_SERVER;
|
||||
imageServerInput: string = DEFAULT_IMAGE_API_SERVER;
|
||||
passkeyExpirationDescription: string = "";
|
||||
passkeyExpirationMinutes: number = DEFAULT_PASSKEY_EXPIRATION_MINUTES;
|
||||
previousPasskeyExpirationMinutes: number = DEFAULT_PASSKEY_EXPIRATION_MINUTES;
|
||||
@@ -1356,6 +1398,8 @@ export default class AccountViewView extends Vue {
|
||||
this.partnerApiServer = settings.partnerApiServer || this.partnerApiServer;
|
||||
this.partnerApiServerInput =
|
||||
settings.partnerApiServer || this.partnerApiServerInput;
|
||||
this.imageServer = settings.imageServer || this.imageServer;
|
||||
this.imageServerInput = settings.imageServer || this.imageServerInput;
|
||||
this.profileImageUrl = settings.profileImageUrl;
|
||||
this.showContactGives = !!settings.showContactGivesInline;
|
||||
this.passkeyExpirationMinutes =
|
||||
@@ -2437,7 +2481,7 @@ export default class AccountViewView extends Vue {
|
||||
const imageResp = await fetchImageRateLimits(
|
||||
this.axios,
|
||||
did,
|
||||
this.DEFAULT_IMAGE_API_SERVER,
|
||||
this.imageServer,
|
||||
);
|
||||
|
||||
if (imageResp && imageResp.status === 200) {
|
||||
@@ -2472,7 +2516,7 @@ export default class AccountViewView extends Vue {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
did: did,
|
||||
apiServer: this.apiServer,
|
||||
imageServer: this.DEFAULT_IMAGE_API_SERVER,
|
||||
imageServer: this.imageServer,
|
||||
partnerApiServer: this.partnerApiServer,
|
||||
errorCode: axiosError?.response?.data?.error?.code,
|
||||
errorMessage: axiosError?.response?.data?.error?.message,
|
||||
@@ -2555,6 +2599,17 @@ export default class AccountViewView extends Vue {
|
||||
});
|
||||
}
|
||||
|
||||
async onClickSaveImageServer(): Promise<void> {
|
||||
await this.$saveSettings({
|
||||
imageServer: this.imageServerInput,
|
||||
});
|
||||
this.imageServer = this.imageServerInput;
|
||||
|
||||
await this.$saveUserSettings(this.activeDid, {
|
||||
imageServer: this.imageServer,
|
||||
});
|
||||
}
|
||||
|
||||
async onClickSavePushServer(): Promise<void> {
|
||||
await this.$saveSettings({
|
||||
webPushServer: this.webPushServerInput,
|
||||
@@ -2585,15 +2640,14 @@ export default class AccountViewView extends Vue {
|
||||
|
||||
async deleteImage(): Promise<void> {
|
||||
try {
|
||||
// Extract the image ID from the full URL
|
||||
const imageId = this.profileImageUrl?.split("/").pop();
|
||||
if (!imageId) {
|
||||
if (!this.profileImageUrl) {
|
||||
this.notify.error("Invalid image URL");
|
||||
return;
|
||||
}
|
||||
|
||||
// the image server looks the image up by its full URL
|
||||
const response = await this.axios.delete(
|
||||
this.apiServer + "/api/image/" + imageId,
|
||||
this.imageServer + "/image/" + encodeURIComponent(this.profileImageUrl),
|
||||
{ headers: await getHeaders(this.activeDid) },
|
||||
);
|
||||
if (response.status === 204) {
|
||||
@@ -2609,12 +2663,12 @@ export default class AccountViewView extends Vue {
|
||||
}
|
||||
} catch (error) {
|
||||
if (isApiError(error) && error.response?.status === 404) {
|
||||
// it already doesn't exist so we won't say anything to the user
|
||||
// Clear the local reference since the image is gone
|
||||
// still drop it from the profile, which is what the user asked for
|
||||
this.profileImageUrl = "";
|
||||
await this.$saveSettings({
|
||||
profileImageUrl: "",
|
||||
});
|
||||
this.notify.error(NOTIFY_IMAGE_DELETE_NOT_FOUND.message, TIMEOUTS.LONG);
|
||||
} else {
|
||||
this.notify.error(
|
||||
ACCOUNT_VIEW_CONSTANTS.ERRORS.IMAGE_DELETE_ERROR,
|
||||
|
||||
@@ -71,26 +71,25 @@
|
||||
<code class="bg-gray-100 px-2 py-1 rounded">{{ deepLinkUrl }}</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Platform info for debugging -->
|
||||
<div
|
||||
v-if="isDevelopment"
|
||||
class="text-center text-xs text-gray-400 mt-4"
|
||||
>
|
||||
<p>
|
||||
Platform: {{ isMobile ? (isIOS ? "iOS" : "Android") : "Desktop" }}
|
||||
</p>
|
||||
<p>User Agent: {{ userAgent.substring(0, 50) }}...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="pageError" class="text-center text-red-500 mb-4">
|
||||
<div v-else-if="!pageError" class="text-center text-gray-600">
|
||||
<p>Processing redirect...</p>
|
||||
</div>
|
||||
|
||||
<!-- Outside the chain above: a redirect failure is set after the
|
||||
buttons render, and it points the user back up to them. -->
|
||||
<div v-if="pageError" class="text-center text-red-500 mt-4 mb-4">
|
||||
{{ pageError }}
|
||||
</div>
|
||||
|
||||
<div v-else class="text-center text-gray-600">
|
||||
<p>Processing redirect...</p>
|
||||
</div>
|
||||
<details class="text-center text-xs text-gray-400 mt-4">
|
||||
<summary class="cursor-pointer">Debug Info</summary>
|
||||
<p>
|
||||
Platform: {{ isMobile ? (isIOS ? "iOS" : "Android") : "Desktop" }}
|
||||
</p>
|
||||
<p class="break-all">User Agent: {{ userAgent }}</p>
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -112,7 +111,6 @@ export default class DeepLinkRedirectView extends Vue {
|
||||
destinationUrl: string | null = null; // full path after "/deep-link/"
|
||||
deepLinkUrl: string | null = null; // mobile link starting "timesafari://"
|
||||
webUrl: string | null = null; // web link, eg "https://timesafari.app/..."
|
||||
isDevelopment: boolean = false;
|
||||
userAgent: string = "";
|
||||
platformService = PlatformServiceFactory.getInstance();
|
||||
|
||||
@@ -148,7 +146,6 @@ export default class DeepLinkRedirectView extends Vue {
|
||||
this.deepLinkUrl = `timesafari://${fullPathWithQuery}`;
|
||||
this.webUrl = `${APP_SERVER}/${fullPathWithQuery}`;
|
||||
|
||||
this.isDevelopment = process.env.NODE_ENV !== "production";
|
||||
this.userAgent = navigator.userAgent;
|
||||
|
||||
this.openDeepLink();
|
||||
@@ -190,13 +187,13 @@ export default class DeepLinkRedirectView extends Vue {
|
||||
"Fallback deep link failed: " + errorStringForLog(error),
|
||||
);
|
||||
this.pageError =
|
||||
"Redirecting to the app failed. Please use a manual option below.";
|
||||
"Redirecting to the app failed. Please use a manual option above.";
|
||||
}
|
||||
}, 100);
|
||||
} catch (error) {
|
||||
logger.error("Deep link redirect failed: " + errorStringForLog(error));
|
||||
this.pageError =
|
||||
"Unable to open the app. Please use a manual option below.";
|
||||
"Unable to open the app. Please use a manual option above.";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -271,6 +271,8 @@ import {
|
||||
NOTIFY_GIFTED_DETAILS_CREATE_GIVE_ERROR,
|
||||
NOTIFY_GIFTED_DETAILS_GIFT_RECORDED,
|
||||
NOTIFY_GIFT_ERROR_NO_DESCRIPTION,
|
||||
NOTIFY_IMAGE_DELETE_ERROR,
|
||||
NOTIFY_IMAGE_DELETE_NOT_FOUND,
|
||||
} from "@/constants/notifications";
|
||||
|
||||
@Component({
|
||||
@@ -563,30 +565,33 @@ export default class GiftedDetails extends Vue {
|
||||
}
|
||||
try {
|
||||
const headers = await getHeaders(this.activeDid);
|
||||
const imageServer =
|
||||
(await this.$accountSettings()).imageServer || DEFAULT_IMAGE_API_SERVER;
|
||||
if (
|
||||
window.location.hostname === "localhost" &&
|
||||
!DEFAULT_IMAGE_API_SERVER.includes("localhost")
|
||||
!imageServer.includes("localhost")
|
||||
) {
|
||||
logger.log(
|
||||
"Using shared image API server, so only users on that server can play with images.",
|
||||
);
|
||||
}
|
||||
const response = await this.axios.delete(
|
||||
DEFAULT_IMAGE_API_SERVER + "/image/" + encodeURIComponent(imageUrl),
|
||||
imageServer + "/image/" + encodeURIComponent(imageUrl),
|
||||
{ headers },
|
||||
);
|
||||
if (response.status === 204) {
|
||||
// don't bother with a notification
|
||||
} else {
|
||||
logger.error("Problem deleting image:", response);
|
||||
this.notify.error(NOTIFY_IMAGE_DELETE_ERROR.message, TIMEOUTS.LONG);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error deleting image:", error);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if ((error as any)?.response?.status === 404) {
|
||||
logger.log("Image was already deleted:", error);
|
||||
this.notify.error(NOTIFY_IMAGE_DELETE_NOT_FOUND.message, TIMEOUTS.LONG);
|
||||
} else {
|
||||
logger.error("Failed to delete image from server:", error);
|
||||
this.notify.error(NOTIFY_IMAGE_DELETE_ERROR.message, TIMEOUTS.LONG);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,13 +335,14 @@ import {
|
||||
NotificationIface,
|
||||
} from "../constants/app";
|
||||
import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
|
||||
import { createNotifyHelpers } from "../utils/notify";
|
||||
import { createNotifyHelpers, TIMEOUTS } from "../utils/notify";
|
||||
import {
|
||||
NOTIFY_PROJECT_ACCOUNT_LOADING_ERROR,
|
||||
NOTIFY_PROJECT_RETRIEVAL_ERROR,
|
||||
NOTIFY_PROJECT_DELETE_IMAGE_CONFIRM,
|
||||
NOTIFY_PROJECT_DELETE_IMAGE_ERROR,
|
||||
NOTIFY_PROJECT_DELETE_IMAGE_GENERAL_ERROR,
|
||||
NOTIFY_IMAGE_DELETE_NOT_FOUND,
|
||||
NOTIFY_PROJECT_INVALID_LOCATION,
|
||||
NOTIFY_PROJECT_INVALID_START_DATE,
|
||||
NOTIFY_PROJECT_INVALID_END_DATE,
|
||||
@@ -617,9 +618,11 @@ export default class NewEditProjectView extends Vue {
|
||||
}
|
||||
try {
|
||||
const headers = (await getHeaders(this.activeDid)) as AxiosRequestHeaders;
|
||||
const imageServer =
|
||||
(await this.$accountSettings()).imageServer || DEFAULT_IMAGE_API_SERVER;
|
||||
if (
|
||||
window.location.hostname === "localhost" &&
|
||||
!DEFAULT_IMAGE_API_SERVER.includes("localhost")
|
||||
!imageServer.includes("localhost")
|
||||
) {
|
||||
logger.log(
|
||||
"Using shared image API server, so only users on that server can play with images.",
|
||||
@@ -627,9 +630,7 @@ export default class NewEditProjectView extends Vue {
|
||||
}
|
||||
|
||||
const response = await this.axios.delete(
|
||||
DEFAULT_IMAGE_API_SERVER +
|
||||
"/image/" +
|
||||
encodeURIComponent(this.imageUrl),
|
||||
imageServer + "/image/" + encodeURIComponent(this.imageUrl),
|
||||
{ headers },
|
||||
);
|
||||
if (response.status === 204) {
|
||||
@@ -645,12 +646,10 @@ export default class NewEditProjectView extends Vue {
|
||||
} catch (error) {
|
||||
logger.error("Error deleting image:", error);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if ((error as any).response.status === 404) {
|
||||
logger.log("The image was already deleted:", error);
|
||||
|
||||
if ((error as any)?.response?.status === 404) {
|
||||
// still drop it from the project, which is what the user asked for
|
||||
this.imageUrl = "";
|
||||
|
||||
// it already doesn't exist so we won't say anything to the user
|
||||
this.notify.error(NOTIFY_IMAGE_DELETE_NOT_FOUND.message, TIMEOUTS.LONG);
|
||||
} else {
|
||||
this.notify.error(NOTIFY_PROJECT_DELETE_IMAGE_GENERAL_ERROR.message);
|
||||
}
|
||||
|
||||
@@ -338,19 +338,19 @@ export default class SharedPhotoView extends Vue {
|
||||
);
|
||||
formData.append("claimType", imageType);
|
||||
|
||||
const imageServer =
|
||||
(await this.$accountSettings()).imageServer || DEFAULT_IMAGE_API_SERVER;
|
||||
if (
|
||||
window.location.hostname === "localhost" &&
|
||||
!DEFAULT_IMAGE_API_SERVER.includes("localhost")
|
||||
!imageServer.includes("localhost")
|
||||
) {
|
||||
logger.log(
|
||||
"Using shared image API server, so only users on that server can play with images.",
|
||||
);
|
||||
}
|
||||
const response = await axios.post(
|
||||
DEFAULT_IMAGE_API_SERVER + "/image",
|
||||
formData,
|
||||
{ headers },
|
||||
);
|
||||
const response = await axios.post(imageServer + "/image", formData, {
|
||||
headers,
|
||||
});
|
||||
if (response?.data?.url) {
|
||||
this.imageBlob = undefined;
|
||||
this.imageFileName = undefined;
|
||||
|
||||
@@ -15,17 +15,7 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!isDev"
|
||||
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border rounded-md overflow-hidden px-4 py-3"
|
||||
role="alert"
|
||||
>
|
||||
This screen is hidden in production Vite builds (for example when built
|
||||
with
|
||||
<span class="font-mono text-sm">--mode production</span>).
|
||||
</div>
|
||||
|
||||
<NotificationDebugPanel v-else />
|
||||
<NotificationDebugPanel />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@@ -33,13 +23,11 @@
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { Router } from "vue-router";
|
||||
import NotificationDebugPanel from "@/components/dev/NotificationDebugPanel.vue";
|
||||
import { includeDevToolkitRoutes } from "@/utils/includeDevToolkitRoutes";
|
||||
|
||||
@Component({
|
||||
components: { NotificationDebugPanel },
|
||||
})
|
||||
export default class NotificationDebugView extends Vue {
|
||||
$router!: Router;
|
||||
readonly isDev: boolean = includeDevToolkitRoutes;
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user