Compare commits
158 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b14c3ab345 | |||
| 823058e335 | |||
| 60ec6924f3 | |||
| 18fc895fcb | |||
| 42295159a0 | |||
| db408ac30d | |||
| 1ced5689c3 | |||
| 263a803875 | |||
| 58afb8fa0c | |||
| 4aaa19ea1b | |||
| 2f9010cd13 | |||
| 12fcdfcd4f | |||
| 317ab57ed2 | |||
| 52ef67740a | |||
| 68ebd32e15 | |||
| e94aa3c119 | |||
| 4d10fe7cc0 | |||
| 841928783b | |||
| 6e5e1a0846 | |||
| d57f4747a6 | |||
| 94a0077b09 | |||
| f2eb04adff | |||
| d4d5979a35 | |||
| dde6e54657 | |||
| 698a7513b8 | |||
| ea3f5a6779 | |||
| f5fce8e2e7 | |||
| 46b5c01c49 | |||
| dd069329ee | |||
| c1b52b66ff | |||
| 5873e8aa60 | |||
| c582082816 | |||
| 6ddba63ff9 | |||
| 5a7750a91b | |||
| 8c71b7d9b9 | |||
| b5a28c71ad | |||
| ccdd18a863 | |||
| 2244ecad9b | |||
| da2457da9f | |||
| c18b29e7d6 | |||
| 3a954201ce | |||
| c8bc8ee8bf | |||
| 8c3e52ce8c | |||
| 303b8967e9 | |||
| f3debe6c02 | |||
| 374ca7f265 | |||
| 91689e5b90 | |||
| a64eaba45c | |||
| 394a1e7d30 | |||
| d5b1fab1e7 | |||
| 10a1e6e640 | |||
| 84af4d2d8e | |||
| acddff79f0 | |||
| 489707b9b2 | |||
| 33902dbefe | |||
| 1b318a7a52 | |||
| b6a4b38d14 | |||
| a3eb6d52c0 | |||
| d2c537d275 | |||
| 9eefd6600d | |||
| ad034b1641 | |||
| d94860014c | |||
| 33af39ee93 | |||
| 1d56a2193d | |||
| 75905e4652 | |||
| d07b9cde5f | |||
| d8a9cc5a7e | |||
| 863d11352f | |||
| b4cc770cdf | |||
| 901e56a625 | |||
| 479fed34f7 | |||
| 81d7b08aed | |||
| a582b1ea73 | |||
| 1c0b2a09df | |||
| 3a42a1b560 | |||
| db203bf00d | |||
| ffb36af734 | |||
| b399fa8dcc | |||
| 5bba5959f7 | |||
| 2ad65e394e | |||
| 345b20bf5d | |||
| b9fb251b32 | |||
| dd9a9c0df2 | |||
| 115b5f9fbe | |||
| 3ad7dcfeb4 | |||
| 60d107aed2 | |||
| 08d8d45ecb | |||
| c40e8ce1a7 | |||
| 993bf8d2e6 | |||
| c3c65c3970 | |||
| a5b868cd56 | |||
| 8fcc56a408 | |||
| c8dfbc936b | |||
| f1e76a1ed1 | |||
| 6ecc3e6770 | |||
| b05c408977 | |||
| e484c3cb00 | |||
| 69d0e11ba4 | |||
| 27d9d4fff1 | |||
| c089812363 | |||
| 07dd1e97dc | |||
| 7f6a1bff34 | |||
| 7d1310722a | |||
| cb57710654 | |||
| c74c116667 | |||
| 0ba55f2387 | |||
| 622214713b | |||
| d8cf48381b | |||
| 7dc7b5abeb | |||
| 324db6a9e8 | |||
| 466541caf5 | |||
| 19f657e348 | |||
| 98a0511b34 | |||
| 0ec620dff9 | |||
| 1301c2c74f | |||
| 7848859153 | |||
| 2d67a9bcf6 | |||
| a7e9318819 | |||
| d66371d573 | |||
| 5684d1a9cf | |||
| fa4bc6894f | |||
| 72919cb1c2 | |||
| 6a3a02bc34 | |||
| db69c56f57 | |||
| a0c6e46184 | |||
| 65aabf5feb | |||
| 131cc99c47 | |||
| 5909b593ab | |||
| f0b2b7c8b3 | |||
| 24a7fa4174 | |||
| 3f2813b63b | |||
| 3e214881a3 | |||
| af171bd2c9 | |||
| 565ccb399a | |||
| fd99866b1e | |||
| 506276f594 | |||
| d4df23545d | |||
| e53d2eb8da | |||
| 22cbb9fe1c | |||
| fedc99b0f0 | |||
| 7d4ba6c806 | |||
| a0e97d5e5b | |||
| 24045a7e2a | |||
| 8d3433b167 | |||
| 0f705c459a | |||
| 08ee07d157 | |||
| cfbff94b4c | |||
| 34477e8ea6 | |||
| eab0ea4eef | |||
| 8ec4d9c548 | |||
| 9defe20f91 | |||
| 614cdcdf53 | |||
| bcd94ee75e | |||
| def6de321c | |||
| 6c7c533637 | |||
| f0207b35d0 | |||
| 858e04d7fa | |||
| b7dcb77378 |
@@ -1,3 +1,8 @@
|
||||
--ignore-dir=.svelte-kit
|
||||
--ignore-dir=android
|
||||
--ignore-dir=build
|
||||
--ignore-dir=ios/DerivedData
|
||||
--ignore-dir=ios/App/App/public
|
||||
--ignore-file=match:.svg
|
||||
--ignore-file=match:package-lock.json
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
VITE_DEFAULT_PUBKEYS=fe7f6bc6f7338b76bbf80db402ade65953e20b2f23e66e898204b63cc42539a3,391819e2f2f13b90cac7209419eb574ef7c0d1f4e81867fc24c47a3ce5e8a248,84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240,dace63b00c42e6e017d00dd190a9328386002ff597b841eb5ef91de4f1ce8491,82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2,58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196,eeb11961b25442b16389fe6c7ebea9adf0ac36dd596816ea7119e521b8821b9e,b676ded7c768d66a757aa3967b1243d90bf57afb09d1044d3219d8d424e4aea0,61066504617ee79387021e18c89fb79d1ddbc3e7bff19cf2298f40466f8715e9,3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24,6389be6491e7b693e9f368ece88fcd145f07c068d2c1bbae4247b9b5ef439d32,97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322
|
||||
VITE_BURROW_URL=
|
||||
VITE_PLATFORM_URL=https://flotilla.social
|
||||
VITE_PLATFORM_TERMS=https://flotilla.social/terms
|
||||
VITE_PLATFORM_PRIVACY=https://flotilla.social/privacy
|
||||
VITE_PLATFORM_NAME=Flotilla
|
||||
VITE_PLATFORM_LOGO=static/flotilla.png
|
||||
VITE_PLATFORM_RELAY=
|
||||
VITE_PLATFORM_ACCENT="#7161FF"
|
||||
VITE_PLATFORM_DESCRIPTION="Flotilla is nostr — for communities."
|
||||
VITE_GLITCHTIP_API_KEY=
|
||||
GLITCHTIP_AUTH_TOKEN=
|
||||
@@ -0,0 +1,17 @@
|
||||
VITE_DEFAULT_PUBKEYS=06639a386c9c1014217622ccbcf40908c4f1a0c33e23f8d6d68f4abf655f8f71,266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5,391819e2f2f13b90cac7209419eb574ef7c0d1f4e81867fc24c47a3ce5e8a248,3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d,3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24,55f04590674f3648f4cdc9dc8ce32da2a282074cd0b020596ee033d12d385185,58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196,61066504617ee79387021e18c89fb79d1ddbc3e7bff19cf2298f40466f8715e9,6389be6491e7b693e9f368ece88fcd145f07c068d2c1bbae4247b9b5ef439d32,63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed,6e75f7972397ca3295e0f4ca0fbc6eb9cc79be85bafdd56bd378220ca8eee74e,76c71aae3a491f1d9eec47cba17e229cda4113a0bbb6e6ae1776d7643e29cafa,7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194,82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2,84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240,97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322,b676ded7c768d66a757aa3967b1243d90bf57afb09d1044d3219d8d424e4aea0,dace63b00c42e6e017d00dd190a9328386002ff597b841eb5ef91de4f1ce8491,eeb11961b25442b16389fe6c7ebea9adf0ac36dd596816ea7119e521b8821b9e,fe7f6bc6f7338b76bbf80db402ade65953e20b2f23e66e898204b63cc42539a3
|
||||
VITE_BURROW_URL=
|
||||
VITE_PLATFORM_URL=https://flotilla.social
|
||||
VITE_PLATFORM_TERMS=https://flotilla.social/terms
|
||||
VITE_PLATFORM_PRIVACY=https://flotilla.social/privacy
|
||||
VITE_PLATFORM_NAME=Flotilla
|
||||
VITE_PLATFORM_LOGO=static/flotilla.png
|
||||
VITE_PLATFORM_RELAY=
|
||||
VITE_PLATFORM_ACCENT="#7161FF"
|
||||
VITE_PLATFORM_SECONDARY="#EB5E28"
|
||||
VITE_PLATFORM_DESCRIPTION="Flotilla is nostr — for communities."
|
||||
VITE_INDEXER_RELAYS=wss://purplepag.es/,wss://relay.damus.io/,wss://relay.nostr.band/
|
||||
VITE_SIGNER_RELAYS=wss://relay.nsec.app/,wss://bucket.coracle.social/
|
||||
VITE_NOTIFIER_PUBKEY=27b7c2ed89ef78322114225ea3ebf5f72c7767c2528d4d0c1854d039c00085df
|
||||
VITE_NOTIFIER_RELAY=wss://anchor.coracle.social/
|
||||
VITE_GLITCHTIP_API_KEY=
|
||||
GLITCHTIP_AUTH_TOKEN=
|
||||
@@ -7,6 +7,8 @@ build
|
||||
gradlew*
|
||||
_app
|
||||
release
|
||||
ios/DerivedData/
|
||||
ios/App/Pods/
|
||||
android/capacitor-cordova-android-plugins
|
||||
android/app/src/androidTest
|
||||
android/app/src/test
|
||||
|
||||
@@ -1,25 +1,10 @@
|
||||
node_modules
|
||||
|
||||
# Output
|
||||
.output
|
||||
.vercel
|
||||
/.svelte-kit
|
||||
/build
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Env
|
||||
.env.local
|
||||
.env
|
||||
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
# Android
|
||||
.idea
|
||||
|
||||
# Generated assets
|
||||
static/favicon.ico
|
||||
static/pwa-64x64.png
|
||||
@@ -29,3 +14,55 @@ static/apple-touch-icon-180x180.png
|
||||
static/maskable-icon-512x512.png
|
||||
src/assets/icons/*.webp
|
||||
manifest.webmanifest
|
||||
|
||||
# Capacitor
|
||||
ios/App/public/
|
||||
ios/App/Pods/
|
||||
ios/App/Podfile.lock
|
||||
ios/DerivedData/
|
||||
android/app/src/main/assets/public/
|
||||
|
||||
# Web/JavaScript
|
||||
node_modules/
|
||||
build/
|
||||
.svelte-kit/
|
||||
|
||||
# iOS
|
||||
ios/App/App/public
|
||||
ios/DerivedData
|
||||
xcuserdata/
|
||||
*.xcworkspace/
|
||||
*.mode1v3
|
||||
*.mode2v3
|
||||
*.perspectivev3
|
||||
*.pbxuser
|
||||
*.xccheckout
|
||||
*.moved-aside
|
||||
*.hmap
|
||||
*.ipa
|
||||
*.xcuserstate
|
||||
*.dSYM.zip
|
||||
*.dSYM
|
||||
|
||||
# Android
|
||||
*.apk
|
||||
*.aab
|
||||
*.ap_
|
||||
*.dex
|
||||
*.class
|
||||
bin/
|
||||
gen/
|
||||
out/
|
||||
.gradle/
|
||||
local.properties
|
||||
proguard/
|
||||
google-services.json
|
||||
GoogleService-Info.plist
|
||||
|
||||
# IDEs and editors
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
# OS generated
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
npm run lint
|
||||
npm run check
|
||||
pnpm run lint
|
||||
pnpm run check
|
||||
|
||||
@@ -1,5 +1,99 @@
|
||||
# Changelog
|
||||
|
||||
# 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
|
||||
|
||||
* Fix add relay button
|
||||
* Fix safe inset areas
|
||||
* Better rendering for errors from relays
|
||||
* Improve remote signer login
|
||||
|
||||
# 1.0.1
|
||||
|
||||
* Fix relay images in nav
|
||||
* Fix relay nav overflow
|
||||
|
||||
# 1.0.0
|
||||
|
||||
* Add alerts via Anchor
|
||||
* Fix nip46 signer connect
|
||||
* Allow use of cleartext relays on native builds
|
||||
* Fix some modal state bugs caused by svelte 5
|
||||
* Detect blossom support on community relays
|
||||
* Use user blossom server list in settings
|
||||
* Fix some feed bugs
|
||||
* Improve thunk indicator
|
||||
* Update storage adapters
|
||||
* Fix modal flash
|
||||
* Switch to pnpm
|
||||
* Improve calendar windowing
|
||||
|
||||
# 0.2.14
|
||||
|
||||
* Add calendar event editing
|
||||
|
||||
# 0.2.13
|
||||
|
||||
* Fix android keyboard issue
|
||||
|
||||
# 0.2.12
|
||||
|
||||
* Fix keyboard covering chat input
|
||||
* Fix thread replies
|
||||
* Make error reporting and analytics optional
|
||||
* Replace long press with tap target
|
||||
* Fix time input
|
||||
* Fix nevent hints for url-specific stuff
|
||||
* Fix confirm and reactions on mobile
|
||||
* Add reply to chat on mobile
|
||||
* Fix profile suggestions
|
||||
|
||||
# 0.2.11
|
||||
|
||||
* Add in-app signup flow on ios
|
||||
* Add profile deletion
|
||||
|
||||
# 0.2.10
|
||||
|
||||
* Improve space discovery
|
||||
|
||||
# 0.2.9
|
||||
|
||||
* Add NIP 01 signup flow on mobile
|
||||
|
||||
# 0.2.8
|
||||
|
||||
* Show spinner when joining a room
|
||||
* Reduce self-rate limiting of REQs
|
||||
* Fix disabled signers link
|
||||
* Prepare for iOS release
|
||||
* Improve threads and calendar pages
|
||||
* Improve quote rendering and new messages button
|
||||
|
||||
# 0.2.7
|
||||
|
||||
* Add calendar events
|
||||
* Migrate to svelte 5 (fixes some bugs, probably introduces others)
|
||||
* Migrate to new welshman editor
|
||||
* Make reply indicator nicer
|
||||
* Make share indicator nicer
|
||||
* Improve feed loading
|
||||
* Show marker for last activity in chat
|
||||
|
||||
# 0.2.6
|
||||
|
||||
* Add reply to long-press menu
|
||||
|
||||
@@ -2,19 +2,19 @@
|
||||
|
||||
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 draft NIP: https://github.com/coracle-social/nips/blob/relay-chat/xx.md
|
||||
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:
|
||||
|
||||
- `npm install`
|
||||
- `npm run build`
|
||||
- `pnpm install`
|
||||
- `pnpm run build`
|
||||
- `npx serve build`
|
||||
|
||||
## 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_PLATFORM_URL` - The url where the app will be hosted. This is only used for build-time population of meta tags.
|
||||
@@ -38,7 +38,7 @@ First, create an `A` record with your DNS provider pointing to the IP of your se
|
||||
|
||||
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.
|
||||
Now, create a new user where your code will be stored, clone the repository, fill in your `.env` file, and build the app.
|
||||
|
||||
```sh
|
||||
# Replace with your password
|
||||
@@ -65,12 +65,12 @@ git clone https://github.com/coracle-social/flotilla.git
|
||||
cd ~/flotilla
|
||||
nvm install
|
||||
nvm use
|
||||
npm i
|
||||
pnpm i
|
||||
|
||||
# Optionally create and populate .env.local to suit your use case
|
||||
# Optionally create and populate .env to suit your use case
|
||||
|
||||
# Build the app
|
||||
NODE_OPTIONS=--max_old_space_size=16384 npm run build
|
||||
NODE_OPTIONS=--max_old_space_size=16384 pnpm run build
|
||||
|
||||
# Exit back to root
|
||||
exit
|
||||
@@ -108,4 +108,4 @@ Now, visit your domain. You should be all set up!
|
||||
|
||||
# Development
|
||||
|
||||
Run `npm run dev` to get a dev server, and `npm run check:watch` to watch for typescript errors. When you're ready to commit, run `npm run format && npm run lint` and fix any errors that come up.
|
||||
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.
|
||||
|
||||
@@ -5,10 +5,10 @@ android {
|
||||
compileSdk rootProject.ext.compileSdkVersion
|
||||
defaultConfig {
|
||||
applicationId "social.flotilla"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 6
|
||||
versionName "0.2.6"
|
||||
minSdk rootProject.ext.minSdkVersion
|
||||
targetSdk rootProject.ext.targetSdkVersion
|
||||
versionCode 18
|
||||
versionName "1.0.4"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
|
||||
android {
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
sourceCompatibility JavaVersion.VERSION_21
|
||||
targetCompatibility JavaVersion.VERSION_21
|
||||
}
|
||||
}
|
||||
|
||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||
dependencies {
|
||||
implementation project(':capacitor-app')
|
||||
implementation project(':capacitor-keyboard')
|
||||
implementation project(':nostr-signer-capacitor-plugin')
|
||||
|
||||
}
|
||||
|
||||
@@ -6,12 +6,14 @@
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="true">
|
||||
<activity
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation"
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/title_activity_main"
|
||||
android:theme="@style/AppTheme.NoActionBarLaunch"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:launchMode="singleTask"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
|
||||
@@ -8,7 +8,7 @@ buildscript {
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.8.0'
|
||||
classpath 'com.google.gms:google-services:4.4.0'
|
||||
classpath 'com.google.gms:google-services:4.4.2'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
||||
include ':capacitor-android'
|
||||
project(':capacitor-android').projectDir = new File('../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-app'
|
||||
project(':capacitor-app').projectDir = new File('../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'
|
||||
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 ':nostr-signer-capacitor-plugin'
|
||||
project(':nostr-signer-capacitor-plugin').projectDir = new File('../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,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
@@ -55,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
@@ -83,7 +85,9 @@ done
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||
' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
@@ -144,7 +148,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
@@ -152,7 +156,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
@@ -201,11 +205,11 @@ fi
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
@@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
ext {
|
||||
minSdkVersion = 22
|
||||
compileSdkVersion = 34
|
||||
targetSdkVersion = 34
|
||||
androidxActivityVersion = '1.8.0'
|
||||
minSdkVersion = 23
|
||||
compileSdkVersion = 35
|
||||
targetSdkVersion = 35
|
||||
androidxActivityVersion = '1.9.2'
|
||||
//https://github.com/ionic-team/capacitor/issues/7866
|
||||
// androidxAppCompatVersion = '1.7.0'
|
||||
androidxAppCompatVersion = '1.6.1'
|
||||
androidxCoordinatorLayoutVersion = '1.2.0'
|
||||
androidxCoreVersion = '1.12.0'
|
||||
androidxFragmentVersion = '1.6.2'
|
||||
androidxCoreVersion = '1.15.0'
|
||||
androidxFragmentVersion = '1.8.4'
|
||||
coreSplashScreenVersion = '1.0.1'
|
||||
androidxWebkitVersion = '1.9.0'
|
||||
androidxWebkitVersion = '1.12.1'
|
||||
junitVersion = '4.13.2'
|
||||
androidxJunitVersion = '1.1.5'
|
||||
androidxEspressoCoreVersion = '3.5.1'
|
||||
androidxJunitVersion = '1.2.1'
|
||||
androidxEspressoCoreVersion = '3.6.1'
|
||||
cordovaAndroidVersion = '10.1.1'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Fetch tags and set to env vars
|
||||
git fetch --prune --unshallow --tags
|
||||
git describe --tags --abbrev=0
|
||||
export VITE_BUILD_VERSION=$RENDER_GIT_COMMIT
|
||||
export VITE_BUILD_HASH=$RENDER_GIT_COMMIT
|
||||
|
||||
# Remove link overrides
|
||||
node remove-pnpm-overrides.js package.json
|
||||
|
||||
# When CI=true as it is on render.com, removing link overrides breaks the lockfile
|
||||
pnpm i --no-frozen-lockfile
|
||||
|
||||
# Rebuild sharp
|
||||
pnpm rebuild
|
||||
|
||||
# The build runs out of memory at times
|
||||
NODE_OPTIONS=--max_old_space_size=16384 pnpm run build
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
temp_env=$(declare -p -x)
|
||||
|
||||
if [ -f .env ]; then
|
||||
source .env
|
||||
if [ -f .env.template ]; then
|
||||
source .env.template
|
||||
fi
|
||||
|
||||
if [ -f .env.local ]; then
|
||||
source .env.local
|
||||
if [ -f .env ]; then
|
||||
source .env
|
||||
fi
|
||||
|
||||
# Avoid overwriting env vars provided directly
|
||||
|
||||
@@ -10,8 +10,17 @@ const config: CapacitorConfig = {
|
||||
plugins: {
|
||||
SplashScreen: {
|
||||
androidSplashResourceName: "splash"
|
||||
}
|
||||
}
|
||||
},
|
||||
Keyboard: {
|
||||
style: "DARK",
|
||||
resizeOnFullScreen: true,
|
||||
},
|
||||
},
|
||||
// Use this for live reload https://capacitorjs.com/docs/guides/live-reload
|
||||
// server: {
|
||||
// url: "http://192.168.1.115:1847",
|
||||
// cleartext: true
|
||||
// },
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
App/build
|
||||
App/Pods
|
||||
App/output
|
||||
App/App/public
|
||||
DerivedData
|
||||
xcuserdata
|
||||
|
||||
# Cordova plugins for Capacitor
|
||||
capacitor-cordova-ios-plugins
|
||||
|
||||
# Generated Config files
|
||||
App/App/capacitor.config.json
|
||||
App/App/config.xml
|
||||
@@ -0,0 +1,420 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 48;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; };
|
||||
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
|
||||
504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; };
|
||||
504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; };
|
||||
504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; };
|
||||
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
|
||||
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
|
||||
AC8D5382B9575A9124613C5D /* Pods_Flotilla_Chat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA42C7E3CF3FFF7A17A3A729 /* Pods_Flotilla_Chat.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
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>"; };
|
||||
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
|
||||
504EC3041FED79650016851F /* Flotilla Chat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Flotilla Chat.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
504EC30C1FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
504EC30E1FED79650016851F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
|
||||
7B9FA71C362B734D9F965709 /* Pods-Flotilla Chat.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Flotilla Chat.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Flotilla Chat/Pods-Flotilla Chat.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
|
||||
DA42C7E3CF3FFF7A17A3A729 /* Pods_Flotilla_Chat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Flotilla_Chat.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
504EC3011FED79650016851F /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
AC8D5382B9575A9124613C5D /* Pods_Flotilla_Chat.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DA42C7E3CF3FFF7A17A3A729 /* Pods_Flotilla_Chat.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
504EC2FB1FED79650016851F = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
504EC3061FED79650016851F /* App */,
|
||||
504EC3051FED79650016851F /* Products */,
|
||||
7F8756D8B27F46E3366F6CEA /* Pods */,
|
||||
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
504EC3051FED79650016851F /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
504EC3041FED79650016851F /* Flotilla Chat.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
504EC3061FED79650016851F /* App */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
50379B222058CBB4000EE86E /* capacitor.config.json */,
|
||||
504EC3071FED79650016851F /* AppDelegate.swift */,
|
||||
504EC30B1FED79650016851F /* Main.storyboard */,
|
||||
504EC30E1FED79650016851F /* Assets.xcassets */,
|
||||
504EC3101FED79650016851F /* LaunchScreen.storyboard */,
|
||||
504EC3131FED79650016851F /* Info.plist */,
|
||||
2FAD9762203C412B000D30F8 /* config.xml */,
|
||||
50B271D01FEDC1A000F3C39B /* public */,
|
||||
);
|
||||
path = App;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7F8756D8B27F46E3366F6CEA /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */,
|
||||
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */,
|
||||
7B9FA71C362B734D9F965709 /* Pods-Flotilla Chat.debug.xcconfig */,
|
||||
1F53EE54954731A2328CBC4B /* Pods-Flotilla Chat.release.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
504EC3031FED79650016851F /* Flotilla Chat */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "Flotilla Chat" */;
|
||||
buildPhases = (
|
||||
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */,
|
||||
504EC3001FED79650016851F /* Sources */,
|
||||
504EC3011FED79650016851F /* Frameworks */,
|
||||
504EC3021FED79650016851F /* Resources */,
|
||||
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = "Flotilla Chat";
|
||||
productName = App;
|
||||
productReference = 504EC3041FED79650016851F /* Flotilla Chat.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
504EC2FC1FED79650016851F /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 920;
|
||||
LastUpgradeCheck = 920;
|
||||
TargetAttributes = {
|
||||
504EC3031FED79650016851F = {
|
||||
CreatedOnToolsVersion = 9.2;
|
||||
LastSwiftMigration = 1100;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */;
|
||||
compatibilityVersion = "Xcode 8.0";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 504EC2FB1FED79650016851F;
|
||||
productRefGroup = 504EC3051FED79650016851F /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
504EC3031FED79650016851F /* Flotilla Chat */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
504EC3021FED79650016851F /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */,
|
||||
50B271D11FEDC1A000F3C39B /* public in Resources */,
|
||||
504EC30F1FED79650016851F /* Assets.xcassets in Resources */,
|
||||
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */,
|
||||
504EC30D1FED79650016851F /* Main.storyboard in Resources */,
|
||||
2FAD9763203C412B000D30F8 /* config.xml in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-Flotilla Chat-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Flotilla Chat/Pods-Flotilla Chat-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
504EC3001FED79650016851F /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
504EC30B1FED79650016851F /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
504EC30C1FED79650016851F /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
504EC3101FED79650016851F /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
504EC3111FED79650016851F /* Base */,
|
||||
);
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
504EC3141FED79650016851F /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
504EC3151FED79650016851F /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
504EC3171FED79650016851F /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7B9FA71C362B734D9F965709 /* Pods-Flotilla Chat.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 11;
|
||||
DEVELOPMENT_TEAM = S26U9DYW3A;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 1.0.4;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
504EC3181FED79650016851F /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 1F53EE54954731A2328CBC4B /* Pods-Flotilla Chat.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 11;
|
||||
DEVELOPMENT_TEAM = S26U9DYW3A;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 1.0.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
504EC3141FED79650016851F /* Debug */,
|
||||
504EC3151FED79650016851F /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "Flotilla Chat" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
504EC3171FED79650016851F /* Debug */,
|
||||
504EC3181FED79650016851F /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 504EC2FC1FED79650016851F /* Project object */;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import UIKit
|
||||
import Capacitor
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
// Override point for customization after application launch.
|
||||
return true
|
||||
}
|
||||
|
||||
func applicationWillResignActive(_ application: UIApplication) {
|
||||
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
|
||||
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
|
||||
}
|
||||
|
||||
func applicationDidEnterBackground(_ application: UIApplication) {
|
||||
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
|
||||
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
|
||||
}
|
||||
|
||||
func applicationWillEnterForeground(_ application: UIApplication) {
|
||||
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
||||
}
|
||||
|
||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
||||
// Called when the app was launched with a url. Feel free to add additional processing here,
|
||||
// but if you want the App API to support tracking app url opens, make sure to keep this call
|
||||
return ApplicationDelegateProxy.shared.application(app, open: url, options: options)
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
|
||||
// Called when the app was launched with an activity, including Universal Links.
|
||||
// Feel free to add additional processing here, but if you want the App API to support
|
||||
// tracking app url opens, make sure to keep this call
|
||||
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
|
||||
}
|
||||
|
||||
}
|
||||
|
After Width: | Height: | Size: 21 KiB |
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"idiom": "universal",
|
||||
"size": "1024x1024",
|
||||
"filename": "AppIcon-512@2x.png",
|
||||
"platform": "ios"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"idiom": "universal",
|
||||
"filename": "Default@1x~universal~anyany.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"idiom": "universal",
|
||||
"filename": "Default@2x~universal~anyany.png",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"idiom": "universal",
|
||||
"filename": "Default@3x~universal~anyany.png",
|
||||
"scale": "3x"
|
||||
},
|
||||
{
|
||||
"appearances": [
|
||||
{
|
||||
"appearance": "luminosity",
|
||||
"value": "dark"
|
||||
}
|
||||
],
|
||||
"idiom": "universal",
|
||||
"scale": "1x",
|
||||
"filename": "Default@1x~universal~anyany-dark.png"
|
||||
},
|
||||
{
|
||||
"appearances": [
|
||||
{
|
||||
"appearance": "luminosity",
|
||||
"value": "dark"
|
||||
}
|
||||
],
|
||||
"idiom": "universal",
|
||||
"scale": "2x",
|
||||
"filename": "Default@2x~universal~anyany-dark.png"
|
||||
},
|
||||
{
|
||||
"appearances": [
|
||||
{
|
||||
"appearance": "luminosity",
|
||||
"value": "dark"
|
||||
}
|
||||
],
|
||||
"idiom": "universal",
|
||||
"scale": "3x",
|
||||
"filename": "Default@3x~universal~anyany-dark.png"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"version": 1,
|
||||
"author": "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 40 KiB |
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17132" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17105"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<imageView key="view" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Splash" id="snD-IY-ifK">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
</imageView>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="Splash" width="1366" height="1366"/>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14111" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14088"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Bridge View Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="CAPBridgeViewController" customModule="Capacitor" sceneMemberID="viewController"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Flotilla</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UIStatusBarStyle</key>
|
||||
<string></string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,26 @@
|
||||
require_relative '../../node_modules/.pnpm/@capacitor+ios@7.2.0_@capacitor+core@7.2.0/node_modules/@capacitor/ios/scripts/pods_helpers'
|
||||
|
||||
platform :ios, '14.0'
|
||||
use_frameworks!
|
||||
|
||||
# workaround to avoid Xcode caching of Pods that requires
|
||||
# Product -> Clean Build Folder after new Cordova plugins installed
|
||||
# Requires CocoaPods 1.6 or newer
|
||||
install! 'cocoapods', :disable_input_output_paths => true
|
||||
|
||||
def capacitor_pods
|
||||
pod 'Capacitor', :path => '../../node_modules/.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 '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 'NostrSignerCapacitorPlugin', :path => '../../node_modules/.pnpm/nostr-signer-capacitor-plugin@0.0.4_@capacitor+core@7.2.0/node_modules/nostr-signer-capacitor-plugin'
|
||||
end
|
||||
|
||||
target 'Flotilla Chat' do
|
||||
capacitor_pods
|
||||
# Add your Pods here
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
assertDeploymentTarget(installer)
|
||||
end
|
||||
@@ -1,49 +1,48 @@
|
||||
{
|
||||
"name": "flotilla",
|
||||
"version": "0.2.6",
|
||||
"version": "1.0.4",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "./build.sh",
|
||||
"sourcemaps": "sentry-cli --url https://glitchtip.coracle.social --auth-token $GLITCHTIP_AUTH_TOKEN --api-key $VITE_GLITCHTIP_API_KEY sourcemaps --org coracle --project flotilla --release $(cat package.json|jq -r '.version') upload --url-prefix /_app/immutable/ build/_app/immutable",
|
||||
"release:android": "cap build android --androidreleasetype APK --signing-type apksigner",
|
||||
"sourcemaps": "./build.sh && ./sourcemaps.sh",
|
||||
"release:android": "./build.sh && cap build android --androidreleasetype APK --signing-type apksigner",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --check src && eslint src",
|
||||
"format": "prettier --write src",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"overrides": {
|
||||
"@capacitor/core": "^7.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/assets": "^3.0.5",
|
||||
"@sentry/cli": "^2.40.0",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||
"@sveltejs/kit": "^2.5.27",
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||
"@types/eslint": "^9.6.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"classnames": "^2.5.1",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.36.0",
|
||||
"eslint-plugin-svelte": "^2.45.1",
|
||||
"globals": "^15.0.0",
|
||||
"postcss": "^8.4.40",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier-plugin-svelte": "^3.1.2",
|
||||
"svelte": "^4.2.7",
|
||||
"svelte-check": "^3.6.0",
|
||||
"prettier-plugin-svelte": "^3.2.6",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"tailwindcss": "^3.4.7",
|
||||
"typescript": "^5.0.0",
|
||||
"typescript": "^5.5.0",
|
||||
"typescript-eslint": "^8.0.0",
|
||||
"vite": "^5.0.3"
|
||||
"vite": "^5.4.4"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@capacitor/android": "^7.0.1",
|
||||
"@capacitor/android": "^7.0.0",
|
||||
"@capacitor/app": "^7.0.0",
|
||||
"@capacitor/cli": "^6.2.0",
|
||||
"@capacitor/cli": "^7.0.0",
|
||||
"@capacitor/core": "^7.0.1",
|
||||
"@capacitor/ios": "^7.0.0",
|
||||
"@capacitor/keyboard": "^7.0.0",
|
||||
"@noble/curves": "^1.5.0",
|
||||
"@noble/hashes": "^1.4.0",
|
||||
"@poppanator/sveltekit-svg": "^4.2.1",
|
||||
@@ -52,16 +51,18 @@
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@vite-pwa/assets-generator": "^0.2.6",
|
||||
"@vite-pwa/sveltekit": "^0.6.6",
|
||||
"@welshman/app": "~0.0.41",
|
||||
"@welshman/content": "~0.0.16",
|
||||
"@welshman/dvm": "~0.0.14",
|
||||
"@welshman/editor": "~0.0.10",
|
||||
"@welshman/feeds": "~0.0.30",
|
||||
"@welshman/lib": "~0.0.38",
|
||||
"@welshman/net": "~0.0.46",
|
||||
"@welshman/signer": "~0.0.20",
|
||||
"@welshman/store": "~0.0.15",
|
||||
"@welshman/util": "~0.0.59",
|
||||
"@welshman/app": "^0.2.5",
|
||||
"@welshman/content": "^0.2.2",
|
||||
"@welshman/dvm": "^0.2.0",
|
||||
"@welshman/editor": "^0.2.4",
|
||||
"@welshman/feeds": "^0.2.2",
|
||||
"@welshman/lib": "^0.2.2",
|
||||
"@welshman/net": "^0.2.3",
|
||||
"@welshman/relay": "^0.2.0",
|
||||
"@welshman/router": "^0.2.0",
|
||||
"@welshman/signer": "^0.2.3",
|
||||
"@welshman/store": "^0.2.0",
|
||||
"@welshman/util": "^0.2.3",
|
||||
"daisyui": "^4.12.10",
|
||||
"date-picker-svelte": "^2.13.0",
|
||||
"dotenv": "^16.4.5",
|
||||
@@ -69,9 +70,32 @@
|
||||
"fuse.js": "^7.0.0",
|
||||
"husky": "^9.1.6",
|
||||
"idb": "^8.0.0",
|
||||
"nostr-signer-capacitor-plugin": "^0.0.3",
|
||||
"nostr-signer-capacitor-plugin": "^0.0.4",
|
||||
"nostr-tools": "^2.7.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||
"qrcode": "^1.5.4"
|
||||
},
|
||||
"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": [
|
||||
"@sentry/cli",
|
||||
"esbuild"
|
||||
],
|
||||
"onlyBuiltDependencies": [
|
||||
"sharp"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import dotenv from "dotenv"
|
||||
import {defineConfig, minimalPreset as preset} from "@vite-pwa/assets-generator/config"
|
||||
|
||||
dotenv.config({path: ".env.local"})
|
||||
dotenv.config({path: ".env"})
|
||||
dotenv.config({path: ".env.template"})
|
||||
|
||||
export default defineConfig({
|
||||
preset,
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// 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) {
|
||||
// Use $package notation to make sure we only get one copy of each welshman dependency
|
||||
// TODO: move welshman to a single package to straighten all this out.
|
||||
for (const k of Object.keys(pkg.pnpm.overrides)) {
|
||||
pkg.pnpm.overrides[k] = '$' + k
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
hash=$(find build -type f -print0 | sort -z | xargs -0 sha1sum | sha1sum | awk '{print $1}')
|
||||
|
||||
sentry-cli \
|
||||
--url https://glitchtip.coracle.social \
|
||||
--auth-token $GLITCHTIP_AUTH_TOKEN \
|
||||
--api-key $VITE_GLITCHTIP_API_KEY \
|
||||
sourcemaps \
|
||||
--org coracle \
|
||||
--project flotilla \
|
||||
--release $hash \
|
||||
upload \
|
||||
--url-prefix /_app/immutable/ \
|
||||
build/_app/immutable
|
||||
@@ -1,7 +1,11 @@
|
||||
@import "@welshman/editor/index.css";
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Fonts */
|
||||
|
||||
@font-face {
|
||||
font-family: "Satoshis";
|
||||
font-style: normal;
|
||||
@@ -38,8 +42,18 @@
|
||||
url("/fonts/Italic.ttf") format("truetype");
|
||||
}
|
||||
|
||||
/* root */
|
||||
|
||||
:root {
|
||||
font-family: Lato;
|
||||
--sait: env(safe-area-inset-top);
|
||||
--saib: env(safe-area-inset-bottom);
|
||||
--sail: env(safe-area-inset-left);
|
||||
--sair: env(safe-area-inset-right);
|
||||
}
|
||||
|
||||
[data-theme] {
|
||||
@apply bg-base-300;
|
||||
--base-100: oklch(var(--b1));
|
||||
--base-200: oklch(var(--b2));
|
||||
--base-300: oklch(var(--b3));
|
||||
@@ -50,6 +64,84 @@
|
||||
--secondary-content: oklch(var(--sc));
|
||||
}
|
||||
|
||||
/* safe area insets */
|
||||
|
||||
@layer components {
|
||||
.pt-sai {
|
||||
padding-top: var(--sait);
|
||||
}
|
||||
|
||||
.pr-sai {
|
||||
padding-right: var(--sair);
|
||||
}
|
||||
|
||||
.pb-sai {
|
||||
padding-bottom: var(--saib);
|
||||
}
|
||||
|
||||
.pl-sai {
|
||||
padding-left: var(--sail);
|
||||
}
|
||||
|
||||
.px-sai {
|
||||
@apply pl-sai pr-sai;
|
||||
}
|
||||
|
||||
.py-sai {
|
||||
@apply pt-sai pb-sai;
|
||||
}
|
||||
|
||||
.p-sai {
|
||||
@apply py-sai px-sai;
|
||||
}
|
||||
|
||||
.mt-sai {
|
||||
padding-top: var(--sait);
|
||||
}
|
||||
|
||||
.mr-sai {
|
||||
padding-right: var(--sair);
|
||||
}
|
||||
|
||||
.mb-sai {
|
||||
padding-bottom: var(--saib);
|
||||
}
|
||||
|
||||
.ml-sai {
|
||||
padding-left: var(--sail);
|
||||
}
|
||||
|
||||
.mx-sai {
|
||||
@apply ml-sai mr-sai;
|
||||
}
|
||||
|
||||
.my-sai {
|
||||
@apply mt-sai mb-sai;
|
||||
}
|
||||
|
||||
.m-sai {
|
||||
@apply my-sai mx-sai;
|
||||
}
|
||||
|
||||
.top-sai {
|
||||
top: var(--sait);
|
||||
}
|
||||
|
||||
.right-sai {
|
||||
right: var(--sair);
|
||||
}
|
||||
|
||||
.bottom-sai {
|
||||
bottom: var(--saib);
|
||||
}
|
||||
|
||||
.left-sai {
|
||||
left: var(--sail);
|
||||
}
|
||||
}
|
||||
|
||||
/* utilities */
|
||||
|
||||
.bg-alt,
|
||||
.bg-alt .bg-alt .bg-alt,
|
||||
.hover\:bg-alt:hover,
|
||||
@@ -199,6 +291,14 @@
|
||||
--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 {
|
||||
@apply max-h-[350px] overflow-y-auto p-2 px-4;
|
||||
}
|
||||
@@ -234,13 +334,32 @@
|
||||
color: var(--base-content);
|
||||
}
|
||||
|
||||
/* content rendered by welshman/content */
|
||||
|
||||
.welshman-content a {
|
||||
@apply link;
|
||||
}
|
||||
|
||||
.welshman-content-error a {
|
||||
@apply underline;
|
||||
}
|
||||
|
||||
/* date input */
|
||||
|
||||
.date-time-field {
|
||||
@apply input input-bordered rounded px-0;
|
||||
.picker {
|
||||
--date-picker-foreground: var(--base-content);
|
||||
--date-picker-background: var(--base-300);
|
||||
--date-picker-highlight-border: var(--primary);
|
||||
--date-picker-selected-color: var(--primary-content);
|
||||
--date-picker-selected-background: var(--primary);
|
||||
}
|
||||
|
||||
.date-time-field {
|
||||
@apply input input-bordered rounded-lg px-0;
|
||||
}
|
||||
|
||||
.date-time-field input {
|
||||
@apply !h-full !w-full !border-none !bg-inherit !text-inherit;
|
||||
@apply !h-full !w-full !rounded-lg !border-none !bg-inherit !px-4 !text-inherit;
|
||||
}
|
||||
|
||||
/* emoji picker */
|
||||
@@ -256,3 +375,33 @@ emoji-picker {
|
||||
--input-font-color: var(--base-content);
|
||||
--outline-color: var(--base-100);
|
||||
}
|
||||
|
||||
/* progress */
|
||||
|
||||
progress[value]::-webkit-progress-value {
|
||||
transition: width 0.5s;
|
||||
}
|
||||
|
||||
/* content width for fixed elements */
|
||||
|
||||
.cw {
|
||||
@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 {
|
||||
@apply md:bottom-sai bottom-[calc(var(--saib)+3.5rem)];
|
||||
}
|
||||
|
||||
/* chat view */
|
||||
|
||||
.chat__compose {
|
||||
@apply cb cw fixed;
|
||||
}
|
||||
|
||||
.chat__scroll-down {
|
||||
@apply fixed bottom-28 right-4 md:bottom-16;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content" />
|
||||
<meta name="theme-color" content="{ACCENT}" />
|
||||
<meta name="description" content="{DESCRIPTION}" />
|
||||
<meta name="og:url" content="{URL}" />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint prefer-rest-params: 0 */
|
||||
|
||||
import {page} from "$app/stores"
|
||||
import {getSetting} from "@app/state"
|
||||
|
||||
const w = window as any
|
||||
|
||||
@@ -12,7 +13,7 @@ w.plausible =
|
||||
|
||||
export const setupAnalytics = () => {
|
||||
page.subscribe($page => {
|
||||
if ($page.route) {
|
||||
if ($page.route && getSetting("report_usage")) {
|
||||
w.plausible("pageview", {u: $page.route.id})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import * as nip19 from "nostr-tools/nip19"
|
||||
import {get} from "svelte/store"
|
||||
import {ctx, sample, uniq, sleep, chunk, equals} from "@welshman/lib"
|
||||
import {randomId, poll, uniq, equals, TIMEZONE, LOCALE} from "@welshman/lib"
|
||||
import type {Feed} from "@welshman/feeds"
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import {
|
||||
DELETE,
|
||||
REPORT,
|
||||
@@ -26,39 +28,31 @@ import {
|
||||
getTag,
|
||||
getListTags,
|
||||
getRelayTags,
|
||||
isShareableRelayUrl,
|
||||
getRelayTagValues,
|
||||
toNostrURI,
|
||||
getRelaysFromList,
|
||||
RelayMode,
|
||||
} from "@welshman/util"
|
||||
import type {TrustedEvent, EventContent, EventTemplate, List} from "@welshman/util"
|
||||
import type {SubscribeRequestWithHandlers} from "@welshman/net"
|
||||
import {PublishStatus, AuthStatus, SocketStatus} from "@welshman/net"
|
||||
import {Nip59, makeSecret, stamp, Nip46Broker} from "@welshman/signer"
|
||||
import {Pool, PublishStatus, AuthStatus, SocketStatus} from "@welshman/net"
|
||||
import {Router} from "@welshman/router"
|
||||
import {
|
||||
pubkey,
|
||||
signer,
|
||||
repository,
|
||||
publishThunk,
|
||||
publishThunks,
|
||||
loadProfile,
|
||||
loadInboxRelaySelections,
|
||||
profilesByPubkey,
|
||||
relaySelectionsByPubkey,
|
||||
getWriteRelayUrls,
|
||||
loadFollows,
|
||||
loadMutes,
|
||||
tagEvent,
|
||||
tagEventForReaction,
|
||||
getRelayUrls,
|
||||
userRelaySelections,
|
||||
userInboxRelaySelections,
|
||||
nip44EncryptToSelf,
|
||||
loadRelay,
|
||||
addSession,
|
||||
clearStorage,
|
||||
dropSession,
|
||||
tagEventForComment,
|
||||
tagEventForQuote,
|
||||
thunkIsComplete,
|
||||
} from "@welshman/app"
|
||||
import type {Thunk} from "@welshman/app"
|
||||
import {
|
||||
@@ -66,10 +60,9 @@ import {
|
||||
PROTECTED,
|
||||
userMembership,
|
||||
INDEXER_RELAYS,
|
||||
NIP46_PERMS,
|
||||
loadMembership,
|
||||
loadSettings,
|
||||
getDefaultPubkeys,
|
||||
ALERT,
|
||||
NOTIFIER_PUBKEY,
|
||||
NOTIFIER_RELAY,
|
||||
userRoomsByUrl,
|
||||
} from "@app/state"
|
||||
|
||||
@@ -77,7 +70,7 @@ import {
|
||||
|
||||
export const getPubkeyHints = (pubkey: string) => {
|
||||
const selections = relaySelectionsByPubkey.get().get(pubkey)
|
||||
const relays = selections ? getWriteRelayUrls(selections) : []
|
||||
const relays = selections ? getRelaysFromList(selections, RelayMode.Write) : []
|
||||
const hints = relays.length ? relays : INDEXER_RELAYS
|
||||
|
||||
return hints
|
||||
@@ -90,14 +83,20 @@ export const getPubkeyPetname = (pubkey: string) => {
|
||||
return display
|
||||
}
|
||||
|
||||
export const getThunkError = async (thunk: Thunk) => {
|
||||
const result = await thunk.result
|
||||
const [{status, message}] = Object.values(result) as any
|
||||
export const getThunkError = (thunk: Thunk) =>
|
||||
new Promise<string>(resolve => {
|
||||
thunk.subscribe($thunk => {
|
||||
for (const [relay, status] of Object.entries($thunk.status)) {
|
||||
if (status === PublishStatus.Failure) {
|
||||
resolve($thunk.details[relay])
|
||||
}
|
||||
}
|
||||
|
||||
if (status !== PublishStatus.Success) {
|
||||
return message
|
||||
}
|
||||
}
|
||||
if (thunkIsComplete($thunk)) {
|
||||
resolve("")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
export const prependParent = (parent: TrustedEvent | undefined, {content, tags}: EventContent) => {
|
||||
if (parent) {
|
||||
@@ -105,7 +104,7 @@ export const prependParent = (parent: TrustedEvent | undefined, {content, tags}:
|
||||
id: parent.id,
|
||||
kind: parent.kind,
|
||||
author: parent.pubkey,
|
||||
relays: ctx.app.router.Event(parent).limit(3).getUrls(),
|
||||
relays: Router.get().Event(parent).limit(3).getUrls(),
|
||||
})
|
||||
|
||||
tags = [...tags, tagEventForQuote(parent)]
|
||||
@@ -115,38 +114,6 @@ export const prependParent = (parent: TrustedEvent | undefined, {content, tags}:
|
||||
return {content, tags}
|
||||
}
|
||||
|
||||
// Log in
|
||||
|
||||
export const loginWithNip46 = async ({
|
||||
relays,
|
||||
signerPubkey,
|
||||
clientSecret = makeSecret(),
|
||||
connectSecret = "",
|
||||
}: {
|
||||
relays: string[]
|
||||
signerPubkey: string
|
||||
clientSecret?: string
|
||||
connectSecret?: string
|
||||
}) => {
|
||||
const broker = Nip46Broker.get({relays, clientSecret, signerPubkey})
|
||||
const result = await broker.connect(connectSecret, NIP46_PERMS)
|
||||
|
||||
// TODO: remove ack result
|
||||
if (!["ack", connectSecret].includes(result)) return false
|
||||
|
||||
const pubkey = await broker.getPublicKey()
|
||||
|
||||
if (!pubkey) return false
|
||||
|
||||
await loadUserData(pubkey)
|
||||
|
||||
const handler = {relays, pubkey: signerPubkey}
|
||||
|
||||
addSession({method: "nip46", pubkey, secret: clientSecret, handler})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Log out
|
||||
|
||||
export const logout = async () => {
|
||||
@@ -161,47 +128,6 @@ export const logout = async () => {
|
||||
localStorage.clear()
|
||||
}
|
||||
|
||||
// Loaders
|
||||
|
||||
export const loadUserData = (
|
||||
pubkey: string,
|
||||
request: Partial<SubscribeRequestWithHandlers> = {},
|
||||
) => {
|
||||
const promise = Promise.race([
|
||||
sleep(3000),
|
||||
Promise.all([
|
||||
loadInboxRelaySelections(pubkey, request),
|
||||
loadMembership(pubkey, request),
|
||||
loadSettings(pubkey, request),
|
||||
loadProfile(pubkey, request),
|
||||
loadFollows(pubkey, request),
|
||||
loadMutes(pubkey, request),
|
||||
]),
|
||||
])
|
||||
|
||||
// Load followed profiles slowly in the background without clogging other stuff up. Only use a single
|
||||
// indexer relay to avoid too many redundant validations, which slow things down and eat bandwidth
|
||||
promise.then(async () => {
|
||||
for (const pubkeys of chunk(50, getDefaultPubkeys())) {
|
||||
const relays = sample(1, INDEXER_RELAYS)
|
||||
|
||||
await sleep(1000)
|
||||
|
||||
for (const pubkey of pubkeys) {
|
||||
loadMembership(pubkey, {relays})
|
||||
loadProfile(pubkey, {relays})
|
||||
loadFollows(pubkey, {relays})
|
||||
loadMutes(pubkey, {relays})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return promise
|
||||
}
|
||||
|
||||
export const discoverRelays = (lists: List[]) =>
|
||||
Promise.all(uniq(lists.flatMap(getRelayUrls)).filter(isShareableRelayUrl).map(loadRelay))
|
||||
|
||||
// Synchronization
|
||||
|
||||
export const broadcastUserData = async (relays: string[]) => {
|
||||
@@ -248,7 +174,7 @@ export const nip29 = {
|
||||
export const addSpaceMembership = async (url: string) => {
|
||||
const list = get(userMembership) || makeList({kind: GROUPS})
|
||||
const event = await addToListPublicly(list, ["r", url]).reconcile(nip44EncryptToSelf)
|
||||
const relays = uniq([...ctx.app.router.FromUser().getUrls(), ...getRelayTagValues(event.tags)])
|
||||
const relays = uniq([...Router.get().FromUser().getUrls(), ...getRelayTagValues(event.tags)])
|
||||
|
||||
return publishThunk({event, relays})
|
||||
}
|
||||
@@ -257,11 +183,7 @@ export const removeSpaceMembership = async (url: string) => {
|
||||
const list = get(userMembership) || makeList({kind: GROUPS})
|
||||
const pred = (t: string[]) => t[t[0] === "r" ? 1 : 2] === url
|
||||
const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf)
|
||||
const relays = uniq([
|
||||
url,
|
||||
...ctx.app.router.FromUser().getUrls(),
|
||||
...getRelayTagValues(event.tags),
|
||||
])
|
||||
const relays = uniq([url, ...Router.get().FromUser().getUrls(), ...getRelayTagValues(event.tags)])
|
||||
|
||||
return publishThunk({event, relays})
|
||||
}
|
||||
@@ -273,7 +195,7 @@ export const addRoomMembership = async (url: string, room: string, name: string)
|
||||
["group", room, url, name],
|
||||
]
|
||||
const event = await addToListPublicly(list, ...newTags).reconcile(nip44EncryptToSelf)
|
||||
const relays = uniq([...ctx.app.router.FromUser().getUrls(), ...getRelayTagValues(event.tags)])
|
||||
const relays = uniq([...Router.get().FromUser().getUrls(), ...getRelayTagValues(event.tags)])
|
||||
|
||||
return publishThunk({event, relays})
|
||||
}
|
||||
@@ -282,11 +204,7 @@ export const removeRoomMembership = async (url: string, room: string) => {
|
||||
const list = get(userMembership) || makeList({kind: GROUPS})
|
||||
const pred = (t: string[]) => equals(["group", room, url], t.slice(0, 3))
|
||||
const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf)
|
||||
const relays = uniq([
|
||||
url,
|
||||
...ctx.app.router.FromUser().getUrls(),
|
||||
...getRelayTagValues(event.tags),
|
||||
])
|
||||
const relays = uniq([url, ...Router.get().FromUser().getUrls(), ...getRelayTagValues(event.tags)])
|
||||
|
||||
return publishThunk({event, relays})
|
||||
}
|
||||
@@ -308,7 +226,7 @@ export const setRelayPolicy = (url: string, read: boolean, write: boolean) => {
|
||||
relays: [
|
||||
url,
|
||||
...INDEXER_RELAYS,
|
||||
...ctx.app.router.FromUser().getUrls(),
|
||||
...Router.get().FromUser().getUrls(),
|
||||
...userRoomsByUrl.get().keys(),
|
||||
],
|
||||
})
|
||||
@@ -318,7 +236,7 @@ export const setInboxRelayPolicy = (url: string, enabled: boolean) => {
|
||||
const list = get(userInboxRelaySelections) || makeList({kind: INBOX_RELAYS})
|
||||
|
||||
// Only update inbox policies if they already exist or we're adding them
|
||||
if (enabled || getRelayUrls(list).includes(url)) {
|
||||
if (enabled || getRelaysFromList(list).includes(url)) {
|
||||
const tags = getRelayTags(getListTags(list)).filter(t => normalizeRelayUrl(t[1]) !== url)
|
||||
|
||||
if (enabled) {
|
||||
@@ -329,7 +247,7 @@ export const setInboxRelayPolicy = (url: string, enabled: boolean) => {
|
||||
event: createEvent(list.kind, {tags}),
|
||||
relays: [
|
||||
...INDEXER_RELAYS,
|
||||
...ctx.app.router.FromUser().getUrls(),
|
||||
...Router.get().FromUser().getUrls(),
|
||||
...userRoomsByUrl.get().keys(),
|
||||
],
|
||||
})
|
||||
@@ -339,28 +257,31 @@ export const setInboxRelayPolicy = (url: string, enabled: boolean) => {
|
||||
// Relay access
|
||||
|
||||
export const checkRelayAccess = async (url: string, claim = "") => {
|
||||
const connection = ctx.net.pool.get(url)
|
||||
const socket = Pool.get().get(url)
|
||||
|
||||
await connection.auth.attempt(5000)
|
||||
await socket.auth.attemptAuth(e => signer.get()?.sign(e))
|
||||
|
||||
const thunk = publishThunk({
|
||||
event: createEvent(AUTH_JOIN, {tags: [["claim", claim]]}),
|
||||
relays: [url],
|
||||
})
|
||||
|
||||
const result = await thunk.result
|
||||
const error = await getThunkError(thunk)
|
||||
|
||||
if (result[url].status === PublishStatus.Failure) {
|
||||
if (error) {
|
||||
const message =
|
||||
connection.auth.message?.replace(/^.*: /, "") ||
|
||||
result[url].message?.replace(/^.*: /, "") ||
|
||||
socket.auth.details?.replace(/^\w+: /, "") ||
|
||||
error?.replace(/^\w+: /, "") ||
|
||||
"join request rejected"
|
||||
|
||||
// If it's a strict NIP 29 relay don't worry about requesting access
|
||||
// TODO: remove this if relay29 ever gets less strict
|
||||
if (message !== "missing group (`h`) tag") {
|
||||
return `Failed to join relay (${message})`
|
||||
}
|
||||
if (message === "missing group (`h`) tag") return
|
||||
|
||||
// Ignore messages about the relay ignoring ours
|
||||
if (error?.startsWith("mute: ")) return
|
||||
|
||||
return message
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,25 +294,30 @@ export const checkRelayProfile = async (url: string) => {
|
||||
}
|
||||
|
||||
export const checkRelayConnection = async (url: string) => {
|
||||
const connection = ctx.net.pool.get(url)
|
||||
const socket = Pool.get().get(url)
|
||||
|
||||
await connection.socket.open()
|
||||
socket.attemptToOpen()
|
||||
|
||||
if (connection.socket.status !== SocketStatus.Open) {
|
||||
await poll({
|
||||
signal: AbortSignal.timeout(3000),
|
||||
condition: () => socket.status === SocketStatus.Open,
|
||||
})
|
||||
|
||||
if (socket.status !== SocketStatus.Open) {
|
||||
return `Failed to connect`
|
||||
}
|
||||
}
|
||||
|
||||
export const checkRelayAuth = async (url: string, timeout = 3000) => {
|
||||
const connection = ctx.net.pool.get(url)
|
||||
const socket = Pool.get().get(url)
|
||||
const okStatuses = [AuthStatus.None, AuthStatus.Ok]
|
||||
|
||||
await connection.auth.attempt(timeout)
|
||||
await socket.auth.attemptAuth(e => signer.get()?.sign(e))
|
||||
|
||||
// Only raise an error if it's not a timeout.
|
||||
// If it is, odds are the problem is with our signer, not the relay
|
||||
if (!okStatuses.includes(connection.auth.status) && connection.auth.message) {
|
||||
return `Failed to authenticate (${connection.auth.message})`
|
||||
if (!okStatuses.includes(socket.auth.status) && socket.auth.details) {
|
||||
return `Failed to authenticate (${socket.auth.details})`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -413,28 +339,6 @@ export const attemptRelayAccess = async (url: string, claim = "") => {
|
||||
|
||||
// Actions
|
||||
|
||||
export const sendWrapped = async ({
|
||||
template,
|
||||
pubkeys,
|
||||
delay,
|
||||
}: {
|
||||
template: EventTemplate
|
||||
pubkeys: string[]
|
||||
delay?: number
|
||||
}) => {
|
||||
const nip59 = Nip59.fromSigner(signer.get()!)
|
||||
|
||||
return publishThunks(
|
||||
await Promise.all(
|
||||
uniq(pubkeys).map(async recipient => ({
|
||||
event: await nip59.wrap(recipient, stamp(template)),
|
||||
relays: ctx.app.router.PubkeyInbox(recipient).getUrls(),
|
||||
delay,
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export const makeDelete = ({event}: {event: TrustedEvent}) => {
|
||||
const tags = [["k", String(event.kind)], ...tagEvent(event)]
|
||||
const groupTag = getTag("h", event.tags)
|
||||
@@ -476,10 +380,12 @@ export const publishReport = ({
|
||||
export type ReactionParams = {
|
||||
event: TrustedEvent
|
||||
content: string
|
||||
tags?: string[][]
|
||||
}
|
||||
|
||||
export const makeReaction = ({event, content}: ReactionParams) => {
|
||||
const tags = tagEventForReaction(event)
|
||||
export const makeReaction = ({content, event, tags: paramTags = []}: ReactionParams) => {
|
||||
const tags = [...paramTags, ...tagEventForReaction(event)]
|
||||
|
||||
const groupTag = getTag("h", event.tags)
|
||||
|
||||
if (groupTag) {
|
||||
@@ -504,3 +410,45 @@ export const makeComment = ({event, content, tags = []}: CommentParams) =>
|
||||
|
||||
export const publishComment = ({relays, ...params}: CommentParams & {relays: string[]}) =>
|
||||
publishThunk({event: makeComment(params), relays})
|
||||
|
||||
export type AlertParams = {
|
||||
feed: Feed
|
||||
cron: string
|
||||
email: string
|
||||
bunker: string
|
||||
secret: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export const makeAlert = async ({cron, email, feed, bunker, secret, description}: AlertParams) => {
|
||||
const tags = [
|
||||
["feed", JSON.stringify(feed)],
|
||||
["cron", cron],
|
||||
["email", email],
|
||||
["locale", LOCALE],
|
||||
["timezone", TIMEZONE],
|
||||
["description", description],
|
||||
["channel", "email"],
|
||||
[
|
||||
"handler",
|
||||
"31990:97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322:1737058597050",
|
||||
"wss://relay.nostr.band/",
|
||||
"web",
|
||||
],
|
||||
]
|
||||
|
||||
if (bunker) {
|
||||
tags.push(["nip46", secret, bunker])
|
||||
}
|
||||
|
||||
return createEvent(ALERT, {
|
||||
content: await signer.get().nip44.encrypt(NOTIFIER_PUBKEY, JSON.stringify(tags)),
|
||||
tags: [
|
||||
["d", randomId()],
|
||||
["p", NOTIFIER_PUBKEY],
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
export const publishAlert = async (params: AlertParams) =>
|
||||
publishThunk({event: await makeAlert(params), relays: [NOTIFIER_RELAY]})
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
<script lang="ts">
|
||||
import {preventDefault} from "@lib/html"
|
||||
import {randomInt, displayList, TIMEZONE, identity} from "@welshman/lib"
|
||||
import {displayRelayUrl, getTagValue, THREAD, MESSAGE, EVENT_TIME, COMMENT} from "@welshman/util"
|
||||
import type {Filter} from "@welshman/util"
|
||||
import type {Nip46ResponseWithResult} from "@welshman/signer"
|
||||
import {makeIntersectionFeed, makeRelayFeed, feedFromFilters} from "@welshman/feeds"
|
||||
import {pubkey} from "@welshman/app"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import InfoBunker from "@app/components/InfoBunker.svelte"
|
||||
import BunkerConnect, {BunkerConnectController} from "@app/components/BunkerConnect.svelte"
|
||||
import {
|
||||
GENERAL,
|
||||
alerts,
|
||||
getMembershipUrls,
|
||||
getMembershipRoomsByUrl,
|
||||
userMembership,
|
||||
} from "@app/state"
|
||||
import {loadAlertStatuses} from "@app/requests"
|
||||
import {publishAlert} from "@app/commands"
|
||||
import {pushToast} from "@app/toast"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
const timezoneOffset = parseInt(TIMEZONE.slice(3)) / 100
|
||||
const minute = randomInt(0, 59)
|
||||
const hour = (17 - timezoneOffset) % 24
|
||||
const WEEKLY = `0 ${minute} ${hour} * * 1`
|
||||
const DAILY = `0 ${minute} ${hour} * * *`
|
||||
|
||||
let loading = false
|
||||
let cron = WEEKLY
|
||||
let email = $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 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 () => {
|
||||
if (!email.includes("@")) {
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: "Please provide an email address",
|
||||
})
|
||||
}
|
||||
|
||||
if (!relay) {
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: "Please select a space",
|
||||
})
|
||||
}
|
||||
|
||||
if (!notifyThreads && !notifyCalendar && !notifyChat) {
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: "Please select something to be notified about",
|
||||
})
|
||||
}
|
||||
|
||||
const filters: Filter[] = []
|
||||
const display: string[] = []
|
||||
|
||||
if (notifyThreads) {
|
||||
display.push("threads")
|
||||
filters.push({kinds: [THREAD]})
|
||||
filters.push({kinds: [COMMENT], "#k": [String(THREAD)]})
|
||||
}
|
||||
|
||||
if (notifyCalendar) {
|
||||
display.push("calendar events")
|
||||
filters.push({kinds: [EVENT_TIME]})
|
||||
filters.push({kinds: [COMMENT], "#k": [String(EVENT_TIME)]})
|
||||
}
|
||||
|
||||
if (notifyChat) {
|
||||
display.push("chat")
|
||||
filters.push({
|
||||
kinds: [MESSAGE],
|
||||
"#h": [GENERAL, ...getMembershipRoomsByUrl(relay, $userMembership)],
|
||||
})
|
||||
}
|
||||
|
||||
loading = true
|
||||
|
||||
try {
|
||||
const cadence = cron?.endsWith("1") ? "Weekly" : "Daily"
|
||||
const description = `${cadence} alert for ${displayList(display)} on ${displayRelayUrl(relay)}, sent via email.`
|
||||
const feed = makeIntersectionFeed(feedFromFilters(filters), makeRelayFeed(relay))
|
||||
const thunk = await publishAlert({cron, email, feed, bunker, secret, description})
|
||||
|
||||
await thunk.result
|
||||
await loadAlertStatuses($pubkey!)
|
||||
|
||||
pushToast({message: "Your alert has been successfully created!"})
|
||||
back()
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<form class="column gap-4" onsubmit={preventDefault(submit)}>
|
||||
<ModalHeader>
|
||||
{#snippet title()}
|
||||
Add an Alert
|
||||
{/snippet}
|
||||
</ModalHeader>
|
||||
{#if showBunker}
|
||||
<div class="card2 flex flex-col items-center gap-4 bg-base-300">
|
||||
<p>Scan using a nostr signer, or click to copy.</p>
|
||||
<BunkerConnect {controller} />
|
||||
<Button class="btn btn-neutral btn-sm" onclick={hideBunker}>Cancel</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Email Address*</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<input placeholder="email@example.com" bind:value={email} />
|
||||
</label>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Frequency*</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<select bind:value={cron} class="select select-bordered">
|
||||
<option value={WEEKLY}>Weekly</option>
|
||||
<option value={DAILY}>Daily</option>
|
||||
</select>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Space*</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<select bind:value={relay} class="select select-bordered">
|
||||
<option value="" disabled selected>Choose a space URL</option>
|
||||
{#each getMembershipUrls($userMembership) as url (url)}
|
||||
<option value={url}>{displayRelayUrl(url)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
<FieldInline>
|
||||
{#snippet label()}
|
||||
<p>Notifications*</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<div class="flex items-center justify-end gap-4">
|
||||
<span class="flex gap-3">
|
||||
<input type="checkbox" class="checkbox" bind:checked={notifyThreads} />
|
||||
Threads
|
||||
</span>
|
||||
<span class="flex gap-3">
|
||||
<input type="checkbox" class="checkbox" bind:checked={notifyCalendar} />
|
||||
Calendar
|
||||
</span>
|
||||
<span class="flex gap-3">
|
||||
<input type="checkbox" class="checkbox" bind:checked={notifyChat} />
|
||||
Chat
|
||||
</span>
|
||||
</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>
|
||||
</div>
|
||||
<p class="text-sm">
|
||||
Required for receiving alerts about spaces with access controls. You can get one from your
|
||||
<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>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon="alt-arrow-left" />
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary" disabled={loading || showBunker}>
|
||||
<Spinner {loading}>Confirm</Spinner>
|
||||
<Icon icon="alt-arrow-right" />
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import Confirm from "@lib/components/Confirm.svelte"
|
||||
import type {Alert} from "@app/state"
|
||||
import {NOTIFIER_RELAY} from "@app/state"
|
||||
import {publishDelete} from "@app/commands"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
||||
type Props = {
|
||||
alert: Alert
|
||||
}
|
||||
|
||||
const {alert}: Props = $props()
|
||||
|
||||
const confirm = () => {
|
||||
publishDelete({event: alert.event, relays: [NOTIFIER_RELAY]})
|
||||
pushToast({message: "Your alert has been deleted!"})
|
||||
history.back()
|
||||
}
|
||||
</script>
|
||||
|
||||
<Confirm {confirm} message="You'll no longer receive messages for this alert." />
|
||||
@@ -0,0 +1,70 @@
|
||||
<script lang="ts">
|
||||
import {parseJson, nthEq} from "@welshman/lib"
|
||||
import {displayFeeds} from "@welshman/feeds"
|
||||
import {getAddress, getTagValue, getTagValues} from "@welshman/util"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import AlertDelete from "@app/components/AlertDelete.svelte"
|
||||
import type {Alert} from "@app/state"
|
||||
import {alertStatuses} from "@app/state"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
type Props = {
|
||||
alert: Alert
|
||||
}
|
||||
|
||||
const {alert}: Props = $props()
|
||||
|
||||
const address = $derived(getAddress(alert.event))
|
||||
const status = $derived($alertStatuses.find(s => s.event.tags.some(nthEq(1, address))))
|
||||
const cron = $derived(getTagValue("cron", alert.tags))
|
||||
const channel = $derived(getTagValue("channel", alert.tags))
|
||||
const feeds = $derived(getTagValues("feed", alert.tags))
|
||||
const description = $derived(
|
||||
getTagValue("description", alert.tags) ||
|
||||
[
|
||||
`${cron?.endsWith("1") ? "Weekly" : "Daily"} alert for events`,
|
||||
displayFeeds(feeds.map(parseJson)),
|
||||
`sent via ${channel}.`,
|
||||
].join(" "),
|
||||
)
|
||||
|
||||
const startDelete = () => pushModal(AlertDelete, {alert})
|
||||
</script>
|
||||
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="flex items-start gap-4">
|
||||
<Button class="py-1" onclick={startDelete}>
|
||||
<Icon icon="trash-bin-2" />
|
||||
</Button>
|
||||
<div class="flex-inline gap-1">{description}</div>
|
||||
</div>
|
||||
{#if status}
|
||||
{@const statusText = getTagValue("status", status.tags) || "error"}
|
||||
{#if statusText === "ok"}
|
||||
<span
|
||||
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)}>
|
||||
Active
|
||||
</span>
|
||||
{:else if statusText === "pending"}
|
||||
<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"
|
||||
data-tip={getTagValue("message", status.tags)}>
|
||||
Pending
|
||||
</span>
|
||||
{:else}
|
||||
<span
|
||||
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)}>
|
||||
{statusText.replace("-", " ").replace(/^(.)/, x => x.toUpperCase())}
|
||||
</span>
|
||||
{/if}
|
||||
{:else}
|
||||
<span
|
||||
class="tooltip tooltip-left cursor-pointer rounded-full border border-solid border-error px-3 py-1 text-sm text-error"
|
||||
data-tip="The notification server did not respond to your request.">
|
||||
Inactive
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {pubkey} from "@welshman/app"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import AlertAdd from "@app/components/AlertAdd.svelte"
|
||||
import AlertItem from "@app/components/AlertItem.svelte"
|
||||
import {loadAlertStatuses, loadAlerts} from "@app/requests"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {alerts} from "@app/state"
|
||||
|
||||
const startAlert = () => pushModal(AlertAdd)
|
||||
|
||||
onMount(() => {
|
||||
loadAlertStatuses($pubkey!)
|
||||
loadAlerts($pubkey!)
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="card2 bg-alt flex flex-col gap-6 shadow-xl">
|
||||
<div class="flex items-center justify-between">
|
||||
<strong class="flex items-center gap-3">
|
||||
<Icon icon="inbox" />
|
||||
Alerts
|
||||
</strong>
|
||||
<Button class="btn btn-primary btn-sm" onclick={startAlert}>
|
||||
<Icon icon="add-circle" />
|
||||
Add Alert
|
||||
</Button>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
{#each $alerts as alert (alert.event.id)}
|
||||
<AlertItem {alert} />
|
||||
{:else}
|
||||
<p class="text-center opacity-75 py-12">No alerts found</p>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type {Snippet} from "svelte"
|
||||
import {page} from "$app/stores"
|
||||
import {pubkey} from "@welshman/app"
|
||||
import Landing from "@app/components/Landing.svelte"
|
||||
@@ -9,6 +10,12 @@
|
||||
import {BURROW_URL} from "@app/state"
|
||||
import {modals, pushModal} from "@app/modal"
|
||||
|
||||
interface Props {
|
||||
children: Snippet
|
||||
}
|
||||
|
||||
const {children}: Props = $props()
|
||||
|
||||
if (BURROW_URL && !$pubkey) {
|
||||
if ($page.url.pathname === "/confirm-email") {
|
||||
pushModal(EmailConfirm, {
|
||||
@@ -29,7 +36,7 @@
|
||||
<div class="flex h-screen overflow-hidden">
|
||||
{#if $pubkey}
|
||||
<PrimaryNav>
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
</PrimaryNav>
|
||||
{:else if !$modals[$page.url.hash.slice(1)]}
|
||||
<Landing />
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
<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">
|
||||
import {onMount, onDestroy} from "svelte"
|
||||
import {slideAndFade} from "@lib/transition"
|
||||
import QRCode from "@app/components/QRCode.svelte"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
||||
type Props = {
|
||||
controller: BunkerConnectController
|
||||
}
|
||||
|
||||
const {controller}: Props = $props()
|
||||
|
||||
onMount(() => {
|
||||
controller.start()
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
controller.stop()
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if controller.url}
|
||||
<div class="flex justify-center" out:slideAndFade>
|
||||
<QRCode code={controller.url} />
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import {pushModal} from "@app/modal"
|
||||
import InfoBunker from "@app/components/InfoBunker.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Field from "@lib/components/Field.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
|
||||
type Props = {
|
||||
bunker: string
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
let {loading, bunker = $bindable("")}: Props = $props()
|
||||
</script>
|
||||
|
||||
<Field>
|
||||
{#snippet label()}
|
||||
<p>Bunker Link*</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon="cpu" />
|
||||
<input disabled={loading} bind:value={bunker} class="grow" placeholder="bunker://" />
|
||||
</label>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<p>
|
||||
A login link provided by a nostr signing app.
|
||||
<Button class="link" onclick={() => pushModal(InfoBunker)}>What is a bunker link?</Button>
|
||||
</p>
|
||||
{/snippet}
|
||||
</Field>
|
||||
@@ -0,0 +1,55 @@
|
||||
<script lang="ts">
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import {pubkey} from "@welshman/app"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
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 CalendarEventEdit from "@app/components/CalendarEventEdit.svelte"
|
||||
import {publishDelete, publishReaction} from "@app/commands"
|
||||
import {makeCalendarPath} from "@app/routes"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
const {
|
||||
url,
|
||||
event,
|
||||
showActivity = false,
|
||||
}: {
|
||||
url: string
|
||||
event: TrustedEvent
|
||||
showActivity?: boolean
|
||||
} = $props()
|
||||
|
||||
const path = makeCalendarPath(url, event.id)
|
||||
|
||||
const editEvent = () => pushModal(CalendarEventEdit, {url, event})
|
||||
|
||||
const deleteReaction = (event: TrustedEvent) => publishDelete({relays: [url], event})
|
||||
|
||||
const createReaction = (template: EventContent) =>
|
||||
publishReaction({...template, event, relays: [url]})
|
||||
</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="Event">
|
||||
{#snippet customActions()}
|
||||
{#if event.pubkey === $pubkey}
|
||||
<li>
|
||||
<Button onclick={editEvent}>
|
||||
<Icon size={4} icon="pen" />
|
||||
Edit Event
|
||||
</Button>
|
||||
</li>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</EventActions>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import CalendarEventForm from "@app/components/CalendarEventForm.svelte"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
}
|
||||
|
||||
const {url}: Props = $props()
|
||||
</script>
|
||||
|
||||
<CalendarEventForm {url}>
|
||||
{#snippet header()}
|
||||
<ModalHeader>
|
||||
{#snippet title()}
|
||||
<div>Create an Event</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<div>Invite other group members to events online or in real life.</div>
|
||||
{/snippet}
|
||||
</ModalHeader>
|
||||
{/snippet}
|
||||
</CalendarEventForm>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import {fromPairs, LOCALE, secondsToDate} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
|
||||
type Props = {
|
||||
event: TrustedEvent
|
||||
}
|
||||
|
||||
const {event}: Props = $props()
|
||||
const meta = $derived(fromPairs(event.tags) as Record<string, string>)
|
||||
const startDate = $derived(secondsToDate(parseInt(meta.start)))
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="hidden h-32 w-32 min-w-32 flex-col items-center justify-center gap-1 rounded-box bg-base-300 p-2 sm:flex">
|
||||
<strong>{Intl.DateTimeFormat(LOCALE, {month: "short"}).format(startDate)}</strong>
|
||||
<span class="text-4xl">{Intl.DateTimeFormat(LOCALE, {day: "numeric"}).format(startDate)}</span>
|
||||
<span class="text-xs opacity-75"
|
||||
>{Intl.DateTimeFormat(LOCALE, {weekday: "long"}).format(startDate)}</span>
|
||||
</div>
|
||||
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {getTagValue} from "@welshman/util"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import CalendarEventForm from "@app/components/CalendarEventForm.svelte"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
event: TrustedEvent
|
||||
}
|
||||
|
||||
const {url, event}: Props = $props()
|
||||
|
||||
const initialValues = {
|
||||
d: getTagValue("d", event.tags)!,
|
||||
title: getTagValue("title", event.tags)!,
|
||||
location: getTagValue("location", event.tags)!,
|
||||
start: parseInt(getTagValue("start", event.tags)!),
|
||||
end: parseInt(getTagValue("end", event.tags)!),
|
||||
content: event.content,
|
||||
}
|
||||
</script>
|
||||
|
||||
<CalendarEventForm {url} {initialValues}>
|
||||
{#snippet header()}
|
||||
<ModalHeader>
|
||||
{#snippet title()}
|
||||
<div>Edit this Event</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<div>Invite other group members to events online or in real life.</div>
|
||||
{/snippet}
|
||||
</ModalHeader>
|
||||
{/snippet}
|
||||
</CalendarEventForm>
|
||||
@@ -0,0 +1,171 @@
|
||||
<script lang="ts">
|
||||
import type {Snippet} from "svelte"
|
||||
import {writable} from "svelte/store"
|
||||
import {randomId, HOUR} from "@welshman/lib"
|
||||
import {createEvent, EVENT_TIME} from "@welshman/util"
|
||||
import {publishThunk} from "@welshman/app"
|
||||
import {preventDefault} from "@lib/html"
|
||||
import {daysBetween} from "@lib/util"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Field from "@lib/components/Field.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import DateTimeInput from "@lib/components/DateTimeInput.svelte"
|
||||
import EditorContent from "@app/editor/EditorContent.svelte"
|
||||
import {PROTECTED, GENERAL, tagRoom} from "@app/state"
|
||||
import {makeEditor} from "@app/editor"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
header: Snippet
|
||||
initialValues?: {
|
||||
d: string
|
||||
title: string
|
||||
content: string
|
||||
location: string
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
}
|
||||
|
||||
const {url, header, initialValues}: Props = $props()
|
||||
|
||||
const uploading = writable(false)
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const selectFiles = () => editor.then(ed => ed.chain().selectFiles().run())
|
||||
|
||||
const submit = async () => {
|
||||
if ($uploading) return
|
||||
|
||||
if (!title) {
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: "Please provide a title.",
|
||||
})
|
||||
}
|
||||
|
||||
if (!start || !end) {
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: "Please provide start and end times.",
|
||||
})
|
||||
}
|
||||
|
||||
if (start >= end) {
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: "End time must be later than start time.",
|
||||
})
|
||||
}
|
||||
|
||||
const ed = await editor
|
||||
const event = createEvent(EVENT_TIME, {
|
||||
content: ed.getText({blockSeparator: "\n"}).trim(),
|
||||
tags: [
|
||||
["d", initialValues?.d || randomId()],
|
||||
["title", title],
|
||||
["location", location || ""],
|
||||
["start", start.toString()],
|
||||
["end", end.toString()],
|
||||
...daysBetween(start, end).map(D => ["D", D.toString()]),
|
||||
...ed.storage.nostr.getEditorTags(),
|
||||
tagRoom(GENERAL, url),
|
||||
PROTECTED,
|
||||
],
|
||||
})
|
||||
|
||||
pushToast({message: "Your event has been saved!"})
|
||||
publishThunk({event, relays: [url]})
|
||||
history.back()
|
||||
}
|
||||
|
||||
const content = initialValues?.content || ""
|
||||
const editor = makeEditor({url, submit, uploading, content})
|
||||
|
||||
let title = $state(initialValues?.title || "")
|
||||
let location = $state(initialValues?.location || "")
|
||||
let start: number | undefined = $state(initialValues?.start)
|
||||
let end: number | undefined = $state(initialValues?.end)
|
||||
let endDirty = Boolean(initialValues?.end)
|
||||
|
||||
$effect(() => {
|
||||
if (!endDirty && start) {
|
||||
end = start + HOUR
|
||||
} else if (end) {
|
||||
endDirty = true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<form novalidate class="column gap-4" onsubmit={preventDefault(submit)}>
|
||||
{@render header()}
|
||||
<Field>
|
||||
{#snippet label()}
|
||||
<p>Title*</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<input bind:value={title} class="grow" type="text" />
|
||||
</label>
|
||||
{/snippet}
|
||||
</Field>
|
||||
<Field>
|
||||
{#snippet label()}
|
||||
<p>Summary</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<div class="relative z-feature flex gap-2 border-t border-solid border-base-100 bg-base-100">
|
||||
<div class="input-editor flex-grow overflow-hidden">
|
||||
<EditorContent {editor} />
|
||||
</div>
|
||||
<Button data-tip="Add an image" class="center btn tooltip" onclick={selectFiles}>
|
||||
{#if $uploading}
|
||||
<span class="loading loading-spinner loading-xs"></span>
|
||||
{:else}
|
||||
<Icon icon="gallery-send" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Field>
|
||||
<Field>
|
||||
{#snippet label()}
|
||||
Start*
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<DateTimeInput bind:value={start} />
|
||||
{/snippet}
|
||||
</Field>
|
||||
<Field>
|
||||
{#snippet label()}
|
||||
End*
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<DateTimeInput bind:value={end} />
|
||||
{/snippet}
|
||||
</Field>
|
||||
<Field>
|
||||
{#snippet label()}
|
||||
<p>Location (optional)</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon="map-point" />
|
||||
<input bind:value={location} class="grow" type="text" />
|
||||
</label>
|
||||
{/snippet}
|
||||
</Field>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon="alt-arrow-left" />
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary" disabled={$uploading}>
|
||||
<Spinner loading={$uploading}>Save Event</Spinner>
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
fromPairs,
|
||||
formatTimestamp,
|
||||
formatTimestampAsDate,
|
||||
formatTimestampAsTime,
|
||||
} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
|
||||
type Props = {
|
||||
event: TrustedEvent
|
||||
}
|
||||
|
||||
const {event}: Props = $props()
|
||||
const meta = $derived(fromPairs(event.tags) as Record<string, string>)
|
||||
const start = $derived(parseInt(meta.start))
|
||||
const end = $derived(parseInt(meta.end))
|
||||
const startDateDisplay = $derived(formatTimestampAsDate(start))
|
||||
const endDateDisplay = $derived(formatTimestampAsDate(end))
|
||||
const isSingleDay = $derived(startDateDisplay === endDateDisplay)
|
||||
</script>
|
||||
|
||||
<div class="flex flex-grow flex-wrap justify-between gap-2">
|
||||
<p class="text-xl">{meta.title || meta.name}</p>
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<Icon icon="clock-circle" size={4} />
|
||||
<span class="sm:hidden">{formatTimestampAsDate(start)}</span>
|
||||
{formatTimestampAsTime(start)} — {isSingleDay
|
||||
? formatTimestampAsTime(end)
|
||||
: formatTimestamp(end)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import CalendarEventActions from "@app/components/CalendarEventActions.svelte"
|
||||
import CalendarEventHeader from "@app/components/CalendarEventHeader.svelte"
|
||||
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||
import {makeCalendarPath} from "@app/routes"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
event: TrustedEvent
|
||||
}
|
||||
|
||||
const {url, event}: Props = $props()
|
||||
</script>
|
||||
|
||||
<Link class="col-3 card2 bg-alt w-full cursor-pointer" href={makeCalendarPath(url, event.id)}>
|
||||
<CalendarEventHeader {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>
|
||||
<CalendarEventActions showActivity {url} {event} />
|
||||
</div>
|
||||
</Link>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import {fromPairs} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||
|
||||
type Props = {
|
||||
event: TrustedEvent
|
||||
url: string
|
||||
}
|
||||
|
||||
const {event, url}: Props = $props()
|
||||
const meta = $derived(fromPairs(event.tags) as Record<string, string>)
|
||||
</script>
|
||||
|
||||
<div class="flex min-w-0 flex-col gap-1 text-sm opacity-75">
|
||||
<span class="flex items-center gap-1">
|
||||
<Icon icon="user-circle" size={4} />
|
||||
Posted by <ProfileLink pubkey={event.pubkey} {url} />
|
||||
</span>
|
||||
{#if meta.location}
|
||||
<span class="flex items-start gap-1">
|
||||
<Icon icon="map-point" class="mt-[2px]" size={4} />
|
||||
<span class="break-words">{meta.location}</span>
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,49 +1,50 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {writable} from "svelte/store"
|
||||
import {EditorContent} from "svelte-tiptap"
|
||||
import {isMobile} from "@lib/html"
|
||||
import type {EventContent} from "@welshman/util"
|
||||
import {isMobile, preventDefault} from "@lib/html"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import EditorContent from "@app/editor/EditorContent.svelte"
|
||||
import {makeEditor} from "@app/editor"
|
||||
|
||||
export let onSubmit: any
|
||||
export let content = ""
|
||||
type Props = {
|
||||
url?: string
|
||||
onSubmit: (event: EventContent) => void
|
||||
}
|
||||
|
||||
export const focus = () => $editor.chain().focus().run()
|
||||
const {onSubmit, url}: Props = $props()
|
||||
|
||||
const autofocus = !isMobile
|
||||
|
||||
const uploading = writable(false)
|
||||
|
||||
const uploadFiles = () => $editor!.chain().selectFiles().run()
|
||||
export const focus = () => editor.then(ed => ed.chain().focus().run())
|
||||
|
||||
const submit = () => {
|
||||
const uploadFiles = () => editor.then(ed => ed.chain().selectFiles().run())
|
||||
|
||||
const submit = async () => {
|
||||
if ($uploading) return
|
||||
|
||||
const content = $editor!.getText({blockSeparator: "\n"}).trim()
|
||||
const tags = $editor!.storage.nostr.getEditorTags()
|
||||
const ed = await editor
|
||||
const content = ed.getText({blockSeparator: "\n"}).trim()
|
||||
const tags = ed.storage.nostr.getEditorTags()
|
||||
|
||||
if (!content) return
|
||||
|
||||
onSubmit({content, tags})
|
||||
|
||||
$editor!.chain().clearContent().run()
|
||||
ed.chain().clearContent().run()
|
||||
}
|
||||
|
||||
const editor = makeEditor({autofocus: !isMobile, submit, uploading, aggressive: true})
|
||||
|
||||
onMount(() => {
|
||||
$editor!.chain().setContent(content).run()
|
||||
})
|
||||
const editor = makeEditor({url, autofocus, submit, uploading, aggressive: true})
|
||||
</script>
|
||||
|
||||
<form
|
||||
class="relative z-feature flex gap-2 p-2"
|
||||
on:submit|preventDefault={$uploading ? undefined : 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}
|
||||
on:click={uploadFiles}>
|
||||
onclick={uploadFiles}>
|
||||
{#if $uploading}
|
||||
<span class="loading loading-spinner loading-xs"></span>
|
||||
{:else}
|
||||
@@ -51,13 +52,13 @@
|
||||
{/if}
|
||||
</Button>
|
||||
<div class="chat-editor flex-grow overflow-hidden">
|
||||
<EditorContent editor={$editor} />
|
||||
<EditorContent {editor} />
|
||||
</div>
|
||||
<Button
|
||||
data-tip="{window.navigator.platform.includes('Mac') ? 'cmd' : 'ctrl'}+enter to send"
|
||||
class="center tooltip tooltip-left absolute right-4 h-10 w-10 min-w-10 rounded-full"
|
||||
disabled={$uploading}
|
||||
on:click={submit}>
|
||||
onclick={submit}>
|
||||
<Icon icon="plain" />
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
@@ -4,20 +4,32 @@
|
||||
import {slide} from "@lib/transition"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Content from "@app/components/Content.svelte"
|
||||
import NoteContent from "@app/components/NoteContent.svelte"
|
||||
|
||||
export let event: TrustedEvent
|
||||
export let clear: () => void
|
||||
const {
|
||||
verb,
|
||||
event,
|
||||
clear,
|
||||
}: {
|
||||
verb: string
|
||||
event: TrustedEvent
|
||||
clear: () => void
|
||||
} = $props()
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="relative border-l-2 border-solid border-primary bg-base-300 px-2 py-1 pr-8 text-xs"
|
||||
transition:slide>
|
||||
<p class="text-primary">Replying to @{displayProfileByPubkey(event.pubkey)}</p>
|
||||
<p class="text-primary">{verb} @{displayProfileByPubkey(event.pubkey)}</p>
|
||||
{#key event.id}
|
||||
<Content {event} hideMedia minLength={100} maxLength={300} expandMode="disabled" />
|
||||
<NoteContent
|
||||
{event}
|
||||
hideMediaAtDepth={0}
|
||||
minLength={100}
|
||||
maxLength={300}
|
||||
expandMode="disabled" />
|
||||
{/key}
|
||||
<Button class="absolute right-2 top-2 cursor-pointer" on:click={clear}>
|
||||
<Button class="absolute right-2 top-2 cursor-pointer" onclick={clear}>
|
||||
<Icon icon="close-circle" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
<script lang="ts">
|
||||
import {hash} from "@welshman/lib"
|
||||
import {now} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {
|
||||
thunks,
|
||||
pubkey,
|
||||
deriveProfile,
|
||||
deriveProfileDisplay,
|
||||
formatTimestampAsDate,
|
||||
formatTimestampAsTime,
|
||||
} from "@welshman/app"
|
||||
import {hash, now, formatTimestampAsTime, formatTimestampAsDate} from "@welshman/lib"
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import {thunks, deriveProfile, deriveProfileDisplay} from "@welshman/app"
|
||||
import {isMobile} from "@lib/html"
|
||||
import LongPress from "@lib/components/LongPress.svelte"
|
||||
import TapTarget from "@lib/components/TapTarget.svelte"
|
||||
import Avatar from "@lib/components/Avatar.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
@@ -26,51 +18,51 @@
|
||||
import {publishDelete, publishReaction} from "@app/commands"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
export let url, room
|
||||
export let event: TrustedEvent
|
||||
export let replyTo: any = undefined
|
||||
export let showPubkey = false
|
||||
export let inert = false
|
||||
interface Props {
|
||||
url: string
|
||||
room: string
|
||||
event: TrustedEvent
|
||||
replyTo?: (event: TrustedEvent) => void
|
||||
showPubkey?: boolean
|
||||
inert?: boolean
|
||||
}
|
||||
|
||||
const {url, room, event, replyTo = undefined, showPubkey = false, inert = false}: Props = $props()
|
||||
|
||||
const thunk = $thunks[event.id]
|
||||
const today = formatTimestampAsDate(now())
|
||||
const profile = deriveProfile(event.pubkey)
|
||||
const profileDisplay = deriveProfileDisplay(event.pubkey)
|
||||
const profile = deriveProfile(event.pubkey, [url])
|
||||
const profileDisplay = deriveProfileDisplay(event.pubkey, [url])
|
||||
const [_, colorValue] = colors[parseInt(hash(event.pubkey)) % colors.length]
|
||||
|
||||
const reply = () => replyTo(event)
|
||||
const reply = () => replyTo!(event)
|
||||
|
||||
const onLongPress = () => pushModal(ChannelMessageMenuMobile, {url, event, reply})
|
||||
const onTap = () => pushModal(ChannelMessageMenuMobile, {url, event, reply})
|
||||
|
||||
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey})
|
||||
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey, url})
|
||||
|
||||
const onReactionClick = (content: string, events: TrustedEvent[]) => {
|
||||
const reaction = events.find(e => e.pubkey === $pubkey)
|
||||
const deleteReaction = (event: TrustedEvent) => publishDelete({relays: [url], event})
|
||||
|
||||
if (reaction) {
|
||||
publishDelete({relays: [url], event: reaction})
|
||||
} else {
|
||||
publishReaction({event, content, relays: [url]})
|
||||
}
|
||||
}
|
||||
const createReaction = (template: EventContent) =>
|
||||
publishReaction({...template, event, relays: [url]})
|
||||
</script>
|
||||
|
||||
<LongPress
|
||||
<TapTarget
|
||||
data-event={event.id}
|
||||
onLongPress={inert ? null : onLongPress}
|
||||
onTap={inert ? null : onTap}
|
||||
class="group relative flex w-full cursor-default flex-col p-2 pb-3 text-left">
|
||||
<div class="flex w-full gap-3 overflow-auto">
|
||||
{#if showPubkey}
|
||||
<Button on:click={openProfile} class="flex items-start">
|
||||
<Button onclick={openProfile} class="flex items-start">
|
||||
<Avatar src={$profile?.picture} class="border border-solid border-base-content" size={8} />
|
||||
</Button>
|
||||
{:else}
|
||||
<div class="w-8 min-w-8 max-w-8" />
|
||||
<div class="w-8 min-w-8 max-w-8"></div>
|
||||
{/if}
|
||||
<div class="min-w-0 flex-grow pr-1">
|
||||
{#if showPubkey}
|
||||
<div class="flex items-center gap-2">
|
||||
<Button on:click={openProfile} class="text-sm font-bold" style="color: {colorValue}">
|
||||
<Button onclick={openProfile} class="text-sm font-bold" style="color: {colorValue}">
|
||||
{$profileDisplay}
|
||||
</Button>
|
||||
<span class="text-xs opacity-50">
|
||||
@@ -84,7 +76,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
<div class="text-sm">
|
||||
<Content {event} quoteProps={{minimal: true, relays: [url]}} />
|
||||
<Content {event} {url} />
|
||||
{#if thunk}
|
||||
<ThunkStatus {thunk} class="mt-2" />
|
||||
{/if}
|
||||
@@ -92,18 +84,24 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-2 ml-10 mt-1">
|
||||
<ReactionSummary {url} {event} {onReactionClick} reactionClass="tooltip-right" />
|
||||
<ReactionSummary
|
||||
{url}
|
||||
{event}
|
||||
{deleteReaction}
|
||||
{createReaction}
|
||||
reactionClass="tooltip-right" />
|
||||
</div>
|
||||
<button
|
||||
class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all"
|
||||
class:group-hover:opacity-100={!isMobile}
|
||||
on:click|stopPropagation>
|
||||
<ChannelMessageEmojiButton {url} {room} {event} />
|
||||
{#if replyTo}
|
||||
<Button class="btn join-item btn-xs" on:click={reply}>
|
||||
<Icon icon="reply" size={4} />
|
||||
</Button>
|
||||
{/if}
|
||||
<ChannelMessageMenuButton {url} {event} />
|
||||
</button>
|
||||
</LongPress>
|
||||
{#if !isMobile}
|
||||
<button
|
||||
class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all"
|
||||
class:group-hover:opacity-100={!isMobile}>
|
||||
<ChannelMessageEmojiButton {url} {room} {event} />
|
||||
{#if replyTo}
|
||||
<Button class="btn join-item btn-xs" onclick={reply}>
|
||||
<Icon icon="reply" size={4} />
|
||||
</Button>
|
||||
{/if}
|
||||
<ChannelMessageMenuButton {url} {event} />
|
||||
</button>
|
||||
{/if}
|
||||
</TapTarget>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import {publishReaction} from "@app/commands"
|
||||
|
||||
export let url, room, event
|
||||
const {url, room, event} = $props()
|
||||
|
||||
// Tell svelte-check to shut up
|
||||
noop(room)
|
||||
|
||||
@@ -4,12 +4,10 @@
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import EventInfo from "@app/components/EventInfo.svelte"
|
||||
import EventReport from "@app/components/EventReport.svelte"
|
||||
import ConfirmDelete from "@app/components/ConfirmDelete.svelte"
|
||||
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
export let url
|
||||
export let event
|
||||
export let onClick
|
||||
const {url, event, onClick} = $props()
|
||||
|
||||
const report = () => {
|
||||
onClick()
|
||||
@@ -18,32 +16,32 @@
|
||||
|
||||
const showInfo = () => {
|
||||
onClick()
|
||||
pushModal(EventInfo, {event})
|
||||
pushModal(EventInfo, {url, event})
|
||||
}
|
||||
|
||||
const showDelete = () => {
|
||||
onClick()
|
||||
pushModal(ConfirmDelete, {url, event})
|
||||
pushModal(EventDeleteConfirm, {url, event})
|
||||
}
|
||||
</script>
|
||||
|
||||
<ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-xl">
|
||||
<li>
|
||||
<Button on:click={showInfo}>
|
||||
<Button onclick={showInfo}>
|
||||
<Icon size={4} icon="code-2" />
|
||||
Message Details
|
||||
</Button>
|
||||
</li>
|
||||
{#if event.pubkey === $pubkey}
|
||||
<li>
|
||||
<Button on:click={showDelete} class="text-error">
|
||||
<Button onclick={showDelete} class="text-error">
|
||||
<Icon size={4} icon="trash-bin-2" />
|
||||
Delete Message
|
||||
</Button>
|
||||
</li>
|
||||
{:else}
|
||||
<li>
|
||||
<Button class="text-error" on:click={report}>
|
||||
<Button class="text-error" onclick={report}>
|
||||
<Icon size={4} icon="danger" />
|
||||
Report Content
|
||||
</Button>
|
||||
|
||||
@@ -6,27 +6,29 @@
|
||||
import Tippy from "@lib/components/Tippy.svelte"
|
||||
import ChannelMessageMenu from "@app/components/ChannelMessageMenu.svelte"
|
||||
|
||||
export let url, event
|
||||
const {url, event} = $props()
|
||||
|
||||
const open = () => popover.show()
|
||||
const open = () => popover?.show()
|
||||
|
||||
const onClick = () => popover.hide()
|
||||
const onClick = () => popover?.hide()
|
||||
|
||||
const onMouseMove = ({clientX, clientY}: any) => {
|
||||
const {x, y, width, height} = popover.popper.getBoundingClientRect()
|
||||
if (popover) {
|
||||
const {x, y, width, height} = popover.popper.getBoundingClientRect()
|
||||
|
||||
if (!between([x, x + width], clientX) || !between([y, y + height + 30], clientY)) {
|
||||
popover.hide()
|
||||
if (!between([x, x + width], clientX) || !between([y, y + height + 30], clientY)) {
|
||||
popover.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let popover: Instance
|
||||
let popover: Instance | undefined = $state()
|
||||
</script>
|
||||
|
||||
<svelte:document on:mousemove={onMouseMove} />
|
||||
<svelte:document onmousemove={onMouseMove} />
|
||||
|
||||
<div class="flex">
|
||||
<Button class="btn join-item btn-xs" on:click={open}>
|
||||
<Button class="btn join-item btn-xs" onclick={open}>
|
||||
<Icon icon="menu-dots" size={4} />
|
||||
</Button>
|
||||
<Tippy
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
<script lang="ts">
|
||||
import type {NativeEmoji} from "emoji-picker-element/shared"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {pubkey} from "@welshman/app"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import EmojiPicker from "@lib/components/EmojiPicker.svelte"
|
||||
import EventInfo from "@app/components/EventInfo.svelte"
|
||||
import ConfirmDelete from "@app/components/ConfirmDelete.svelte"
|
||||
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
||||
import {publishReaction} from "@app/commands"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
export let url
|
||||
export let event
|
||||
export let reply
|
||||
type Props = {
|
||||
url: string
|
||||
event: TrustedEvent
|
||||
reply: () => void
|
||||
}
|
||||
|
||||
const onEmoji = (emoji: NativeEmoji) => {
|
||||
const {url, event, reply}: Props = $props()
|
||||
|
||||
const onEmoji = ((event: TrustedEvent, url: string, emoji: NativeEmoji) => {
|
||||
history.back()
|
||||
publishReaction({event, relays: [url], content: emoji.unicode})
|
||||
}
|
||||
}).bind(undefined, event, url)
|
||||
|
||||
const showEmojiPicker = () => pushModal(EmojiPicker, {onClick: onEmoji}, {replaceState: true})
|
||||
|
||||
@@ -25,26 +30,26 @@
|
||||
reply()
|
||||
}
|
||||
|
||||
const showInfo = () => pushModal(EventInfo, {event}, {replaceState: true})
|
||||
const showInfo = () => pushModal(EventInfo, {url, event}, {replaceState: true})
|
||||
|
||||
const showDelete = () => pushModal(ConfirmDelete, {url, event})
|
||||
const showDelete = () => pushModal(EventDeleteConfirm, {url, event})
|
||||
</script>
|
||||
|
||||
<div class="col-2">
|
||||
<Button class="btn btn-primary w-full" on:click={showEmojiPicker}>
|
||||
<Button class="btn btn-primary w-full" onclick={showEmojiPicker}>
|
||||
<Icon size={4} icon="smile-circle" />
|
||||
Send Reaction
|
||||
</Button>
|
||||
<Button class="btn btn-neutral w-full" on:click={sendReply}>
|
||||
<Button class="btn btn-neutral w-full" onclick={sendReply}>
|
||||
<Icon size={4} icon="reply" />
|
||||
Send Reply
|
||||
</Button>
|
||||
<Button class="btn btn-neutral" on:click={showInfo}>
|
||||
<Button class="btn btn-neutral" onclick={showInfo}>
|
||||
<Icon size={4} icon="code-2" />
|
||||
Message Details
|
||||
</Button>
|
||||
{#if event.pubkey === $pubkey}
|
||||
<Button class="btn btn-neutral text-error" on:click={showDelete}>
|
||||
<Button class="btn btn-neutral text-error" onclick={showDelete}>
|
||||
<Icon size={4} icon="trash-bin-2" />
|
||||
Delete Message
|
||||
</Button>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<script lang="ts">
|
||||
import {GENERAL, channelsById, makeChannelId} from "@app/state"
|
||||
|
||||
export let url
|
||||
export let room
|
||||
const {url, room} = $props()
|
||||
</script>
|
||||
|
||||
{#if room === GENERAL}
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
<script lang="ts" context="module">
|
||||
type Element = {
|
||||
id: string
|
||||
type: "date" | "note"
|
||||
value: string | TrustedEvent
|
||||
showPubkey: boolean
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type {Snippet} from "svelte"
|
||||
import {onMount} from "svelte"
|
||||
import {derived} from "svelte/store"
|
||||
import {int, nthNe, MINUTE, sortBy, remove} from "@welshman/lib"
|
||||
import {int, nthNe, MINUTE, sortBy, remove, formatTimestampAsDate} from "@welshman/lib"
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import {createEvent, DIRECT_MESSAGE, INBOX_RELAYS} from "@welshman/util"
|
||||
import {pubkey, formatTimestampAsDate, inboxRelaySelectionsByPubkey, load} from "@welshman/app"
|
||||
import {
|
||||
pubkey,
|
||||
tagPubkey,
|
||||
sendWrapped,
|
||||
loadUsingOutbox,
|
||||
inboxRelaySelectionsByPubkey,
|
||||
} from "@welshman/app"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import PageBar from "@lib/components/PageBar.svelte"
|
||||
import PageContent from "@lib/components/PageContent.svelte"
|
||||
import Divider from "@lib/components/Divider.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import ProfileName from "@app/components/ProfileName.svelte"
|
||||
@@ -26,57 +24,63 @@
|
||||
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||
import ProfileList from "@app/components/ProfileList.svelte"
|
||||
import ChatMessage from "@app/components/ChatMessage.svelte"
|
||||
import ChatCompose from "@app/components/ChannelCompose.svelte"
|
||||
import ChatComposeParent from "@app/components/ChannelComposeParent.svelte"
|
||||
import {userSettingValues, deriveChat, splitChatId, PLATFORM_NAME} from "@app/state"
|
||||
import ChatCompose from "@app/components/ChatCompose.svelte"
|
||||
import ChatComposeParent from "@app/components/ChatComposeParent.svelte"
|
||||
import {
|
||||
INDEXER_RELAYS,
|
||||
userSettingValues,
|
||||
deriveChat,
|
||||
splitChatId,
|
||||
PLATFORM_NAME,
|
||||
} from "@app/state"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {sendWrapped, prependParent} from "@app/commands"
|
||||
import {prependParent} from "@app/commands"
|
||||
|
||||
export let id
|
||||
type Props = {
|
||||
id: string
|
||||
info?: Snippet
|
||||
}
|
||||
|
||||
const {id, info}: Props = $props()
|
||||
|
||||
const chat = deriveChat(id)
|
||||
const pubkeys = splitChatId(id)
|
||||
const others = remove($pubkey!, pubkeys)
|
||||
const missingInboxes = derived(inboxRelaySelectionsByPubkey, $m =>
|
||||
pubkeys.filter(pk => !$m.has(pk)),
|
||||
)
|
||||
|
||||
const assertEvent = (e: any) => e as TrustedEvent
|
||||
|
||||
const assertNotNil = <T,>(x: T | undefined) => x!
|
||||
const missingInboxes = $derived(pubkeys.filter(pk => !$inboxRelaySelectionsByPubkey.has(pk)))
|
||||
|
||||
const showMembers = () =>
|
||||
pushModal(ProfileList, {pubkeys: others, title: `People in this conversation`})
|
||||
|
||||
const replyTo = (event: TrustedEvent) => {
|
||||
parent = event
|
||||
compose.focus()
|
||||
compose?.focus()
|
||||
}
|
||||
|
||||
const clearParent = () => {
|
||||
parent = undefined
|
||||
}
|
||||
|
||||
const onSubmit = async ({content, tags}: EventContent) => {
|
||||
const onSubmit = async (params: EventContent) => {
|
||||
// Remove p tags since they result in forking the conversation
|
||||
const tags = [...params.tags.filter(nthNe(0, "p")), ...remove($pubkey!, pubkeys).map(tagPubkey)]
|
||||
|
||||
await sendWrapped({
|
||||
pubkeys,
|
||||
template: createEvent(
|
||||
DIRECT_MESSAGE,
|
||||
prependParent(parent, {content, tags: tags.filter(nthNe(0, "p"))}),
|
||||
),
|
||||
template: createEvent(DIRECT_MESSAGE, prependParent(parent, {...params, tags})),
|
||||
delay: $userSettingValues.send_delay,
|
||||
})
|
||||
|
||||
clearParent()
|
||||
}
|
||||
|
||||
let loading = true
|
||||
let parent: TrustedEvent | undefined
|
||||
let elements: Element[] = []
|
||||
let compose: ChatCompose
|
||||
let loading = $state(true)
|
||||
let compose: ChatCompose | undefined = $state()
|
||||
let parent: TrustedEvent | undefined = $state()
|
||||
let chatCompose: HTMLElement | undefined = $state()
|
||||
let dynamicPadding: HTMLElement | undefined = $state()
|
||||
|
||||
$: {
|
||||
elements = []
|
||||
const elements = $derived.by(() => {
|
||||
const elements = []
|
||||
|
||||
let previousDate
|
||||
let previousPubkey
|
||||
@@ -102,12 +106,32 @@
|
||||
previousCreatedAt = created_at
|
||||
}
|
||||
|
||||
elements.reverse()
|
||||
}
|
||||
return elements.reverse()
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
// Don't use loadInboxRelaySelection because we want to force reload
|
||||
load({filters: [{kinds: [INBOX_RELAYS], authors: others}]})
|
||||
for (const pubkey of others) {
|
||||
loadUsingOutbox({
|
||||
pubkey,
|
||||
kind: INBOX_RELAYS,
|
||||
relays: INDEXER_RELAYS,
|
||||
})
|
||||
}
|
||||
|
||||
const observer = new ResizeObserver(() => {
|
||||
if (dynamicPadding && chatCompose) {
|
||||
dynamicPadding.style.minHeight = `${chatCompose.offsetHeight}px`
|
||||
}
|
||||
})
|
||||
|
||||
observer.observe(chatCompose!)
|
||||
observer.observe(dynamicPadding!)
|
||||
|
||||
return () => {
|
||||
observer.unobserve(chatCompose!)
|
||||
observer.unobserve(dynamicPadding!)
|
||||
}
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
@@ -115,88 +139,116 @@
|
||||
}, 5000)
|
||||
</script>
|
||||
|
||||
<div class="relative flex h-full w-full flex-col">
|
||||
{#if others.length > 0}
|
||||
<PageBar>
|
||||
<div slot="title" class="flex flex-col gap-1 sm:flex-row sm:gap-2">
|
||||
{#if others.length === 1}
|
||||
{@const pubkey = others[0]}
|
||||
{@const onClick = () => pushModal(ProfileDetail, {pubkey})}
|
||||
<Button on:click={onClick} class="row-2">
|
||||
<ProfileCircle {pubkey} size={5} />
|
||||
<ProfileName {pubkey} />
|
||||
</Button>
|
||||
{:else}
|
||||
<div class="flex items-center gap-2">
|
||||
<ProfileCircles pubkeys={others} size={5} />
|
||||
<p class="overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<ProfileName pubkey={others[0]} />
|
||||
and
|
||||
{#if others.length === 2}
|
||||
<ProfileName pubkey={others[1]} />
|
||||
{:else}
|
||||
{others.length - 1}
|
||||
{others.length > 2 ? "others" : "other"}
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
{#if others.length > 2}
|
||||
<Button on:click={showMembers} class="btn btn-link hidden sm:block"
|
||||
>Show all members</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
<div slot="action">
|
||||
{#if remove($pubkey, $missingInboxes).length > 0}
|
||||
{@const count = remove($pubkey, $missingInboxes).length}
|
||||
{@const label = count > 1 ? "inboxes are" : "inbox is"}
|
||||
<div
|
||||
class="row-2 badge badge-error badge-lg tooltip tooltip-left cursor-pointer"
|
||||
data-tip="{count} {label} not configured.">
|
||||
<Icon icon="danger" />
|
||||
{count}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</PageBar>
|
||||
{/if}
|
||||
<div class="-mt-2 flex flex-grow flex-col-reverse overflow-auto py-2">
|
||||
{#if $missingInboxes.includes(assertNotNil($pubkey))}
|
||||
<div class="py-12">
|
||||
<div class="card2 col-2 m-auto max-w-md items-center text-center">
|
||||
<p class="row-2 text-lg text-error">
|
||||
<Icon icon="danger" />
|
||||
Your inbox is not configured.
|
||||
</p>
|
||||
<p>
|
||||
In order to deliver messages, {PLATFORM_NAME} needs to know where to send them. Please visit
|
||||
your <Link class="link" href="/settings/relays">relay settings page</Link> to set up your
|
||||
inbox.
|
||||
<PageBar>
|
||||
{#snippet title()}
|
||||
<div class="flex flex-col gap-1 sm:flex-row sm:gap-2">
|
||||
{#if others.length === 0}
|
||||
<div class="row-2">
|
||||
<ProfileCircle pubkey={$pubkey!} size={5} />
|
||||
<ProfileName pubkey={$pubkey!} />
|
||||
</div>
|
||||
{:else if others.length === 1}
|
||||
{@const pubkey = others[0]}
|
||||
{@const onClick = () => pushModal(ProfileDetail, {pubkey})}
|
||||
<Button onclick={onClick} class="row-2">
|
||||
<ProfileCircle {pubkey} size={5} />
|
||||
<ProfileName {pubkey} />
|
||||
</Button>
|
||||
{:else}
|
||||
<div class="flex items-center gap-2">
|
||||
<ProfileCircles pubkeys={others} size={5} />
|
||||
<p class="overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<ProfileName pubkey={others[0]} />
|
||||
and
|
||||
{#if others.length === 2}
|
||||
<ProfileName pubkey={others[1]} />
|
||||
{:else}
|
||||
{others.length - 1}
|
||||
{others.length > 2 ? "others" : "other"}
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#each elements as { type, id, value, showPubkey } (id)}
|
||||
{#if type === "date"}
|
||||
<Divider>{value}</Divider>
|
||||
{:else}
|
||||
<ChatMessage event={assertEvent(value)} {pubkeys} {showPubkey} {replyTo} />
|
||||
{/if}
|
||||
{/each}
|
||||
<p
|
||||
class="m-auto flex h-10 max-w-sm flex-col items-center justify-center gap-4 py-20 text-center">
|
||||
<Spinner {loading}>
|
||||
{#if loading}
|
||||
Looking for messages...
|
||||
{:else}
|
||||
End of message history
|
||||
{#if others.length > 2}
|
||||
<Button onclick={showMembers} class="btn btn-link hidden sm:block"
|
||||
>Show all members</Button>
|
||||
{/if}
|
||||
</Spinner>
|
||||
<slot name="info" />
|
||||
</p>
|
||||
</div>
|
||||
{#if parent}
|
||||
<ChatComposeParent event={parent} clear={clearParent} />
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet action()}
|
||||
<div>
|
||||
{#if remove($pubkey, missingInboxes).length > 0}
|
||||
{@const count = remove($pubkey, missingInboxes).length}
|
||||
{@const label = count > 1 ? "inboxes are" : "inbox is"}
|
||||
<div
|
||||
class="row-2 badge badge-error badge-lg tooltip tooltip-left cursor-pointer"
|
||||
data-tip="{count} {label} not configured.">
|
||||
<Icon icon="danger" />
|
||||
{count}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</PageBar>
|
||||
|
||||
<PageContent class="flex flex-col-reverse pt-4">
|
||||
<div bind:this={dynamicPadding}></div>
|
||||
{#if missingInboxes.includes($pubkey!)}
|
||||
<div class="py-12">
|
||||
<div class="card2 col-2 m-auto max-w-md items-center text-center">
|
||||
<p class="row-2 text-lg text-error">
|
||||
<Icon icon="danger" />
|
||||
Your inbox is not configured.
|
||||
</p>
|
||||
<p>
|
||||
In order to deliver messages, {PLATFORM_NAME} needs to know where to send them. Please visit
|
||||
your <Link class="link" href="/settings/relays">relay settings page</Link> to set up your inbox.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else if missingInboxes.length > 0}
|
||||
<div class="py-12">
|
||||
<div class="card2 col-2 m-auto max-w-md items-center text-center">
|
||||
<p class="row-2 text-lg text-error">
|
||||
<Icon icon="danger" />
|
||||
{missingInboxes.length}
|
||||
{missingInboxes.length > 1 ? "inboxes are" : "inbox is"} not configured.
|
||||
</p>
|
||||
<p>
|
||||
In order to deliver messages, {PLATFORM_NAME} needs to know where to send them. Please make
|
||||
sure everyone in this conversation has set up their inbox relays.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#each elements as { type, id, value, showPubkey } (id)}
|
||||
{#if type === "date"}
|
||||
<Divider>{value}</Divider>
|
||||
{:else}
|
||||
<ChatMessage
|
||||
event={$state.snapshot(value as TrustedEvent)}
|
||||
{pubkeys}
|
||||
{showPubkey}
|
||||
{replyTo} />
|
||||
{/if}
|
||||
{/each}
|
||||
<p class="m-auto flex h-10 max-w-sm flex-col items-center justify-center gap-4 py-20 text-center">
|
||||
<Spinner {loading}>
|
||||
{#if loading}
|
||||
Looking for messages...
|
||||
{:else}
|
||||
End of message history
|
||||
{/if}
|
||||
</Spinner>
|
||||
{@render info?.()}
|
||||
</p>
|
||||
</PageContent>
|
||||
|
||||
<div class="chat__compose bg-base-200" bind:this={chatCompose}>
|
||||
<div>
|
||||
{#if parent}
|
||||
<ChatComposeParent event={parent} clear={clearParent} verb="Replying to" />
|
||||
{/if}
|
||||
</div>
|
||||
<ChatCompose bind:this={compose} {onSubmit} />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<script lang="ts">
|
||||
import {writable} from "svelte/store"
|
||||
import type {EventContent} from "@welshman/util"
|
||||
import {isMobile, preventDefault} from "@lib/html"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import EditorContent from "@app/editor/EditorContent.svelte"
|
||||
import {makeEditor} from "@app/editor"
|
||||
|
||||
type Props = {
|
||||
url?: string
|
||||
onSubmit: (event: EventContent) => void
|
||||
}
|
||||
|
||||
const {onSubmit, url}: Props = $props()
|
||||
|
||||
const autofocus = !isMobile
|
||||
|
||||
const uploading = writable(false)
|
||||
|
||||
export const focus = () => editor.then(ed => ed.chain().focus().run())
|
||||
|
||||
const submit = async () => {
|
||||
if ($uploading) return
|
||||
|
||||
const ed = await editor
|
||||
const content = ed.getText({blockSeparator: "\n"}).trim()
|
||||
const tags = ed.storage.nostr.getEditorTags()
|
||||
|
||||
if (!content) return
|
||||
|
||||
onSubmit({content, tags})
|
||||
|
||||
ed.chain().clearContent().run()
|
||||
}
|
||||
|
||||
const editor = makeEditor({
|
||||
url,
|
||||
autofocus,
|
||||
submit,
|
||||
uploading,
|
||||
aggressive: true,
|
||||
disableFileUpload: true,
|
||||
})
|
||||
</script>
|
||||
|
||||
<form class="relative z-feature flex gap-2 p-2" onsubmit={preventDefault(submit)}>
|
||||
<div class="chat-editor flex-grow overflow-hidden">
|
||||
<EditorContent {editor} />
|
||||
</div>
|
||||
<Button
|
||||
data-tip="{window.navigator.platform.includes('Mac') ? 'cmd' : 'ctrl'}+enter to send"
|
||||
class="center tooltip tooltip-left absolute right-4 h-10 w-10 min-w-10 rounded-full"
|
||||
disabled={$uploading}
|
||||
onclick={submit}>
|
||||
<Icon icon="plain" />
|
||||
</Button>
|
||||
</form>
|
||||
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {displayProfileByPubkey} from "@welshman/app"
|
||||
import {slide} from "@lib/transition"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import NoteContent from "@app/components/NoteContent.svelte"
|
||||
|
||||
const {
|
||||
verb,
|
||||
event,
|
||||
clear,
|
||||
}: {
|
||||
verb: string
|
||||
event: TrustedEvent
|
||||
clear: () => void
|
||||
} = $props()
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="relative border-l-2 border-solid border-primary bg-base-300 px-2 py-1 pr-8 text-xs"
|
||||
transition:slide>
|
||||
<p class="text-primary">{verb} @{displayProfileByPubkey(event.pubkey)}</p>
|
||||
{#key event.id}
|
||||
<NoteContent
|
||||
{event}
|
||||
hideMediaAtDepth={0}
|
||||
minLength={100}
|
||||
maxLength={300}
|
||||
expandMode="disabled" />
|
||||
{/key}
|
||||
<Button class="absolute right-2 top-2 cursor-pointer" onclick={clear}>
|
||||
<Icon icon="close-circle" />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -2,6 +2,7 @@
|
||||
import {goto} from "$app/navigation"
|
||||
import {WRAP} from "@welshman/util"
|
||||
import {repository} from "@welshman/app"
|
||||
import {preventDefault} from "@lib/html"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
@@ -10,9 +11,11 @@
|
||||
import {canDecrypt, PLATFORM_NAME, ensureUnwrapped} from "@app/state"
|
||||
import {clearModals} from "@app/modal"
|
||||
|
||||
export let next
|
||||
const {next} = $props()
|
||||
|
||||
let loading = false
|
||||
const nextUrl = $state.snapshot(next)
|
||||
|
||||
let loading = $state(false)
|
||||
|
||||
const enableChat = async () => {
|
||||
canDecrypt.set(true)
|
||||
@@ -22,7 +25,7 @@
|
||||
}
|
||||
|
||||
clearModals()
|
||||
goto(next)
|
||||
goto(nextUrl)
|
||||
}
|
||||
|
||||
const submit = async () => {
|
||||
@@ -38,10 +41,14 @@
|
||||
const back = () => history.back()
|
||||
</script>
|
||||
|
||||
<form class="column gap-4" on:submit|preventDefault={submit}>
|
||||
<form class="column gap-4" onsubmit={preventDefault(submit)}>
|
||||
<ModalHeader>
|
||||
<div slot="title">Enable Messages</div>
|
||||
<div slot="info">Do you want to enable direct messages?</div>
|
||||
{#snippet title()}
|
||||
<div>Enable Messages</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<div>Do you want to enable direct messages?</div>
|
||||
{/snippet}
|
||||
</ModalHeader>
|
||||
<p>
|
||||
By default, direct messages are disabled, since loading them requires
|
||||
@@ -52,7 +59,7 @@
|
||||
to decrypt data.
|
||||
</p>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" on:click={back}>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon="alt-arrow-left" />
|
||||
Go back
|
||||
</Button>
|
||||
|
||||
@@ -12,13 +12,18 @@
|
||||
import {makeChatPath} from "@app/routes"
|
||||
import {notifications} from "@app/notifications"
|
||||
|
||||
export let id: string
|
||||
export let pubkeys: string[]
|
||||
export let messages: TrustedEvent[]
|
||||
interface Props {
|
||||
id: string
|
||||
pubkeys: string[]
|
||||
messages: TrustedEvent[]
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
const others = remove($pubkey!, pubkeys)
|
||||
const active = $page.params.chat === id
|
||||
const path = makeChatPath(pubkeys)
|
||||
const {...props}: Props = $props()
|
||||
|
||||
const others = remove($pubkey!, props.pubkeys)
|
||||
const active = $page.params.chat === props.id
|
||||
const path = makeChatPath(props.pubkeys)
|
||||
|
||||
onMount(() => {
|
||||
for (const pk of others) {
|
||||
@@ -27,15 +32,15 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<Link class="flex flex-col justify-start gap-1" href={makeChatPath(pubkeys)}>
|
||||
<Link class="flex flex-col justify-start gap-1" href={makeChatPath(props.pubkeys)}>
|
||||
<div
|
||||
class="cursor-pointer border-t border-solid border-base-100 px-6 py-2 transition-colors hover:bg-base-100 {$$props.class}"
|
||||
class="cursor-pointer border-t border-solid border-base-100 px-6 py-2 transition-colors hover:bg-base-100 {props.class}"
|
||||
class:bg-base-100={active}>
|
||||
<div class="flex flex-col justify-start gap-1">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
{#if others.length === 0}
|
||||
<ProfileCircle pubkey={$pubkey} size={5} />
|
||||
<ProfileCircle pubkey={$pubkey!} size={5} />
|
||||
Note to self
|
||||
{:else if others.length === 1}
|
||||
<ProfileCircle pubkey={others[0]} size={5} />
|
||||
@@ -50,11 +55,11 @@
|
||||
{/if}
|
||||
</div>
|
||||
{#if !active && $notifications.has(path)}
|
||||
<div class="h-2 w-2 rounded-full bg-primary" transition:fade />
|
||||
<div class="h-2 w-2 rounded-full bg-primary" transition:fade></div>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="overflow-hidden text-ellipsis whitespace-nowrap text-sm">
|
||||
{messages[0].content}
|
||||
{props.messages[0].content}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
</script>
|
||||
|
||||
<div class="col-2">
|
||||
<Button class="btn btn-primary" on:click={startChat}>
|
||||
<Button class="btn btn-primary" onclick={startChat}>
|
||||
<Icon size={4} icon="add-circle" />
|
||||
Start chat
|
||||
</Button>
|
||||
<Button class="btn btn-neutral" on:click={markAsRead}>
|
||||
<Button class="btn btn-neutral" onclick={markAsRead}>
|
||||
<Icon size={4} icon="check-circle" />
|
||||
Mark all read
|
||||
</Button>
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
<script lang="ts">
|
||||
import {type Instance} from "tippy.js"
|
||||
import {hash} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {
|
||||
thunks,
|
||||
deriveProfile,
|
||||
deriveProfileDisplay,
|
||||
formatTimestampAsTime,
|
||||
pubkey,
|
||||
} from "@welshman/app"
|
||||
import {hash, formatTimestampAsTime} from "@welshman/lib"
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import {thunks, pubkey, deriveProfile, deriveProfileDisplay, sendWrapped} from "@welshman/app"
|
||||
import {isMobile} from "@lib/html"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Tippy from "@lib/components/Tippy.svelte"
|
||||
import LongPress from "@lib/components/LongPress.svelte"
|
||||
import TapTarget from "@lib/components/TapTarget.svelte"
|
||||
import Avatar from "@lib/components/Avatar.svelte"
|
||||
import Content from "@app/components/Content.svelte"
|
||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||
@@ -22,13 +16,17 @@
|
||||
import ChatMessageMenu from "@app/components/ChatMessageMenu.svelte"
|
||||
import ChatMessageMenuMobile from "@app/components/ChatMessageMenuMobile.svelte"
|
||||
import {colors} from "@app/state"
|
||||
import {makeDelete, makeReaction, sendWrapped} from "@app/commands"
|
||||
import {makeDelete, makeReaction} from "@app/commands"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
export let event: TrustedEvent
|
||||
export let replyTo: any = undefined
|
||||
export let pubkeys: string[]
|
||||
export let showPubkey = false
|
||||
interface Props {
|
||||
event: TrustedEvent
|
||||
replyTo: (event: TrustedEvent) => void
|
||||
pubkeys: string[]
|
||||
showPubkey?: boolean
|
||||
}
|
||||
|
||||
const {event, replyTo, pubkeys, showPubkey = false}: Props = $props()
|
||||
|
||||
const thunk = $thunks[event.id]
|
||||
const isOwn = event.pubkey === $pubkey
|
||||
@@ -36,27 +34,28 @@
|
||||
const profileDisplay = deriveProfileDisplay(event.pubkey)
|
||||
const [_, colorValue] = colors[parseInt(hash(event.pubkey)) % colors.length]
|
||||
|
||||
const onReactionClick = async (content: string, events: TrustedEvent[]) => {
|
||||
const reaction = events.find(e => e.pubkey === $pubkey)
|
||||
const template = reaction ? makeDelete({event: reaction}) : makeReaction({event, content})
|
||||
const reply = () => replyTo(event)
|
||||
|
||||
await sendWrapped({template, pubkeys})
|
||||
}
|
||||
const deleteReaction = (event: TrustedEvent) =>
|
||||
sendWrapped({template: makeDelete({event}), pubkeys})
|
||||
|
||||
const createReaction = (template: EventContent) =>
|
||||
sendWrapped({template: makeReaction({event, ...template}), pubkeys})
|
||||
|
||||
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey})
|
||||
|
||||
const showMobileMenu = () => pushModal(ChatMessageMenuMobile, {event, pubkeys})
|
||||
const showMobileMenu = () => pushModal(ChatMessageMenuMobile, {event, pubkeys, reply})
|
||||
|
||||
const togglePopover = () => {
|
||||
if (popoverIsVisible) {
|
||||
popover.hide()
|
||||
popover?.hide()
|
||||
} else {
|
||||
popover.show()
|
||||
popover?.show()
|
||||
}
|
||||
}
|
||||
|
||||
let popover: Instance
|
||||
let popoverIsVisible = false
|
||||
let popover: Instance | undefined = $state()
|
||||
let popoverIsVisible = $state(false)
|
||||
</script>
|
||||
|
||||
{#if thunk}
|
||||
@@ -68,45 +67,44 @@
|
||||
class:chat-start={!isOwn}
|
||||
class:flex-row-reverse={!isOwn}
|
||||
class:chat-end={isOwn}>
|
||||
<Tippy
|
||||
bind:popover
|
||||
component={ChatMessageMenu}
|
||||
props={{event, pubkeys, popover, replyTo}}
|
||||
params={{
|
||||
interactive: true,
|
||||
trigger: "manual",
|
||||
onShow() {
|
||||
popoverIsVisible = true
|
||||
},
|
||||
onHidden() {
|
||||
popoverIsVisible = false
|
||||
},
|
||||
}}>
|
||||
<button
|
||||
type="button"
|
||||
class="opacity-0 transition-all"
|
||||
class:group-hover:opacity-100={!isMobile}
|
||||
on:click={togglePopover}>
|
||||
<Icon icon="menu-dots" size={4} />
|
||||
</button>
|
||||
</Tippy>
|
||||
{#if !isMobile}
|
||||
<Tippy
|
||||
bind:popover
|
||||
component={ChatMessageMenu}
|
||||
props={{event, pubkeys, popover, replyTo}}
|
||||
params={{
|
||||
interactive: true,
|
||||
trigger: "manual",
|
||||
onShow() {
|
||||
popoverIsVisible = true
|
||||
},
|
||||
onHidden() {
|
||||
popoverIsVisible = false
|
||||
},
|
||||
}}>
|
||||
<button
|
||||
type="button"
|
||||
class="opacity-0 transition-all"
|
||||
class:group-hover:opacity-100={!isMobile}
|
||||
onclick={togglePopover}>
|
||||
<Icon icon="menu-dots" size={4} />
|
||||
</button>
|
||||
</Tippy>
|
||||
{/if}
|
||||
<div class="flex min-w-0 flex-col" class:items-end={isOwn}>
|
||||
<LongPress
|
||||
class="bg-alt chat-bubble mx-1 flex cursor-auto flex-col gap-1 text-left lg:max-w-2xl"
|
||||
onLongPress={showMobileMenu}>
|
||||
<TapTarget
|
||||
class="bg-alt chat-bubble mx-1 mb-2 flex cursor-auto flex-col gap-1 text-left lg:max-w-2xl"
|
||||
onTap={showMobileMenu}>
|
||||
{#if showPubkey}
|
||||
<div class="flex items-center gap-2">
|
||||
{#if !isOwn}
|
||||
<Button on:click={openProfile} class="flex items-center gap-1">
|
||||
<Button onclick={openProfile} class="flex items-center gap-1">
|
||||
<Avatar
|
||||
src={$profile?.picture}
|
||||
class="border border-solid border-base-content"
|
||||
size={4} />
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
on:click={openProfile}
|
||||
class="text-sm font-bold"
|
||||
style="color: {colorValue}">
|
||||
<Button onclick={openProfile} class="text-sm font-bold" style="color: {colorValue}">
|
||||
{$profileDisplay}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -119,9 +117,9 @@
|
||||
<div class="text-sm">
|
||||
<Content showEntire {event} />
|
||||
</div>
|
||||
</LongPress>
|
||||
<div class="row-2 z-feature -mt-1 ml-4">
|
||||
<ReactionSummary {event} {onReactionClick} noTooltip />
|
||||
</TapTarget>
|
||||
<div class="row-2 z-feature -mt-4 ml-4">
|
||||
<ReactionSummary {event} {deleteReaction} {createReaction} noTooltip />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
<script lang="ts">
|
||||
import type {NativeEmoji} from "emoji-picker-element/shared"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {sendWrapped} from "@welshman/app"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import EmojiButton from "@lib/components/EmojiButton.svelte"
|
||||
import {makeReaction, sendWrapped} from "@app/commands"
|
||||
import {makeReaction} from "@app/commands"
|
||||
|
||||
export let event: TrustedEvent
|
||||
export let pubkeys: string[]
|
||||
interface Props {
|
||||
event: TrustedEvent
|
||||
pubkeys: string[]
|
||||
}
|
||||
|
||||
const {event, pubkeys}: Props = $props()
|
||||
|
||||
const onEmoji = (emoji: NativeEmoji) =>
|
||||
sendWrapped({template: makeReaction({event, content: emoji.unicode}), pubkeys})
|
||||
|
||||
@@ -5,10 +5,7 @@
|
||||
import EventInfo from "@app/components/EventInfo.svelte"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
export let event
|
||||
export let pubkeys
|
||||
export let popover
|
||||
export let replyTo
|
||||
const {event, pubkeys, popover, replyTo} = $props()
|
||||
|
||||
const reply = () => replyTo(event)
|
||||
|
||||
@@ -21,11 +18,11 @@
|
||||
<div class="join border border-solid border-neutral text-xs">
|
||||
<ChatMessageEmojiButton {event} {pubkeys} />
|
||||
{#if replyTo}
|
||||
<Button class="btn join-item btn-xs" on:click={reply}>
|
||||
<Button class="btn join-item btn-xs" onclick={reply}>
|
||||
<Icon size={4} icon="reply" />
|
||||
</Button>
|
||||
{/if}
|
||||
<Button class="btn join-item btn-xs" on:click={showInfo}>
|
||||
<Button class="btn join-item btn-xs" onclick={showInfo}>
|
||||
<Icon size={4} icon="code-2" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,23 +1,35 @@
|
||||
<script lang="ts">
|
||||
import type {NativeEmoji} from "emoji-picker-element/shared"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {sendWrapped} from "@welshman/app"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import EmojiPicker from "@lib/components/EmojiPicker.svelte"
|
||||
import EventInfo from "@app/components/EventInfo.svelte"
|
||||
import {makeReaction, sendWrapped} from "@app/commands"
|
||||
import {makeReaction} from "@app/commands"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {clip} from "@app/toast"
|
||||
|
||||
export let event
|
||||
export let pubkeys
|
||||
|
||||
const onEmoji = (emoji: NativeEmoji) => {
|
||||
history.back()
|
||||
sendWrapped({template: makeReaction({event, content: emoji.unicode}), pubkeys})
|
||||
type Props = {
|
||||
pubkeys: string[]
|
||||
event: TrustedEvent
|
||||
reply: () => void
|
||||
}
|
||||
|
||||
const {event, pubkeys, reply}: Props = $props()
|
||||
|
||||
const onEmoji = ((event: TrustedEvent, pubkeys: string[], emoji: NativeEmoji) => {
|
||||
history.back()
|
||||
sendWrapped({template: makeReaction({event, content: emoji.unicode}), pubkeys})
|
||||
}).bind(undefined, event, pubkeys)
|
||||
|
||||
const showEmojiPicker = () => pushModal(EmojiPicker, {onClick: onEmoji}, {replaceState: true})
|
||||
|
||||
const sendReply = () => {
|
||||
history.back()
|
||||
reply()
|
||||
}
|
||||
|
||||
const copyText = () => {
|
||||
history.back()
|
||||
clip(event.content)
|
||||
@@ -27,15 +39,19 @@
|
||||
</script>
|
||||
|
||||
<div class="col-2">
|
||||
<Button class="btn btn-primary w-full" on:click={showEmojiPicker}>
|
||||
<Button class="btn btn-primary w-full" onclick={showEmojiPicker}>
|
||||
<Icon size={4} icon="smile-circle" />
|
||||
Send Reaction
|
||||
</Button>
|
||||
<Button class="btn btn-neutral w-full" on:click={copyText}>
|
||||
<Button class="btn btn-neutral w-full" onclick={sendReply}>
|
||||
<Icon size={4} icon="reply" />
|
||||
Send Reply
|
||||
</Button>
|
||||
<Button class="btn btn-neutral w-full" onclick={copyText}>
|
||||
<Icon size={4} icon="copy" />
|
||||
Copy Text
|
||||
</Button>
|
||||
<Button class="btn btn-neutral" on:click={showInfo}>
|
||||
<Button class="btn btn-neutral" onclick={showInfo}>
|
||||
<Icon size={4} icon="code-2" />
|
||||
Message Details
|
||||
</Button>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import {goto} from "$app/navigation"
|
||||
import {pubkey} from "@welshman/app"
|
||||
import {preventDefault} from "@lib/html"
|
||||
import Field from "@lib/components/Field.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
@@ -13,21 +14,25 @@
|
||||
|
||||
const onSubmit = () => goto(makeChatPath([...pubkeys, $pubkey!]))
|
||||
|
||||
let pubkeys: string[] = []
|
||||
let pubkeys: string[] = $state([])
|
||||
</script>
|
||||
|
||||
<form class="column gap-4" on:submit|preventDefault={onSubmit}>
|
||||
<form class="column gap-4" onsubmit={preventDefault(onSubmit)}>
|
||||
<ModalHeader>
|
||||
<div slot="title">Start a Chat</div>
|
||||
<div slot="info">Create an encrypted chat room for private conversations.</div>
|
||||
{#snippet title()}
|
||||
<div>Start a Chat</div>
|
||||
{/snippet}
|
||||
{#snippet info()}
|
||||
<div>Create an encrypted chat room for private conversations.</div>
|
||||
{/snippet}
|
||||
</ModalHeader>
|
||||
<Field>
|
||||
<div slot="input">
|
||||
{#snippet input()}
|
||||
<ProfileMultiSelect autofocus bind:value={pubkeys} />
|
||||
</div>
|
||||
{/snippet}
|
||||
</Field>
|
||||
<ModalFooter>
|
||||
<Button class="btn btn-link" on:click={back}>
|
||||
<Button class="btn btn-link" onclick={back}>
|
||||
<Icon icon="alt-arrow-left" />
|
||||
Go back
|
||||
</Button>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
truncate,
|
||||
renderAsHtml,
|
||||
isText,
|
||||
isEmoji,
|
||||
isTopic,
|
||||
isCode,
|
||||
isCashu,
|
||||
@@ -17,10 +18,12 @@
|
||||
isAddress,
|
||||
isNewline,
|
||||
} from "@welshman/content"
|
||||
import {preventDefault, stopPropagation} from "@lib/html"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import ContentToken from "@app/components/ContentToken.svelte"
|
||||
import ContentEmoji from "@app/components/ContentEmoji.svelte"
|
||||
import ContentCode from "@app/components/ContentCode.svelte"
|
||||
import ContentLinkInline from "@app/components/ContentLinkInline.svelte"
|
||||
import ContentLinkBlock from "@app/components/ContentLinkBlock.svelte"
|
||||
@@ -30,14 +33,27 @@
|
||||
import ContentMention from "@app/components/ContentMention.svelte"
|
||||
import {entityLink, userSettingValues} from "@app/state"
|
||||
|
||||
export let event
|
||||
export let minLength = 500
|
||||
export let maxLength = 700
|
||||
export let showEntire = false
|
||||
export let hideMedia = false
|
||||
export let expandMode = "block"
|
||||
export let quoteProps: Record<string, any> = {}
|
||||
export let depth = 0
|
||||
interface Props {
|
||||
event: any
|
||||
minLength?: number
|
||||
maxLength?: number
|
||||
showEntire?: boolean
|
||||
hideMediaAtDepth?: number
|
||||
expandMode?: string
|
||||
depth?: number
|
||||
url?: string
|
||||
}
|
||||
|
||||
let {
|
||||
event,
|
||||
minLength = 500,
|
||||
maxLength = 700,
|
||||
showEntire = $bindable(false),
|
||||
hideMediaAtDepth = 1,
|
||||
expandMode = "block",
|
||||
depth = 0,
|
||||
url,
|
||||
}: Props = $props()
|
||||
|
||||
const fullContent = parse(event)
|
||||
|
||||
@@ -48,13 +64,13 @@
|
||||
const isBlock = (i: number) => {
|
||||
const parsed = fullContent[i]
|
||||
|
||||
if (!parsed || hideMedia) return false
|
||||
if (!parsed || hideMediaAtDepth <= depth) return false
|
||||
|
||||
if (isLink(parsed) && $userSettingValues.show_media && isStartOrEnd(i)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if ((isEvent(parsed) || isAddress(parsed)) && isStartOrEnd(i) && depth < 1) {
|
||||
if ((isEvent(parsed) || isAddress(parsed)) && isStartOrEnd(i)) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -82,20 +98,23 @@
|
||||
warning = null
|
||||
}
|
||||
|
||||
let warning =
|
||||
$userSettingValues.hide_sensitive && event.tags.find(nthEq(0, "content-warning"))?.[1]
|
||||
let warning = $state(
|
||||
$userSettingValues.hide_sensitive && event.tags.find(nthEq(0, "content-warning"))?.[1],
|
||||
)
|
||||
|
||||
$: shortContent = showEntire
|
||||
? fullContent
|
||||
: truncate(fullContent, {
|
||||
minLength,
|
||||
maxLength,
|
||||
mediaLength: hideMedia ? 20 : 200,
|
||||
})
|
||||
const shortContent = $derived(
|
||||
showEntire
|
||||
? fullContent
|
||||
: truncate(fullContent, {
|
||||
minLength,
|
||||
maxLength,
|
||||
mediaLength: hideMediaAtDepth <= depth ? 20 : 200,
|
||||
}),
|
||||
)
|
||||
|
||||
$: hasEllipsis = shortContent.some(isEllipsis)
|
||||
$: expandInline = hasEllipsis && expandMode === "inline"
|
||||
$: expandBlock = hasEllipsis && expandMode === "block"
|
||||
const hasEllipsis = $derived(shortContent.some(isEllipsis))
|
||||
const expandInline = $derived(hasEllipsis && expandMode === "inline")
|
||||
const expandBlock = $derived(hasEllipsis && expandMode === "block")
|
||||
</script>
|
||||
|
||||
<div class="relative">
|
||||
@@ -104,7 +123,7 @@
|
||||
<Icon icon="danger" />
|
||||
<p>
|
||||
This note has been flagged by the author as "{warning}".<br />
|
||||
<Button class="link" on:click={ignoreWarning}>Show anyway</Button>
|
||||
<Button class="link" onclick={ignoreWarning}>Show anyway</Button>
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
@@ -112,10 +131,12 @@
|
||||
class="overflow-hidden text-ellipsis break-words"
|
||||
style={expandBlock ? "mask-image: linear-gradient(0deg, transparent 0px, black 100px)" : ""}>
|
||||
{#each shortContent as parsed, i}
|
||||
{#if isNewline(parsed)}
|
||||
<ContentNewline value={parsed.value.slice(isBlock(i - 1) ? 1 : 0)} />
|
||||
{#if isNewline(parsed) && !isBlock(i - 1)}
|
||||
<ContentNewline value={parsed.value} />
|
||||
{:else if isTopic(parsed)}
|
||||
<ContentTopic value={parsed.value} />
|
||||
{:else if isEmoji(parsed)}
|
||||
<ContentEmoji value={parsed.value} />
|
||||
{:else if isCode(parsed)}
|
||||
<ContentCode
|
||||
value={parsed.value}
|
||||
@@ -124,19 +145,15 @@
|
||||
<ContentToken value={parsed.value} />
|
||||
{:else if isLink(parsed)}
|
||||
{#if isBlock(i)}
|
||||
<ContentLinkBlock value={parsed.value} />
|
||||
<ContentLinkBlock value={parsed.value} {event} />
|
||||
{:else}
|
||||
<ContentLinkInline value={parsed.value} />
|
||||
{/if}
|
||||
{:else if isProfile(parsed)}
|
||||
<ContentMention value={parsed.value} />
|
||||
<ContentMention value={parsed.value} {url} />
|
||||
{:else if isEvent(parsed) || isAddress(parsed)}
|
||||
{#if isBlock(i)}
|
||||
<ContentQuote {...quoteProps} value={parsed.value} {depth} {event}>
|
||||
<div slot="note-content" let:event>
|
||||
<svelte:self {quoteProps} {hideMedia} {event} depth={depth + 1} />
|
||||
</div>
|
||||
</ContentQuote>
|
||||
<ContentQuote {depth} {url} {hideMediaAtDepth} value={parsed.value} {event} />
|
||||
{:else}
|
||||
<Link
|
||||
external
|
||||
@@ -158,7 +175,7 @@
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-neutral"
|
||||
on:click|stopPropagation|preventDefault={expand}>
|
||||
onclick={stopPropagation(preventDefault(expand))}>
|
||||
See more
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
export let value
|
||||
export let isBlock
|
||||
const {value, isBlock} = $props()
|
||||
</script>
|
||||
|
||||
<code
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import type {ParsedEmojiValue} from "@welshman/content"
|
||||
import {imgproxy} from "@app/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,15 @@
|
||||
<script lang="ts">
|
||||
import {ellipsize, postJson} from "@welshman/lib"
|
||||
import {dufflepud, imgproxy} from "@app/state"
|
||||
import {preventDefault, stopPropagation} from "@lib/html"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
|
||||
import ContentLinkBlockImage from "@app/components/ContentLinkBlockImage.svelte"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
export let value
|
||||
const {value, event} = $props()
|
||||
|
||||
let hideImage = $state(false)
|
||||
|
||||
const url = value.url.toString()
|
||||
|
||||
@@ -19,7 +23,11 @@
|
||||
return json
|
||||
}
|
||||
|
||||
const expand = () => pushModal(ContentLinkDetail, {url}, {fullscreen: true})
|
||||
const onError = () => {
|
||||
hideImage = true
|
||||
}
|
||||
|
||||
const expand = () => pushModal(ContentLinkDetail, {value, event}, {fullscreen: true})
|
||||
</script>
|
||||
|
||||
<Link external href={url} class="my-2 block">
|
||||
@@ -29,19 +37,20 @@
|
||||
<track kind="captions" />
|
||||
</video>
|
||||
{:else if url.match(/\.(jpe?g|png|gif|webp)$/)}
|
||||
<button type="button" on:click|stopPropagation|preventDefault={expand}>
|
||||
<img alt="Link preview" src={imgproxy(url)} class="m-auto max-h-96 rounded-box" />
|
||||
<button type="button" onclick={stopPropagation(preventDefault(expand))}>
|
||||
<ContentLinkBlockImage {value} {event} class="m-auto max-h-96 rounded-box" />
|
||||
</button>
|
||||
{:else}
|
||||
{#await loadPreview()}
|
||||
<div class="center my-12 w-full">
|
||||
<span class="loading loading-spinner" />
|
||||
<span class="loading loading-spinner"></span>
|
||||
</div>
|
||||
{:then preview}
|
||||
<div class="bg-alt flex max-w-xl flex-col leading-normal">
|
||||
{#if preview.image}
|
||||
{#if preview.image && !hideImage}
|
||||
<img
|
||||
alt="Link preview"
|
||||
onerror={onError}
|
||||
src={imgproxy(preview.image)}
|
||||
class="bg-alt max-h-72 object-contain object-center" />
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<script lang="ts">
|
||||
import {onDestroy} from "svelte"
|
||||
import {now} from "@welshman/lib"
|
||||
import {BLOSSOM_AUTH, makeEvent, getTags, getTagValue, tagsFromIMeta} from "@welshman/util"
|
||||
import {signer} from "@welshman/app"
|
||||
import {imgproxy} from "@app/state"
|
||||
|
||||
const {value, event, ...props} = $props()
|
||||
|
||||
const url = value.url.toString()
|
||||
|
||||
// If we fail to fetch the image, try authenticating if we have a blossom hash
|
||||
const onerror = async () => {
|
||||
const meta = getTags("imeta", event.tags)
|
||||
.map(tagsFromIMeta)
|
||||
.find(meta => getTagValue("url", meta) === url)
|
||||
const hash = meta ? getTagValue("x", meta) : undefined
|
||||
|
||||
if (hash && $signer) {
|
||||
const event = await signer.get().sign(
|
||||
makeEvent(BLOSSOM_AUTH, {
|
||||
tags: [
|
||||
["t", "get"],
|
||||
["x", hash],
|
||||
["expiration", String(now() + 30)],
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Nostr ${btoa(JSON.stringify(event))}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (res.status === 200) {
|
||||
src = URL.createObjectURL(await res.blob())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let src = $state(imgproxy(url))
|
||||
|
||||
onDestroy(() => {
|
||||
URL.revokeObjectURL(src)
|
||||
})
|
||||
</script>
|
||||
|
||||
<img alt="" {src} {onerror} {...props} />
|
||||
@@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import ContentLinkBlockImage from "@app/components/ContentLinkBlockImage.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import {imgproxy} from "@app/state"
|
||||
|
||||
export let url
|
||||
const {value, event} = $props()
|
||||
|
||||
const back = () => history.back()
|
||||
</script>
|
||||
|
||||
<Button class="m-auto h-screen w-screen cursor-pointer p-4" on:click={back}>
|
||||
<img alt="" src={imgproxy(url)} class="m-auto max-h-full max-w-full rounded-box" />
|
||||
<Button class="m-auto h-screen w-screen cursor-pointer p-4" onclick={back}>
|
||||
<ContentLinkBlockImage {value} {event} class="m-auto max-h-full max-w-full rounded-box" />
|
||||
</Button>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<script lang="ts">
|
||||
import {displayUrl} from "@welshman/lib"
|
||||
import {preventDefault} from "@lib/html"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
export let value
|
||||
const {value} = $props()
|
||||
|
||||
const url = value.url.toString()
|
||||
|
||||
@@ -14,7 +15,7 @@
|
||||
|
||||
{#if url.match(/\.(jpe?g|png|gif|webp)$/)}
|
||||
<!-- Use a real link so people can copy the href -->
|
||||
<a href={url} class="link-content whitespace-nowrap" on:click|preventDefault={expand}>
|
||||
<a href={url} class="link-content whitespace-nowrap" onclick={preventDefault(expand)}>
|
||||
<Icon icon="link-round" size={3} class="inline-block" />
|
||||
{displayUrl(url)}
|
||||
</a>
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
<script lang="ts">
|
||||
import {removeNil} from "@welshman/lib"
|
||||
import type {ProfilePointer} from "@welshman/content"
|
||||
import {displayProfile} from "@welshman/util"
|
||||
import {deriveProfile} from "@welshman/app"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
export let value
|
||||
type Props = {
|
||||
value: ProfilePointer
|
||||
url?: string
|
||||
}
|
||||
|
||||
const profile = deriveProfile(value.pubkey)
|
||||
const {value, url}: Props = $props()
|
||||
|
||||
const openProfile = () => pushModal(ProfileDetail, {pubkey: value.pubkey})
|
||||
const profile = deriveProfile(value.pubkey, removeNil([url]))
|
||||
|
||||
const openProfile = () => pushModal(ProfileDetail, {pubkey: value.pubkey, url})
|
||||
</script>
|
||||
|
||||
<Button on:click={openProfile} class="link-content">
|
||||
<Button onclick={openProfile} class="link-content">
|
||||
@{displayProfile($profile)}
|
||||
</Button>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
export let value
|
||||
const {value} = $props()
|
||||
</script>
|
||||
|
||||
{#each value as _}
|
||||
|
||||
@@ -1,66 +1,54 @@
|
||||
<script lang="ts">
|
||||
import {nip19} from "nostr-tools"
|
||||
import * as nip19 from "nostr-tools/nip19"
|
||||
import {goto} from "$app/navigation"
|
||||
import {ctx, nthEq} from "@welshman/lib"
|
||||
import {nthEq} from "@welshman/lib"
|
||||
import {Router} from "@welshman/router"
|
||||
import {tracker, repository} from "@welshman/app"
|
||||
import {Address, DIRECT_MESSAGE, MESSAGE, THREAD} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Address, DIRECT_MESSAGE, MESSAGE, THREAD, EVENT_TIME} from "@welshman/util"
|
||||
import {scrollToEvent} from "@lib/html"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import NoteCard from "@app/components/NoteCard.svelte"
|
||||
import NoteContent from "@app/components/NoteContent.svelte"
|
||||
import {deriveEvent, entityLink, ROOM} from "@app/state"
|
||||
import {makeThreadPath, makeRoomPath} from "@app/routes"
|
||||
import {makeThreadPath, makeCalendarPath, makeRoomPath} from "@app/routes"
|
||||
|
||||
export let value
|
||||
export let event
|
||||
export let depth = 0
|
||||
export let relays: string[] = []
|
||||
export let minimal = false
|
||||
type Props = {
|
||||
value: any
|
||||
hideMediaAtDepth: number
|
||||
event: TrustedEvent
|
||||
depth: number
|
||||
url?: string
|
||||
}
|
||||
|
||||
const {id, identifier, kind, pubkey, relays: relayHints = []} = value
|
||||
const {value, event, depth, hideMediaAtDepth, url}: Props = $props()
|
||||
|
||||
const {id, identifier, kind, pubkey, relays = []} = value
|
||||
const idOrAddress = id || new Address(kind, pubkey, identifier).toString()
|
||||
const mergedRelays = [
|
||||
...relays,
|
||||
...ctx.app.router.Quote(event, idOrAddress, relayHints).getUrls(),
|
||||
]
|
||||
const mergedRelays = Router.get().Quote(event, idOrAddress, relays).getUrls()
|
||||
|
||||
if (url) {
|
||||
mergedRelays.push(url)
|
||||
}
|
||||
|
||||
const quote = deriveEvent(idOrAddress, mergedRelays)
|
||||
const entity = id
|
||||
? nip19.neventEncode({id, relays: mergedRelays})
|
||||
: new Address(kind, pubkey, identifier, mergedRelays).toNaddr()
|
||||
|
||||
const scrollToEvent = (id: string) => {
|
||||
const element = document.querySelector(`[data-event="${id}"]`) as any
|
||||
|
||||
if (element) {
|
||||
element.scrollIntoView({behavior: "smooth"})
|
||||
element.style =
|
||||
"filter: brightness(1.5); transition-property: all; transition-duration: 400ms;"
|
||||
|
||||
setTimeout(() => {
|
||||
element.style = "transition-property: all; transition-duration: 300ms;"
|
||||
}, 800)
|
||||
|
||||
setTimeout(() => {
|
||||
element.style = ""
|
||||
}, 800 + 400)
|
||||
}
|
||||
|
||||
return Boolean(element)
|
||||
}
|
||||
|
||||
const openMessage = (url: string, room: string, id: string) => {
|
||||
const event = repository.getEvent(id)
|
||||
|
||||
if (event) {
|
||||
goto(makeRoomPath(url, room))
|
||||
|
||||
// TODO: if the event doesn't immediately load, this won't work. Scroll up until it's found
|
||||
setTimeout(() => scrollToEvent(id), 300)
|
||||
scrollToEvent(id)
|
||||
}
|
||||
|
||||
return Boolean(event)
|
||||
}
|
||||
|
||||
const onClick = (e: Event) => {
|
||||
const onclick = () => {
|
||||
if ($quote) {
|
||||
if ($quote.kind === DIRECT_MESSAGE) {
|
||||
return scrollToEvent($quote.id)
|
||||
@@ -74,6 +62,10 @@
|
||||
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)
|
||||
}
|
||||
@@ -86,6 +78,10 @@
|
||||
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)
|
||||
}
|
||||
@@ -97,10 +93,10 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Button class="my-2 block max-w-full text-left" on:click={onClick}>
|
||||
<Button class="my-2 block max-w-full text-left" {onclick}>
|
||||
{#if $quote}
|
||||
<NoteCard {minimal} event={$quote} class="bg-alt rounded-box p-4">
|
||||
<slot name="note-content" event={$quote} {depth} />
|
||||
<NoteCard event={$quote} {url} class="bg-alt rounded-box p-4">
|
||||
<NoteContent {hideMediaAtDepth} {url} event={$quote} depth={depth + 1} />
|
||||
</NoteCard>
|
||||
{:else}
|
||||
<div class="rounded-box p-4">
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import {clip} from "@app/toast"
|
||||
|
||||
export let value
|
||||
const {value} = $props()
|
||||
|
||||
const copy = () => clip(value)
|
||||
</script>
|
||||
|
||||
<Button on:click={copy} class="link-content">
|
||||
<Button onclick={copy} class="link-content">
|
||||
<Icon icon="bolt" size={3} class="inline-block translate-y-px" />
|
||||
{value.slice(0, 16)}...
|
||||
</Button>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
export let value
|
||||
const {value} = $props()
|
||||
</script>
|
||||
|
||||
<span class="link-content">
|
||||
|
||||
@@ -7,15 +7,14 @@
|
||||
import {pushModal} from "@app/modal"
|
||||
import {BURROW_URL} from "@app/state"
|
||||
|
||||
export let email
|
||||
export let confirm_token
|
||||
const {email, confirm_token} = $props()
|
||||
|
||||
const login = () => {
|
||||
pushModal(LogInPassword, {email}, {path: "/"})
|
||||
}
|
||||
|
||||
let error: string
|
||||
let loading = true
|
||||
let error = $state("")
|
||||
let loading = $state(true)
|
||||
|
||||
onMount(async () => {
|
||||
const [res] = await Promise.all([
|
||||
@@ -49,5 +48,5 @@
|
||||
{/if}
|
||||
</Spinner>
|
||||
</p>
|
||||
<Button class="btn btn-primary" on:click={login} disabled={loading}>Continue to Login</Button>
|
||||
<Button class="btn btn-primary" onclick={login} disabled={loading}>Continue to Login</Button>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
import type {Snippet} from "svelte"
|
||||
import type {Instance} from "tippy.js"
|
||||
import type {NativeEmoji} from "emoji-picker-element/shared"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Tippy from "@lib/components/Tippy.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import EmojiButton from "@lib/components/EmojiButton.svelte"
|
||||
import EventMenu from "@app/components/EventMenu.svelte"
|
||||
import {publishReaction} from "@app/commands"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
noun: string
|
||||
event: TrustedEvent
|
||||
customActions?: Snippet
|
||||
}
|
||||
|
||||
const {url, noun, event, customActions}: Props = $props()
|
||||
|
||||
const showPopover = () => popover?.show()
|
||||
|
||||
const hidePopover = () => popover?.hide()
|
||||
|
||||
const onEmoji = (emoji: NativeEmoji) =>
|
||||
publishReaction({event, content: emoji.unicode, relays: [url]})
|
||||
|
||||
let popover: Instance | undefined = $state()
|
||||
</script>
|
||||
|
||||
<Button class="join rounded-full">
|
||||
<EmojiButton {onEmoji} class="btn join-item btn-neutral btn-xs">
|
||||
<Icon icon="smile-circle" size={4} />
|
||||
</EmojiButton>
|
||||
<Tippy
|
||||
bind:popover
|
||||
component={EventMenu}
|
||||
props={{url, noun, event, customActions, onClick: hidePopover}}
|
||||
params={{trigger: "manual", interactive: true}}>
|
||||
<Button class="btn join-item btn-neutral btn-xs" onclick={showPopover}>
|
||||
<Icon icon="menu-dots" size={4} />
|
||||
</Button>
|
||||
</Tippy>
|
||||
</Button>
|
||||