Compare commits
135 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ea4aeb75c | |||
| 456d111925 | |||
| 837ae4b38e | |||
| ffbcbf86c3 | |||
| bcda637192 | |||
| 72c7dd6126 | |||
| a2a4b3599f | |||
| 4955a4f16c | |||
| bb1ff4fb11 | |||
| b81f7c9ed3 | |||
| 689cfb6d45 | |||
| 9da3141650 | |||
| e4fe18df2f | |||
| ba80ebac63 | |||
| d4943daa82 | |||
| cde03ec0fe | |||
| 4f6c08f8a2 | |||
| 38e0fc53ad | |||
| 2a30ca5306 | |||
| 4a4ea13bef | |||
| 239bd3f31a | |||
| 831ec05012 | |||
| 0cc0598287 | |||
| 0a5bc618c2 | |||
| 069904f07a | |||
| 03b42c8276 | |||
| 8697cc23be | |||
| 69e1f97e72 | |||
| 3e832af3e4 | |||
| 84b8650fa4 | |||
| 83abb5aa94 | |||
| a12eddb47b | |||
| c87166247c | |||
| 037c8cb41b | |||
| 79de2e1176 | |||
| d4b026a3ad | |||
| 00f383ff2e | |||
| 6f6bb508db | |||
| e2a0672ca5 | |||
| e2a5fe7a79 | |||
| 5d02ae75dc | |||
| 2460bbbc83 | |||
| 084d8d931b | |||
| 6ee4ac1a89 | |||
| 1d07097350 | |||
| 63d6b362c7 | |||
| bfed277ea9 | |||
| 9e8aa2ef3a | |||
| 4bbc0878f7 | |||
| 16a3ba2a9b | |||
| 7c11eb8947 | |||
| 6bdc8d4d9f | |||
| b9048936ba | |||
| b9620f4443 | |||
| f2249fe592 | |||
| fd42a0e8d4 | |||
| 37d52ba35f | |||
| 3037323dc0 | |||
| 5301ef876d | |||
| aa054d8b1a | |||
| 3655790e5f | |||
| 6cca823ed4 | |||
| 18a383edab | |||
| 43da7d628e | |||
| 2fae3ca248 | |||
| d99ada44f5 | |||
| cb0119b9b8 | |||
| dac9ef8e4e | |||
| 528917b90e | |||
| a22db78967 | |||
| 5718510779 | |||
| f877dc7fbe | |||
| df03fb1116 | |||
| 7455b49f8d | |||
| ae00eb0b9c | |||
| b82e434c70 | |||
| 576c9c2c95 | |||
| cef046b3ae | |||
| 18ae6f6044 | |||
| 664f3c01e0 | |||
| 15e82c4e41 | |||
| 397ecf773e | |||
| 45397e7fb8 | |||
| 11aa841241 | |||
| cc1c18d55f | |||
| e3fbd69e6e | |||
| ac756bf266 | |||
| 8e28ff13e9 | |||
| d8b87db784 | |||
| 0b8c6c4a49 | |||
| 9f4f468bf0 | |||
| 7563dff621 | |||
| f782898b62 | |||
| d0601400cd | |||
| d262da39e5 | |||
| 7d617d8399 | |||
| d2b7db18af | |||
| 89c2690254 | |||
| 34945d1c42 | |||
| 43b207c4dc | |||
| 55efb3fdfd | |||
| c4a1ad2e33 | |||
| fd8442c632 | |||
| e0875eb9b9 | |||
| 962ac7d80c | |||
| 5338ee11bc | |||
| 6d2e9a037d | |||
| ac8530bd9a | |||
| f7d11cf124 | |||
| 72d85e5740 | |||
| e57b5721f6 | |||
| 4ba6c72459 | |||
| c33698c662 | |||
| cf4e40c4cf | |||
| 664da505cd | |||
| 573d4e3cfb | |||
| b2dc41f25b | |||
| b3bc0e4957 | |||
| 0e79e5b9cc | |||
| 34c7bfcffb | |||
| fd9fee8f50 | |||
| b14c3ab345 | |||
| 823058e335 | |||
| 60ec6924f3 | |||
| 18fc895fcb | |||
| 42295159a0 | |||
| db408ac30d | |||
| 1ced5689c3 | |||
| 263a803875 | |||
| 58afb8fa0c | |||
| 4aaa19ea1b | |||
| 2f9010cd13 | |||
| 12fcdfcd4f | |||
| 317ab57ed2 | |||
| 52ef67740a |
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
android
|
||||||
|
ios
|
||||||
|
build
|
||||||
@@ -5,10 +5,14 @@ VITE_PLATFORM_TERMS=https://flotilla.social/terms
|
|||||||
VITE_PLATFORM_PRIVACY=https://flotilla.social/privacy
|
VITE_PLATFORM_PRIVACY=https://flotilla.social/privacy
|
||||||
VITE_PLATFORM_NAME=Flotilla
|
VITE_PLATFORM_NAME=Flotilla
|
||||||
VITE_PLATFORM_LOGO=static/flotilla.png
|
VITE_PLATFORM_LOGO=static/flotilla.png
|
||||||
VITE_PLATFORM_RELAY=
|
VITE_PLATFORM_RELAYS=
|
||||||
VITE_PLATFORM_ACCENT="#7161FF"
|
VITE_PLATFORM_ACCENT="#7161FF"
|
||||||
|
VITE_PLATFORM_SECONDARY="#EB5E28"
|
||||||
VITE_PLATFORM_DESCRIPTION="Flotilla is nostr — for communities."
|
VITE_PLATFORM_DESCRIPTION="Flotilla is nostr — for communities."
|
||||||
VITE_INDEXER_RELAYS=wss://purplepag.es/,wss://relay.damus.io/,wss://relay.nostr.band/
|
VITE_INDEXER_RELAYS=wss://purplepag.es/,wss://relay.damus.io/,wss://relay.nostr.band/,wss://indexer.coracle.social/
|
||||||
VITE_SIGNER_RELAYS=wss://relay.nsec.app/,wss://bucket.coracle.social/
|
VITE_SIGNER_RELAYS=wss://relay.nsec.app/,wss://bucket.coracle.social/
|
||||||
|
VITE_NOTIFIER_PUBKEY=27b7c2ed89ef78322114225ea3ebf5f72c7767c2528d4d0c1854d039c00085df
|
||||||
|
VITE_NOTIFIER_RELAY=wss://anchor.coracle.social/
|
||||||
|
VITE_VAPID_PUBLIC_KEY=BIt2D4BdgdbCowD_0d3Np6GbrIGHxd7aIEUeZNe3hQuRlHz02OhzvDaai0XSFoJYVzSzdMjdyW-QhvW9_yq8j4Y
|
||||||
VITE_GLITCHTIP_API_KEY=
|
VITE_GLITCHTIP_API_KEY=
|
||||||
GLITCHTIP_AUTH_TOKEN=
|
GLITCHTIP_AUTH_TOKEN=
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
package-lock.json -diff
|
pnpm-lock.yaml -diff
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
name: Docker
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ['master']
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push-image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
attestations: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Log in to the Container registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
with:
|
||||||
|
driver: docker-container
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
id: push
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
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
|
||||||
+2
-1
@@ -1,5 +1,5 @@
|
|||||||
# Env
|
# Env
|
||||||
.env.local
|
.env
|
||||||
|
|
||||||
# Vite
|
# Vite
|
||||||
vite.config.js.timestamp-*
|
vite.config.js.timestamp-*
|
||||||
@@ -60,6 +60,7 @@ google-services.json
|
|||||||
GoogleService-Info.plist
|
GoogleService-Info.plist
|
||||||
|
|
||||||
# IDEs and editors
|
# IDEs and editors
|
||||||
|
.roo
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,8 @@
|
|||||||
pnpm run lint
|
pnpm run lint
|
||||||
pnpm run check
|
pnpm run check
|
||||||
|
|
||||||
|
if [[ ! -z $(cat package.json | grep 'link:') ]]; then
|
||||||
|
echo "Some packages are linked to local files!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,74 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
# 1.2.3
|
||||||
|
|
||||||
|
* Add `created_at` to event info dialog
|
||||||
|
* Add signer status to profile page
|
||||||
|
* Re-work bunker login flow
|
||||||
|
* Add in-app onboarding flow
|
||||||
|
* Only protect events if relay authenticates
|
||||||
|
* Filter out non-global chats from global chat
|
||||||
|
* Improve publish status indicator
|
||||||
|
* Fix encrypted upload content type
|
||||||
|
* Add relays to event details dialog
|
||||||
|
* Add universal link handler for apps
|
||||||
|
|
||||||
|
# 1.2.2
|
||||||
|
|
||||||
|
* Fix phantom chat notifications
|
||||||
|
* Fix zaps on mobile
|
||||||
|
|
||||||
|
# 1.2.1
|
||||||
|
|
||||||
|
* Add zaps to chat, threads, and events
|
||||||
|
* Add funding goals
|
||||||
|
* Add NWC support
|
||||||
|
* Add wallet settings page
|
||||||
|
* Handle invalid bunker url
|
||||||
|
* Fix sidebar overflow
|
||||||
|
* Fix profile npub display
|
||||||
|
|
||||||
|
# 1.2.0
|
||||||
|
|
||||||
|
* Fix sort order of thread comments
|
||||||
|
* Fix link display when no title is available
|
||||||
|
* Fix making profiles non-protected
|
||||||
|
* Replace bunker url with relay claims for notifier auth
|
||||||
|
* Add push notifications on all platforms
|
||||||
|
* Add "mark all as read" on desktop
|
||||||
|
* Re-design space dashboard
|
||||||
|
|
||||||
|
# 1.1.1
|
||||||
|
|
||||||
|
* Add chat quick link
|
||||||
|
|
||||||
|
# 1.1.0
|
||||||
|
|
||||||
|
* Add better theming support
|
||||||
|
* Improve forms for entering invite codes
|
||||||
|
* Rely more heavily on NIP 29 for rooms
|
||||||
|
* Support multiple platform relays
|
||||||
|
* Remove default general room
|
||||||
|
* Remove room tag from threads/calendars
|
||||||
|
* Show pubkey on profile detail
|
||||||
|
* Support pasting pubkey into chat start dialog
|
||||||
|
* Add minimal style for quoted messages
|
||||||
|
|
||||||
|
# 1.0.4
|
||||||
|
|
||||||
|
* Fix thunk status click handler
|
||||||
|
* Remove duplicate dependencies
|
||||||
|
* Improve navigation on white-labeled instances
|
||||||
|
* Add setting for font size
|
||||||
|
|
||||||
|
# 1.0.3
|
||||||
|
|
||||||
|
* Add light theme
|
||||||
|
* Use correct alerts server
|
||||||
|
* Ignore relay errors for claims
|
||||||
|
* Fix inline code blocks
|
||||||
|
* Add custom emoji parsing and display
|
||||||
|
|
||||||
# 1.0.2
|
# 1.0.2
|
||||||
|
|
||||||
* Fix add relay button
|
* Fix add relay button
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# Contributing guidelines
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Flotilla is a svelte/typescript/capacitor project. It's intended to be an alternative to Discord for Nostr users. A high-quality UX is a priority, with an emphasis on well-tested, intuitive designs, and robust implementations.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
Run `pnpm run dev` to get a dev server, and `pnpm run check:watch` to watch for typescript errors. When you're ready to commit, a pre-commit hook will run to lint and typecheck your work. To run the project on Android or iOS, use Android Studio or Xcode.
|
||||||
|
|
||||||
|
The `master` branch is automatically deployed to production, so always work on feature branches based on the `dev` branch. This project frequently uses unreleased versions of [welshman](https://welshman.coracle.social), using `pnpm` link to hotlink a local copy of the code. To set that up, clone welshman to the parent directory of your `coracle` client, then add `link:../welshman/packages/packagename` to the `pnpm.overrides` section of your `package.json`. Below is a nodejs script that will do that automatically for you:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8'))
|
||||||
|
|
||||||
|
packageJson.pnpm.overrides = Object.keys(packageJson.dependencies)
|
||||||
|
.filter(pkg => pkg.startsWith('@welshman/'))
|
||||||
|
.reduce((acc, pkg) => {
|
||||||
|
const packageName = pkg.split('/')[1]
|
||||||
|
acc[pkg] = `link:../welshman/packages/${packageName}`
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
fs.writeFileSync('./package.json', JSON.stringify(packageJson, null, 2) + '\n')
|
||||||
|
|
||||||
|
console.log('Added welshman package overrides.')
|
||||||
|
```
|
||||||
|
|
||||||
|
Be sure to avoid committing overrides to either `package.json` or `pnpm-lock.yaml`. These overrides can generally be added, installed, and removed, and will persist until another `pnpm install` command gets run.
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
The main parts of the application are as follows:
|
||||||
|
|
||||||
|
- `static` - static assets like fonts, images, etc.
|
||||||
|
- `src/assets` - svgs for use as icons.
|
||||||
|
- `src/lib` - general purpose components and utilities.
|
||||||
|
- `src/app/core/state` - environment variables, constants, custom stores, and some utilities derived from them.
|
||||||
|
- `src/app/core/requests` - utilities related to loading data from the nostr network.
|
||||||
|
- `src/app/core/commands` - utilities related to publishing nostr events and uploading media to blossom servers.
|
||||||
|
- `src/app/utils` - other application logic, including stuff related to modals, routing, etc.
|
||||||
|
- `src/app/editor` - configuration for `@welshman/editor` for use in various app views.
|
||||||
|
- `src/app/components` - reusable components that depend on other `app` stuff.
|
||||||
|
- `src/routes` - file-based routing interpreted by sveltekit.
|
||||||
|
|
||||||
|
Application organization is based on an acyclic dependency graph:
|
||||||
|
|
||||||
|
- `routes` can depend on anything
|
||||||
|
- `app/components` can depend on anything in `app` or `lib`
|
||||||
|
- `app/utils` and `app/core` can only depend on `lib`
|
||||||
|
- `lib` (and everything else) can depend only on external libraries
|
||||||
|
|
||||||
|
The main stylistic/organizational rule when working in this project is that imports should be sorted based on the dependency graph. Third-party libraries should come first, then `lib`, then `app`.
|
||||||
|
|
||||||
|
## System Architecture
|
||||||
|
|
||||||
|
Flotilla's architecture generally mirrors the file structure. State is stored using Svelte `store`s provided either by `@welshman/app` or by `app/core/state`, allowing for idiomatic svelte 4 usage (svelte 5 runes are [ghey](https://habla.news/u/hodlbod@coracle.social/1739830562159) and not allowed outside of UI components).
|
||||||
|
|
||||||
|
State is then synchronized to local storage or indexeddb using storage helpers provided by welshman in `routes/+layout.svelte`. Other top level synchronization logic generally belongs there.
|
||||||
|
|
||||||
|
`app/core/state` contains all environment variables, constants, custom stores, and utilities derived from them. Most stores are `derived` from our event `repository` using `deriveEventsMapped`, which efficiently queries the repository and maps events to custom data structures. Some of these data structures are provided by `welshman`, and some are defined in `app/core/state`. In either case, they can always be mapped back to an event, which is important for updating replaceables without dropping unknown data.
|
||||||
|
|
||||||
|
Here are a few important domain objects:
|
||||||
|
|
||||||
|
- Spaces are relays used as community groups. Their `url`s are core to a lot of data and components, and are frequently passed around from place to place.
|
||||||
|
- Chats are direct message conversations. There is currently some ambiguity in routing, since relays that don't support NIP 29 also have a "chat" tab, which uses vanilla NIP-C7.
|
||||||
|
- NIP 29 groups are variously called "rooms" and "channels". Conventionally, a "room" is a group id, while a "channel" as an object representing the group's metadata.
|
||||||
|
- "Alerts" are records of requests the user has made to be notified, following [this NIP](https://github.com/nostr-protocol/nips/pull/1796)
|
||||||
|
|
||||||
|
`app/core/requests` contains utilities related to loading data from the nostr network. This might include feed manager utilities, loaders, or listeners.
|
||||||
|
|
||||||
|
`app/core/commands` contains utilities related to publishing nostr events and uploading media to blossom servers. This also includes utilities related to sending lighting payments, authenticating with relays, or probing relay policy. Event creation should generally be split into `make` functions which build the event, and `publish` functions which publish the event using `publishThunk`.
|
||||||
|
|
||||||
|
Any of these utilities can be included either in `app/components` or `routes`. Crucial to keep in mind is that nearly all global state runs through welshman's `repository` in a unidirectional way. To update state, run `publishThunk`, which immediately publishes the event to the local repository. State can be read from the repository using `deriveEventsMapped` or other utilities provided by welshman like `deriveProfile`.
|
||||||
|
|
||||||
|
Thunks are designed to reduce UI latency, handling signatures and delayed sending the background. In most cases, thunk status should be displayed to the user so that they can cancel sending or address errors.
|
||||||
|
|
||||||
|
Toast, modals, and sidebar dialogs are controlled in `app/util/modal` and `app/util/toast`. In both cases, component objects can be passed along with parameters, but care has to be taken that the calling component either doesn't unmount before the modal (as when one modal replaces another), or that `$state.snapshot` is appropriately called on any state runes. These components frequently run into weird svelte compiler bugs too, in which case you may have to do some silly things to cope.
|
||||||
|
|
||||||
|
## Issues and Pull Requests
|
||||||
|
|
||||||
|
All work by contributors should be done against an issue. If there is no issue for the work you're doing, please open one or ask the project owner to open one. All PRs should be opened against the `dev` branch (unless for hotfixes).
|
||||||
|
|
||||||
|
## Communication
|
||||||
|
|
||||||
|
Discussion about development is done [on Flotilla](https://app.flotilla.social/spaces/internal.coracle.social). The group is currently closed, so please let me know if you'd like access.
|
||||||
|
|
||||||
|
## Project License
|
||||||
|
|
||||||
|
This project is licensed under the MIT license. By contributing, you agree to waive all intellectual property rights to your contributions to this project.
|
||||||
|
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
FROM node:20-slim
|
||||||
|
|
||||||
|
# Install pnpm
|
||||||
|
RUN npm install -g pnpm@latest
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package.json pnpm-lock.yaml ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN pnpm i
|
||||||
|
|
||||||
|
# Copy the rest of the application
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
ENV NODE_OPTIONS=--max_old_space_size=16384
|
||||||
|
RUN pnpm run build
|
||||||
|
|
||||||
|
# Default to serving the build directory
|
||||||
|
CMD ["npx", "serve", "build"]
|
||||||
|
|
||||||
@@ -4,23 +4,15 @@ A discord-like nostr client based on the idea of "relays as groups".
|
|||||||
|
|
||||||
If you would like to be interoperable with Flotilla, please check out this guide: https://habla.news/u/hodlbod@coracle.social/1741286140797
|
If you would like to be interoperable with Flotilla, please check out this guide: https://habla.news/u/hodlbod@coracle.social/1741286140797
|
||||||
|
|
||||||
# Deploy
|
|
||||||
|
|
||||||
To run your own Flotilla, it's as simple as:
|
|
||||||
|
|
||||||
- `pnpm install`
|
|
||||||
- `pnpm run build`
|
|
||||||
- `npx serve build`
|
|
||||||
|
|
||||||
## Environment
|
## Environment
|
||||||
|
|
||||||
You can also optionally create an `.env.local` file and populate it with the following environment variables (see `.env` for examples):
|
You can also optionally create an `.env` file and populate it with the following environment variables (see `.env` 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. This is only used for build-time population of meta tags.
|
- `VITE_PLATFORM_URL` - The url where the app will be hosted. This is only used for build-time population of meta tags.
|
||||||
- `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
|
||||||
- `VITE_PLATFORM_RELAY` - A relay url that will make flotilla operate in "platform mode". Disables all space browse/add/select functionality and makes the 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
|
||||||
- `VITE_GLITCHTIP_API_KEY` - A Sentry DSN for use with glitchtip (error reporting)
|
- `VITE_GLITCHTIP_API_KEY` - A Sentry DSN for use with glitchtip (error reporting)
|
||||||
@@ -28,84 +20,29 @@ You can also optionally create an `.env.local` file and populate it with the fol
|
|||||||
|
|
||||||
If you're deploying a custom version of flotilla, be sure to remove the `plausible.coracle.social` script from `app.html`. This sends analytics to a server hosted by the developer.
|
If you're deploying a custom version of flotilla, be sure to remove the `plausible.coracle.social` script from `app.html`. This sends analytics to a server hosted by the developer.
|
||||||
|
|
||||||
## Nginx/TLS (optional)
|
## Development
|
||||||
|
|
||||||
If you'd like to set up flotilla on a server you control, you'll want to set up a reverse proxy and provision a TSL certificate for the domain you'll be using. You should also make sure to add swap to your server.
|
See [./CONTRIBUTING.md](CONTRIBUTING.md).
|
||||||
|
|
||||||
There will be some parts of the following templates, for example `<SERVER NAME>`, which you'll need to fill in before running the code.
|
## Deployment
|
||||||
|
|
||||||
First, create an `A` record with your DNS provider pointing to the IP of your server. This will allow certbot to create your certificate later.
|
To run your own Flotilla, it's as simple as:
|
||||||
|
|
||||||
Next install `nginx`, `git`, and `certbot`. If you're on a debian- or ubuntu-based distro, run `sudo apt-get update && sudo apt-get install nginx git certbot python3-certbot-nginx`.
|
|
||||||
|
|
||||||
Now, create a new user where your code will be stored, clone the repository, fill in your `.env.local` file, and build the app.
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Replace with your password
|
pnpm install
|
||||||
PASSWORD=<YOUR PASSWORD HERE>
|
pnpm run build
|
||||||
|
npx serve build
|
||||||
# Add the user and set a password
|
|
||||||
adduser flotilla
|
|
||||||
echo flotilla:$PASSWORD | chpasswd
|
|
||||||
|
|
||||||
# Login as flotilla
|
|
||||||
sudo su flotilla
|
|
||||||
|
|
||||||
# Go to flotilla's home directory
|
|
||||||
cd ~
|
|
||||||
|
|
||||||
# Install nvm, yarn, clone repos
|
|
||||||
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
|
|
||||||
|
|
||||||
# Update PATH
|
|
||||||
. ~/.bashrc
|
|
||||||
|
|
||||||
# Clone repository and install dependencies
|
|
||||||
git clone https://github.com/coracle-social/flotilla.git
|
|
||||||
cd ~/flotilla
|
|
||||||
nvm install
|
|
||||||
nvm use
|
|
||||||
pnpm i
|
|
||||||
|
|
||||||
# Optionally create and populate .env.local to suit your use case
|
|
||||||
|
|
||||||
# Build the app
|
|
||||||
NODE_OPTIONS=--max_old_space_size=16384 pnpm run build
|
|
||||||
|
|
||||||
# Exit back to root
|
|
||||||
exit
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Once you've exited back to root, you can set up nginx. Place the following in a file named after your domain in the `/etc/nginx/sites-available` directory, for example, `flotilla.example.com`. This should match the `A` record you registered above.
|
Or, if you prefer to use a container:
|
||||||
|
|
||||||
```conf
|
```sh
|
||||||
server {
|
podman run -d -p 3000:3000 ghcr.io/coracle-social/flotilla:latest
|
||||||
listen 80;
|
|
||||||
server_name <SERVER NAME>;
|
|
||||||
root /home/flotilla/flotilla/build;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files $uri /index.html;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Now you can run `certbot`, which will provision a TLS certificate for your domain and update your nginx configuration.
|
Alternatively, you can copy the build files into a directory of your choice and serve it yourself:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mkdir ./mount
|
||||||
|
podman run -v ./mount:/app/mount ghcr.io/coracle-social/flotilla:latest bash -c 'cp -r build/* mount'
|
||||||
```
|
```
|
||||||
certbot --nginx -d <SERVER NAME>
|
|
||||||
```
|
|
||||||
|
|
||||||
Now, enable the site and restart nginx. If you want to be careful, run `nginx -t` before restarting nginx.
|
|
||||||
|
|
||||||
```
|
|
||||||
ln -s /etc/nginx/sites-{available,enabled}/<SERVER NAME>
|
|
||||||
service nginx restart
|
|
||||||
```
|
|
||||||
|
|
||||||
Now, visit your domain. You should be all set up!
|
|
||||||
|
|
||||||
# Development
|
|
||||||
|
|
||||||
Run `pnpm run dev` to get a dev server, and `pnpm run check:watch` to watch for typescript errors. When you're ready to commit, run `pnpm run format && pnpm run lint` and fix any errors that come up.
|
|
||||||
|
|||||||
@@ -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 16
|
versionCode 24
|
||||||
versionName "1.0.2"
|
versionName "1.2.3"
|
||||||
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.
|
||||||
|
|||||||
@@ -9,8 +9,11 @@ android {
|
|||||||
|
|
||||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation project(':capacitor-community-safe-area')
|
||||||
implementation project(':capacitor-app')
|
implementation project(':capacitor-app')
|
||||||
implementation project(':capacitor-keyboard')
|
implementation project(':capacitor-keyboard')
|
||||||
|
implementation project(':capacitor-push-notifications')
|
||||||
|
implementation project(':capawesome-capacitor-badge')
|
||||||
implementation project(':nostr-signer-capacitor-plugin')
|
implementation project(':nostr-signer-capacitor-plugin')
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,13 @@
|
|||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter android:autoVerify="true">
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data android:scheme="https" android:host="app.flotilla.social" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
@@ -34,4 +41,5 @@
|
|||||||
<!-- Permissions -->
|
<!-- Permissions -->
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||||
<item name="colorPrimary">@color/colorPrimary</item>
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
<item name="windowActionBar">false</item>
|
<item name="windowActionBar">false</item>
|
||||||
<item name="windowNoTitle">true</item>
|
<item name="windowNoTitle">true</item>
|
||||||
<item name="android:background">@null</item>
|
<item name="android:background">@null</item>
|
||||||
|
<item name="android:windowOptOutEdgeToEdgeEnforcement" tools:ignore="NewApi">true</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,20 @@
|
|||||||
include ':capacitor-android'
|
include ':capacitor-android'
|
||||||
project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@7.2.0_@capacitor+core@7.2.0/node_modules/@capacitor/android/capacitor')
|
project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@7.2.0_@capacitor+core@7.2.0/node_modules/@capacitor/android/capacitor')
|
||||||
|
|
||||||
|
include ':capacitor-community-safe-area'
|
||||||
|
project(':capacitor-community-safe-area').projectDir = new File('../node_modules/.pnpm/@capacitor-community+safe-area@7.0.0-alpha.1_@capacitor+core@7.2.0/node_modules/@capacitor-community/safe-area/android')
|
||||||
|
|
||||||
include ':capacitor-app'
|
include ':capacitor-app'
|
||||||
project(':capacitor-app').projectDir = new File('../node_modules/.pnpm/@capacitor+app@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/app/android')
|
project(':capacitor-app').projectDir = new File('../node_modules/.pnpm/@capacitor+app@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/app/android')
|
||||||
|
|
||||||
include ':capacitor-keyboard'
|
include ':capacitor-keyboard'
|
||||||
project(':capacitor-keyboard').projectDir = new File('../node_modules/.pnpm/@capacitor+keyboard@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/keyboard/android')
|
project(':capacitor-keyboard').projectDir = new File('../node_modules/.pnpm/@capacitor+keyboard@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/keyboard/android')
|
||||||
|
|
||||||
|
include ':capacitor-push-notifications'
|
||||||
|
project(':capacitor-push-notifications').projectDir = new File('../node_modules/.pnpm/@capacitor+push-notifications@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/push-notifications/android')
|
||||||
|
|
||||||
|
include ':capawesome-capacitor-badge'
|
||||||
|
project(':capawesome-capacitor-badge').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-badge@7.0.1_@capacitor+core@7.2.0/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@7.2.0/node_modules/nostr-signer-capacitor-plugin/android')
|
project(':nostr-signer-capacitor-plugin').projectDir = new File('../node_modules/.pnpm/nostr-signer-capacitor-plugin@0.0.4_@capacitor+core@7.2.0/node_modules/nostr-signer-capacitor-plugin/android')
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
# Fetch tags and set to env vars
|
# Fetch tags and set to env vars
|
||||||
git fetch --prune --unshallow --tags
|
git fetch --prune --unshallow --tags || true
|
||||||
git describe --tags --abbrev=0
|
git describe --tags --abbrev=0 || true
|
||||||
export VITE_BUILD_VERSION=$RENDER_GIT_COMMIT
|
export VITE_BUILD_VERSION=$RENDER_GIT_COMMIT
|
||||||
export VITE_BUILD_HASH=$RENDER_GIT_COMMIT
|
export VITE_BUILD_HASH=$RENDER_GIT_COMMIT
|
||||||
|
|
||||||
# Remove link overrides
|
# Install dependencies
|
||||||
node remove-pnpm-overrides.js package.json
|
CI=0 pnpm i
|
||||||
|
|
||||||
# When CI=true as it is on render.com, removing link overrides breaks the lockfile
|
|
||||||
pnpm i --no-frozen-lockfile
|
|
||||||
|
|
||||||
# Rebuild sharp
|
# Rebuild sharp
|
||||||
pnpm rebuild
|
pnpm rebuild
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
temp_env=$(declare -p -x)
|
temp_env=$(declare -p -x)
|
||||||
|
|
||||||
if [ -f .env ]; then
|
if [ -f .env.template ]; then
|
||||||
source .env
|
source .env.template
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -f .env.local ]; then
|
if [ -f .env ]; then
|
||||||
source .env.local
|
source .env
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Avoid overwriting env vars provided directly
|
# Avoid overwriting env vars provided directly
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ const config: CapacitorConfig = {
|
|||||||
server: {
|
server: {
|
||||||
androidScheme: "https"
|
androidScheme: "https"
|
||||||
},
|
},
|
||||||
|
android: {
|
||||||
|
adjustMarginsForEdgeToEdge: false,
|
||||||
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
SplashScreen: {
|
SplashScreen: {
|
||||||
androidSplashResourceName: "splash"
|
androidSplashResourceName: "splash"
|
||||||
@@ -15,6 +18,10 @@ const config: CapacitorConfig = {
|
|||||||
style: "DARK",
|
style: "DARK",
|
||||||
resizeOnFullScreen: true,
|
resizeOnFullScreen: true,
|
||||||
},
|
},
|
||||||
|
Badge: {
|
||||||
|
persist: true,
|
||||||
|
autoClear: true
|
||||||
|
},
|
||||||
},
|
},
|
||||||
// Use this for live reload https://capacitorjs.com/docs/guides/live-reload
|
// Use this for live reload https://capacitorjs.com/docs/guides/live-reload
|
||||||
// server: {
|
// server: {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
051414282E0CC28400BE0BC8 /* Flotilla Chat.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Flotilla Chat.entitlements"; sourceTree = "<group>"; };
|
||||||
1F53EE54954731A2328CBC4B /* Pods-Flotilla Chat.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Flotilla Chat.release.xcconfig"; path = "Pods/Target Support Files/Pods-Flotilla Chat/Pods-Flotilla Chat.release.xcconfig"; sourceTree = "<group>"; };
|
1F53EE54954731A2328CBC4B /* Pods-Flotilla Chat.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Flotilla Chat.release.xcconfig"; path = "Pods/Target Support Files/Pods-Flotilla Chat/Pods-Flotilla Chat.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };
|
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };
|
||||||
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
|
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
|
||||||
@@ -57,6 +58,7 @@
|
|||||||
504EC2FB1FED79650016851F = {
|
504EC2FB1FED79650016851F = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
051414282E0CC28400BE0BC8 /* Flotilla Chat.entitlements */,
|
||||||
504EC3061FED79650016851F /* App */,
|
504EC3061FED79650016851F /* App */,
|
||||||
504EC3051FED79650016851F /* Products */,
|
504EC3051FED79650016851F /* Products */,
|
||||||
7F8756D8B27F46E3366F6CEA /* Pods */,
|
7F8756D8B27F46E3366F6CEA /* Pods */,
|
||||||
@@ -349,16 +351,17 @@
|
|||||||
baseConfigurationReference = 7B9FA71C362B734D9F965709 /* Pods-Flotilla Chat.debug.xcconfig */;
|
baseConfigurationReference = 7B9FA71C362B734D9F965709 /* Pods-Flotilla Chat.debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
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 = 10;
|
CURRENT_PROJECT_VERSION = 17;
|
||||||
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 = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
MARKETING_VERSION = 1.0.2;
|
MARKETING_VERSION = 1.2.3;
|
||||||
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)";
|
||||||
@@ -374,16 +377,17 @@
|
|||||||
baseConfigurationReference = 1F53EE54954731A2328CBC4B /* Pods-Flotilla Chat.release.xcconfig */;
|
baseConfigurationReference = 1F53EE54954731A2328CBC4B /* Pods-Flotilla Chat.release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
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 = 10;
|
CURRENT_PROJECT_VERSION = 17;
|
||||||
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 = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
MARKETING_VERSION = 1.0.2;
|
MARKETING_VERSION = 1.2.3;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
|||||||
@@ -46,4 +46,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
|
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
||||||
|
// Enable push notifications https://capacitorjs.com/docs/apis/push-notifications
|
||||||
|
NotificationCenter.default.post(name: .capacitorDidRegisterForRemoteNotifications, object: deviceToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
|
||||||
|
// Enable push notifications https://capacitorjs.com/docs/apis/push-notifications
|
||||||
|
NotificationCenter.default.post(name: .capacitorDidFailToRegisterForRemoteNotifications, object: error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,5 +49,9 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false/>
|
<false/>
|
||||||
|
<key>UIBackgroundModes</key>
|
||||||
|
<array>
|
||||||
|
<string>remote-notification</string>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>aps-environment</key>
|
||||||
|
<string>development</string>
|
||||||
|
<key>com.apple.developer.associated-domains</key>
|
||||||
|
<array>
|
||||||
|
<string>applinks:app.flotilla.social</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -11,8 +11,11 @@ install! 'cocoapods', :disable_input_output_paths => true
|
|||||||
def capacitor_pods
|
def capacitor_pods
|
||||||
pod 'Capacitor', :path => '../../node_modules/.pnpm/@capacitor+ios@7.2.0_@capacitor+core@7.2.0/node_modules/@capacitor/ios'
|
pod 'Capacitor', :path => '../../node_modules/.pnpm/@capacitor+ios@7.2.0_@capacitor+core@7.2.0/node_modules/@capacitor/ios'
|
||||||
pod 'CapacitorCordova', :path => '../../node_modules/.pnpm/@capacitor+ios@7.2.0_@capacitor+core@7.2.0/node_modules/@capacitor/ios'
|
pod 'CapacitorCordova', :path => '../../node_modules/.pnpm/@capacitor+ios@7.2.0_@capacitor+core@7.2.0/node_modules/@capacitor/ios'
|
||||||
|
pod 'CapacitorCommunitySafeArea', :path => '../../node_modules/.pnpm/@capacitor-community+safe-area@7.0.0-alpha.1_@capacitor+core@7.2.0/node_modules/@capacitor-community/safe-area'
|
||||||
pod 'CapacitorApp', :path => '../../node_modules/.pnpm/@capacitor+app@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/app'
|
pod 'CapacitorApp', :path => '../../node_modules/.pnpm/@capacitor+app@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/app'
|
||||||
pod 'CapacitorKeyboard', :path => '../../node_modules/.pnpm/@capacitor+keyboard@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/keyboard'
|
pod 'CapacitorKeyboard', :path => '../../node_modules/.pnpm/@capacitor+keyboard@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/keyboard'
|
||||||
|
pod 'CapacitorPushNotifications', :path => '../../node_modules/.pnpm/@capacitor+push-notifications@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/push-notifications'
|
||||||
|
pod 'CapawesomeCapacitorBadge', :path => '../../node_modules/.pnpm/@capawesome+capacitor-badge@7.0.1_@capacitor+core@7.2.0/node_modules/@capawesome/capacitor-badge'
|
||||||
pod 'NostrSignerCapacitorPlugin', :path => '../../node_modules/.pnpm/nostr-signer-capacitor-plugin@0.0.4_@capacitor+core@7.2.0/node_modules/nostr-signer-capacitor-plugin'
|
pod 'NostrSignerCapacitorPlugin', :path => '../../node_modules/.pnpm/nostr-signer-capacitor-plugin@0.0.4_@capacitor+core@7.2.0/node_modules/nostr-signer-capacitor-plugin'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+25
-31
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "flotilla",
|
"name": "flotilla",
|
||||||
"version": "1.0.2",
|
"version": "1.2.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@capacitor/assets": "^3.0.5",
|
"@capacitor/assets": "^3.0.5",
|
||||||
|
"@eslint/js": "^9.26.0",
|
||||||
"@sentry/cli": "^2.40.0",
|
"@sentry/cli": "^2.40.0",
|
||||||
"@sveltejs/kit": "^2.5.27",
|
"@sveltejs/kit": "^2.5.27",
|
||||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||||
@@ -37,32 +38,36 @@
|
|||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@capacitor-community/safe-area": "7.0.0-alpha.1",
|
||||||
"@capacitor/android": "^7.0.0",
|
"@capacitor/android": "^7.0.0",
|
||||||
"@capacitor/app": "^7.0.0",
|
"@capacitor/app": "^7.0.0",
|
||||||
"@capacitor/cli": "^7.0.0",
|
"@capacitor/cli": "^7.0.0",
|
||||||
"@capacitor/core": "^7.0.1",
|
"@capacitor/core": "^7.0.1",
|
||||||
"@capacitor/ios": "^7.0.0",
|
"@capacitor/ios": "^7.0.0",
|
||||||
"@capacitor/keyboard": "^7.0.0",
|
"@capacitor/keyboard": "^7.0.0",
|
||||||
"@noble/curves": "^1.5.0",
|
"@capacitor/push-notifications": "^7.0.1",
|
||||||
"@noble/hashes": "^1.4.0",
|
"@capawesome/capacitor-badge": "^7.0.1",
|
||||||
|
"@getalby/sdk": "^5.1.0",
|
||||||
"@poppanator/sveltekit-svg": "^4.2.1",
|
"@poppanator/sveltekit-svg": "^4.2.1",
|
||||||
"@sentry/browser": "^8.35.0",
|
"@sentry/browser": "^8.35.0",
|
||||||
"@sveltejs/adapter-static": "^3.0.4",
|
"@sveltejs/adapter-static": "^3.0.4",
|
||||||
|
"@tiptap/core": "^2.12.0",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
|
"@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.6",
|
"@vite-pwa/sveltekit": "^0.6.6",
|
||||||
"@welshman/app": "^0.2.4",
|
"@welshman/app": "^0.4.3",
|
||||||
"@welshman/content": "^0.2.1",
|
"@welshman/content": "^0.4.3",
|
||||||
"@welshman/dvm": "^0.2.0",
|
"@welshman/editor": "^0.4.3",
|
||||||
"@welshman/editor": "^0.2.1",
|
"@welshman/feeds": "^0.4.3",
|
||||||
"@welshman/feeds": "^0.2.2",
|
"@welshman/lib": "^0.4.3",
|
||||||
"@welshman/lib": "^0.2.2",
|
"@welshman/net": "^0.4.3",
|
||||||
"@welshman/net": "^0.2.3",
|
"@welshman/relay": "^0.4.3",
|
||||||
"@welshman/relay": "^0.2.0",
|
"@welshman/router": "^0.4.3",
|
||||||
"@welshman/router": "^0.2.0",
|
"@welshman/signer": "^0.4.3",
|
||||||
"@welshman/signer": "^0.2.3",
|
"@welshman/store": "^0.4.3",
|
||||||
"@welshman/store": "^0.2.0",
|
"@welshman/util": "^0.4.3",
|
||||||
"@welshman/util": "^0.2.2",
|
"compressorjs": "^1.2.1",
|
||||||
"daisyui": "^4.12.10",
|
"daisyui": "^4.12.10",
|
||||||
"date-picker-svelte": "^2.13.0",
|
"date-picker-svelte": "^2.13.0",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
@@ -71,25 +76,14 @@
|
|||||||
"husky": "^9.1.6",
|
"husky": "^9.1.6",
|
||||||
"idb": "^8.0.0",
|
"idb": "^8.0.0",
|
||||||
"nostr-signer-capacitor-plugin": "^0.0.4",
|
"nostr-signer-capacitor-plugin": "^0.0.4",
|
||||||
"nostr-tools": "^2.7.2",
|
"nostr-tools": "^2.14.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||||
"qrcode": "^1.5.4"
|
"qr-scanner": "^1.4.2",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
|
"throttle-debounce": "^5.0.2",
|
||||||
|
"tippy.js": "^6.3.7"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"overrides": {
|
|
||||||
"@welshman/lib": "link:../welshman/packages/lib",
|
|
||||||
"@welshman/util": "link:../welshman/packages/util",
|
|
||||||
"@welshman/app": "link:../welshman/packages/app",
|
|
||||||
"@welshman/content": "link:../welshman/packages/content",
|
|
||||||
"@welshman/dvm": "link:../welshman/packages/dvm",
|
|
||||||
"@welshman/feeds": "link:../welshman/packages/feeds",
|
|
||||||
"@welshman/net": "link:../welshman/packages/net",
|
|
||||||
"@welshman/relay": "link:../welshman/packages/relay",
|
|
||||||
"@welshman/router": "link:../welshman/packages/router",
|
|
||||||
"@welshman/signer": "link:../welshman/packages/signer",
|
|
||||||
"@welshman/store": "link:../welshman/packages/store",
|
|
||||||
"@welshman/editor": "link:../welshman/packages/editor"
|
|
||||||
},
|
|
||||||
"ignoredBuiltDependencies": [
|
"ignoredBuiltDependencies": [
|
||||||
"@sentry/cli",
|
"@sentry/cli",
|
||||||
"esbuild"
|
"esbuild"
|
||||||
|
|||||||
Generated
+959
-58
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
|||||||
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.local"})
|
|
||||||
dotenv.config({path: ".env"})
|
dotenv.config({path: ".env"})
|
||||||
|
dotenv.config({path: ".env.template"})
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
preset,
|
preset,
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
// This script is necessary for installing stuff on a host, since our links don't exist there.
|
|
||||||
|
|
||||||
import fs from "fs"
|
|
||||||
|
|
||||||
const pkgName = process.argv[2]
|
|
||||||
|
|
||||||
if (!pkgName?.endsWith("package.json")) {
|
|
||||||
console.log("File passed was not a package.json file")
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const pkg = JSON.parse(fs.readFileSync(pkgName, "utf8"))
|
|
||||||
|
|
||||||
if (pkg.pnpm && pkg.pnpm.overrides) {
|
|
||||||
delete pkg.pnpm.overrides
|
|
||||||
fs.writeFileSync(pkgName, JSON.stringify(pkg, null, 2) + "\n")
|
|
||||||
console.log("Removed pnpm.overrides from package.json")
|
|
||||||
} else {
|
|
||||||
console.log("No pnpm.overrides found in package.json")
|
|
||||||
}
|
|
||||||
+22
-12
@@ -46,6 +46,14 @@
|
|||||||
|
|
||||||
:root {
|
:root {
|
||||||
font-family: Lato;
|
font-family: Lato;
|
||||||
|
--sait: var(--safe-area-inset-top, env(safe-area-inset-top));
|
||||||
|
--saib: var(--safe-area-inset-bottom, env(safe-area-inset-bottom));
|
||||||
|
--sail: var(--safe-area-inset-left, env(safe-area-inset-left));
|
||||||
|
--sair: var(--safe-area-inset-right, env(safe-area-inset-right));
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme] {
|
||||||
|
@apply bg-base-300;
|
||||||
--base-100: oklch(var(--b1));
|
--base-100: oklch(var(--b1));
|
||||||
--base-200: oklch(var(--b2));
|
--base-200: oklch(var(--b2));
|
||||||
--base-300: oklch(var(--b3));
|
--base-300: oklch(var(--b3));
|
||||||
@@ -54,16 +62,6 @@
|
|||||||
--primary-content: oklch(var(--pc));
|
--primary-content: oklch(var(--pc));
|
||||||
--secondary: oklch(var(--s));
|
--secondary: oklch(var(--s));
|
||||||
--secondary-content: oklch(var(--sc));
|
--secondary-content: oklch(var(--sc));
|
||||||
--sait: env(safe-area-inset-top);
|
|
||||||
--saib: env(safe-area-inset-bottom);
|
|
||||||
--sail: env(safe-area-inset-left);
|
|
||||||
--sair: env(safe-area-inset-right);
|
|
||||||
}
|
|
||||||
|
|
||||||
:root,
|
|
||||||
body,
|
|
||||||
html {
|
|
||||||
@apply bg-base-300;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* safe area insets */
|
/* safe area insets */
|
||||||
@@ -162,11 +160,11 @@ html {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card2 {
|
.card2 {
|
||||||
@apply rounded-box p-6 text-base-content;
|
@apply rounded-box p-4 text-base-content sm:p-6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card2.card2-sm {
|
.card2.card2-sm {
|
||||||
@apply p-4 text-base-content;
|
@apply p-2 text-base-content sm:p-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.column {
|
.column {
|
||||||
@@ -293,6 +291,14 @@ html {
|
|||||||
--tiptap-active-fg: var(--base-content);
|
--tiptap-active-fg: var(--base-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tiptap-suggestions__item {
|
||||||
|
@apply border-l-2 border-solid border-base-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap-suggestions__selected {
|
||||||
|
@apply border-primary;
|
||||||
|
}
|
||||||
|
|
||||||
.tiptap {
|
.tiptap {
|
||||||
@apply max-h-[350px] overflow-y-auto p-2 px-4;
|
@apply max-h-[350px] overflow-y-auto p-2 px-4;
|
||||||
}
|
}
|
||||||
@@ -382,6 +388,10 @@ progress[value]::-webkit-progress-value {
|
|||||||
@apply w-full md:left-[18.5rem] md:w-[calc(100%-18.5rem-var(--sair))];
|
@apply w-full md:left-[18.5rem] md:w-[calc(100%-18.5rem-var(--sair))];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cw-full {
|
||||||
|
@apply w-full md:left-[4rem] md:w-[calc(100%-4rem-var(--sair))];
|
||||||
|
}
|
||||||
|
|
||||||
.cb {
|
.cb {
|
||||||
@apply md:bottom-sai bottom-[calc(var(--saib)+3.5rem)];
|
@apply md:bottom-sai bottom-[calc(var(--saib)+3.5rem)];
|
||||||
}
|
}
|
||||||
|
|||||||
+146
-110
@@ -1,30 +1,56 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {onMount} from "svelte"
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import {randomInt, displayList, TIMEZONE, identity} from "@welshman/lib"
|
import {decrypt} from "@welshman/signer"
|
||||||
import {displayRelayUrl, getTagValue, THREAD, MESSAGE, EVENT_TIME, COMMENT} from "@welshman/util"
|
import {randomInt, parseJson, fromPairs, displayList, TIMEZONE, identity} from "@welshman/lib"
|
||||||
|
import {
|
||||||
|
displayRelayUrl,
|
||||||
|
getTagValue,
|
||||||
|
getAddress,
|
||||||
|
THREAD,
|
||||||
|
MESSAGE,
|
||||||
|
EVENT_TIME,
|
||||||
|
COMMENT,
|
||||||
|
} from "@welshman/util"
|
||||||
import type {Filter} from "@welshman/util"
|
import type {Filter} from "@welshman/util"
|
||||||
import type {Nip46ResponseWithResult} from "@welshman/signer"
|
|
||||||
import {makeIntersectionFeed, makeRelayFeed, feedFromFilters} from "@welshman/feeds"
|
import {makeIntersectionFeed, makeRelayFeed, feedFromFilters} from "@welshman/feeds"
|
||||||
import {pubkey} from "@welshman/app"
|
import {pubkey, signer, getThunkError} from "@welshman/app"
|
||||||
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 FieldInline from "@lib/components/FieldInline.svelte"
|
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import InfoBunker from "@app/components/InfoBunker.svelte"
|
|
||||||
import BunkerConnect, {BunkerConnectController} from "@app/components/BunkerConnect.svelte"
|
|
||||||
import {
|
import {
|
||||||
GENERAL,
|
|
||||||
alerts,
|
alerts,
|
||||||
getMembershipUrls,
|
getMembershipUrls,
|
||||||
getMembershipRoomsByUrl,
|
|
||||||
userMembership,
|
userMembership,
|
||||||
} from "@app/state"
|
NOTIFIER_PUBKEY,
|
||||||
import {loadAlertStatuses} from "@app/requests"
|
NOTIFIER_RELAY,
|
||||||
import {publishAlert} from "@app/commands"
|
} from "@app/core/state"
|
||||||
import {pushToast} from "@app/toast"
|
import {loadAlertStatuses, requestRelayClaim} from "@app/core/requests"
|
||||||
import {pushModal} from "@app/modal"
|
import {publishAlert, attemptAuth} from "@app/core/commands"
|
||||||
|
import type {AlertParams} from "@app/core/commands"
|
||||||
|
import {platform, platformName, canSendPushNotifications, getPushInfo} from "@app/util/push"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url?: string
|
||||||
|
channel?: string
|
||||||
|
notifyChat?: boolean
|
||||||
|
notifyThreads?: boolean
|
||||||
|
notifyCalendar?: boolean
|
||||||
|
hideSpaceField?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
url = "",
|
||||||
|
channel = "email",
|
||||||
|
notifyChat = true,
|
||||||
|
notifyThreads = true,
|
||||||
|
notifyCalendar = true,
|
||||||
|
hideSpaceField = false,
|
||||||
|
}: Props = $props()
|
||||||
|
|
||||||
const timezoneOffset = parseInt(TIMEZONE.slice(3)) / 100
|
const timezoneOffset = parseInt(TIMEZONE.slice(3)) / 100
|
||||||
const minute = randomInt(0, 59)
|
const minute = randomInt(0, 59)
|
||||||
@@ -32,49 +58,21 @@
|
|||||||
const WEEKLY = `0 ${minute} ${hour} * * 1`
|
const WEEKLY = `0 ${minute} ${hour} * * 1`
|
||||||
const DAILY = `0 ${minute} ${hour} * * *`
|
const DAILY = `0 ${minute} ${hour} * * *`
|
||||||
|
|
||||||
let loading = false
|
let loading = $state(false)
|
||||||
let cron = WEEKLY
|
let cron = $state(WEEKLY)
|
||||||
let email = $alerts.map(a => getTagValue("email", a.tags)).filter(identity)[0] || ""
|
let email = $state($alerts.map(a => getTagValue("email", a.tags)).filter(identity)[0] || "")
|
||||||
let relay = ""
|
|
||||||
let bunker = ""
|
|
||||||
let secret = ""
|
|
||||||
let notifyThreads = true
|
|
||||||
let notifyCalendar = true
|
|
||||||
let notifyChat = false
|
|
||||||
let showBunker = false
|
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const controller = new BunkerConnectController({
|
|
||||||
onNostrConnect: (response: Nip46ResponseWithResult) => {
|
|
||||||
bunker = controller.broker.getBunkerUrl()
|
|
||||||
secret = controller.broker.params.clientSecret
|
|
||||||
showBunker = false
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const connectBunker = () => {
|
|
||||||
showBunker = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const hideBunker = () => {
|
|
||||||
showBunker = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearBunker = () => {
|
|
||||||
bunker = ""
|
|
||||||
secret = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if (!email.includes("@")) {
|
if (channel === "email" && !email.includes("@")) {
|
||||||
return pushToast({
|
return pushToast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
message: "Please provide an email address",
|
message: "Please provide an email address",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!relay) {
|
if (!url) {
|
||||||
return pushToast({
|
return pushToast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
message: "Please select a space",
|
message: "Please select a space",
|
||||||
@@ -105,22 +103,70 @@
|
|||||||
|
|
||||||
if (notifyChat) {
|
if (notifyChat) {
|
||||||
display.push("chat")
|
display.push("chat")
|
||||||
filters.push({
|
filters.push({kinds: [MESSAGE]})
|
||||||
kinds: [MESSAGE],
|
|
||||||
"#h": [GENERAL, ...getMembershipRoomsByUrl(relay, $userMembership)],
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loading = true
|
loading = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cadence = cron?.endsWith("1") ? "Weekly" : "Daily"
|
const claim = url ? await requestRelayClaim(url) : undefined
|
||||||
const description = `${cadence} alert for ${displayList(display)} on ${displayRelayUrl(relay)}, sent via email.`
|
const claims = claim ? {[url]: claim} : {}
|
||||||
const feed = makeIntersectionFeed(feedFromFilters(filters), makeRelayFeed(relay))
|
const feed = makeIntersectionFeed(feedFromFilters(filters), makeRelayFeed(url))
|
||||||
const thunk = await publishAlert({cron, email, feed, bunker, secret, description})
|
const description = `for ${displayList(display)} on ${displayRelayUrl(url)}`
|
||||||
|
const params: AlertParams = {feed, claims, description}
|
||||||
|
|
||||||
await thunk.result
|
if (channel === "email") {
|
||||||
await loadAlertStatuses($pubkey!)
|
const cadence = cron?.endsWith("1") ? "Weekly" : "Daily"
|
||||||
|
|
||||||
|
params.description = `${cadence} alert ${description}, sent via email.`
|
||||||
|
params.email = {
|
||||||
|
cron,
|
||||||
|
email,
|
||||||
|
handler: [
|
||||||
|
"31990:97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322:1737058597050",
|
||||||
|
"wss://relay.nostr.band/",
|
||||||
|
"web",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
// @ts-ignore
|
||||||
|
params[platform] = await getPushInfo()
|
||||||
|
params.description = `${platformName} push notification ${description}.`
|
||||||
|
} catch (e: any) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: String(e),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we don't do this we'll get an event rejection
|
||||||
|
await attemptAuth(NOTIFIER_RELAY)
|
||||||
|
|
||||||
|
const thunk = await publishAlert(params)
|
||||||
|
const error = await getThunkError(thunk)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: `Failed to send your alert to the notification server (${error}).`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch our new status to make sure it's active
|
||||||
|
const address = getAddress(thunk.event)
|
||||||
|
const statusEvents = await loadAlertStatuses($pubkey!)
|
||||||
|
const statusEvent = statusEvents.find(event => getTagValue("d", event.tags) === address)
|
||||||
|
const statusTags = statusEvent
|
||||||
|
? parseJson(await decrypt(signer.get(), NOTIFIER_PUBKEY, statusEvent.content))
|
||||||
|
: []
|
||||||
|
const {status = "error", message = "Your alert was not activated"}: Record<string, string> =
|
||||||
|
fromPairs(statusTags)
|
||||||
|
|
||||||
|
if (status === "error") {
|
||||||
|
return pushToast({theme: "error", message})
|
||||||
|
}
|
||||||
|
|
||||||
pushToast({message: "Your alert has been successfully created!"})
|
pushToast({message: "Your alert has been successfully created!"})
|
||||||
back()
|
back()
|
||||||
@@ -128,6 +174,12 @@
|
|||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!canSendPushNotifications()) {
|
||||||
|
channel = "email"
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="column gap-4" onsubmit={preventDefault(submit)}>
|
<form class="column gap-4" onsubmit={preventDefault(submit)}>
|
||||||
@@ -136,13 +188,20 @@
|
|||||||
Add an Alert
|
Add an Alert
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
{#if showBunker}
|
{#if canSendPushNotifications()}
|
||||||
<div class="card2 flex flex-col items-center gap-4 bg-base-300">
|
<FieldInline>
|
||||||
<p>Scan using a nostr signer, or click to copy.</p>
|
{#snippet label()}
|
||||||
<BunkerConnect {controller} />
|
<p>Alert Type*</p>
|
||||||
<Button class="btn btn-neutral btn-sm" onclick={hideBunker}>Cancel</Button>
|
{/snippet}
|
||||||
</div>
|
{#snippet input()}
|
||||||
{:else}
|
<select bind:value={channel} class="select select-bordered">
|
||||||
|
<option value="email">Email Digest</option>
|
||||||
|
<option value="push">Push Notification</option>
|
||||||
|
</select>
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
|
{/if}
|
||||||
|
{#if channel === "email"}
|
||||||
<FieldInline>
|
<FieldInline>
|
||||||
{#snippet label()}
|
{#snippet label()}
|
||||||
<p>Email Address*</p>
|
<p>Email Address*</p>
|
||||||
@@ -164,12 +223,14 @@
|
|||||||
</select>
|
</select>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</FieldInline>
|
</FieldInline>
|
||||||
|
{/if}
|
||||||
|
{#if !hideSpaceField}
|
||||||
<FieldInline>
|
<FieldInline>
|
||||||
{#snippet label()}
|
{#snippet label()}
|
||||||
<p>Space*</p>
|
<p>Space*</p>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet input()}
|
{#snippet input()}
|
||||||
<select bind:value={relay} class="select select-bordered">
|
<select bind:value={url} class="select select-bordered">
|
||||||
<option value="" disabled selected>Choose a space URL</option>
|
<option value="" disabled selected>Choose a space URL</option>
|
||||||
{#each getMembershipUrls($userMembership) as url (url)}
|
{#each getMembershipUrls($userMembership) as url (url)}
|
||||||
<option value={url}>{displayRelayUrl(url)}</option>
|
<option value={url}>{displayRelayUrl(url)}</option>
|
||||||
@@ -177,59 +238,34 @@
|
|||||||
</select>
|
</select>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</FieldInline>
|
</FieldInline>
|
||||||
<FieldInline>
|
{/if}
|
||||||
{#snippet label()}
|
<FieldInline>
|
||||||
<p>Notifications*</p>
|
{#snippet label()}
|
||||||
{/snippet}
|
<p>Notifications*</p>
|
||||||
{#snippet input()}
|
{/snippet}
|
||||||
<div class="flex items-center justify-end gap-4">
|
{#snippet input()}
|
||||||
<span class="flex gap-3">
|
<div class="flex items-center justify-end gap-4">
|
||||||
<input type="checkbox" class="checkbox" bind:checked={notifyThreads} />
|
<span class="flex gap-3">
|
||||||
Threads
|
<input type="checkbox" class="checkbox" bind:checked={notifyThreads} />
|
||||||
</span>
|
Threads
|
||||||
<span class="flex gap-3">
|
</span>
|
||||||
<input type="checkbox" class="checkbox" bind:checked={notifyCalendar} />
|
<span class="flex gap-3">
|
||||||
Calendar
|
<input type="checkbox" class="checkbox" bind:checked={notifyCalendar} />
|
||||||
</span>
|
Calendar
|
||||||
<span class="flex gap-3">
|
</span>
|
||||||
<input type="checkbox" class="checkbox" bind:checked={notifyChat} />
|
<span class="flex gap-3">
|
||||||
Chat
|
<input type="checkbox" class="checkbox" bind:checked={notifyChat} />
|
||||||
</span>
|
Chat
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
</FieldInline>
|
|
||||||
<div class="card2 flex flex-col gap-3 bg-base-300">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<strong>Connect a Bunker</strong>
|
|
||||||
<span class="flex items-center gap-2 text-sm" class:text-primary={bunker}>
|
|
||||||
{#if bunker}
|
|
||||||
<Icon icon="check-circle" size={5} />
|
|
||||||
Connected
|
|
||||||
{:else}
|
|
||||||
<Icon icon="close-circle" size={5} />
|
|
||||||
Not Connected
|
|
||||||
{/if}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm">
|
{/snippet}
|
||||||
Required for receiving alerts about spaces with access controls. You can get one from your
|
</FieldInline>
|
||||||
<Button class="text-primary" onclick={() => pushModal(InfoBunker)}>remote signer app</Button
|
|
||||||
>.
|
|
||||||
</p>
|
|
||||||
{#if bunker}
|
|
||||||
<Button class="btn btn-neutral btn-sm flex-grow" onclick={clearBunker}>Disconnect</Button>
|
|
||||||
{:else}
|
|
||||||
<Button class="btn btn-primary btn-sm w-full flex-grow" onclick={connectBunker}
|
|
||||||
>Connect</Button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button class="btn btn-link" onclick={back}>
|
<Button class="btn btn-link" onclick={back}>
|
||||||
<Icon icon="alt-arrow-left" />
|
<Icon icon="alt-arrow-left" />
|
||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" class="btn btn-primary" disabled={loading || showBunker}>
|
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||||
<Spinner {loading}>Confirm</Spinner>
|
<Spinner {loading}>Confirm</Spinner>
|
||||||
<Icon icon="alt-arrow-right" />
|
<Icon icon="alt-arrow-right" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Confirm from "@lib/components/Confirm.svelte"
|
import Confirm from "@lib/components/Confirm.svelte"
|
||||||
import type {Alert} from "@app/state"
|
import type {Alert} from "@app/core/state"
|
||||||
import {NOTIFIER_RELAY} from "@app/state"
|
import {NOTIFIER_RELAY, NOTIFIER_PUBKEY} from "@app/core/state"
|
||||||
import {publishDelete} from "@app/commands"
|
import {publishDelete} from "@app/core/commands"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
alert: Alert
|
alert: Alert
|
||||||
@@ -12,7 +12,10 @@
|
|||||||
const {alert}: Props = $props()
|
const {alert}: Props = $props()
|
||||||
|
|
||||||
const confirm = () => {
|
const confirm = () => {
|
||||||
publishDelete({event: alert.event, relays: [NOTIFIER_RELAY]})
|
const relays = [NOTIFIER_RELAY]
|
||||||
|
const tags = [["p", NOTIFIER_PUBKEY]]
|
||||||
|
|
||||||
|
publishDelete({event: alert.event, relays, tags, protect: false})
|
||||||
pushToast({message: "Your alert has been deleted!"})
|
pushToast({message: "Your alert has been deleted!"})
|
||||||
history.back()
|
history.back()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {parseJson, nthEq} from "@welshman/lib"
|
import {parseJson} from "@welshman/lib"
|
||||||
import {displayFeeds} from "@welshman/feeds"
|
import {displayFeeds} from "@welshman/feeds"
|
||||||
import {getAddress, getTagValue, getTagValues} from "@welshman/util"
|
import {getAddress, getTagValue, getTagValues} from "@welshman/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 AlertDelete from "@app/components/AlertDelete.svelte"
|
import AlertDelete from "@app/components/AlertDelete.svelte"
|
||||||
import type {Alert} from "@app/state"
|
import type {Alert} from "@app/core/state"
|
||||||
import {alertStatuses} from "@app/state"
|
import {deriveAlertStatus} from "@app/core/state"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
alert: Alert
|
alert: Alert
|
||||||
@@ -15,8 +15,7 @@
|
|||||||
|
|
||||||
const {alert}: Props = $props()
|
const {alert}: Props = $props()
|
||||||
|
|
||||||
const address = $derived(getAddress(alert.event))
|
const status = deriveAlertStatus(getAddress(alert.event))
|
||||||
const status = $derived($alertStatuses.find(s => s.event.tags.some(nthEq(1, address))))
|
|
||||||
const cron = $derived(getTagValue("cron", alert.tags))
|
const cron = $derived(getTagValue("cron", alert.tags))
|
||||||
const channel = $derived(getTagValue("channel", alert.tags))
|
const channel = $derived(getTagValue("channel", alert.tags))
|
||||||
const feeds = $derived(getTagValues("feed", alert.tags))
|
const feeds = $derived(getTagValues("feed", alert.tags))
|
||||||
@@ -39,24 +38,24 @@
|
|||||||
</Button>
|
</Button>
|
||||||
<div class="flex-inline gap-1">{description}</div>
|
<div class="flex-inline gap-1">{description}</div>
|
||||||
</div>
|
</div>
|
||||||
{#if status}
|
{#if $status}
|
||||||
{@const statusText = getTagValue("status", status.tags) || "error"}
|
{@const statusText = getTagValue("status", $status.tags) || "error"}
|
||||||
{#if statusText === "ok"}
|
{#if statusText === "ok"}
|
||||||
<span
|
<span
|
||||||
class="tooltip tooltip-left cursor-pointer rounded-full border border-solid border-base-content px-3 py-1 text-sm"
|
class="tooltip tooltip-left cursor-pointer rounded-full border border-solid border-base-content px-3 py-1 text-sm"
|
||||||
data-tip={getTagValue("message", status.tags)}>
|
data-tip={getTagValue("message", $status.tags)}>
|
||||||
Active
|
Active
|
||||||
</span>
|
</span>
|
||||||
{:else if statusText === "pending"}
|
{:else if statusText === "pending"}
|
||||||
<span
|
<span
|
||||||
class="tooltip tooltip-left cursor-pointer rounded-full border border-solid border-base-content border-yellow-500 px-3 py-1 text-sm text-yellow-500"
|
class="tooltip tooltip-left cursor-pointer rounded-full border border-solid border-base-content border-yellow-500 px-3 py-1 text-sm text-yellow-500"
|
||||||
data-tip={getTagValue("message", status.tags)}>
|
data-tip={getTagValue("message", $status.tags)}>
|
||||||
Pending
|
Pending
|
||||||
</span>
|
</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span
|
<span
|
||||||
class="tooltip tooltip-left cursor-pointer rounded-full border border-solid border-error px-3 py-1 text-sm text-error"
|
class="tooltip tooltip-left cursor-pointer rounded-full border border-solid border-error px-3 py-1 text-sm text-error"
|
||||||
data-tip={getTagValue("message", status.tags)}>
|
data-tip={getTagValue("message", $status.tags)}>
|
||||||
{statusText.replace("-", " ").replace(/^(.)/, x => x.toUpperCase())}
|
{statusText.replace("-", " ").replace(/^(.)/, x => x.toUpperCase())}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,20 +1,25 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {getTagValue} from "@welshman/util"
|
||||||
import {pubkey} from "@welshman/app"
|
|
||||||
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 AlertAdd from "@app/components/AlertAdd.svelte"
|
import AlertAdd from "@app/components/AlertAdd.svelte"
|
||||||
import AlertItem from "@app/components/AlertItem.svelte"
|
import AlertItem from "@app/components/AlertItem.svelte"
|
||||||
import {loadAlertStatuses, loadAlerts} from "@app/requests"
|
import {pushModal} from "@app/util/modal"
|
||||||
import {pushModal} from "@app/modal"
|
import {alerts} from "@app/core/state"
|
||||||
import {alerts} from "@app/state"
|
|
||||||
|
|
||||||
const startAlert = () => pushModal(AlertAdd)
|
type Props = {
|
||||||
|
url?: string
|
||||||
|
channel?: string
|
||||||
|
hideSpaceField?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
const {url = "", channel = "push", hideSpaceField = false}: Props = $props()
|
||||||
loadAlertStatuses($pubkey!)
|
|
||||||
loadAlerts($pubkey!)
|
const startAlert = () => pushModal(AlertAdd, {url, channel, hideSpaceField})
|
||||||
})
|
|
||||||
|
const filteredAlerts = $derived(
|
||||||
|
url ? $alerts.filter(a => getTagValue("feed", a.tags)?.includes(url)) : $alerts,
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="card2 bg-alt flex flex-col gap-6 shadow-xl">
|
<div class="card2 bg-alt flex flex-col gap-6 shadow-xl">
|
||||||
@@ -29,10 +34,10 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
{#each $alerts as alert (alert.event.id)}
|
{#each filteredAlerts as alert (alert.event.id)}
|
||||||
<AlertItem {alert} />
|
<AlertItem {alert} />
|
||||||
{:else}
|
{:else}
|
||||||
<p class="text-center opacity-75 py-12">No alerts found</p>
|
<p class="text-center opacity-75 py-12">Nothing here yet!</p>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
import PrimaryNav from "@app/components/PrimaryNav.svelte"
|
import PrimaryNav from "@app/components/PrimaryNav.svelte"
|
||||||
import EmailConfirm from "@app/components/EmailConfirm.svelte"
|
import EmailConfirm from "@app/components/EmailConfirm.svelte"
|
||||||
import PasswordReset from "@app/components/PasswordReset.svelte"
|
import PasswordReset from "@app/components/PasswordReset.svelte"
|
||||||
import {BURROW_URL} from "@app/state"
|
import {BURROW_URL} from "@app/core/state"
|
||||||
import {modals, pushModal} from "@app/modal"
|
import {modals, pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: Snippet
|
children: Snippet
|
||||||
|
|||||||
@@ -1,79 +1,25 @@
|
|||||||
<script module lang="ts">
|
|
||||||
import type {Nip46ResponseWithResult} from "@welshman/signer"
|
|
||||||
import {Nip46Broker, makeSecret} from "@welshman/signer"
|
|
||||||
import {NIP46_PERMS, PLATFORM_URL, PLATFORM_NAME, PLATFORM_LOGO, SIGNER_RELAYS} from "@app/state"
|
|
||||||
|
|
||||||
export class BunkerConnectController {
|
|
||||||
url = $state("")
|
|
||||||
bunker = $state("")
|
|
||||||
loading = $state(false)
|
|
||||||
clientSecret = makeSecret()
|
|
||||||
abortController = new AbortController()
|
|
||||||
broker = new Nip46Broker({clientSecret: this.clientSecret, relays: SIGNER_RELAYS})
|
|
||||||
onNostrConnect: (response: Nip46ResponseWithResult) => void
|
|
||||||
|
|
||||||
constructor({onNostrConnect}: {onNostrConnect: (response: Nip46ResponseWithResult) => void}) {
|
|
||||||
this.onNostrConnect = onNostrConnect
|
|
||||||
}
|
|
||||||
|
|
||||||
async start() {
|
|
||||||
this.url = await this.broker.makeNostrconnectUrl({
|
|
||||||
perms: NIP46_PERMS,
|
|
||||||
url: PLATFORM_URL,
|
|
||||||
name: PLATFORM_NAME,
|
|
||||||
image: PLATFORM_LOGO,
|
|
||||||
})
|
|
||||||
|
|
||||||
let response
|
|
||||||
try {
|
|
||||||
response = await this.broker.waitForNostrconnect(this.url, this.abortController.signal)
|
|
||||||
} catch (errorResponse: any) {
|
|
||||||
if (errorResponse?.error) {
|
|
||||||
pushToast({
|
|
||||||
theme: "error",
|
|
||||||
message: `Received error from signer: ${errorResponse.error}`,
|
|
||||||
})
|
|
||||||
} else if (errorResponse) {
|
|
||||||
console.error(errorResponse)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response) {
|
|
||||||
this.loading = true
|
|
||||||
this.onNostrConnect(response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
this.broker.cleanup()
|
|
||||||
this.abortController.abort()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount, onDestroy} from "svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import {slideAndFade} from "@lib/transition"
|
|
||||||
import QRCode from "@app/components/QRCode.svelte"
|
import QRCode from "@app/components/QRCode.svelte"
|
||||||
import {pushToast} from "@app/toast"
|
import type {Nip46Controller} from "@app/util/nip46"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
controller: BunkerConnectController
|
controller: Nip46Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
const {controller}: Props = $props()
|
const {controller}: Props = $props()
|
||||||
|
const {url, loading} = controller
|
||||||
onMount(() => {
|
|
||||||
controller.start()
|
|
||||||
})
|
|
||||||
|
|
||||||
onDestroy(() => {
|
|
||||||
controller.stop()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if controller.url}
|
{#if $url}
|
||||||
<div class="flex justify-center" out:slideAndFade>
|
{#if $loading}
|
||||||
<QRCode code={controller.url} />
|
<div class="flex justify-center">
|
||||||
</div>
|
<Spinner loading>Establishing connection...</Spinner>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-col items-center gap-2">
|
||||||
|
<QRCode code={$url} />
|
||||||
|
<p class="text-sm opacity-75">Scan with your signer to log in, or click to copy.</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,16 +1,30 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {pushModal} from "@app/modal"
|
import {debounce} from "throttle-debounce"
|
||||||
import InfoBunker from "@app/components/InfoBunker.svelte"
|
import Scanner from "@lib/components/Scanner.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Field from "@lib/components/Field.svelte"
|
import Field from "@lib/components/Field.svelte"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import InfoBunker from "@app/components/InfoBunker.svelte"
|
||||||
|
import type {Nip46Controller} from "@app/util/nip46"
|
||||||
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
bunker: string
|
controller: Nip46Controller
|
||||||
loading: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let {loading, bunker = $bindable("")}: Props = $props()
|
const {controller}: Props = $props()
|
||||||
|
const {loading, bunker} = controller
|
||||||
|
|
||||||
|
const toggleScanner = () => {
|
||||||
|
showScanner = !showScanner
|
||||||
|
}
|
||||||
|
|
||||||
|
const onScan = debounce(1000, async (data: string) => {
|
||||||
|
showScanner = false
|
||||||
|
$bunker = data
|
||||||
|
})
|
||||||
|
|
||||||
|
let showScanner = $state(false)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Field>
|
<Field>
|
||||||
@@ -20,7 +34,10 @@
|
|||||||
{#snippet input()}
|
{#snippet input()}
|
||||||
<label class="input input-bordered flex w-full items-center gap-2">
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
<Icon icon="cpu" />
|
<Icon icon="cpu" />
|
||||||
<input disabled={loading} bind:value={bunker} class="grow" placeholder="bunker://" />
|
<input disabled={$loading} bind:value={$bunker} class="grow" placeholder="bunker://" />
|
||||||
|
<Button onclick={toggleScanner}>
|
||||||
|
<Icon icon="qr-code" />
|
||||||
|
</Button>
|
||||||
</label>
|
</label>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet info()}
|
{#snippet info()}
|
||||||
@@ -30,3 +47,6 @@
|
|||||||
</p>
|
</p>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Field>
|
</Field>
|
||||||
|
{#if showScanner}
|
||||||
|
<Scanner onscan={onScan} />
|
||||||
|
{/if}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||||
import {pubkey} from "@welshman/app"
|
import {pubkey} from "@welshman/app"
|
||||||
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"
|
||||||
@@ -8,9 +8,9 @@
|
|||||||
import EventActivity from "@app/components/EventActivity.svelte"
|
import EventActivity from "@app/components/EventActivity.svelte"
|
||||||
import EventActions from "@app/components/EventActions.svelte"
|
import EventActions from "@app/components/EventActions.svelte"
|
||||||
import CalendarEventEdit from "@app/components/CalendarEventEdit.svelte"
|
import CalendarEventEdit from "@app/components/CalendarEventEdit.svelte"
|
||||||
import {publishDelete, publishReaction} from "@app/commands"
|
import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands"
|
||||||
import {makeCalendarPath} from "@app/routes"
|
import {makeCalendarPath} from "@app/util/routes"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
const {
|
const {
|
||||||
url,
|
url,
|
||||||
@@ -22,24 +22,22 @@
|
|||||||
showActivity?: boolean
|
showActivity?: boolean
|
||||||
} = $props()
|
} = $props()
|
||||||
|
|
||||||
|
const shouldProtect = canEnforceNip70(url)
|
||||||
|
|
||||||
const path = makeCalendarPath(url, event.id)
|
const path = makeCalendarPath(url, event.id)
|
||||||
|
|
||||||
const editEvent = () => pushModal(CalendarEventEdit, {url, event})
|
const editEvent = () => pushModal(CalendarEventEdit, {url, event})
|
||||||
|
|
||||||
const onReactionClick = (content: string, events: TrustedEvent[]) => {
|
const deleteReaction = async (event: TrustedEvent) =>
|
||||||
const reaction = events.find(e => e.pubkey === $pubkey)
|
publishDelete({relays: [url], event, protect: await shouldProtect})
|
||||||
|
|
||||||
if (reaction) {
|
const createReaction = async (template: EventContent) =>
|
||||||
publishDelete({relays: [url], event: reaction})
|
publishReaction({...template, event, relays: [url], protect: await shouldProtect})
|
||||||
} else {
|
|
||||||
publishReaction({event, content, relays: [url]})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
||||||
<ReactionSummary {url} {event} {onReactionClick} reactionClass="tooltip-left" />
|
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
|
||||||
<ThunkStatusOrDeleted {event} />
|
<ThunkStatusOrDeleted {event} />
|
||||||
{#if showActivity}
|
{#if showActivity}
|
||||||
<EventActivity {url} {path} {event} />
|
<EventActivity {url} {path} {event} />
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import type {Snippet} from "svelte"
|
import type {Snippet} from "svelte"
|
||||||
import {writable} from "svelte/store"
|
import {writable} from "svelte/store"
|
||||||
import {randomId, HOUR} from "@welshman/lib"
|
import {randomId, HOUR} from "@welshman/lib"
|
||||||
import {createEvent, EVENT_TIME} from "@welshman/util"
|
import {makeEvent, EVENT_TIME} from "@welshman/util"
|
||||||
import {publishThunk} from "@welshman/app"
|
import {publishThunk} from "@welshman/app"
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import {daysBetween} from "@lib/util"
|
import {daysBetween} from "@lib/util"
|
||||||
@@ -13,9 +13,10 @@
|
|||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import DateTimeInput from "@lib/components/DateTimeInput.svelte"
|
import DateTimeInput from "@lib/components/DateTimeInput.svelte"
|
||||||
import EditorContent from "@app/editor/EditorContent.svelte"
|
import EditorContent from "@app/editor/EditorContent.svelte"
|
||||||
import {PROTECTED, GENERAL, tagRoom} from "@app/state"
|
import {PROTECTED} from "@app/core/state"
|
||||||
import {makeEditor} from "@app/editor"
|
import {makeEditor} from "@app/editor"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
|
import {canEnforceNip70} from "@app/core/commands"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
@@ -32,6 +33,8 @@
|
|||||||
|
|
||||||
const {url, header, initialValues}: Props = $props()
|
const {url, header, initialValues}: Props = $props()
|
||||||
|
|
||||||
|
const shouldProtect = canEnforceNip70(url)
|
||||||
|
|
||||||
const uploading = writable(false)
|
const uploading = writable(false)
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
@@ -63,20 +66,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ed = await editor
|
const ed = await editor
|
||||||
const event = createEvent(EVENT_TIME, {
|
const content = ed.getText({blockSeparator: "\n"}).trim()
|
||||||
content: ed.getText({blockSeparator: "\n"}).trim(),
|
const tags = [
|
||||||
tags: [
|
["d", initialValues?.d || randomId()],
|
||||||
["d", initialValues?.d || randomId()],
|
["title", title],
|
||||||
["title", title],
|
["location", location || ""],
|
||||||
["location", location || ""],
|
["start", start.toString()],
|
||||||
["start", start.toString()],
|
["end", end.toString()],
|
||||||
["end", end.toString()],
|
...daysBetween(start, end).map(D => ["D", D.toString()]),
|
||||||
...daysBetween(start, end).map(D => ["D", D.toString()]),
|
...ed.storage.nostr.getEditorTags(),
|
||||||
...ed.storage.nostr.getEditorTags(),
|
]
|
||||||
tagRoom(GENERAL, url),
|
|
||||||
PROTECTED,
|
if (await shouldProtect) {
|
||||||
],
|
tags.push(PROTECTED)
|
||||||
})
|
}
|
||||||
|
|
||||||
|
const event = makeEvent(EVENT_TIME, {content, tags})
|
||||||
|
|
||||||
pushToast({message: "Your event has been saved!"})
|
pushToast({message: "Your event has been saved!"})
|
||||||
publishThunk({event, relays: [url]})
|
publishThunk({event, relays: [url]})
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import CalendarEventActions from "@app/components/CalendarEventActions.svelte"
|
import CalendarEventActions from "@app/components/CalendarEventActions.svelte"
|
||||||
import CalendarEventHeader from "@app/components/CalendarEventHeader.svelte"
|
import CalendarEventHeader from "@app/components/CalendarEventHeader.svelte"
|
||||||
import ProfileLink from "@app/components/ProfileLink.svelte"
|
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||||
import {makeCalendarPath} from "@app/routes"
|
import {makeCalendarPath} from "@app/util/routes"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
|
|||||||
@@ -1,35 +1,36 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {hash, now, formatTimestampAsTime, formatTimestampAsDate} from "@welshman/lib"
|
import {hash, now, formatTimestampAsTime, formatTimestampAsDate} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||||
import {thunks, pubkey, deriveProfile, deriveProfileDisplay} from "@welshman/app"
|
import {thunks, deriveProfile, deriveProfileDisplay} from "@welshman/app"
|
||||||
import {isMobile} from "@lib/html"
|
import {isMobile} from "@lib/html"
|
||||||
import TapTarget from "@lib/components/TapTarget.svelte"
|
import TapTarget from "@lib/components/TapTarget.svelte"
|
||||||
import Avatar from "@lib/components/Avatar.svelte"
|
import Avatar from "@lib/components/Avatar.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"
|
||||||
import Content from "@app/components/Content.svelte"
|
import Content from "@app/components/Content.svelte"
|
||||||
import ThunkStatus from "@app/components/ThunkStatus.svelte"
|
import ThunkFailure from "@app/components/ThunkFailure.svelte"
|
||||||
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||||
|
import ChannelMessageZapButton from "@app/components/ChannelMessageZapButton.svelte"
|
||||||
import ChannelMessageEmojiButton from "@app/components/ChannelMessageEmojiButton.svelte"
|
import ChannelMessageEmojiButton from "@app/components/ChannelMessageEmojiButton.svelte"
|
||||||
import ChannelMessageMenuButton from "@app/components/ChannelMessageMenuButton.svelte"
|
import ChannelMessageMenuButton from "@app/components/ChannelMessageMenuButton.svelte"
|
||||||
import ChannelMessageMenuMobile from "@app/components/ChannelMessageMenuMobile.svelte"
|
import ChannelMessageMenuMobile from "@app/components/ChannelMessageMenuMobile.svelte"
|
||||||
import {colors} from "@app/state"
|
import {colors, ENABLE_ZAPS} from "@app/core/state"
|
||||||
import {publishDelete, publishReaction} from "@app/commands"
|
import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
url: string
|
url: string
|
||||||
room: string
|
|
||||||
event: TrustedEvent
|
event: TrustedEvent
|
||||||
replyTo?: (event: TrustedEvent) => void
|
replyTo?: (event: TrustedEvent) => void
|
||||||
showPubkey?: boolean
|
showPubkey?: boolean
|
||||||
inert?: boolean
|
inert?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const {url, room, event, replyTo = undefined, showPubkey = false, inert = false}: Props = $props()
|
const {url, event, replyTo = undefined, showPubkey = false, inert = false}: Props = $props()
|
||||||
|
|
||||||
const thunk = $thunks[event.id]
|
const thunk = $thunks[event.id]
|
||||||
|
const shouldProtect = canEnforceNip70(url)
|
||||||
const today = formatTimestampAsDate(now())
|
const today = formatTimestampAsDate(now())
|
||||||
const profile = deriveProfile(event.pubkey, [url])
|
const profile = deriveProfile(event.pubkey, [url])
|
||||||
const profileDisplay = deriveProfileDisplay(event.pubkey, [url])
|
const profileDisplay = deriveProfileDisplay(event.pubkey, [url])
|
||||||
@@ -41,15 +42,11 @@
|
|||||||
|
|
||||||
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey, url})
|
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey, url})
|
||||||
|
|
||||||
const onReactionClick = (content: string, events: TrustedEvent[]) => {
|
const deleteReaction = async (event: TrustedEvent) =>
|
||||||
const reaction = events.find(e => e.pubkey === $pubkey)
|
publishDelete({relays: [url], event, protect: await shouldProtect})
|
||||||
|
|
||||||
if (reaction) {
|
const createReaction = async (template: EventContent) =>
|
||||||
publishDelete({relays: [url], event: reaction})
|
publishReaction({...template, event, relays: [url], protect: await shouldProtect})
|
||||||
} else {
|
|
||||||
publishReaction({event, content, relays: [url]})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<TapTarget
|
<TapTarget
|
||||||
@@ -81,21 +78,29 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<Content {event} {url} />
|
<Content minimalQuote {event} {url} />
|
||||||
{#if thunk}
|
{#if thunk}
|
||||||
<ThunkStatus {thunk} class="mt-2" />
|
<ThunkFailure showToastOnRetry {thunk} class="mt-2" />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row-2 ml-10 mt-1">
|
<div class="row-2 ml-10 mt-1 pl-1">
|
||||||
<ReactionSummary {url} {event} {onReactionClick} reactionClass="tooltip-right" />
|
<ReactionSummary
|
||||||
|
{url}
|
||||||
|
{event}
|
||||||
|
{deleteReaction}
|
||||||
|
{createReaction}
|
||||||
|
reactionClass="tooltip-right" />
|
||||||
</div>
|
</div>
|
||||||
{#if !isMobile}
|
{#if !isMobile}
|
||||||
<button
|
<button
|
||||||
class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all"
|
class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all"
|
||||||
class:group-hover:opacity-100={!isMobile}>
|
class:group-hover:opacity-100={!isMobile}>
|
||||||
<ChannelMessageEmojiButton {url} {room} {event} />
|
{#if ENABLE_ZAPS}
|
||||||
|
<ChannelMessageZapButton {url} {event} />
|
||||||
|
{/if}
|
||||||
|
<ChannelMessageEmojiButton {url} {event} />
|
||||||
{#if replyTo}
|
{#if replyTo}
|
||||||
<Button class="btn join-item btn-xs" onclick={reply}>
|
<Button class="btn join-item btn-xs" onclick={reply}>
|
||||||
<Icon icon="reply" size={4} />
|
<Icon icon="reply" size={4} />
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {noop} from "@welshman/lib"
|
|
||||||
import type {NativeEmoji} from "emoji-picker-element/shared"
|
import type {NativeEmoji} from "emoji-picker-element/shared"
|
||||||
import EmojiButton from "@lib/components/EmojiButton.svelte"
|
import EmojiButton from "@lib/components/EmojiButton.svelte"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import {publishReaction} from "@app/commands"
|
import {publishReaction, canEnforceNip70} from "@app/core/commands"
|
||||||
|
|
||||||
const {url, room, event} = $props()
|
const {url, event} = $props()
|
||||||
|
|
||||||
// Tell svelte-check to shut up
|
const shouldProtect = canEnforceNip70(url)
|
||||||
noop(room)
|
|
||||||
|
|
||||||
const onEmoji = (emoji: NativeEmoji) =>
|
const onEmoji = async (emoji: NativeEmoji) =>
|
||||||
publishReaction({event, relays: [url], content: emoji.unicode})
|
publishReaction({
|
||||||
|
event,
|
||||||
|
relays: [url],
|
||||||
|
content: emoji.unicode,
|
||||||
|
protect: await shouldProtect,
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<EmojiButton {onEmoji} class="btn join-item btn-xs">
|
<EmojiButton {onEmoji} class="btn join-item btn-xs">
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import EventInfo from "@app/components/EventInfo.svelte"
|
import EventInfo from "@app/components/EventInfo.svelte"
|
||||||
import EventReport from "@app/components/EventReport.svelte"
|
import EventReport from "@app/components/EventReport.svelte"
|
||||||
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
const {url, event, onClick} = $props()
|
const {url, event, onClick} = $props()
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,12 @@
|
|||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import EmojiPicker from "@lib/components/EmojiPicker.svelte"
|
import EmojiPicker from "@lib/components/EmojiPicker.svelte"
|
||||||
|
import ZapButton from "@app/components/ZapButton.svelte"
|
||||||
import EventInfo from "@app/components/EventInfo.svelte"
|
import EventInfo from "@app/components/EventInfo.svelte"
|
||||||
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
||||||
import {publishReaction} from "@app/commands"
|
import {ENABLE_ZAPS} from "@app/core/state"
|
||||||
import {pushModal} from "@app/modal"
|
import {publishReaction, canEnforceNip70} from "@app/core/commands"
|
||||||
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
@@ -18,9 +20,16 @@
|
|||||||
|
|
||||||
const {url, event, reply}: Props = $props()
|
const {url, event, reply}: Props = $props()
|
||||||
|
|
||||||
const onEmoji = ((event: TrustedEvent, url: string, emoji: NativeEmoji) => {
|
const shouldProtect = canEnforceNip70(url)
|
||||||
|
|
||||||
|
const onEmoji = (async (event: TrustedEvent, url: string, emoji: NativeEmoji) => {
|
||||||
history.back()
|
history.back()
|
||||||
publishReaction({event, relays: [url], content: emoji.unicode})
|
publishReaction({
|
||||||
|
event,
|
||||||
|
relays: [url],
|
||||||
|
content: emoji.unicode,
|
||||||
|
protect: await shouldProtect,
|
||||||
|
})
|
||||||
}).bind(undefined, event, url)
|
}).bind(undefined, event, url)
|
||||||
|
|
||||||
const showEmojiPicker = () => pushModal(EmojiPicker, {onClick: onEmoji}, {replaceState: true})
|
const showEmojiPicker = () => pushModal(EmojiPicker, {onClick: onEmoji}, {replaceState: true})
|
||||||
@@ -40,6 +49,12 @@
|
|||||||
<Icon size={4} icon="smile-circle" />
|
<Icon size={4} icon="smile-circle" />
|
||||||
Send Reaction
|
Send Reaction
|
||||||
</Button>
|
</Button>
|
||||||
|
{#if ENABLE_ZAPS}
|
||||||
|
<ZapButton replaceState {url} {event} class="btn btn-secondary w-full">
|
||||||
|
<Icon size={4} icon="bolt" />
|
||||||
|
Send Zap
|
||||||
|
</ZapButton>
|
||||||
|
{/if}
|
||||||
<Button class="btn btn-neutral w-full" onclick={sendReply}>
|
<Button class="btn btn-neutral w-full" onclick={sendReply}>
|
||||||
<Icon size={4} icon="reply" />
|
<Icon size={4} icon="reply" />
|
||||||
Send Reply
|
Send Reply
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import ZapButton from "@app/components/ZapButton.svelte"
|
||||||
|
|
||||||
|
const {url, event} = $props()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ZapButton {url} {event} class="btn join-item btn-xs">
|
||||||
|
<Icon icon="bolt" size={4} />
|
||||||
|
</ZapButton>
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {GENERAL, channelsById, makeChannelId} from "@app/state"
|
import {channelsById, makeChannelId} from "@app/core/state"
|
||||||
|
|
||||||
const {url, room} = $props()
|
const {url, room} = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if room === GENERAL}
|
{$channelsById.get(makeChannelId(url, room))?.name || room}
|
||||||
general
|
|
||||||
{:else}
|
|
||||||
{$channelsById.get(makeChannelId(url, room))?.name || room}
|
|
||||||
{/if}
|
|
||||||
|
|||||||
@@ -1,16 +1,36 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {Snippet} from "svelte"
|
import type {Snippet} from "svelte"
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {int, nthNe, MINUTE, sortBy, remove, formatTimestampAsDate} from "@welshman/lib"
|
import {
|
||||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
int,
|
||||||
import {createEvent, DIRECT_MESSAGE, INBOX_RELAYS} from "@welshman/util"
|
ms,
|
||||||
|
partition,
|
||||||
|
spec,
|
||||||
|
nthEq,
|
||||||
|
nthNe,
|
||||||
|
MINUTE,
|
||||||
|
sortBy,
|
||||||
|
remove,
|
||||||
|
formatTimestampAsDate,
|
||||||
|
} from "@welshman/lib"
|
||||||
|
import type {TrustedEvent, EventTemplate, EventContent} from "@welshman/util"
|
||||||
|
import {parse, isLink} from "@welshman/content"
|
||||||
|
import {
|
||||||
|
makeEvent,
|
||||||
|
tagsFromIMeta,
|
||||||
|
getTags,
|
||||||
|
DIRECT_MESSAGE,
|
||||||
|
DIRECT_MESSAGE_FILE,
|
||||||
|
} from "@welshman/util"
|
||||||
import {
|
import {
|
||||||
pubkey,
|
pubkey,
|
||||||
tagPubkey,
|
tagPubkey,
|
||||||
sendWrapped,
|
sendWrapped,
|
||||||
loadUsingOutbox,
|
mergeThunks,
|
||||||
|
loadInboxRelaySelections,
|
||||||
inboxRelaySelectionsByPubkey,
|
inboxRelaySelectionsByPubkey,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
|
import type {AbstractThunk} from "@welshman/app"
|
||||||
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 Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
@@ -26,15 +46,17 @@
|
|||||||
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 ChatComposeParent from "@app/components/ChatComposeParent.svelte"
|
import ChatComposeParent from "@app/components/ChatComposeParent.svelte"
|
||||||
|
import ThunkToast from "@app/components/ThunkToast.svelte"
|
||||||
import {
|
import {
|
||||||
INDEXER_RELAYS,
|
INDEXER_RELAYS,
|
||||||
userSettingValues,
|
userSettingValues,
|
||||||
deriveChat,
|
deriveChat,
|
||||||
splitChatId,
|
splitChatId,
|
||||||
PLATFORM_NAME,
|
PLATFORM_NAME,
|
||||||
} from "@app/state"
|
} from "@app/core/state"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
import {prependParent} from "@app/commands"
|
import {prependParent} from "@app/core/commands"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id: string
|
id: string
|
||||||
@@ -61,13 +83,63 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onSubmit = async (params: EventContent) => {
|
const onSubmit = async (params: EventContent) => {
|
||||||
// Remove p tags since they result in forking the conversation
|
const ptags = remove($pubkey!, pubkeys).map(tagPubkey)
|
||||||
const tags = [...params.tags.filter(nthNe(0, "p")), ...remove($pubkey!, pubkeys).map(tagPubkey)]
|
|
||||||
|
|
||||||
await sendWrapped({
|
// Remove p tags since they result in forking the conversation
|
||||||
pubkeys,
|
params.tags = params.tags.filter(nthNe(0, "p"))
|
||||||
template: createEvent(DIRECT_MESSAGE, prependParent(parent, {...params, tags})),
|
|
||||||
delay: $userSettingValues.send_delay,
|
// 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: AbstractThunk[] = []
|
||||||
|
for (let i = 0; i < templates.length; i++) {
|
||||||
|
const template = templates[i]
|
||||||
|
|
||||||
|
thunks.push(
|
||||||
|
await sendWrapped({pubkeys, template, delay: $userSettingValues.send_delay + ms(i)}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pushToast({
|
||||||
|
timeout: 30_000,
|
||||||
|
children: {
|
||||||
|
component: ThunkToast,
|
||||||
|
props: {thunk: mergeThunks(thunks)},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
clearParent()
|
clearParent()
|
||||||
@@ -98,7 +170,7 @@
|
|||||||
id,
|
id,
|
||||||
type: "note",
|
type: "note",
|
||||||
value: event,
|
value: event,
|
||||||
showPubkey: created_at - previousCreatedAt > int(15, MINUTE) || previousPubkey !== pubkey,
|
showPubkey: created_at - previousCreatedAt > int(2, MINUTE) || previousPubkey !== pubkey,
|
||||||
})
|
})
|
||||||
|
|
||||||
previousDate = date
|
previousDate = date
|
||||||
@@ -110,13 +182,8 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// Don't use loadInboxRelaySelection because we want to force reload
|
|
||||||
for (const pubkey of others) {
|
for (const pubkey of others) {
|
||||||
loadUsingOutbox({
|
loadInboxRelaySelections(pubkey, INDEXER_RELAYS, true)
|
||||||
pubkey,
|
|
||||||
kind: INBOX_RELAYS,
|
|
||||||
relays: INDEXER_RELAYS,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const observer = new ResizeObserver(() => {
|
const observer = new ResizeObserver(() => {
|
||||||
@@ -191,7 +258,7 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
</PageBar>
|
</PageBar>
|
||||||
|
|
||||||
<PageContent class="flex flex-col-reverse pt-4">
|
<PageContent class="flex flex-col-reverse gap-2 pt-4">
|
||||||
<div bind:this={dynamicPadding}></div>
|
<div bind:this={dynamicPadding}></div>
|
||||||
{#if missingInboxes.includes($pubkey!)}
|
{#if missingInboxes.includes($pubkey!)}
|
||||||
<div class="py-12">
|
<div class="py-12">
|
||||||
|
|||||||
@@ -20,6 +20,8 @@
|
|||||||
|
|
||||||
export const focus = () => editor.then(ed => ed.chain().focus().run())
|
export const focus = () => editor.then(ed => ed.chain().focus().run())
|
||||||
|
|
||||||
|
const uploadFiles = () => editor.then(ed => ed.chain().selectFiles().run())
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if ($uploading) return
|
if ($uploading) return
|
||||||
|
|
||||||
@@ -40,11 +42,21 @@
|
|||||||
submit,
|
submit,
|
||||||
uploading,
|
uploading,
|
||||||
aggressive: true,
|
aggressive: true,
|
||||||
disableFileUpload: true,
|
|
||||||
})
|
})
|
||||||
</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)}>
|
||||||
|
<Button
|
||||||
|
data-tip="Add an image"
|
||||||
|
class="center tooltip tooltip-right h-10 w-10 min-w-10 rounded-box bg-base-300 transition-colors hover:bg-base-200"
|
||||||
|
disabled={$uploading}
|
||||||
|
onclick={uploadFiles}>
|
||||||
|
{#if $uploading}
|
||||||
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
|
{:else}
|
||||||
|
<Icon icon="gallery-send" />
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
<div class="chat-editor flex-grow overflow-hidden">
|
<div class="chat-editor flex-grow overflow-hidden">
|
||||||
<EditorContent {editor} />
|
<EditorContent {editor} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import {canDecrypt, PLATFORM_NAME, ensureUnwrapped} from "@app/state"
|
import {canDecrypt, PLATFORM_NAME, ensureUnwrapped} from "@app/core/state"
|
||||||
import {clearModals} from "@app/modal"
|
import {clearModals} from "@app/util/modal"
|
||||||
|
|
||||||
const {next} = $props()
|
const {next} = $props()
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
import ProfileName from "@app/components/ProfileName.svelte"
|
import ProfileName from "@app/components/ProfileName.svelte"
|
||||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||||
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
||||||
import {makeChatPath} from "@app/routes"
|
import {makeChatPath} from "@app/util/routes"
|
||||||
import {notifications} from "@app/notifications"
|
import {notifications} from "@app/util/notifications"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string
|
id: string
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
const {...props}: Props = $props()
|
const {...props}: Props = $props()
|
||||||
|
|
||||||
const others = remove($pubkey!, props.pubkeys)
|
const others = remove($pubkey!, props.pubkeys)
|
||||||
const active = $page.params.chat === props.id
|
const active = $derived($page.params.chat === props.id)
|
||||||
const path = makeChatPath(props.pubkeys)
|
const path = makeChatPath(props.pubkeys)
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
@@ -59,6 +59,11 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<p class="overflow-hidden text-ellipsis whitespace-nowrap text-sm">
|
<p class="overflow-hidden text-ellipsis whitespace-nowrap text-sm">
|
||||||
|
<span class="opacity-50">
|
||||||
|
{#if props.messages[0].pubkey === $pubkey}
|
||||||
|
You:
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
{props.messages[0].content}
|
{props.messages[0].content}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
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 ChatStart from "@app/components/ChatStart.svelte"
|
import ChatStart from "@app/components/ChatStart.svelte"
|
||||||
import {setChecked} from "@app/notifications"
|
import {setChecked} from "@app/util/notifications"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
const startChat = () => pushModal(ChatStart, {}, {replaceState: true})
|
const startChat = () => pushModal(ChatStart, {}, {replaceState: true})
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {type Instance} from "tippy.js"
|
import {type Instance} from "tippy.js"
|
||||||
import {hash, formatTimestampAsTime} from "@welshman/lib"
|
import {hash, formatTimestampAsTime} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||||
import {thunks, pubkey, deriveProfile, deriveProfileDisplay, sendWrapped} from "@welshman/app"
|
import {thunks, pubkey, deriveProfile, deriveProfileDisplay, sendWrapped} from "@welshman/app"
|
||||||
import {isMobile} from "@lib/html"
|
import {isMobile} from "@lib/html"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
@@ -11,13 +11,13 @@
|
|||||||
import Avatar from "@lib/components/Avatar.svelte"
|
import Avatar from "@lib/components/Avatar.svelte"
|
||||||
import Content from "@app/components/Content.svelte"
|
import Content from "@app/components/Content.svelte"
|
||||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||||
import ThunkStatus from "@app/components/ThunkStatus.svelte"
|
import ThunkFailure from "@app/components/ThunkFailure.svelte"
|
||||||
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||||
import ChatMessageMenu from "@app/components/ChatMessageMenu.svelte"
|
import ChatMessageMenu from "@app/components/ChatMessageMenu.svelte"
|
||||||
import ChatMessageMenuMobile from "@app/components/ChatMessageMenuMobile.svelte"
|
import ChatMessageMenuMobile from "@app/components/ChatMessageMenuMobile.svelte"
|
||||||
import {colors} from "@app/state"
|
import {colors} from "@app/core/state"
|
||||||
import {makeDelete, makeReaction} from "@app/commands"
|
import {makeDelete, makeReaction} from "@app/core/commands"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
event: TrustedEvent
|
event: TrustedEvent
|
||||||
@@ -36,12 +36,11 @@
|
|||||||
|
|
||||||
const reply = () => replyTo(event)
|
const reply = () => replyTo(event)
|
||||||
|
|
||||||
const onReactionClick = async (content: string, events: TrustedEvent[]) => {
|
const deleteReaction = (event: TrustedEvent) =>
|
||||||
const reaction = events.find(e => e.pubkey === $pubkey)
|
sendWrapped({template: makeDelete({event, protect: false}), pubkeys})
|
||||||
const template = reaction ? makeDelete({event: reaction}) : makeReaction({event, content})
|
|
||||||
|
|
||||||
await sendWrapped({template, pubkeys})
|
const createReaction = (template: EventContent) =>
|
||||||
}
|
sendWrapped({template: makeReaction({event, protect: false, ...template}), pubkeys})
|
||||||
|
|
||||||
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey})
|
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey})
|
||||||
|
|
||||||
@@ -60,7 +59,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if thunk}
|
{#if thunk}
|
||||||
<ThunkStatus {thunk} class="mt-1" />
|
<ThunkFailure showToastOnRetry {thunk} class="mt-1" />
|
||||||
{/if}
|
{/if}
|
||||||
<div
|
<div
|
||||||
data-event={event.id}
|
data-event={event.id}
|
||||||
@@ -120,7 +119,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</TapTarget>
|
</TapTarget>
|
||||||
<div class="row-2 z-feature -mt-4 ml-4">
|
<div class="row-2 z-feature -mt-4 ml-4">
|
||||||
<ReactionSummary {event} {onReactionClick} noTooltip />
|
<ReactionSummary {event} {deleteReaction} {createReaction} noTooltip />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import {sendWrapped} from "@welshman/app"
|
import {sendWrapped} from "@welshman/app"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import EmojiButton from "@lib/components/EmojiButton.svelte"
|
import EmojiButton from "@lib/components/EmojiButton.svelte"
|
||||||
import {makeReaction} from "@app/commands"
|
import {makeReaction} from "@app/core/commands"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
event: TrustedEvent
|
event: TrustedEvent
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
const {event, pubkeys}: Props = $props()
|
const {event, pubkeys}: Props = $props()
|
||||||
|
|
||||||
const onEmoji = (emoji: NativeEmoji) =>
|
const onEmoji = (emoji: NativeEmoji) =>
|
||||||
sendWrapped({template: makeReaction({event, content: emoji.unicode}), pubkeys})
|
sendWrapped({template: makeReaction({event, content: emoji.unicode, protect: false}), pubkeys})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<EmojiButton {onEmoji} class="btn join-item btn-xs">
|
<EmojiButton {onEmoji} class="btn join-item btn-xs">
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
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/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
const {event, pubkeys, popover, replyTo} = $props()
|
const {event, pubkeys, popover, replyTo} = $props()
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import EmojiPicker from "@lib/components/EmojiPicker.svelte"
|
import EmojiPicker from "@lib/components/EmojiPicker.svelte"
|
||||||
import EventInfo from "@app/components/EventInfo.svelte"
|
import EventInfo from "@app/components/EventInfo.svelte"
|
||||||
import {makeReaction} from "@app/commands"
|
import {makeReaction} from "@app/core/commands"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
import {clip} from "@app/toast"
|
import {clip} from "@app/util/toast"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
pubkeys: string[]
|
pubkeys: string[]
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
const onEmoji = ((event: TrustedEvent, pubkeys: string[], emoji: NativeEmoji) => {
|
const onEmoji = ((event: TrustedEvent, pubkeys: string[], emoji: NativeEmoji) => {
|
||||||
history.back()
|
history.back()
|
||||||
sendWrapped({template: makeReaction({event, content: emoji.unicode}), pubkeys})
|
sendWrapped({template: makeReaction({event, content: emoji.unicode, protect: false}), pubkeys})
|
||||||
}).bind(undefined, event, pubkeys)
|
}).bind(undefined, event, pubkeys)
|
||||||
|
|
||||||
const showEmojiPicker = () => pushModal(EmojiPicker, {onClick: onEmoji}, {replaceState: true})
|
const showEmojiPicker = () => pushModal(EmojiPicker, {onClick: onEmoji}, {replaceState: true})
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import * as nip19 from "nostr-tools/nip19"
|
||||||
|
import {onMount} from "svelte"
|
||||||
|
import {writable} from "svelte/store"
|
||||||
import {goto} from "$app/navigation"
|
import {goto} from "$app/navigation"
|
||||||
|
import {tryCatch, uniq} from "@welshman/lib"
|
||||||
|
import {fromNostrURI} from "@welshman/util"
|
||||||
import {pubkey} from "@welshman/app"
|
import {pubkey} from "@welshman/app"
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import Field from "@lib/components/Field.svelte"
|
import Field from "@lib/components/Field.svelte"
|
||||||
@@ -8,13 +13,42 @@
|
|||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import ProfileMultiSelect from "@app/components/ProfileMultiSelect.svelte"
|
import ProfileMultiSelect from "@app/components/ProfileMultiSelect.svelte"
|
||||||
import {makeChatPath} from "@app/routes"
|
import {makeChatPath} from "@app/util/routes"
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const onSubmit = () => goto(makeChatPath([...pubkeys, $pubkey!]))
|
const onSubmit = () => goto(makeChatPath([...pubkeys, $pubkey!]))
|
||||||
|
|
||||||
|
const addPubkey = (pubkey: string) => {
|
||||||
|
pubkeys = uniq([...pubkeys, pubkey])
|
||||||
|
term.set("")
|
||||||
|
}
|
||||||
|
|
||||||
|
const term = writable("")
|
||||||
|
|
||||||
let pubkeys: string[] = $state([])
|
let pubkeys: string[] = $state([])
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
return term.subscribe(t => {
|
||||||
|
if (t.match(/^[0-9a-f]{64}$/)) {
|
||||||
|
addPubkey(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (t.match(/^(nostr:)?(npub1|nprofile1)/)) {
|
||||||
|
tryCatch(() => {
|
||||||
|
const {type, data} = nip19.decode(fromNostrURI(t))
|
||||||
|
|
||||||
|
if (type === "npub") {
|
||||||
|
addPubkey(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "nprofile") {
|
||||||
|
addPubkey(data.pubkey)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="column gap-4" onsubmit={preventDefault(onSubmit)}>
|
<form class="column gap-4" onsubmit={preventDefault(onSubmit)}>
|
||||||
@@ -28,7 +62,7 @@
|
|||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<Field>
|
<Field>
|
||||||
{#snippet input()}
|
{#snippet input()}
|
||||||
<ProfileMultiSelect autofocus bind:value={pubkeys} />
|
<ProfileMultiSelect autofocus bind:value={pubkeys} {term} />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Field>
|
</Field>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||||
|
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||||
|
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
|
||||||
|
import EventActivity from "@app/components/EventActivity.svelte"
|
||||||
|
import EventActions from "@app/components/EventActions.svelte"
|
||||||
|
import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands"
|
||||||
|
import {makeThreadPath} from "@app/util/routes"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
url: any
|
||||||
|
event: any
|
||||||
|
showActivity?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, event, showActivity = false}: Props = $props()
|
||||||
|
|
||||||
|
const shouldProtect = canEnforceNip70(url)
|
||||||
|
|
||||||
|
const path = makeThreadPath(url, event.id)
|
||||||
|
|
||||||
|
const deleteReaction = async (event: TrustedEvent) =>
|
||||||
|
publishDelete({relays: [url], event, protect: await shouldProtect})
|
||||||
|
|
||||||
|
const createReaction = async (template: EventContent) =>
|
||||||
|
publishReaction({...template, event, relays: [url], protect: await shouldProtect})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
||||||
|
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
|
||||||
|
<ThunkStatusOrDeleted {event} />
|
||||||
|
{#if showActivity}
|
||||||
|
<EventActivity {url} {path} {event} />
|
||||||
|
{/if}
|
||||||
|
<EventActions {url} {event} noun="Comment" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
truncate,
|
truncate,
|
||||||
renderAsHtml,
|
renderAsHtml,
|
||||||
isText,
|
isText,
|
||||||
|
isEmoji,
|
||||||
isTopic,
|
isTopic,
|
||||||
isCode,
|
isCode,
|
||||||
isCashu,
|
isCashu,
|
||||||
@@ -22,6 +23,7 @@
|
|||||||
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 ContentToken from "@app/components/ContentToken.svelte"
|
import ContentToken from "@app/components/ContentToken.svelte"
|
||||||
|
import ContentEmoji from "@app/components/ContentEmoji.svelte"
|
||||||
import ContentCode from "@app/components/ContentCode.svelte"
|
import ContentCode from "@app/components/ContentCode.svelte"
|
||||||
import ContentLinkInline from "@app/components/ContentLinkInline.svelte"
|
import ContentLinkInline from "@app/components/ContentLinkInline.svelte"
|
||||||
import ContentLinkBlock from "@app/components/ContentLinkBlock.svelte"
|
import ContentLinkBlock from "@app/components/ContentLinkBlock.svelte"
|
||||||
@@ -29,7 +31,7 @@
|
|||||||
import ContentQuote from "@app/components/ContentQuote.svelte"
|
import ContentQuote from "@app/components/ContentQuote.svelte"
|
||||||
import ContentTopic from "@app/components/ContentTopic.svelte"
|
import ContentTopic from "@app/components/ContentTopic.svelte"
|
||||||
import ContentMention from "@app/components/ContentMention.svelte"
|
import ContentMention from "@app/components/ContentMention.svelte"
|
||||||
import {entityLink, userSettingValues} from "@app/state"
|
import {entityLink, userSettingValues} from "@app/core/state"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
event: any
|
event: any
|
||||||
@@ -38,6 +40,7 @@
|
|||||||
showEntire?: boolean
|
showEntire?: boolean
|
||||||
hideMediaAtDepth?: number
|
hideMediaAtDepth?: number
|
||||||
expandMode?: string
|
expandMode?: string
|
||||||
|
minimalQuote?: boolean
|
||||||
depth?: number
|
depth?: number
|
||||||
url?: string
|
url?: string
|
||||||
}
|
}
|
||||||
@@ -49,6 +52,7 @@
|
|||||||
showEntire = $bindable(false),
|
showEntire = $bindable(false),
|
||||||
hideMediaAtDepth = 1,
|
hideMediaAtDepth = 1,
|
||||||
expandMode = "block",
|
expandMode = "block",
|
||||||
|
minimalQuote = false,
|
||||||
depth = 0,
|
depth = 0,
|
||||||
url,
|
url,
|
||||||
}: Props = $props()
|
}: Props = $props()
|
||||||
@@ -133,6 +137,8 @@
|
|||||||
<ContentNewline value={parsed.value} />
|
<ContentNewline value={parsed.value} />
|
||||||
{:else if isTopic(parsed)}
|
{:else if isTopic(parsed)}
|
||||||
<ContentTopic value={parsed.value} />
|
<ContentTopic value={parsed.value} />
|
||||||
|
{:else if isEmoji(parsed)}
|
||||||
|
<ContentEmoji value={parsed.value} />
|
||||||
{:else if isCode(parsed)}
|
{:else if isCode(parsed)}
|
||||||
<ContentCode
|
<ContentCode
|
||||||
value={parsed.value}
|
value={parsed.value}
|
||||||
@@ -149,7 +155,13 @@
|
|||||||
<ContentMention value={parsed.value} {url} />
|
<ContentMention value={parsed.value} {url} />
|
||||||
{:else if isEvent(parsed) || isAddress(parsed)}
|
{:else if isEvent(parsed) || isAddress(parsed)}
|
||||||
{#if isBlock(i)}
|
{#if isBlock(i)}
|
||||||
<ContentQuote {depth} {url} {hideMediaAtDepth} value={parsed.value} {event} />
|
<ContentQuote
|
||||||
|
{depth}
|
||||||
|
{url}
|
||||||
|
{hideMediaAtDepth}
|
||||||
|
value={parsed.value}
|
||||||
|
{event}
|
||||||
|
minimal={minimalQuote} />
|
||||||
{:else}
|
{:else}
|
||||||
<Link
|
<Link
|
||||||
external
|
external
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type {ParsedEmojiValue} from "@welshman/content"
|
||||||
|
import {imgproxy} from "@app/core/state"
|
||||||
|
|
||||||
|
export let value: ParsedEmojiValue
|
||||||
|
|
||||||
|
const alt = `:${value.name}:`
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if value.url}
|
||||||
|
<img
|
||||||
|
{alt}
|
||||||
|
src={imgproxy(value.url, {w: 24, h: 24})}
|
||||||
|
class="-mt-0.5 inline h-[1em] min-w-[1em] align-middle" />
|
||||||
|
{:else}
|
||||||
|
{alt}
|
||||||
|
{/if}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {ellipsize, postJson} from "@welshman/lib"
|
import {ellipsize, displayUrl, postJson} from "@welshman/lib"
|
||||||
import {dufflepud, imgproxy} from "@app/state"
|
import {dufflepud, imgproxy} from "@app/core/state"
|
||||||
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/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
const {value, event} = $props()
|
const {value, event} = $props()
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Link external href={url} class="my-2 block">
|
<Link external href={url} class="my-2 block">
|
||||||
<div class="overflow-hidden rounded-box leading-[0]">
|
<div class="overflow-hidden rounded-box">
|
||||||
{#if url.match(/\.(mov|webm|mp4)$/)}
|
{#if url.match(/\.(mov|webm|mp4)$/)}
|
||||||
<video controls src={url} class="max-h-96 object-contain object-center">
|
<video controls src={url} class="max-h-96 object-contain object-center">
|
||||||
<track kind="captions" />
|
<track kind="captions" />
|
||||||
@@ -52,15 +52,13 @@
|
|||||||
alt="Link preview"
|
alt="Link preview"
|
||||||
onerror={onError}
|
onerror={onError}
|
||||||
src={imgproxy(preview.image)}
|
src={imgproxy(preview.image)}
|
||||||
class="bg-alt max-h-72 object-contain object-center" />
|
class="bg-alt max-h-72 rounded-t-box object-contain object-center" />
|
||||||
{/if}
|
|
||||||
{#if preview.title}
|
|
||||||
<div class="flex flex-col gap-2 p-4">
|
|
||||||
<strong class="overflow-hidden text-ellipsis whitespace-nowrap"
|
|
||||||
>{preview.title}</strong>
|
|
||||||
<p>{ellipsize(preview.description, 140)}</p>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
<div class="flex flex-col gap-2 p-4">
|
||||||
|
<strong class="overflow-hidden text-ellipsis whitespace-nowrap"
|
||||||
|
>{preview.title || displayUrl(url)}</strong>
|
||||||
|
<p>{ellipsize(preview.description, 140)}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:catch}
|
{:catch}
|
||||||
<p class="bg-alt p-12 text-center leading-normal">
|
<p class="bg-alt p-12 text-center leading-normal">
|
||||||
|
|||||||
@@ -1,49 +1,52 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onDestroy} from "svelte"
|
import {onMount, onDestroy} from "svelte"
|
||||||
import {now} from "@welshman/lib"
|
import {displayUrl} from "@welshman/lib"
|
||||||
import {BLOSSOM_AUTH, makeEvent, getTags, getTagValue, tagsFromIMeta} from "@welshman/util"
|
import {getTags, decryptFile, getTagValue, tagsFromIMeta} from "@welshman/util"
|
||||||
import {signer} from "@welshman/app"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import {imgproxy} from "@app/state"
|
import {imgproxy} from "@app/core/state"
|
||||||
|
|
||||||
const {value, event, ...props} = $props()
|
const {value, event, ...props} = $props()
|
||||||
|
|
||||||
const url = value.url.toString()
|
const url = value.url.toString()
|
||||||
|
const meta =
|
||||||
// If we fail to fetch the image, try authenticating if we have a blossom hash
|
getTags("imeta", event.tags)
|
||||||
const onerror = async () => {
|
|
||||||
const meta = getTags("imeta", event.tags)
|
|
||||||
.map(tagsFromIMeta)
|
.map(tagsFromIMeta)
|
||||||
.find(meta => getTagValue("url", meta) === url)
|
.find(meta => getTagValue("url", meta) === url) || event.tags
|
||||||
const hash = meta ? getTagValue("x", meta) : undefined
|
|
||||||
|
|
||||||
if (hash && $signer) {
|
const key = getTagValue("decryption-key", meta)
|
||||||
const event = await signer.get().sign(
|
const nonce = getTagValue("decryption-nonce", meta)
|
||||||
makeEvent(BLOSSOM_AUTH, {
|
const algorithm = getTagValue("encryption-algorithm", meta)
|
||||||
tags: [
|
|
||||||
["t", "get"],
|
|
||||||
["x", hash],
|
|
||||||
["expiration", String(now() + 30)],
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
const res = await fetch(url, {
|
const onError = () => {
|
||||||
headers: {
|
hasError = true
|
||||||
Authorization: `Nostr ${btoa(JSON.stringify(event))}`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if (res.status === 200) {
|
|
||||||
src = URL.createObjectURL(await res.blob())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let hasError = $state(false)
|
||||||
let src = $state(imgproxy(url))
|
let src = $state(imgproxy(url))
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
if (algorithm === "aes-gcm" && key && nonce) {
|
||||||
|
const response = await fetch(url)
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const ciphertext = new Uint8Array(await response.arrayBuffer())
|
||||||
|
const decryptedData = await decryptFile({ciphertext, key, nonce, algorithm})
|
||||||
|
|
||||||
|
src = URL.createObjectURL(new Blob([decryptedData]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
URL.revokeObjectURL(src)
|
URL.revokeObjectURL(src)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<img alt="" {src} {onerror} {...props} />
|
{#if hasError}
|
||||||
|
<a href={url} class="link-content whitespace-nowrap">
|
||||||
|
<Icon icon="link-round" size={3} class="inline-block" />
|
||||||
|
{displayUrl(url)}
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<img alt="" {src} onerror={onError} {...props} />
|
||||||
|
{/if}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
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/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
const {value} = $props()
|
const {value} = $props()
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {removeNil} from "@welshman/lib"
|
import {removeNil} from "@welshman/lib"
|
||||||
import type {ProfilePointer} from "@welshman/content"
|
import type {ProfilePointer} from "@welshman/content"
|
||||||
import {displayProfile} from "@welshman/util"
|
import {deriveProfileDisplay} from "@welshman/app"
|
||||||
import {deriveProfile} from "@welshman/app"
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value: ProfilePointer
|
value: ProfilePointer
|
||||||
@@ -14,11 +13,11 @@
|
|||||||
|
|
||||||
const {value, url}: Props = $props()
|
const {value, url}: Props = $props()
|
||||||
|
|
||||||
const profile = deriveProfile(value.pubkey, removeNil([url]))
|
const display = deriveProfileDisplay(value.pubkey, removeNil([url]))
|
||||||
|
|
||||||
const openProfile = () => pushModal(ProfileDetail, {pubkey: value.pubkey, url})
|
const openProfile = () => pushModal(ProfileDetail, {pubkey: value.pubkey, url})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button onclick={openProfile} class="link-content">
|
<Button onclick={openProfile} class="link-content">
|
||||||
@{displayProfile($profile)}
|
@{$display}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as nip19 from "nostr-tools/nip19"
|
import * as nip19 from "nostr-tools/nip19"
|
||||||
import {goto} from "$app/navigation"
|
|
||||||
import {nthEq} from "@welshman/lib"
|
|
||||||
import {Router} from "@welshman/router"
|
import {Router} from "@welshman/router"
|
||||||
import {tracker, repository} from "@welshman/app"
|
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {Address, DIRECT_MESSAGE, MESSAGE, THREAD, EVENT_TIME} from "@welshman/util"
|
import {Address, MESSAGE} from "@welshman/util"
|
||||||
import {scrollToEvent} from "@lib/html"
|
|
||||||
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 NoteCard from "@app/components/NoteCard.svelte"
|
import NoteCard from "@app/components/NoteCard.svelte"
|
||||||
import NoteContent from "@app/components/NoteContent.svelte"
|
import NoteContent from "@app/components/NoteContent.svelte"
|
||||||
import {deriveEvent, entityLink, ROOM} from "@app/state"
|
import {deriveEvent, entityLink} from "@app/core/state"
|
||||||
import {makeThreadPath, makeCalendarPath, makeRoomPath} from "@app/routes"
|
import {goToEvent} from "@app/util/routes"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value: any
|
value: any
|
||||||
@@ -20,9 +16,10 @@
|
|||||||
event: TrustedEvent
|
event: TrustedEvent
|
||||||
depth: number
|
depth: number
|
||||||
url?: string
|
url?: string
|
||||||
|
minimal?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const {value, event, depth, hideMediaAtDepth, url}: Props = $props()
|
const {value, event, depth, hideMediaAtDepth, url, minimal}: Props = $props()
|
||||||
|
|
||||||
const {id, identifier, kind, pubkey, relays = []} = value
|
const {id, identifier, kind, pubkey, relays = []} = value
|
||||||
const idOrAddress = id || new Address(kind, pubkey, identifier).toString()
|
const idOrAddress = id || new Address(kind, pubkey, identifier).toString()
|
||||||
@@ -37,67 +34,28 @@
|
|||||||
? nip19.neventEncode({id, relays: mergedRelays})
|
? nip19.neventEncode({id, relays: mergedRelays})
|
||||||
: new Address(kind, pubkey, identifier, mergedRelays).toNaddr()
|
: new Address(kind, pubkey, identifier, mergedRelays).toNaddr()
|
||||||
|
|
||||||
const openMessage = (url: string, room: string, id: string) => {
|
|
||||||
const event = repository.getEvent(id)
|
|
||||||
|
|
||||||
if (event) {
|
|
||||||
goto(makeRoomPath(url, room))
|
|
||||||
scrollToEvent(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Boolean(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onclick = () => {
|
const onclick = () => {
|
||||||
if ($quote) {
|
if ($quote) {
|
||||||
if ($quote.kind === DIRECT_MESSAGE) {
|
goToEvent($quote)
|
||||||
return scrollToEvent($quote.id)
|
} else {
|
||||||
}
|
window.open(entityLink(entity))
|
||||||
|
|
||||||
const [url] = tracker.getRelays($quote.id)
|
|
||||||
const room = $quote.tags.find(nthEq(0, ROOM))?.[1]
|
|
||||||
|
|
||||||
if (url && room) {
|
|
||||||
if ($quote.kind === THREAD) {
|
|
||||||
return goto(makeThreadPath(url, $quote.id))
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($quote.kind === EVENT_TIME) {
|
|
||||||
return goto(makeCalendarPath(url, $quote.id))
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($quote.kind === MESSAGE) {
|
|
||||||
return scrollToEvent($quote.id) || openMessage(url, room, $quote.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
const kind = $quote.tags.find(nthEq(0, "K"))?.[1]
|
|
||||||
const id = $quote.tags.find(nthEq(0, "E"))?.[1]
|
|
||||||
|
|
||||||
if (id && kind) {
|
|
||||||
if (parseInt(kind) === THREAD) {
|
|
||||||
return goto(makeThreadPath(url, id))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parseInt(kind) === EVENT_TIME) {
|
|
||||||
return goto(makeCalendarPath(url, id))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parseInt(kind) === MESSAGE) {
|
|
||||||
return scrollToEvent(id) || openMessage(url, room, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.open(entityLink(entity))
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button class="my-2 block max-w-full text-left" {onclick}>
|
<Button class="my-2 block max-w-full text-left" {onclick}>
|
||||||
{#if $quote}
|
{#if $quote}
|
||||||
<NoteCard event={$quote} {url} class="bg-alt rounded-box p-4">
|
{#if minimal && $quote.kind === MESSAGE}
|
||||||
<NoteContent {hideMediaAtDepth} {url} event={$quote} depth={depth + 1} />
|
<div
|
||||||
</NoteCard>
|
class="border-l-2 border-solid border-l-primary py-1 pl-2 opacity-90"
|
||||||
|
style="background-color: color-mix(in srgb, var(--primary) 10%, var(--base-300) 90%);">
|
||||||
|
<NoteContent {hideMediaAtDepth} {url} event={$quote} depth={depth + 1} />
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<NoteCard event={$quote} {url} class="bg-alt rounded-box p-4">
|
||||||
|
<NoteContent {hideMediaAtDepth} {url} event={$quote} depth={depth + 1} />
|
||||||
|
</NoteCard>
|
||||||
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<div class="rounded-box p-4">
|
<div class="rounded-box p-4">
|
||||||
<Spinner loading>Loading event...</Spinner>
|
<Spinner loading>Loading event...</Spinner>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
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 {clip} from "@app/toast"
|
import {clip} from "@app/util/toast"
|
||||||
|
|
||||||
const {value} = $props()
|
const {value} = $props()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {formatTimestamp} from "@welshman/lib"
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Content from "@app/components/Content.svelte"
|
||||||
|
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||||
|
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
||||||
|
import {goToEvent} from "@app/util/routes"
|
||||||
|
import {displayChannel} from "@app/core/state"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
room?: string
|
||||||
|
events: TrustedEvent[]
|
||||||
|
latest: TrustedEvent
|
||||||
|
earliest: TrustedEvent
|
||||||
|
participants: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, room, events, latest, earliest, participants}: Props = $props()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Button class="card2 bg-alt" onclick={() => goToEvent(earliest)}>
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<ProfileCircle pubkey={earliest.pubkey} size={10} />
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<div class="flex items-center gap-2 text-sm opacity-70">
|
||||||
|
{#if room}
|
||||||
|
<span class="truncate font-medium text-blue-400">
|
||||||
|
#{displayChannel(url, room)}
|
||||||
|
</span>
|
||||||
|
<span class="opacity-50">•</span>
|
||||||
|
{/if}
|
||||||
|
<span class="text-nowrap">{formatTimestamp(earliest.created_at)}</span>
|
||||||
|
</div>
|
||||||
|
<Content minimalQuote minLength={100} maxLength={400} event={earliest} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ml-13 flex items-center justify-between">
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<Icon icon="alt-arrow-left" />
|
||||||
|
<span class="text-sm opacity-70">
|
||||||
|
{events.length}
|
||||||
|
{events.length === 1 ? "message" : "messages"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<ProfileCircles pubkeys={participants} size={6} />
|
||||||
|
<span class="text-sm opacity-70">
|
||||||
|
{participants.length}
|
||||||
|
{participants.length === 1 ? "participant" : "participants"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if latest !== earliest}
|
||||||
|
<Button class="card2 bg-alt" onclick={() => goToEvent(latest)}>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-2 text-sm opacity-70">
|
||||||
|
<ProfileCircle pubkey={latest.pubkey} size={5} />
|
||||||
|
<span class="font-medium">Latest reply:</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs opacity-50">
|
||||||
|
{formatTimestamp(latest.created_at)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Content minimalQuote minLength={100} maxLength={400} event={latest} />
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
@@ -4,8 +4,8 @@
|
|||||||
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 LogInPassword from "@app/components/LogInPassword.svelte"
|
import LogInPassword from "@app/components/LogInPassword.svelte"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
import {BURROW_URL} from "@app/state"
|
import {BURROW_URL} from "@app/core/state"
|
||||||
|
|
||||||
const {email, confirm_token} = $props()
|
const {email, confirm_token} = $props()
|
||||||
|
|
||||||
|
|||||||
@@ -6,30 +6,45 @@
|
|||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Tippy from "@lib/components/Tippy.svelte"
|
import Tippy from "@lib/components/Tippy.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import ZapButton from "@app/components/ZapButton.svelte"
|
||||||
import EmojiButton from "@lib/components/EmojiButton.svelte"
|
import EmojiButton from "@lib/components/EmojiButton.svelte"
|
||||||
import EventMenu from "@app/components/EventMenu.svelte"
|
import EventMenu from "@app/components/EventMenu.svelte"
|
||||||
import {publishReaction} from "@app/commands"
|
import {ENABLE_ZAPS} from "@app/core/state"
|
||||||
|
import {publishReaction, canEnforceNip70} from "@app/core/commands"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
noun: string
|
noun: string
|
||||||
event: TrustedEvent
|
event: TrustedEvent
|
||||||
|
hideZap?: boolean
|
||||||
customActions?: Snippet
|
customActions?: Snippet
|
||||||
}
|
}
|
||||||
|
|
||||||
const {url, noun, event, customActions}: Props = $props()
|
const {url, noun, event, hideZap, customActions}: Props = $props()
|
||||||
|
|
||||||
|
const shouldProtect = canEnforceNip70(url)
|
||||||
|
|
||||||
const showPopover = () => popover?.show()
|
const showPopover = () => popover?.show()
|
||||||
|
|
||||||
const hidePopover = () => popover?.hide()
|
const hidePopover = () => popover?.hide()
|
||||||
|
|
||||||
const onEmoji = (emoji: NativeEmoji) =>
|
const onEmoji = async (emoji: NativeEmoji) =>
|
||||||
publishReaction({event, content: emoji.unicode, relays: [url]})
|
publishReaction({
|
||||||
|
event,
|
||||||
|
content: emoji.unicode,
|
||||||
|
relays: [url],
|
||||||
|
protect: await shouldProtect,
|
||||||
|
})
|
||||||
|
|
||||||
let popover: Instance | undefined = $state()
|
let popover: Instance | undefined = $state()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button class="join rounded-full">
|
<Button class="join rounded-full">
|
||||||
|
{#if ENABLE_ZAPS && !hideZap}
|
||||||
|
<ZapButton {url} {event} class="btn join-item btn-neutral btn-xs">
|
||||||
|
<Icon icon="bolt" size={4} />
|
||||||
|
</ZapButton>
|
||||||
|
{/if}
|
||||||
<EmojiButton {onEmoji} class="btn join-item btn-neutral btn-xs">
|
<EmojiButton {onEmoji} class="btn join-item btn-neutral btn-xs">
|
||||||
<Icon icon="smile-circle" size={4} />
|
<Icon icon="smile-circle" size={4} />
|
||||||
</EmojiButton>
|
</EmojiButton>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import {deriveEvents} from "@welshman/store"
|
import {deriveEvents} from "@welshman/store"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {repository} from "@welshman/app"
|
import {repository} from "@welshman/app"
|
||||||
import {notifications} from "@app/notifications"
|
import {notifications} from "@app/util/notifications"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
|
||||||
const {url, path, event}: {url: string; path: string; event: TrustedEvent} = $props()
|
const {url, path, event}: {url: string; path: string; event: TrustedEvent} = $props()
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import Confirm from "@lib/components/Confirm.svelte"
|
import Confirm from "@lib/components/Confirm.svelte"
|
||||||
import {publishDelete} from "@app/commands"
|
import {publishDelete, canEnforceNip70} from "@app/core/commands"
|
||||||
import {clearModals} from "@app/modal"
|
import {clearModals} from "@app/util/modal"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
@@ -11,8 +11,10 @@
|
|||||||
|
|
||||||
const {url, event}: Props = $props()
|
const {url, event}: Props = $props()
|
||||||
|
|
||||||
|
const shouldProtect = canEnforceNip70(url)
|
||||||
|
|
||||||
const confirm = async () => {
|
const confirm = async () => {
|
||||||
await publishDelete({event, relays: [url]})
|
await publishDelete({event, relays: [url], protect: await shouldProtect})
|
||||||
|
|
||||||
clearModals()
|
clearModals()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as nip19 from "nostr-tools/nip19"
|
import * as nip19 from "nostr-tools/nip19"
|
||||||
import {Router} from "@welshman/router"
|
import {Router} from "@welshman/router"
|
||||||
|
import {LOCALE, secondsToDate} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import {displayRelayUrl} from "@welshman/util"
|
||||||
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 ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import {clip} from "@app/toast"
|
import {trackerStore} from "@app/core/state"
|
||||||
|
import {clip} from "@app/util/toast"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url?: string
|
url?: string
|
||||||
@@ -17,11 +20,17 @@
|
|||||||
|
|
||||||
const relays = url ? [url] : Router.get().Event(event).getUrls()
|
const relays = url ? [url] : Router.get().Event(event).getUrls()
|
||||||
const nevent1 = nip19.neventEncode({...event, relays})
|
const nevent1 = nip19.neventEncode({...event, relays})
|
||||||
|
const seenOn = $trackerStore.getRelays(event.id)
|
||||||
const npub1 = nip19.npubEncode(event.pubkey)
|
const npub1 = nip19.npubEncode(event.pubkey)
|
||||||
const json = JSON.stringify(event, null, 2)
|
const json = JSON.stringify(event, null, 2)
|
||||||
const copyLink = () => clip(nevent1)
|
const copyLink = () => clip(nevent1)
|
||||||
const copyPubkey = () => clip(npub1)
|
const copyPubkey = () => clip(npub1)
|
||||||
const copyJson = () => clip(json)
|
const copyJson = () => clip(json)
|
||||||
|
|
||||||
|
const formatter = new Intl.DateTimeFormat(LOCALE, {
|
||||||
|
dateStyle: "long",
|
||||||
|
timeStyle: "long",
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="column gap-4">
|
<div class="column gap-4">
|
||||||
@@ -33,6 +42,14 @@
|
|||||||
<div>The full details of this event are shown below.</div>
|
<div>The full details of this event are shown below.</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
<FieldInline>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Created At</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<p>{formatter.format(secondsToDate(event.created_at))}</p>
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
<FieldInline>
|
<FieldInline>
|
||||||
{#snippet label()}
|
{#snippet label()}
|
||||||
<p>Event Link</p>
|
<p>Event Link</p>
|
||||||
@@ -61,6 +78,22 @@
|
|||||||
</label>
|
</label>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</FieldInline>
|
</FieldInline>
|
||||||
|
{#if !url && seenOn.size > 0}
|
||||||
|
<FieldInline>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Seen On</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
{#each seenOn as url, i (url)}
|
||||||
|
<span class="bg-alt badge flex gap-1">
|
||||||
|
{displayRelayUrl(url)}
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
|
{/if}
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<pre class="card2 card2-sm bg-alt overflow-auto text-xs"><code>{json}</code></pre>
|
<pre class="card2 card2-sm bg-alt overflow-auto text-xs"><code>{json}</code></pre>
|
||||||
<p class="absolute right-2 top-2 flex flex-grow items-center justify-between">
|
<p class="absolute right-2 top-2 flex flex-grow items-center justify-between">
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
import EventReport from "@app/components/EventReport.svelte"
|
import EventReport from "@app/components/EventReport.svelte"
|
||||||
import EventShare from "@app/components/EventShare.svelte"
|
import EventShare from "@app/components/EventShare.svelte"
|
||||||
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
|
|||||||
@@ -7,13 +7,15 @@
|
|||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import EditorContent from "@app/editor/EditorContent.svelte"
|
import EditorContent from "@app/editor/EditorContent.svelte"
|
||||||
import {publishComment} from "@app/commands"
|
import {publishComment, canEnforceNip70} from "@app/core/commands"
|
||||||
import {tagRoom, GENERAL, PROTECTED} from "@app/state"
|
import {PROTECTED} from "@app/core/state"
|
||||||
import {makeEditor} from "@app/editor"
|
import {makeEditor} from "@app/editor"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
const {url, event, onClose, onSubmit} = $props()
|
const {url, event, onClose, onSubmit} = $props()
|
||||||
|
|
||||||
|
const shouldProtect = canEnforceNip70(url)
|
||||||
|
|
||||||
const uploading = writable(false)
|
const uploading = writable(false)
|
||||||
|
|
||||||
const selectFiles = () => editor.then(ed => ed.commands.selectFiles())
|
const selectFiles = () => editor.then(ed => ed.commands.selectFiles())
|
||||||
@@ -23,7 +25,11 @@
|
|||||||
|
|
||||||
const ed = await editor
|
const ed = await editor
|
||||||
const content = ed.getText({blockSeparator: "\n"}).trim()
|
const content = ed.getText({blockSeparator: "\n"}).trim()
|
||||||
const tags = [...ed.storage.nostr.getEditorTags(), tagRoom(GENERAL, url), PROTECTED]
|
const tags = ed.storage.nostr.getEditorTags()
|
||||||
|
|
||||||
|
if (await shouldProtect) {
|
||||||
|
tags.push(PROTECTED)
|
||||||
|
}
|
||||||
|
|
||||||
if (!content) {
|
if (!content) {
|
||||||
return pushToast({
|
return pushToast({
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {publishReport} from "@app/commands"
|
import {publishReport} from "@app/core/commands"
|
||||||
|
|
||||||
const {url, event} = $props()
|
const {url, event} = $props()
|
||||||
|
|
||||||
|
|||||||
@@ -6,18 +6,20 @@
|
|||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Profile from "@app/components/Profile.svelte"
|
import Profile from "@app/components/Profile.svelte"
|
||||||
import {publishDelete} from "@app/commands"
|
import {publishDelete, canEnforceNip70} from "@app/core/commands"
|
||||||
|
|
||||||
const {url, event} = $props()
|
const {url, event} = $props()
|
||||||
|
|
||||||
|
const shouldProtect = canEnforceNip70(url)
|
||||||
|
|
||||||
const reports = deriveEvents(repository, {
|
const reports = deriveEvents(repository, {
|
||||||
filters: [{kinds: [REPORT], "#e": [event.id]}],
|
filters: [{kinds: [REPORT], "#e": [event.id]}],
|
||||||
})
|
})
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const deleteReport = (report: TrustedEvent) => {
|
const deleteReport = async (report: TrustedEvent) => {
|
||||||
publishDelete({event: report, relays: [url]})
|
publishDelete({event: report, relays: [url], protect: await shouldProtect})
|
||||||
|
|
||||||
if ($reports.length === 0) {
|
if ($reports.length === 0) {
|
||||||
history.back()
|
history.back()
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import ChannelName from "@app/components/ChannelName.svelte"
|
import ChannelName from "@app/components/ChannelName.svelte"
|
||||||
import {channelsByUrl} from "@app/state"
|
import {channelsByUrl} from "@app/core/state"
|
||||||
import {makeRoomPath} from "@app/routes"
|
import {makeRoomPath} from "@app/util/routes"
|
||||||
import {setKey} from "@app/implicit"
|
import {setKey} from "@lib/implicit"
|
||||||
|
|
||||||
const {url, noun, event}: {url: string; noun: string; event: TrustedEvent} = $props()
|
const {url, noun, event}: {url: string; noun: string; event: TrustedEvent} = $props()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||||
|
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||||
|
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
|
||||||
|
import EventActivity from "@app/components/EventActivity.svelte"
|
||||||
|
import EventActions from "@app/components/EventActions.svelte"
|
||||||
|
import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands"
|
||||||
|
import {makeGoalPath} from "@app/util/routes"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
url: any
|
||||||
|
event: any
|
||||||
|
showActivity?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, event, showActivity = false}: Props = $props()
|
||||||
|
|
||||||
|
const shouldProtect = canEnforceNip70(url)
|
||||||
|
|
||||||
|
const path = makeGoalPath(url, event.id)
|
||||||
|
|
||||||
|
const deleteReaction = async (event: TrustedEvent) =>
|
||||||
|
publishDelete({relays: [url], event, protect: await shouldProtect})
|
||||||
|
|
||||||
|
const createReaction = async (template: EventContent) =>
|
||||||
|
publishReaction({...template, event, relays: [url], protect: await shouldProtect})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
||||||
|
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
|
||||||
|
<ThunkStatusOrDeleted {event} />
|
||||||
|
{#if showActivity}
|
||||||
|
<EventActivity {url} {path} {event} />
|
||||||
|
{/if}
|
||||||
|
<EventActions {url} {event} hideZap noun="Goal" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {writable} from "svelte/store"
|
||||||
|
import {makeEvent, ZAP_GOAL} from "@welshman/util"
|
||||||
|
import {publishThunk} from "@welshman/app"
|
||||||
|
import {isMobile, preventDefault} from "@lib/html"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Field from "@lib/components/Field.svelte"
|
||||||
|
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import EditorContent from "@app/editor/EditorContent.svelte"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
import {PROTECTED} from "@app/core/state"
|
||||||
|
import {makeEditor} from "@app/editor"
|
||||||
|
import {canEnforceNip70} from "@app/core/commands"
|
||||||
|
|
||||||
|
const {url} = $props()
|
||||||
|
|
||||||
|
const shouldProtect = canEnforceNip70(url)
|
||||||
|
|
||||||
|
const uploading = writable(false)
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const selectFiles = () => editor.then(ed => ed.commands.selectFiles())
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
if ($uploading) return
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Please provide a title for your funding goal.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const ed = await editor
|
||||||
|
const summary = ed.getText({blockSeparator: "\n"}).trim()
|
||||||
|
|
||||||
|
if (!summary.trim()) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Please provide details about your funding goal.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const tags = [
|
||||||
|
...ed.storage.nostr.getEditorTags(),
|
||||||
|
["summary", summary],
|
||||||
|
["amount", String(amount)],
|
||||||
|
["relays", url],
|
||||||
|
]
|
||||||
|
|
||||||
|
if (await shouldProtect) {
|
||||||
|
tags.push(PROTECTED)
|
||||||
|
}
|
||||||
|
|
||||||
|
publishThunk({
|
||||||
|
relays: [url],
|
||||||
|
event: makeEvent(ZAP_GOAL, {content, tags}),
|
||||||
|
})
|
||||||
|
|
||||||
|
history.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
const editor = makeEditor({url, submit, uploading, placeholder: "What's on your mind?"})
|
||||||
|
|
||||||
|
let content = $state("")
|
||||||
|
let amount = $state(1000)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form class="column gap-4" onsubmit={preventDefault(submit)}>
|
||||||
|
<ModalHeader>
|
||||||
|
{#snippet title()}
|
||||||
|
<div>Create a Funding Goal</div>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
<div>Request contributions for your fundraiser.</div>
|
||||||
|
{/snippet}
|
||||||
|
</ModalHeader>
|
||||||
|
<div class="col-8 relative">
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Title*</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
|
<!-- svelte-ignore a11y_autofocus -->
|
||||||
|
<input
|
||||||
|
autofocus={!isMobile}
|
||||||
|
bind:value={content}
|
||||||
|
class="grow"
|
||||||
|
type="text"
|
||||||
|
placeholder="What do funds go towards?" />
|
||||||
|
</label>
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
<div class="relative">
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Details*</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<div class="note-editor flex-grow overflow-hidden">
|
||||||
|
<EditorContent {editor} />
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
<Button
|
||||||
|
data-tip="Add an image"
|
||||||
|
class="tooltip tooltip-left absolute bottom-1 right-2"
|
||||||
|
onclick={selectFiles}>
|
||||||
|
{#if $uploading}
|
||||||
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
|
{:else}
|
||||||
|
<Icon icon="paperclip" size={3} />
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<FieldInline>
|
||||||
|
{#snippet label()}
|
||||||
|
Goal Amount (sats)*
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<div class="flex flex-grow justify-end">
|
||||||
|
<label class="input input-bordered flex items-center gap-2">
|
||||||
|
<Icon icon="bolt" />
|
||||||
|
<input bind:value={amount} type="number" class="w-28" />
|
||||||
|
<p class="opacity-50">sats</p>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
|
<input
|
||||||
|
class="range range-primary -mt-2"
|
||||||
|
type="range"
|
||||||
|
min="1000"
|
||||||
|
max="100000"
|
||||||
|
step="1000"
|
||||||
|
bind:value={amount} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
<Icon icon="alt-arrow-left" />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" class="btn btn-primary">Create Goal</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import {getTagValue} from "@welshman/util"
|
||||||
|
import Link from "@lib/components/Link.svelte"
|
||||||
|
import Content from "@app/components/Content.svelte"
|
||||||
|
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||||
|
import GoalActions from "@app/components/GoalActions.svelte"
|
||||||
|
import GoalSummary from "@app/components/GoalSummary.svelte"
|
||||||
|
import {makeGoalPath} from "@app/util/routes"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
event: TrustedEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, event}: Props = $props()
|
||||||
|
|
||||||
|
const summary = getTagValue("summary", event.tags)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Link class="col-2 card2 bg-alt w-full cursor-pointer" href={makeGoalPath(url, event.id)}>
|
||||||
|
<p class="text-2xl">{event.content}</p>
|
||||||
|
<Content
|
||||||
|
event={{content: summary, tags: event.tags}}
|
||||||
|
{url}
|
||||||
|
expandMode="inline"
|
||||||
|
minLength={50}
|
||||||
|
maxLength={300} />
|
||||||
|
<GoalSummary {url} {event} />
|
||||||
|
<div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row">
|
||||||
|
<span class="whitespace-nowrap py-1 text-sm opacity-75">
|
||||||
|
Posted by <ProfileLink pubkey={event.pubkey} {url} />
|
||||||
|
</span>
|
||||||
|
<GoalActions showActivity {url} {event} />
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {now, DAY, uniq, sum} from "@welshman/lib"
|
||||||
|
import type {Zap, TrustedEvent} from "@welshman/util"
|
||||||
|
import {getTagValue, fromMsats, ZAP_RESPONSE} from "@welshman/util"
|
||||||
|
import {deriveEventsMapped} from "@welshman/store"
|
||||||
|
import {repository, getValidZap} from "@welshman/app"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import ZapButton from "@app/components/ZapButton.svelte"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
event: TrustedEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, event}: Props = $props()
|
||||||
|
|
||||||
|
const zaps = deriveEventsMapped<Zap>(repository, {
|
||||||
|
filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}],
|
||||||
|
itemToEvent: item => item.response,
|
||||||
|
eventToItem: (response: TrustedEvent) => getValidZap(response, event),
|
||||||
|
})
|
||||||
|
|
||||||
|
const goalAmount = parseInt(getTagValue("amount", event.tags) || "0")
|
||||||
|
const zapAmount = $derived(fromMsats(sum($zaps.map(zap => zap.invoiceAmount))))
|
||||||
|
const contributorsCount = $derived(uniq($zaps.map(zap => zap.request.pubkey)).length)
|
||||||
|
const daysOld = Math.ceil((now() - event.created_at) / DAY)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="card2 bg-alt flex flex-col gap-8">
|
||||||
|
<div class="flex gap-8">
|
||||||
|
<div>
|
||||||
|
<p class="text-xl text-primary">{zapAmount} sats</p>
|
||||||
|
<p class="text-sm opacity-75">funded of {goalAmount} sats</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-xl">{contributorsCount}</p>
|
||||||
|
<p class="text-sm opacity-75">{contributorsCount === 1 ? "contributor" : "contributors"}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-xl">{daysOld}</p>
|
||||||
|
<p class="text-sm opacity-75">{daysOld === 1 ? "day" : "days"} old</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<progress class="progress progress-primary" value={zapAmount} max={goalAmount}></progress>
|
||||||
|
<ZapButton {url} {event} class="btn btn-primary lg:m-auto lg:px-20">
|
||||||
|
<Icon icon="bolt" />
|
||||||
|
Contribute to this goal
|
||||||
|
</ZapButton>
|
||||||
|
</div>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.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"
|
||||||
import {PLATFORM_NAME} from "@app/state"
|
import {PLATFORM_NAME} from "@app/core/state"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="column gap-4">
|
<div class="column gap-4">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import {PLATFORM_NAME} from "@app/state"
|
import {PLATFORM_NAME} from "@app/core/state"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="column gap-4">
|
<div class="column gap-4">
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import ProfileEject from "@app/components/ProfileEject.svelte"
|
import ProfileEject from "@app/components/ProfileEject.svelte"
|
||||||
import {PLATFORM_NAME} from "@app/state"
|
import {PLATFORM_NAME} from "@app/core/state"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import {PLATFORM_NAME} from "@app/state"
|
import {PLATFORM_NAME} from "@app/core/state"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="column gap-4">
|
<div class="column gap-4">
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {deriveZapperForPubkey} from "@welshman/app"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||||
|
|
||||||
|
const {pubkey} = $props()
|
||||||
|
|
||||||
|
const zapper = deriveZapperForPubkey(pubkey)
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="column gap-4">
|
||||||
|
<ModalHeader>
|
||||||
|
{#snippet title()}
|
||||||
|
<div>Unable to Zap</div>
|
||||||
|
{/snippet}
|
||||||
|
</ModalHeader>
|
||||||
|
<p>
|
||||||
|
Zapping <ProfileLink {pubkey} class="!text-primary" /> isn't possible right now because
|
||||||
|
{#if $zapper}
|
||||||
|
their zap receiver isn't correctly set up.
|
||||||
|
{:else}
|
||||||
|
they don't currently have a zap receiver set up.
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
<Icon icon="alt-arrow-left" />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</div>
|
||||||
@@ -6,8 +6,8 @@
|
|||||||
import CardButton from "@lib/components/CardButton.svelte"
|
import CardButton from "@lib/components/CardButton.svelte"
|
||||||
import LogIn from "@app/components/LogIn.svelte"
|
import LogIn from "@app/components/LogIn.svelte"
|
||||||
import SignUp from "@app/components/SignUp.svelte"
|
import SignUp from "@app/components/SignUp.svelte"
|
||||||
import {PLATFORM_TERMS, PLATFORM_PRIVACY, PLATFORM_NAME} from "@app/state"
|
import {PLATFORM_TERMS, PLATFORM_PRIVACY, PLATFORM_NAME} from "@app/core/state"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
const logIn = () => pushModal(LogIn)
|
const logIn = () => pushModal(LogIn)
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,11 @@
|
|||||||
import InfoNostr from "@app/components/InfoNostr.svelte"
|
import InfoNostr from "@app/components/InfoNostr.svelte"
|
||||||
import LogInBunker from "@app/components/LogInBunker.svelte"
|
import LogInBunker from "@app/components/LogInBunker.svelte"
|
||||||
import LogInPassword from "@app/components/LogInPassword.svelte"
|
import LogInPassword from "@app/components/LogInPassword.svelte"
|
||||||
import {pushModal, clearModals} from "@app/modal"
|
import {pushModal, clearModals} from "@app/util/modal"
|
||||||
import {PLATFORM_NAME, BURROW_URL} from "@app/state"
|
import {PLATFORM_NAME, BURROW_URL} from "@app/core/state"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {loadUserData} from "@app/requests"
|
import {loadUserData} from "@app/core/requests"
|
||||||
import {setChecked} from "@app/notifications"
|
import {setChecked} from "@app/util/notifications"
|
||||||
|
|
||||||
let signers: any[] = $state([])
|
let signers: any[] = $state([])
|
||||||
let loading: string | undefined = $state()
|
let loading: string | undefined = $state()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {onMount, onDestroy} from "svelte"
|
||||||
import type {Nip46ResponseWithResult} from "@welshman/signer"
|
import type {Nip46ResponseWithResult} from "@welshman/signer"
|
||||||
import {Nip46Broker, makeSecret} from "@welshman/signer"
|
import {Nip46Broker, makeSecret} from "@welshman/signer"
|
||||||
import {loginWithNip01, loginWithNip46} from "@welshman/app"
|
import {loginWithNip01, loginWithNip46} from "@welshman/app"
|
||||||
@@ -8,17 +9,24 @@
|
|||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import BunkerConnect, {BunkerConnectController} from "@app/components/BunkerConnect.svelte"
|
import BunkerConnect from "@app/components/BunkerConnect.svelte"
|
||||||
import BunkerUrl from "@app/components/BunkerUrl.svelte"
|
import BunkerUrl from "@app/components/BunkerUrl.svelte"
|
||||||
import {loadUserData} from "@app/requests"
|
import {Nip46Controller} from "@app/util/nip46"
|
||||||
import {clearModals} from "@app/modal"
|
import {loadUserData} from "@app/core/requests"
|
||||||
import {setChecked} from "@app/notifications"
|
import {clearModals} from "@app/util/modal"
|
||||||
import {pushToast} from "@app/toast"
|
import {setChecked} from "@app/util/notifications"
|
||||||
import {SIGNER_RELAYS, NIP46_PERMS} from "@app/state"
|
import {pushToast} from "@app/util/toast"
|
||||||
|
import {SIGNER_RELAYS, NIP46_PERMS} from "@app/core/state"
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => {
|
||||||
|
if (mode === "connect") {
|
||||||
|
selectBunker()
|
||||||
|
} else {
|
||||||
|
history.back()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const controller = new BunkerConnectController({
|
const controller = new Nip46Controller({
|
||||||
onNostrConnect: async (response: Nip46ResponseWithResult) => {
|
onNostrConnect: async (response: Nip46ResponseWithResult) => {
|
||||||
const pubkey = await controller.broker.getPublicKey()
|
const pubkey = await controller.broker.getPublicKey()
|
||||||
|
|
||||||
@@ -30,21 +38,23 @@
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const {loading, bunker} = controller
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
if (controller.loading) return
|
if ($loading) return
|
||||||
|
|
||||||
const {signerPubkey, connectSecret, relays} = Nip46Broker.parseBunkerUrl(controller.bunker)
|
|
||||||
|
|
||||||
if (!signerPubkey || relays.length === 0) {
|
|
||||||
return pushToast({
|
|
||||||
theme: "error",
|
|
||||||
message: "Sorry, it looks like that's an invalid bunker link.",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
controller.loading = true
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const {signerPubkey, connectSecret, relays} = Nip46Broker.parseBunkerUrl($bunker)
|
||||||
|
|
||||||
|
if (!signerPubkey || relays.length === 0) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Sorry, it looks like that's an invalid bunker link.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.loading.set(true)
|
||||||
|
|
||||||
const {clientSecret} = controller
|
const {clientSecret} = controller
|
||||||
const broker = new Nip46Broker({relays, clientSecret, signerPubkey})
|
const broker = new Nip46Broker({relays, clientSecret, signerPubkey})
|
||||||
const result = await broker.connect(connectSecret, NIP46_PERMS)
|
const result = await broker.connect(connectSecret, NIP46_PERMS)
|
||||||
@@ -64,43 +74,73 @@
|
|||||||
message: "Something went wrong, please try again!",
|
message: "Something went wrong, please try again!",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Something went wrong, please try again!",
|
||||||
|
})
|
||||||
} finally {
|
} finally {
|
||||||
controller.loading = false
|
controller.loading.set(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
clearModals()
|
clearModals()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const selectConnect = () => {
|
||||||
|
mode = "connect"
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectBunker = () => {
|
||||||
|
mode = "bunker"
|
||||||
|
}
|
||||||
|
|
||||||
|
let mode: string = $state("bunker")
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
// For testing and for play store reviewers
|
// For testing and for play store reviewers
|
||||||
if (controller.bunker === "reviewkey") {
|
if ($bunker === "reviewkey") {
|
||||||
loginWithNip01(makeSecret())
|
loginWithNip01(makeSecret())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
controller.start()
|
||||||
|
})
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
controller.stop()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="column gap-4" onsubmit={preventDefault(onSubmit)}>
|
<form class="column gap-4" onsubmit={preventDefault(onSubmit)}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
{#snippet title()}
|
{#snippet title()}
|
||||||
<div>Log In</div>
|
<div>Log In with a Signer</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet info()}
|
{#snippet info()}
|
||||||
<div>Connect your signer by scanning the QR code below or pasting a bunker link.</div>
|
<div>Using a remote signer app helps you keep your keys safe.</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<BunkerConnect {controller} />
|
<div class:hidden={mode !== "bunker"}></div>
|
||||||
<BunkerUrl loading={controller.loading} bind:bunker={controller.bunker} />
|
{#if mode === "connect"}
|
||||||
|
<BunkerConnect {controller} />
|
||||||
|
{:else}
|
||||||
|
<BunkerUrl {controller} />
|
||||||
|
<Button class="btn {$bunker ? 'btn-neutral' : 'btn-primary'}" onclick={selectConnect}
|
||||||
|
>Log in with a QR code instead</Button>
|
||||||
|
{/if}
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button class="btn btn-link" onclick={back} disabled={controller.loading}>
|
<Button class="btn btn-link" onclick={back} disabled={$loading}>
|
||||||
<Icon icon="alt-arrow-left" />
|
<Icon icon="alt-arrow-left" />
|
||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
{#if mode === "bunker"}
|
||||||
type="submit"
|
<Button type="submit" class="btn btn-primary" disabled={$loading || !$bunker}>
|
||||||
class="btn btn-primary"
|
<Spinner loading={$loading}>Next</Spinner>
|
||||||
disabled={controller.loading || !controller.bunker}>
|
<Icon icon="alt-arrow-right" />
|
||||||
<Spinner loading={controller.loading}>Next</Spinner>
|
</Button>
|
||||||
<Icon icon="alt-arrow-right" />
|
{/if}
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -12,11 +12,17 @@
|
|||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import PasswordResetRequest from "@app/components/PasswordResetRequest.svelte"
|
import PasswordResetRequest from "@app/components/PasswordResetRequest.svelte"
|
||||||
import {loadUserData} from "@app/requests"
|
import {loadUserData} from "@app/core/requests"
|
||||||
import {clearModals, pushModal} from "@app/modal"
|
import {clearModals, pushModal} from "@app/util/modal"
|
||||||
import {setChecked} from "@app/notifications"
|
import {setChecked} from "@app/util/notifications"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {NIP46_PERMS, BURROW_URL, PLATFORM_URL, PLATFORM_NAME, PLATFORM_LOGO} from "@app/state"
|
import {
|
||||||
|
NIP46_PERMS,
|
||||||
|
BURROW_URL,
|
||||||
|
PLATFORM_URL,
|
||||||
|
PLATFORM_NAME,
|
||||||
|
PLATFORM_LOGO,
|
||||||
|
} from "@app/core/state"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
email?: string
|
email?: string
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import {logout} from "@app/commands"
|
import {logout} from "@app/core/commands"
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import CardButton from "@lib/components/CardButton.svelte"
|
import CardButton from "@lib/components/CardButton.svelte"
|
||||||
import LogOut from "@app/components/LogOut.svelte"
|
import LogOut from "@app/components/LogOut.svelte"
|
||||||
import {PLATFORM_NAME} from "@app/state"
|
import {PLATFORM_NAME} from "@app/core/state"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
const logout = () => pushModal(LogOut)
|
const logout = () => pushModal(LogOut)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {displayRelayUrl} from "@welshman/util"
|
import {displayRelayUrl, getTagValue} from "@welshman/util"
|
||||||
|
import {deriveRelay} from "@welshman/app"
|
||||||
import {fly} from "@lib/transition"
|
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"
|
||||||
@@ -12,25 +13,34 @@
|
|||||||
import SpaceExit from "@app/components/SpaceExit.svelte"
|
import SpaceExit from "@app/components/SpaceExit.svelte"
|
||||||
import SpaceJoin from "@app/components/SpaceJoin.svelte"
|
import SpaceJoin from "@app/components/SpaceJoin.svelte"
|
||||||
import ProfileList from "@app/components/ProfileList.svelte"
|
import ProfileList from "@app/components/ProfileList.svelte"
|
||||||
|
import AlertAdd from "@app/components/AlertAdd.svelte"
|
||||||
|
import Alerts from "@app/components/Alerts.svelte"
|
||||||
import RoomCreate from "@app/components/RoomCreate.svelte"
|
import RoomCreate from "@app/components/RoomCreate.svelte"
|
||||||
import MenuSpaceRoomItem from "@app/components/MenuSpaceRoomItem.svelte"
|
import MenuSpaceRoomItem from "@app/components/MenuSpaceRoomItem.svelte"
|
||||||
import {
|
import {
|
||||||
|
ENABLE_ZAPS,
|
||||||
userRoomsByUrl,
|
userRoomsByUrl,
|
||||||
hasMembershipUrl,
|
hasMembershipUrl,
|
||||||
memberships,
|
memberships,
|
||||||
deriveUserRooms,
|
deriveUserRooms,
|
||||||
deriveOtherRooms,
|
deriveOtherRooms,
|
||||||
} from "@app/state"
|
hasNip29,
|
||||||
import {notifications} from "@app/notifications"
|
alerts,
|
||||||
import {pushModal} from "@app/modal"
|
} from "@app/core/state"
|
||||||
import {makeSpacePath} from "@app/routes"
|
import {notifications} from "@app/util/notifications"
|
||||||
|
import {pushModal} from "@app/util/modal"
|
||||||
|
import {makeSpacePath} from "@app/util/routes"
|
||||||
|
|
||||||
const {url} = $props()
|
const {url} = $props()
|
||||||
|
|
||||||
|
const relay = deriveRelay(url)
|
||||||
|
const chatPath = makeSpacePath(url, "chat")
|
||||||
|
const goalsPath = makeSpacePath(url, "goals")
|
||||||
const threadsPath = makeSpacePath(url, "threads")
|
const threadsPath = makeSpacePath(url, "threads")
|
||||||
const calendarPath = makeSpacePath(url, "calendar")
|
const calendarPath = makeSpacePath(url, "calendar")
|
||||||
const userRooms = deriveUserRooms(url)
|
const userRooms = deriveUserRooms(url)
|
||||||
const otherRooms = deriveOtherRooms(url)
|
const otherRooms = deriveOtherRooms(url)
|
||||||
|
const hasAlerts = $derived($alerts.some(a => getTagValue("feed", a.tags)?.includes(url)))
|
||||||
|
|
||||||
const openMenu = () => {
|
const openMenu = () => {
|
||||||
showMenu = true
|
showMenu = true
|
||||||
@@ -55,6 +65,13 @@
|
|||||||
|
|
||||||
const addRoom = () => pushModal(RoomCreate, {url}, {replaceState})
|
const addRoom = () => pushModal(RoomCreate, {url}, {replaceState})
|
||||||
|
|
||||||
|
const manageAlerts = () => {
|
||||||
|
const component = hasAlerts ? Alerts : AlertAdd
|
||||||
|
const params = {url, channel: "push", hideSpaceField: true}
|
||||||
|
|
||||||
|
pushModal(component, params, {replaceState})
|
||||||
|
}
|
||||||
|
|
||||||
let showMenu = $state(false)
|
let showMenu = $state(false)
|
||||||
let replaceState = $state(false)
|
let replaceState = $state(false)
|
||||||
let element: Element | undefined = $state()
|
let element: Element | undefined = $state()
|
||||||
@@ -68,18 +85,20 @@
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={element}>
|
<div bind:this={element} class="flex h-full flex-col justify-between">
|
||||||
<SecondaryNavSection class="max-h-screen">
|
<SecondaryNavSection>
|
||||||
<div>
|
<div>
|
||||||
<SecondaryNavItem class="w-full !justify-between" onclick={openMenu}>
|
<SecondaryNavItem class="w-full !justify-between" onclick={openMenu}>
|
||||||
<strong class="ellipsize">{displayRelayUrl(url)}</strong>
|
<strong class="ellipsize flex items-center gap-3">
|
||||||
|
{displayRelayUrl(url)}
|
||||||
|
</strong>
|
||||||
<Icon icon="alt-arrow-down" />
|
<Icon icon="alt-arrow-down" />
|
||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
{#if showMenu}
|
{#if showMenu}
|
||||||
<Popover hideOnClick onClose={toggleMenu}>
|
<Popover hideOnClick onClose={toggleMenu}>
|
||||||
<ul
|
<ul
|
||||||
transition:fly
|
transition:fly
|
||||||
class="menu absolute z-popover mt-2 w-full rounded-box bg-base-100 p-2 shadow-xl">
|
class="menu absolute z-popover mt-2 w-full gap-1 rounded-box bg-base-100 p-2 shadow-xl">
|
||||||
<li>
|
<li>
|
||||||
<Button onclick={showMembers}>
|
<Button onclick={showMembers}>
|
||||||
<Icon icon="user-rounded" />
|
<Icon icon="user-rounded" />
|
||||||
@@ -109,10 +128,18 @@
|
|||||||
</Popover>
|
</Popover>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex min-h-0 flex-col gap-1 overflow-auto">
|
<div class="flex max-h-[calc(100vh-150px)] min-h-0 flex-col gap-1 overflow-auto">
|
||||||
<SecondaryNavItem {replaceState} href={makeSpacePath(url)}>
|
<SecondaryNavItem {replaceState} href={makeSpacePath(url)}>
|
||||||
<Icon icon="home-smile" /> Home
|
<Icon icon="home-smile" /> Home
|
||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
|
{#if ENABLE_ZAPS}
|
||||||
|
<SecondaryNavItem
|
||||||
|
{replaceState}
|
||||||
|
href={goalsPath}
|
||||||
|
notification={$notifications.has(goalsPath)}>
|
||||||
|
<Icon icon="star-fall-minimalistic-2" /> Goals
|
||||||
|
</SecondaryNavItem>
|
||||||
|
{/if}
|
||||||
<SecondaryNavItem
|
<SecondaryNavItem
|
||||||
{replaceState}
|
{replaceState}
|
||||||
href={threadsPath}
|
href={threadsPath}
|
||||||
@@ -125,28 +152,45 @@
|
|||||||
notification={$notifications.has(calendarPath)}>
|
notification={$notifications.has(calendarPath)}>
|
||||||
<Icon icon="calendar-minimalistic" /> Calendar
|
<Icon icon="calendar-minimalistic" /> Calendar
|
||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
<div class="h-2"></div>
|
{#if hasNip29($relay)}
|
||||||
<SecondaryNavHeader>Your Rooms</SecondaryNavHeader>
|
{#if $userRooms.length > 0}
|
||||||
{#each $userRooms as room, i (room)}
|
<div class="h-2"></div>
|
||||||
<MenuSpaceRoomItem {replaceState} notify {url} {room} />
|
<SecondaryNavHeader>Your Rooms</SecondaryNavHeader>
|
||||||
{/each}
|
{/if}
|
||||||
{#if $otherRooms.length > 0}
|
{#each $userRooms as room, i (room)}
|
||||||
<div class="h-2"></div>
|
<MenuSpaceRoomItem {replaceState} notify {url} {room} />
|
||||||
<SecondaryNavHeader>
|
{/each}
|
||||||
{#if $userRooms.length > 0}
|
{#if $otherRooms.length > 0}
|
||||||
Other Rooms
|
<div class="h-2"></div>
|
||||||
{:else}
|
<SecondaryNavHeader>
|
||||||
Rooms
|
{#if $userRooms.length > 0}
|
||||||
{/if}
|
Other Rooms
|
||||||
</SecondaryNavHeader>
|
{:else}
|
||||||
|
Rooms
|
||||||
|
{/if}
|
||||||
|
</SecondaryNavHeader>
|
||||||
|
{/if}
|
||||||
|
{#each $otherRooms as room, i (room)}
|
||||||
|
<MenuSpaceRoomItem {replaceState} {url} {room} />
|
||||||
|
{/each}
|
||||||
|
<SecondaryNavItem {replaceState} onclick={addRoom}>
|
||||||
|
<Icon icon="add-circle" />
|
||||||
|
Create room
|
||||||
|
</SecondaryNavItem>
|
||||||
|
{:else}
|
||||||
|
<SecondaryNavItem
|
||||||
|
{replaceState}
|
||||||
|
href={chatPath}
|
||||||
|
notification={$notifications.has(chatPath)}>
|
||||||
|
<Icon icon="chat-round" /> Chat
|
||||||
|
</SecondaryNavItem>
|
||||||
{/if}
|
{/if}
|
||||||
{#each $otherRooms as room, i (room)}
|
|
||||||
<MenuSpaceRoomItem {replaceState} {url} {room} />
|
|
||||||
{/each}
|
|
||||||
<SecondaryNavItem {replaceState} onclick={addRoom}>
|
|
||||||
<Icon icon="add-circle" />
|
|
||||||
Create room
|
|
||||||
</SecondaryNavItem>
|
|
||||||
</div>
|
</div>
|
||||||
</SecondaryNavSection>
|
</SecondaryNavSection>
|
||||||
|
<div class="p-4">
|
||||||
|
<button class="btn btn-neutral btn-sm w-full" onclick={manageAlerts}>
|
||||||
|
<Icon icon="bell" />
|
||||||
|
Manage Alerts
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
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 MenuSpace from "@app/components/MenuSpace.svelte"
|
import MenuSpace from "@app/components/MenuSpace.svelte"
|
||||||
import {notifications} from "@app/notifications"
|
import {notifications} from "@app/util/notifications"
|
||||||
import {makeSpacePath} from "@app/routes"
|
import {makeSpacePath} from "@app/util/routes"
|
||||||
import {pushDrawer} from "@app/modal"
|
import {pushDrawer} from "@app/util/modal"
|
||||||
|
|
||||||
const {url} = $props()
|
const {url} = $props()
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
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 ChannelName from "@app/components/ChannelName.svelte"
|
import ChannelName from "@app/components/ChannelName.svelte"
|
||||||
import {makeRoomPath} from "@app/routes"
|
import {makeRoomPath} from "@app/util/routes"
|
||||||
import {deriveChannel, channelIsLocked} from "@app/state"
|
import {deriveChannel} from "@app/core/state"
|
||||||
import {notifications} from "@app/notifications"
|
import {notifications} from "@app/util/notifications"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
url: any
|
url: any
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
href={path}
|
href={path}
|
||||||
{replaceState}
|
{replaceState}
|
||||||
notification={notify ? $notifications.has(path) : false}>
|
notification={notify ? $notifications.has(path) : false}>
|
||||||
{#if channelIsLocked($channel)}
|
{#if $channel?.closed || $channel?.private}
|
||||||
<Icon icon="lock" size={4} />
|
<Icon icon="lock" size={4} />
|
||||||
{:else}
|
{:else}
|
||||||
<Icon icon="hashtag" />
|
<Icon icon="hashtag" />
|
||||||
|
|||||||
@@ -5,23 +5,22 @@
|
|||||||
import CardButton from "@lib/components/CardButton.svelte"
|
import CardButton from "@lib/components/CardButton.svelte"
|
||||||
import MenuSpacesItem from "@app/components/MenuSpacesItem.svelte"
|
import MenuSpacesItem from "@app/components/MenuSpacesItem.svelte"
|
||||||
import SpaceAdd from "@app/components/SpaceAdd.svelte"
|
import SpaceAdd from "@app/components/SpaceAdd.svelte"
|
||||||
import {userRoomsByUrl, PLATFORM_RELAY} from "@app/state"
|
import {userRoomsByUrl, PLATFORM_RELAYS} from "@app/core/state"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
const addSpace = () => pushModal(SpaceAdd)
|
const addSpace = () => pushModal(SpaceAdd)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="column menu gap-2">
|
<div class="column menu gap-2">
|
||||||
{#if PLATFORM_RELAY}
|
{#each PLATFORM_RELAYS as url (url)}
|
||||||
<MenuSpacesItem url={PLATFORM_RELAY} />
|
<MenuSpacesItem {url} />
|
||||||
<Divider />
|
{:else}
|
||||||
{:else if $userRoomsByUrl.size > 0}
|
{#if $userRoomsByUrl.size > 0}
|
||||||
{#each $userRoomsByUrl.keys() as url (url)}
|
{#each $userRoomsByUrl.keys() as url (url)}
|
||||||
<MenuSpacesItem {url} />
|
<MenuSpacesItem {url} />
|
||||||
{/each}
|
{/each}
|
||||||
<Divider />
|
<Divider />
|
||||||
{/if}
|
{/if}
|
||||||
{#if !PLATFORM_RELAY}
|
|
||||||
<Button onclick={addSpace}>
|
<Button onclick={addSpace}>
|
||||||
<CardButton>
|
<CardButton>
|
||||||
{#snippet icon()}
|
{#snippet icon()}
|
||||||
@@ -35,5 +34,5 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
</CardButton>
|
</CardButton>
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
import SpaceAvatar from "@app/components/SpaceAvatar.svelte"
|
import SpaceAvatar from "@app/components/SpaceAvatar.svelte"
|
||||||
import RelayName from "@app/components/RelayName.svelte"
|
import RelayName from "@app/components/RelayName.svelte"
|
||||||
import RelayDescription from "@app/components/RelayDescription.svelte"
|
import RelayDescription from "@app/components/RelayDescription.svelte"
|
||||||
import {makeSpacePath} from "@app/routes"
|
import {makeSpacePath} from "@app/util/routes"
|
||||||
import {notifications} from "@app/notifications"
|
import {notifications} from "@app/util/notifications"
|
||||||
|
|
||||||
const {url} = $props()
|
const {url} = $props()
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
import Drawer from "@lib/components/Drawer.svelte"
|
import Drawer from "@lib/components/Drawer.svelte"
|
||||||
import Dialog from "@lib/components/Dialog.svelte"
|
import Dialog from "@lib/components/Dialog.svelte"
|
||||||
import {modals, clearModals} from "@app/modal"
|
import {modals, clearModals} from "@app/util/modal"
|
||||||
|
|
||||||
const onKeyDown = (e: any) => {
|
const onKeyDown = (e: any) => {
|
||||||
if (e.code === "Escape" && e.target === document.body) {
|
if (e.code === "Escape" && e.target === document.body) {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Profile from "@app/components/Profile.svelte"
|
import Profile from "@app/components/Profile.svelte"
|
||||||
import ProfileName from "@app/components/ProfileName.svelte"
|
import ProfileName from "@app/components/ProfileName.svelte"
|
||||||
import {entityLink} from "@app/state"
|
import {entityLink} from "@app/core/state"
|
||||||
|
|
||||||
const {
|
const {
|
||||||
event,
|
event,
|
||||||
|
|||||||
@@ -1,34 +1,36 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {NativeEmoji} from "emoji-picker-element/shared"
|
import type {NativeEmoji} from "emoji-picker-element/shared"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||||
import {pubkey} from "@welshman/app"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import EmojiButton from "@lib/components/EmojiButton.svelte"
|
import EmojiButton from "@lib/components/EmojiButton.svelte"
|
||||||
import NoteContent from "@app/components/NoteContent.svelte"
|
import NoteContent from "@app/components/NoteContent.svelte"
|
||||||
import NoteCard from "@app/components/NoteCard.svelte"
|
import NoteCard from "@app/components/NoteCard.svelte"
|
||||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||||
import {publishDelete, publishReaction} from "@app/commands"
|
import {publishDelete, publishReaction, canEnforceNip70} from "@app/core/commands"
|
||||||
|
|
||||||
const {url, event} = $props()
|
const {url, event} = $props()
|
||||||
|
|
||||||
const onReactionClick = (content: string, events: TrustedEvent[]) => {
|
const shouldProtect = canEnforceNip70(url)
|
||||||
const reaction = events.find(e => e.pubkey === $pubkey)
|
|
||||||
|
|
||||||
if (reaction) {
|
const deleteReaction = async (event: TrustedEvent) =>
|
||||||
publishDelete({relays: [url], event: reaction})
|
publishDelete({relays: [url], event, protect: await shouldProtect})
|
||||||
} else {
|
|
||||||
publishReaction({event, content, relays: [url]})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onEmoji = (emoji: NativeEmoji) =>
|
const createReaction = async (template: EventContent) =>
|
||||||
publishReaction({event, content: emoji.unicode, relays: [url]})
|
publishReaction({...template, event, relays: [url], protect: await shouldProtect})
|
||||||
|
|
||||||
|
const onEmoji = async (emoji: NativeEmoji) =>
|
||||||
|
publishReaction({
|
||||||
|
event,
|
||||||
|
content: emoji.unicode,
|
||||||
|
relays: [url],
|
||||||
|
protect: await shouldProtect,
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<NoteCard {event} {url} class="card2 bg-alt">
|
<NoteCard {event} {url} class="card2 bg-alt">
|
||||||
<NoteContent {event} expandMode="inline" />
|
<NoteContent {event} expandMode="inline" />
|
||||||
<div class="flex w-full justify-between gap-2">
|
<div class="flex w-full justify-between gap-2">
|
||||||
<ReactionSummary {url} {event} {onReactionClick} reactionClass="tooltip-right">
|
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-right">
|
||||||
<EmojiButton {onEmoji} class="btn btn-neutral btn-xs h-[26px] rounded-box">
|
<EmojiButton {onEmoji} class="btn btn-neutral btn-xs h-[26px] rounded-box">
|
||||||
<Icon icon="smile-circle" size={4} />
|
<Icon icon="smile-circle" size={4} />
|
||||||
</EmojiButton>
|
</EmojiButton>
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import LogInPassword from "@app/components/LogInPassword.svelte"
|
import LogInPassword from "@app/components/LogInPassword.svelte"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {BURROW_URL} from "@app/state"
|
import {BURROW_URL} from "@app/core/state"
|
||||||
|
|
||||||
const {email, reset_token} = $props()
|
const {email, reset_token} = $props()
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import LogInPassword from "@app/components/LogInPassword.svelte"
|
import LogInPassword from "@app/components/LogInPassword.svelte"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {BURROW_URL} from "@app/state"
|
import {BURROW_URL} from "@app/core/state"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
email: string
|
email: string
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
|
||||||
import {formatTimestampRelative} from "@welshman/lib"
|
|
||||||
import type {Filter} from "@welshman/util"
|
|
||||||
import {deriveEvents} from "@welshman/store"
|
|
||||||
import {load} from "@welshman/net"
|
|
||||||
import {Router} from "@welshman/router"
|
|
||||||
import {repository, loadRelaySelections} from "@welshman/app"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Profile from "@app/components/Profile.svelte"
|
import Profile from "@app/components/Profile.svelte"
|
||||||
import ProfileInfo from "@app/components/ProfileInfo.svelte"
|
import ProfileInfo from "@app/components/ProfileInfo.svelte"
|
||||||
import {makeChatPath} from "@app/routes"
|
import ProfileBadges from "@app/components/ProfileBadges.svelte"
|
||||||
|
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||||
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
pubkey: string
|
pubkey: string
|
||||||
@@ -19,37 +14,21 @@
|
|||||||
|
|
||||||
const {pubkey, url}: Props = $props()
|
const {pubkey, url}: Props = $props()
|
||||||
|
|
||||||
const filters: Filter[] = [{authors: [pubkey], limit: 1}]
|
const openProfile = () => pushModal(ProfileDetail, {pubkey, url})
|
||||||
const events = deriveEvents(repository, {filters})
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
// Make sure we have their relay selections before we load their posts
|
|
||||||
await loadRelaySelections(pubkey)
|
|
||||||
|
|
||||||
// Load at least one note, regardless of time frame
|
|
||||||
load({
|
|
||||||
filters: [{authors: [pubkey], limit: 1}],
|
|
||||||
relays: Router.get().FromPubkeys([pubkey]).getUrls(),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="card2 bg-alt col-2 shadow-xl">
|
<div class="card2 bg-alt flex flex-col gap-4 shadow-xl">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<Profile {pubkey} {url} />
|
<Profile {pubkey} {url} />
|
||||||
<Link class="btn btn-primary hidden sm:flex" href={makeChatPath([pubkey])}>
|
<Button onclick={openProfile} class="btn btn-primary hidden sm:flex">
|
||||||
<Icon icon="letter" />
|
<Icon icon="user-circle" />
|
||||||
Start a Chat
|
View Profile
|
||||||
</Link>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<ProfileInfo {pubkey} {url} />
|
<ProfileInfo {pubkey} {url} />
|
||||||
{#if $events.length > 0}
|
<ProfileBadges {pubkey} {url} />
|
||||||
<div class="bg-alt badge badge-neutral border-none">
|
<Button onclick={openProfile} class="btn btn-primary sm:hidden">
|
||||||
Last active {formatTimestampRelative($events[0].created_at)}
|
<Icon icon="user-circle" />
|
||||||
</div>
|
View Profile
|
||||||
{/if}
|
</Button>
|
||||||
<Link class="btn btn-primary sm:hidden" href={makeChatPath([pubkey])}>
|
|
||||||
<Icon icon="letter" />
|
|
||||||
Start a Chat
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user