Compare commits
13 Commits
dfedf4e879
..
1.6.5
| Author | SHA1 | Date | |
|---|---|---|---|
| c691400630 | |||
| 6acbfb1181 | |||
| 5f7474140f | |||
| c56d6f4c75 | |||
| c874ae50e5 | |||
| 25e7cc97f9 | |||
| 837e4bc537 | |||
| 65fa93d853 | |||
| 28b0276f17 | |||
| 10ac15f8a2 | |||
| a45633e214 | |||
| a42ba5446a | |||
| ccfe1bded5 |
@@ -2,3 +2,11 @@ node_modules
|
|||||||
android
|
android
|
||||||
ios
|
ios
|
||||||
build
|
build
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Env files (keep .env for build; exclude local overrides)
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
# Env
|
# Env
|
||||||
.env
|
.env
|
||||||
|
.env.local
|
||||||
|
|
||||||
# Vite
|
# Vite
|
||||||
vite.config.js.timestamp-*
|
vite.config.js.timestamp-*
|
||||||
|
|||||||
@@ -1,5 +1,31 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
# 1.6.5
|
||||||
|
|
||||||
|
* Attempt to fix permission grant for notifications
|
||||||
|
* Make sync logic more robust
|
||||||
|
* Add unban/unallow support
|
||||||
|
* Improve support for downloading/opening protected images
|
||||||
|
* Add manual send/receive to wallet
|
||||||
|
* Show wallet status when wallet is unreachable
|
||||||
|
* Update nostr signer capacitor plugin
|
||||||
|
* Fix some safe area insets
|
||||||
|
* Update NIP 55 signer plugin (fixes Primal login)
|
||||||
|
* Refine space join dialogs and discover page
|
||||||
|
* Reopen the last DM that was open when navigating back to chat
|
||||||
|
* Get rid of ChatEnable interstitial
|
||||||
|
* Enable auth for relays we're publishing to
|
||||||
|
* Drag and drop space icons
|
||||||
|
* Add better muting support
|
||||||
|
* Add back button to settings menu
|
||||||
|
* Add page titles
|
||||||
|
* Improve scroll to event behavior
|
||||||
|
* Add in-memory search to rooms
|
||||||
|
* Fix editing messages with html tags
|
||||||
|
* Fix DM media detection
|
||||||
|
* Clean up reporting dialogs
|
||||||
|
* Improve room detail
|
||||||
|
|
||||||
# 1.6.4
|
# 1.6.4
|
||||||
|
|
||||||
* Clean up modal design
|
* Clean up modal design
|
||||||
|
|||||||
+17
-10
@@ -1,24 +1,31 @@
|
|||||||
FROM node:20-slim
|
# Stage 1: Build
|
||||||
|
# Uses .env from build context for config (logo, branding, etc.)
|
||||||
|
# Optional: docker build --build-arg VITE_BUILD_HASH=$(git rev-parse --short HEAD) -t flotilla .
|
||||||
|
|
||||||
|
FROM node:20-bookworm AS builder
|
||||||
|
|
||||||
# Install pnpm
|
|
||||||
RUN npm install -g pnpm@latest
|
RUN npm install -g pnpm@latest
|
||||||
|
|
||||||
# Set working directory
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy package files
|
|
||||||
COPY package.json pnpm-lock.yaml ./
|
COPY package.json pnpm-lock.yaml ./
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
RUN pnpm i
|
RUN pnpm i
|
||||||
|
|
||||||
# Copy the rest of the application
|
# Copy everything (including .env when present) - build.sh will source it
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Build the application
|
ARG VITE_BUILD_HASH
|
||||||
|
ENV VITE_BUILD_HASH=${VITE_BUILD_HASH}
|
||||||
|
|
||||||
ENV NODE_OPTIONS=--max_old_space_size=16384
|
ENV NODE_OPTIONS=--max_old_space_size=16384
|
||||||
RUN pnpm run build
|
RUN pnpm run build
|
||||||
|
|
||||||
# Default to serving the build directory
|
# Stage 2: Runtime
|
||||||
CMD ["npx", "serve", "build"]
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy only the built output - no source, no .env, no dev deps
|
||||||
|
COPY --from=builder /app/build ./build
|
||||||
|
|
||||||
|
CMD ["npx", "serve", "build"]
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ You can also optionally create an `.env` file and populate it with the following
|
|||||||
- `VITE_DEFAULT_PUBKEYS` - A comma-separated list of hex pubkeys for bootstrapping web of trust
|
- `VITE_DEFAULT_PUBKEYS` - A comma-separated list of hex pubkeys for bootstrapping web of trust
|
||||||
- `VITE_PLATFORM_URL` - The url where the app will be hosted
|
- `VITE_PLATFORM_URL` - The url where the app will be hosted
|
||||||
- `VITE_PLATFORM_NAME` - The name of the app
|
- `VITE_PLATFORM_NAME` - The name of the app
|
||||||
- `VITE_PLATFORM_LOGO` - A logo url for the app
|
- `VITE_PLATFORM_LOGO` - A logo url for the app. Can be a local path or https link. Must be a PNG file.
|
||||||
- `VITE_PLATFORM_RELAYS` - A list of comma-separated relay urls that will make flotilla operate in "platform mode". Disables all space browse/add/select functionality and makes the first platform relay the home page.
|
- `VITE_PLATFORM_RELAYS` - A list of comma-separated relay urls that will make flotilla operate in "platform mode". Disables all space browse/add/select functionality and makes the first platform relay the home page.
|
||||||
- `VITE_PLATFORM_ACCENT` - A hex color for the app's accent color
|
- `VITE_PLATFORM_ACCENT` - A hex color for the app's accent color
|
||||||
- `VITE_PLATFORM_DESCRIPTION` - A description of the app
|
- `VITE_PLATFORM_DESCRIPTION` - A description of the app
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ android {
|
|||||||
applicationId "social.flotilla"
|
applicationId "social.flotilla"
|
||||||
minSdk rootProject.ext.minSdkVersion
|
minSdk rootProject.ext.minSdkVersion
|
||||||
targetSdk rootProject.ext.targetSdkVersion
|
targetSdk rootProject.ext.targetSdkVersion
|
||||||
versionCode 40
|
versionCode 41
|
||||||
versionName "1.6.4"
|
versionName "1.6.5"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ temp_env=$(declare -p -x)
|
|||||||
if [ -f .env.template ]; then
|
if [ -f .env.template ]; then
|
||||||
source .env.template
|
source .env.template
|
||||||
fi
|
fi
|
||||||
|
if [ -f .env.local ]; then
|
||||||
|
source .env.local
|
||||||
|
fi
|
||||||
|
|
||||||
# Avoid overwriting env vars provided directly
|
# Avoid overwriting env vars provided directly
|
||||||
# https://stackoverflow.com/a/69127685/1467342
|
# https://stackoverflow.com/a/69127685/1467342
|
||||||
@@ -14,12 +17,13 @@ if [[ -z $VITE_BUILD_HASH ]]; then
|
|||||||
export VITE_BUILD_HASH=$(git rev-parse --short HEAD)
|
export VITE_BUILD_HASH=$(git rev-parse --short HEAD)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $VITE_PLATFORM_LOGO =~ ^https://* ]]; then
|
if [[ $VITE_PLATFORM_LOGO =~ ^https:// ]]; then
|
||||||
curl $VITE_PLATFORM_LOGO > static/logo.png
|
curl -fSL "$VITE_PLATFORM_LOGO" -o static/logo.png
|
||||||
export VITE_PLATFORM_LOGO=static/logo.png
|
export VITE_PLATFORM_LOGO=static/logo.png
|
||||||
fi
|
fi
|
||||||
|
|
||||||
npx pwa-assets-generator
|
# Ensure generator uses local path (dotenv may have loaded URL from .env)
|
||||||
|
VITE_PLATFORM_LOGO="${VITE_PLATFORM_LOGO}" npx pwa-assets-generator
|
||||||
npx vite build
|
npx vite build
|
||||||
|
|
||||||
# Replace index.html variables with stuff from our env
|
# Replace index.html variables with stuff from our env
|
||||||
|
|||||||
@@ -358,14 +358,14 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 31;
|
CURRENT_PROJECT_VERSION = 32;
|
||||||
DEVELOPMENT_TEAM = S26U9DYW3A;
|
DEVELOPMENT_TEAM = S26U9DYW3A;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
MARKETING_VERSION = 1.6.4;
|
MARKETING_VERSION = 1.6.5;
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
@@ -385,14 +385,14 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "Flotilla Chat.entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 31;
|
CURRENT_PROJECT_VERSION = 32;
|
||||||
DEVELOPMENT_TEAM = S26U9DYW3A;
|
DEVELOPMENT_TEAM = S26U9DYW3A;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
MARKETING_VERSION = 1.6.4;
|
MARKETING_VERSION = 1.6.5;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
|||||||
+13
-13
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "flotilla",
|
"name": "flotilla",
|
||||||
"version": "1.6.4",
|
"version": "1.6.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
"tauri:info": "tauri info",
|
"tauri:info": "tauri info",
|
||||||
"tauri:icons": "tauri icon assets/logo.png --output src-tauri/icons",
|
"tauri:icons": "tauri icon assets/logo.png --output src-tauri/icons",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "./check.sh",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
"lint": "prettier --check src && eslint src",
|
"lint": "prettier --check src && eslint src",
|
||||||
"format": "git diff head --name-only --diff-filter d | grep -E '(js|ts|svelte|css)$' | xargs -r prettier --write",
|
"format": "git diff head --name-only --diff-filter d | grep -E '(js|ts|svelte|css)$' | xargs -r prettier --write",
|
||||||
"format:all": "prettier --write src",
|
"format:all": "prettier --write src",
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
"@getalby/lightning-tools": "^6.1.0",
|
"@getalby/lightning-tools": "^6.1.0",
|
||||||
"@getalby/sdk": "^5.1.2",
|
"@getalby/sdk": "^5.1.2",
|
||||||
"@noble/curves": "^1.9.7",
|
"@noble/curves": "^1.9.7",
|
||||||
"@pomade/core": "^0.1.1",
|
"@pomade/core": "^0.2.1",
|
||||||
"@poppanator/sveltekit-svg": "^4.2.1",
|
"@poppanator/sveltekit-svg": "^4.2.1",
|
||||||
"@sveltejs/adapter-static": "^3.0.10",
|
"@sveltejs/adapter-static": "^3.0.10",
|
||||||
"@tiptap/core": "^2.27.2",
|
"@tiptap/core": "^2.27.2",
|
||||||
@@ -65,16 +65,16 @@
|
|||||||
"@types/throttle-debounce": "^5.0.2",
|
"@types/throttle-debounce": "^5.0.2",
|
||||||
"@vite-pwa/assets-generator": "^0.2.6",
|
"@vite-pwa/assets-generator": "^0.2.6",
|
||||||
"@vite-pwa/sveltekit": "^0.6.8",
|
"@vite-pwa/sveltekit": "^0.6.8",
|
||||||
"@welshman/app": "^0.8.7",
|
"@welshman/app": "^0.8.8",
|
||||||
"@welshman/content": "^0.8.7",
|
"@welshman/content": "^0.8.8",
|
||||||
"@welshman/editor": "^0.8.7",
|
"@welshman/editor": "^0.8.8",
|
||||||
"@welshman/feeds": "^0.8.7",
|
"@welshman/feeds": "^0.8.8",
|
||||||
"@welshman/lib": "^0.8.7",
|
"@welshman/lib": "^0.8.8",
|
||||||
"@welshman/net": "^0.8.7",
|
"@welshman/net": "^0.8.8",
|
||||||
"@welshman/router": "^0.8.7",
|
"@welshman/router": "^0.8.8",
|
||||||
"@welshman/signer": "^0.8.7",
|
"@welshman/signer": "^0.8.8",
|
||||||
"@welshman/store": "^0.8.7",
|
"@welshman/store": "^0.8.8",
|
||||||
"@welshman/util": "^0.8.7",
|
"@welshman/util": "^0.8.8",
|
||||||
"compressorjs-next": "^1.1.2",
|
"compressorjs-next": "^1.1.2",
|
||||||
"daisyui": "^4.12.24",
|
"daisyui": "^4.12.24",
|
||||||
"date-picker-svelte": "^2.17.0",
|
"date-picker-svelte": "^2.17.0",
|
||||||
|
|||||||
Generated
+798
-493
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
import dotenv from "dotenv"
|
import dotenv from "dotenv"
|
||||||
import {defineConfig, minimalPreset as preset} from "@vite-pwa/assets-generator/config"
|
import {defineConfig, minimalPreset as preset} from "@vite-pwa/assets-generator/config"
|
||||||
|
|
||||||
dotenv.config({path: ".env"})
|
dotenv.config({path: ".env.local"})
|
||||||
dotenv.config({path: ".env.template"})
|
dotenv.config({path: ".env.template"})
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
|||||||
@@ -15,10 +15,10 @@
|
|||||||
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import StringMultiInput from "@lib/components/StringMultiInput.svelte"
|
||||||
import KeyDownload from "@app/components/KeyDownload.svelte"
|
import KeyDownload from "@app/components/KeyDownload.svelte"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {pushModal, clearModals} from "@app/util/modal"
|
import {pushModal, clearModals} from "@app/util/modal"
|
||||||
import {POMADE_SIGNERS} from "@app/core/state"
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
peersByPrefix: Map<string, string>
|
peersByPrefix: Map<string, string>
|
||||||
@@ -32,18 +32,6 @@
|
|||||||
} = $session as SessionPomade
|
} = $session as SessionPomade
|
||||||
|
|
||||||
const confirmRecovery = async () => {
|
const confirmRecovery = async () => {
|
||||||
const otps = input
|
|
||||||
.split(/\n/)
|
|
||||||
.map(x => x.trim())
|
|
||||||
.filter(x => x.match(/^[0-9]{8}$/))
|
|
||||||
|
|
||||||
if (otps.length < 2) {
|
|
||||||
return pushToast({
|
|
||||||
theme: "error",
|
|
||||||
message: "Failed to recover, not enough valid recovery codes were provided.",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const request = await Client.recoverWithChallenge(email, peersByPrefix, otps)
|
const request = await Client.recoverWithChallenge(email, peersByPrefix, otps)
|
||||||
|
|
||||||
if (!request.ok) {
|
if (!request.ok) {
|
||||||
@@ -82,7 +70,7 @@
|
|||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
let loading = $state(false)
|
let loading = $state(false)
|
||||||
let input = $state("")
|
let otps = $state<string[]>([])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal tag="form" onsubmit={preventDefault(submit)}>
|
<Modal tag="form" onsubmit={preventDefault(submit)}>
|
||||||
@@ -96,17 +84,14 @@
|
|||||||
For security reasons, you may receive three or more emails with recovery codes in them. Please
|
For security reasons, you may receive three or more emails with recovery codes in them. Please
|
||||||
paste <strong>all</strong> recovery codes into the text box below, on separate lines.
|
paste <strong>all</strong> recovery codes into the text box below, on separate lines.
|
||||||
</p>
|
</p>
|
||||||
<textarea
|
<StringMultiInput bind:value={otps} placeholder="Enter your recovery codes..." />
|
||||||
rows={POMADE_SIGNERS.length + 1}
|
|
||||||
class="textarea textarea-bordered leading-4"
|
|
||||||
bind:value={input}></textarea>
|
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button class="btn btn-link" onclick={back}>
|
<Button class="btn btn-link" onclick={back}>
|
||||||
<Icon icon={AltArrowLeft} />
|
<Icon icon={AltArrowLeft} />
|
||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
<Button type="submit" class="btn btn-primary" disabled={loading || otps.length < 2}>
|
||||||
<Spinner {loading}>Confirm recovery</Spinner>
|
<Spinner {loading}>Confirm recovery</Spinner>
|
||||||
<Icon icon={AltArrowRight} />
|
<Icon icon={AltArrowRight} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -34,7 +34,6 @@
|
|||||||
|
|
||||||
const onSuccess = async (session: Session) => {
|
const onSuccess = async (session: Session) => {
|
||||||
addSession(session)
|
addSession(session)
|
||||||
pushToast({message: "Successfully logged in!"})
|
|
||||||
setChecked("*")
|
setChecked("*")
|
||||||
clearModals()
|
clearModals()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {uniq} from "@welshman/lib"
|
||||||
import {Client} from "@pomade/core"
|
import {Client} from "@pomade/core"
|
||||||
import {loginWithPomade} from "@welshman/app"
|
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
@@ -17,6 +17,8 @@
|
|||||||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import LogInOTP from "@app/components/LogInOTP.svelte"
|
import LogInOTP from "@app/components/LogInOTP.svelte"
|
||||||
|
import LogInSelect from "@app/components/LogInSelect.svelte"
|
||||||
|
import {deleteDeactivatedPomadeSessions, loginWithPomade} from "@app/util/pomade"
|
||||||
import {pushModal, clearModals} from "@app/util/modal"
|
import {pushModal, clearModals} from "@app/util/modal"
|
||||||
import {setChecked} from "@app/util/notifications"
|
import {setChecked} from "@app/util/notifications"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
@@ -37,7 +39,7 @@
|
|||||||
try {
|
try {
|
||||||
const {ok, options, messages, clientSecret} = await Client.loginWithPassword(email, password)
|
const {ok, options, messages, clientSecret} = await Client.loginWithPassword(email, password)
|
||||||
|
|
||||||
if (!ok) {
|
if (!ok || options.length === 0) {
|
||||||
console.error(messages)
|
console.error(messages)
|
||||||
|
|
||||||
return pushToast({
|
return pushToast({
|
||||||
@@ -46,21 +48,25 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const [client, peers] = options[0]!
|
if (uniq(options.map(o => o.pubkey)).length > 1) {
|
||||||
const {clientOptions, ...res} = await Client.selectLogin(clientSecret, client, peers)
|
pushModal(LogInSelect, {email, options, clientSecret})
|
||||||
|
|
||||||
if (res.ok && clientOptions) {
|
|
||||||
loginWithPomade(clientOptions.group.group_pk.slice(2), email, clientOptions)
|
|
||||||
pushToast({message: "Successfully logged in!"})
|
|
||||||
setChecked("*")
|
|
||||||
clearModals()
|
|
||||||
} else {
|
} else {
|
||||||
console.error(res.messages)
|
const {client, peers} = options[0]
|
||||||
|
const {clientOptions, ...res} = await Client.selectLogin(clientSecret, client, peers)
|
||||||
|
|
||||||
pushToast({
|
if (res.ok && clientOptions) {
|
||||||
theme: "error",
|
loginWithPomade(clientOptions, email)
|
||||||
message: "Sorry, we were unable to log you in.",
|
deleteDeactivatedPomadeSessions()
|
||||||
})
|
setChecked("*")
|
||||||
|
clearModals()
|
||||||
|
} else {
|
||||||
|
console.error(res.messages)
|
||||||
|
|
||||||
|
pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Sorry, we were unable to log you in.",
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading = false
|
loading = false
|
||||||
|
|||||||
@@ -57,7 +57,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
loginWithNip01(secret)
|
loginWithNip01(secret)
|
||||||
pushToast({message: "Successfully logged in!"})
|
|
||||||
setChecked("*")
|
setChecked("*")
|
||||||
clearModals()
|
clearModals()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {uniq} from "@welshman/lib"
|
||||||
import {Client} from "@pomade/core"
|
import {Client} from "@pomade/core"
|
||||||
import {loginWithPomade} from "@welshman/app"
|
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
@@ -13,10 +13,12 @@
|
|||||||
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import {clearModals} from "@app/util/modal"
|
import StringMultiInput from "@lib/components/StringMultiInput.svelte"
|
||||||
import {setChecked} from "@app/util/notifications"
|
import LogInSelect from "@app/components/LogInSelect.svelte"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {POMADE_SIGNERS} from "@app/core/state"
|
import {setChecked} from "@app/util/notifications"
|
||||||
|
import {pushModal, clearModals} from "@app/util/modal"
|
||||||
|
import {deleteDeactivatedPomadeSessions, loginWithPomade} from "@app/util/pomade"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
email: string
|
email: string
|
||||||
@@ -28,18 +30,6 @@
|
|||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
const otps = input
|
|
||||||
.split(/\n/)
|
|
||||||
.map(x => x.trim())
|
|
||||||
.filter(x => x.match(/^[0-9]{8}$/))
|
|
||||||
|
|
||||||
if (otps.length < 2) {
|
|
||||||
return pushToast({
|
|
||||||
theme: "error",
|
|
||||||
message: "Failed to recover, not enough valid recovery codes were provided.",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
loading = true
|
loading = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -49,7 +39,7 @@
|
|||||||
otps,
|
otps,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!ok) {
|
if (!ok || options.length === 0) {
|
||||||
console.error(messages)
|
console.error(messages)
|
||||||
|
|
||||||
return pushToast({
|
return pushToast({
|
||||||
@@ -58,28 +48,32 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const [client, peers] = options[0]!
|
if (uniq(options.map(o => o.pubkey)).length > 1) {
|
||||||
const {clientOptions, ...res} = await Client.selectLogin(clientSecret, client, peers)
|
pushModal(LogInSelect, {email, options, clientSecret})
|
||||||
|
|
||||||
if (res.ok && clientOptions) {
|
|
||||||
loginWithPomade(clientOptions.group.group_pk.slice(2), email, clientOptions)
|
|
||||||
pushToast({message: "Successfully logged in!"})
|
|
||||||
setChecked("*")
|
|
||||||
clearModals()
|
|
||||||
} else {
|
} else {
|
||||||
console.error(res.messages)
|
const {client, peers} = options[0]
|
||||||
|
const {clientOptions, ...res} = await Client.selectLogin(clientSecret, client, peers)
|
||||||
|
|
||||||
pushToast({
|
if (res.ok && clientOptions) {
|
||||||
theme: "error",
|
loginWithPomade(clientOptions, email)
|
||||||
message: "Sorry, we were unable to log you in.",
|
deleteDeactivatedPomadeSessions()
|
||||||
})
|
setChecked("*")
|
||||||
|
clearModals()
|
||||||
|
} else {
|
||||||
|
console.error(res.messages)
|
||||||
|
|
||||||
|
pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Sorry, we were unable to log you in.",
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let input = $state("")
|
let otps = $state<string[]>([])
|
||||||
let loading = $state(false)
|
let loading = $state(false)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -94,17 +88,14 @@
|
|||||||
For security reasons, you may receive three or more emails with login codes in them. Please
|
For security reasons, you may receive three or more emails with login codes in them. Please
|
||||||
paste <strong>all</strong> login codes into the text box below, on separate lines.
|
paste <strong>all</strong> login codes into the text box below, on separate lines.
|
||||||
</p>
|
</p>
|
||||||
<textarea
|
<StringMultiInput bind:value={otps} placeholder="Enter your login codes..." />
|
||||||
rows={POMADE_SIGNERS.length + 1}
|
|
||||||
class="textarea textarea-bordered leading-4"
|
|
||||||
bind:value={input}></textarea>
|
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button class="btn btn-link" onclick={back} disabled={loading}>
|
<Button class="btn btn-link" onclick={back} disabled={loading}>
|
||||||
<Icon icon={AltArrowLeft} />
|
<Icon icon={AltArrowLeft} />
|
||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
<Button type="submit" class="btn btn-primary" disabled={loading || otps.length < 3}>
|
||||||
<Spinner {loading}>Log In</Spinner>
|
<Spinner {loading}>Log In</Spinner>
|
||||||
<Icon icon={AltArrowRight} />
|
<Icon icon={AltArrowRight} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type {AccountOption} from "@pomade/core"
|
||||||
|
import {Client} from "@pomade/core"
|
||||||
|
import {uniqBy} from "@welshman/lib"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Modal from "@lib/components/Modal.svelte"
|
||||||
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||||
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import Profile from "@app/components/Profile.svelte"
|
||||||
|
import {deleteDeactivatedPomadeSessions, loginWithPomade} from "@app/util/pomade"
|
||||||
|
import {setChecked} from "@app/util/notifications"
|
||||||
|
import {clearModals} from "@app/util/modal"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
email: string
|
||||||
|
options: AccountOption[]
|
||||||
|
clientSecret: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {email, options, clientSecret}: Props = $props()
|
||||||
|
|
||||||
|
let loading = $state(false)
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const selectAccount = async ({client, peers}: AccountOption) => {
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {clientOptions, ...res} = await Client.selectLogin(clientSecret, client, peers)
|
||||||
|
|
||||||
|
if (res.ok && clientOptions) {
|
||||||
|
loginWithPomade(clientOptions, email)
|
||||||
|
deleteDeactivatedPomadeSessions()
|
||||||
|
setChecked("*")
|
||||||
|
clearModals()
|
||||||
|
} else {
|
||||||
|
console.error(res.messages)
|
||||||
|
|
||||||
|
pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Sorry, we were unable to log you in.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal>
|
||||||
|
<ModalBody>
|
||||||
|
<ModalHeader>
|
||||||
|
<ModalTitle>Select Account</ModalTitle>
|
||||||
|
<ModalSubtitle
|
||||||
|
>Multiple accounts are associated with {email}. Please select one to continue.</ModalSubtitle>
|
||||||
|
</ModalHeader>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
{#each uniqBy(o => o.pubkey, options) as option (option.pubkey)}
|
||||||
|
<Button
|
||||||
|
onclick={() => selectAccount(option)}
|
||||||
|
disabled={loading}
|
||||||
|
class="card2 bg-alt flex w-full items-center p-3 text-left">
|
||||||
|
<Profile pubkey={option.pubkey} />
|
||||||
|
</Button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back} disabled={loading}>
|
||||||
|
<Icon icon={AltArrowLeft} />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Spinner {loading} />
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
@@ -9,8 +9,7 @@
|
|||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import {Push} from "@app/util/notifications"
|
import {logout} from "@app/util/logout"
|
||||||
import {kv, db} from "@app/core/storage"
|
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
@@ -18,13 +17,7 @@
|
|||||||
loading = true
|
loading = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Push.disable()
|
await logout()
|
||||||
await kv.clear()
|
|
||||||
await db.clear()
|
|
||||||
|
|
||||||
localStorage.clear()
|
|
||||||
|
|
||||||
window.location.href = "/"
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
loading = false
|
loading = false
|
||||||
|
|||||||
@@ -22,8 +22,8 @@
|
|||||||
{#if props.event.content}
|
{#if props.event.content}
|
||||||
<Content {...props} />
|
<Content {...props} />
|
||||||
{/if}
|
{/if}
|
||||||
<div class="grid grid-cols-3 sm:grid-cols-5 lg:grid-cols-9">
|
<div class="grid grid-cols-3 sm:grid-cols-5 lg:grid-cols-9 gap-2">
|
||||||
{#each images as image (image)}
|
{#each images as image, i (i + image)}
|
||||||
<ContentLinkBlock event={props.event} value={{url: image}} />
|
<ContentLinkBlock event={props.event} value={{url: image}} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {Client} from "@pomade/core"
|
||||||
|
import type {SessionPomade} from "@welshman/app"
|
||||||
|
import {session} from "@welshman/app"
|
||||||
|
import {preventDefault} from "@lib/html"
|
||||||
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
|
import Modal from "@lib/components/Modal.svelte"
|
||||||
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||||
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import StringMultiInput from "@lib/components/StringMultiInput.svelte"
|
||||||
|
import PasswordResetConfirm from "@app/components/PasswordResetConfirm.svelte"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
import {pushModal} from "@app/util/modal"
|
||||||
|
import {getPomadeClient} from "@app/util/pomade"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
peersByPrefix: Map<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
const {peersByPrefix}: Props = $props()
|
||||||
|
|
||||||
|
const {email} = $session as SessionPomade
|
||||||
|
|
||||||
|
const confirmRecovery = async () => {
|
||||||
|
const request = await Client.recoverWithChallenge(email, peersByPrefix, otps)
|
||||||
|
|
||||||
|
if (!request.ok) {
|
||||||
|
console.log(request.messages)
|
||||||
|
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: `Failed to validate email ownership: ${request.messages[0]?.res?.message.toLowerCase()}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = await getPomadeClient()
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
throw new Error("Unable to get client during password reset flow")
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await Client.selectRecovery(
|
||||||
|
request.clientSecret,
|
||||||
|
await client.getPubkey(),
|
||||||
|
client.peers,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
console.log(result.messages)
|
||||||
|
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: `Failed to validate email ownership: ${result.messages[0]?.res?.message.toLowerCase()}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pushModal(PasswordResetConfirm, {userSecret: result.userSecret})
|
||||||
|
}
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await confirmRecovery()
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
let loading = $state(false)
|
||||||
|
let otps = $state<string[]>([])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal tag="form" onsubmit={preventDefault(submit)}>
|
||||||
|
<ModalBody>
|
||||||
|
<ModalHeader>
|
||||||
|
<ModalTitle>Update your Password</ModalTitle>
|
||||||
|
<ModalSubtitle>Confirm your Email</ModalSubtitle>
|
||||||
|
</ModalHeader>
|
||||||
|
<p>Let's start by confirming your email.</p>
|
||||||
|
<p>
|
||||||
|
For security reasons, you may receive three or more emails with confirmation codes in them.
|
||||||
|
Please paste <strong>all</strong> confirmation codes into the text box below, on separate lines.
|
||||||
|
</p>
|
||||||
|
<StringMultiInput bind:value={otps} placeholder="Enter your confirmation codes..." />
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
<Icon icon={AltArrowLeft} />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" class="btn btn-primary" disabled={loading || otps.length < 2}>
|
||||||
|
<Spinner {loading}>Continue</Spinner>
|
||||||
|
<Icon icon={AltArrowRight} />
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {Client} from "@pomade/core"
|
||||||
|
import {session} from "@welshman/app"
|
||||||
|
import type {SessionPomade} from "@welshman/app"
|
||||||
|
import {preventDefault} from "@lib/html"
|
||||||
|
import Key from "@assets/icons/key.svg?dataurl"
|
||||||
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||||
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Modal from "@lib/components/Modal.svelte"
|
||||||
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||||
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import {loginWithPomade, deleteCurrentPomadeSession} from "@app/util/pomade"
|
||||||
|
import {clearModals} from "@app/util/modal"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
userSecret: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {userSecret}: Props = $props()
|
||||||
|
|
||||||
|
const {email} = $session as SessionPomade
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
if (password.trim().length < 12) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Password must be at least 12 characters long.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
pushToast({
|
||||||
|
timeout: 60_000,
|
||||||
|
message: "Registering your new password, please wait...",
|
||||||
|
})
|
||||||
|
|
||||||
|
const {clientOptions, ...registerRes} = await Client.register(2, 3, userSecret)
|
||||||
|
|
||||||
|
if (!registerRes.ok) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Failed to register your new password! Please try again.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const setupRes = await new Client(clientOptions).setupRecovery(email, password)
|
||||||
|
|
||||||
|
if (!setupRes.ok) {
|
||||||
|
const message = setupRes.messages[0]?.res?.message || "Please try again."
|
||||||
|
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: `Failed to register your new password! ${message}.`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteCurrentPomadeSession()
|
||||||
|
|
||||||
|
pushToast({message: "Your password has been updated!"})
|
||||||
|
loginWithPomade(clientOptions, email)
|
||||||
|
clearModals()
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
|
||||||
|
pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Failed to register your new password! Please try again.",
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let password = $state("")
|
||||||
|
let loading = $state(false)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal tag="form" onsubmit={preventDefault(onSubmit)}>
|
||||||
|
<ModalBody>
|
||||||
|
<ModalHeader>
|
||||||
|
<ModalTitle>Update your Password</ModalTitle>
|
||||||
|
<ModalSubtitle>Please provide your new password.</ModalSubtitle>
|
||||||
|
</ModalHeader>
|
||||||
|
<FieldInline>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>New Password*</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
|
<Icon icon={Key} />
|
||||||
|
<input type="password" bind:value={password} />
|
||||||
|
</label>
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
<Icon icon={AltArrowLeft} />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button class="btn btn-primary" type="submit" disabled={loading || !password}>
|
||||||
|
<Spinner {loading}>Continue</Spinner>
|
||||||
|
<Icon icon={AltArrowRight} />
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
@@ -1,51 +1,30 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {onMount} from "svelte"
|
||||||
import {Client} from "@pomade/core"
|
import {Client} from "@pomade/core"
|
||||||
import type {SessionItem} from "@pomade/core"
|
|
||||||
import {session, isPomadeSession} from "@welshman/app"
|
import {session, isPomadeSession} from "@welshman/app"
|
||||||
|
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
||||||
|
import {fly} from "@lib/transition"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Popover from "@lib/components/Popover.svelte"
|
||||||
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {onMount} from "svelte"
|
import {loadOtherPomadeSessions} from "@app/util/pomade"
|
||||||
|
import type {PomadeSessionWithPeers} from "@app/util/pomade"
|
||||||
|
|
||||||
type SessionWithPeers = SessionItem & {peers: string[]}
|
const toggleMenu = (client: string) => {
|
||||||
|
menuClient = menuClient === client ? "" : client
|
||||||
let sessions = $state<SessionWithPeers[]>([])
|
|
||||||
let deletingSession = $state<string | null>(null)
|
|
||||||
|
|
||||||
const loadSessions = async () => {
|
|
||||||
if (!isPomadeSession($session)) return
|
|
||||||
|
|
||||||
const client = new Client($session.clientOptions)
|
|
||||||
const result = await client.listSessions()
|
|
||||||
const pubkey = await client.getPubkey()
|
|
||||||
|
|
||||||
if (result.ok) {
|
|
||||||
// Group sessions by client pubkey and collect peers
|
|
||||||
const sessionMap = new Map<string, SessionWithPeers>()
|
|
||||||
|
|
||||||
for (const message of result.messages) {
|
|
||||||
if (!message.res?.items) continue
|
|
||||||
|
|
||||||
for (const item of message.res.items) {
|
|
||||||
const existing = sessionMap.get(item.client)
|
|
||||||
|
|
||||||
if (existing) {
|
|
||||||
existing.peers.push(message.url)
|
|
||||||
} else if (item.client !== pubkey) {
|
|
||||||
sessionMap.set(item.client, {...item, peers: [message.url]})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sessions = Array.from(sessionMap.values())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteSession = async (sessionItem: SessionWithPeers) => {
|
const closeMenu = () => {
|
||||||
if (!isPomadeSession($session)) return
|
menuClient = ""
|
||||||
|
}
|
||||||
|
|
||||||
deletingSession = sessionItem.client
|
let menuClient = $state("")
|
||||||
|
let sessions = $state<PomadeSessionWithPeers[]>([])
|
||||||
|
|
||||||
|
const deleteSession = async (sessionItem: PomadeSessionWithPeers) => {
|
||||||
|
if (!isPomadeSession($session)) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = new Client($session.clientOptions)
|
const client = new Client($session.clientOptions)
|
||||||
@@ -70,8 +49,6 @@
|
|||||||
theme: "error",
|
theme: "error",
|
||||||
message: "Failed to delete session",
|
message: "Failed to delete session",
|
||||||
})
|
})
|
||||||
} finally {
|
|
||||||
deletingSession = null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +62,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
loadSessions()
|
loadOtherPomadeSessions().then(_sessions => {
|
||||||
|
sessions = _sessions || []
|
||||||
|
})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -93,28 +72,46 @@
|
|||||||
<div class="flex flex-col gap-4 border-t border-solid border-base-100 pt-4">
|
<div class="flex flex-col gap-4 border-t border-solid border-base-100 pt-4">
|
||||||
<strong>Other Sessions</strong>
|
<strong>Other Sessions</strong>
|
||||||
{#each sessions as sessionItem (sessionItem.client)}
|
{#each sessions as sessionItem (sessionItem.client)}
|
||||||
<div class="flex justify-between text-sm">
|
<div class="flex flex-col gap-2">
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex justify-between items-center">
|
||||||
<span>{sessionItem.client.slice(0, 8)}</span>
|
<div class="flex gap-3 items-center text-sm">
|
||||||
<div class="flex gap-1">
|
<span>Session {sessionItem.client.slice(0, 8)}</span>
|
||||||
<div class="badge badge-neutral">
|
<span class="opacity-75">
|
||||||
Created {formatDate(sessionItem.created_at)}
|
{#if sessionItem.deactivated_at}
|
||||||
</div>
|
Deactivated
|
||||||
<div class="badge badge-neutral">
|
{/if}
|
||||||
Last active: {formatDate(sessionItem.last_activity)}
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="relative">
|
||||||
|
<Button
|
||||||
|
class="btn btn-circle btn-ghost btn-sm"
|
||||||
|
onclick={() => toggleMenu(sessionItem.client)}>
|
||||||
|
<Icon icon={MenuDots} />
|
||||||
|
</Button>
|
||||||
|
{#if menuClient === sessionItem.client}
|
||||||
|
<Popover hideOnClick onClose={closeMenu}>
|
||||||
|
<ul
|
||||||
|
transition:fly
|
||||||
|
class="menu absolute right-0 z-popover mt-2 w-48 gap-1 rounded-box bg-base-100 p-2 shadow-md">
|
||||||
|
<li>
|
||||||
|
<Button onclick={() => deleteSession(sessionItem)}>
|
||||||
|
<Icon icon={TrashBin2} />
|
||||||
|
Delete Session
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Popover>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<div class="badge badge-neutral">
|
||||||
|
Created {formatDate(sessionItem.created_at)}
|
||||||
|
</div>
|
||||||
|
<div class="badge badge-neutral">
|
||||||
|
Active {formatDate(sessionItem.last_activity)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
class="btn btn-error btn-sm"
|
|
||||||
disabled={deletingSession !== null}
|
|
||||||
onclick={() => deleteSession(sessionItem)}>
|
|
||||||
{#if deletingSession === sessionItem.client}
|
|
||||||
<span class="loading loading-spinner"></span>
|
|
||||||
{:else}
|
|
||||||
<Icon icon={TrashBin2} />
|
|
||||||
{/if}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,9 +23,8 @@
|
|||||||
import Modal from "@lib/components/Modal.svelte"
|
import Modal from "@lib/components/Modal.svelte"
|
||||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
import {INDEXER_RELAYS, PLATFORM_NAME, userSpaceUrls} from "@app/core/state"
|
import {INDEXER_RELAYS, PLATFORM_NAME, userSpaceUrls} from "@app/core/state"
|
||||||
import {kv, db} from "@app/core/storage"
|
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {Push} from "@app/util/notifications"
|
import {logout} from "@app/util/logout"
|
||||||
|
|
||||||
let progress: number | undefined = $state(undefined)
|
let progress: number | undefined = $state(undefined)
|
||||||
let confirmText = $state("")
|
let confirmText = $state("")
|
||||||
@@ -88,13 +87,7 @@
|
|||||||
await sleep(2000)
|
await sleep(2000)
|
||||||
|
|
||||||
// Goodbye forever!
|
// Goodbye forever!
|
||||||
await Push.disable()
|
await logout()
|
||||||
await kv.clear()
|
|
||||||
await db.clear()
|
|
||||||
|
|
||||||
localStorage.clear()
|
|
||||||
|
|
||||||
window.location.href = "/"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirm = async () => {
|
const confirm = async () => {
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {Profile} from "@welshman/util"
|
import type {Profile} from "@welshman/util"
|
||||||
import {getTag, makeProfile} from "@welshman/util"
|
import {getTag, makeProfile} from "@welshman/util"
|
||||||
import {pubkey, profilesByPubkey} from "@welshman/app"
|
import {pubkey, profilesByPubkey, waitForThunkError} from "@welshman/app"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
import {errorMessage} from "@lib/util"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import ProfileEditForm from "@app/components/ProfileEditForm.svelte"
|
import ProfileEditForm from "@app/components/ProfileEditForm.svelte"
|
||||||
import {clearModals} from "@app/util/modal"
|
import {clearModals} from "@app/util/modal"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
@@ -17,11 +19,33 @@
|
|||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const onsubmit = ({profile, shouldBroadcast}: {profile: Profile; shouldBroadcast: boolean}) => {
|
const onsubmit = async ({
|
||||||
updateProfile({profile, shouldBroadcast})
|
profile,
|
||||||
pushToast({message: "Your profile has been updated!"})
|
shouldBroadcast,
|
||||||
clearModals()
|
}: {
|
||||||
|
profile: Profile
|
||||||
|
shouldBroadcast: boolean
|
||||||
|
}) => {
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const error = await waitForThunkError(updateProfile({profile, shouldBroadcast}))
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: `Failed to update your profile: ${errorMessage(error)}`,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
pushToast({message: "Your profile has been updated!"})
|
||||||
|
clearModals()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let loading = $state(false)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ProfileEditForm {initialValues} {onsubmit}>
|
<ProfileEditForm {initialValues} {onsubmit}>
|
||||||
@@ -30,6 +54,9 @@
|
|||||||
<Icon icon={AltArrowLeft} />
|
<Icon icon={AltArrowLeft} />
|
||||||
Go Back
|
Go Back
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" class="btn btn-primary">Save Changes</Button>
|
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||||
|
<Spinner {loading} />
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</ProfileEditForm>
|
</ProfileEditForm>
|
||||||
|
|||||||
@@ -1,15 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {ClientOptions} from "@pomade/core"
|
import type {ClientOptions} from "@pomade/core"
|
||||||
import type {Profile} from "@welshman/util"
|
import type {Profile} from "@welshman/util"
|
||||||
import {
|
import {makeProfile, makeSecret, RELAYS, MESSAGING_RELAYS, makeEvent} from "@welshman/util"
|
||||||
makeProfile,
|
import {loginWithNip01, publishThunk} from "@welshman/app"
|
||||||
makeSecret,
|
|
||||||
getPubkey,
|
|
||||||
RELAYS,
|
|
||||||
MESSAGING_RELAYS,
|
|
||||||
makeEvent,
|
|
||||||
} from "@welshman/util"
|
|
||||||
import {loginWithNip01, loginWithPomade, publishThunk} from "@welshman/app"
|
|
||||||
import Key from "@assets/icons/key-minimalistic.svg?dataurl"
|
import Key from "@assets/icons/key-minimalistic.svg?dataurl"
|
||||||
import Letter from "@assets/icons/letter.svg?dataurl"
|
import Letter from "@assets/icons/letter.svg?dataurl"
|
||||||
import {getKey, setKey} from "@lib/implicit"
|
import {getKey, setKey} from "@lib/implicit"
|
||||||
@@ -23,8 +16,6 @@
|
|||||||
import SignUpEmail from "@app/components/SignUpEmail.svelte"
|
import SignUpEmail from "@app/components/SignUpEmail.svelte"
|
||||||
import SignUpProfile from "@app/components/SignUpProfile.svelte"
|
import SignUpProfile from "@app/components/SignUpProfile.svelte"
|
||||||
import SignUpComplete from "@app/components/SignUpComplete.svelte"
|
import SignUpComplete from "@app/components/SignUpComplete.svelte"
|
||||||
import {setChecked} from "@app/util/notifications"
|
|
||||||
import {pushModal, clearModals} from "@app/util/modal"
|
|
||||||
import {initProfile} from "@app/core/commands"
|
import {initProfile} from "@app/core/commands"
|
||||||
import {
|
import {
|
||||||
POMADE_SIGNERS,
|
POMADE_SIGNERS,
|
||||||
@@ -33,6 +24,9 @@
|
|||||||
DEFAULT_RELAYS,
|
DEFAULT_RELAYS,
|
||||||
DEFAULT_MESSAGING_RELAYS,
|
DEFAULT_MESSAGING_RELAYS,
|
||||||
} from "@app/core/state"
|
} from "@app/core/state"
|
||||||
|
import {setChecked} from "@app/util/notifications"
|
||||||
|
import {loginWithPomade} from "@app/util/pomade"
|
||||||
|
import {pushModal, clearModals} from "@app/util/modal"
|
||||||
|
|
||||||
setKey("signup.email", "")
|
setKey("signup.email", "")
|
||||||
setKey("signup.secret", makeSecret())
|
setKey("signup.secret", makeSecret())
|
||||||
@@ -73,10 +67,9 @@
|
|||||||
complete: () => pushModal(SignUpComplete, {next: flows.email.finalize}),
|
complete: () => pushModal(SignUpComplete, {next: flows.email.finalize}),
|
||||||
finalize: () => {
|
finalize: () => {
|
||||||
const email = getKey<string>("signup.email")!
|
const email = getKey<string>("signup.email")!
|
||||||
const secret = getKey<string>("signup.secret")!
|
|
||||||
const clientOptions = getKey<ClientOptions>("signup.clientOptions")!
|
const clientOptions = getKey<ClientOptions>("signup.clientOptions")!
|
||||||
|
|
||||||
loginWithPomade(getPubkey(secret), email, clientOptions)
|
loginWithPomade(clientOptions, email)
|
||||||
completeSignup()
|
completeSignup()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import SignUpEmailConfirm from "@app/components/SignUpEmailConfirm.svelte"
|
import SignUpEmailConfirm from "@app/components/SignUpEmailConfirm.svelte"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast, popToast} from "@app/util/toast"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -40,6 +40,11 @@
|
|||||||
loading = true
|
loading = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const toastId = pushToast({
|
||||||
|
timeout: 60_000,
|
||||||
|
message: "Creating your account, please wait...",
|
||||||
|
})
|
||||||
|
|
||||||
const secret = getKey<string>("signup.secret")!
|
const secret = getKey<string>("signup.secret")!
|
||||||
const {clientOptions, ...registerRes} = await Client.register(2, 3, secret)
|
const {clientOptions, ...registerRes} = await Client.register(2, 3, secret)
|
||||||
|
|
||||||
@@ -54,7 +59,8 @@
|
|||||||
const setupRes = await client.setupRecovery(email, password)
|
const setupRes = await client.setupRecovery(email, password)
|
||||||
|
|
||||||
if (!setupRes.ok) {
|
if (!setupRes.ok) {
|
||||||
const message = setupRes.messages[0]?.res?.message || "Please try again."
|
const message =
|
||||||
|
setupRes.messages.find(m => m.res && !m.res?.ok)?.res?.message || "Please try again."
|
||||||
|
|
||||||
return pushToast({
|
return pushToast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
@@ -74,6 +80,7 @@
|
|||||||
setKey("signup.email", email)
|
setKey("signup.email", email)
|
||||||
setKey("signup.clientOptions", clientOptions)
|
setKey("signup.clientOptions", clientOptions)
|
||||||
|
|
||||||
|
popToast(toastId)
|
||||||
pushModal(SignUpEmailConfirm, {next})
|
pushModal(SignUpEmailConfirm, {next})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
|
|||||||
@@ -256,7 +256,7 @@
|
|||||||
<SecondaryNavHeader>Your Rooms</SecondaryNavHeader>
|
<SecondaryNavHeader>Your Rooms</SecondaryNavHeader>
|
||||||
{/if}
|
{/if}
|
||||||
{#each $userRooms as h, i (h)}
|
{#each $userRooms as h, i (h)}
|
||||||
<SpaceMenuRoomItem {replaceState} {url} {h} />
|
<SpaceMenuRoomItem notify {replaceState} {url} {h} />
|
||||||
{/each}
|
{/each}
|
||||||
{#if $otherRooms.length > 0}
|
{#if $otherRooms.length > 0}
|
||||||
<div class="h-2"></div>
|
<div class="h-2"></div>
|
||||||
|
|||||||
@@ -3,11 +3,15 @@
|
|||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import SpaceMenu from "@app/components/SpaceMenu.svelte"
|
import SpaceMenu from "@app/components/SpaceMenu.svelte"
|
||||||
|
import {notifications} from "@app/util/notifications"
|
||||||
|
import {makeSpacePath} from "@app/util/routes"
|
||||||
import {pushDrawer} from "@app/util/modal"
|
import {pushDrawer} from "@app/util/modal"
|
||||||
import {deriveSocketStatus} from "@app/core/state"
|
import {deriveSocketStatus} from "@app/core/state"
|
||||||
|
|
||||||
const {url} = $props()
|
const {url} = $props()
|
||||||
|
|
||||||
|
const path = makeSpacePath(url) + ":mobile"
|
||||||
|
|
||||||
const status = deriveSocketStatus(url)
|
const status = deriveSocketStatus(url)
|
||||||
|
|
||||||
const openMenu = () => pushDrawer(SpaceMenu, {url})
|
const openMenu = () => pushDrawer(SpaceMenu, {url})
|
||||||
@@ -17,5 +21,7 @@
|
|||||||
<Icon icon={MenuDots} />
|
<Icon icon={MenuDots} />
|
||||||
{#if $status.theme !== "success"}
|
{#if $status.theme !== "success"}
|
||||||
<div class="absolute right-0 top-0 -mr-1 -mt-1 h-2 w-2 rounded-full bg-{$status.theme}"></div>
|
<div class="absolute right-0 top-0 -mr-1 -mt-1 h-2 w-2 rounded-full bg-{$status.theme}"></div>
|
||||||
|
{:else if $notifications.has(path)}
|
||||||
|
<div class="absolute right-0 top-0 -mr-1 -mt-1 h-2 w-2 rounded-full bg-primary"></div>
|
||||||
{/if}
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -4,16 +4,18 @@
|
|||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
|
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
|
||||||
import RoomNameWithImage from "@app/components/RoomNameWithImage.svelte"
|
import RoomNameWithImage from "@app/components/RoomNameWithImage.svelte"
|
||||||
|
import {notifications} from "@app/util/notifications"
|
||||||
import {makeRoomPath} from "@app/util/routes"
|
import {makeRoomPath} from "@app/util/routes"
|
||||||
import {deriveShouldNotify} from "@app/core/state"
|
import {deriveShouldNotify} from "@app/core/state"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
url: any
|
url: any
|
||||||
h: any
|
h: any
|
||||||
|
notify?: boolean
|
||||||
replaceState?: boolean
|
replaceState?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const {url, h, replaceState = false}: Props = $props()
|
const {url, h, notify = false, replaceState = false}: Props = $props()
|
||||||
|
|
||||||
const path = makeRoomPath(url, h)
|
const path = makeRoomPath(url, h)
|
||||||
const shouldNotifyForSpace = deriveShouldNotify(url)
|
const shouldNotifyForSpace = deriveShouldNotify(url)
|
||||||
@@ -21,7 +23,10 @@
|
|||||||
const showDifferenceIcon = $derived($shouldNotifyForRoom !== $shouldNotifyForSpace)
|
const showDifferenceIcon = $derived($shouldNotifyForRoom !== $shouldNotifyForSpace)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SecondaryNavItem href={path} {replaceState}>
|
<SecondaryNavItem
|
||||||
|
href={path}
|
||||||
|
{replaceState}
|
||||||
|
notification={notify ? $notifications.has(path) : false}>
|
||||||
<RoomNameWithImage {url} {h} />
|
<RoomNameWithImage {url} {h} />
|
||||||
{#if showDifferenceIcon}
|
{#if showDifferenceIcon}
|
||||||
<Icon icon={$shouldNotifyForRoom ? VolumeLoud : VolumeCross} size={4} class="opacity-50" />
|
<Icon icon={$shouldNotifyForRoom ? VolumeLoud : VolumeCross} size={4} class="opacity-50" />
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {makeProfile} from "@welshman/util"
|
||||||
import {getWalletAddress} from "@welshman/util"
|
import {getWalletAddress} from "@welshman/util"
|
||||||
|
import {userProfile, waitForThunkError, session} from "@welshman/app"
|
||||||
|
import {errorMessage} from "@lib/util"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import Modal from "@lib/components/Modal.svelte"
|
import Modal from "@lib/components/Modal.svelte"
|
||||||
@@ -9,8 +12,7 @@
|
|||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import {updateProfile} from "@app/core/commands"
|
import {updateProfile} from "@app/core/commands"
|
||||||
import {clearModals} from "@app/util/modal"
|
import {clearModals} from "@app/util/modal"
|
||||||
import {userProfile, session} from "@welshman/app"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {makeProfile} from "@welshman/util"
|
|
||||||
|
|
||||||
const lud16 = getWalletAddress($session!.wallet!)
|
const lud16 = getWalletAddress($session!.wallet!)
|
||||||
|
|
||||||
@@ -20,9 +22,13 @@
|
|||||||
loading = true
|
loading = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await updateProfile({profile: {...profile, lud16}})
|
const error = await waitForThunkError(updateProfile({profile: {...profile, lud16}}))
|
||||||
|
|
||||||
clearModals()
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: `Failed to update profile: ${errorMessage(error)}`})
|
||||||
|
} else {
|
||||||
|
clearModals()
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {getWalletAddress} from "@welshman/util"
|
import {getWalletAddress} from "@welshman/util"
|
||||||
import {session, userProfile} from "@welshman/app"
|
import {session, waitForThunkError, userProfile} from "@welshman/app"
|
||||||
|
import {errorMessage} from "@lib/util"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
@@ -17,7 +18,7 @@
|
|||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
let address = $state($userProfile?.lud16 || "")
|
let address = $state($userProfile?.lud16 || "")
|
||||||
let isLoading = $state(false)
|
let loading = $state(false)
|
||||||
|
|
||||||
const walletLud16 = $derived($session?.wallet ? getWalletAddress($session.wallet) : undefined)
|
const walletLud16 = $derived($session?.wallet ? getWalletAddress($session.wallet) : undefined)
|
||||||
|
|
||||||
@@ -28,20 +29,28 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const save = async () => {
|
const save = async () => {
|
||||||
isLoading = true
|
loading = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await updateProfile({
|
const error = await waitForThunkError(
|
||||||
profile: {
|
updateProfile({
|
||||||
...$userProfile,
|
profile: {
|
||||||
lud06: undefined,
|
...$userProfile,
|
||||||
lud16: address.trim() || undefined,
|
lud06: undefined,
|
||||||
},
|
lud16: address.trim() || undefined,
|
||||||
})
|
},
|
||||||
back()
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: `Failed to update profile: ${errorMessage(error)}`})
|
||||||
|
} else {
|
||||||
|
back()
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
pushToast({theme: "error", message: "Failed to update profile"})
|
pushToast({theme: "error", message: "Failed to update profile"})
|
||||||
} finally {
|
} finally {
|
||||||
isLoading = false
|
loading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -61,7 +70,7 @@
|
|||||||
placeholder="user@domain.com"
|
placeholder="user@domain.com"
|
||||||
bind:value={address}
|
bind:value={address}
|
||||||
class="input input-bordered flex w-full"
|
class="input input-bordered flex w-full"
|
||||||
disabled={isLoading} />
|
disabled={loading} />
|
||||||
<p class="text-xs opacity-75">
|
<p class="text-xs opacity-75">
|
||||||
You can enter one manually or use your connected wallet's address (if available). Leave
|
You can enter one manually or use your connected wallet's address (if available). Leave
|
||||||
empty to remove your lightning address
|
empty to remove your lightning address
|
||||||
@@ -78,7 +87,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<p class="text-xs opacity-75">{walletLud16}</p>
|
<p class="text-xs opacity-75">{walletLud16}</p>
|
||||||
</div>
|
</div>
|
||||||
<Button class="btn btn-outline btn-sm" onclick={useWalletAddress} disabled={isLoading}>
|
<Button class="btn btn-outline btn-sm" onclick={useWalletAddress} disabled={loading}>
|
||||||
Use This
|
Use This
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -87,9 +96,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button class="btn btn-neutral" onclick={back} disabled={isLoading}>Cancel</Button>
|
<Button class="btn btn-neutral" onclick={back} disabled={loading}>Cancel</Button>
|
||||||
<Button class="btn btn-primary" onclick={save} disabled={isLoading}>
|
<Button class="btn btn-primary" onclick={save} disabled={loading}>
|
||||||
{#if isLoading}
|
{#if loading}
|
||||||
<span class="loading loading-spinner loading-sm"></span>
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
{:else}
|
{:else}
|
||||||
<Icon icon={CheckCircle} />
|
<Icon icon={CheckCircle} />
|
||||||
|
|||||||
@@ -676,7 +676,7 @@ export const initProfile = (profile: Profile) => {
|
|||||||
return publishThunk({event, relays: []})
|
return publishThunk({event, relays: []})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const updateProfile = async ({
|
export const updateProfile = ({
|
||||||
profile,
|
profile,
|
||||||
shouldBroadcast = !getTag(PROTECTED, profile.event?.tags || []),
|
shouldBroadcast = !getTag(PROTECTED, profile.event?.tags || []),
|
||||||
}: {
|
}: {
|
||||||
@@ -697,5 +697,5 @@ export const updateProfile = async ({
|
|||||||
const event = makeEvent(template.kind, template)
|
const event = makeEvent(template.kind, template)
|
||||||
const relays = router.merge(scenarios).getUrls()
|
const relays = router.merge(scenarios).getUrls()
|
||||||
|
|
||||||
await publishThunk({event, relays}).complete
|
return publishThunk({event, relays})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import {kv, db} from "@app/core/storage"
|
||||||
|
import {Push} from "@app/util/notifications"
|
||||||
|
import {deactivateCurrentPomadeSession} from "@app/util/pomade"
|
||||||
|
|
||||||
|
export const logout = async () => {
|
||||||
|
await deactivateCurrentPomadeSession()
|
||||||
|
await Push.disable()
|
||||||
|
await kv.clear()
|
||||||
|
await db.clear()
|
||||||
|
|
||||||
|
localStorage.clear()
|
||||||
|
|
||||||
|
window.location.href = "/"
|
||||||
|
}
|
||||||
@@ -12,12 +12,14 @@ import {
|
|||||||
repository,
|
repository,
|
||||||
publishThunk,
|
publishThunk,
|
||||||
loadRelay,
|
loadRelay,
|
||||||
|
relaysByUrl,
|
||||||
waitForThunkError,
|
waitForThunkError,
|
||||||
userMessagingRelayList,
|
userMessagingRelayList,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
import {
|
import {
|
||||||
on,
|
on,
|
||||||
call,
|
call,
|
||||||
|
find,
|
||||||
assoc,
|
assoc,
|
||||||
poll,
|
poll,
|
||||||
prop,
|
prop,
|
||||||
@@ -44,7 +46,14 @@ import {
|
|||||||
Address,
|
Address,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import {buildUrl} from "@lib/util"
|
import {buildUrl} from "@lib/util"
|
||||||
import {makeSpacePath, makeChatPath, getEventPath, goToEvent} from "@app/util/routes"
|
import {
|
||||||
|
makeSpacePath,
|
||||||
|
makeRoomPath,
|
||||||
|
makeSpaceChatPath,
|
||||||
|
makeChatPath,
|
||||||
|
getEventPath,
|
||||||
|
goToEvent,
|
||||||
|
} from "@app/util/routes"
|
||||||
import {
|
import {
|
||||||
DM_KINDS,
|
DM_KINDS,
|
||||||
CONTENT_KINDS,
|
CONTENT_KINDS,
|
||||||
@@ -57,9 +66,11 @@ import {
|
|||||||
userSettingsValues,
|
userSettingsValues,
|
||||||
userGroupList,
|
userGroupList,
|
||||||
getSpaceUrlsFromGroupList,
|
getSpaceUrlsFromGroupList,
|
||||||
|
getSpaceRoomsFromGroupList,
|
||||||
makeCommentFilter,
|
makeCommentFilter,
|
||||||
userSpaceUrls,
|
userSpaceUrls,
|
||||||
shouldNotify,
|
shouldNotify,
|
||||||
|
hasNip29,
|
||||||
device,
|
device,
|
||||||
} from "@app/core/state"
|
} from "@app/core/state"
|
||||||
import {kv} from "@app/core/storage"
|
import {kv} from "@app/core/storage"
|
||||||
@@ -125,6 +136,7 @@ export const allNotifications = derived(
|
|||||||
pubkey,
|
pubkey,
|
||||||
checked,
|
checked,
|
||||||
chatsById,
|
chatsById,
|
||||||
|
relaysByUrl,
|
||||||
userGroupList,
|
userGroupList,
|
||||||
deriveEventsByIdByUrl({
|
deriveEventsByIdByUrl({
|
||||||
tracker,
|
tracker,
|
||||||
@@ -135,7 +147,7 @@ export const allNotifications = derived(
|
|||||||
identity,
|
identity,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
([$pubkey, $checked, $chatsById, $userGroupList, eventsByIdByUrl]) => {
|
([$pubkey, $checked, $chatsById, $relaysByUrl, $userGroupList, eventsByIdByUrl]) => {
|
||||||
const hasNotification = (path: string, latestEvent?: TrustedEvent) => {
|
const hasNotification = (path: string, latestEvent?: TrustedEvent) => {
|
||||||
if (!latestEvent || latestEvent.pubkey === $pubkey) {
|
if (!latestEvent || latestEvent.pubkey === $pubkey) {
|
||||||
return false
|
return false
|
||||||
@@ -168,12 +180,34 @@ export const allNotifications = derived(
|
|||||||
|
|
||||||
for (const url of getSpaceUrlsFromGroupList($userGroupList)) {
|
for (const url of getSpaceUrlsFromGroupList($userGroupList)) {
|
||||||
const spacePath = makeSpacePath(url)
|
const spacePath = makeSpacePath(url)
|
||||||
|
const spacePathMobile = spacePath + ":mobile"
|
||||||
const eventsById = eventsByIdByUrl.get(url) || new Map()
|
const eventsById = eventsByIdByUrl.get(url) || new Map()
|
||||||
const latestEvent = first(sortEventsDesc(eventsById.values()))
|
const latestEvent = first(sortEventsDesc(eventsById.values()))
|
||||||
|
|
||||||
if (hasNotification(spacePath, latestEvent)) {
|
if (hasNotification(spacePath, latestEvent)) {
|
||||||
paths.add(spacePath)
|
paths.add(spacePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasNip29($relaysByUrl.get(url))) {
|
||||||
|
for (const h of getSpaceRoomsFromGroupList(url, $userGroupList)) {
|
||||||
|
const roomPath = makeRoomPath(url, h)
|
||||||
|
const latestEvent = find(e => e.tags.some(spec(["h", h])), eventsById.values())
|
||||||
|
|
||||||
|
if (hasNotification(roomPath, latestEvent)) {
|
||||||
|
paths.add(spacePathMobile)
|
||||||
|
paths.add(spacePath)
|
||||||
|
paths.add(roomPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const messagesPath = makeSpaceChatPath(url)
|
||||||
|
|
||||||
|
if (hasNotification(messagesPath, first(eventsById.values()))) {
|
||||||
|
paths.add(spacePathMobile)
|
||||||
|
paths.add(spacePath)
|
||||||
|
paths.add(messagesPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return paths
|
return paths
|
||||||
@@ -268,7 +302,7 @@ class CapacitorNotifications implements IPushAdapter {
|
|||||||
async request(prompt = true) {
|
async request(prompt = true) {
|
||||||
let status = await PushNotifications.checkPermissions()
|
let status = await PushNotifications.checkPermissions()
|
||||||
|
|
||||||
if (prompt && status.receive === "prompt") {
|
if (prompt && ["prompt", "prompt-with-rationale"].includes(status.receive)) {
|
||||||
status = await PushNotifications.requestPermissions()
|
status = await PushNotifications.requestPermissions()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import {get} from "svelte/store"
|
||||||
|
import {Client, type SessionItem, type ClientOptions} from "@pomade/core"
|
||||||
|
import {ifLet, reject, spec} from "@welshman/lib"
|
||||||
|
import {session, isPomadeSession, loginWithPomade as _loginWithPomade} from "@welshman/app"
|
||||||
|
|
||||||
|
export const getPomadeClient = async () => {
|
||||||
|
const $session = get(session)
|
||||||
|
|
||||||
|
if (isPomadeSession($session)) {
|
||||||
|
return new Client($session.clientOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PomadeSessionWithPeers = SessionItem & {peers: string[]}
|
||||||
|
|
||||||
|
export const loadPomadeSessions = async () => {
|
||||||
|
const sessionMap = new Map<string, PomadeSessionWithPeers>()
|
||||||
|
const client = await getPomadeClient()
|
||||||
|
|
||||||
|
if (client) {
|
||||||
|
const result = await client.listSessions()
|
||||||
|
|
||||||
|
for (const message of result.messages) {
|
||||||
|
if (!message.res?.items) continue
|
||||||
|
|
||||||
|
for (const item of message.res.items) {
|
||||||
|
const existing = sessionMap.get(item.client)
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
existing.peers.push(message.url)
|
||||||
|
} else {
|
||||||
|
sessionMap.set(item.client, {...item, peers: [message.url]})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(sessionMap.values())
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadOtherPomadeSessions = async () => {
|
||||||
|
const client = await getPomadeClient()
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return reject(spec({client: await client.getPubkey()}), await loadPomadeSessions())
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deletePomadeSession = async (clientPubkey: string, peers: string[]) =>
|
||||||
|
ifLet(await getPomadeClient(), client => client.deleteSession(clientPubkey, peers))
|
||||||
|
|
||||||
|
export const deactivatePomadeSession = async (clientPubkey: string, peers: string[]) =>
|
||||||
|
ifLet(await getPomadeClient(), client => client.deactivateSession(clientPubkey, peers))
|
||||||
|
|
||||||
|
export const deleteCurrentPomadeSession = async () =>
|
||||||
|
ifLet(await getPomadeClient(), async client =>
|
||||||
|
client.deleteSession(await client.getPubkey(), client.peers),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const deactivateCurrentPomadeSession = async () =>
|
||||||
|
ifLet(await getPomadeClient(), async client =>
|
||||||
|
client.deactivateSession(await client.getPubkey(), client.peers),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const deleteDeactivatedPomadeSessions = async () => {
|
||||||
|
const sessions = await loadOtherPomadeSessions()
|
||||||
|
|
||||||
|
for (const item of sessions || []) {
|
||||||
|
if (item.deactivated_at) {
|
||||||
|
await deletePomadeSession(item.client, item.peers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loginWithPomade = (clientOptions: ClientOptions, email: string) =>
|
||||||
|
_loginWithPomade(clientOptions.group.group_pk.slice(2), email, clientOptions)
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
class:text-base-content={active}
|
class:text-base-content={active}
|
||||||
class:bg-base-100={active}>
|
class:bg-base-100={active}>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
{#if !active && notification}
|
{#if notification}
|
||||||
<div class="absolute right-2 top-5 h-2 w-2 rounded-full bg-primary" transition:fade></div>
|
<div class="absolute right-2 top-5 h-2 w-2 rounded-full bg-primary" transition:fade></div>
|
||||||
{/if}
|
{/if}
|
||||||
</a>
|
</a>
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
class="{restProps.class} relative flex w-full items-center gap-3 text-left transition-all hover:bg-base-100 hover:text-base-content"
|
class="{restProps.class} relative flex w-full items-center gap-3 text-left transition-all hover:bg-base-100 hover:text-base-content"
|
||||||
class:text-base-content={active}
|
class:text-base-content={active}
|
||||||
class:bg-base-100={active}>
|
class:bg-base-100={active}>
|
||||||
{#if !active && notification}
|
{#if notification}
|
||||||
<div class="absolute right-2 top-5 h-2 w-2 rounded-full bg-primary" transition:fade></div>
|
<div class="absolute right-2 top-5 h-2 w-2 rounded-full bg-primary" transition:fade></div>
|
||||||
{/if}
|
{/if}
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {writable} from "svelte/store"
|
||||||
|
import type {Writable} from "svelte/store"
|
||||||
|
import {remove, uniq} from "@welshman/lib"
|
||||||
|
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
value: string[]
|
||||||
|
term?: Writable<string>
|
||||||
|
placeholder?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
let {value = $bindable(), term = writable(""), placeholder = ""}: Props = $props()
|
||||||
|
|
||||||
|
const normalizeItem = (text: string) => text.trim()
|
||||||
|
|
||||||
|
const addItems = (text: string) => {
|
||||||
|
const items = text.split(/[\n,]/).map(normalizeItem).filter(Boolean)
|
||||||
|
|
||||||
|
if (items.length > 0) {
|
||||||
|
value = uniq([...value, ...items])
|
||||||
|
}
|
||||||
|
|
||||||
|
term.set("")
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeItem = (item: string) => {
|
||||||
|
value = remove(item, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Enter" && $term) {
|
||||||
|
e.preventDefault()
|
||||||
|
addItems($term)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPaste = (e: ClipboardEvent) => {
|
||||||
|
const text = e.clipboardData?.getData("text")
|
||||||
|
|
||||||
|
if (text) {
|
||||||
|
e.preventDefault()
|
||||||
|
addItems(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onBlur = () => {
|
||||||
|
if ($term.trim()) {
|
||||||
|
addItems($term)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div>
|
||||||
|
{#each value as item (item)}
|
||||||
|
<div class="flex-inline badge badge-neutral mr-1 gap-1">
|
||||||
|
<Button class="flex items-center" onclick={() => removeItem(item)}>
|
||||||
|
<Icon icon={CloseCircle} size={4} class="-ml-1 mt-px" />
|
||||||
|
</Button>
|
||||||
|
<span>{item}</span>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
|
<input
|
||||||
|
bind:value={$term}
|
||||||
|
class="grow"
|
||||||
|
type="text"
|
||||||
|
{placeholder}
|
||||||
|
onkeydown={onKeyDown}
|
||||||
|
onpaste={onPaste}
|
||||||
|
onblur={onBlur} />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as nip19 from "nostr-tools/nip19"
|
import * as nip19 from "nostr-tools/nip19"
|
||||||
|
import {Client} from "@pomade/core"
|
||||||
import {hexToBytes} from "@welshman/lib"
|
import {hexToBytes} from "@welshman/lib"
|
||||||
import {displayPubkey, displayProfile} from "@welshman/util"
|
import {displayPubkey, displayProfile} from "@welshman/util"
|
||||||
import {pubkey, session, displayNip05, deriveProfile} from "@welshman/app"
|
import {pubkey, session, SessionMethod, displayNip05, deriveProfile} from "@welshman/app"
|
||||||
import {slideAndFade} from "@lib/transition"
|
import {slideAndFade} from "@lib/transition"
|
||||||
import PenNewSquare from "@assets/icons/pen-new-square.svg?dataurl"
|
import PenNewSquare from "@assets/icons/pen-new-square.svg?dataurl"
|
||||||
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
||||||
import Key from "@assets/icons/key-minimalistic.svg?dataurl"
|
import Key from "@assets/icons/key-minimalistic.svg?dataurl"
|
||||||
|
import Letter from "@assets/icons/letter.svg?dataurl"
|
||||||
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
||||||
import Copy from "@assets/icons/copy.svg?dataurl"
|
import Copy from "@assets/icons/copy.svg?dataurl"
|
||||||
import Settings from "@assets/icons/settings.svg?dataurl"
|
import Settings from "@assets/icons/settings.svg?dataurl"
|
||||||
@@ -16,15 +18,16 @@
|
|||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||||
import ContentMinimal from "@app/components/ContentMinimal.svelte"
|
import ContentMinimal from "@app/components/ContentMinimal.svelte"
|
||||||
import ProfileEdit from "@app/components/ProfileEdit.svelte"
|
import ProfileEdit from "@app/components/ProfileEdit.svelte"
|
||||||
import ProfileDelete from "@app/components/ProfileDelete.svelte"
|
import ProfileDelete from "@app/components/ProfileDelete.svelte"
|
||||||
import SignerStatus from "@app/components/SignerStatus.svelte"
|
import SignerStatus from "@app/components/SignerStatus.svelte"
|
||||||
|
import PasswordReset from "@app/components/PasswordReset.svelte"
|
||||||
import InfoKeys from "@app/components/InfoKeys.svelte"
|
import InfoKeys from "@app/components/InfoKeys.svelte"
|
||||||
import {PLATFORM_NAME} from "@app/core/state"
|
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
import {clip} from "@app/util/toast"
|
import {clip, pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
const npub = nip19.npubEncode($pubkey!)
|
const npub = nip19.npubEncode($pubkey!)
|
||||||
const profile = deriveProfile($pubkey!)
|
const profile = deriveProfile($pubkey!)
|
||||||
@@ -38,9 +41,29 @@
|
|||||||
|
|
||||||
const startDelete = () => pushModal(ProfileDelete)
|
const startDelete = () => pushModal(ProfileDelete)
|
||||||
|
|
||||||
|
const startPasswordReset = async () => {
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {ok, peersByPrefix} = await Client.requestChallenge($session!.email)
|
||||||
|
|
||||||
|
if (!ok) {
|
||||||
|
pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Failed to initiate password reset!",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pushModal(PasswordReset, {peersByPrefix})
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const startRecovery = () => pushModal(InfoKeys)
|
const startRecovery = () => pushModal(InfoKeys)
|
||||||
|
|
||||||
let showAdvanced = false
|
let loading = $state(false)
|
||||||
|
let showAdvanced = $state(false)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="content column gap-4">
|
<div class="content column gap-4">
|
||||||
@@ -69,10 +92,11 @@
|
|||||||
<ContentMinimal event={{content: $profile?.about || "", tags: []}} />
|
<ContentMinimal event={{content: $profile?.about || "", tags: []}} />
|
||||||
{/key}
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
{#if $session?.email}
|
<div class="card2 bg-alt col-4 shadow-md">
|
||||||
<div class="card2 bg-alt col-4 shadow-md">
|
{#if $session?.method === SessionMethod.Pomade}
|
||||||
<FieldInline>
|
<FieldInline>
|
||||||
{#snippet label()}
|
{#snippet label()}
|
||||||
|
<Icon icon={Letter} />
|
||||||
<p>Email Address</p>
|
<p>Email Address</p>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet input()}
|
{#snippet input()}
|
||||||
@@ -81,16 +105,8 @@
|
|||||||
<input readonly value={$session.email} class="grow" />
|
<input readonly value={$session.email} class="grow" />
|
||||||
</label>
|
</label>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet info()}
|
|
||||||
<p>
|
|
||||||
Your email and password can only be used to log into {PLATFORM_NAME}.
|
|
||||||
<Button class="link" onclick={startRecovery}>Start holding your own keys</Button>
|
|
||||||
</p>
|
|
||||||
{/snippet}
|
|
||||||
</FieldInline>
|
</FieldInline>
|
||||||
</div>
|
{/if}
|
||||||
{/if}
|
|
||||||
<div class="card2 bg-alt col-4 shadow-md">
|
|
||||||
<FieldInline>
|
<FieldInline>
|
||||||
{#snippet label()}
|
{#snippet label()}
|
||||||
<p class="flex items-center gap-3">
|
<p class="flex items-center gap-3">
|
||||||
@@ -139,6 +155,14 @@
|
|||||||
</FieldInline>
|
</FieldInline>
|
||||||
{/if}
|
{/if}
|
||||||
<SignerStatus />
|
<SignerStatus />
|
||||||
|
{#if $session?.method === SessionMethod.Pomade}
|
||||||
|
<div class="flex gap-2 justify-end">
|
||||||
|
<Button class="btn" onclick={startPasswordReset}>
|
||||||
|
<Spinner {loading}>Update your password</Spinner>
|
||||||
|
</Button>
|
||||||
|
<Button class="btn btn-primary" onclick={startRecovery}>Start holding your own keys</Button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="card2 bg-alt shadow-md">
|
<div class="card2 bg-alt shadow-md">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
|
|||||||
@@ -337,7 +337,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onEditPrevious = () => {
|
const onEditPrevious = () => {
|
||||||
const prev = $events.find(e => e.pubkey === $pubkey)
|
const prev = $events.toReversed().find(e => e.pubkey === $pubkey)
|
||||||
|
|
||||||
if (prev && canEditEvent(prev)) {
|
if (prev && canEditEvent(prev)) {
|
||||||
onEditEvent(prev)
|
onEditEvent(prev)
|
||||||
|
|||||||
@@ -273,7 +273,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onEditPrevious = () => {
|
const onEditPrevious = () => {
|
||||||
const prev = $events.find(e => e.pubkey === $pubkey)
|
const prev = $events.toReversed().find(e => e.pubkey === $pubkey)
|
||||||
|
|
||||||
if (prev && canEditEvent(prev)) {
|
if (prev && canEditEvent(prev)) {
|
||||||
onEditEvent(prev)
|
onEditEvent(prev)
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@ import {config} from "dotenv"
|
|||||||
import daisyui from "daisyui"
|
import daisyui from "daisyui"
|
||||||
import themes from "daisyui/src/theming/themes"
|
import themes from "daisyui/src/theming/themes"
|
||||||
|
|
||||||
config({path: ".env"})
|
config({path: ".env.local"})
|
||||||
config({path: ".env.template"})
|
config({path: ".env.template"})
|
||||||
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ import {SvelteKitPWA} from "@vite-pwa/sveltekit"
|
|||||||
import {sveltekit} from "@sveltejs/kit/vite"
|
import {sveltekit} from "@sveltejs/kit/vite"
|
||||||
import svg from "@poppanator/sveltekit-svg"
|
import svg from "@poppanator/sveltekit-svg"
|
||||||
|
|
||||||
config({path: ".env"})
|
config({path: ".env.local"})
|
||||||
config({path: ".env.template"})
|
config({path: ".env.template"})
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
|||||||
Reference in New Issue
Block a user