Compare commits
161 Commits
1.6.4
...
9364b46e5d
| Author | SHA1 | Date | |
|---|---|---|---|
| 9364b46e5d | |||
| af76726c88 | |||
| 33b9edb8b5 | |||
| d410a6a043 | |||
| 2fa0f29fc6 | |||
| c1c5d99a2a | |||
| 7b5e1eae20 | |||
| 1792dd157d | |||
| 2e1a0363bd | |||
| 61a3514cd2 | |||
| f89f1da947 | |||
| edc22bc88d | |||
| 5135da826e | |||
| b340b9e76d | |||
| 36eb8ace32 | |||
| 103a87e880 | |||
| 2acf971a10 | |||
| 1e42fd9b2d | |||
| 87d509df06 | |||
| 588c4b9c3f | |||
| 504764338c | |||
| 50199268f7 | |||
| 25228f785d | |||
| 697804c621 | |||
| 298e419b02 | |||
| 63d9adef19 | |||
| 697c116956 | |||
| 05de3dd7e0 | |||
| 0b09b63d85 | |||
| c518926c2b | |||
| a691f7b80a | |||
| 84be1ae47b | |||
| 89d11dccd3 | |||
| b1e52c29b4 | |||
| ea8a9652c9 | |||
| 3e14b31a09 | |||
| c18154915d | |||
| 703d573f96 | |||
| 6fcda91764 | |||
| 79728f68a4 | |||
| 83133b50e9 | |||
| f4e4ca1038 | |||
| ea3be89341 | |||
| e0d2988877 | |||
| fa976abe89 | |||
| 87b37bf0d8 | |||
| 6e72bc4b00 | |||
| 1b99ed6704 | |||
| 6330150c66 | |||
| 559df4b948 | |||
| 9bd57b0caa | |||
| 652461fffc | |||
| de76cfd208 | |||
| 108a9fef06 | |||
| 140440ca4c | |||
| 9a2fcec3f6 | |||
| 52f2f31ce6 | |||
| 3049efe889 | |||
| df9f3a707a | |||
| eeaad7b9b3 | |||
| 0cbcbfc47b | |||
| 73fb411122 | |||
| 7d3176da78 | |||
| 0ab1cfadde | |||
| fa4afe0c01 | |||
| ca259b119b | |||
| 0daab7a46c | |||
| 9fe155c118 | |||
| 147c756cc1 | |||
| c7fb404404 | |||
| 2546146ca8 | |||
| ffa776fd42 | |||
| a59ffb8758 | |||
| 9e74c94871 | |||
| 77294e7f1c | |||
| 57f2f4a619 | |||
| 1df2284ea3 | |||
| 189af077e7 | |||
| 10e4d83bce | |||
| 5d6661f964 | |||
| e6e11bb8f2 | |||
| 0e65e834da | |||
| 19f532c12e | |||
| bfc997ba37 | |||
| 99966a976e | |||
| cd54bc2880 | |||
| ffdd689331 | |||
| af41d81981 | |||
| 10d28ed364 | |||
| b02f4bd53a | |||
| 7ce8e3dbe6 | |||
| 2446d5cdb8 | |||
| d015018a16 | |||
| 6231c75e34 | |||
| 2f3bc6cc6f | |||
| 16c6015919 | |||
| e6b291cc68 | |||
| ae523c1ca6 | |||
| 7c86c1477f | |||
| 71f162f20d | |||
| eeacaca725 | |||
| af52ee25eb | |||
| eef32ca11e | |||
| 1ae821bff8 | |||
| 65483a6ef0 | |||
| 606a9343d9 | |||
| 7dfa6538be | |||
| 476d010ebe | |||
| 96d2efebc8 | |||
| f60f5af424 | |||
| 3da0334083 | |||
| c970038943 | |||
| 4000477bdb | |||
| ba11d53922 | |||
| beef606024 | |||
| 2adf64da55 | |||
| fd3fb8573c | |||
| e0d94d9794 | |||
| 7d049150a0 | |||
| 527ef59adc | |||
| b39775daef | |||
| 4bdb21560a | |||
| 797a9c32aa | |||
| bc864b29f8 | |||
| 482121db5c | |||
| 0fa26c8d0a | |||
| f5c768d6a7 | |||
| c43544734a | |||
| 86d99916f7 | |||
| 135dbc8789 | |||
| fc14de9b0f | |||
| c77197d959 | |||
| 56dddbdd86 | |||
| cbafcf6939 | |||
| 4b156ee699 | |||
| a4e883b09a | |||
| b114a724e2 | |||
| 621c0d839c | |||
| 021c1fc7c4 | |||
| bda91080ab | |||
| a9828be25c | |||
| dde9dbfbfe | |||
| ca7d126a3c | |||
| 7f6450375b | |||
| c9954db3fe | |||
| 3d268f1f9d | |||
| 66a7a2a7af | |||
| 7823e1d803 | |||
| d5e91ce874 | |||
| 6f32c1932f | |||
| cb06c4e954 | |||
| 9188c0a8bc | |||
| 30653fe344 | |||
| 5bb55c453f | |||
| 3024e08ca5 | |||
| aaf1f25167 | |||
| aabbb758a4 | |||
| d824f928b5 | |||
| 445ed27eb8 | |||
| 21f3970ca8 | |||
| 919fe29ffb |
@@ -1,5 +1,6 @@
|
|||||||
--ignore-dir=.svelte-kit
|
--ignore-dir=.svelte-kit
|
||||||
--ignore-dir=android
|
--ignore-dir=android
|
||||||
|
--ignore-dir=target
|
||||||
--ignore-dir=build
|
--ignore-dir=build
|
||||||
--ignore-dir=ios/DerivedData
|
--ignore-dir=ios/DerivedData
|
||||||
--ignore-dir=ios/App/App/public
|
--ignore-dir=ios/App/App/public
|
||||||
|
|||||||
@@ -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,6 +1,6 @@
|
|||||||
VITE_DEFAULT_PUBKEYS=06639a386c9c1014217622ccbcf40908c4f1a0c33e23f8d6d68f4abf655f8f71,266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5,391819e2f2f13b90cac7209419eb574ef7c0d1f4e81867fc24c47a3ce5e8a248,3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d,3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24,55f04590674f3648f4cdc9dc8ce32da2a282074cd0b020596ee033d12d385185,58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196,61066504617ee79387021e18c89fb79d1ddbc3e7bff19cf2298f40466f8715e9,6389be6491e7b693e9f368ece88fcd145f07c068d2c1bbae4247b9b5ef439d32,63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed,6e75f7972397ca3295e0f4ca0fbc6eb9cc79be85bafdd56bd378220ca8eee74e,76c71aae3a491f1d9eec47cba17e229cda4113a0bbb6e6ae1776d7643e29cafa,7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194,82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2,84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240,97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322,b676ded7c768d66a757aa3967b1243d90bf57afb09d1044d3219d8d424e4aea0,dace63b00c42e6e017d00dd190a9328386002ff597b841eb5ef91de4f1ce8491,eeb11961b25442b16389fe6c7ebea9adf0ac36dd596816ea7119e521b8821b9e,fe7f6bc6f7338b76bbf80db402ade65953e20b2f23e66e898204b63cc42539a3
|
VITE_DEFAULT_PUBKEYS=06639a386c9c1014217622ccbcf40908c4f1a0c33e23f8d6d68f4abf655f8f71,266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5,391819e2f2f13b90cac7209419eb574ef7c0d1f4e81867fc24c47a3ce5e8a248,3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d,3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24,55f04590674f3648f4cdc9dc8ce32da2a282074cd0b020596ee033d12d385185,58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196,61066504617ee79387021e18c89fb79d1ddbc3e7bff19cf2298f40466f8715e9,6389be6491e7b693e9f368ece88fcd145f07c068d2c1bbae4247b9b5ef439d32,63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed,6e75f7972397ca3295e0f4ca0fbc6eb9cc79be85bafdd56bd378220ca8eee74e,76c71aae3a491f1d9eec47cba17e229cda4113a0bbb6e6ae1776d7643e29cafa,7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194,82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2,84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240,97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322,b676ded7c768d66a757aa3967b1243d90bf57afb09d1044d3219d8d424e4aea0,dace63b00c42e6e017d00dd190a9328386002ff597b841eb5ef91de4f1ce8491,eeb11961b25442b16389fe6c7ebea9adf0ac36dd596816ea7119e521b8821b9e,fe7f6bc6f7338b76bbf80db402ade65953e20b2f23e66e898204b63cc42539a3
|
||||||
VITE_DEFAULT_BLOSSOM_SERVERS=https://blossom.primal.net/
|
VITE_DEFAULT_BLOSSOM_SERVERS=https://blossom.primal.net/
|
||||||
VITE_POMADE_SIGNERS=
|
VITE_POMADE_SIGNERS=https://pomade.coracle.social,https://pomade.fiatjaf.com,https://pomade.nostrver.se,https://pomade.scuttle.works
|
||||||
VITE_PLATFORM_URL=https://app.flotilla.social
|
VITE_PLATFORM_URL=https://app.flotilla.social
|
||||||
VITE_PLATFORM_TERMS=https://flotilla.social/terms
|
VITE_PLATFORM_TERMS=https://flotilla.social/terms
|
||||||
VITE_PLATFORM_PRIVACY=https://flotilla.social/privacy
|
VITE_PLATFORM_PRIVACY=https://flotilla.social/privacy
|
||||||
@@ -15,7 +15,7 @@ VITE_PUSH_BRIDGE=wss://npb.coracle.social/
|
|||||||
VITE_BLOCKED_RELAYS=brb.io,relay.nostr.band,nostr.mutinywallet.com,feeds.nostr.band,nostr.zbd.gg,wot.utxo.one,blastr.f7z.xyz,relay.current.fyi
|
VITE_BLOCKED_RELAYS=brb.io,relay.nostr.band,nostr.mutinywallet.com,feeds.nostr.band,nostr.zbd.gg,wot.utxo.one,blastr.f7z.xyz,relay.current.fyi
|
||||||
VITE_INDEXER_RELAYS=purplepag.es,relay.damus.io,indexer.coracle.social
|
VITE_INDEXER_RELAYS=purplepag.es,relay.damus.io,indexer.coracle.social
|
||||||
VITE_DEFAULT_RELAYS=relay.damus.io,relay.primal.net,nostr.mom
|
VITE_DEFAULT_RELAYS=relay.damus.io,relay.primal.net,nostr.mom
|
||||||
VITE_DEFAULT_MESSAGING_RELAYS=auth.nostr1.com
|
VITE_DEFAULT_MESSAGING_RELAYS=auth.nostr1.com,nostr-01.uid.ovh,relay.keychat.io,relay.0xchat.com
|
||||||
VITE_SIGNER_RELAYS=relay.nsec.app,ephemeral.snowflare.cc,bucket.coracle.social
|
VITE_SIGNER_RELAYS=relay.nsec.app,ephemeral.snowflare.cc,bucket.coracle.social
|
||||||
VITE_VAPID_PUBLIC_KEY=BIt2D4BdgdbCowD_0d3Np6GbrIGHxd7aIEUeZNe3hQuRlHz02OhzvDaai0XSFoJYVzSzdMjdyW-QhvW9_yq8j4Y
|
VITE_VAPID_PUBLIC_KEY=BIt2D4BdgdbCowD_0d3Np6GbrIGHxd7aIEUeZNe3hQuRlHz02OhzvDaai0XSFoJYVzSzdMjdyW-QhvW9_yq8j4Y
|
||||||
VITE_GLITCHTIP_API_KEY=
|
VITE_GLITCHTIP_API_KEY=
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
src/assets
|
src/assets
|
||||||
|
target
|
||||||
build
|
build
|
||||||
.idea
|
.idea
|
||||||
.gradle
|
.gradle
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ name: Docker
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: ['master']
|
branches: [master]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: ghcr.io
|
REGISTRY: ghcr.io
|
||||||
IMAGE_NAME: ${{ github.repository }}
|
IMAGE_NAME: coracle-social/flotilla
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-push-image:
|
build-and-push-image:
|
||||||
@@ -14,8 +14,6 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
attestations: write
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
@@ -25,8 +23,8 @@ jobs:
|
|||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
id: meta
|
id: meta
|
||||||
@@ -50,10 +48,3 @@ jobs:
|
|||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|
||||||
- name: Generate artifact attestation
|
|
||||||
uses: actions/attest-build-provenance@v2
|
|
||||||
with:
|
|
||||||
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
|
|
||||||
subject-digest: ${{ steps.push.outputs.digest }}
|
|
||||||
push-to-registry: true
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
# Env
|
# Env
|
||||||
.env
|
.env
|
||||||
|
.env.local
|
||||||
|
|
||||||
# Vite
|
# Vite
|
||||||
vite.config.js.timestamp-*
|
vite.config.js.timestamp-*
|
||||||
@@ -24,9 +25,14 @@ android/app/src/main/assets/public/
|
|||||||
|
|
||||||
# Web/JavaScript
|
# Web/JavaScript
|
||||||
node_modules/
|
node_modules/
|
||||||
|
.pnpm-store/
|
||||||
build/
|
build/
|
||||||
.svelte-kit/
|
.svelte-kit/
|
||||||
|
|
||||||
|
# Rust/Tauri
|
||||||
|
*target/
|
||||||
|
src-tauri/binaries/
|
||||||
|
|
||||||
# iOS
|
# iOS
|
||||||
ios/App/App/public
|
ios/App/App/public
|
||||||
ios/DerivedData
|
ios/DerivedData
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ src/
|
|||||||
- Derive all other data inside the component from identifiers
|
- Derive all other data inside the component from identifiers
|
||||||
- Example: Don't pass `members` prop, derive it from `h` inside component
|
- Example: Don't pass `members` prop, derive it from `h` inside component
|
||||||
|
|
||||||
**Code Style:**
|
**CRITICAL Code Style Guidelines:**
|
||||||
|
|
||||||
- **No `null`** - only use `undefined`
|
- **No `null`** - only use `undefined`
|
||||||
- Svelte 5 runes (`$state`, `$derived`, `$effect`) only in UI components
|
- Svelte 5 runes (`$state`, `$derived`, `$effect`) only in UI components
|
||||||
@@ -168,6 +168,16 @@ src/
|
|||||||
- When dynamically building classes, use `cx` from `classnames` rather than embedded ternaries or svelte 4's old `class:` syntax.
|
- When dynamically building classes, use `cx` from `classnames` rather than embedded ternaries or svelte 4's old `class:` syntax.
|
||||||
- When creating forms, use `FieldInline` or `Field` instead of custom elements/tailwindcss
|
- When creating forms, use `FieldInline` or `Field` instead of custom elements/tailwindcss
|
||||||
- Do not define svelte event handlers inline, instead name them and put them in the script section of templates
|
- Do not define svelte event handlers inline, instead name them and put them in the script section of templates
|
||||||
|
- Avoid using `as`, except where necessary. Instead, annotate function parameters, and ensure upstream values are typed correctly.
|
||||||
|
|
||||||
|
**Human-First Simplicity (Jon Staab Style):**
|
||||||
|
|
||||||
|
- Prefer direct, readable code over layered abstractions.
|
||||||
|
- Do not add indirection (extra helpers, wrappers, stores, or derived state) unless it removes real repeated complexity.
|
||||||
|
- Reuse existing Welshman and Flotilla primitives before introducing new utilities or dependencies.
|
||||||
|
- Favor linear control flow and explicit naming over clever patterns.
|
||||||
|
- Remove defensive checks that do not apply in this runtime model.
|
||||||
|
- When two approaches work, pick the one that feels more human and easier to maintain.
|
||||||
|
|
||||||
## Common Tasks
|
## Common Tasks
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,36 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
# Current
|
||||||
|
|
||||||
|
* Enable email/password login
|
||||||
|
* Add up/edit to direct messages
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|||||||
@@ -1,24 +1,32 @@
|
|||||||
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
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends curl
|
||||||
|
|
||||||
# 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
|
FROM node:20-alpine
|
||||||
CMD ["npx", "serve", "build"]
|
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy only the built output - no source, no .env, no dev deps
|
||||||
|
COPY --from=builder /app/build ./build
|
||||||
|
|
||||||
|
CMD ["npx", "serve", "-s", "build"]
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ If you would like to be interoperable with Flotilla, please check out this guide
|
|||||||
|
|
||||||
## Environment
|
## Environment
|
||||||
|
|
||||||
You can also optionally create an `.env` file and populate it with the following environment variables (see `.env` for examples):
|
You can also optionally create an `.env.local` file and populate it with the following environment variables (see `.env.template` for examples):
|
||||||
|
|
||||||
- `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
|
||||||
@@ -20,7 +20,7 @@ If you're deploying a custom version of flotilla, be sure to remove the `plausib
|
|||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
See [./CONTRIBUTING.md](CONTRIBUTING.md).
|
See [CONTRIBUTING.md](AGENTS.md).
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ To run your own Flotilla, it's as simple as:
|
|||||||
```sh
|
```sh
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm run build
|
pnpm run build
|
||||||
npx serve build
|
npx serve -s build
|
||||||
```
|
```
|
||||||
|
|
||||||
Or, if you prefer to use a container:
|
Or, if you prefer to use a container:
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -42,4 +42,6 @@
|
|||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -27,4 +27,4 @@ include ':capawesome-capacitor-badge'
|
|||||||
project(':capawesome-capacitor-badge').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-badge@8.0.0_@capacitor+core@8.0.1/node_modules/@capawesome/capacitor-badge/android')
|
project(':capawesome-capacitor-badge').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-badge@8.0.0_@capacitor+core@8.0.1/node_modules/@capawesome/capacitor-badge/android')
|
||||||
|
|
||||||
include ':nostr-signer-capacitor-plugin'
|
include ':nostr-signer-capacitor-plugin'
|
||||||
project(':nostr-signer-capacitor-plugin').projectDir = new File('../node_modules/.pnpm/nostr-signer-capacitor-plugin@0.0.4_@capacitor+core@8.0.1/node_modules/nostr-signer-capacitor-plugin/android')
|
project(':nostr-signer-capacitor-plugin').projectDir = new File('../node_modules/.pnpm/nostr-signer-capacitor-plugin@https+++codeload.github.com+coracle-social+nostr-signer-c_2704ecccfd05fcfb1ad8852744422b7c/node_modules/nostr-signer-capacitor-plugin/android')
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ const config: CapacitorConfig = {
|
|||||||
appId: "social.flotilla",
|
appId: "social.flotilla",
|
||||||
appName: "Flotilla",
|
appName: "Flotilla",
|
||||||
webDir: "build",
|
webDir: "build",
|
||||||
|
ios: {
|
||||||
|
scheme: "Flotilla Chat",
|
||||||
|
},
|
||||||
android: {
|
android: {
|
||||||
adjustMarginsForEdgeToEdge: true,
|
adjustMarginsForEdgeToEdge: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 = 30;
|
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 = 30;
|
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 = "";
|
||||||
|
|||||||
@@ -20,8 +20,16 @@
|
|||||||
<string>$(MARKETING_VERSION)</string>
|
<string>$(MARKETING_VERSION)</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
<false/>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
|
<string>Flotilla uses the microphone for voice chat in rooms.</string>
|
||||||
|
<key>UIBackgroundModes</key>
|
||||||
|
<array>
|
||||||
|
<string>remote-notification</string>
|
||||||
|
</array>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
@@ -47,11 +55,5 @@
|
|||||||
</array>
|
</array>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
|
||||||
<false/>
|
|
||||||
<key>UIBackgroundModes</key>
|
|
||||||
<array>
|
|
||||||
<string>remote-notification</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ def capacitor_pods
|
|||||||
pod 'CapacitorPreferences', :path => '../../node_modules/.pnpm/@capacitor+preferences@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/preferences'
|
pod 'CapacitorPreferences', :path => '../../node_modules/.pnpm/@capacitor+preferences@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/preferences'
|
||||||
pod 'CapacitorPushNotifications', :path => '../../node_modules/.pnpm/@capacitor+push-notifications@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/push-notifications'
|
pod 'CapacitorPushNotifications', :path => '../../node_modules/.pnpm/@capacitor+push-notifications@8.0.0_@capacitor+core@8.0.1/node_modules/@capacitor/push-notifications'
|
||||||
pod 'CapawesomeCapacitorBadge', :path => '../../node_modules/.pnpm/@capawesome+capacitor-badge@8.0.0_@capacitor+core@8.0.1/node_modules/@capawesome/capacitor-badge'
|
pod 'CapawesomeCapacitorBadge', :path => '../../node_modules/.pnpm/@capawesome+capacitor-badge@8.0.0_@capacitor+core@8.0.1/node_modules/@capawesome/capacitor-badge'
|
||||||
pod 'NostrSignerCapacitorPlugin', :path => '../../node_modules/.pnpm/nostr-signer-capacitor-plugin@0.0.4_@capacitor+core@8.0.1/node_modules/nostr-signer-capacitor-plugin'
|
pod 'NostrSignerCapacitorPlugin', :path => '../../node_modules/.pnpm/nostr-signer-capacitor-plugin@https+++codeload.github.com+coracle-social+nostr-signer-c_2704ecccfd05fcfb1ad8852744422b7c/node_modules/nostr-signer-capacitor-plugin'
|
||||||
end
|
end
|
||||||
|
|
||||||
target 'Flotilla Chat' do
|
target 'Flotilla Chat' do
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import path from 'path'
|
|
||||||
import { execSync } from 'child_process'
|
import { execSync } from 'child_process'
|
||||||
|
|
||||||
if (execSync('git status --porcelain', { encoding: 'utf8' }).trim()) {
|
const force = process.argv.includes('--force')
|
||||||
console.error('Error: Git working tree is dirty. Please commit or stash your changes first.')
|
|
||||||
|
if (execSync('git status --porcelain', { encoding: 'utf8' }).trim() && !force) {
|
||||||
|
console.error('Error: Git working tree is dirty. Please commit or stash your changes first, or re-run with --force.')
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,7 +23,9 @@ pkg.pnpm.overrides["@welshman/router"] = "link:../welshman/packages/router"
|
|||||||
pkg.pnpm.overrides["@welshman/signer"] = "link:../welshman/packages/signer"
|
pkg.pnpm.overrides["@welshman/signer"] = "link:../welshman/packages/signer"
|
||||||
pkg.pnpm.overrides["@welshman/store"] = "link:../welshman/packages/store"
|
pkg.pnpm.overrides["@welshman/store"] = "link:../welshman/packages/store"
|
||||||
pkg.pnpm.overrides["@welshman/util"] = "link:../welshman/packages/util"
|
pkg.pnpm.overrides["@welshman/util"] = "link:../welshman/packages/util"
|
||||||
|
// pkg.pnpm.overrides["nostr-editor"] = "link:../nostr-editor"
|
||||||
// pkg.pnpm.overrides["@pomade/core"] = "link:../pomade/packages/core"
|
// pkg.pnpm.overrides["@pomade/core"] = "link:../pomade/packages/core"
|
||||||
|
// pkg.pnpm.overrides["nostr-signer-capacitor-plugin"] = "link:../nostr-signer-capacitor-plugin"
|
||||||
|
|
||||||
fs.writeFileSync('./package.json', JSON.stringify(pkg, null, 2) + '\n')
|
fs.writeFileSync('./package.json', JSON.stringify(pkg, null, 2) + '\n')
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "flotilla",
|
"name": "flotilla",
|
||||||
"version": "1.6.4",
|
"version": "1.6.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
"build": "./build.sh",
|
"build": "./build.sh",
|
||||||
"release:android": "./build.sh && cap build android --androidreleasetype APK --signing-type apksigner",
|
"release:android": "./build.sh && cap build android --androidreleasetype APK --signing-type apksigner",
|
||||||
|
"tauri:dev": "tauri dev",
|
||||||
|
"tauri:build": "tauri build",
|
||||||
|
"tauri:info": "tauri info",
|
||||||
|
"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": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
"lint": "prettier --check src && eslint src",
|
"lint": "prettier --check src && eslint src",
|
||||||
@@ -18,6 +22,7 @@
|
|||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
"@sveltejs/kit": "^2.50.1",
|
"@sveltejs/kit": "^2.50.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "^4.0.4",
|
"@sveltejs/vite-plugin-svelte": "^4.0.4",
|
||||||
|
"@tauri-apps/cli": "^2.9.6",
|
||||||
"@types/eslint": "^9.6.1",
|
"@types/eslint": "^9.6.1",
|
||||||
"autoprefixer": "^10.4.23",
|
"autoprefixer": "^10.4.23",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
@@ -52,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.0.12",
|
"@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",
|
||||||
@@ -60,17 +65,17 @@
|
|||||||
"@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.4",
|
"@welshman/app": "^0.8.9",
|
||||||
"@welshman/content": "^0.8.4",
|
"@welshman/content": "^0.8.9",
|
||||||
"@welshman/editor": "^0.8.4",
|
"@welshman/editor": "^0.8.9",
|
||||||
"@welshman/feeds": "^0.8.4",
|
"@welshman/feeds": "^0.8.9",
|
||||||
"@welshman/lib": "^0.8.4",
|
"@welshman/lib": "^0.8.9",
|
||||||
"@welshman/net": "^0.8.4",
|
"@welshman/net": "^0.8.9",
|
||||||
"@welshman/router": "^0.8.4",
|
"@welshman/router": "^0.8.9",
|
||||||
"@welshman/signer": "^0.8.4",
|
"@welshman/signer": "^0.8.9",
|
||||||
"@welshman/store": "^0.8.4",
|
"@welshman/store": "^0.8.9",
|
||||||
"@welshman/util": "^0.8.4",
|
"@welshman/util": "^0.8.9",
|
||||||
"compressorjs": "^1.2.1",
|
"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",
|
||||||
"dotenv": "^16.6.1",
|
"dotenv": "^16.6.1",
|
||||||
@@ -78,7 +83,8 @@
|
|||||||
"fuse.js": "^7.1.0",
|
"fuse.js": "^7.1.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"idb": "^8.0.3",
|
"idb": "^8.0.3",
|
||||||
"nostr-signer-capacitor-plugin": "^0.0.4",
|
"livekit-client": "^2.17.2",
|
||||||
|
"nostr-signer-capacitor-plugin": "github:coracle-social/nostr-signer-capacitor-plugin#main",
|
||||||
"nostr-tools": "^2.19.4",
|
"nostr-tools": "^2.19.4",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||||
"qr-scanner": "^1.4.2",
|
"qr-scanner": "^1.4.2",
|
||||||
@@ -91,7 +97,8 @@
|
|||||||
"esbuild"
|
"esbuild"
|
||||||
],
|
],
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
"sharp"
|
"sharp",
|
||||||
|
"nostr-signer-capacitor-plugin"
|
||||||
],
|
],
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"sharp": "0.35.0-rc.0"
|
"sharp": "0.35.0-rc.0"
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "1.92.0"
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[package]
|
||||||
|
name = "flotilla"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "flotilla_lib"
|
||||||
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "2.5.3", features = [] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tauri = { version = "2.9.5", features = [] }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["custom-protocol"]
|
||||||
|
custom-protocol = ["tauri/custom-protocol"]
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
|
"identifier": "default",
|
||||||
|
"description": "Default desktop capability for the main window",
|
||||||
|
"windows": ["main"],
|
||||||
|
"permissions": ["core:default"]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
</adaptive-icon>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#fff</color>
|
||||||
|
</resources>
|
||||||
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 668 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 926 B |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
@@ -0,0 +1,2 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "1.92.0"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
|
pub fn run() {
|
||||||
|
tauri::Builder::default()
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running tauri application");
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
flotilla_lib::run();
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||||
|
"productName": "Flotilla",
|
||||||
|
"mainBinaryName": "flotilla",
|
||||||
|
"identifier": "social.flotilla.app",
|
||||||
|
"build": {
|
||||||
|
"beforeDevCommand": "pnpm dev",
|
||||||
|
"beforeBuildCommand": "pnpm build",
|
||||||
|
"devUrl": "http://localhost:1847",
|
||||||
|
"frontendDist": "../build"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"security": {
|
||||||
|
"capabilities": ["default"]
|
||||||
|
},
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"label": "main",
|
||||||
|
"title": "Flotilla",
|
||||||
|
"width": 1240,
|
||||||
|
"height": 775,
|
||||||
|
"resizable": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": false,
|
||||||
|
"targets": "all",
|
||||||
|
"icon": [
|
||||||
|
"icons/32x32.png",
|
||||||
|
"icons/128x128.png",
|
||||||
|
"icons/128x128@2x.png",
|
||||||
|
"icons/icon.icns",
|
||||||
|
"icons/icon.ico"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -274,7 +274,7 @@
|
|||||||
.input-editor,
|
.input-editor,
|
||||||
.chat-editor,
|
.chat-editor,
|
||||||
.note-editor {
|
.note-editor {
|
||||||
@apply -m-1 min-h-12 p-1 text-sm;
|
@apply -m-1 p-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tiptap {
|
.tiptap {
|
||||||
@@ -300,7 +300,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tiptap {
|
.tiptap {
|
||||||
@apply max-h-[350px] overflow-y-auto p-2 px-4;
|
@apply max-h-[350px] min-h-10 overflow-y-auto p-2 px-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tiptap p.is-editor-empty:first-child::before {
|
.tiptap p.is-editor-empty:first-child::before {
|
||||||
@@ -402,6 +402,10 @@ progress[value]::-webkit-progress-value {
|
|||||||
@apply md:bottom-sai bottom-[calc(var(--saib)+3.5rem)];
|
@apply md:bottom-sai bottom-[calc(var(--saib)+3.5rem)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ct {
|
||||||
|
@apply top-[calc(var(--sait)+5rem)] md:top-[calc(var(--sait)+3rem)];
|
||||||
|
}
|
||||||
|
|
||||||
/* Keyboard open state adjustments */
|
/* Keyboard open state adjustments */
|
||||||
|
|
||||||
body.keyboard-open .cb {
|
body.keyboard-open .cb {
|
||||||
@@ -418,6 +422,20 @@ body.keyboard-open .hide-on-keyboard {
|
|||||||
@apply cb cw fixed z-compose;
|
@apply cb cw fixed z-compose;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat__scroll-down {
|
.chat__compose-zone {
|
||||||
@apply fixed bottom-28 right-4 z-feature md:bottom-16;
|
@apply cb cw fixed z-compose;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat__compose-zone .chat__compose-inner {
|
||||||
|
@apply min-w-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat__scroll-down {
|
||||||
|
@apply pb-sai fixed bottom-28 right-4 z-feature md:bottom-16;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* content visibility */
|
||||||
|
|
||||||
|
.cv {
|
||||||
|
content-visibility: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {Snippet} from "svelte"
|
import type {Snippet} from "svelte"
|
||||||
import {page} from "$app/stores"
|
|
||||||
import {pubkey} from "@welshman/app"
|
import {pubkey} from "@welshman/app"
|
||||||
import Dialog from "@lib/components/Dialog.svelte"
|
import Dialog from "@lib/components/Dialog.svelte"
|
||||||
import Landing from "@app/components/Landing.svelte"
|
import Landing from "@app/components/Landing.svelte"
|
||||||
import Toast from "@app/components/Toast.svelte"
|
import Toast from "@app/components/Toast.svelte"
|
||||||
import PrimaryNav from "@app/components/PrimaryNav.svelte"
|
import PrimaryNav from "@app/components/PrimaryNav.svelte"
|
||||||
import {modals} from "@app/util/modal"
|
import {modal} from "@app/util/modal"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: Snippet
|
children: Snippet
|
||||||
@@ -20,7 +19,7 @@
|
|||||||
<PrimaryNav>
|
<PrimaryNav>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</PrimaryNav>
|
</PrimaryNav>
|
||||||
{:else if !$modals[$page.url.hash.slice(1)]}
|
{:else if !$modal}
|
||||||
<Dialog children={{component: Landing, props: {}}} />
|
<Dialog children={{component: Landing, props: {}}} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
class="col-3 card2 bg-alt w-full cursor-pointer shadow-md"
|
class="cv col-3 card2 bg-alt w-full cursor-pointer shadow-md"
|
||||||
href={makeCalendarPath(url, getAddress(event))}>
|
href={makeCalendarPath(url, getAddress(event))}>
|
||||||
<CalendarEventHeader {event} />
|
<CalendarEventHeader {event} />
|
||||||
<div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row">
|
<div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row">
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
import type {Snippet} from "svelte"
|
import type {Snippet} from "svelte"
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {
|
import {
|
||||||
|
ago,
|
||||||
int,
|
int,
|
||||||
ms,
|
ms,
|
||||||
partition,
|
partition,
|
||||||
|
ifLet,
|
||||||
spec,
|
spec,
|
||||||
nthEq,
|
nthEq,
|
||||||
nthNe,
|
nthNe,
|
||||||
@@ -46,11 +48,12 @@
|
|||||||
import ChatMembers from "@app/components/ChatMembers.svelte"
|
import ChatMembers from "@app/components/ChatMembers.svelte"
|
||||||
import ChatMessage from "@app/components/ChatMessage.svelte"
|
import ChatMessage from "@app/components/ChatMessage.svelte"
|
||||||
import ChatCompose from "@app/components/ChatCompose.svelte"
|
import ChatCompose from "@app/components/ChatCompose.svelte"
|
||||||
|
import ChatComposeEdit from "@app/components/ChatComposeEdit.svelte"
|
||||||
import ChatComposeParent from "@app/components/ChatComposeParent.svelte"
|
import ChatComposeParent from "@app/components/ChatComposeParent.svelte"
|
||||||
import ThunkToast from "@app/components/ThunkToast.svelte"
|
import ThunkToast from "@app/components/ThunkToast.svelte"
|
||||||
import {userSettingsValues, PLATFORM_NAME, deriveChat} from "@app/core/state"
|
import {userSettingsValues, PLATFORM_NAME, deriveChat} from "@app/core/state"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
import {prependParent} from "@app/core/commands"
|
import {makeDelete, prependParent} from "@app/core/commands"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -78,73 +81,115 @@
|
|||||||
parent = undefined
|
parent = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSubmit = async (params: EventContent) => {
|
const clearEventToEdit = () => {
|
||||||
const ptags = remove($pubkey!, pubkeys).map(tagPubkey)
|
eventToEdit = undefined
|
||||||
|
|
||||||
// Remove p tags since they result in forking the conversation
|
|
||||||
params.tags = params.tags.filter(nthNe(0, "p"))
|
|
||||||
|
|
||||||
// Add our reply quote to content
|
|
||||||
params = prependParent(parent, params)
|
|
||||||
|
|
||||||
const [imetaTags, tags] = partition(nthEq(0, "imeta"), params.tags)
|
|
||||||
const imetas = getTags("imeta", imetaTags).map(tagsFromIMeta)
|
|
||||||
const templates: EventTemplate[] = []
|
|
||||||
const buffer = []
|
|
||||||
|
|
||||||
const addTemplate = (kind: number, content: string, tags: string[][]) => {
|
|
||||||
content = content.trim()
|
|
||||||
|
|
||||||
if (content) {
|
|
||||||
templates.push(makeEvent(kind, {content, tags: [...tags, ...ptags]}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const p of parse(params)) {
|
|
||||||
const imeta = isLink(p)
|
|
||||||
? imetas.find(tags => tags.find(spec(["url", p.value.url.toString()])))
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
if (isLink(p) && imeta) {
|
|
||||||
addTemplate(DIRECT_MESSAGE, buffer.splice(0).join(""), tags)
|
|
||||||
addTemplate(
|
|
||||||
DIRECT_MESSAGE_FILE,
|
|
||||||
p.value.url.toString(),
|
|
||||||
imeta.slice(1).filter(nthNe(0, "url")),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
buffer.push(p.raw)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addTemplate(DIRECT_MESSAGE, buffer.splice(0).join(""), tags)
|
|
||||||
|
|
||||||
// Split the message into multiple pieces so that we can use kind 15 to send images per nip 17
|
|
||||||
// Sleep 1 second between each one to make sure timestamps are distinct
|
|
||||||
const thunks = await Promise.all(
|
|
||||||
Array.from(enumerate(templates)).map(([i, event]) =>
|
|
||||||
sendWrapped({
|
|
||||||
event,
|
|
||||||
recipients: pubkeys,
|
|
||||||
delay: $userSettingsValues.send_delay + ms(i),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
pushToast({
|
|
||||||
timeout: 30_000,
|
|
||||||
children: {
|
|
||||||
component: ThunkToast,
|
|
||||||
props: {thunk: mergeThunks(thunks)},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
clearParent()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onSubmit = async (params: EventContent) => {
|
||||||
|
try {
|
||||||
|
const ptags = remove($pubkey!, pubkeys).map(tagPubkey)
|
||||||
|
|
||||||
|
// Remove p tags since they result in forking the conversation
|
||||||
|
params.tags = params.tags.filter(nthNe(0, "p"))
|
||||||
|
|
||||||
|
// Add our reply quote to content
|
||||||
|
params = prependParent(parent, params)
|
||||||
|
|
||||||
|
if (eventToEdit) {
|
||||||
|
if (eventToEdit.content === params.content) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendWrapped({
|
||||||
|
event: makeDelete({event: eventToEdit, protect: false}),
|
||||||
|
recipients: pubkeys,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const [imetaTags, tags] = partition(nthEq(0, "imeta"), params.tags)
|
||||||
|
const imetas = getTags("imeta", imetaTags).map(tagsFromIMeta)
|
||||||
|
const templates: EventTemplate[] = []
|
||||||
|
const buffer = []
|
||||||
|
|
||||||
|
const addTemplate = (kind: number, content: string, tags: string[][]) => {
|
||||||
|
content = content.trim()
|
||||||
|
|
||||||
|
if (content) {
|
||||||
|
templates.push(
|
||||||
|
makeEvent(kind, {
|
||||||
|
content,
|
||||||
|
tags: [...tags, ...ptags],
|
||||||
|
created_at: eventToEdit?.created_at,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const p of parse(params)) {
|
||||||
|
const imeta = isLink(p)
|
||||||
|
? imetas.find(tags => tags.find(spec(["url", p.value.url.toString()])))
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
if (isLink(p) && imeta) {
|
||||||
|
addTemplate(DIRECT_MESSAGE, buffer.splice(0).join(""), tags)
|
||||||
|
addTemplate(
|
||||||
|
DIRECT_MESSAGE_FILE,
|
||||||
|
p.value.url.toString(),
|
||||||
|
imeta.slice(1).filter(nthNe(0, "url")),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
buffer.push(p.raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addTemplate(DIRECT_MESSAGE, buffer.splice(0).join(""), tags)
|
||||||
|
|
||||||
|
// Split the message into multiple pieces so that we can use kind 15 to send images per nip 17
|
||||||
|
// Sleep 1 second between each one to make sure timestamps are distinct
|
||||||
|
const thunks = await Promise.all(
|
||||||
|
Array.from(enumerate(templates)).map(([i, event]) =>
|
||||||
|
sendWrapped({
|
||||||
|
event,
|
||||||
|
recipients: pubkeys,
|
||||||
|
delay: $userSettingsValues.send_delay + ms(i),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
pushToast({
|
||||||
|
timeout: 30_000,
|
||||||
|
children: {
|
||||||
|
component: ThunkToast,
|
||||||
|
props: {thunk: mergeThunks(thunks)},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
clearParent()
|
||||||
|
clearEventToEdit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onEscape = () => {
|
||||||
|
clearParent()
|
||||||
|
clearEventToEdit()
|
||||||
|
}
|
||||||
|
|
||||||
|
const canEditEvent = (event: TrustedEvent) =>
|
||||||
|
event.pubkey === $pubkey &&
|
||||||
|
event.kind === DIRECT_MESSAGE &&
|
||||||
|
event.created_at >= ago(500, MINUTE)
|
||||||
|
|
||||||
|
const onEditEvent = (event: TrustedEvent) => {
|
||||||
|
clearParent()
|
||||||
|
eventToEdit = event
|
||||||
|
}
|
||||||
|
|
||||||
|
const onEditPrevious = () => ifLet($chat?.messages.toReversed().find(canEditEvent), onEditEvent)
|
||||||
|
|
||||||
let loading = $state(true)
|
let loading = $state(true)
|
||||||
let compose: ChatCompose | undefined = $state()
|
let compose: ChatCompose | undefined = $state()
|
||||||
let parent: TrustedEvent | undefined = $state()
|
let parent: TrustedEvent | undefined = $state()
|
||||||
|
let eventToEdit: TrustedEvent | undefined = $state()
|
||||||
let chatCompose: HTMLElement | undefined = $state()
|
let chatCompose: HTMLElement | undefined = $state()
|
||||||
let dynamicPadding: HTMLElement | undefined = $state()
|
let dynamicPadding: HTMLElement | undefined = $state()
|
||||||
|
|
||||||
@@ -204,36 +249,36 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<PageBar>
|
<PageBar>
|
||||||
{#snippet title()}
|
<div class="flex items-center justify-between gap-4">
|
||||||
<Button class="flex flex-col gap-1 sm:flex-row sm:gap-2" onclick={showMembers}>
|
<div class="ellipsize flex items-center gap-4 whitespace-nowrap">
|
||||||
{#if others.length === 0}
|
<Button class="flex flex-col gap-1 sm:flex-row sm:gap-2" onclick={showMembers}>
|
||||||
<div class="row-2">
|
{#if others.length === 0}
|
||||||
<ProfileCircle pubkey={$pubkey!} size={5} />
|
<div class="row-2">
|
||||||
<ProfileName pubkey={$pubkey!} />
|
<ProfileCircle pubkey={$pubkey!} size={5} />
|
||||||
</div>
|
<ProfileName pubkey={$pubkey!} />
|
||||||
{:else if others.length === 1}
|
</div>
|
||||||
<div class="row-2">
|
{:else if others.length === 1}
|
||||||
<ProfileCircle pubkey={others[0]} size={5} />
|
<div class="row-2">
|
||||||
<ProfileName pubkey={others[0]} />
|
<ProfileCircle pubkey={others[0]} size={5} />
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<ProfileCircles pubkeys={others} size={5} />
|
|
||||||
<p class="overflow-hidden text-ellipsis whitespace-nowrap">
|
|
||||||
<ProfileName pubkey={others[0]} />
|
<ProfileName pubkey={others[0]} />
|
||||||
and
|
</div>
|
||||||
{#if others.length === 2}
|
{:else}
|
||||||
<ProfileName pubkey={others[1]} />
|
<div class="flex items-center gap-2">
|
||||||
{:else}
|
<ProfileCircles pubkeys={others} size={5} />
|
||||||
{others.length - 1}
|
<p class="overflow-hidden text-ellipsis whitespace-nowrap">
|
||||||
{others.length > 2 ? "others" : "other"}
|
<ProfileName pubkey={others[0]} />
|
||||||
{/if}
|
and
|
||||||
</p>
|
{#if others.length === 2}
|
||||||
</div>
|
<ProfileName pubkey={others[1]} />
|
||||||
{/if}
|
{:else}
|
||||||
</Button>
|
{others.length - 1}
|
||||||
{/snippet}
|
{others.length > 2 ? "others" : "other"}
|
||||||
{#snippet action()}
|
{/if}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
{#if remove($pubkey, missingRelayLists).length > 0}
|
{#if remove($pubkey, missingRelayLists).length > 0}
|
||||||
{@const count = remove($pubkey, missingRelayLists).length}
|
{@const count = remove($pubkey, missingRelayLists).length}
|
||||||
{@const label = count > 1 ? "lists are" : "list is"}
|
{@const label = count > 1 ? "lists are" : "list is"}
|
||||||
@@ -244,7 +289,7 @@
|
|||||||
{count}
|
{count}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/snippet}
|
</div>
|
||||||
</PageBar>
|
</PageBar>
|
||||||
|
|
||||||
<PageContent class="flex flex-col-reverse gap-2 pt-4">
|
<PageContent class="flex flex-col-reverse gap-2 pt-4">
|
||||||
@@ -285,7 +330,9 @@
|
|||||||
event={$state.snapshot(value as TrustedEvent)}
|
event={$state.snapshot(value as TrustedEvent)}
|
||||||
{pubkeys}
|
{pubkeys}
|
||||||
{showPubkey}
|
{showPubkey}
|
||||||
{replyTo} />
|
{replyTo}
|
||||||
|
canEdit={canEditEvent}
|
||||||
|
onEdit={onEditEvent} />
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
<p class="m-auto flex h-10 max-w-sm flex-col items-center justify-center gap-4 py-20 text-center">
|
<p class="m-auto flex h-10 max-w-sm flex-col items-center justify-center gap-4 py-20 text-center">
|
||||||
@@ -305,6 +352,16 @@
|
|||||||
{#if parent}
|
{#if parent}
|
||||||
<ChatComposeParent event={parent} clear={clearParent} verb="Replying to" />
|
<ChatComposeParent event={parent} clear={clearParent} verb="Replying to" />
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if eventToEdit}
|
||||||
|
<ChatComposeEdit clear={clearEventToEdit} />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<ChatCompose bind:this={compose} {onSubmit} />
|
{#key eventToEdit}
|
||||||
|
<ChatCompose
|
||||||
|
bind:this={compose}
|
||||||
|
{onSubmit}
|
||||||
|
{onEscape}
|
||||||
|
{onEditPrevious}
|
||||||
|
content={eventToEdit?.content} />
|
||||||
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {onDestroy, onMount} from "svelte"
|
||||||
import {writable} from "svelte/store"
|
import {writable} from "svelte/store"
|
||||||
import type {EventContent} from "@welshman/util"
|
import type {EventContent} from "@welshman/util"
|
||||||
import {isMobile, preventDefault} from "@lib/html"
|
import {isMobile, preventDefault} from "@lib/html"
|
||||||
@@ -10,10 +11,13 @@
|
|||||||
import {makeEditor} from "@app/editor"
|
import {makeEditor} from "@app/editor"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
content?: string
|
||||||
|
onEscape?: () => void
|
||||||
|
onEditPrevious?: () => void
|
||||||
onSubmit: (event: EventContent) => void
|
onSubmit: (event: EventContent) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const {onSubmit}: Props = $props()
|
const {content, onEscape, onEditPrevious, onSubmit}: Props = $props()
|
||||||
|
|
||||||
const autofocus = !isMobile
|
const autofocus = !isMobile
|
||||||
|
|
||||||
@@ -21,6 +25,19 @@
|
|||||||
|
|
||||||
export const focus = () => editor.then(ed => ed.chain().focus().run())
|
export const focus = () => editor.then(ed => ed.chain().focus().run())
|
||||||
|
|
||||||
|
export const canEnterEditPrevious = () =>
|
||||||
|
editor.then(ed => ed.getText({blockSeparator: "\n"}) === "")
|
||||||
|
|
||||||
|
const handleKeyDown = async (event: KeyboardEvent) => {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
onEscape?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === "ArrowUp" && (await canEnterEditPrevious())) {
|
||||||
|
onEditPrevious?.()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const uploadFiles = () => editor.then(ed => ed.chain().selectFiles().run())
|
const uploadFiles = () => editor.then(ed => ed.chain().selectFiles().run())
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
@@ -38,12 +55,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const editor = makeEditor({
|
const editor = makeEditor({
|
||||||
|
content,
|
||||||
autofocus,
|
autofocus,
|
||||||
submit,
|
submit,
|
||||||
uploading,
|
uploading,
|
||||||
aggressive: true,
|
aggressive: true,
|
||||||
encryptFiles: true,
|
encryptFiles: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
const ed = await editor
|
||||||
|
ed.view.dom.addEventListener("keydown", handleKeyDown)
|
||||||
|
})
|
||||||
|
|
||||||
|
onDestroy(async () => {
|
||||||
|
const ed = await editor
|
||||||
|
ed?.view?.dom.removeEventListener("keydown", handleKeyDown)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="relative z-feature flex gap-2 p-2" onsubmit={preventDefault(submit)}>
|
<form class="relative z-feature flex gap-2 p-2" onsubmit={preventDefault(submit)}>
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {slide} from "@lib/transition"
|
||||||
|
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
|
||||||
|
const {
|
||||||
|
clear,
|
||||||
|
}: {
|
||||||
|
clear: () => void
|
||||||
|
} = $props()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="relative flex h-8 items-center justify-between border-l-2 border-solid border-primary bg-base-300 px-2 pr-7 text-xs"
|
||||||
|
transition:slide>
|
||||||
|
<p class="text-primary">Editing message</p>
|
||||||
|
<Button onclick={clear} class="flex items-center">
|
||||||
|
<Icon icon={CloseCircle} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {goto} from "$app/navigation"
|
|
||||||
import {preventDefault} from "@lib/html"
|
|
||||||
import {shouldUnwrap} from "@welshman/app"
|
|
||||||
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 {PLATFORM_NAME} from "@app/core/state"
|
|
||||||
import {clearModals} from "@app/util/modal"
|
|
||||||
|
|
||||||
const {next} = $props()
|
|
||||||
|
|
||||||
const nextUrl = $state.snapshot(next)
|
|
||||||
|
|
||||||
let loading = $state(false)
|
|
||||||
|
|
||||||
const submit = async () => {
|
|
||||||
loading = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
shouldUnwrap.set(true)
|
|
||||||
clearModals()
|
|
||||||
goto(nextUrl)
|
|
||||||
} finally {
|
|
||||||
loading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const back = () => history.back()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Modal tag="form" onsubmit={preventDefault(submit)}>
|
|
||||||
<ModalBody>
|
|
||||||
<ModalHeader>
|
|
||||||
<ModalTitle>Enable Messages</ModalTitle>
|
|
||||||
<ModalSubtitle>Do you want to enable direct messages?</ModalSubtitle>
|
|
||||||
</ModalHeader>
|
|
||||||
<p>
|
|
||||||
By default, direct messages are disabled, since loading them requires
|
|
||||||
{PLATFORM_NAME} to download and decrypt a lot of data.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
If you'd like to enable them, please make sure your signer is set up to to auto-approve
|
|
||||||
requests to decrypt data.
|
|
||||||
</p>
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter>
|
|
||||||
<Button class="btn btn-link" onclick={back}>
|
|
||||||
<Icon icon={AltArrowLeft} />
|
|
||||||
Go back
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
|
||||||
<Spinner {loading}>Enable Messages</Spinner>
|
|
||||||
<Icon icon={AltArrowRight} />
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</Modal>
|
|
||||||
@@ -23,11 +23,13 @@
|
|||||||
interface Props {
|
interface Props {
|
||||||
event: TrustedEvent
|
event: TrustedEvent
|
||||||
replyTo: (event: TrustedEvent) => void
|
replyTo: (event: TrustedEvent) => void
|
||||||
|
canEdit?: (event: TrustedEvent) => boolean
|
||||||
|
onEdit?: (event: TrustedEvent) => void
|
||||||
pubkeys: string[]
|
pubkeys: string[]
|
||||||
showPubkey?: boolean
|
showPubkey?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const {event, replyTo, pubkeys, showPubkey = false}: Props = $props()
|
const {event, replyTo, canEdit, onEdit, pubkeys, showPubkey = false}: Props = $props()
|
||||||
|
|
||||||
const isOwn = event.pubkey === $pubkey
|
const isOwn = event.pubkey === $pubkey
|
||||||
const profileDisplay = deriveProfileDisplay(event.pubkey)
|
const profileDisplay = deriveProfileDisplay(event.pubkey)
|
||||||
@@ -35,6 +37,7 @@
|
|||||||
const [_, colorValue] = colors[hash(event.pubkey) % colors.length]
|
const [_, colorValue] = colors[hash(event.pubkey) % colors.length]
|
||||||
|
|
||||||
const reply = () => replyTo(event)
|
const reply = () => replyTo(event)
|
||||||
|
const edit = canEdit?.(event) ? () => onEdit?.(event) : undefined
|
||||||
|
|
||||||
const deleteReaction = (event: TrustedEvent) =>
|
const deleteReaction = (event: TrustedEvent) =>
|
||||||
sendWrapped({event: makeDelete({event, protect: false}), recipients: pubkeys})
|
sendWrapped({event: makeDelete({event, protect: false}), recipients: pubkeys})
|
||||||
@@ -44,7 +47,7 @@
|
|||||||
|
|
||||||
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey})
|
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey})
|
||||||
|
|
||||||
const showMobileMenu = () => pushModal(ChatMessageMenuMobile, {event, pubkeys, reply})
|
const showMobileMenu = () => pushModal(ChatMessageMenuMobile, {event, pubkeys, reply, edit})
|
||||||
|
|
||||||
const togglePopover = () => {
|
const togglePopover = () => {
|
||||||
if (popoverIsVisible) {
|
if (popoverIsVisible) {
|
||||||
@@ -71,7 +74,7 @@
|
|||||||
<Tippy
|
<Tippy
|
||||||
bind:popover
|
bind:popover
|
||||||
component={ChatMessageMenu}
|
component={ChatMessageMenu}
|
||||||
props={{event, pubkeys, popover, replyTo}}
|
props={{event, pubkeys, popover, replyTo, edit}}
|
||||||
params={{
|
params={{
|
||||||
interactive: true,
|
interactive: true,
|
||||||
trigger: "manual",
|
trigger: "manual",
|
||||||
@@ -93,7 +96,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<div class="flex min-w-0 flex-col" class:items-end={isOwn}>
|
<div class="flex min-w-0 flex-col" class:items-end={isOwn}>
|
||||||
<TapTarget
|
<TapTarget
|
||||||
class="bg-alt chat-bubble mx-1 mb-2 flex cursor-auto flex-col gap-1 text-left lg:max-w-2xl"
|
class="bg-alt chat-bubble mx-1 mb-2 flex cursor-auto flex-col gap-1 text-left lg:max-w-2xl min-w-[100px]"
|
||||||
onTap={showMobileMenu}>
|
onTap={showMobileMenu}>
|
||||||
{#if showPubkey}
|
{#if showPubkey}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
|||||||
@@ -4,12 +4,14 @@
|
|||||||
import ChatMessageEmojiButton from "@app/components/ChatMessageEmojiButton.svelte"
|
import ChatMessageEmojiButton from "@app/components/ChatMessageEmojiButton.svelte"
|
||||||
import EventInfo from "@app/components/EventInfo.svelte"
|
import EventInfo from "@app/components/EventInfo.svelte"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
import Pen from "@assets/icons/pen.svg?dataurl"
|
||||||
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
||||||
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
||||||
|
|
||||||
const {event, pubkeys, popover, replyTo} = $props()
|
const {event, pubkeys, popover, replyTo, edit} = $props()
|
||||||
|
|
||||||
const reply = () => replyTo(event)
|
const reply = () => replyTo(event)
|
||||||
|
const onEdit = () => edit?.()
|
||||||
|
|
||||||
const showInfo = () => {
|
const showInfo = () => {
|
||||||
popover.hide()
|
popover.hide()
|
||||||
@@ -24,6 +26,11 @@
|
|||||||
<Icon size={4} icon={Reply} />
|
<Icon size={4} icon={Reply} />
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if edit}
|
||||||
|
<Button class="btn join-item btn-xs" onclick={onEdit}>
|
||||||
|
<Icon size={4} icon={Pen} />
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
<Button class="btn join-item btn-xs" onclick={showInfo}>
|
<Button class="btn join-item btn-xs" onclick={showInfo}>
|
||||||
<Icon size={4} icon={Code2} />
|
<Icon size={4} icon={Code2} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {sendWrapped} from "@welshman/app"
|
import {sendWrapped} from "@welshman/app"
|
||||||
import SmileCircle from "@assets/icons/smile-circle.svg?dataurl"
|
import SmileCircle from "@assets/icons/smile-circle.svg?dataurl"
|
||||||
|
import Pen from "@assets/icons/pen.svg?dataurl"
|
||||||
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
||||||
import Copy from "@assets/icons/copy.svg?dataurl"
|
import Copy from "@assets/icons/copy.svg?dataurl"
|
||||||
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
||||||
@@ -20,9 +21,10 @@
|
|||||||
pubkeys: string[]
|
pubkeys: string[]
|
||||||
event: TrustedEvent
|
event: TrustedEvent
|
||||||
reply: () => void
|
reply: () => void
|
||||||
|
edit?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const {event, pubkeys, reply}: Props = $props()
|
const {event, pubkeys, reply, edit}: Props = $props()
|
||||||
|
|
||||||
const onEmoji = ((event: TrustedEvent, pubkeys: string[], emoji: NativeEmoji) => {
|
const onEmoji = ((event: TrustedEvent, pubkeys: string[], emoji: NativeEmoji) => {
|
||||||
history.back()
|
history.back()
|
||||||
@@ -39,6 +41,11 @@
|
|||||||
reply()
|
reply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sendEdit = () => {
|
||||||
|
history.back()
|
||||||
|
edit?.()
|
||||||
|
}
|
||||||
|
|
||||||
const copyText = () => {
|
const copyText = () => {
|
||||||
history.back()
|
history.back()
|
||||||
clip(event.content)
|
clip(event.content)
|
||||||
@@ -62,6 +69,12 @@
|
|||||||
<Icon size={4} icon={Reply} />
|
<Icon size={4} icon={Reply} />
|
||||||
Send Reply
|
Send Reply
|
||||||
</Button>
|
</Button>
|
||||||
|
{#if edit}
|
||||||
|
<Button class="btn btn-neutral w-full" onclick={sendEdit}>
|
||||||
|
<Icon size={4} icon={Pen} />
|
||||||
|
Edit Message
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
<Button class="btn btn-primary w-full" onclick={showEmojiPicker}>
|
<Button class="btn btn-primary w-full" onclick={showEmojiPicker}>
|
||||||
<Icon size={4} icon={SmileCircle} />
|
<Icon size={4} icon={SmileCircle} />
|
||||||
Send Reaction
|
Send Reaction
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {uniq} from "@welshman/lib"
|
||||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||||
import {getTagValue, getAddress} from "@welshman/util"
|
import {getTagValue, getTagValues, getAddress} from "@welshman/util"
|
||||||
import {pubkey} from "@welshman/app"
|
import {pubkey} from "@welshman/app"
|
||||||
import Pen2 from "@assets/icons/pen-2.svg?dataurl"
|
import Pen2 from "@assets/icons/pen-2.svg?dataurl"
|
||||||
|
import {normalizeTopic} from "@lib/util"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
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"
|
||||||
@@ -27,6 +29,7 @@
|
|||||||
const {url, event, showRoom, showActivity}: Props = $props()
|
const {url, event, showRoom, showActivity}: Props = $props()
|
||||||
|
|
||||||
const h = getTagValue("h", event.tags)
|
const h = getTagValue("h", event.tags)
|
||||||
|
const topics = getTagValues("t", event.tags)
|
||||||
const path = makeClassifiedPath(url, getAddress(event))
|
const path = makeClassifiedPath(url, getAddress(event))
|
||||||
const shouldProtect = canEnforceNip70(url)
|
const shouldProtect = canEnforceNip70(url)
|
||||||
|
|
||||||
@@ -45,6 +48,13 @@
|
|||||||
Posted in #<RoomName {h} {url} />
|
Posted in #<RoomName {h} {url} />
|
||||||
</Link>
|
</Link>
|
||||||
{/if}
|
{/if}
|
||||||
|
<div class="flex min-w-0 flex-wrap gap-2">
|
||||||
|
{#each uniq(topics) as topic (topic)}
|
||||||
|
<button type="button" class="btn btn-xs rounded-full font-normal">
|
||||||
|
#{normalizeTopic(topic)}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
|
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
|
||||||
<ThunkStatusOrDeleted {event}>
|
<ThunkStatusOrDeleted {event}>
|
||||||
<ClassifiedStatus {event} />
|
<ClassifiedStatus {event} />
|
||||||
|
|||||||
@@ -18,7 +18,8 @@
|
|||||||
const {d, title, status} = fromPairs(event.tags)
|
const {d, title, status} = fromPairs(event.tags)
|
||||||
const [_, price = 0, currency = "SAT"] = getTag("price", event.tags) || []
|
const [_, price = 0, currency = "SAT"] = getTag("price", event.tags) || []
|
||||||
const images = getTagValues("image", event.tags)
|
const images = getTagValues("image", event.tags)
|
||||||
const initialValues = {d, title, status, content, price: Number(price), currency, images}
|
const topics = getTagValues("t", event.tags)
|
||||||
|
const initialValues = {d, title, status, content, price: Number(price), currency, images, topics}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ClassifiedForm {url} {initialValues}>
|
<ClassifiedForm {url} {initialValues}>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {Snippet} from "svelte"
|
import type {Snippet} from "svelte"
|
||||||
import {randomId} from "@welshman/lib"
|
import {removeUndefined, randomId, uniq} from "@welshman/lib"
|
||||||
import {makeEvent, CLASSIFIED} from "@welshman/util"
|
import {makeEvent, CLASSIFIED} from "@welshman/util"
|
||||||
import {publishThunk} from "@welshman/app"
|
import {publishThunk} from "@welshman/app"
|
||||||
import {isMobile, preventDefault} from "@lib/html"
|
import {isMobile, preventDefault} from "@lib/html"
|
||||||
|
import {normalizeTopic} from "@lib/util"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Field from "@lib/components/Field.svelte"
|
import Field from "@lib/components/Field.svelte"
|
||||||
@@ -14,6 +15,7 @@
|
|||||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
import ImagesInput from "@lib/components/ImagesInput.svelte"
|
import ImagesInput from "@lib/components/ImagesInput.svelte"
|
||||||
import CurrencyInput from "@app/components/CurrencyInput.svelte"
|
import CurrencyInput from "@app/components/CurrencyInput.svelte"
|
||||||
|
import TopicMultiSelect from "@app/components/TopicMultiSelect.svelte"
|
||||||
import EditorContent from "@app/editor/EditorContent.svelte"
|
import EditorContent from "@app/editor/EditorContent.svelte"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {PROTECTED} from "@app/core/state"
|
import {PROTECTED} from "@app/core/state"
|
||||||
@@ -32,6 +34,7 @@
|
|||||||
currency?: string
|
currency?: string
|
||||||
images?: string[]
|
images?: string[]
|
||||||
status?: string
|
status?: string
|
||||||
|
topics?: string[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,6 +74,10 @@
|
|||||||
...ed.storage.nostr.getEditorTags(),
|
...ed.storage.nostr.getEditorTags(),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
for (const topic of topics) {
|
||||||
|
tags.push(["t", topic])
|
||||||
|
}
|
||||||
|
|
||||||
if (await shouldProtect) {
|
if (await shouldProtect) {
|
||||||
tags.push(PROTECTED)
|
tags.push(PROTECTED)
|
||||||
}
|
}
|
||||||
@@ -118,6 +125,7 @@
|
|||||||
let price = $state(Number(initialValues?.price || 0))
|
let price = $state(Number(initialValues?.price || 0))
|
||||||
let currency = $state(initialValues?.currency || "SAT")
|
let currency = $state(initialValues?.currency || "SAT")
|
||||||
let images = $state<(string | File)[]>(initialValues?.images || [])
|
let images = $state<(string | File)[]>(initialValues?.images || [])
|
||||||
|
let topics = $state(uniq(removeUndefined((initialValues?.topics || []).map(normalizeTopic))))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal tag="form" onsubmit={preventDefault(submit)}>
|
<Modal tag="form" onsubmit={preventDefault(submit)}>
|
||||||
@@ -150,6 +158,14 @@
|
|||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Field>
|
</Field>
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Topics</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<TopicMultiSelect bind:value={topics} />
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
<Field>
|
<Field>
|
||||||
{#snippet label()}
|
{#snippet label()}
|
||||||
<p>Price*</p>
|
<p>Price*</p>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
class="col-2 card2 bg-alt w-full cursor-pointer shadow-xl"
|
class="cv col-2 card2 bg-alt w-full cursor-pointer shadow-xl"
|
||||||
href={makeClassifiedPath(url, getAddress(event))}>
|
href={makeClassifiedPath(url, getAddress(event))}>
|
||||||
{#if title}
|
{#if title}
|
||||||
<div class="flex w-full items-center justify-between gap-2">
|
<div class="flex w-full items-center justify-between gap-2">
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {call, ellipsize, displayUrl, postJson} from "@welshman/lib"
|
import {call, ellipsize, displayUrl, postJson} from "@welshman/lib"
|
||||||
import {isRelayUrl} from "@welshman/util"
|
import {isRelayUrl, getTagValue} from "@welshman/util"
|
||||||
import {preventDefault, stopPropagation} from "@lib/html"
|
import {preventDefault, stopPropagation} from "@lib/html"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
|
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
|
||||||
import ContentLinkBlockImage from "@app/components/ContentLinkBlockImage.svelte"
|
import ContentLinkBlockImage from "@app/components/ContentLinkBlockImage.svelte"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
import {dufflepud, PLATFORM_URL} from "@app/core/state"
|
import {dufflepud, PLATFORM_URL, IMAGE_CONTENT_TYPES, VIDEO_CONTENT_TYPES} from "@app/core/state"
|
||||||
import {makeSpacePath} from "@app/util/routes"
|
import {makeSpacePath} from "@app/util/routes"
|
||||||
|
|
||||||
const {value, event} = $props()
|
const {value, event} = $props()
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
let hideImage = $state(false)
|
let hideImage = $state(false)
|
||||||
|
|
||||||
const url = value.url.toString()
|
const url = value.url.toString()
|
||||||
|
const fileType = getTagValue("file-type", event.tags) || ""
|
||||||
const [href, external] = call(() => {
|
const [href, external] = call(() => {
|
||||||
if (isRelayUrl(url)) return [makeSpacePath(url), false]
|
if (isRelayUrl(url)) return [makeSpacePath(url), false]
|
||||||
if (url.startsWith(PLATFORM_URL)) return [url.replace(PLATFORM_URL, ""), false]
|
if (url.startsWith(PLATFORM_URL)) return [url.replace(PLATFORM_URL, ""), false]
|
||||||
@@ -40,11 +41,11 @@
|
|||||||
|
|
||||||
<Link {external} {href} class="my-2 block">
|
<Link {external} {href} class="my-2 block">
|
||||||
<div class="overflow-hidden rounded-box">
|
<div class="overflow-hidden rounded-box">
|
||||||
{#if url.match(/\.(mov|webm|mp4)$/)}
|
{#if url.match(/\.(mov|webm|mp4)$/) || VIDEO_CONTENT_TYPES.includes(fileType)}
|
||||||
<video controls src={url} class="max-h-96 rounded-box object-contain object-center">
|
<video controls src={url} class="max-h-96 rounded-box object-contain object-center">
|
||||||
<track kind="captions" />
|
<track kind="captions" />
|
||||||
</video>
|
</video>
|
||||||
{:else if url.match(/\.(jpe?g|png|gif|webp)$/)}
|
{:else if url.match(/\.(jpe?g|png|gif|webp)$/) || IMAGE_CONTENT_TYPES.includes(fileType)}
|
||||||
<button type="button" onclick={stopPropagation(preventDefault(expand))}>
|
<button type="button" onclick={stopPropagation(preventDefault(expand))}>
|
||||||
<ContentLinkBlockImage {value} {event} class="m-auto max-h-96 rounded-box" />
|
<ContentLinkBlockImage {value} {event} class="m-auto max-h-96 rounded-box" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount, onDestroy} from "svelte"
|
import {onMount, onDestroy} from "svelte"
|
||||||
import {displayUrl} from "@welshman/lib"
|
import {displayUrl, once} from "@welshman/lib"
|
||||||
import {
|
import {
|
||||||
getTags,
|
getTags,
|
||||||
getBlob,
|
getBlob,
|
||||||
@@ -26,8 +26,24 @@
|
|||||||
const key = getTagValue("decryption-key", meta)
|
const key = getTagValue("decryption-key", meta)
|
||||||
const nonce = getTagValue("decryption-nonce", meta)
|
const nonce = getTagValue("decryption-nonce", meta)
|
||||||
const algorithm = getTagValue("encryption-algorithm", meta)
|
const algorithm = getTagValue("encryption-algorithm", meta)
|
||||||
|
const mime = getTagValue("m", meta)
|
||||||
|
const fileName =
|
||||||
|
getTagValue("filename", meta) ||
|
||||||
|
getTagValue("name", meta) ||
|
||||||
|
decodeURIComponent(new URL(url).pathname.split("/").filter(Boolean).at(-1) || "image")
|
||||||
|
|
||||||
const onError = async () => {
|
const revokeSrc = () => {
|
||||||
|
if (src.startsWith("blob:")) {
|
||||||
|
URL.revokeObjectURL(src)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setBlobSrc = (data: Blob | Uint8Array<ArrayBuffer>, type?: string) => {
|
||||||
|
revokeSrc()
|
||||||
|
src = URL.createObjectURL(new File([data], fileName, type ? {type} : undefined))
|
||||||
|
}
|
||||||
|
|
||||||
|
const onError = once(async () => {
|
||||||
// If the image failed to load, try authenticating
|
// If the image failed to load, try authenticating
|
||||||
if (hash && $signer) {
|
if (hash && $signer) {
|
||||||
const server = new URL(url).origin
|
const server = new URL(url).origin
|
||||||
@@ -36,14 +52,15 @@
|
|||||||
const res = await getBlob(server, hash, {authEvent})
|
const res = await getBlob(server, hash, {authEvent})
|
||||||
|
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
src = URL.createObjectURL(await res.blob())
|
const blob = await res.blob()
|
||||||
|
setBlobSrc(blob, blob.type || undefined)
|
||||||
} else {
|
} else {
|
||||||
hasError = true
|
hasError = true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
hasError = true
|
hasError = true
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
let hasError = $state(false)
|
let hasError = $state(false)
|
||||||
let src = $state("")
|
let src = $state("")
|
||||||
@@ -57,7 +74,7 @@
|
|||||||
const ciphertext = new Uint8Array(await response.arrayBuffer())
|
const ciphertext = new Uint8Array(await response.arrayBuffer())
|
||||||
const decryptedData = await decryptFile({ciphertext, key, nonce, algorithm})
|
const decryptedData = await decryptFile({ciphertext, key, nonce, algorithm})
|
||||||
|
|
||||||
src = URL.createObjectURL(new Blob([new Uint8Array(decryptedData)]))
|
setBlobSrc(new Uint8Array(decryptedData), mime)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
src = url
|
src = url
|
||||||
@@ -65,7 +82,7 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
URL.revokeObjectURL(src)
|
revokeSrc()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {call, displayUrl} from "@welshman/lib"
|
import {call, displayUrl} from "@welshman/lib"
|
||||||
import {isRelayUrl} from "@welshman/util"
|
import {isRelayUrl, getTagValue} from "@welshman/util"
|
||||||
import {preventDefault, stopPropagation} from "@lib/html"
|
import {preventDefault, stopPropagation} from "@lib/html"
|
||||||
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
|
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
import {PLATFORM_URL} from "@app/core/state"
|
import {PLATFORM_URL, IMAGE_CONTENT_TYPES} from "@app/core/state"
|
||||||
import {makeSpacePath} from "@app/util/routes"
|
import {makeSpacePath} from "@app/util/routes"
|
||||||
|
|
||||||
const {value, event} = $props()
|
const {value, event} = $props()
|
||||||
|
|
||||||
const url = value.url.toString()
|
const url = value.url.toString()
|
||||||
|
const fileType = getTagValue("file-type", event.tags) || ""
|
||||||
const [href, external] = call(() => {
|
const [href, external] = call(() => {
|
||||||
if (isRelayUrl(url)) return [makeSpacePath(url), false]
|
if (isRelayUrl(url)) return [makeSpacePath(url), false]
|
||||||
if (url.startsWith(PLATFORM_URL)) return [url.replace(PLATFORM_URL, ""), false]
|
if (url.startsWith(PLATFORM_URL)) return [url.replace(PLATFORM_URL, ""), false]
|
||||||
@@ -23,7 +24,7 @@
|
|||||||
const expand = () => pushModal(ContentLinkDetail, {value, event}, {fullscreen: true})
|
const expand = () => pushModal(ContentLinkDetail, {value, event}, {fullscreen: true})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if url.match(/\.(jpe?g|png|gif|webp)$/)}
|
{#if url.match(/\.(jpe?g|png|gif|webp)$/) || IMAGE_CONTENT_TYPES.includes(fileType)}
|
||||||
<!-- Use a real link so people can copy the href -->
|
<!-- Use a real link so people can copy the href -->
|
||||||
<a
|
<a
|
||||||
href={url}
|
href={url}
|
||||||
|
|||||||