Compare commits
No commits in common. "main" and "nak" have entirely different histories.
41
.github/workflows/release.yml
vendored
41
.github/workflows/release.yml
vendored
@ -8,6 +8,42 @@ env:
|
|||||||
DOCKER_CLI_EXPERIMENTAL: enabled
|
DOCKER_CLI_EXPERIMENTAL: enabled
|
||||||
TAG_FMT: "^refs/tags/(((.?[0-9]+){3,4}))$"
|
TAG_FMT: "^refs/tags/(((.?[0-9]+){3,4}))$"
|
||||||
jobs:
|
jobs:
|
||||||
|
tauri_release:
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
platform: [macos-latest, ubuntu-20.04, windows-latest]
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Install dependencies (ubuntu only)
|
||||||
|
if: matrix.platform == 'ubuntu-20.04'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libayatana-appindicator3-dev librsvg2-dev
|
||||||
|
- name: Rust setup
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
- name: Rust cache
|
||||||
|
uses: swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: "./src-tauri -> target"
|
||||||
|
|
||||||
|
- name: Sync node version and setup cache
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: "16"
|
||||||
|
cache: "yarn"
|
||||||
|
- name: Install frontend dependencies
|
||||||
|
run: yarn install
|
||||||
|
- name: Build the app
|
||||||
|
uses: tauri-apps/tauri-action@v0
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
tagName: ${{ github.ref_name }}
|
||||||
app:
|
app:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
@ -56,8 +92,7 @@ jobs:
|
|||||||
alias: ${{ secrets.KEY_ALIAS }}
|
alias: ${{ secrets.KEY_ALIAS }}
|
||||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||||
env:
|
|
||||||
BUILD_TOOLS_VERSION: "33.0.0"
|
|
||||||
- name: Sign APK
|
- name: Sign APK
|
||||||
uses: r0adkll/sign-android-release@v1
|
uses: r0adkll/sign-android-release@v1
|
||||||
with:
|
with:
|
||||||
@ -66,8 +101,6 @@ jobs:
|
|||||||
alias: ${{ secrets.KEY_ALIAS }}
|
alias: ${{ secrets.KEY_ALIAS }}
|
||||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||||
env:
|
|
||||||
BUILD_TOOLS_VERSION: "33.0.0"
|
|
||||||
- name: Rename files
|
- name: Rename files
|
||||||
run: |-
|
run: |-
|
||||||
mkdir -p snort_android/app/release
|
mkdir -p snort_android/app/release
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -12,5 +12,4 @@ dist/
|
|||||||
*.log
|
*.log
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.pnp*
|
.pnp*
|
||||||
docs/
|
docs/
|
||||||
.wrangler/
|
|
@ -38,14 +38,13 @@ Snort supports the following NIP's:
|
|||||||
- [x] NIP-50: Search
|
- [x] NIP-50: Search
|
||||||
- [x] NIP-51: Lists
|
- [x] NIP-51: Lists
|
||||||
- [x] NIP-53: Live Events
|
- [x] NIP-53: Live Events
|
||||||
- [x] NIP-55: Android signer application
|
|
||||||
- [x] NIP-57: Zaps
|
- [x] NIP-57: Zaps
|
||||||
- [x] NIP-58: Badges
|
- [x] NIP-58: Badges
|
||||||
- [x] NIP-59: Gift Wrap
|
- [x] NIP-59: Gift Wrap
|
||||||
- [x] NIP-65: Relay List Metadata
|
- [x] NIP-65: Relay List Metadata
|
||||||
- [x] NIP-75: Zap Goals
|
- [x] NIP-75: Zap Goals
|
||||||
- [x] NIP-78: App specific data
|
- [x] NIP-78: App specific data
|
||||||
- [x] NIP-89: App handlers
|
- [ ] NIP-89: App handlers
|
||||||
- [x] NIP-94: File Metadata
|
- [x] NIP-94: File Metadata
|
||||||
- [x] NIP-96: HTTP File Storage Integration (Draft)
|
- [x] NIP-96: HTTP File Storage Integration (Draft)
|
||||||
- [x] NIP-98: HTTP Auth
|
- [x] NIP-98: HTTP Auth
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
# Reactions
|
|
||||||
|
|
||||||
## Problem
|
|
||||||
|
|
||||||
When presented with a feed of notes, either a timeline (social) or a live chat log (live stream chat)
|
|
||||||
how do you fetch the reactions to such notes and maintain realtime updates.
|
|
||||||
|
|
||||||
## Current solution
|
|
||||||
|
|
||||||
When a list of reactions is requested we use the expensive `buildDiff` operation to compute a
|
|
||||||
list of new (added) filters and send them to relays.
|
|
||||||
|
|
||||||
Usually if `leaveOpen` is specified (as it should be for realtime updates) this new trace will be sent
|
|
||||||
as a separate subscription causing exhasution.
|
|
||||||
|
|
||||||
Another side effect of this this approach is that over time (especially in live chat) the number of filters that get passed to `buildDiff` increases and so the computation time takes longer and causes jank (https://git.v0l.io/Kieran/zap.stream/issues/126).
|
|
||||||
|
|
||||||
There is also the question of updating the "root" query, since this is not updated, each independant query trace receives its own set of updates which is a problem of its own.
|
|
||||||
|
|
||||||
## Proposed solution (Live chat)
|
|
||||||
|
|
||||||
The ideal solution is to update only the "root" query as new filters are detected along with appending the current timestamp as the `since` value.
|
|
||||||
|
|
||||||
In this way only 1 subscription is maintained, the "root" query trace.
|
|
||||||
|
|
||||||
Each time a new set of filters is created from `buildDiff` we push the same `REQ` again with the new filters which **should** result in no new results from the relays as we expect there to be none `since` the current time is the time of the latest message.
|
|
||||||
|
|
||||||
## Proposed solution (Timeline)
|
|
||||||
|
|
||||||
TBD
|
|
@ -37,7 +37,7 @@ export const onRequest: PagesFunction<Env> = async context => {
|
|||||||
return new Response(body, {
|
return new Response(body, {
|
||||||
headers: {
|
headers: {
|
||||||
...Object.fromEntries(rsp.headers.entries()),
|
...Object.fromEntries(rsp.headers.entries()),
|
||||||
"cache-control": "no-cache",
|
"cache-control": "public, max-age=60",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
maintainers:
|
|
||||||
- npub1g53mukxnjkcmr94fhryzkqutdz2ukq4ks0gvy5af25rgmwsl4ngq43drvk
|
|
||||||
- npub1v0lxxxxutpvrelsksy8cdhgfux9l6a42hsj2qzquu2zk7vc9qnkszrqj49
|
|
||||||
relays:
|
|
||||||
- wss://relay.snort.social/
|
|
||||||
- wss://pyramid.fiatjaf.com/
|
|
||||||
- wss://nos.lol/
|
|
||||||
- ws://skzzn6cimfdv5e2phjc4yr5v7ikbxtn5f7dkwn5c7v47tduzlbosqmqd.onion/
|
|
11
nap.yaml
11
nap.yaml
@ -1,11 +0,0 @@
|
|||||||
id: "social.snort.app"
|
|
||||||
name: "Snort"
|
|
||||||
description: ""
|
|
||||||
icon: "https://snort.social/nostrich_256.png"
|
|
||||||
images:
|
|
||||||
- "https://snort.social/nostrich_512.png"
|
|
||||||
repository: "https://github.com/v0l/snort"
|
|
||||||
license: "MIT"
|
|
||||||
tags:
|
|
||||||
- "social"
|
|
||||||
- "twitter"
|
|
@ -21,12 +21,12 @@
|
|||||||
"packageManager": "yarn@4.1.1",
|
"packageManager": "yarn@4.1.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/workers-types": "^4.20230307.0",
|
"@cloudflare/workers-types": "^4.20230307.0",
|
||||||
|
"@tauri-apps/cli": "^1.2.3",
|
||||||
"eslint": "^8.48.0",
|
"eslint": "^8.48.0",
|
||||||
"prettier": "^3.0.3",
|
"prettier": "^3.0.3",
|
||||||
"typescript": "^5.2.2"
|
"typescript": "^5.2.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2.0.0-rc.14",
|
|
||||||
"typedoc": "^0.25.7"
|
"typedoc": "^0.25.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,12 @@ module.exports = {
|
|||||||
parser: "@typescript-eslint/parser",
|
parser: "@typescript-eslint/parser",
|
||||||
plugins: ["@typescript-eslint", "formatjs", "react-refresh", "simple-import-sort"],
|
plugins: ["@typescript-eslint", "formatjs", "react-refresh", "simple-import-sort"],
|
||||||
rules: {
|
rules: {
|
||||||
|
"formatjs/enforce-id": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
idInterpolationPattern: "[sha512:contenthash:base64:6]",
|
||||||
|
},
|
||||||
|
],
|
||||||
"react/react-in-jsx-scope": "off",
|
"react/react-in-jsx-scope": "off",
|
||||||
"react-hooks/exhaustive-deps": "off",
|
"react-hooks/exhaustive-deps": "off",
|
||||||
"react-refresh/only-export-components": "error",
|
"react-refresh/only-export-components": "error",
|
||||||
|
3
packages/app/.gitignore
vendored
3
packages/app/.gitignore
vendored
@ -25,5 +25,4 @@ yarn-error.log*
|
|||||||
.idea
|
.idea
|
||||||
|
|
||||||
dist/
|
dist/
|
||||||
dev-dist/
|
dev-dist/
|
||||||
.wrangler/
|
|
@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"plugins": [
|
|
||||||
[
|
|
||||||
"formatjs",
|
|
||||||
{
|
|
||||||
"idInterpolationPattern": "[sha512:contenthash:base64:6]",
|
|
||||||
"ast": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
@ -12,10 +12,11 @@
|
|||||||
"defaultZapPoolFee": 1,
|
"defaultZapPoolFee": 1,
|
||||||
"features": {
|
"features": {
|
||||||
"analytics": true,
|
"analytics": true,
|
||||||
"subscriptions": false,
|
"subscriptions": true,
|
||||||
"deck": false,
|
"deck": true,
|
||||||
"zapPool": false,
|
"zapPool": true,
|
||||||
"communityLeaders": false,
|
"notificationGraph": true,
|
||||||
|
"communityLeaders": true,
|
||||||
"nostrAddress": true,
|
"nostrAddress": true,
|
||||||
"pushNotifications": true
|
"pushNotifications": true
|
||||||
},
|
},
|
||||||
@ -35,32 +36,23 @@
|
|||||||
"list": "naddr1qq4xc6tnw3ez6vp58y6rywpjxckngdtyxukngwr9vckkze33vcknzcnrxcenje35xqmn2cczyp3lucccm3v9s087z6qslpkap8schltk427zfgqgrn3g2menq5zw6qcyqqq82vqprpmhxue69uhhyetvv9ujuumwdae8gtnnda3kjctv7rajfl"
|
"list": "naddr1qq4xc6tnw3ez6vp58y6rywpjxckngdtyxukngwr9vckkze33vcknzcnrxcenje35xqmn2cczyp3lucccm3v9s087z6qslpkap8schltk427zfgqgrn3g2menq5zw6qcyqqq82vqprpmhxue69uhhyetvv9ujuumwdae8gtnnda3kjctv7rajfl"
|
||||||
},
|
},
|
||||||
"noteCreatorToast": false,
|
"noteCreatorToast": false,
|
||||||
"hideFromNavbar": [],
|
"hideFromNavbar": ["/graph"],
|
||||||
"deckSubKind": 1,
|
"deckSubKind": 1,
|
||||||
"showPowIcon": true,
|
"showPowIcon": true,
|
||||||
"eventLinkPrefix": "nevent",
|
"eventLinkPrefix": "nevent",
|
||||||
"profileLinkPrefix": "nprofile",
|
"profileLinkPrefix": "nprofile",
|
||||||
"defaultRelays": {
|
"defaultRelays": {
|
||||||
"wss://relay.snort.social/": {
|
"wss://relay.snort.social/": { "read": true, "write": true },
|
||||||
"read": true,
|
"wss://nostr.wine/": { "read": true, "write": false },
|
||||||
"write": true
|
"wss://relay.damus.io/": { "read": true, "write": true },
|
||||||
},
|
"wss://nos.lol/": { "read": true, "write": true }
|
||||||
"wss://nostr.wine/": {
|
|
||||||
"read": true,
|
|
||||||
"write": false
|
|
||||||
},
|
|
||||||
"wss://relay.damus.io/": {
|
|
||||||
"read": true,
|
|
||||||
"write": true
|
|
||||||
},
|
|
||||||
"wss://nos.lol/": {
|
|
||||||
"read": true,
|
|
||||||
"write": true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"alby": {
|
"alby": {
|
||||||
"clientId": "pohiJjPhQR",
|
"clientId": "pohiJjPhQR",
|
||||||
"clientSecret": "GAl1YKLA3FveK1gLBYok"
|
"clientSecret": "GAl1YKLA3FveK1gLBYok"
|
||||||
},
|
},
|
||||||
"chatChannels": []
|
"chatChannels": [
|
||||||
|
{ "type": "telegram", "value": "https://t.me/irismessenger" },
|
||||||
|
{ "type": "nip28", "value": "23286a4602ada10cc10200553bff62a110e8dc0eacddf73277395a89ddf26a09" }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
"subscriptions": true,
|
"subscriptions": true,
|
||||||
"deck": true,
|
"deck": true,
|
||||||
"zapPool": true,
|
"zapPool": true,
|
||||||
|
"notificationGraph": false,
|
||||||
"communityLeaders": true
|
"communityLeaders": true
|
||||||
},
|
},
|
||||||
"defaultPreferences": {
|
"defaultPreferences": {
|
||||||
|
@ -1,49 +0,0 @@
|
|||||||
{
|
|
||||||
"appName": "めく",
|
|
||||||
"appNameCapitalized": "めく",
|
|
||||||
"appTitle": "めく",
|
|
||||||
"hostname": "meku.app",
|
|
||||||
"nip05Domain": "meku.app",
|
|
||||||
"icon": "/nostr.jpg",
|
|
||||||
"navLogo": null,
|
|
||||||
"publicDir": "public/nostr",
|
|
||||||
"httpCache": "",
|
|
||||||
"animalNamePlaceholders": false,
|
|
||||||
"defaultZapPoolFee": 0,
|
|
||||||
"features": {
|
|
||||||
"analytics": true,
|
|
||||||
"subscriptions": false,
|
|
||||||
"deck": false,
|
|
||||||
"zapPool": false,
|
|
||||||
"communityLeaders": false,
|
|
||||||
"nostrAddress": false,
|
|
||||||
"pushNotifications": true
|
|
||||||
},
|
|
||||||
"signUp": {
|
|
||||||
"quickStart": false,
|
|
||||||
"defaultFollows": []
|
|
||||||
},
|
|
||||||
"defaultPreferences": {
|
|
||||||
"hideMutedNotes": false,
|
|
||||||
"defaultRootTab": "following",
|
|
||||||
"language": "ja"
|
|
||||||
},
|
|
||||||
"media": {
|
|
||||||
"bypassImgProxyError": false,
|
|
||||||
"preferLargeMedia": true
|
|
||||||
},
|
|
||||||
"communityLeaders": null,
|
|
||||||
"noteCreatorToast": false,
|
|
||||||
"hideFromNavbar": [],
|
|
||||||
"deckSubKind": 1,
|
|
||||||
"showPowIcon": true,
|
|
||||||
"eventLinkPrefix": "nevent",
|
|
||||||
"profileLinkPrefix": "nprofile",
|
|
||||||
"defaultRelays": {
|
|
||||||
"wss://relay.nostr.wirednet.jp/": { "read": true, "write": true },
|
|
||||||
"wss://yabu.me/": { "read": true, "write": true },
|
|
||||||
"wss://nos.lol/": { "read": true, "write": true }
|
|
||||||
},
|
|
||||||
"alby": null,
|
|
||||||
"chatChannels": null
|
|
||||||
}
|
|
@ -15,6 +15,7 @@
|
|||||||
"subscriptions": false,
|
"subscriptions": false,
|
||||||
"deck": false,
|
"deck": false,
|
||||||
"zapPool": false,
|
"zapPool": false,
|
||||||
|
"notificationGraph": true,
|
||||||
"communityLeaders": false,
|
"communityLeaders": false,
|
||||||
"nostrAddress": false,
|
"nostrAddress": false,
|
||||||
"pushNotifications": false
|
"pushNotifications": false
|
||||||
@ -33,7 +34,7 @@
|
|||||||
},
|
},
|
||||||
"communityLeaders": null,
|
"communityLeaders": null,
|
||||||
"noteCreatorToast": true,
|
"noteCreatorToast": true,
|
||||||
"hideFromNavbar": [],
|
"hideFromNavbar": ["/graph"],
|
||||||
"deckSubKind": 1,
|
"deckSubKind": 1,
|
||||||
"showPowIcon": true,
|
"showPowIcon": true,
|
||||||
"eventLinkPrefix": "nevent",
|
"eventLinkPrefix": "nevent",
|
||||||
|
@ -1,49 +0,0 @@
|
|||||||
{
|
|
||||||
"appName": "Soloco",
|
|
||||||
"appNameCapitalized": "Soloco",
|
|
||||||
"appTitle": "Soloco",
|
|
||||||
"hostname": "soloco.nl",
|
|
||||||
"nip05Domain": "soloco.nl",
|
|
||||||
"icon": "/nostrich_512.png",
|
|
||||||
"favicon": "public/favicon.ico",
|
|
||||||
"appleTouchIconUrl": "/nostrich_512.png",
|
|
||||||
"navLogo": null,
|
|
||||||
"publicDir": "public/snort",
|
|
||||||
"httpCache": "",
|
|
||||||
"animalNamePlaceholders": false,
|
|
||||||
"defaultZapPoolFee": 0,
|
|
||||||
"features": {
|
|
||||||
"analytics": false,
|
|
||||||
"subscriptions": false,
|
|
||||||
"deck": false,
|
|
||||||
"zapPool": false,
|
|
||||||
"communityLeaders": false,
|
|
||||||
"nostrAddress": false,
|
|
||||||
"pushNotifications": true
|
|
||||||
},
|
|
||||||
"signUp": {
|
|
||||||
"quickStart": false,
|
|
||||||
"defaultFollows": []
|
|
||||||
},
|
|
||||||
"defaultPreferences": {
|
|
||||||
"hideMutedNotes": false,
|
|
||||||
"defaultRootTab": "following",
|
|
||||||
"language": "nl"
|
|
||||||
},
|
|
||||||
"media": {
|
|
||||||
"bypassImgProxyError": false,
|
|
||||||
"preferLargeMedia": true
|
|
||||||
},
|
|
||||||
"communityLeaders": null,
|
|
||||||
"noteCreatorToast": true,
|
|
||||||
"hideFromNavbar": [],
|
|
||||||
"deckSubKind": 1,
|
|
||||||
"showPowIcon": true,
|
|
||||||
"eventLinkPrefix": "nevent",
|
|
||||||
"profileLinkPrefix": "nprofile",
|
|
||||||
"defaultRelays": {
|
|
||||||
"wss://soloco.nl/": { "read": true, "write": false }
|
|
||||||
},
|
|
||||||
"alby": null,
|
|
||||||
"chatChannels": null
|
|
||||||
}
|
|
1
packages/app/custom.d.ts
vendored
1
packages/app/custom.d.ts
vendored
@ -57,6 +57,7 @@ declare const CONFIG: {
|
|||||||
subscriptions: boolean;
|
subscriptions: boolean;
|
||||||
deck: boolean;
|
deck: boolean;
|
||||||
zapPool: boolean;
|
zapPool: boolean;
|
||||||
|
notificationGraph: boolean;
|
||||||
communityLeaders: boolean;
|
communityLeaders: boolean;
|
||||||
nostrAddress: boolean;
|
nostrAddress: boolean;
|
||||||
pushNotifications: boolean;
|
pushNotifications: boolean;
|
||||||
|
@ -4,13 +4,11 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cashu/cashu-ts": "^1.0.0-rc.3",
|
"@cashu/cashu-ts": "^1.0.0-rc.3",
|
||||||
"@here/maps-api-for-javascript": "^1.50.0",
|
"@here/maps-api-for-javascript": "^1.50.0",
|
||||||
"@livekit/components-react": "^2.5.4",
|
"@noble/curves": "^1.0.0",
|
||||||
"@livekit/protocol": "^1.22.0",
|
"@noble/hashes": "^1.3.3",
|
||||||
"@noble/curves": "^1.4.0",
|
"@scure/base": "^1.1.1",
|
||||||
"@noble/hashes": "^1.4.0",
|
"@scure/bip32": "^1.3.0",
|
||||||
"@scure/base": "^1.1.6",
|
"@scure/bip39": "^1.1.1",
|
||||||
"@scure/bip32": "^1.5.0",
|
|
||||||
"@scure/bip39": "^1.4.0",
|
|
||||||
"@snort/shared": "workspace:*",
|
"@snort/shared": "workspace:*",
|
||||||
"@snort/system": "workspace:*",
|
"@snort/system": "workspace:*",
|
||||||
"@snort/system-react": "workspace:*",
|
"@snort/system-react": "workspace:*",
|
||||||
@ -18,7 +16,7 @@
|
|||||||
"@snort/system-web": "workspace:*",
|
"@snort/system-web": "workspace:*",
|
||||||
"@snort/wallet": "workspace:*",
|
"@snort/wallet": "workspace:*",
|
||||||
"@snort/worker-relay": "workspace:*",
|
"@snort/worker-relay": "workspace:*",
|
||||||
"@szhsin/react-menu": "^3.5.3",
|
"@szhsin/react-menu": "^3.3.1",
|
||||||
"@uidotdev/usehooks": "^2.4.1",
|
"@uidotdev/usehooks": "^2.4.1",
|
||||||
"@void-cat/api": "^1.0.12",
|
"@void-cat/api": "^1.0.12",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
@ -31,7 +29,6 @@
|
|||||||
"highlight.js": "^11.8.0",
|
"highlight.js": "^11.8.0",
|
||||||
"latlon-geohash": "^2.0.0",
|
"latlon-geohash": "^2.0.0",
|
||||||
"light-bolt11-decoder": "^2.1.0",
|
"light-bolt11-decoder": "^2.1.0",
|
||||||
"livekit-client": "^2.5.2",
|
|
||||||
"lottie-react": "^2.4.0",
|
"lottie-react": "^2.4.0",
|
||||||
"marked": "^9.1.0",
|
"marked": "^9.1.0",
|
||||||
"marked-footnote": "^1.0.0",
|
"marked-footnote": "^1.0.0",
|
||||||
@ -47,7 +44,6 @@
|
|||||||
"react-textarea-autosize": "^8.4.0",
|
"react-textarea-autosize": "^8.4.0",
|
||||||
"recharts": "^2.8.0",
|
"recharts": "^2.8.0",
|
||||||
"three": "^0.157.0",
|
"three": "^0.157.0",
|
||||||
"tslib": "^2.7.0",
|
|
||||||
"typescript-lru-cache": "^2.0.0",
|
"typescript-lru-cache": "^2.0.0",
|
||||||
"use-long-press": "^3.2.0",
|
"use-long-press": "^3.2.0",
|
||||||
"use-sync-external-store": "^1.2.0",
|
"use-sync-external-store": "^1.2.0",
|
||||||
@ -67,9 +63,7 @@
|
|||||||
"test:watch": "vitest watch",
|
"test:watch": "vitest watch",
|
||||||
"intl-extract": "formatjs extract 'src/**/*.ts*' --ignore='**/*.d.ts' --out-file src/lang.json --flatten true",
|
"intl-extract": "formatjs extract 'src/**/*.ts*' --ignore='**/*.d.ts' --out-file src/lang.json --flatten true",
|
||||||
"intl-compile": "formatjs compile src/lang.json --out-file src/translations/en.json",
|
"intl-compile": "formatjs compile src/lang.json --out-file src/translations/en.json",
|
||||||
"eslint": "eslint .",
|
"eslint": "eslint ."
|
||||||
"deploy:meku": "NODE_CONFIG_ENV=meku yarn build && npx wrangler pages deploy --project-name meku build/",
|
|
||||||
"deploy:notestr": "NODE_CONFIG_ENV=nostr yarn build && npx wrangler pages deploy --project-name nostr-generic build/"
|
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"extends": [
|
||||||
@ -105,13 +99,11 @@
|
|||||||
"@types/webtorrent": "^0.109.3",
|
"@types/webtorrent": "^0.109.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.1.0",
|
"@typescript-eslint/eslint-plugin": "^6.1.0",
|
||||||
"@typescript-eslint/parser": "^6.1.0",
|
"@typescript-eslint/parser": "^6.1.0",
|
||||||
"@vitejs/plugin-basic-ssl": "^1.2.0",
|
|
||||||
"@vitejs/plugin-react": "^4.2.0",
|
"@vitejs/plugin-react": "^4.2.0",
|
||||||
"@webbtc/webln-types": "^3.0.0",
|
"@webbtc/webln-types": "^2.1.0",
|
||||||
"@webscopeio/react-textarea-autocomplete": "^4.9.2",
|
"@webscopeio/react-textarea-autocomplete": "^4.9.2",
|
||||||
"@welldone-software/why-did-you-render": "^8.0.1",
|
"@welldone-software/why-did-you-render": "^8.0.1",
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.16",
|
||||||
"babel-plugin-formatjs": "^10.5.14",
|
|
||||||
"config": "^3.3.9",
|
"config": "^3.3.9",
|
||||||
"eslint": "^8.48.0",
|
"eslint": "^8.48.0",
|
||||||
"eslint-config-react-app": "^7.0.1",
|
"eslint-config-react-app": "^7.0.1",
|
||||||
|
@ -1,4 +1,2 @@
|
|||||||
/*
|
/*
|
||||||
Content-Security-Policy: default-src 'self'; manifest-src *; child-src 'none'; worker-src 'self'; frame-src https://youtube.com https://www.youtube.com https://platform.twitter.com https://embed.tidal.com https://w.soundcloud.com https://www.mixcloud.com https://open.spotify.com https://player.twitch.tv https://embed.music.apple.com https://embed.wavlake.com https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline'; connect-src *; img-src * data: blob:; font-src 'self'; media-src * blob:; script-src 'self' 'wasm-unsafe-eval' https://platform.twitter.com https://embed.tidal.com https://challenges.cloudflare.com;
|
Content-Security-Policy: default-src 'self'; manifest-src *; child-src 'none'; worker-src 'self'; frame-src https://youtube.com https://www.youtube.com https://platform.twitter.com https://embed.tidal.com https://w.soundcloud.com https://www.mixcloud.com https://open.spotify.com https://player.twitch.tv https://embed.music.apple.com https://embed.wavlake.com https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline'; connect-src *; img-src * data: blob:; font-src 'self'; media-src * blob:; script-src 'self' 'wasm-unsafe-eval' https://platform.twitter.com https://embed.tidal.com https://challenges.cloudflare.com;
|
||||||
/service-worker.js
|
|
||||||
Cache-Control: max-age=604800, must-revalidate;
|
|
@ -1,4 +1,2 @@
|
|||||||
/*
|
/*
|
||||||
Content-Security-Policy: default-src 'self'; manifest-src *; child-src 'none'; worker-src 'self'; frame-src https://youtube.com https://www.youtube.com https://platform.twitter.com https://embed.tidal.com https://w.soundcloud.com https://www.mixcloud.com https://open.spotify.com https://player.twitch.tv https://embed.music.apple.com https://embed.wavlake.com https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline'; connect-src *; img-src * data: blob:; font-src 'self'; media-src * blob:; script-src 'self' 'wasm-unsafe-eval' https://platform.twitter.com https://embed.tidal.com https://challenges.cloudflare.com;
|
Content-Security-Policy: default-src 'self'; manifest-src *; child-src 'none'; worker-src 'self'; frame-src https://youtube.com https://www.youtube.com https://platform.twitter.com https://embed.tidal.com https://w.soundcloud.com https://www.mixcloud.com https://open.spotify.com https://player.twitch.tv https://embed.music.apple.com https://embed.wavlake.com https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline'; connect-src *; img-src * data: blob:; font-src 'self'; media-src * blob:; script-src 'self' 'wasm-unsafe-eval' https://platform.twitter.com https://embed.tidal.com https://challenges.cloudflare.com;
|
||||||
/service-worker.js
|
|
||||||
Cache-Control: max-age=604800, must-revalidate;
|
|
@ -27,8 +27,4 @@ export class ChatCache extends FeedCache<NostrEvent> {
|
|||||||
takeSnapshot(): Array<NostrEvent> {
|
takeSnapshot(): Array<NostrEvent> {
|
||||||
return [...this.cache.values()];
|
return [...this.cache.values()];
|
||||||
}
|
}
|
||||||
|
|
||||||
async search() {
|
|
||||||
return <Array<NostrEvent>>[];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import { CachedTable, CacheEvents } from "@snort/shared";
|
import { CachedTable, CacheEvents } from "@snort/shared";
|
||||||
import { CacheRelay, NostrEvent } from "@snort/system";
|
import { NostrEvent } from "@snort/system";
|
||||||
|
import { WorkerRelayInterface } from "@snort/worker-relay";
|
||||||
import { EventEmitter } from "eventemitter3";
|
import { EventEmitter } from "eventemitter3";
|
||||||
|
|
||||||
export class EventCacheWorker extends EventEmitter<CacheEvents> implements CachedTable<NostrEvent> {
|
export class EventCacheWorker extends EventEmitter<CacheEvents> implements CachedTable<NostrEvent> {
|
||||||
#relay: CacheRelay;
|
#relay: WorkerRelayInterface;
|
||||||
#keys = new Set<string>();
|
#keys = new Set<string>();
|
||||||
#cache = new Map<string, NostrEvent>();
|
#cache = new Map<string, NostrEvent>();
|
||||||
|
|
||||||
constructor(relay: CacheRelay) {
|
constructor(relay: WorkerRelayInterface) {
|
||||||
super();
|
super();
|
||||||
this.#relay = relay;
|
this.#relay = relay;
|
||||||
}
|
}
|
||||||
@ -23,17 +24,6 @@ export class EventCacheWorker extends EventEmitter<CacheEvents> implements Cache
|
|||||||
this.#keys = new Set<string>(ids as unknown as Array<string>);
|
this.#keys = new Set<string>(ids as unknown as Array<string>);
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(q: string) {
|
|
||||||
const results = await this.#relay.query([
|
|
||||||
"REQ",
|
|
||||||
"events-search",
|
|
||||||
{
|
|
||||||
search: q,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
keysOnTable(): string[] {
|
keysOnTable(): string[] {
|
||||||
return [...this.#keys];
|
return [...this.#keys];
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { EventKind, EventPublisher, TaggedNostrEvent } from "@snort/system";
|
import { EventKind, EventPublisher, RequestBuilder, TaggedNostrEvent } from "@snort/system";
|
||||||
|
|
||||||
import { db, UnwrappedGift } from "@/Db";
|
import { db, UnwrappedGift } from "@/Db";
|
||||||
import { findTag, unwrap } from "@/Utils";
|
import { findTag, unwrap } from "@/Utils";
|
||||||
|
import { LoginSession, LoginSessionType } from "@/Utils/Login";
|
||||||
|
|
||||||
import { RefreshFeedCache, TWithCreated } from "./RefreshFeedCache";
|
import { RefreshFeedCache } from "./RefreshFeedCache";
|
||||||
|
|
||||||
export class GiftWrapCache extends RefreshFeedCache<UnwrappedGift> {
|
export class GiftWrapCache extends RefreshFeedCache<UnwrappedGift> {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -14,8 +15,11 @@ export class GiftWrapCache extends RefreshFeedCache<UnwrappedGift> {
|
|||||||
return of.id;
|
return of.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
buildSub(): void {
|
buildSub(session: LoginSession, rb: RequestBuilder): void {
|
||||||
// not used
|
const pubkey = session.publicKey;
|
||||||
|
if (pubkey && session.type === LoginSessionType.PrivateKey) {
|
||||||
|
rb.withFilter().kinds([EventKind.GiftWrap]).tag("p", [pubkey]).since(this.newest());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
takeSnapshot(): Array<UnwrappedGift> {
|
takeSnapshot(): Array<UnwrappedGift> {
|
||||||
@ -53,8 +57,4 @@ export class GiftWrapCache extends RefreshFeedCache<UnwrappedGift> {
|
|||||||
}
|
}
|
||||||
await this.bulkSet(unwrapped);
|
await this.bulkSet(unwrapped);
|
||||||
}
|
}
|
||||||
|
|
||||||
search(): Promise<TWithCreated<UnwrappedGift>[]> {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import { CachedTable, CacheEvents, removeUndefined, unixNowMs, unwrap } from "@snort/shared";
|
import { CachedTable, CacheEvents, removeUndefined, unixNowMs, unwrap } from "@snort/shared";
|
||||||
import { CachedMetadata, CacheRelay, mapEventToProfile, NostrEvent } from "@snort/system";
|
import { CachedMetadata, mapEventToProfile, NostrEvent } from "@snort/system";
|
||||||
|
import { WorkerRelayInterface } from "@snort/worker-relay";
|
||||||
import debug from "debug";
|
import debug from "debug";
|
||||||
import { EventEmitter } from "eventemitter3";
|
import { EventEmitter } from "eventemitter3";
|
||||||
|
|
||||||
export class ProfileCacheRelayWorker extends EventEmitter<CacheEvents> implements CachedTable<CachedMetadata> {
|
export class ProfileCacheRelayWorker extends EventEmitter<CacheEvents> implements CachedTable<CachedMetadata> {
|
||||||
#relay: CacheRelay;
|
#relay: WorkerRelayInterface;
|
||||||
#keys = new Set<string>();
|
#keys = new Set<string>();
|
||||||
#cache = new Map<string, CachedMetadata>();
|
#cache = new Map<string, CachedMetadata>();
|
||||||
#log = debug("ProfileCacheRelayWorker");
|
#log = debug("ProfileCacheRelayWorker");
|
||||||
|
|
||||||
constructor(relay: CacheRelay) {
|
constructor(relay: WorkerRelayInterface) {
|
||||||
super();
|
super();
|
||||||
this.#relay = relay;
|
this.#relay = relay;
|
||||||
}
|
}
|
||||||
@ -28,18 +29,6 @@ export class ProfileCacheRelayWorker extends EventEmitter<CacheEvents> implement
|
|||||||
this.#log(`Loaded %d/%d in %d ms`, this.#cache.size, this.#keys.size, (unixNowMs() - start).toLocaleString());
|
this.#log(`Loaded %d/%d in %d ms`, this.#cache.size, this.#keys.size, (unixNowMs() - start).toLocaleString());
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(q: string) {
|
|
||||||
const profiles = await this.#relay.query([
|
|
||||||
"REQ",
|
|
||||||
"profiles-search",
|
|
||||||
{
|
|
||||||
kinds: [0],
|
|
||||||
search: q,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
return removeUndefined(profiles.map(mapEventToProfile));
|
|
||||||
}
|
|
||||||
|
|
||||||
keysOnTable(): string[] {
|
keysOnTable(): string[] {
|
||||||
return [...this.#keys];
|
return [...this.#keys];
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import { CachedTable, CacheEvents, removeUndefined, unixNowMs, unwrap } from "@snort/shared";
|
import { CachedTable, CacheEvents, removeUndefined, unixNowMs, unwrap } from "@snort/shared";
|
||||||
import { CacheRelay, EventKind, NostrEvent, UsersFollows } from "@snort/system";
|
import { EventKind, NostrEvent, UsersFollows } from "@snort/system";
|
||||||
|
import { WorkerRelayInterface } from "@snort/worker-relay";
|
||||||
import debug from "debug";
|
import debug from "debug";
|
||||||
import { EventEmitter } from "eventemitter3";
|
import { EventEmitter } from "eventemitter3";
|
||||||
|
|
||||||
export class UserFollowsWorker extends EventEmitter<CacheEvents> implements CachedTable<UsersFollows> {
|
export class UserFollowsWorker extends EventEmitter<CacheEvents> implements CachedTable<UsersFollows> {
|
||||||
#relay: CacheRelay;
|
#relay: WorkerRelayInterface;
|
||||||
#keys = new Set<string>();
|
#keys = new Set<string>();
|
||||||
#cache = new Map<string, UsersFollows>();
|
#cache = new Map<string, UsersFollows>();
|
||||||
#log = debug("UserFollowsWorker");
|
#log = debug("UserFollowsWorker");
|
||||||
|
|
||||||
constructor(relay: CacheRelay) {
|
constructor(relay: WorkerRelayInterface) {
|
||||||
super();
|
super();
|
||||||
this.#relay = relay;
|
this.#relay = relay;
|
||||||
}
|
}
|
||||||
@ -28,18 +29,6 @@ export class UserFollowsWorker extends EventEmitter<CacheEvents> implements Cach
|
|||||||
this.#log(`Loaded %d/%d in %d ms`, this.#cache.size, this.#keys.size, (unixNowMs() - start).toLocaleString());
|
this.#log(`Loaded %d/%d in %d ms`, this.#cache.size, this.#keys.size, (unixNowMs() - start).toLocaleString());
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(q: string) {
|
|
||||||
const results = await this.#relay.query([
|
|
||||||
"REQ",
|
|
||||||
"contacts-search",
|
|
||||||
{
|
|
||||||
kinds: [3],
|
|
||||||
search: q,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
return removeUndefined(results.map(mapEventToUserFollows));
|
|
||||||
}
|
|
||||||
|
|
||||||
keysOnTable(): string[] {
|
keysOnTable(): string[] {
|
||||||
return [...this.#keys];
|
return [...this.#keys];
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { CacheRelay, Connection, ConnectionCacheRelay, RelayMetricCache, UserRelaysCache } from "@snort/system";
|
import { RelayMetricCache, UserRelaysCache } from "@snort/system";
|
||||||
import { SnortSystemDb } from "@snort/system-web";
|
import { SnortSystemDb } from "@snort/system-web";
|
||||||
import { WorkerRelayInterface } from "@snort/worker-relay";
|
import { WorkerRelayInterface } from "@snort/worker-relay";
|
||||||
import WorkerVite from "@snort/worker-relay/src/worker?worker";
|
import WorkerVite from "@snort/worker-relay/src/worker?worker";
|
||||||
@ -8,52 +8,12 @@ import { GiftWrapCache } from "./GiftWrapCache";
|
|||||||
import { ProfileCacheRelayWorker } from "./ProfileWorkerCache";
|
import { ProfileCacheRelayWorker } from "./ProfileWorkerCache";
|
||||||
import { UserFollowsWorker } from "./UserFollowsWorker";
|
import { UserFollowsWorker } from "./UserFollowsWorker";
|
||||||
|
|
||||||
const cacheRelay = localStorage.getItem("cache-relay");
|
export const Relay = new WorkerRelayInterface(
|
||||||
|
|
||||||
const workerRelay = new WorkerRelayInterface(
|
|
||||||
import.meta.env.DEV ? new URL("@snort/worker-relay/dist/esm/worker.mjs", import.meta.url) : new WorkerVite(),
|
import.meta.env.DEV ? new URL("@snort/worker-relay/dist/esm/worker.mjs", import.meta.url) : new WorkerVite(),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const Relay: CacheRelay = cacheRelay
|
|
||||||
? new ConnectionCacheRelay(new Connection(cacheRelay, { read: true, write: true }))
|
|
||||||
: workerRelay;
|
|
||||||
|
|
||||||
async function tryUseCacheRelay(url: string) {
|
|
||||||
try {
|
|
||||||
const conn = new Connection(url, { read: true, write: true });
|
|
||||||
await conn.connect(true);
|
|
||||||
localStorage.setItem("cache-relay", url);
|
|
||||||
return conn;
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function tryUseLocalRelay() {
|
|
||||||
let conn = await tryUseCacheRelay("ws://localhost:4869");
|
|
||||||
if (!conn) {
|
|
||||||
conn = await tryUseCacheRelay("ws://umbrel:4848");
|
|
||||||
}
|
|
||||||
return conn;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function initRelayWorker() {
|
export async function initRelayWorker() {
|
||||||
try {
|
try {
|
||||||
if (Relay instanceof ConnectionCacheRelay) {
|
await Relay.init({
|
||||||
await Relay.connection.connect(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
localStorage.removeItem("cache-relay");
|
|
||||||
console.error(e);
|
|
||||||
if (cacheRelay) {
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await workerRelay.debug("*");
|
|
||||||
await workerRelay.init({
|
|
||||||
databasePath: "relay.db",
|
databasePath: "relay.db",
|
||||||
insertBatchSize: 100,
|
insertBatchSize: 100,
|
||||||
});
|
});
|
||||||
|
@ -12,12 +12,7 @@ interface IconButtonProps {
|
|||||||
|
|
||||||
const IconButton = ({ onClick, icon, children, className }: IconButtonProps) => {
|
const IconButton = ({ onClick, icon, children, className }: IconButtonProps) => {
|
||||||
return (
|
return (
|
||||||
<button
|
<button className={classNames("icon", className)} type="button" onClick={onClick}>
|
||||||
className={classNames(
|
|
||||||
"flex items-center justify-center aspect-square w-10 h-10 !p-0 !m-0 bg-gray-dark text-white",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
onClick={onClick}>
|
|
||||||
<Icon {...icon} />
|
<Icon {...icon} />
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
|
@ -4,7 +4,7 @@ import { ReactNode, useState } from "react";
|
|||||||
import Icon from "@/Components/Icons/Icon";
|
import Icon from "@/Components/Icons/Icon";
|
||||||
|
|
||||||
interface CollapsedProps {
|
interface CollapsedProps {
|
||||||
text?: ReactNode;
|
text?: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
setCollapsed(b: boolean): void;
|
setCollapsed(b: boolean): void;
|
||||||
@ -33,11 +33,10 @@ interface CollapsedSectionProps {
|
|||||||
title: ReactNode;
|
title: ReactNode;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
startClosed?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CollapsedSection = ({ title, children, className, startClosed }: CollapsedSectionProps) => {
|
export const CollapsedSection = ({ title, children, className }: CollapsedSectionProps) => {
|
||||||
const [collapsed, setCollapsed] = useState(startClosed ?? true);
|
const [collapsed, setCollapsed] = useState(true);
|
||||||
const icon = (
|
const icon = (
|
||||||
<div className={classNames("collapse-icon", { flip: !collapsed })}>
|
<div className={classNames("collapse-icon", { flip: !collapsed })}>
|
||||||
<Icon name="arrowFront" />
|
<Icon name="arrowFront" />
|
||||||
|
@ -19,7 +19,7 @@ export function LeaderBadge() {
|
|||||||
}}>
|
}}>
|
||||||
<AwardIcon size={16} />
|
<AwardIcon size={16} />
|
||||||
<div className="text-xs font-medium text-[#AC88FF]">
|
<div className="text-xs font-medium text-[#AC88FF]">
|
||||||
<FormattedMessage defaultMessage="Community Leader" />
|
<FormattedMessage defaultMessage="Community Leader" id="7YkSA2" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{showModal && (
|
{showModal && (
|
||||||
@ -28,7 +28,7 @@ export function LeaderBadge() {
|
|||||||
<CloseButton className="absolute right-2 top-2" onClick={() => setShowModal(false)} />
|
<CloseButton className="absolute right-2 top-2" onClick={() => setShowModal(false)} />
|
||||||
<AwardIcon size={80} />
|
<AwardIcon size={80} />
|
||||||
<div className="text-3xl font-semibold">
|
<div className="text-3xl font-semibold">
|
||||||
<FormattedMessage defaultMessage="Community Leader" />
|
<FormattedMessage defaultMessage="Community Leader" id="7YkSA2" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-secondary">
|
<p className="text-secondary">
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
@ -38,7 +38,7 @@ export function LeaderBadge() {
|
|||||||
</p>
|
</p>
|
||||||
<Link to="/settings/invite">
|
<Link to="/settings/invite">
|
||||||
<button className="primary">
|
<button className="primary">
|
||||||
<FormattedMessage defaultMessage="Become a leader" />
|
<FormattedMessage defaultMessage="Become a leader" id="M6C/px" />
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
@ -22,12 +22,7 @@ export default function Copy({ text, maxSize = 32, className, showText, mask }:
|
|||||||
: displayText;
|
: displayText;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={classNames("copy flex pointer g8 items-center", className)} onClick={() => copy(text)}>
|
||||||
className={classNames("copy flex pointer g8 items-center", className)}
|
|
||||||
onClick={e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
copy(text);
|
|
||||||
}}>
|
|
||||||
{(showText ?? true) && <span className="copy-body">{trimmed}</span>}
|
{(showText ?? true) && <span className="copy-body">{trimmed}</span>}
|
||||||
<span className="icon" style={{ color: copied ? "var(--success)" : "var(--highlight)" }}>
|
<span className="icon" style={{ color: copied ? "var(--success)" : "var(--highlight)" }}>
|
||||||
{copied ? <Icon name="check" size={14} /> : <Icon name="copy-solid" size={14} />}
|
{copied ? <Icon name="check" size={14} /> : <Icon name="copy-solid" size={14} />}
|
||||||
|
@ -3,15 +3,21 @@ const AppleMusicEmbed = ({ link }: { link: string }) => {
|
|||||||
const isSongLink = /\?i=\d+$/.test(convertedUrl);
|
const isSongLink = /\?i=\d+$/.test(convertedUrl);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<iframe
|
<>
|
||||||
allow="autoplay *; encrypted-media *; fullscreen *; clipboard-write"
|
<iframe
|
||||||
frameBorder="0"
|
allow="autoplay *; encrypted-media *; fullscreen *; clipboard-write"
|
||||||
height={isSongLink ? 175 : 450}
|
frameBorder="0"
|
||||||
style={{ width: "100%", maxWidth: 660, overflow: "hidden", background: "transparent" }}
|
// eslint-disable-next-line react/no-unknown-property
|
||||||
sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation"
|
credentialless=""
|
||||||
src={convertedUrl}
|
height={isSongLink ? 175 : 450}
|
||||||
loading="lazy"
|
style={{ width: "100%", maxWidth: 660, overflow: "hidden", background: "transparent" }}
|
||||||
/>
|
sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation"
|
||||||
|
src={convertedUrl}
|
||||||
|
/>
|
||||||
|
<a href={link} target="_blank" rel="noreferrer" onClick={e => e.stopPropagation()} className="ext">
|
||||||
|
{link}
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ export default function CashuNuts({ token }: { token: string }) {
|
|||||||
<Icon name="copy" />
|
<Icon name="copy" />
|
||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
<AsyncButton onClick={() => redeemToken(token)}>
|
<AsyncButton onClick={() => redeemToken(token)}>
|
||||||
<FormattedMessage defaultMessage="Redeem" />
|
<FormattedMessage defaultMessage="Redeem" id="XrSk2j" description="Button: Redeem Cashu token" />
|
||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { Bech32Regex } from "@snort/shared";
|
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
import AppleMusicEmbed from "@/Components/Embed/AppleMusicEmbed";
|
import AppleMusicEmbed from "@/Components/Embed/AppleMusicEmbed";
|
||||||
@ -11,11 +10,11 @@ import SpotifyEmbed from "@/Components/Embed/SpotifyEmbed";
|
|||||||
import TidalEmbed from "@/Components/Embed/TidalEmbed";
|
import TidalEmbed from "@/Components/Embed/TidalEmbed";
|
||||||
import TwitchEmbed from "@/Components/Embed/TwitchEmbed";
|
import TwitchEmbed from "@/Components/Embed/TwitchEmbed";
|
||||||
import WavlakeEmbed from "@/Components/Embed/WavlakeEmbed";
|
import WavlakeEmbed from "@/Components/Embed/WavlakeEmbed";
|
||||||
import YoutubeEmbed from "@/Components/Embed/YoutubeEmbed";
|
|
||||||
import { magnetURIDecode } from "@/Utils";
|
import { magnetURIDecode } from "@/Utils";
|
||||||
import {
|
import {
|
||||||
AppleMusicRegex,
|
AppleMusicRegex,
|
||||||
MixCloudRegex,
|
MixCloudRegex,
|
||||||
|
NostrNestsRegex,
|
||||||
SoundCloudRegex,
|
SoundCloudRegex,
|
||||||
SpotifyRegex,
|
SpotifyRegex,
|
||||||
TidalRegex,
|
TidalRegex,
|
||||||
@ -35,23 +34,57 @@ export default function HyperText({ link, depth, showLinkPreview, children }: Hy
|
|||||||
const a = link;
|
const a = link;
|
||||||
try {
|
try {
|
||||||
const url = new URL(a);
|
const url = new URL(a);
|
||||||
|
const youtubeId = YoutubeUrlRegex.test(a) && RegExp.$1;
|
||||||
|
const tidalId = TidalRegex.test(a) && RegExp.$1;
|
||||||
|
const soundcloundId = SoundCloudRegex.test(a) && RegExp.$1;
|
||||||
|
const mixcloudId = MixCloudRegex.test(a) && RegExp.$1;
|
||||||
|
const isSpotifyLink = SpotifyRegex.test(a);
|
||||||
|
const isTwitchLink = TwitchRegex.test(a);
|
||||||
|
const isAppleMusicLink = AppleMusicRegex.test(a);
|
||||||
|
const isNostrNestsLink = NostrNestsRegex.test(a);
|
||||||
|
const isWavlakeLink = WavlakeRegex.test(a);
|
||||||
|
|
||||||
let m = null;
|
if (youtubeId) {
|
||||||
if (a.match(YoutubeUrlRegex)) {
|
return (
|
||||||
return <YoutubeEmbed link={a} />;
|
<>
|
||||||
} else if (a.match(TidalRegex)) {
|
<iframe
|
||||||
|
// eslint-disable-next-line react/no-unknown-property
|
||||||
|
credentialless=""
|
||||||
|
className="-mx-4 md:mx-0 w-max my-2"
|
||||||
|
src={`https://www.youtube.com/embed/${youtubeId}`}
|
||||||
|
title="YouTube video player"
|
||||||
|
key={youtubeId}
|
||||||
|
frameBorder="0"
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||||
|
allowFullScreen={true}
|
||||||
|
/>
|
||||||
|
<a href={a} onClick={e => e.stopPropagation()} target="_blank" rel="noreferrer" className="ext">
|
||||||
|
{a}
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else if (tidalId) {
|
||||||
return <TidalEmbed link={a} />;
|
return <TidalEmbed link={a} />;
|
||||||
} else if (a.match(SoundCloudRegex)) {
|
} else if (soundcloundId) {
|
||||||
return <SoundCloudEmbed link={a} />;
|
return <SoundCloudEmbed link={a} />;
|
||||||
} else if (a.match(MixCloudRegex)) {
|
} else if (mixcloudId) {
|
||||||
return <MixCloudEmbed link={a} />;
|
return <MixCloudEmbed link={a} />;
|
||||||
} else if (a.match(SpotifyRegex)) {
|
} else if (isSpotifyLink) {
|
||||||
return <SpotifyEmbed link={a} />;
|
return <SpotifyEmbed link={a} />;
|
||||||
} else if (a.match(TwitchRegex)) {
|
} else if (isTwitchLink) {
|
||||||
return <TwitchEmbed link={a} />;
|
return <TwitchEmbed link={a} />;
|
||||||
} else if (a.match(AppleMusicRegex)) {
|
} else if (isAppleMusicLink) {
|
||||||
return <AppleMusicEmbed link={a} />;
|
return <AppleMusicEmbed link={a} />;
|
||||||
} else if (a.match(WavlakeRegex)) {
|
} else if (isNostrNestsLink) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<a href={a} onClick={e => e.stopPropagation()} target="_blank" rel="noreferrer" className="ext">
|
||||||
|
{children ?? a}
|
||||||
|
</a>
|
||||||
|
{/*<NostrNestsEmbed link={a} />,*/}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else if (isWavlakeLink) {
|
||||||
return <WavlakeEmbed link={a} />;
|
return <WavlakeEmbed link={a} />;
|
||||||
} else if (url.protocol === "nostr:" || url.protocol === "web+nostr:") {
|
} else if (url.protocol === "nostr:" || url.protocol === "web+nostr:") {
|
||||||
return <NostrLink link={a} depth={depth} />;
|
return <NostrLink link={a} depth={depth} />;
|
||||||
@ -60,8 +93,6 @@ export default function HyperText({ link, depth, showLinkPreview, children }: Hy
|
|||||||
if (parsed) {
|
if (parsed) {
|
||||||
return <MagnetLink magnet={parsed} />;
|
return <MagnetLink magnet={parsed} />;
|
||||||
}
|
}
|
||||||
} else if ((m = a.match(Bech32Regex)) != null) {
|
|
||||||
return <NostrLink link={`nostr:${m[1]}`} depth={depth} />;
|
|
||||||
} else if (showLinkPreview ?? true) {
|
} else if (showLinkPreview ?? true) {
|
||||||
return <LinkPreview url={a} />;
|
return <LinkPreview url={a} />;
|
||||||
}
|
}
|
||||||
|
@ -77,7 +77,7 @@ export default function Invoice(props: InvoiceProps) {
|
|||||||
{description && <p>{description}</p>}
|
{description && <p>{description}</p>}
|
||||||
{isPaid ? (
|
{isPaid ? (
|
||||||
<div className="paid">
|
<div className="paid">
|
||||||
<FormattedMessage defaultMessage="Paid" />
|
<FormattedMessage defaultMessage="Paid" id="u/vOPu" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<button disabled={isExpired} type="button" onClick={payInvoice}>
|
<button disabled={isExpired} type="button" onClick={payInvoice}>
|
||||||
|
@ -10,7 +10,7 @@ const MagnetLink = ({ magnet }: MagnetLinkProps) => {
|
|||||||
return (
|
return (
|
||||||
<div className="note-invoice">
|
<div className="note-invoice">
|
||||||
<h4>
|
<h4>
|
||||||
<FormattedMessage defaultMessage="Magnet Link" />
|
<FormattedMessage defaultMessage="Magnet Link" id="Gcn9NQ" />
|
||||||
</h4>
|
</h4>
|
||||||
<a href={magnet.raw} rel="noreferrer">
|
<a href={magnet.raw} rel="noreferrer">
|
||||||
{magnet.dn ?? magnet.infoHash}
|
{magnet.dn ?? magnet.infoHash}
|
||||||
|
@ -2,21 +2,24 @@ import usePreferences from "@/Hooks/usePreferences";
|
|||||||
import { MixCloudRegex } from "@/Utils/Const";
|
import { MixCloudRegex } from "@/Utils/Const";
|
||||||
|
|
||||||
const MixCloudEmbed = ({ link }: { link: string }) => {
|
const MixCloudEmbed = ({ link }: { link: string }) => {
|
||||||
const match = link.match(MixCloudRegex);
|
const feedPath = (MixCloudRegex.test(link) && RegExp.$1) + "%2F" + (MixCloudRegex.test(link) && RegExp.$2);
|
||||||
if (!match) return;
|
|
||||||
const feedPath = match[1] + "%2F" + match[2];
|
|
||||||
|
|
||||||
const theme = usePreferences(s => s.theme);
|
const theme = usePreferences(s => s.theme);
|
||||||
const lightParams = theme === "light" ? "light=1" : "light=0";
|
const lightParams = theme === "light" ? "light=1" : "light=0";
|
||||||
return (
|
return (
|
||||||
<iframe
|
<>
|
||||||
title="SoundCloud player"
|
<br />
|
||||||
width="100%"
|
<iframe
|
||||||
height="120"
|
title="SoundCloud player"
|
||||||
frameBorder="0"
|
width="100%"
|
||||||
src={`https://www.mixcloud.com/widget/iframe/?hide_cover=1&${lightParams}&feed=%2F${feedPath}%2F`}
|
height="120"
|
||||||
loading="lazy"
|
frameBorder="0"
|
||||||
/>
|
src={`https://www.mixcloud.com/widget/iframe/?hide_cover=1&${lightParams}&feed=%2F${feedPath}%2F`}
|
||||||
|
/>
|
||||||
|
<a href={link} target="_blank" rel="noreferrer">
|
||||||
|
{link}
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -65,6 +65,7 @@ export default function PubkeyList({ ev, className }: { ev: NostrEvent; classNam
|
|||||||
return (
|
return (
|
||||||
<FollowListBase
|
<FollowListBase
|
||||||
pubkeys={ids}
|
pubkeys={ids}
|
||||||
|
showAbout={true}
|
||||||
className={className}
|
className={className}
|
||||||
title={findTag(ev, "title") ?? findTag(ev, "d")}
|
title={findTag(ev, "title") ?? findTag(ev, "d")}
|
||||||
actions={
|
actions={
|
||||||
@ -80,11 +81,6 @@ export default function PubkeyList({ ev, className }: { ev: NostrEvent; classNam
|
|||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
profilePreviewProps={{
|
|
||||||
options: {
|
|
||||||
about: true,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,19 @@
|
|||||||
const SoundCloudEmbed = ({ link }: { link: string }) => {
|
const SoundCloudEmbed = ({ link }: { link: string }) => {
|
||||||
return (
|
return (
|
||||||
<iframe
|
<>
|
||||||
width="100%"
|
<iframe
|
||||||
height="166"
|
// eslint-disable-next-line react/no-unknown-property
|
||||||
allow="autoplay"
|
credentialless=""
|
||||||
src={`https://w.soundcloud.com/player/?url=${link}`}
|
width="100%"
|
||||||
loading="lazy"
|
height="166"
|
||||||
/>
|
scrolling="no"
|
||||||
|
allow="autoplay"
|
||||||
|
src={`https://w.soundcloud.com/player/?url=${link}`}
|
||||||
|
/>
|
||||||
|
<a href={link} target="_blank" rel="noreferrer" onClick={e => e.stopPropagation()} className="ext">
|
||||||
|
{link}
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2,15 +2,22 @@ const SpotifyEmbed = ({ link }: { link: string }) => {
|
|||||||
const convertedUrl = link.replace(/\/(track|album|playlist|episode)\/([a-zA-Z0-9]+)/, "/embed/$1/$2");
|
const convertedUrl = link.replace(/\/(track|album|playlist|episode)\/([a-zA-Z0-9]+)/, "/embed/$1/$2");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<iframe
|
<>
|
||||||
style={{ borderRadius: 12 }}
|
<iframe
|
||||||
src={convertedUrl}
|
// eslint-disable-next-line react/no-unknown-property
|
||||||
width="100%"
|
credentialless=""
|
||||||
height="352"
|
style={{ borderRadius: 12 }}
|
||||||
frameBorder="0"
|
src={convertedUrl}
|
||||||
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
|
width="100%"
|
||||||
loading="lazy"
|
height="352"
|
||||||
/>
|
frameBorder="0"
|
||||||
|
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<a href={link} target="_blank" rel="noreferrer" onClick={e => e.stopPropagation()} className="ext">
|
||||||
|
{link}
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -53,17 +53,17 @@ const TidalEmbed = ({ link }: { link: string }) => {
|
|||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const iframe = (
|
||||||
|
// eslint-disable-next-line react/no-unknown-property
|
||||||
|
<iframe src={source} style={extraStyles} width="100%" title="TIDAL Embed" frameBorder={0} credentialless="" />
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<iframe
|
<>
|
||||||
src={source}
|
{iframe}
|
||||||
style={extraStyles}
|
<a href={link} target="_blank" rel="noreferrer" onClick={e => e.stopPropagation()} className="ext">
|
||||||
width="100%"
|
{link}
|
||||||
allow="encrypted-media *; clipboard-write *; clipboard-read *"
|
</a>
|
||||||
sandbox="allow-scripts allow-popups allow-forms allow-same-origin"
|
</>
|
||||||
title="TIDAL Embed"
|
|
||||||
frameBorder={0}
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -3,12 +3,12 @@ const TwitchEmbed = ({ link }: { link: string }) => {
|
|||||||
|
|
||||||
const args = `?channel=${channel}&parent=${window.location.hostname}&muted=true`;
|
const args = `?channel=${channel}&parent=${window.location.hostname}&muted=true`;
|
||||||
return (
|
return (
|
||||||
<iframe
|
<>
|
||||||
src={`https://player.twitch.tv/${args}`}
|
<iframe src={`https://player.twitch.tv/${args}`} className="w-max" allowFullScreen={true} />
|
||||||
className="aspect-video w-full"
|
<a href={link} target="_blank" rel="noreferrer" onClick={e => e.stopPropagation()} className="ext">
|
||||||
allowFullScreen={true}
|
{link}
|
||||||
loading="lazy"
|
</a>
|
||||||
/>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2,7 +2,21 @@ const WavlakeEmbed = ({ link }: { link: string }) => {
|
|||||||
const convertedUrl = link.replace(/(?:player\.|www\.)?wavlake\.com/, "embed.wavlake.com");
|
const convertedUrl = link.replace(/(?:player\.|www\.)?wavlake\.com/, "embed.wavlake.com");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<iframe style={{ borderRadius: 12 }} src={convertedUrl} width="100%" height="380" frameBorder="0" loading="lazy" />
|
<>
|
||||||
|
<iframe
|
||||||
|
// eslint-disable-next-line react/no-unknown-property
|
||||||
|
credentialless=""
|
||||||
|
style={{ borderRadius: 12 }}
|
||||||
|
src={convertedUrl}
|
||||||
|
width="100%"
|
||||||
|
height="380"
|
||||||
|
frameBorder="0"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<a href={link} target="_blank" rel="noreferrer" onClick={e => e.stopPropagation()} className="ext">
|
||||||
|
{link}
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
import { YoutubeUrlRegex } from "@/Utils/Const";
|
|
||||||
|
|
||||||
export default function YoutubeEmbed({ link }: { link: string }) {
|
|
||||||
const m = link.match(YoutubeUrlRegex);
|
|
||||||
if (!m) return;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<iframe
|
|
||||||
className="-mx-4 md:mx-0 w-max my-2"
|
|
||||||
src={`https://www.youtube.com/embed/${m[1]}${m[3] ? `?list=${m[3].slice(6)}` : ""}`}
|
|
||||||
title="YouTube video player"
|
|
||||||
frameBorder="0"
|
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
|
||||||
allowFullScreen={true}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
@ -32,7 +32,7 @@ export default function ZapstrEmbed({ ev }: { ev: NostrEvent }) {
|
|||||||
</div>
|
</div>
|
||||||
<Link to={`https://zapstr.live/?track=${link}`} target="_blank">
|
<Link to={`https://zapstr.live/?track=${link}`} target="_blank">
|
||||||
<button>
|
<button>
|
||||||
<FormattedMessage defaultMessage="Open on Zapstr" />
|
<FormattedMessage defaultMessage="Open on Zapstr" id="Lu5/Bj" />
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
|
101
packages/app/src/Components/Event/Create/NoteCreator.css
Normal file
101
packages/app/src/Components/Event/Create/NoteCreator.css
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
.note-creator-modal .modal-body > div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-creator-modal .note.card {
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
min-height: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-creator-modal .note.card.note-quote {
|
||||||
|
border: 1px solid var(--gray);
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-creator-modal h4 {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 1.21px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--gray-light);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-creator-relay {
|
||||||
|
background-color: var(--gray-dark);
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-creator textarea {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
resize: none;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
margin: 8px 12px;
|
||||||
|
min-height: 100px;
|
||||||
|
width: stretch;
|
||||||
|
width: -webkit-fill-available;
|
||||||
|
width: -moz-available;
|
||||||
|
max-height: 210px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-creator textarea::placeholder {
|
||||||
|
color: var(--font-secondary-color);
|
||||||
|
font-size: var(--font-size);
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-creator.poll textarea {
|
||||||
|
min-height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-creator .error {
|
||||||
|
position: absolute;
|
||||||
|
left: 16px;
|
||||||
|
bottom: 12px;
|
||||||
|
color: var(--error);
|
||||||
|
margin-right: 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-creator-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-creator-icon.pfp .avatar {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-creator-modal .rti--container {
|
||||||
|
background-color: unset !important;
|
||||||
|
box-shadow: unset !important;
|
||||||
|
border: 2px solid var(--border-color) !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
padding: 4px 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-creator-modal .rti--tag {
|
||||||
|
color: black !important;
|
||||||
|
padding: 4px 10px !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
display: unset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-creator-modal .rti--input {
|
||||||
|
width: 100% !important;
|
||||||
|
border: unset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-creator-modal .rti--tag button {
|
||||||
|
padding: 0 0 0 var(--rti-s);
|
||||||
|
}
|
@ -1,9 +1,8 @@
|
|||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
|
import "./NoteCreator.css";
|
||||||
|
|
||||||
import { fetchNip05Pubkey, unixNow } from "@snort/shared";
|
import { fetchNip05Pubkey, unixNow } from "@snort/shared";
|
||||||
import { EventBuilder, EventKind, NostrLink, NostrPrefix, TaggedNostrEvent, tryParseNostrLink } from "@snort/system";
|
import { EventBuilder, EventKind, NostrLink, NostrPrefix, TaggedNostrEvent, tryParseNostrLink } from "@snort/system";
|
||||||
import { useUserProfile } from "@snort/system-react";
|
|
||||||
import { ZapTarget } from "@snort/wallet";
|
|
||||||
import { Menu, MenuItem } from "@szhsin/react-menu";
|
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { ClipboardEventHandler, DragEvent, useEffect } from "react";
|
import { ClipboardEventHandler, DragEvent, useEffect } from "react";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
@ -11,26 +10,25 @@ import { FormattedMessage, useIntl } from "react-intl";
|
|||||||
import AsyncButton from "@/Components/Button/AsyncButton";
|
import AsyncButton from "@/Components/Button/AsyncButton";
|
||||||
import { AsyncIcon } from "@/Components/Button/AsyncIcon";
|
import { AsyncIcon } from "@/Components/Button/AsyncIcon";
|
||||||
import CloseButton from "@/Components/Button/CloseButton";
|
import CloseButton from "@/Components/Button/CloseButton";
|
||||||
import IconButton from "@/Components/Button/IconButton";
|
|
||||||
import { sendEventToRelays } from "@/Components/Event/Create/util";
|
import { sendEventToRelays } from "@/Components/Event/Create/util";
|
||||||
import Note from "@/Components/Event/EventComponent";
|
import Note from "@/Components/Event/EventComponent";
|
||||||
import Flyout from "@/Components/flyout";
|
|
||||||
import Icon from "@/Components/Icons/Icon";
|
import Icon from "@/Components/Icons/Icon";
|
||||||
import { ToggleSwitch } from "@/Components/Icons/Toggle";
|
import { ToggleSwitch } from "@/Components/Icons/Toggle";
|
||||||
import Modal from "@/Components/Modal/Modal";
|
import Modal from "@/Components/Modal/Modal";
|
||||||
import Textarea from "@/Components/Textarea/Textarea";
|
import Textarea from "@/Components/Textarea/Textarea";
|
||||||
import { Toastore } from "@/Components/Toaster/Toaster";
|
import { Toastore } from "@/Components/Toaster/Toaster";
|
||||||
import { MediaServerFileList } from "@/Components/Upload/file-picker";
|
import ProfileImage from "@/Components/User/ProfileImage";
|
||||||
import Avatar from "@/Components/User/Avatar";
|
|
||||||
import useEventPublisher from "@/Hooks/useEventPublisher";
|
import useEventPublisher from "@/Hooks/useEventPublisher";
|
||||||
import useLogin from "@/Hooks/useLogin";
|
import useLogin from "@/Hooks/useLogin";
|
||||||
import usePreferences from "@/Hooks/usePreferences";
|
import usePreferences from "@/Hooks/usePreferences";
|
||||||
import useRelays from "@/Hooks/useRelays";
|
import useRelays from "@/Hooks/useRelays";
|
||||||
import { useNoteCreator } from "@/State/NoteCreator";
|
import { useNoteCreator } from "@/State/NoteCreator";
|
||||||
import { openFile, trackEvent } from "@/Utils";
|
import { openFile, trackEvent } from "@/Utils";
|
||||||
import useFileUpload, { addExtensionToNip94Url, nip94TagsToIMeta, readNip94Tags } from "@/Utils/Upload";
|
import useFileUpload from "@/Utils/Upload";
|
||||||
import { GetPowWorker } from "@/Utils/wasm";
|
import { GetPowWorker } from "@/Utils/wasm";
|
||||||
|
import { ZapTarget } from "@/Utils/Zapper";
|
||||||
|
|
||||||
|
import FileUploadProgress from "../FileUpload";
|
||||||
import { OkResponseRow } from "./OkResponseRow";
|
import { OkResponseRow } from "./OkResponseRow";
|
||||||
|
|
||||||
const previewNoteOptions = {
|
const previewNoteOptions = {
|
||||||
@ -61,7 +59,6 @@ export function NoteCreator() {
|
|||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const uploader = useFileUpload();
|
const uploader = useFileUpload();
|
||||||
const publicKey = useLogin(s => s.publicKey);
|
const publicKey = useLogin(s => s.publicKey);
|
||||||
const profile = useUserProfile(publicKey);
|
|
||||||
const pow = usePreferences(s => s.pow);
|
const pow = usePreferences(s => s.pow);
|
||||||
const relays = useRelays();
|
const relays = useRelays();
|
||||||
const { system, publisher: pub } = useEventPublisher();
|
const { system, publisher: pub } = useEventPublisher();
|
||||||
@ -148,18 +145,6 @@ export function NoteCreator() {
|
|||||||
extraTags ??= [];
|
extraTags ??= [];
|
||||||
extraTags.push(...note.hashTags.map(a => ["t", a.toLowerCase()]));
|
extraTags.push(...note.hashTags.map(a => ["t", a.toLowerCase()]));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const ex of note.otherEvents ?? []) {
|
|
||||||
const meta = readNip94Tags(ex.tags);
|
|
||||||
if (!meta.url) continue;
|
|
||||||
if (!note.note.endsWith("\n")) {
|
|
||||||
note.note += "\n";
|
|
||||||
}
|
|
||||||
note.note += addExtensionToNip94Url(meta);
|
|
||||||
extraTags ??= [];
|
|
||||||
extraTags.push(nip94TagsToIMeta(meta));
|
|
||||||
}
|
|
||||||
|
|
||||||
// add quote repost
|
// add quote repost
|
||||||
if (note.quote) {
|
if (note.quote) {
|
||||||
if (!note.note.endsWith("\n")) {
|
if (!note.note.endsWith("\n")) {
|
||||||
@ -226,16 +211,19 @@ export function NoteCreator() {
|
|||||||
}
|
}
|
||||||
trackEvent("PostNote", props);
|
trackEvent("PostNote", props);
|
||||||
|
|
||||||
sendEventToRelays(system, ev, note.selectedCustomRelays, r => {
|
const events = (note.otherEvents ?? []).concat(ev);
|
||||||
if (CONFIG.noteCreatorToast) {
|
events.map(a =>
|
||||||
r.forEach(rr => {
|
sendEventToRelays(system, a, note.selectedCustomRelays, r => {
|
||||||
Toastore.push({
|
if (CONFIG.noteCreatorToast) {
|
||||||
element: c => <OkResponseRow rsp={rr} close={c} />,
|
r.forEach(rr => {
|
||||||
expire: unixNow() + (rr.ok ? 5 : 55555),
|
Toastore.push({
|
||||||
|
element: c => <OkResponseRow rsp={rr} close={c} />,
|
||||||
|
expire: unixNow() + (rr.ok ? 5 : 55555),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
}
|
}),
|
||||||
});
|
);
|
||||||
note.update(n => n.reset());
|
note.update(n => n.reset());
|
||||||
localStorage.removeItem("msgDraft");
|
localStorage.removeItem("msgDraft");
|
||||||
}
|
}
|
||||||
@ -260,17 +248,29 @@ export function NoteCreator() {
|
|||||||
|
|
||||||
async function uploadFile(file: File) {
|
async function uploadFile(file: File) {
|
||||||
try {
|
try {
|
||||||
if (file && uploader) {
|
if (file) {
|
||||||
const rx = await uploader.upload(file, file.name);
|
const rx = await uploader.upload(file, file.name);
|
||||||
note.update(v => {
|
note.update(v => {
|
||||||
if (rx.header) {
|
if (rx.header) {
|
||||||
v.otherEvents ??= [];
|
const link = `nostr:${new NostrLink(NostrPrefix.Event, rx.header.id, rx.header.kind).encode(
|
||||||
v.otherEvents.push(rx.header);
|
CONFIG.eventLinkPrefix,
|
||||||
|
)}`;
|
||||||
|
v.note = `${v.note ? `${v.note}\n` : ""}${link}`;
|
||||||
|
v.otherEvents = [...(v.otherEvents ?? []), rx.header];
|
||||||
} else if (rx.url) {
|
} else if (rx.url) {
|
||||||
v.note = `${v.note ? `${v.note}\n` : ""}${rx.url}`;
|
v.note = `${v.note ? `${v.note}\n` : ""}${rx.url}`;
|
||||||
if (rx.metadata) {
|
if (rx.metadata) {
|
||||||
v.extraTags ??= [];
|
v.extraTags ??= [];
|
||||||
const imeta = nip94TagsToIMeta(rx.metadata);
|
const imeta = ["imeta", `url ${rx.url}`];
|
||||||
|
if (rx.metadata.blurhash) {
|
||||||
|
imeta.push(`blurhash ${rx.metadata.blurhash}`);
|
||||||
|
}
|
||||||
|
if (rx.metadata.width && rx.metadata.height) {
|
||||||
|
imeta.push(`dim ${rx.metadata.width}x${rx.metadata.height}`);
|
||||||
|
}
|
||||||
|
if (rx.metadata.hash) {
|
||||||
|
imeta.push(`x ${rx.metadata.hash}`);
|
||||||
|
}
|
||||||
v.extraTags.push(imeta);
|
v.extraTags.push(imeta);
|
||||||
}
|
}
|
||||||
} else if (rx?.error) {
|
} else if (rx?.error) {
|
||||||
@ -330,12 +330,12 @@ export function NoteCreator() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h4>
|
<h4>
|
||||||
<FormattedMessage defaultMessage="Poll Options" />
|
<FormattedMessage defaultMessage="Poll Options" id="vhlWFg" />
|
||||||
</h4>
|
</h4>
|
||||||
{note.pollOptions?.map((a, i) => (
|
{note.pollOptions?.map((a, i) => (
|
||||||
<div className="form-group w-max" key={`po-${i}`}>
|
<div className="form-group w-max" key={`po-${i}`}>
|
||||||
<div>
|
<div>
|
||||||
<FormattedMessage defaultMessage="Option: {n}" values={{ n: i + 1 }} />
|
<FormattedMessage defaultMessage="Option: {n}" id="mfe8RW" values={{ n: i + 1 }} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input type="text" value={a} onChange={e => changePollOption(i, e.target.value)} />
|
<input type="text" value={a} onChange={e => changePollOption(i, e.target.value)} />
|
||||||
@ -369,12 +369,12 @@ export function NoteCreator() {
|
|||||||
|
|
||||||
function renderRelayCustomisation() {
|
function renderRelayCustomisation() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col g8">
|
||||||
{Object.entries(relays)
|
{Object.entries(relays)
|
||||||
.filter(el => el[1].write)
|
.filter(el => el[1].write)
|
||||||
.map(a => a[0])
|
.map(a => a[0])
|
||||||
.map((r, i, a) => (
|
.map((r, i, a) => (
|
||||||
<div className="p flex items-center justify-between bg-gray br" key={r}>
|
<div className="p flex justify-between note-creator-relay" key={r}>
|
||||||
<div>{r}</div>
|
<div>{r}</div>
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
@ -422,24 +422,24 @@ export function NoteCreator() {
|
|||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<h4>
|
<h4>
|
||||||
<FormattedMessage defaultMessage="Custom Relays" />
|
<FormattedMessage defaultMessage="Custom Relays" id="EcZF24" />
|
||||||
</h4>
|
</h4>
|
||||||
<p>
|
<p>
|
||||||
<FormattedMessage defaultMessage="Send note to a subset of your write relays" />
|
<FormattedMessage defaultMessage="Send note to a subset of your write relays" id="th5lxp" />
|
||||||
</p>
|
</p>
|
||||||
{renderRelayCustomisation()}
|
{renderRelayCustomisation()}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col g8">
|
<div className="flex flex-col g8">
|
||||||
<h4>
|
<h4>
|
||||||
<FormattedMessage defaultMessage="Zap Splits" />
|
<FormattedMessage defaultMessage="Zap Splits" id="5CB6zB" />
|
||||||
</h4>
|
</h4>
|
||||||
<FormattedMessage defaultMessage="Zaps on this note will be split to the following users." />
|
<FormattedMessage defaultMessage="Zaps on this note will be split to the following users." id="LwYmVi" />
|
||||||
<div className="flex flex-col g8">
|
<div className="flex flex-col g8">
|
||||||
{[...(note.zapSplits ?? [])].map((v: ZapTarget, i, arr) => (
|
{[...(note.zapSplits ?? [])].map((v: ZapTarget, i, arr) => (
|
||||||
<div className="flex items-center g8" key={`${v.name}-${v.value}`}>
|
<div className="flex items-center g8" key={`${v.name}-${v.value}`}>
|
||||||
<div className="flex flex-col flex-4 g4">
|
<div className="flex flex-col flex-4 g4">
|
||||||
<h4>
|
<h4>
|
||||||
<FormattedMessage defaultMessage="Recipient" />
|
<FormattedMessage defaultMessage="Recipient" id="8Rkoyb" />
|
||||||
</h4>
|
</h4>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -454,7 +454,7 @@ export function NoteCreator() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col flex-1 g4">
|
<div className="flex flex-col flex-1 g4">
|
||||||
<h4>
|
<h4>
|
||||||
<FormattedMessage defaultMessage="Weight" />
|
<FormattedMessage defaultMessage="Weight" id="zCb8fX" />
|
||||||
</h4>
|
</h4>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
@ -470,7 +470,7 @@ export function NoteCreator() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col g4">
|
<div className="flex flex-col s g4">
|
||||||
<div> </div>
|
<div> </div>
|
||||||
<Icon
|
<Icon
|
||||||
name="close"
|
name="close"
|
||||||
@ -484,18 +484,24 @@ export function NoteCreator() {
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
note.update(v => (v.zapSplits = [...(v.zapSplits ?? []), { type: "pubkey", value: "", weight: 1 }]))
|
note.update(v => (v.zapSplits = [...(v.zapSplits ?? []), { type: "pubkey", value: "", weight: 1 }]))
|
||||||
}>
|
}>
|
||||||
<FormattedMessage defaultMessage="Add" />
|
<FormattedMessage defaultMessage="Add" id="2/2yg+" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<span className="warning">
|
<span className="warning">
|
||||||
<FormattedMessage defaultMessage="Not all clients support this, you may still receive some zaps as if zap splits was not configured" />
|
<FormattedMessage
|
||||||
|
defaultMessage="Not all clients support this, you may still receive some zaps as if zap splits was not configured"
|
||||||
|
id="6bgpn+"
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col g8">
|
<div className="flex flex-col g8">
|
||||||
<h4>
|
<h4>
|
||||||
<FormattedMessage defaultMessage="Sensitive Content" />
|
<FormattedMessage defaultMessage="Sensitive Content" id="bQdA2k" />
|
||||||
</h4>
|
</h4>
|
||||||
<FormattedMessage defaultMessage="Users must accept the content warning to show the content of your note." />
|
<FormattedMessage
|
||||||
|
defaultMessage="Users must accept the content warning to show the content of your note."
|
||||||
|
id="UUPFlt"
|
||||||
|
/>
|
||||||
<input
|
<input
|
||||||
className="w-max"
|
className="w-max"
|
||||||
type="text"
|
type="text"
|
||||||
@ -509,7 +515,7 @@ export function NoteCreator() {
|
|||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<span className="warning">
|
<span className="warning">
|
||||||
<FormattedMessage defaultMessage="Not all clients support this yet" />
|
<FormattedMessage defaultMessage="Not all clients support this yet" id="gXgY3+" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@ -519,46 +525,32 @@ export function NoteCreator() {
|
|||||||
function noteCreatorFooter() {
|
function noteCreatorFooter() {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<div className="flex items-center gap-4 text-gray-light cursor-pointer">
|
<div className="flex items-center g8">
|
||||||
<Avatar pubkey={publicKey ?? ""} user={profile} size={28} showTitle={true} />
|
<ProfileImage
|
||||||
<Menu
|
pubkey={publicKey ?? ""}
|
||||||
menuButton={
|
className="note-creator-icon"
|
||||||
<AsyncIcon iconName="attachment" iconSize={24} className="hover:text-gray-superlight transition" />
|
link=""
|
||||||
}
|
showUsername={false}
|
||||||
menuClassName="ctx-menu no-icons">
|
showFollowDistance={false}
|
||||||
<div className="close-menu-container">
|
showProfileCard={false}
|
||||||
{/* This menu item serves as a "close menu" button;
|
/>
|
||||||
it allows the user to click anywhere nearby the menu to close it. */}
|
|
||||||
<MenuItem>
|
|
||||||
<div className="close-menu" />
|
|
||||||
</MenuItem>
|
|
||||||
</div>
|
|
||||||
<MenuItem onClick={() => note.update(s => (s.filePicker = "compact"))}>
|
|
||||||
<FormattedMessage defaultMessage="From Server" />
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem onClick={() => attachFile()}>
|
|
||||||
<FormattedMessage defaultMessage="From File" />
|
|
||||||
</MenuItem>
|
|
||||||
</Menu>
|
|
||||||
|
|
||||||
{note.pollOptions === undefined && !note.replyTo && (
|
{note.pollOptions === undefined && !note.replyTo && (
|
||||||
<AsyncIcon
|
<AsyncIcon
|
||||||
iconName="bar-chart"
|
iconName="list"
|
||||||
iconSize={24}
|
iconSize={24}
|
||||||
onClick={() => note.update(v => (v.pollOptions = ["A", "B"]))}
|
onClick={() => note.update(v => (v.pollOptions = ["A", "B"]))}
|
||||||
className={classNames("hover:text-gray-superlight transition", {
|
className={classNames("note-creator-icon", { active: note.pollOptions !== undefined })}
|
||||||
"text-white": note.pollOptions !== undefined,
|
|
||||||
})}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<AsyncIcon iconName="image-plus" iconSize={24} onClick={attachFile} className="note-creator-icon" />
|
||||||
<AsyncIcon
|
<AsyncIcon
|
||||||
iconName="settings-outline"
|
iconName="settings-04"
|
||||||
iconSize={24}
|
iconSize={24}
|
||||||
onClick={() => note.update(v => (v.advanced = !v.advanced))}
|
onClick={() => note.update(v => (v.advanced = !v.advanced))}
|
||||||
className={classNames("hover:text-gray-superlight transition", { "text-white": note.advanced })}
|
className={classNames("note-creator-icon", { active: note.advanced })}
|
||||||
/>
|
/>
|
||||||
<span className="sm:inline hidden">
|
<span className="sm:inline hidden">
|
||||||
<FormattedMessage defaultMessage="Preview" />
|
<FormattedMessage defaultMessage="Preview" id="TJo5E6" />
|
||||||
</span>
|
</span>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
onClick={() => loadPreview()}
|
onClick={() => loadPreview()}
|
||||||
@ -566,9 +558,18 @@ export function NoteCreator() {
|
|||||||
className={classNames({ active: Boolean(note.preview) })}
|
className={classNames({ active: Boolean(note.preview) })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<AsyncButton onClick={onSubmit} className="primary">
|
<div className="flex g8">
|
||||||
{note.replyTo ? <FormattedMessage defaultMessage="Reply" /> : <FormattedMessage defaultMessage="Send" />}
|
<button className="secondary" onClick={cancel}>
|
||||||
</AsyncButton>
|
<FormattedMessage defaultMessage="Cancel" id="47FYwb" />
|
||||||
|
</button>
|
||||||
|
<AsyncButton onClick={onSubmit} className="primary">
|
||||||
|
{note.replyTo ? (
|
||||||
|
<FormattedMessage defaultMessage="Reply" id="9HU8vw" />
|
||||||
|
) : (
|
||||||
|
<FormattedMessage defaultMessage="Send" id="9WRlF4" />
|
||||||
|
)}
|
||||||
|
</AsyncButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -616,7 +617,7 @@ export function NoteCreator() {
|
|||||||
{note.replyTo && (
|
{note.replyTo && (
|
||||||
<>
|
<>
|
||||||
<h4>
|
<h4>
|
||||||
<FormattedMessage defaultMessage="Reply To" />
|
<FormattedMessage defaultMessage="Reply To" id="8ED/4u" />
|
||||||
</h4>
|
</h4>
|
||||||
<div className="max-h-64 overflow-y-auto">
|
<div className="max-h-64 overflow-y-auto">
|
||||||
<Note className="hover:bg-transparent" data={note.replyTo} options={replyToNoteOptions} />
|
<Note className="hover:bg-transparent" data={note.replyTo} options={replyToNoteOptions} />
|
||||||
@ -627,7 +628,7 @@ export function NoteCreator() {
|
|||||||
{note.quote && (
|
{note.quote && (
|
||||||
<>
|
<>
|
||||||
<h4>
|
<h4>
|
||||||
<FormattedMessage defaultMessage="Quote Repost" />
|
<FormattedMessage defaultMessage="Quote Repost" id="C7642/" />
|
||||||
</h4>
|
</h4>
|
||||||
<div className="max-h-64 overflow-y-auto">
|
<div className="max-h-64 overflow-y-auto">
|
||||||
<Note className="hover:bg-transparent" data={note.quote} options={quoteNoteOptions} />
|
<Note className="hover:bg-transparent" data={note.quote} options={quoteNoteOptions} />
|
||||||
@ -637,22 +638,13 @@ export function NoteCreator() {
|
|||||||
)}
|
)}
|
||||||
{note.preview && getPreviewNote()}
|
{note.preview && getPreviewNote()}
|
||||||
{!note.preview && (
|
{!note.preview && (
|
||||||
<div className="flex flex-col gap-4">
|
<>
|
||||||
<div className="font-medium flex justify-between items-center">
|
<div onPaste={handlePaste} className={classNames("note-creator", { poll: Boolean(note.pollOptions) })}>
|
||||||
<FormattedMessage defaultMessage="Compose a note" />
|
|
||||||
<AsyncIcon
|
|
||||||
iconName="x"
|
|
||||||
className="bg-gray rounded-full items-center justify-center flex p-1 cursor-pointer"
|
|
||||||
onClick={cancel}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div onPaste={handlePaste} className={classNames({ poll: Boolean(note.pollOptions) })}>
|
|
||||||
<Textarea
|
<Textarea
|
||||||
className="!border-none !resize-none !p-0 !rounded-none !text-sm"
|
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
autoFocus={true}
|
autoFocus
|
||||||
onChange={c => onChange(c)}
|
onChange={c => onChange(c)}
|
||||||
value={note.note}
|
value={note.note}
|
||||||
onFocus={() => note.update(v => (v.active = true))}
|
onFocus={() => note.update(v => (v.active = true))}
|
||||||
@ -664,74 +656,12 @@ export function NoteCreator() {
|
|||||||
/>
|
/>
|
||||||
{renderPollOptions()}
|
{renderPollOptions()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
)}
|
|
||||||
{(note.otherEvents?.length ?? 0) > 0 && !note.preview && (
|
|
||||||
<div className="flex gap-2 flex-wrap">
|
|
||||||
{note.otherEvents
|
|
||||||
?.map(a => ({
|
|
||||||
event: a,
|
|
||||||
tags: readNip94Tags(a.tags),
|
|
||||||
}))
|
|
||||||
.filter(a => a.tags.url)
|
|
||||||
.map(a => (
|
|
||||||
<div key={a.tags.url} className="relative">
|
|
||||||
<img
|
|
||||||
className="object-cover w-[80px] h-[80px] !mt-0 rounded-lg"
|
|
||||||
src={addExtensionToNip94Url(a.tags)}
|
|
||||||
/>
|
|
||||||
<Icon
|
|
||||||
name="x"
|
|
||||||
className="absolute -top-[0.25rem] -right-[0.25rem] bg-gray rounded-full cursor-pointer"
|
|
||||||
onClick={() =>
|
|
||||||
note.update(
|
|
||||||
n => (n.otherEvents = n.otherEvents?.filter(b => readNip94Tags(b.tags).url !== a.tags.url)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
{uploader.progress.length > 0 && <FileUploadProgress progress={uploader.progress} />}
|
||||||
{noteCreatorFooter()}
|
{noteCreatorFooter()}
|
||||||
{note.error && <span className="error">{note.error}</span>}
|
{note.error && <span className="error">{note.error}</span>}
|
||||||
{note.advanced && noteCreatorAdvanced()}
|
{note.advanced && noteCreatorAdvanced()}
|
||||||
<Flyout
|
|
||||||
show={note.filePicker !== "hidden"}
|
|
||||||
width={note.filePicker !== "compact" ? "70vw" : undefined}
|
|
||||||
onClose={() => note.update(v => (v.filePicker = "hidden"))}
|
|
||||||
side="right"
|
|
||||||
title={
|
|
||||||
<div className="text-xl font-medium">
|
|
||||||
<FormattedMessage defaultMessage="Attach Media" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
actions={
|
|
||||||
<>
|
|
||||||
<IconButton
|
|
||||||
className="max-lg:!hidden"
|
|
||||||
icon={{
|
|
||||||
name: "expand",
|
|
||||||
}}
|
|
||||||
onClick={() => note.update(n => (n.filePicker = n.filePicker === "wide" ? "compact" : "wide"))}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}>
|
|
||||||
<div className="overflow-y-auto h-[calc(100%-2rem)]">
|
|
||||||
{note.filePicker !== "hidden" && (
|
|
||||||
<MediaServerFileList
|
|
||||||
onPicked={files => {
|
|
||||||
note.update(n => {
|
|
||||||
n.otherEvents ??= [];
|
|
||||||
n.otherEvents?.push(...files);
|
|
||||||
n.filePicker = "hidden";
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
cols={note.filePicker === "compact" ? 2 : 6}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Flyout>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -744,7 +674,11 @@ export function NoteCreator() {
|
|||||||
|
|
||||||
if (!note.show) return null;
|
if (!note.show) return null;
|
||||||
return (
|
return (
|
||||||
<Modal id="note-creator" bodyClassName="modal-body gap-4" onClose={reset}>
|
<Modal
|
||||||
|
id="note-creator"
|
||||||
|
bodyClassName="modal-body flex flex-col gap-4"
|
||||||
|
className="note-creator-modal"
|
||||||
|
onClose={reset}>
|
||||||
{noteCreatorForm()}
|
{noteCreatorForm()}
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
@ -15,12 +15,10 @@ export const NoteCreatorButton = ({
|
|||||||
className,
|
className,
|
||||||
alwaysShow,
|
alwaysShow,
|
||||||
showText,
|
showText,
|
||||||
withModal,
|
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
alwaysShow?: boolean;
|
alwaysShow?: boolean;
|
||||||
showText?: boolean;
|
showText?: boolean;
|
||||||
withModal: boolean;
|
|
||||||
}) => {
|
}) => {
|
||||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@ -76,12 +74,12 @@ export const NoteCreatorButton = ({
|
|||||||
<Icon name="plus" size={16} />
|
<Icon name="plus" size={16} />
|
||||||
{showText && (
|
{showText && (
|
||||||
<span className="ml-2 hidden xl:inline">
|
<span className="ml-2 hidden xl:inline">
|
||||||
<FormattedMessage defaultMessage="New Note" />
|
<FormattedMessage defaultMessage="New Note" id="2mcwT8" />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{withModal && <NoteCreator key="global-note-creator" />}
|
<NoteCreator key="global-note-creator" />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -13,10 +13,10 @@ const HiddenNote = ({ children }: { children: React.ReactNode }) => {
|
|||||||
) : (
|
) : (
|
||||||
<div className="bb p flex items-center justify-between">
|
<div className="bb p flex items-center justify-between">
|
||||||
<div className="text-sm text-secondary">
|
<div className="text-sm text-secondary">
|
||||||
<FormattedMessage defaultMessage="This note has been muted" />
|
<FormattedMessage defaultMessage="This note has been muted" id="qfmMQh" />
|
||||||
</div>
|
</div>
|
||||||
<button className="btn btn-sm btn-neutral" onClick={() => setShow(true)}>
|
<button className="btn btn-sm btn-neutral" onClick={() => setShow(true)}>
|
||||||
<FormattedMessage defaultMessage="Show" />
|
<FormattedMessage defaultMessage="Show" id="K7AkdL" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -14,7 +14,7 @@ interface ShowMoreProps {
|
|||||||
const LoadMore = ({ text, onClick, className = "" }: ShowMoreProps) => {
|
const LoadMore = ({ text, onClick, className = "" }: ShowMoreProps) => {
|
||||||
return (
|
return (
|
||||||
<button type="button" className={className} onClick={onClick}>
|
<button type="button" className={className} onClick={onClick}>
|
||||||
{text || <FormattedMessage defaultMessage="Load more" />}
|
{text || <FormattedMessage defaultMessage="Load more" id="00LcfG" />}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import "./LongFormText.css";
|
import "./LongFormText.css";
|
||||||
|
|
||||||
import { TaggedNostrEvent } from "@snort/system";
|
import { NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||||
|
import { useEventReactions, useReactions } from "@snort/system-react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { CSSProperties, useCallback, useRef, useState } from "react";
|
import React, { CSSProperties, useCallback, useRef, useState } from "react";
|
||||||
import { FormattedMessage, FormattedNumber } from "react-intl";
|
import { FormattedMessage, FormattedNumber } from "react-intl";
|
||||||
|
|
||||||
import Text from "@/Components/Text/Text";
|
import Text from "@/Components/Text/Text";
|
||||||
@ -31,6 +32,8 @@ export function LongFormText(props: LongFormTextProps) {
|
|||||||
const [reading, setReading] = useState(false);
|
const [reading, setReading] = useState(false);
|
||||||
const [showMore, setShowMore] = useState(false);
|
const [showMore, setShowMore] = useState(false);
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const related = useReactions("note:reactions", [NostrLink.fromEvent(props.ev)], undefined, false);
|
||||||
|
const { reactions, reposts, zaps } = useEventReactions(NostrLink.fromEvent(props.ev), related);
|
||||||
|
|
||||||
function previewText() {
|
function previewText() {
|
||||||
return (
|
return (
|
||||||
@ -97,7 +100,11 @@ export function LongFormText(props: LongFormTextProps) {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setShowMore(!showMore);
|
setShowMore(!showMore);
|
||||||
}}>
|
}}>
|
||||||
{showMore ? <FormattedMessage defaultMessage="Show less" /> : <FormattedMessage defaultMessage="Show more" />}
|
{showMore ? (
|
||||||
|
<FormattedMessage defaultMessage="Show less" id="qyJtWy" />
|
||||||
|
) : (
|
||||||
|
<FormattedMessage defaultMessage="Show more" id="aWpBzj" />
|
||||||
|
)}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -107,7 +114,7 @@ export function LongFormText(props: LongFormTextProps) {
|
|||||||
function fullText() {
|
function fullText() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NoteFooter ev={props.ev} />
|
<NoteFooter ev={props.ev} reposts={reposts} zaps={zaps} positive={reactions.positive} />
|
||||||
<hr />
|
<hr />
|
||||||
<div className="flex g8">
|
<div className="flex g8">
|
||||||
<div>
|
<div>
|
||||||
@ -122,12 +129,12 @@ export function LongFormText(props: LongFormTextProps) {
|
|||||||
<div>‧</div>
|
<div>‧</div>
|
||||||
{!reading && (
|
{!reading && (
|
||||||
<div className="pointer" onClick={() => readArticle()}>
|
<div className="pointer" onClick={() => readArticle()}>
|
||||||
<FormattedMessage defaultMessage="Listen to this article" />
|
<FormattedMessage defaultMessage="Listen to this article" id="nihgfo" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{reading && (
|
{reading && (
|
||||||
<div className="pointer" onClick={() => stopReading()}>
|
<div className="pointer" onClick={() => stopReading()}>
|
||||||
<FormattedMessage defaultMessage="Stop listening" />
|
<FormattedMessage defaultMessage="Stop listening" id="U1aPPi" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -136,7 +143,7 @@ export function LongFormText(props: LongFormTextProps) {
|
|||||||
<Markdown content={content} tags={props.ev.tags} ref={ref} />
|
<Markdown content={content} tags={props.ev.tags} ref={ref} />
|
||||||
{shouldTruncate && !showMore && <ToggleShowMore />}
|
{shouldTruncate && !showMore && <ToggleShowMore />}
|
||||||
<hr />
|
<hr />
|
||||||
<NoteFooter ev={props.ev} />
|
<NoteFooter ev={props.ev} reposts={reposts} zaps={zaps} positive={reactions.positive} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,10 @@ export function NostrFileElement({ ev }: { ev: NostrEvent }) {
|
|||||||
|
|
||||||
if (u && m) {
|
if (u && m) {
|
||||||
return (
|
return (
|
||||||
<Reveal message={<FormattedMessage defaultMessage="Click to load content from {link}" values={{ link: u }} />}>
|
<Reveal
|
||||||
|
message={
|
||||||
|
<FormattedMessage defaultMessage="Click to load content from {link}" id="lsNFM1" values={{ link: u }} />
|
||||||
|
}>
|
||||||
<MediaElement
|
<MediaElement
|
||||||
mime={m}
|
mime={m}
|
||||||
url={u}
|
url={u}
|
||||||
@ -41,7 +44,7 @@ export function NostrFileElement({ ev }: { ev: NostrEvent }) {
|
|||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<b className="error">
|
<b className="error">
|
||||||
<FormattedMessage defaultMessage="Unknown file header: {name}" values={{ name: ev.content }} />
|
<FormattedMessage defaultMessage="Unknown file header: {name}" id="PamNxw" values={{ name: ev.content }} />
|
||||||
</b>
|
</b>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { EventKind, NostrLink, TaggedNostrEvent } from "@snort/system";
|
import { EventKind, NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||||
import { WorkerRelayInterface } from "@snort/worker-relay";
|
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
import { useInView } from "react-intersection-observer";
|
import { useInView } from "react-intersection-observer";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
@ -15,12 +14,14 @@ import { TranslationInfo } from "@/Components/Event/Note/TranslationInfo";
|
|||||||
import { NoteTranslation } from "@/Components/Event/Note/types";
|
import { NoteTranslation } from "@/Components/Event/Note/types";
|
||||||
import Username from "@/Components/User/Username";
|
import Username from "@/Components/User/Username";
|
||||||
import useModeration from "@/Hooks/useModeration";
|
import useModeration from "@/Hooks/useModeration";
|
||||||
|
import { findTag } from "@/Utils";
|
||||||
import { chainKey } from "@/Utils/Thread/ChainKey";
|
import { chainKey } from "@/Utils/Thread/ChainKey";
|
||||||
|
|
||||||
import { NoteProps, NotePropsOptions } from "../EventComponent";
|
import messages from "../../messages";
|
||||||
|
import Text from "../../Text/Text";
|
||||||
|
import { NoteProps } from "../EventComponent";
|
||||||
import HiddenNote from "../HiddenNote";
|
import HiddenNote from "../HiddenNote";
|
||||||
import Poll from "../Poll";
|
import Poll from "../Poll";
|
||||||
import NoteAppHandler from "./NoteAppHandler";
|
|
||||||
import NoteFooter from "./NoteFooter/NoteFooter";
|
import NoteFooter from "./NoteFooter/NoteFooter";
|
||||||
|
|
||||||
const defaultOptions = {
|
const defaultOptions = {
|
||||||
@ -39,10 +40,10 @@ export function Note(props: NoteProps) {
|
|||||||
const { data: ev, highlight, options: opt, ignoreModeration = false, className, waitUntilInView } = props;
|
const { data: ev, highlight, options: opt, ignoreModeration = false, className, waitUntilInView } = props;
|
||||||
const baseClassName = classNames("note min-h-[110px] flex flex-col gap-4 card", className ?? "");
|
const baseClassName = classNames("note min-h-[110px] flex flex-col gap-4 card", className ?? "");
|
||||||
const { isEventMuted } = useModeration();
|
const { isEventMuted } = useModeration();
|
||||||
const { ref, inView } = useInView({ triggerOnce: true });
|
const { ref, inView } = useInView({ triggerOnce: true, rootMargin: "2000px" });
|
||||||
const { ref: setSeenAtRef, inView: setSeenAtInView } = useInView({ rootMargin: "0px", threshold: 1 });
|
const { ref: setSeenAtRef, inView: setSeenAtInView } = useInView({ rootMargin: "0px", threshold: 1 });
|
||||||
const [showTranslation, setShowTranslation] = useState(true);
|
const [showTranslation, setShowTranslation] = useState(true);
|
||||||
const [translated, setTranslated] = useState<NoteTranslation | null>(translationCache.get(ev.id));
|
const [translated, setTranslated] = useState<NoteTranslation>(translationCache.get(ev.id));
|
||||||
const cachedSetTranslated = useCallback(
|
const cachedSetTranslated = useCallback(
|
||||||
(translation: NoteTranslation) => {
|
(translation: NoteTranslation) => {
|
||||||
translationCache.set(ev.id, translation);
|
translationCache.set(ev.id, translation);
|
||||||
@ -55,9 +56,7 @@ export function Note(props: NoteProps) {
|
|||||||
let timeout: ReturnType<typeof setTimeout>;
|
let timeout: ReturnType<typeof setTimeout>;
|
||||||
if (setSeenAtInView) {
|
if (setSeenAtInView) {
|
||||||
timeout = setTimeout(() => {
|
timeout = setTimeout(() => {
|
||||||
if (Relay instanceof WorkerRelayInterface) {
|
Relay.setEventMetadata(ev.id, { seen_at: Math.round(Date.now() / 1000) });
|
||||||
Relay.setEventMetadata(ev.id, { seen_at: Math.round(Date.now() / 1000) });
|
|
||||||
}
|
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
return () => clearTimeout(timeout);
|
return () => clearTimeout(timeout);
|
||||||
@ -100,7 +99,7 @@ export function Note(props: NoteProps) {
|
|||||||
<div
|
<div
|
||||||
className={classNames(baseClassName, {
|
className={classNames(baseClassName, {
|
||||||
active: highlight,
|
active: highlight,
|
||||||
"hover:bg-nearly-bg-background cursor-pointer": !opt?.isRoot,
|
"hover:bg-nearly-bg-color cursor-pointer": !opt?.isRoot,
|
||||||
})}
|
})}
|
||||||
onClick={e => goToEvent(e, ev)}
|
onClick={e => goToEvent(e, ev)}
|
||||||
ref={ref}>
|
ref={ref}>
|
||||||
@ -111,10 +110,10 @@ export function Note(props: NoteProps) {
|
|||||||
return !ignoreModeration && isEventMuted(ev) ? <HiddenNote>{noteElement}</HiddenNote> : noteElement;
|
return !ignoreModeration && isEventMuted(ev) ? <HiddenNote>{noteElement}</HiddenNote> : noteElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
function useGoToEvent(props: NoteProps, options: NotePropsOptions) {
|
function useGoToEvent(props, options) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
return useCallback(
|
return useCallback(
|
||||||
(e: React.MouseEvent, eTarget: TaggedNostrEvent) => {
|
(e, eTarget) => {
|
||||||
if (options?.canClick === false) {
|
if (options?.canClick === false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -133,20 +132,11 @@ function useGoToEvent(props: NoteProps, options: NotePropsOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
// prevent navigation if selecting text
|
|
||||||
const cellText = document.getSelection();
|
|
||||||
if (cellText?.type === "Range") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// custom onclick handler
|
|
||||||
if (props.onClick) {
|
if (props.onClick) {
|
||||||
props.onClick(eTarget);
|
props.onClick(eTarget);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// link to event
|
|
||||||
const link = NostrLink.fromEvent(eTarget);
|
const link = NostrLink.fromEvent(eTarget);
|
||||||
if (e.metaKey) {
|
if (e.metaKey) {
|
||||||
window.open(`/${link.encode(CONFIG.eventLinkPrefix)}`, "_blank");
|
window.open(`/${link.encode(CONFIG.eventLinkPrefix)}`, "_blank");
|
||||||
@ -170,7 +160,7 @@ function Reaction({ ev }: { ev: TaggedNostrEvent }) {
|
|||||||
<div className="text-gray-medium font-bold">
|
<div className="text-gray-medium font-bold">
|
||||||
<Username pubkey={ev.pubkey} onLinkVisit={() => {}} />
|
<Username pubkey={ev.pubkey} onLinkVisit={() => {}} />
|
||||||
<span> </span>
|
<span> </span>
|
||||||
<FormattedMessage defaultMessage="liked" />
|
<FormattedMessage defaultMessage="liked" id="TvKqBp" />
|
||||||
</div>
|
</div>
|
||||||
<NoteQuote link={link} />
|
<NoteQuote link={link} />
|
||||||
</div>
|
</div>
|
||||||
@ -178,9 +168,23 @@ function Reaction({ ev }: { ev: TaggedNostrEvent }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleNonTextNote(ev: TaggedNostrEvent) {
|
function handleNonTextNote(ev: TaggedNostrEvent) {
|
||||||
if (ev.kind === EventKind.Reaction) {
|
const alt = findTag(ev, "alt");
|
||||||
|
if (alt) {
|
||||||
|
return (
|
||||||
|
<div className="note-quote">
|
||||||
|
<Text id={ev.id} content={alt} tags={[]} creator={ev.pubkey} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (ev.kind === EventKind.Reaction) {
|
||||||
return <Reaction ev={ev} />;
|
return <Reaction ev={ev} />;
|
||||||
} else {
|
} else {
|
||||||
return <NoteAppHandler ev={ev} />;
|
return (
|
||||||
|
<>
|
||||||
|
<h4>
|
||||||
|
<FormattedMessage {...messages.UnknownEventKind} values={{ kind: ev.kind }} />
|
||||||
|
</h4>
|
||||||
|
<pre>{JSON.stringify(ev, undefined, " ")}</pre>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,63 +0,0 @@
|
|||||||
import { mapEventToProfile, NostrLink, TaggedNostrEvent } from "@snort/system";
|
|
||||||
import { FormattedMessage } from "react-intl";
|
|
||||||
|
|
||||||
import Icon from "@/Components/Icons/Icon";
|
|
||||||
import NostrIcon from "@/Components/Icons/Nostrich";
|
|
||||||
import KindName from "@/Components/kind-name";
|
|
||||||
import Avatar from "@/Components/User/Avatar";
|
|
||||||
import DisplayName from "@/Components/User/DisplayName";
|
|
||||||
import useAppHandler from "@/Hooks/useAppHandler";
|
|
||||||
|
|
||||||
export default function NoteAppHandler({ ev }: { ev: TaggedNostrEvent }) {
|
|
||||||
const handlers = useAppHandler(ev.kind);
|
|
||||||
const link = NostrLink.fromEvent(ev);
|
|
||||||
|
|
||||||
const profiles = handlers.apps
|
|
||||||
.filter(a => a.tags.find(b => b[0] === "web" && b[2] === "nevent"))
|
|
||||||
.map(a => ({ profile: mapEventToProfile(a), event: a }))
|
|
||||||
.filter(a => a.profile)
|
|
||||||
.slice(0, 5);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="card flex flex-col gap-3">
|
|
||||||
<small>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Sorry, we dont understand this event kind ({name}), please try one of the following apps instead!"
|
|
||||||
values={{
|
|
||||||
name: <KindName kind={ev.kind} />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</small>
|
|
||||||
<div
|
|
||||||
className="flex justify-between items-center cursor-pointer"
|
|
||||||
onClick={() => {
|
|
||||||
window.open(`nostr:${link.encode()}`, "_blank");
|
|
||||||
}}>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<NostrIcon width={40} />
|
|
||||||
<FormattedMessage defaultMessage="Native App" />
|
|
||||||
</div>
|
|
||||||
<Icon name="link" />
|
|
||||||
</div>
|
|
||||||
{profiles.map(a => (
|
|
||||||
<div
|
|
||||||
className="flex justify-between items-center cursor-pointer"
|
|
||||||
key={a.event.id}
|
|
||||||
onClick={() => {
|
|
||||||
const webHandler = a.event.tags.find(a => a[0] === "web" && a[2] === "nevent")?.[1];
|
|
||||||
if (webHandler) {
|
|
||||||
window.open(webHandler.replace("<bech32>", link.encode()), "_blank");
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Avatar size={40} pubkey={a.event.pubkey} user={a.profile} />
|
|
||||||
<div>
|
|
||||||
<DisplayName pubkey={a.event.pubkey} user={a.profile} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Icon name="link" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -148,7 +148,7 @@ export function NoteContextMenu({ ev, ...props }: NoteContextMenuProps) {
|
|||||||
)}
|
)}
|
||||||
<MenuItem onClick={handleReBroadcastButtonClick}>
|
<MenuItem onClick={handleReBroadcastButtonClick}>
|
||||||
<Icon name="relay" />
|
<Icon name="relay" />
|
||||||
<FormattedMessage defaultMessage="Broadcast Event" />
|
<FormattedMessage defaultMessage="Broadcast Event" id="Gxcr08" />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem onClick={() => translate()}>
|
<MenuItem onClick={() => translate()}>
|
||||||
<Icon name="translate" />
|
<Icon name="translate" />
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { barrierQueue } from "@snort/shared";
|
import { barrierQueue } from "@snort/shared";
|
||||||
import { NostrLink, ParsedZap, TaggedNostrEvent } from "@snort/system";
|
import { NostrLink, ParsedZap, TaggedNostrEvent } from "@snort/system";
|
||||||
import { useUserProfile } from "@snort/system-react";
|
import { useUserProfile } from "@snort/system-react";
|
||||||
import { Zapper, ZapTarget } from "@snort/wallet";
|
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import { useLongPress } from "use-long-press";
|
import { useLongPress } from "use-long-press";
|
||||||
@ -14,6 +13,7 @@ import useEventPublisher from "@/Hooks/useEventPublisher";
|
|||||||
import useLogin from "@/Hooks/useLogin";
|
import useLogin from "@/Hooks/useLogin";
|
||||||
import usePreferences from "@/Hooks/usePreferences";
|
import usePreferences from "@/Hooks/usePreferences";
|
||||||
import { getDisplayName } from "@/Utils";
|
import { getDisplayName } from "@/Utils";
|
||||||
|
import { Zapper, ZapTarget } from "@/Utils/Zapper";
|
||||||
import { ZapPoolController } from "@/Utils/ZapPoolController";
|
import { ZapPoolController } from "@/Utils/ZapPoolController";
|
||||||
import { useWallet } from "@/Wallet";
|
import { useWallet } from "@/Wallet";
|
||||||
|
|
||||||
@ -140,7 +140,13 @@ export const FooterZapButton = ({ ev, zaps, onClickZappers }: ZapIconProps) => {
|
|||||||
<ZapsSummary zaps={zaps} onClick={onClickZappers ?? (() => {})} />
|
<ZapsSummary zaps={zaps} onClick={onClickZappers ?? (() => {})} />
|
||||||
</div>
|
</div>
|
||||||
{showZapModal && (
|
{showZapModal && (
|
||||||
<ZapModal targets={getZapTarget()} onClose={() => setShowZapModal(false)} show={true} allocatePool={true} />
|
<ZapModal
|
||||||
|
targets={getZapTarget()}
|
||||||
|
onClose={() => setShowZapModal(false)}
|
||||||
|
note={ev.id}
|
||||||
|
show={true}
|
||||||
|
allocatePool={true}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -9,7 +9,6 @@ import { ReplyButton } from "@/Components/Event/Note/NoteFooter/ReplyButton";
|
|||||||
import { RepostButton } from "@/Components/Event/Note/NoteFooter/RepostButton";
|
import { RepostButton } from "@/Components/Event/Note/NoteFooter/RepostButton";
|
||||||
import ReactionsModal from "@/Components/Event/Note/ReactionsModal";
|
import ReactionsModal from "@/Components/Event/Note/ReactionsModal";
|
||||||
import useLogin from "@/Hooks/useLogin";
|
import useLogin from "@/Hooks/useLogin";
|
||||||
import useModeration from "@/Hooks/useModeration";
|
|
||||||
import usePreferences from "@/Hooks/usePreferences";
|
import usePreferences from "@/Hooks/usePreferences";
|
||||||
|
|
||||||
export interface NoteFooterProps {
|
export interface NoteFooterProps {
|
||||||
@ -20,14 +19,11 @@ export interface NoteFooterProps {
|
|||||||
export default function NoteFooter(props: NoteFooterProps) {
|
export default function NoteFooter(props: NoteFooterProps) {
|
||||||
const { ev } = props;
|
const { ev } = props;
|
||||||
const link = useMemo(() => NostrLink.fromEvent(ev), [ev.id]);
|
const link = useMemo(() => NostrLink.fromEvent(ev), [ev.id]);
|
||||||
|
const ids = useMemo(() => [link], [link]);
|
||||||
const [showReactions, setShowReactions] = useState(false);
|
const [showReactions, setShowReactions] = useState(false);
|
||||||
const { isMuted } = useModeration();
|
|
||||||
|
|
||||||
const related = useReactions("reactions", link);
|
const related = useReactions("reactions", ids, undefined, false);
|
||||||
const { replies, reactions, zaps, reposts } = useEventReactions(
|
const { replies, reactions, zaps, reposts } = useEventReactions(link, related);
|
||||||
link,
|
|
||||||
related.filter(a => !isMuted(a.pubkey)),
|
|
||||||
);
|
|
||||||
const { positive } = reactions;
|
const { positive } = reactions;
|
||||||
|
|
||||||
const readonly = useLogin(s => s.readonly);
|
const readonly = useLogin(s => s.readonly);
|
||||||
|
@ -58,7 +58,7 @@ export const RepostButton = ({ ev, reposts }: { ev: TaggedNostrEvent; reposts: T
|
|||||||
</div>
|
</div>
|
||||||
<MenuItem onClick={repost} disabled={hasReposted()}>
|
<MenuItem onClick={repost} disabled={hasReposted()}>
|
||||||
<Icon name="repeat" />
|
<Icon name="repeat" />
|
||||||
<FormattedMessage defaultMessage="Repost" />
|
<FormattedMessage defaultMessage="Repost" id="JeoS4y" />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@ -69,7 +69,7 @@ export const RepostButton = ({ ev, reposts }: { ev: TaggedNostrEvent; reposts: T
|
|||||||
})
|
})
|
||||||
}>
|
}>
|
||||||
<Icon name="edit" />
|
<Icon name="edit" />
|
||||||
<FormattedMessage defaultMessage="Quote Repost" />
|
<FormattedMessage defaultMessage="Quote Repost" id="C7642/" />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
|
@ -1,15 +1,21 @@
|
|||||||
import classNames from "classnames";
|
import "../EventComponent.css";
|
||||||
import { FormattedMessage } from "react-intl";
|
|
||||||
|
import ProfileImage from "@/Components/User/ProfileImage";
|
||||||
|
|
||||||
interface NoteGhostProps {
|
interface NoteGhostProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
link: string;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NoteGhost(props: NoteGhostProps) {
|
export default function NoteGhost(props: NoteGhostProps) {
|
||||||
|
const className = `note card ${props.className ?? ""}`;
|
||||||
return (
|
return (
|
||||||
<div className={classNames("p bb", props.className)}>
|
<div className={className}>
|
||||||
<FormattedMessage defaultMessage="Loading note: {id}" values={{ id: props.link }} />
|
<div className="header">
|
||||||
|
<ProfileImage pubkey="" />
|
||||||
|
</div>
|
||||||
|
<div className="body">{props.children}</div>
|
||||||
|
<div className="footer"></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,8 @@
|
|||||||
import { dedupe, sanitizeRelayUrl } from "@snort/shared";
|
import { NostrLink } from "@snort/system";
|
||||||
import { NostrLink, NostrPrefix } from "@snort/system";
|
|
||||||
import { useEventFeed } from "@snort/system-react";
|
import { useEventFeed } from "@snort/system-react";
|
||||||
import { useState } from "react";
|
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
|
||||||
|
|
||||||
import AsyncButton from "@/Components/Button/AsyncButton";
|
|
||||||
import Copy from "@/Components/Copy/Copy";
|
|
||||||
import Note from "@/Components/Event/EventComponent";
|
import Note from "@/Components/Event/EventComponent";
|
||||||
import Spinner from "@/Components/Icons/Spinner";
|
import PageSpinner from "@/Components/PageSpinner";
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
showFooter: false,
|
showFooter: false,
|
||||||
@ -15,52 +10,11 @@ const options = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function NoteQuote({ link, depth }: { link: NostrLink; depth?: number }) {
|
export default function NoteQuote({ link, depth }: { link: NostrLink; depth?: number }) {
|
||||||
const [tryLink, setLink] = useState<NostrLink>(link);
|
const ev = useEventFeed(link);
|
||||||
const [tryRelay, setTryRelay] = useState("");
|
|
||||||
const { formatMessage } = useIntl();
|
|
||||||
|
|
||||||
const ev = useEventFeed(tryLink);
|
|
||||||
if (!ev)
|
if (!ev)
|
||||||
return (
|
return (
|
||||||
<div className="note-quote flex flex-col gap-2">
|
<div className="note-quote flex items-center justify-center h-[110px]">
|
||||||
<Spinner />
|
<PageSpinner />
|
||||||
<div>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Looking for: {noteId}"
|
|
||||||
values={{
|
|
||||||
noteId: <Copy text={tryLink.encode()} />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={tryRelay}
|
|
||||||
onChange={e => setTryRelay(e.target.value)}
|
|
||||||
placeholder={formatMessage({ defaultMessage: "Try another relay" })}
|
|
||||||
/>
|
|
||||||
<AsyncButton
|
|
||||||
onClick={() => {
|
|
||||||
const u = sanitizeRelayUrl(tryRelay);
|
|
||||||
if (u) {
|
|
||||||
const relays = tryLink.relays ?? [];
|
|
||||||
relays.push(u);
|
|
||||||
setLink(
|
|
||||||
new NostrLink(
|
|
||||||
tryLink.type !== NostrPrefix.Address ? NostrPrefix.Event : NostrPrefix.Address,
|
|
||||||
tryLink.id,
|
|
||||||
tryLink.kind,
|
|
||||||
tryLink.author,
|
|
||||||
dedupe(relays),
|
|
||||||
tryLink.marker,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
setTryRelay("");
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
<FormattedMessage defaultMessage="Add" />
|
|
||||||
</AsyncButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
return <Note data={ev} className="note-quote" depth={(depth ?? 0) + 1} options={options} />;
|
return <Note data={ev} className="note-quote" depth={(depth ?? 0) + 1} options={options} />;
|
||||||
|
@ -27,7 +27,11 @@ export const NoteText = memo(function InnerContent(
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setShowMore(!showMore);
|
setShowMore(!showMore);
|
||||||
}}>
|
}}>
|
||||||
{showMore ? <FormattedMessage defaultMessage="Show less" /> : <FormattedMessage defaultMessage="Show more" />}
|
{showMore ? (
|
||||||
|
<FormattedMessage defaultMessage="Show less" id="qyJtWy" />
|
||||||
|
) : (
|
||||||
|
<FormattedMessage defaultMessage="Show more" id="aWpBzj" />
|
||||||
|
)}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -76,10 +80,10 @@ export const NoteText = memo(function InnerContent(
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
. <FormattedMessage defaultMessage="Click here to load anyway" />.{" "}
|
. <FormattedMessage defaultMessage="Click here to load anyway" id="IoQq+a" />.{" "}
|
||||||
<Link to="/settings/moderation">
|
<Link to="/settings/moderation">
|
||||||
<i>
|
<i>
|
||||||
<FormattedMessage defaultMessage="Settings" />
|
<FormattedMessage defaultMessage="Settings" id="D3idYv" />
|
||||||
</i>
|
</i>
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
|
@ -4,20 +4,19 @@ import { FormattedMessage } from "react-intl";
|
|||||||
export interface NoteTimeProps {
|
export interface NoteTimeProps {
|
||||||
from: number;
|
from: number;
|
||||||
fallback?: string;
|
fallback?: string;
|
||||||
className?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const secondsInAMinute = 60;
|
const secondsInAMinute = 60;
|
||||||
const secondsInAnHour = secondsInAMinute * 60;
|
const secondsInAnHour = secondsInAMinute * 60;
|
||||||
const secondsInADay = secondsInAnHour * 24;
|
const secondsInADay = secondsInAnHour * 24;
|
||||||
|
|
||||||
const NoteTime: React.FC<NoteTimeProps> = ({ from, fallback, className }) => {
|
const NoteTime: React.FC<NoteTimeProps> = ({ from, fallback }) => {
|
||||||
const calcTime = useCallback((fromTime: number) => {
|
const calcTime = useCallback((fromTime: number) => {
|
||||||
const currentTime = new Date();
|
const currentTime = new Date();
|
||||||
const timeDifference = Math.floor((currentTime.getTime() - fromTime) / 1000);
|
const timeDifference = Math.floor((currentTime.getTime() - fromTime) / 1000);
|
||||||
|
|
||||||
if (timeDifference < secondsInAMinute) {
|
if (timeDifference < secondsInAMinute) {
|
||||||
return <FormattedMessage defaultMessage="now" />;
|
return <FormattedMessage defaultMessage="now" id="kaaf1E" />;
|
||||||
} else if (timeDifference < secondsInAnHour) {
|
} else if (timeDifference < secondsInAnHour) {
|
||||||
return `${Math.floor(timeDifference / secondsInAMinute)}m`;
|
return `${Math.floor(timeDifference / secondsInAMinute)}m`;
|
||||||
} else if (timeDifference < secondsInADay) {
|
} else if (timeDifference < secondsInADay) {
|
||||||
@ -53,7 +52,7 @@ const NoteTime: React.FC<NoteTimeProps> = ({ from, fallback, className }) => {
|
|||||||
const isoDate = useMemo(() => new Date(from).toISOString(), [from]);
|
const isoDate = useMemo(() => new Date(from).toISOString(), [from]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<time dateTime={isoDate} title={absoluteTime} className={className}>
|
<time dateTime={isoDate} title={absoluteTime}>
|
||||||
{time || fallback}
|
{time || fallback}
|
||||||
</time>
|
</time>
|
||||||
);
|
);
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
import { NostrLink, TaggedNostrEvent } from "@snort/system";
|
import "./ReactionsModal.css";
|
||||||
|
|
||||||
|
import { NostrLink, socialGraphInstance, TaggedNostrEvent } from "@snort/system";
|
||||||
import { useEventReactions, useReactions } from "@snort/system-react";
|
import { useEventReactions, useReactions } from "@snort/system-react";
|
||||||
import { Fragment, useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { FormattedMessage, MessageDescriptor, useIntl } from "react-intl";
|
import { FormattedMessage, MessageDescriptor, useIntl } from "react-intl";
|
||||||
|
|
||||||
|
import CloseButton from "@/Components/Button/CloseButton";
|
||||||
import Icon from "@/Components/Icons/Icon";
|
import Icon from "@/Components/Icons/Icon";
|
||||||
import Modal from "@/Components/Modal/Modal";
|
import Modal from "@/Components/Modal/Modal";
|
||||||
import TabSelectors, { Tab } from "@/Components/TabSelectors/TabSelectors";
|
import TabSelectors, { Tab } from "@/Components/TabSelectors/TabSelectors";
|
||||||
import ProfileImage from "@/Components/User/ProfileImage";
|
import ProfileImage from "@/Components/User/ProfileImage";
|
||||||
import ZapAmount from "@/Components/zap-amount";
|
import { formatShort } from "@/Utils/Number";
|
||||||
import useWoT from "@/Hooks/useWoT";
|
|
||||||
|
|
||||||
import messages from "../../messages";
|
import messages from "../../messages";
|
||||||
|
|
||||||
@ -23,11 +25,14 @@ const ReactionsModal = ({ onClose, event, initialTab = 0 }: ReactionsModalProps)
|
|||||||
|
|
||||||
const link = NostrLink.fromEvent(event);
|
const link = NostrLink.fromEvent(event);
|
||||||
|
|
||||||
const related = useReactions("reactions", link, undefined, false);
|
const related = useReactions("note:reactions", [link], undefined, false);
|
||||||
const { reactions, zaps, reposts } = useEventReactions(link, related);
|
const { reactions, zaps, reposts } = useEventReactions(link, related);
|
||||||
const { positive, negative } = reactions;
|
const { positive, negative } = reactions;
|
||||||
|
|
||||||
const { sortEvents } = useWoT();
|
const sortEvents = (events: Array<TaggedNostrEvent>) =>
|
||||||
|
events.sort(
|
||||||
|
(a, b) => socialGraphInstance.getFollowDistance(a.pubkey) - socialGraphInstance.getFollowDistance(b.pubkey),
|
||||||
|
);
|
||||||
|
|
||||||
const likes = useMemo(() => sortEvents([...positive]), [positive]);
|
const likes = useMemo(() => sortEvents([...positive]), [positive]);
|
||||||
const dislikes = useMemo(() => sortEvents([...negative]), [negative]);
|
const dislikes = useMemo(() => sortEvents([...negative]), [negative]);
|
||||||
@ -55,42 +60,48 @@ const ReactionsModal = ({ onClose, event, initialTab = 0 }: ReactionsModalProps)
|
|||||||
const [tab, setTab] = useState(tabs[initialTab]);
|
const [tab, setTab] = useState(tabs[initialTab]);
|
||||||
|
|
||||||
const renderReactionItem = (ev: TaggedNostrEvent, icon: string, iconClass?: string, size?: number) => (
|
const renderReactionItem = (ev: TaggedNostrEvent, icon: string, iconClass?: string, size?: number) => (
|
||||||
<Fragment key={ev.id}>
|
<div key={ev.id} className="reactions-item">
|
||||||
<div className="mx-auto">
|
<div className="reaction-icon">
|
||||||
<Icon name={icon} size={size} className={iconClass} />
|
<Icon name={icon} size={size} className={iconClass} />
|
||||||
</div>
|
</div>
|
||||||
<ProfileImage pubkey={ev.pubkey} showProfileCard={true} />
|
<ProfileImage pubkey={ev.pubkey} showProfileCard={true} />
|
||||||
</Fragment>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal id="reactions" onClose={onClose}>
|
<Modal id="reactions" className="reactions-modal" onClose={onClose}>
|
||||||
<div className="text-lg font-semibold mb-2">
|
<CloseButton onClick={onClose} className="absolute right-4 top-3" />
|
||||||
<FormattedMessage defaultMessage="Reactions ({n})" values={{ n: total }} />
|
<div className="reactions-header">
|
||||||
|
<h2>
|
||||||
|
<FormattedMessage {...messages.ReactionsCount} values={{ n: total }} />
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<TabSelectors tabs={tabs} tab={tab} setTab={setTab} />
|
<TabSelectors tabs={tabs} tab={tab} setTab={setTab} />
|
||||||
<div className="h-[30vh] overflow-y-auto">
|
<div className="reactions-body" key={tab.value}>
|
||||||
<div className="grid grid-cols-[100px_auto] gap-y-2 items-center py-2" key={tab.value}>
|
{tab.value === 0 && likes.map(ev => renderReactionItem(ev, "heart-solid", "text-heart"))}
|
||||||
{tab.value === 0 && likes.map(ev => renderReactionItem(ev, "heart-solid", "text-heart"))}
|
{tab.value === 1 &&
|
||||||
{tab.value === 1 &&
|
zaps.map(
|
||||||
zaps.map(
|
z =>
|
||||||
z =>
|
z.sender && (
|
||||||
z.sender && (
|
<div key={z.id} className="reactions-item">
|
||||||
<Fragment key={z.id}>
|
<div className="zap-reaction-icon">
|
||||||
<ZapAmount n={z.amount} />
|
<Icon name="zap-solid" size={20} className="text-zap" />
|
||||||
<ProfileImage
|
<span className="zap-amount">{formatShort(z.amount)}</span>
|
||||||
showProfileCard={true}
|
</div>
|
||||||
pubkey={z.anonZap ? "" : z.sender}
|
<ProfileImage
|
||||||
subHeader={<div title={z.content}>{z.content}</div>}
|
showProfileCard={true}
|
||||||
link={z.anonZap ? "" : undefined}
|
pubkey={z.anonZap ? "" : z.sender}
|
||||||
overrideUsername={z.anonZap ? formatMessage({ defaultMessage: "Anonymous" }) : undefined}
|
subHeader={<div title={z.content}>{z.content}</div>}
|
||||||
/>
|
link={z.anonZap ? "" : undefined}
|
||||||
</Fragment>
|
overrideUsername={
|
||||||
),
|
z.anonZap ? formatMessage({ defaultMessage: "Anonymous", id: "LXxsbk" }) : undefined
|
||||||
)}
|
}
|
||||||
{tab.value === 2 && sortedReposts.map(ev => renderReactionItem(ev, "repost", "text-repost", 16))}
|
/>
|
||||||
{tab.value === 3 && dislikes.map(ev => renderReactionItem(ev, "dislike"))}
|
</div>
|
||||||
</div>
|
),
|
||||||
|
)}
|
||||||
|
{tab.value === 2 && sortedReposts.map(ev => renderReactionItem(ev, "repost", "text-repost", 16))}
|
||||||
|
{tab.value === 3 && dislikes.map(ev => renderReactionItem(ev, "dislike"))}
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
@ -12,8 +12,6 @@ import Icon from "@/Components/Icons/Icon";
|
|||||||
import useModeration from "@/Hooks/useModeration";
|
import useModeration from "@/Hooks/useModeration";
|
||||||
import { eventLink, getDisplayName, hexToBech32 } from "@/Utils";
|
import { eventLink, getDisplayName, hexToBech32 } from "@/Utils";
|
||||||
|
|
||||||
import NoteFooter from "./Note/NoteFooter/NoteFooter";
|
|
||||||
|
|
||||||
export interface NoteReactionProps {
|
export interface NoteReactionProps {
|
||||||
data: TaggedNostrEvent;
|
data: TaggedNostrEvent;
|
||||||
root?: TaggedNostrEvent;
|
root?: TaggedNostrEvent;
|
||||||
@ -87,13 +85,13 @@ export default function NoteReaction(props: NoteReactionProps) {
|
|||||||
<Icon name="repeat" size={18} />
|
<Icon name="repeat" size={18} />
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
defaultMessage="{name} reposted"
|
defaultMessage="{name} reposted"
|
||||||
|
id="+xliwN"
|
||||||
values={{
|
values={{
|
||||||
name: getDisplayName(profile, ev.pubkey),
|
name: getDisplayName(profile, ev.pubkey),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{root ? <Note data={root} options={opt} depth={props.depth} /> : null}
|
{root ? <Note data={root} options={opt} depth={props.depth} /> : null}
|
||||||
<NoteFooter ev={ev} />
|
|
||||||
{!root && refEvent ? (
|
{!root && refEvent ? (
|
||||||
<p>
|
<p>
|
||||||
<Link to={eventLink(refEvent[1] ?? "", refEvent[2])}>
|
<Link to={eventLink(refEvent[1] ?? "", refEvent[2])}>
|
||||||
|
@ -135,9 +135,9 @@ export default function Poll(props: PollProps) {
|
|||||||
values={{
|
values={{
|
||||||
type:
|
type:
|
||||||
tallyBy === "zaps" ? (
|
tallyBy === "zaps" ? (
|
||||||
<FormattedMessage defaultMessage="zap" />
|
<FormattedMessage defaultMessage="zap" id="5BVs2e" />
|
||||||
) : (
|
) : (
|
||||||
<FormattedMessage defaultMessage="user" />
|
<FormattedMessage defaultMessage="user" id="sUNhQE" />
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -42,7 +42,7 @@
|
|||||||
top: 48px;
|
top: 48px;
|
||||||
border-left: 1px solid var(--border-color);
|
border-left: 1px solid var(--border-color);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
z-index: -1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subthread-container.subthread-mid:not(.subthread-last) .line-container:before {
|
.subthread-container.subthread-mid:not(.subthread-last) .line-container:before {
|
||||||
@ -52,7 +52,7 @@
|
|||||||
left: calc(48px / 2 + 16px);
|
left: calc(48px / 2 + 16px);
|
||||||
top: 0;
|
top: 0;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
z-index: -1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subthread-container.subthread-last .line-container:before {
|
.subthread-container.subthread-last .line-container:before {
|
||||||
@ -62,7 +62,7 @@
|
|||||||
left: calc(48px / 2 + 16px);
|
left: calc(48px / 2 + 16px);
|
||||||
top: 0;
|
top: 0;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
z-index: -1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider {
|
.divider {
|
||||||
|
@ -57,6 +57,8 @@ export function Thread(props: { onBack?: () => void; disableSpotlight?: boolean
|
|||||||
waitUntilInView={false}
|
waitUntilInView={false}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
return <NoteGhost className={className}>Loading thread root.. ({thread.data?.length} notes loaded)</NoteGhost>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,18 +75,16 @@ export function Thread(props: { onBack?: () => void; disableSpotlight?: boolean
|
|||||||
function renderCurrent() {
|
function renderCurrent() {
|
||||||
if (thread.current) {
|
if (thread.current) {
|
||||||
const note = thread.data.find(n => n.id === thread.current);
|
const note = thread.data.find(n => n.id === thread.current);
|
||||||
if (note) {
|
return (
|
||||||
return (
|
note && (
|
||||||
<Note
|
<Note
|
||||||
data={note}
|
data={note}
|
||||||
options={{ showReactionsLink: true, showMediaSpotlight: true }}
|
options={{ showReactionsLink: true, showMediaSpotlight: true }}
|
||||||
threadChains={thread.chains}
|
threadChains={thread.chains}
|
||||||
onClick={navigateThread}
|
onClick={navigateThread}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
} else {
|
);
|
||||||
return <NoteGhost link={thread.current} />;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,6 +100,7 @@ export function Thread(props: { onBack?: () => void; disableSpotlight?: boolean
|
|||||||
|
|
||||||
const parentText = formatMessage({
|
const parentText = formatMessage({
|
||||||
defaultMessage: "Parent",
|
defaultMessage: "Parent",
|
||||||
|
id: "ADmfQT",
|
||||||
description: "Link to parent note in thread",
|
description: "Link to parent note in thread",
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -133,15 +134,10 @@ export function Thread(props: { onBack?: () => void; disableSpotlight?: boolean
|
|||||||
{thread.root && renderRoot(thread.root)}
|
{thread.root && renderRoot(thread.root)}
|
||||||
{thread.root && renderChain(chainKey(thread.root))}
|
{thread.root && renderChain(chainKey(thread.root))}
|
||||||
{!thread.root && renderCurrent()}
|
{!thread.root && renderCurrent()}
|
||||||
{thread.mutedData.length > 0 && (
|
{!thread.root && !thread.current && (
|
||||||
<div className="p br b mx-2 my-3 bg-gray-ultradark text-gray-light font-medium cursor-pointer">
|
<NoteGhost>
|
||||||
<FormattedMessage
|
<FormattedMessage defaultMessage="Looking up thread..." id="JA+tz3" />
|
||||||
defaultMessage="{n} notes have been muted"
|
</NoteGhost>
|
||||||
values={{
|
|
||||||
n: thread.mutedData.length,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import "./ZapButton.css";
|
import "./ZapButton.css";
|
||||||
|
|
||||||
import { HexKey, NostrLink } from "@snort/system";
|
import { HexKey } from "@snort/system";
|
||||||
import { useUserProfile } from "@snort/system-react";
|
import { useUserProfile } from "@snort/system-react";
|
||||||
import { ZapTarget } from "@snort/wallet";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import Icon from "@/Components/Icons/Icon";
|
import Icon from "@/Components/Icons/Icon";
|
||||||
import ZapModal from "@/Components/ZapModal/ZapModal";
|
import ZapModal from "@/Components/ZapModal/ZapModal";
|
||||||
|
import { ZapTarget } from "@/Utils/Zapper";
|
||||||
|
|
||||||
const ZapButton = ({
|
const ZapButton = ({
|
||||||
pubkey,
|
pubkey,
|
||||||
@ -17,7 +17,7 @@ const ZapButton = ({
|
|||||||
pubkey: HexKey;
|
pubkey: HexKey;
|
||||||
lnurl?: string;
|
lnurl?: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
event?: NostrLink;
|
event?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const profile = useUserProfile(pubkey);
|
const profile = useUserProfile(pubkey);
|
||||||
const [zap, setZap] = useState(false);
|
const [zap, setZap] = useState(false);
|
||||||
@ -37,11 +37,12 @@ const ZapButton = ({
|
|||||||
value: service,
|
value: service,
|
||||||
weight: 1,
|
weight: 1,
|
||||||
name: profile?.display_name || profile?.name,
|
name: profile?.display_name || profile?.name,
|
||||||
zap: { pubkey: pubkey, event },
|
zap: { pubkey: pubkey },
|
||||||
} as ZapTarget,
|
} as ZapTarget,
|
||||||
]}
|
]}
|
||||||
show={zap}
|
show={zap}
|
||||||
onClose={() => setZap(false)}
|
onClose={() => setZap(false)}
|
||||||
|
note={event}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import "./ZapGoal.css";
|
import "./ZapGoal.css";
|
||||||
|
|
||||||
import { NostrEvent, NostrLink } from "@snort/system";
|
import { NostrEvent, NostrLink } from "@snort/system";
|
||||||
import { Zapper } from "@snort/wallet";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { FormattedNumber } from "react-intl";
|
import { FormattedNumber } from "react-intl";
|
||||||
|
|
||||||
@ -11,6 +10,7 @@ import ZapModal from "@/Components/ZapModal/ZapModal";
|
|||||||
import useZapsFeed from "@/Feed/ZapsFeed";
|
import useZapsFeed from "@/Feed/ZapsFeed";
|
||||||
import { findTag } from "@/Utils";
|
import { findTag } from "@/Utils";
|
||||||
import { formatShort } from "@/Utils/Number";
|
import { formatShort } from "@/Utils/Number";
|
||||||
|
import { Zapper } from "@/Utils/Zapper";
|
||||||
|
|
||||||
export function ZapGoal({ ev }: { ev: NostrEvent }) {
|
export function ZapGoal({ ev }: { ev: NostrEvent }) {
|
||||||
const [zap, setZap] = useState(false);
|
const [zap, setZap] = useState(false);
|
||||||
|
@ -26,9 +26,13 @@ export const ZapsSummary = ({ zaps, onClick }: ZapsSummaryProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center cursor-pointer" onClick={myOnClick}>
|
<div className="zaps-summary" onClick={myOnClick}>
|
||||||
<AvatarGroup ids={sortedZappers} onClick={() => {}} />
|
<div className={`top-zap`}>
|
||||||
{zaps.length > 3 && <div className="hidden md:inline-flex">+{zaps.length - 3}</div>}
|
<div className="summary">
|
||||||
|
<AvatarGroup ids={sortedZappers} onClick={() => {}} />
|
||||||
|
{zaps.length > 3 && <div className="hidden md:flex -ml-2">+{zaps.length - 3}</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -13,7 +13,7 @@ export function rootTabItems(base: string, pubKey: string | undefined, tags: Arr
|
|||||||
element: (
|
element: (
|
||||||
<>
|
<>
|
||||||
<Icon name="user-v2" />
|
<Icon name="user-v2" />
|
||||||
<FormattedMessage defaultMessage="For you" />
|
<FormattedMessage defaultMessage="For you" id="xEjBS7" />
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@ -24,7 +24,7 @@ export function rootTabItems(base: string, pubKey: string | undefined, tags: Arr
|
|||||||
element: (
|
element: (
|
||||||
<>
|
<>
|
||||||
<Icon name="user-v2" />
|
<Icon name="user-v2" />
|
||||||
<FormattedMessage defaultMessage="Following" />
|
<FormattedMessage defaultMessage="Following" id="cPIKU2" />
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@ -35,7 +35,7 @@ export function rootTabItems(base: string, pubKey: string | undefined, tags: Arr
|
|||||||
element: (
|
element: (
|
||||||
<>
|
<>
|
||||||
<Icon name="fire" />
|
<Icon name="fire" />
|
||||||
<FormattedMessage defaultMessage="Trending Notes" />
|
<FormattedMessage defaultMessage="Trending Notes" id="Ix8l+B" />
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@ -46,7 +46,7 @@ export function rootTabItems(base: string, pubKey: string | undefined, tags: Arr
|
|||||||
element: (
|
element: (
|
||||||
<>
|
<>
|
||||||
<Icon name="message-chat-circle" />
|
<Icon name="message-chat-circle" />
|
||||||
<FormattedMessage defaultMessage="Conversations" />
|
<FormattedMessage defaultMessage="Conversations" id="1udzha" />
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@ -57,7 +57,7 @@ export function rootTabItems(base: string, pubKey: string | undefined, tags: Arr
|
|||||||
element: (
|
element: (
|
||||||
<>
|
<>
|
||||||
<Icon name="user-v2" />
|
<Icon name="user-v2" />
|
||||||
<FormattedMessage defaultMessage="Followed by friends" />
|
<FormattedMessage defaultMessage="Followed by friends" id="voxBKC" />
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@ -68,7 +68,7 @@ export function rootTabItems(base: string, pubKey: string | undefined, tags: Arr
|
|||||||
element: (
|
element: (
|
||||||
<>
|
<>
|
||||||
<Icon name="thumbs-up" />
|
<Icon name="thumbs-up" />
|
||||||
<FormattedMessage defaultMessage="Suggested Follows" />
|
<FormattedMessage defaultMessage="Suggested Follows" id="C8HhVE" />
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@ -79,7 +79,18 @@ export function rootTabItems(base: string, pubKey: string | undefined, tags: Arr
|
|||||||
element: (
|
element: (
|
||||||
<>
|
<>
|
||||||
<Icon name="hash" />
|
<Icon name="hash" />
|
||||||
<FormattedMessage defaultMessage="Trending Hashtags" />
|
<FormattedMessage defaultMessage="Trending Hashtags" id="XXm7jJ" />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tab: "global",
|
||||||
|
path: `${base}/global`,
|
||||||
|
show: true,
|
||||||
|
element: (
|
||||||
|
<>
|
||||||
|
<Icon name="globe" />
|
||||||
|
<FormattedMessage defaultMessage="Global" id="EWyQH5" />
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@ -90,7 +101,7 @@ export function rootTabItems(base: string, pubKey: string | undefined, tags: Arr
|
|||||||
element: (
|
element: (
|
||||||
<>
|
<>
|
||||||
<Icon name="hash" />
|
<Icon name="hash" />
|
||||||
<FormattedMessage defaultMessage="Topics" />
|
<FormattedMessage defaultMessage="Topics" id="kc79d3" />
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import "./Timeline.css";
|
import "./Timeline.css";
|
||||||
|
|
||||||
import { unixNow } from "@snort/shared";
|
import { socialGraphInstance, TaggedNostrEvent } from "@snort/system";
|
||||||
import { TaggedNostrEvent } from "@snort/system";
|
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
|
|
||||||
import { DisplayAs, DisplayAsSelector } from "@/Components/Feed/DisplayAsSelector";
|
import { DisplayAs, DisplayAsSelector } from "@/Components/Feed/DisplayAsSelector";
|
||||||
@ -9,7 +8,6 @@ import { TimelineRenderer } from "@/Components/Feed/TimelineRenderer";
|
|||||||
import useTimelineFeed, { TimelineFeed, TimelineSubject } from "@/Feed/TimelineFeed";
|
import useTimelineFeed, { TimelineFeed, TimelineSubject } from "@/Feed/TimelineFeed";
|
||||||
import useHistoryState from "@/Hooks/useHistoryState";
|
import useHistoryState from "@/Hooks/useHistoryState";
|
||||||
import useLogin from "@/Hooks/useLogin";
|
import useLogin from "@/Hooks/useLogin";
|
||||||
import useWoT from "@/Hooks/useWoT";
|
|
||||||
import { dedupeByPubkey } from "@/Utils";
|
import { dedupeByPubkey } from "@/Utils";
|
||||||
|
|
||||||
export interface TimelineProps {
|
export interface TimelineProps {
|
||||||
@ -30,7 +28,7 @@ export interface TimelineProps {
|
|||||||
*/
|
*/
|
||||||
const Timeline = (props: TimelineProps) => {
|
const Timeline = (props: TimelineProps) => {
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const [openedAt] = useHistoryState(unixNow(), "openedAt");
|
const [openedAt] = useHistoryState(Math.floor(Date.now() / 1000), "openedAt");
|
||||||
const feedOptions = useMemo(
|
const feedOptions = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
method: props.method,
|
method: props.method,
|
||||||
@ -42,7 +40,6 @@ const Timeline = (props: TimelineProps) => {
|
|||||||
const feed: TimelineFeed = useTimelineFeed(props.subject, feedOptions);
|
const feed: TimelineFeed = useTimelineFeed(props.subject, feedOptions);
|
||||||
const displayAsInitial = props.displayAs ?? login.feedDisplayAs ?? "list";
|
const displayAsInitial = props.displayAs ?? login.feedDisplayAs ?? "list";
|
||||||
const [displayAs, setDisplayAs] = useState<DisplayAs>(displayAsInitial);
|
const [displayAs, setDisplayAs] = useState<DisplayAs>(displayAsInitial);
|
||||||
const wot = useWoT();
|
|
||||||
|
|
||||||
const filterPosts = useCallback(
|
const filterPosts = useCallback(
|
||||||
(nts: readonly TaggedNostrEvent[]) => {
|
(nts: readonly TaggedNostrEvent[]) => {
|
||||||
@ -50,7 +47,7 @@ const Timeline = (props: TimelineProps) => {
|
|||||||
if (props.followDistance === undefined) {
|
if (props.followDistance === undefined) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const followDistance = wot.followDistance(a.pubkey);
|
const followDistance = socialGraphInstance.getFollowDistance(a.pubkey);
|
||||||
return followDistance === props.followDistance;
|
return followDistance === props.followDistance;
|
||||||
};
|
};
|
||||||
return nts
|
return nts
|
||||||
|
@ -1,48 +0,0 @@
|
|||||||
import { NostrEvent, RequestBuilder } from "@snort/system";
|
|
||||||
import { useRequestBuilder } from "@snort/system-react";
|
|
||||||
import { ReactNode, useMemo } from "react";
|
|
||||||
|
|
||||||
import { WindowChunk } from "@/Hooks/useTimelineChunks";
|
|
||||||
|
|
||||||
import { DisplayAs } from "./DisplayAsSelector";
|
|
||||||
import { TimelineRenderer } from "./TimelineRenderer";
|
|
||||||
|
|
||||||
export interface TimelineChunkProps {
|
|
||||||
id: string;
|
|
||||||
chunk: WindowChunk;
|
|
||||||
builder: (rb: RequestBuilder) => void;
|
|
||||||
noteFilter?: (ev: NostrEvent) => boolean;
|
|
||||||
noteRenderer?: (ev: NostrEvent) => ReactNode;
|
|
||||||
noteOnClick?: (ev: NostrEvent) => void;
|
|
||||||
displayAs?: DisplayAs;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple chunk of a timeline using absoliute time ranges
|
|
||||||
*/
|
|
||||||
export default function TimelineChunk(props: TimelineChunkProps) {
|
|
||||||
const sub = useMemo(() => {
|
|
||||||
const rb = new RequestBuilder(`timeline-chunk:${props.id}:${props.chunk.since}-${props.chunk.until}`);
|
|
||||||
props.builder(rb);
|
|
||||||
for (const f of rb.filterBuilders) {
|
|
||||||
f.since(props.chunk.since).until(props.chunk.until);
|
|
||||||
}
|
|
||||||
return rb;
|
|
||||||
}, [props.id, props.chunk.until, props.builder]);
|
|
||||||
|
|
||||||
const feed = useRequestBuilder(sub);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TimelineRenderer
|
|
||||||
frags={{
|
|
||||||
events: feed.filter(a => props.noteFilter?.(a) ?? true),
|
|
||||||
refTime: props.chunk.until,
|
|
||||||
}}
|
|
||||||
noteOnClick={props.noteOnClick}
|
|
||||||
noteRenderer={props.noteRenderer}
|
|
||||||
displayAs={props.displayAs}
|
|
||||||
latest={[]}
|
|
||||||
showLatest={() => {}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,18 +1,16 @@
|
|||||||
import "./Timeline.css";
|
import "./Timeline.css";
|
||||||
|
|
||||||
import { unixNow } from "@snort/shared";
|
import { EventKind, NostrEvent, TaggedNostrEvent } from "@snort/system";
|
||||||
import { EventKind, NostrEvent, RequestBuilder } from "@snort/system";
|
import { ReactNode, useCallback, useMemo, useState } from "react";
|
||||||
import { ReactNode, useCallback, useState } from "react";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { DisplayAs, DisplayAsSelector } from "@/Components/Feed/DisplayAsSelector";
|
import { DisplayAs, DisplayAsSelector } from "@/Components/Feed/DisplayAsSelector";
|
||||||
|
import { TimelineRenderer } from "@/Components/Feed/TimelineRenderer";
|
||||||
|
import useTimelineFeed, { TimelineFeedOptions, TimelineSubject } from "@/Feed/TimelineFeed";
|
||||||
import useFollowsControls from "@/Hooks/useFollowControls";
|
import useFollowsControls from "@/Hooks/useFollowControls";
|
||||||
import useHistoryState from "@/Hooks/useHistoryState";
|
import useHistoryState from "@/Hooks/useHistoryState";
|
||||||
import useLogin from "@/Hooks/useLogin";
|
import useLogin from "@/Hooks/useLogin";
|
||||||
import useTimelineChunks from "@/Hooks/useTimelineChunks";
|
import { dedupeByPubkey } from "@/Utils";
|
||||||
import { Hour } from "@/Utils/Const";
|
|
||||||
|
|
||||||
import { AutoLoadMore } from "../Event/LoadMore";
|
|
||||||
import TimelineChunk from "./TimelineChunk";
|
|
||||||
|
|
||||||
export interface TimelineFollowsProps {
|
export interface TimelineFollowsProps {
|
||||||
postsOnly: boolean;
|
postsOnly: boolean;
|
||||||
@ -21,10 +19,11 @@ export interface TimelineFollowsProps {
|
|||||||
noteRenderer?: (ev: NostrEvent) => ReactNode;
|
noteRenderer?: (ev: NostrEvent) => ReactNode;
|
||||||
noteOnClick?: (ev: NostrEvent) => void;
|
noteOnClick?: (ev: NostrEvent) => void;
|
||||||
displayAs?: DisplayAs;
|
displayAs?: DisplayAs;
|
||||||
|
showDisplayAsSelector?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list of notes by your follows
|
* A list of notes by "subject"
|
||||||
*/
|
*/
|
||||||
const TimelineFollows = (props: TimelineFollowsProps) => {
|
const TimelineFollows = (props: TimelineFollowsProps) => {
|
||||||
const login = useLogin(s => ({
|
const login = useLogin(s => ({
|
||||||
@ -34,44 +33,81 @@ const TimelineFollows = (props: TimelineFollowsProps) => {
|
|||||||
}));
|
}));
|
||||||
const displayAsInitial = props.displayAs ?? login.feedDisplayAs ?? "list";
|
const displayAsInitial = props.displayAs ?? login.feedDisplayAs ?? "list";
|
||||||
const [displayAs, setDisplayAs] = useState<DisplayAs>(displayAsInitial);
|
const [displayAs, setDisplayAs] = useState<DisplayAs>(displayAsInitial);
|
||||||
const [openedAt] = useHistoryState(unixNow(), "openedAt");
|
const [openedAt] = useHistoryState(Math.floor(Date.now() / 1000), "openedAt");
|
||||||
const { isFollowing, followList } = useFollowsControls();
|
const { isFollowing, followList } = useFollowsControls();
|
||||||
const { chunks, showMore } = useTimelineChunks({
|
const subject = useMemo(
|
||||||
now: openedAt,
|
() =>
|
||||||
firstChunkSize: Hour * 2,
|
({
|
||||||
});
|
type: "pubkey",
|
||||||
|
items: followList,
|
||||||
|
discriminator: login.publicKey?.slice(0, 12),
|
||||||
|
extra: rb => {
|
||||||
|
if (login.tags.length > 0) {
|
||||||
|
rb.withFilter().kinds([EventKind.TextNote, EventKind.Repost]).tags(login.tags);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}) as TimelineSubject,
|
||||||
|
[login.publicKey, followList, login.tags],
|
||||||
|
);
|
||||||
|
const feed = useTimelineFeed(subject, { method: "TIME_RANGE", now: openedAt } as TimelineFeedOptions);
|
||||||
|
|
||||||
const builder = useCallback(
|
// TODO allow reposts:
|
||||||
(rb: RequestBuilder) => {
|
const postsOnly = useCallback(
|
||||||
rb.withFilter().authors(followList).kinds([EventKind.TextNote, EventKind.Repost, EventKind.Polls]);
|
(a: NostrEvent) => (props.postsOnly ? !a.tags.some(b => b[0] === "e" || b[0] === "a") : true),
|
||||||
|
[props.postsOnly],
|
||||||
|
);
|
||||||
|
|
||||||
|
const filterPosts = useCallback(
|
||||||
|
(nts: Array<TaggedNostrEvent>) => {
|
||||||
|
const a = nts.filter(a => a.kind !== EventKind.LiveEvent);
|
||||||
|
return a
|
||||||
|
?.filter(postsOnly)
|
||||||
|
.filter(a => props.noteFilter?.(a) ?? true)
|
||||||
|
.filter(a => isFollowing(a.pubkey) || a.tags.filter(a => a[0] === "t").length < 5);
|
||||||
},
|
},
|
||||||
[followList],
|
[postsOnly, props.noteFilter, isFollowing],
|
||||||
);
|
);
|
||||||
|
|
||||||
const filterEvents = useCallback(
|
const mainFeed = useMemo(() => {
|
||||||
(a: NostrEvent) =>
|
return filterPosts(feed.main ?? []);
|
||||||
(props.noteFilter?.(a) ?? true) &&
|
}, [feed.main, filterPosts]);
|
||||||
(props.postsOnly ? !a.tags.some(b => b[0] === "e" || b[0] === "a") : true) &&
|
|
||||||
(isFollowing(a.pubkey) || a.tags.filter(a => a[0] === "t").length < 5),
|
const latestFeed = useMemo(() => {
|
||||||
[props.noteFilter, props.postsOnly, followList],
|
return filterPosts(feed.latest ?? []);
|
||||||
);
|
}, [feed.latest]);
|
||||||
|
|
||||||
|
const latestAuthors = useMemo(() => {
|
||||||
|
return dedupeByPubkey(latestFeed).map(e => e.pubkey);
|
||||||
|
}, [latestFeed]);
|
||||||
|
|
||||||
|
function onShowLatest(scrollToTop = false) {
|
||||||
|
feed.showLatest();
|
||||||
|
if (scrollToTop) {
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DisplayAsSelector activeSelection={displayAs} onSelect={(displayAs: DisplayAs) => setDisplayAs(displayAs)} />
|
<DisplayAsSelector
|
||||||
{chunks.map(c => (
|
show={props.showDisplayAsSelector}
|
||||||
<TimelineChunk
|
activeSelection={displayAs}
|
||||||
key={c.until}
|
onSelect={(displayAs: DisplayAs) => setDisplayAs(displayAs)}
|
||||||
id="follows"
|
/>
|
||||||
chunk={c}
|
<TimelineRenderer
|
||||||
builder={builder}
|
frags={[{ events: mainFeed, refTime: 0 }]}
|
||||||
noteFilter={filterEvents}
|
latest={latestAuthors}
|
||||||
noteOnClick={props.noteOnClick}
|
showLatest={t => onShowLatest(t)}
|
||||||
noteRenderer={props.noteRenderer}
|
noteOnClick={props.noteOnClick}
|
||||||
displayAs={displayAs}
|
noteRenderer={props.noteRenderer}
|
||||||
/>
|
noteContext={e => {
|
||||||
))}
|
if (typeof e.context === "string") {
|
||||||
<AutoLoadMore onClick={() => showMore()} />
|
return <Link to={`/t/${e.context}`}>{`#${e.context}`}</Link>;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
displayAs={displayAs}
|
||||||
|
loadMore={() => feed.loadMore()}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -14,7 +14,7 @@ import ProfileImage from "@/Components/User/ProfileImage";
|
|||||||
import getEventMedia from "@/Utils/getEventMedia";
|
import getEventMedia from "@/Utils/getEventMedia";
|
||||||
|
|
||||||
export interface TimelineRendererProps {
|
export interface TimelineRendererProps {
|
||||||
frags: Array<TimelineFragment> | TimelineFragment;
|
frags: Array<TimelineFragment>;
|
||||||
/**
|
/**
|
||||||
* List of pubkeys who have posted recently
|
* List of pubkeys who have posted recently
|
||||||
*/
|
*/
|
||||||
@ -29,10 +29,10 @@ export interface TimelineRendererProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// filter frags[0].events that have media
|
// filter frags[0].events that have media
|
||||||
function Grid({ frags }: { frags: Array<TimelineFragment> | TimelineFragment }) {
|
function Grid({ frags }: { frags: Array<TimelineFragment> }) {
|
||||||
const [modalEventIndex, setModalEventIndex] = useState<number | undefined>(undefined);
|
const [modalEventIndex, setModalEventIndex] = useState<number | undefined>(undefined);
|
||||||
const allEvents = useMemo(() => {
|
const allEvents = useMemo(() => {
|
||||||
return (Array.isArray(frags) ? frags : [frags]).flatMap(frag => frag.events);
|
return frags.flatMap(frag => frag.events);
|
||||||
}, [frags]);
|
}, [frags]);
|
||||||
const mediaEvents = useMemo(() => {
|
const mediaEvents = useMemo(() => {
|
||||||
return allEvents.filter(event => getEventMedia(event).length > 0);
|
return allEvents.filter(event => getEventMedia(event).length > 0);
|
||||||
@ -99,8 +99,7 @@ export function TimelineRenderer(props: TimelineRendererProps) {
|
|||||||
}, [inView, props.latest]);
|
}, [inView, props.latest]);
|
||||||
|
|
||||||
const renderNotes = () => {
|
const renderNotes = () => {
|
||||||
const frags = Array.isArray(props.frags) ? props.frags : [props.frags];
|
return props.frags.map((frag, index) => (
|
||||||
return frags.map((frag, index) => (
|
|
||||||
<ErrorBoundary key={frag.events[0]?.id + index}>
|
<ErrorBoundary key={frag.events[0]?.id + index}>
|
||||||
<TimelineFragment
|
<TimelineFragment
|
||||||
frag={frag}
|
frag={frag}
|
||||||
@ -114,7 +113,7 @@ export function TimelineRenderer(props: TimelineRendererProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef}>
|
<div ref={containerRef} className="pb-[10vh]">
|
||||||
{props.latest.length > 0 && (
|
{props.latest.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<div className="card latest-notes" onClick={() => props.showLatest(false)} ref={ref}>
|
<div className="card latest-notes" onClick={() => props.showLatest(false)} ref={ref}>
|
||||||
|
@ -27,14 +27,5 @@ export default function UsersFeed({ keyword, sortPopular = true }: { keyword: st
|
|||||||
|
|
||||||
if (!usersFeed) return <PageSpinner />;
|
if (!usersFeed) return <PageSpinner />;
|
||||||
|
|
||||||
return (
|
return <FollowListBase pubkeys={usersFeed} showAbout={true} />;
|
||||||
<FollowListBase
|
|
||||||
pubkeys={usersFeed}
|
|
||||||
profilePreviewProps={{
|
|
||||||
options: {
|
|
||||||
about: true,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -9,8 +9,8 @@
|
|||||||
<symbol id="arrowUp" viewBox="0 0 12 12" fill="none">
|
<symbol id="arrowUp" viewBox="0 0 12 12" fill="none">
|
||||||
<path d="M5.99992 10.6673V1.33398M5.99992 1.33398L1.33325 6.00065M5.99992 1.33398L10.6666 6.00065" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
|
<path d="M5.99992 10.6673V1.33398M5.99992 1.33398L1.33325 6.00065M5.99992 1.33398L10.6666 6.00065" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
</symbol>
|
</symbol>
|
||||||
<symbol id="attachment" viewBox="0 0 24 25" fill="none">
|
<symbol id="attachment" viewBox="0 0 21 22" fill="none">
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.1667 3.5C8.69391 3.5 7.5 4.69391 7.5 6.16667V17C7.5 19.4853 9.51472 21.5 12 21.5C14.4853 21.5 16.5 19.4853 16.5 17V5.75581C16.5 5.20353 16.9477 4.75581 17.5 4.75581C18.0523 4.75581 18.5 5.20353 18.5 5.75581V17C18.5 20.5899 15.5899 23.5 12 23.5C8.41015 23.5 5.5 20.5899 5.5 17V6.16667C5.5 3.58934 7.58934 1.5 10.1667 1.5C12.744 1.5 14.8333 3.58934 14.8333 6.16667V16.9457C14.8333 18.5105 13.5648 19.7791 12 19.7791C10.4352 19.7791 9.16667 18.5105 9.16667 16.9457V7.15116C9.16667 6.59888 9.61438 6.15116 10.1667 6.15116C10.719 6.15116 11.1667 6.59888 11.1667 7.15116V16.9457C11.1667 17.406 11.5398 17.7791 12 17.7791C12.4602 17.7791 12.8333 17.406 12.8333 16.9457V6.16667C12.8333 4.69391 11.6394 3.5 10.1667 3.5Z" fill="currentColor"/>
|
<path d="M19.1525 9.89945L10.1369 18.9151C8.08662 20.9653 4.7625 20.9653 2.71225 18.9151C0.661997 16.8648 0.661998 13.5407 2.71225 11.4904L11.7279 2.47483C13.0947 1.108 15.3108 1.108 16.6776 2.47483C18.0444 3.84167 18.0444 6.05775 16.6776 7.42458L8.01555 16.0866C7.33213 16.7701 6.22409 16.7701 5.54068 16.0866C4.85726 15.4032 4.85726 14.2952 5.54068 13.6118L13.1421 6.01037" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
</symbol>
|
</symbol>
|
||||||
<symbol id="badge" viewBox="0 0 16 15" fill="none">
|
<symbol id="badge" viewBox="0 0 16 15" fill="none">
|
||||||
<path d="M6.00004 7.50065L7.33337 8.83398L10.3334 5.83398M11.9342 2.83299C12.0714 3.16501 12.3349 3.42892 12.6667 3.5667L13.8302 4.04864C14.1622 4.18617 14.426 4.44998 14.5636 4.78202C14.7011 5.11407 14.7011 5.48715 14.5636 5.81919L14.082 6.98185C13.9444 7.31404 13.9442 7.6875 14.0824 8.01953L14.5632 9.18185C14.6313 9.34631 14.6664 9.52259 14.6665 9.70062C14.6665 9.87865 14.6315 10.0549 14.5633 10.2194C14.4952 10.3839 14.3953 10.5333 14.2694 10.6592C14.1435 10.7851 13.9941 10.8849 13.8296 10.953L12.6669 11.4346C12.3349 11.5718 12.071 11.8354 11.9333 12.1672L11.4513 13.3307C11.3138 13.6627 11.05 13.9265 10.718 14.0641C10.3859 14.2016 10.0129 14.2016 9.68085 14.0641L8.51823 13.5825C8.18619 13.4453 7.81326 13.4455 7.48143 13.5832L6.31797 14.0645C5.98612 14.2017 5.61338 14.2016 5.28162 14.0642C4.94986 13.9267 4.68621 13.6632 4.54858 13.3316L4.06652 12.1677C3.92924 11.8357 3.66574 11.5718 3.33394 11.434L2.17048 10.9521C1.8386 10.8146 1.57488 10.5509 1.4373 10.2191C1.29971 9.88724 1.29953 9.51434 1.43678 9.18235L1.91835 8.01968C2.05554 7.68763 2.05526 7.31469 1.91757 6.98284L1.43669 5.81851C1.36851 5.65405 1.3334 5.47777 1.33337 5.29974C1.33335 5.12171 1.3684 4.94542 1.43652 4.78094C1.50465 4.61646 1.60452 4.46702 1.73042 4.34115C1.85632 4.21529 2.00579 4.11546 2.17028 4.04739L3.33291 3.5658C3.66462 3.42863 3.92836 3.16545 4.06624 2.83402L4.54816 1.67052C4.68569 1.33848 4.94949 1.07467 5.28152 0.937137C5.61355 0.7996 5.98662 0.7996 6.31865 0.937137L7.48127 1.41873C7.81331 1.55593 8.18624 1.55565 8.51808 1.41795L9.68202 0.937884C10.014 0.800424 10.387 0.800452 10.719 0.937962C11.0509 1.07547 11.3147 1.3392 11.4522 1.67116L11.9343 2.835L11.9342 2.83299Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
|
<path d="M6.00004 7.50065L7.33337 8.83398L10.3334 5.83398M11.9342 2.83299C12.0714 3.16501 12.3349 3.42892 12.6667 3.5667L13.8302 4.04864C14.1622 4.18617 14.426 4.44998 14.5636 4.78202C14.7011 5.11407 14.7011 5.48715 14.5636 5.81919L14.082 6.98185C13.9444 7.31404 13.9442 7.6875 14.0824 8.01953L14.5632 9.18185C14.6313 9.34631 14.6664 9.52259 14.6665 9.70062C14.6665 9.87865 14.6315 10.0549 14.5633 10.2194C14.4952 10.3839 14.3953 10.5333 14.2694 10.6592C14.1435 10.7851 13.9941 10.8849 13.8296 10.953L12.6669 11.4346C12.3349 11.5718 12.071 11.8354 11.9333 12.1672L11.4513 13.3307C11.3138 13.6627 11.05 13.9265 10.718 14.0641C10.3859 14.2016 10.0129 14.2016 9.68085 14.0641L8.51823 13.5825C8.18619 13.4453 7.81326 13.4455 7.48143 13.5832L6.31797 14.0645C5.98612 14.2017 5.61338 14.2016 5.28162 14.0642C4.94986 13.9267 4.68621 13.6632 4.54858 13.3316L4.06652 12.1677C3.92924 11.8357 3.66574 11.5718 3.33394 11.434L2.17048 10.9521C1.8386 10.8146 1.57488 10.5509 1.4373 10.2191C1.29971 9.88724 1.29953 9.51434 1.43678 9.18235L1.91835 8.01968C2.05554 7.68763 2.05526 7.31469 1.91757 6.98284L1.43669 5.81851C1.36851 5.65405 1.3334 5.47777 1.33337 5.29974C1.33335 5.12171 1.3684 4.94542 1.43652 4.78094C1.50465 4.61646 1.60452 4.46702 1.73042 4.34115C1.85632 4.21529 2.00579 4.11546 2.17028 4.04739L3.33291 3.5658C3.66462 3.42863 3.92836 3.16545 4.06624 2.83402L4.54816 1.67052C4.68569 1.33848 4.94949 1.07467 5.28152 0.937137C5.61355 0.7996 5.98662 0.7996 6.31865 0.937137L7.48127 1.41873C7.81331 1.55593 8.18624 1.55565 8.51808 1.41795L9.68202 0.937884C10.014 0.800424 10.387 0.800452 10.719 0.937962C11.0509 1.07547 11.3147 1.3392 11.4522 1.67116L11.9343 2.835L11.9342 2.83299Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
@ -402,15 +402,18 @@
|
|||||||
<path d="M7.39516 18.3711L7.97961 19.6856C8.15335 20.0768 8.43689 20.4093 8.79583 20.6426C9.15478 20.8759 9.57372 21.0001 10.0018 21C10.4299 21.0001 10.8489 20.8759 11.2078 20.6426C11.5668 20.4093 11.8503 20.0768 12.0241 19.6856L12.6085 18.3711C12.8165 17.9047 13.1665 17.5159 13.6085 17.26C14.0533 17.0034 14.5678 16.8941 15.0785 16.9478L16.5085 17.1C16.9342 17.145 17.3638 17.0656 17.7452 16.8713C18.1266 16.6771 18.4435 16.3763 18.6574 16.0056C18.8716 15.635 18.9736 15.2103 18.9511 14.7829C18.9286 14.3555 18.7826 13.9438 18.5307 13.5978L17.6841 12.4344C17.3826 12.0171 17.2215 11.5148 17.2241 11C17.224 10.4866 17.3866 9.98635 17.6885 9.57111L18.5352 8.40778C18.787 8.06175 18.9331 7.65007 18.9556 7.22267C18.978 6.79528 18.876 6.37054 18.6618 6C18.4479 5.62923 18.1311 5.32849 17.7496 5.13423C17.3682 4.93997 16.9386 4.86053 16.5129 4.90556L15.0829 5.05778C14.5723 5.11141 14.0577 5.00212 13.6129 4.74556C13.1701 4.48825 12.82 4.09736 12.6129 3.62889L12.0241 2.31444C11.8503 1.92317 11.5668 1.59072 11.2078 1.3574C10.8489 1.12408 10.4299 0.99993 10.0018 1C9.57372 0.99993 9.15478 1.12408 8.79583 1.3574C8.43689 1.59072 8.15335 1.92317 7.97961 2.31444L7.39516 3.62889C7.18809 4.09736 6.83804 4.48825 6.39516 4.74556C5.95038 5.00212 5.43583 5.11141 4.92516 5.05778L3.49072 4.90556C3.06505 4.86053 2.63546 4.93997 2.25403 5.13423C1.87261 5.32849 1.55574 5.62923 1.34183 6C1.12765 6.37054 1.02561 6.79528 1.0481 7.22267C1.07058 7.65007 1.21662 8.06175 1.4685 8.40778L2.31516 9.57111C2.61711 9.98635 2.7797 10.4866 2.77961 11C2.7797 11.5134 2.61711 12.0137 2.31516 12.4289L1.4685 13.5922C1.21662 13.9382 1.07058 14.3499 1.0481 14.7773C1.02561 15.2047 1.12765 15.6295 1.34183 16C1.55595 16.3706 1.87286 16.6712 2.25423 16.8654C2.6356 17.0596 3.06508 17.1392 3.49072 17.0944L4.92072 16.9422C5.43139 16.8886 5.94594 16.9979 6.39072 17.2544C6.83525 17.511 7.18693 17.902 7.39516 18.3711Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M7.39516 18.3711L7.97961 19.6856C8.15335 20.0768 8.43689 20.4093 8.79583 20.6426C9.15478 20.8759 9.57372 21.0001 10.0018 21C10.4299 21.0001 10.8489 20.8759 11.2078 20.6426C11.5668 20.4093 11.8503 20.0768 12.0241 19.6856L12.6085 18.3711C12.8165 17.9047 13.1665 17.5159 13.6085 17.26C14.0533 17.0034 14.5678 16.8941 15.0785 16.9478L16.5085 17.1C16.9342 17.145 17.3638 17.0656 17.7452 16.8713C18.1266 16.6771 18.4435 16.3763 18.6574 16.0056C18.8716 15.635 18.9736 15.2103 18.9511 14.7829C18.9286 14.3555 18.7826 13.9438 18.5307 13.5978L17.6841 12.4344C17.3826 12.0171 17.2215 11.5148 17.2241 11C17.224 10.4866 17.3866 9.98635 17.6885 9.57111L18.5352 8.40778C18.787 8.06175 18.9331 7.65007 18.9556 7.22267C18.978 6.79528 18.876 6.37054 18.6618 6C18.4479 5.62923 18.1311 5.32849 17.7496 5.13423C17.3682 4.93997 16.9386 4.86053 16.5129 4.90556L15.0829 5.05778C14.5723 5.11141 14.0577 5.00212 13.6129 4.74556C13.1701 4.48825 12.82 4.09736 12.6129 3.62889L12.0241 2.31444C11.8503 1.92317 11.5668 1.59072 11.2078 1.3574C10.8489 1.12408 10.4299 0.99993 10.0018 1C9.57372 0.99993 9.15478 1.12408 8.79583 1.3574C8.43689 1.59072 8.15335 1.92317 7.97961 2.31444L7.39516 3.62889C7.18809 4.09736 6.83804 4.48825 6.39516 4.74556C5.95038 5.00212 5.43583 5.11141 4.92516 5.05778L3.49072 4.90556C3.06505 4.86053 2.63546 4.93997 2.25403 5.13423C1.87261 5.32849 1.55574 5.62923 1.34183 6C1.12765 6.37054 1.02561 6.79528 1.0481 7.22267C1.07058 7.65007 1.21662 8.06175 1.4685 8.40778L2.31516 9.57111C2.61711 9.98635 2.7797 10.4866 2.77961 11C2.7797 11.5134 2.61711 12.0137 2.31516 12.4289L1.4685 13.5922C1.21662 13.9382 1.07058 14.3499 1.0481 14.7773C1.02561 15.2047 1.12765 15.6295 1.34183 16C1.55595 16.3706 1.87286 16.6712 2.25423 16.8654C2.6356 17.0596 3.06508 17.1392 3.49072 17.0944L4.92072 16.9422C5.43139 16.8886 5.94594 16.9979 6.39072 17.2544C6.83525 17.511 7.18693 17.902 7.39516 18.3711Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path d="M10 14C11.6569 14 13 12.6569 13 11C13 9.34315 11.6569 8 10 8C8.34319 8 7.00004 9.34315 7.00004 11C7.00004 12.6569 8.34319 14 10 14Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M10 14C11.6569 14 13 12.6569 13 11C13 9.34315 11.6569 8 10 8C8.34319 8 7.00004 9.34315 7.00004 11C7.00004 12.6569 8.34319 14 10 14Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
|
||||||
<symbol id="home-solid" viewBox="0 0 18 18" fill="none">
|
<symbol id="home-solid" viewBox="0 0 18 18" fill="none">
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.43553 0.113589C9.15028 0.0363557 8.84962 0.0363557 8.56438 0.113589C8.23324 0.203247 7.95445 0.422125 7.73194 0.596817L7.66975 0.645498L1.95307 5.09173C1.63921 5.3353 1.3627 5.54988 1.15665 5.82847C0.975827 6.07295 0.841122 6.34837 0.759154 6.64121C0.665754 6.97489 0.666132 7.3249 0.666561 7.72218L0.666619 13.8654C0.666604 14.3047 0.666592 14.6837 0.692095 14.9958C0.719012 15.3253 0.778442 15.6529 0.939104 15.9683C1.17879 16.4387 1.56124 16.8211 2.03164 17.0608C2.34696 17.2215 2.67464 17.2809 3.0041 17.3078C3.31624 17.3333 3.6952 17.3333 4.13448 17.3333H13.8654C14.3047 17.3333 14.6837 17.3333 14.9958 17.3078C15.3253 17.2809 15.6529 17.2215 15.9683 17.0608C16.4387 16.8211 16.8211 16.4387 17.0608 15.9683C17.2215 15.6529 17.2809 15.3253 17.3078 14.9958C17.3333 14.6837 17.3333 14.3047 17.3333 13.8654L17.3333 7.72219C17.3338 7.3249 17.3342 6.97489 17.2408 6.64121C17.1588 6.34837 17.0241 6.07295 16.8433 5.82847C16.6372 5.54988 16.3607 5.33529 16.0468 5.09172L10.3302 0.645498L10.268 0.59682C10.0455 0.422128 9.76667 0.203248 9.43553 0.113589ZM6.57867 10.4589C6.46395 10.0132 6.00963 9.74487 5.56392 9.85959C5.11821 9.97431 4.84989 10.4286 4.9646 10.8743C5.4271 12.6713 7.05731 14 8.99995 14C10.9426 14 12.5728 12.6713 13.0353 10.8743C13.15 10.4286 12.8817 9.97431 12.436 9.85959C11.9903 9.74487 11.536 10.0132 11.4212 10.4589C11.1437 11.5374 10.1637 12.3333 8.99995 12.3333C7.8362 12.3333 6.85624 11.5374 6.57867 10.4589Z" fill="currentColor"/>
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.43553 0.113589C9.15028 0.0363557 8.84962 0.0363557 8.56438 0.113589C8.23324 0.203247 7.95445 0.422125 7.73194 0.596817L7.66975 0.645498L1.95307 5.09173C1.63921 5.3353 1.3627 5.54988 1.15665 5.82847C0.975827 6.07295 0.841122 6.34837 0.759154 6.64121C0.665754 6.97489 0.666132 7.3249 0.666561 7.72218L0.666619 13.8654C0.666604 14.3047 0.666592 14.6837 0.692095 14.9958C0.719012 15.3253 0.778442 15.6529 0.939104 15.9683C1.17879 16.4387 1.56124 16.8211 2.03164 17.0608C2.34696 17.2215 2.67464 17.2809 3.0041 17.3078C3.31624 17.3333 3.6952 17.3333 4.13448 17.3333H13.8654C14.3047 17.3333 14.6837 17.3333 14.9958 17.3078C15.3253 17.2809 15.6529 17.2215 15.9683 17.0608C16.4387 16.8211 16.8211 16.4387 17.0608 15.9683C17.2215 15.6529 17.2809 15.3253 17.3078 14.9958C17.3333 14.6837 17.3333 14.3047 17.3333 13.8654L17.3333 7.72219C17.3338 7.3249 17.3342 6.97489 17.2408 6.64121C17.1588 6.34837 17.0241 6.07295 16.8433 5.82847C16.6372 5.54988 16.3607 5.33529 16.0468 5.09172L10.3302 0.645498L10.268 0.59682C10.0455 0.422128 9.76667 0.203248 9.43553 0.113589ZM6.57867 10.4589C6.46395 10.0132 6.00963 9.74487 5.56392 9.85959C5.11821 9.97431 4.84989 10.4286 4.9646 10.8743C5.4271 12.6713 7.05731 14 8.99995 14C10.9426 14 12.5728 12.6713 13.0353 10.8743C13.15 10.4286 12.8817 9.97431 12.436 9.85959C11.9903 9.74487 11.536 10.0132 11.4212 10.4589C11.1437 11.5374 10.1637 12.3333 8.99995 12.3333C7.8362 12.3333 6.85624 11.5374 6.57867 10.4589Z" fill="currentColor"/>
|
||||||
</symbol>
|
</symbol>
|
||||||
<symbol id="home-outline" viewBox="0 0 18 19" fill="none">
|
<symbol id="home-outline" viewBox="0 0 18 19" fill="none">
|
||||||
<path d="M5.77168 11.6668C6.14172 13.1045 7.4468 14.1668 9 14.1668C10.5532 14.1668 11.8583 13.1045 12.2283 11.6668M8.18141 2.30345L2.52949 6.69939C2.15168 6.99324 1.96278 7.14017 1.82669 7.32417C1.70614 7.48716 1.61633 7.67077 1.56169 7.866C1.5 8.08639 1.5 8.3257 1.5 8.80433V14.8334C1.5 15.7669 1.5 16.2336 1.68166 16.5901C1.84144 16.9037 2.09641 17.1587 2.41002 17.3185C2.76654 17.5001 3.23325 17.5001 4.16667 17.5001H13.8333C14.7668 17.5001 15.2335 17.5001 15.59 17.3185C15.9036 17.1587 16.1586 16.9037 16.3183 16.5901C16.5 16.2336 16.5 15.7669 16.5 14.8334V8.80433C16.5 8.3257 16.5 8.08639 16.4383 7.866C16.3837 7.67077 16.2939 7.48716 16.1733 7.32417C16.0372 7.14017 15.8483 6.99324 15.4705 6.69939L9.81859 2.30345C9.52582 2.07574 9.37943 1.96189 9.21779 1.91812C9.07516 1.87951 8.92484 1.87951 8.78221 1.91812C8.62057 1.96189 8.47418 2.07574 8.18141 2.30345Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M5.77168 11.6668C6.14172 13.1045 7.4468 14.1668 9 14.1668C10.5532 14.1668 11.8583 13.1045 12.2283 11.6668M8.18141 2.30345L2.52949 6.69939C2.15168 6.99324 1.96278 7.14017 1.82669 7.32417C1.70614 7.48716 1.61633 7.67077 1.56169 7.866C1.5 8.08639 1.5 8.3257 1.5 8.80433V14.8334C1.5 15.7669 1.5 16.2336 1.68166 16.5901C1.84144 16.9037 2.09641 17.1587 2.41002 17.3185C2.76654 17.5001 3.23325 17.5001 4.16667 17.5001H13.8333C14.7668 17.5001 15.2335 17.5001 15.59 17.3185C15.9036 17.1587 16.1586 16.9037 16.3183 16.5901C16.5 16.2336 16.5 15.7669 16.5 14.8334V8.80433C16.5 8.3257 16.5 8.08639 16.4383 7.866C16.3837 7.67077 16.2939 7.48716 16.1733 7.32417C16.0372 7.14017 15.8483 6.99324 15.4705 6.69939L9.81859 2.30345C9.52582 2.07574 9.37943 1.96189 9.21779 1.91812C9.07516 1.87951 8.92484 1.87951 8.78221 1.91812C8.62057 1.96189 8.47418 2.07574 8.18141 2.30345Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
|
||||||
<symbol id="sign-in" viewBox="0 0 24 24" fill="none">
|
<symbol id="sign-in" viewBox="0 0 24 24" fill="none">
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7 3C6.44772 3 6 3.44772 6 4C6 4.55228 6.44772 5 7 5H18C18.5523 5 19 5.44772 19 6V18C19 18.5523 18.5523 19 18 19H7C6.44772 19 6 19.4477 6 20C6 20.5523 6.44772 21 7 21H18C19.6569 21 21 19.6569 21 18V6C21 4.34315 19.6569 3 18 3H7ZM12.7071 7.29289C12.3166 6.90237 11.6834 6.90237 11.2929 7.29289C10.9024 7.68342 10.9024 8.31658 11.2929 8.70711L13.5858 11H4C3.44772 11 3 11.4477 3 12C3 12.5523 3.44772 13 4 13H13.5858L11.2929 15.2929C10.9024 15.6834 10.9024 16.3166 11.2929 16.7071C11.6834 17.0976 12.3166 17.0976 12.7071 16.7071L16.7071 12.7071C17.0976 12.3166 17.0976 11.6834 16.7071 11.2929L12.7071 7.29289Z" fill="currentColor"/>
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M7 3C6.44772 3 6 3.44772 6 4C6 4.55228 6.44772 5 7 5H18C18.5523 5 19 5.44772 19 6V18C19 18.5523 18.5523 19 18 19H7C6.44772 19 6 19.4477 6 20C6 20.5523 6.44772 21 7 21H18C19.6569 21 21 19.6569 21 18V6C21 4.34315 19.6569 3 18 3H7ZM12.7071 7.29289C12.3166 6.90237 11.6834 6.90237 11.2929 7.29289C10.9024 7.68342 10.9024 8.31658 11.2929 8.70711L13.5858 11H4C3.44772 11 3 11.4477 3 12C3 12.5523 3.44772 13 4 13H13.5858L11.2929 15.2929C10.9024 15.6834 10.9024 16.3166 11.2929 16.7071C11.6834 17.0976 12.3166 17.0976 12.7071 16.7071L16.7071 12.7071C17.0976 12.3166 17.0976 11.6834 16.7071 11.2929L12.7071 7.29289Z" fill="currentColor"/>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
|
||||||
<symbol id="deck-solid" viewBox="0 0 20 20" fill="none">
|
<symbol id="deck-solid" viewBox="0 0 20 20" fill="none">
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.3011 1.66602H14.8653C15.3046 1.666 15.6836 1.66599 15.9957 1.69149C16.3251 1.71841 16.6528 1.77784 16.9681 1.9385C17.4386 2.17818 17.821 2.56064 18.0607 3.03104C18.2213 3.34636 18.2808 3.67404 18.3077 4.00349C18.3332 4.31564 18.3332 4.69462 18.3332 5.13392V14.8648C18.3332 15.3041 18.3332 15.6831 18.3077 15.9952C18.2808 16.3247 18.2213 16.6523 18.0607 16.9677C17.821 17.4381 17.4386 17.8205 16.9681 18.0602C16.6528 18.2209 16.3251 18.2803 15.9957 18.3072C15.6836 18.3327 15.3046 18.3327 14.8653 18.3327H14.301C13.8618 18.3327 13.4828 18.3327 13.1706 18.3072C12.8412 18.2803 12.5135 18.2209 12.1982 18.0602C11.7278 17.8205 11.3453 17.4381 11.1057 16.9677C10.945 16.6523 10.8856 16.3247 10.8586 15.9952C10.8331 15.6831 10.8332 15.3041 10.8332 14.8648V5.1339C10.8332 4.69461 10.8331 4.31564 10.8586 4.00349C10.8856 3.67404 10.945 3.34636 11.1057 3.03104C11.3453 2.56064 11.7278 2.17818 12.1982 1.9385C12.5135 1.77784 12.8412 1.71841 13.1706 1.69149C13.4828 1.66599 13.8618 1.666 14.3011 1.66602Z" fill="currentColor"/>
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.3011 1.66602H14.8653C15.3046 1.666 15.6836 1.66599 15.9957 1.69149C16.3251 1.71841 16.6528 1.77784 16.9681 1.9385C17.4386 2.17818 17.821 2.56064 18.0607 3.03104C18.2213 3.34636 18.2808 3.67404 18.3077 4.00349C18.3332 4.31564 18.3332 4.69462 18.3332 5.13392V14.8648C18.3332 15.3041 18.3332 15.6831 18.3077 15.9952C18.2808 16.3247 18.2213 16.6523 18.0607 16.9677C17.821 17.4381 17.4386 17.8205 16.9681 18.0602C16.6528 18.2209 16.3251 18.2803 15.9957 18.3072C15.6836 18.3327 15.3046 18.3327 14.8653 18.3327H14.301C13.8618 18.3327 13.4828 18.3327 13.1706 18.3072C12.8412 18.2803 12.5135 18.2209 12.1982 18.0602C11.7278 17.8205 11.3453 17.4381 11.1057 16.9677C10.945 16.6523 10.8856 16.3247 10.8586 15.9952C10.8331 15.6831 10.8332 15.3041 10.8332 14.8648V5.1339C10.8332 4.69461 10.8331 4.31564 10.8586 4.00349C10.8856 3.67404 10.945 3.34636 11.1057 3.03104C11.3453 2.56064 11.7278 2.17818 12.1982 1.9385C12.5135 1.77784 12.8412 1.71841 13.1706 1.69149C13.4828 1.66599 13.8618 1.666 14.3011 1.66602Z" fill="currentColor"/>
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.13439 1.66602H5.69863C6.13792 1.666 6.51689 1.66599 6.82903 1.69149C7.15848 1.71841 7.48617 1.77784 7.80148 1.9385C8.27189 2.17818 8.65434 2.56064 8.89402 3.03104C9.05468 3.34636 9.11411 3.67404 9.14103 4.00349C9.16653 4.31564 9.16652 4.69462 9.16651 5.13392V14.8648C9.16652 15.3041 9.16653 15.6831 9.14103 15.9952C9.11411 16.3247 9.05468 16.6523 8.89402 16.9677C8.65434 17.4381 8.27189 17.8205 7.80148 18.0602C7.48617 18.2209 7.15848 18.2803 6.82903 18.3072C6.51689 18.3327 6.13793 18.3327 5.69864 18.3327H5.13437C4.69508 18.3327 4.31612 18.3327 4.00398 18.3072C3.67453 18.2803 3.34685 18.2209 3.03153 18.0602C2.56112 17.8205 2.17867 17.4381 1.93899 16.9677C1.77833 16.6523 1.7189 16.3247 1.69198 15.9952C1.66648 15.6831 1.66649 15.3041 1.6665 14.8648V5.1339C1.66649 4.69461 1.66648 4.31564 1.69198 4.00349C1.7189 3.67404 1.77833 3.34636 1.93899 3.03104C2.17867 2.56064 2.56112 2.17818 3.03153 1.9385C3.34685 1.77784 3.67453 1.71841 4.00398 1.69149C4.31613 1.66599 4.69509 1.666 5.13439 1.66602Z" fill="currentColor"/>
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.13439 1.66602H5.69863C6.13792 1.666 6.51689 1.66599 6.82903 1.69149C7.15848 1.71841 7.48617 1.77784 7.80148 1.9385C8.27189 2.17818 8.65434 2.56064 8.89402 3.03104C9.05468 3.34636 9.11411 3.67404 9.14103 4.00349C9.16653 4.31564 9.16652 4.69462 9.16651 5.13392V14.8648C9.16652 15.3041 9.16653 15.6831 9.14103 15.9952C9.11411 16.3247 9.05468 16.6523 8.89402 16.9677C8.65434 17.4381 8.27189 17.8205 7.80148 18.0602C7.48617 18.2209 7.15848 18.2803 6.82903 18.3072C6.51689 18.3327 6.13793 18.3327 5.69864 18.3327H5.13437C4.69508 18.3327 4.31612 18.3327 4.00398 18.3072C3.67453 18.2803 3.34685 18.2209 3.03153 18.0602C2.56112 17.8205 2.17867 17.4381 1.93899 16.9677C1.77833 16.6523 1.7189 16.3247 1.69198 15.9952C1.66648 15.6831 1.66649 15.3041 1.6665 14.8648V5.1339C1.66649 4.69461 1.66648 4.31564 1.69198 4.00349C1.7189 3.67404 1.77833 3.34636 1.93899 3.03104C2.17867 2.56064 2.56112 2.17818 3.03153 1.9385C3.34685 1.77784 3.67453 1.71841 4.00398 1.69149C4.31613 1.66599 4.69509 1.666 5.13439 1.66602Z" fill="currentColor"/>
|
||||||
@ -419,24 +422,28 @@
|
|||||||
<path d="M4.66667 1.5H4.16667C3.23325 1.5 2.76654 1.5 2.41002 1.68166C2.09641 1.84144 1.84144 2.09641 1.68166 2.41002C1.5 2.76654 1.5 3.23325 1.5 4.16667V13.8333C1.5 14.7668 1.5 15.2335 1.68166 15.59C1.84144 15.9036 2.09641 16.1586 2.41002 16.3183C2.76654 16.5 3.23325 16.5 4.16667 16.5H4.66667C5.60009 16.5 6.0668 16.5 6.42332 16.3183C6.73692 16.1586 6.99189 15.9036 7.15168 15.59C7.33333 15.2335 7.33333 14.7668 7.33333 13.8333V4.16667C7.33333 3.23325 7.33333 2.76654 7.15168 2.41002C6.99189 2.09641 6.73692 1.84144 6.42332 1.68166C6.0668 1.5 5.60009 1.5 4.66667 1.5Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M4.66667 1.5H4.16667C3.23325 1.5 2.76654 1.5 2.41002 1.68166C2.09641 1.84144 1.84144 2.09641 1.68166 2.41002C1.5 2.76654 1.5 3.23325 1.5 4.16667V13.8333C1.5 14.7668 1.5 15.2335 1.68166 15.59C1.84144 15.9036 2.09641 16.1586 2.41002 16.3183C2.76654 16.5 3.23325 16.5 4.16667 16.5H4.66667C5.60009 16.5 6.0668 16.5 6.42332 16.3183C6.73692 16.1586 6.99189 15.9036 7.15168 15.59C7.33333 15.2335 7.33333 14.7668 7.33333 13.8333V4.16667C7.33333 3.23325 7.33333 2.76654 7.15168 2.41002C6.99189 2.09641 6.73692 1.84144 6.42332 1.68166C6.0668 1.5 5.60009 1.5 4.66667 1.5Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path d="M13.8333 1.5H13.3333C12.3999 1.5 11.9332 1.5 11.5767 1.68166C11.2631 1.84144 11.0081 2.09641 10.8483 2.41002C10.6667 2.76654 10.6667 3.23325 10.6667 4.16667V13.8333C10.6667 14.7668 10.6667 15.2335 10.8483 15.59C11.0081 15.9036 11.2631 16.1586 11.5767 16.3183C11.9332 16.5 12.3999 16.5 13.3333 16.5H13.8333C14.7668 16.5 15.2335 16.5 15.59 16.3183C15.9036 16.1586 16.1586 15.9036 16.3183 15.59C16.5 15.2335 16.5 14.7668 16.5 13.8333V4.16667C16.5 3.23325 16.5 2.76654 16.3183 2.41002C16.1586 2.09641 15.9036 1.84144 15.59 1.68166C15.2335 1.5 14.7668 1.5 13.8333 1.5Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M13.8333 1.5H13.3333C12.3999 1.5 11.9332 1.5 11.5767 1.68166C11.2631 1.84144 11.0081 2.09641 10.8483 2.41002C10.6667 2.76654 10.6667 3.23325 10.6667 4.16667V13.8333C10.6667 14.7668 10.6667 15.2335 10.8483 15.59C11.0081 15.9036 11.2631 16.1586 11.5767 16.3183C11.9332 16.5 12.3999 16.5 13.3333 16.5H13.8333C14.7668 16.5 15.2335 16.5 15.59 16.3183C15.9036 16.1586 16.1586 15.9036 16.3183 15.59C16.5 15.2335 16.5 14.7668 16.5 13.8333V4.16667C16.5 3.23325 16.5 2.76654 16.3183 2.41002C16.1586 2.09641 15.9036 1.84144 15.59 1.68166C15.2335 1.5 14.7668 1.5 13.8333 1.5Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
|
||||||
<symbol id="graph-outline" viewBox="0 0 20 22" fill="none">
|
<symbol id="graph-outline" viewBox="0 0 20 22" fill="none">
|
||||||
<path d="M6.59 12.51L13.42 16.49M13.41 5.51L6.59 9.49M19 4C19 5.65685 17.6569 7 16 7C14.3431 7 13 5.65685 13 4C13 2.34315 14.3431 1 16 1C17.6569 1 19 2.34315 19 4ZM7 11C7 12.6569 5.65685 14 4 14C2.34315 14 1 12.6569 1 11C1 9.34315 2.34315 8 4 8C5.65685 8 7 9.34315 7 11ZM19 18C19 19.6569 17.6569 21 16 21C14.3431 21 13 19.6569 13 18C13 16.3431 14.3431 15 16 15C17.6569 15 19 16.3431 19 18Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M6.59 12.51L13.42 16.49M13.41 5.51L6.59 9.49M19 4C19 5.65685 17.6569 7 16 7C14.3431 7 13 5.65685 13 4C13 2.34315 14.3431 1 16 1C17.6569 1 19 2.34315 19 4ZM7 11C7 12.6569 5.65685 14 4 14C2.34315 14 1 12.6569 1 11C1 9.34315 2.34315 8 4 8C5.65685 8 7 9.34315 7 11ZM19 18C19 19.6569 17.6569 21 16 21C14.3431 21 13 19.6569 13 18C13 16.3431 14.3431 15 16 15C17.6569 15 19 16.3431 19 18Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
</symbol>
|
</symbol>
|
||||||
<symbol id="graph-solid" viewBox="0 0 24 24" fill="none">
|
<symbol id="graph-solid" viewBox="0 0 24 24" fill="none">
|
||||||
<path d="M14 5C14 2.79086 15.7909 1 18 1C20.2091 1 22 2.79086 22 5C22 7.20914 20.2091 9 18 9C16.8885 9 15.883 8.54668 15.1581 7.81485L9.85034 10.9123C9.94784 11.2581 10 11.623 10 12C10 12.3768 9.9479 12.7415 9.8505 13.0871L15.1613 16.1819C15.886 15.452 16.8902 15 18 15C20.2091 15 22 16.7909 22 19C22 21.2091 20.2091 23 18 23C15.7909 23 14 21.2091 14 19C14 18.6214 14.0526 18.255 14.1509 17.9079L8.84235 14.8144C8.11742 15.5465 7.11167 16 6 16C3.79086 16 2 14.2091 2 12C2 9.79086 3.79086 8 6 8C7.11146 8 8.11703 8.45332 8.84193 9.18514L14.1497 6.08767C14.0522 5.74185 14 5.37701 14 5Z" fill="currentColor"/>
|
<path d="M14 5C14 2.79086 15.7909 1 18 1C20.2091 1 22 2.79086 22 5C22 7.20914 20.2091 9 18 9C16.8885 9 15.883 8.54668 15.1581 7.81485L9.85034 10.9123C9.94784 11.2581 10 11.623 10 12C10 12.3768 9.9479 12.7415 9.8505 13.0871L15.1613 16.1819C15.886 15.452 16.8902 15 18 15C20.2091 15 22 16.7909 22 19C22 21.2091 20.2091 23 18 23C15.7909 23 14 21.2091 14 19C14 18.6214 14.0526 18.255 14.1509 17.9079L8.84235 14.8144C8.11742 15.5465 7.11167 16 6 16C3.79086 16 2 14.2091 2 12C2 9.79086 3.79086 8 6 8C7.11146 8 8.11703 8.45332 8.84193 9.18514L14.1497 6.08767C14.0522 5.74185 14 5.37701 14 5Z" fill="currentColor"/>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
|
||||||
<symbol id="media" viewBox="0 0 18 18" fill="none">
|
<symbol id="media" viewBox="0 0 18 18" fill="none">
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.80659 0.666994C3.87988 0.667127 4.95525 0.667127 6.02642 0.666994C6.23674 0.666969 6.44157 0.666945 6.61503 0.681118C6.80553 0.696682 7.03031 0.733404 7.25649 0.848652C7.5701 1.00844 7.82506 1.26341 7.98485 1.57701C8.1001 1.8032 8.13682 2.02798 8.15239 2.21847C8.16656 2.39194 8.16654 2.59677 8.16651 2.8071V6.0269C8.16654 6.23722 8.16656 6.44205 8.15239 6.61552C8.13682 6.80602 8.1001 7.03079 7.98485 7.25698C7.82506 7.57058 7.5701 7.82555 7.25649 7.98534C7.03031 8.10059 6.80553 8.13731 6.61503 8.15288C6.44156 8.16705 6.23673 8.16702 6.02641 8.167H2.80661C2.59628 8.16702 2.39145 8.16705 2.21798 8.15288C2.02749 8.13731 1.80271 8.10059 1.57652 7.98534C1.26292 7.82555 1.00795 7.57058 0.848164 7.25698C0.732916 7.03079 0.696193 6.80602 0.680629 6.61552C0.666457 6.44206 0.666481 6.23723 0.666506 6.02691C0.666507 6.01806 0.666508 6.0092 0.666508 6.00033V2.83366C0.666508 2.82479 0.666507 2.81593 0.666506 2.80708C0.666481 2.59676 0.666457 2.39194 0.680629 2.21847C0.696193 2.02798 0.732916 1.8032 0.848164 1.57701C1.00795 1.26341 1.26292 1.00844 1.57652 0.848652C1.80271 0.733404 2.02749 0.696682 2.21798 0.681118C2.39145 0.666945 2.59627 0.666969 2.80659 0.666994Z" fill="currentColor"/>
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.80659 0.666994C3.87988 0.667127 4.95525 0.667127 6.02642 0.666994C6.23674 0.666969 6.44157 0.666945 6.61503 0.681118C6.80553 0.696682 7.03031 0.733404 7.25649 0.848652C7.5701 1.00844 7.82506 1.26341 7.98485 1.57701C8.1001 1.8032 8.13682 2.02798 8.15239 2.21847C8.16656 2.39194 8.16654 2.59677 8.16651 2.8071V6.0269C8.16654 6.23722 8.16656 6.44205 8.15239 6.61552C8.13682 6.80602 8.1001 7.03079 7.98485 7.25698C7.82506 7.57058 7.5701 7.82555 7.25649 7.98534C7.03031 8.10059 6.80553 8.13731 6.61503 8.15288C6.44156 8.16705 6.23673 8.16702 6.02641 8.167H2.80661C2.59628 8.16702 2.39145 8.16705 2.21798 8.15288C2.02749 8.13731 1.80271 8.10059 1.57652 7.98534C1.26292 7.82555 1.00795 7.57058 0.848164 7.25698C0.732916 7.03079 0.696193 6.80602 0.680629 6.61552C0.666457 6.44206 0.666481 6.23723 0.666506 6.02691C0.666507 6.01806 0.666508 6.0092 0.666508 6.00033V2.83366C0.666508 2.82479 0.666507 2.81593 0.666506 2.80708C0.666481 2.59676 0.666457 2.39194 0.680629 2.21847C0.696193 2.02798 0.732916 1.8032 0.848164 1.57701C1.00795 1.26341 1.26292 1.00844 1.57652 0.848652C1.80271 0.733404 2.02749 0.696682 2.21798 0.681118C2.39145 0.666945 2.59627 0.666969 2.80659 0.666994Z" fill="currentColor"/>
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.80659 9.83366C3.87988 9.83379 4.95525 9.83379 6.02642 9.83366C6.23674 9.83364 6.44157 9.83361 6.61503 9.84778C6.80553 9.86335 7.03031 9.90007 7.25649 10.0153C7.5701 10.1751 7.82506 10.4301 7.98485 10.7437C8.1001 10.9699 8.13682 11.1946 8.15239 11.3851C8.16656 11.5586 8.16654 11.7634 8.16651 11.9738V15.1936C8.16654 15.4039 8.16656 15.6087 8.15239 15.7822C8.13682 15.9727 8.1001 16.1975 7.98485 16.4236C7.82506 16.7373 7.5701 16.9922 7.25649 17.152C7.03031 17.2673 6.80553 17.304 6.61503 17.3195C6.44156 17.3337 6.23673 17.3337 6.02641 17.3337H2.80661C2.59628 17.3337 2.39145 17.3337 2.21798 17.3195C2.02749 17.304 1.80271 17.2673 1.57652 17.152C1.26292 16.9922 1.00795 16.7373 0.848164 16.4236C0.732916 16.1975 0.696193 15.9727 0.680629 15.7822C0.666457 15.6087 0.666481 15.4039 0.666506 15.1936C0.666507 15.1847 0.666508 15.1759 0.666508 15.167V12.0003C0.666508 11.9915 0.666507 11.9826 0.666506 11.9737C0.666481 11.7634 0.666457 11.5586 0.680629 11.3851C0.696193 11.1946 0.732916 10.9699 0.848164 10.7437C1.00795 10.4301 1.26292 10.1751 1.57652 10.0153C1.80271 9.90007 2.02749 9.86335 2.21798 9.84778C2.39145 9.83361 2.59627 9.83364 2.80659 9.83366Z" fill="currentColor"/>
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.80659 9.83366C3.87988 9.83379 4.95525 9.83379 6.02642 9.83366C6.23674 9.83364 6.44157 9.83361 6.61503 9.84778C6.80553 9.86335 7.03031 9.90007 7.25649 10.0153C7.5701 10.1751 7.82506 10.4301 7.98485 10.7437C8.1001 10.9699 8.13682 11.1946 8.15239 11.3851C8.16656 11.5586 8.16654 11.7634 8.16651 11.9738V15.1936C8.16654 15.4039 8.16656 15.6087 8.15239 15.7822C8.13682 15.9727 8.1001 16.1975 7.98485 16.4236C7.82506 16.7373 7.5701 16.9922 7.25649 17.152C7.03031 17.2673 6.80553 17.304 6.61503 17.3195C6.44156 17.3337 6.23673 17.3337 6.02641 17.3337H2.80661C2.59628 17.3337 2.39145 17.3337 2.21798 17.3195C2.02749 17.304 1.80271 17.2673 1.57652 17.152C1.26292 16.9922 1.00795 16.7373 0.848164 16.4236C0.732916 16.1975 0.696193 15.9727 0.680629 15.7822C0.666457 15.6087 0.666481 15.4039 0.666506 15.1936C0.666507 15.1847 0.666508 15.1759 0.666508 15.167V12.0003C0.666508 11.9915 0.666507 11.9826 0.666506 11.9737C0.666481 11.7634 0.666457 11.5586 0.680629 11.3851C0.696193 11.1946 0.732916 10.9699 0.848164 10.7437C1.00795 10.4301 1.26292 10.1751 1.57652 10.0153C1.80271 9.90007 2.02749 9.86335 2.21798 9.84778C2.39145 9.83361 2.59627 9.83364 2.80659 9.83366Z" fill="currentColor"/>
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.9733 0.666994C13.0465 0.667127 14.1219 0.667127 15.1931 0.666994C15.4034 0.666969 15.6082 0.666945 15.7817 0.681118C15.9722 0.696682 16.197 0.733404 16.4232 0.848652C16.7368 1.00844 16.9917 1.26341 17.1515 1.57701C17.2668 1.8032 17.3035 2.02798 17.3191 2.21847C17.3332 2.39194 17.3332 2.59677 17.3332 2.8071V6.0269C17.3332 6.23722 17.3332 6.44205 17.3191 6.61552C17.3035 6.80602 17.2668 7.03079 17.1515 7.25698C16.9917 7.57058 16.7368 7.82555 16.4232 7.98534C16.197 8.10059 15.9722 8.13731 15.7817 8.15288C15.6082 8.16705 15.4034 8.16702 15.1931 8.167H11.9733C11.763 8.16702 11.5581 8.16705 11.3847 8.15288C11.1942 8.13731 10.9694 8.10059 10.7432 7.98534C10.4296 7.82555 10.1746 7.57058 10.0148 7.25698C9.89958 7.03079 9.86286 6.80602 9.8473 6.61552C9.83312 6.44206 9.83315 6.23723 9.83317 6.02691C9.83317 6.01806 9.83317 6.0092 9.83317 6.00033V2.83366C9.83317 2.82479 9.83317 2.81593 9.83317 2.80708C9.83315 2.59676 9.83312 2.39194 9.8473 2.21847C9.86286 2.02798 9.89958 1.8032 10.0148 1.57701C10.1746 1.26341 10.4296 1.00844 10.7432 0.848652C10.9694 0.733404 11.1942 0.696682 11.3847 0.681118C11.5581 0.666945 11.7629 0.666969 11.9733 0.666994Z" fill="currentColor"/>
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.9733 0.666994C13.0465 0.667127 14.1219 0.667127 15.1931 0.666994C15.4034 0.666969 15.6082 0.666945 15.7817 0.681118C15.9722 0.696682 16.197 0.733404 16.4232 0.848652C16.7368 1.00844 16.9917 1.26341 17.1515 1.57701C17.2668 1.8032 17.3035 2.02798 17.3191 2.21847C17.3332 2.39194 17.3332 2.59677 17.3332 2.8071V6.0269C17.3332 6.23722 17.3332 6.44205 17.3191 6.61552C17.3035 6.80602 17.2668 7.03079 17.1515 7.25698C16.9917 7.57058 16.7368 7.82555 16.4232 7.98534C16.197 8.10059 15.9722 8.13731 15.7817 8.15288C15.6082 8.16705 15.4034 8.16702 15.1931 8.167H11.9733C11.763 8.16702 11.5581 8.16705 11.3847 8.15288C11.1942 8.13731 10.9694 8.10059 10.7432 7.98534C10.4296 7.82555 10.1746 7.57058 10.0148 7.25698C9.89958 7.03079 9.86286 6.80602 9.8473 6.61552C9.83312 6.44206 9.83315 6.23723 9.83317 6.02691C9.83317 6.01806 9.83317 6.0092 9.83317 6.00033V2.83366C9.83317 2.82479 9.83317 2.81593 9.83317 2.80708C9.83315 2.59676 9.83312 2.39194 9.8473 2.21847C9.86286 2.02798 9.89958 1.8032 10.0148 1.57701C10.1746 1.26341 10.4296 1.00844 10.7432 0.848652C10.9694 0.733404 11.1942 0.696682 11.3847 0.681118C11.5581 0.666945 11.7629 0.666969 11.9733 0.666994Z" fill="currentColor"/>
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.9733 9.83366C13.0465 9.83379 14.1219 9.83379 15.1931 9.83366C15.4034 9.83364 15.6082 9.83361 15.7817 9.84778C15.9722 9.86335 16.197 9.90007 16.4232 10.0153C16.7368 10.1751 16.9917 10.4301 17.1515 10.7437C17.2668 10.9699 17.3035 11.1946 17.3191 11.3851C17.3332 11.5586 17.3332 11.7634 17.3332 11.9738V15.1936C17.3332 15.4039 17.3332 15.6087 17.3191 15.7822C17.3035 15.9727 17.2668 16.1975 17.1515 16.4236C16.9917 16.7373 16.7368 16.9922 16.4232 17.152C16.197 17.2673 15.9722 17.304 15.7817 17.3195C15.6082 17.3337 15.4034 17.3337 15.1931 17.3337H11.9733C11.763 17.3337 11.5581 17.3337 11.3847 17.3195C11.1942 17.304 10.9694 17.2673 10.7432 17.152C10.4296 16.9922 10.1746 16.7373 10.0148 16.4236C9.89958 16.1975 9.86286 15.9727 9.8473 15.7822C9.83312 15.6087 9.83315 15.4039 9.83317 15.1936C9.83317 15.1847 9.83317 15.1759 9.83317 15.167V12.0003C9.83317 11.9915 9.83317 11.9826 9.83317 11.9737C9.83315 11.7634 9.83312 11.5586 9.8473 11.3851C9.86286 11.1946 9.89958 10.9699 10.0148 10.7437C10.1746 10.4301 10.4296 10.1751 10.7432 10.0153C10.9694 9.90007 11.1942 9.86335 11.3847 9.84778C11.5581 9.83361 11.7629 9.83364 11.9733 9.83366Z" fill="currentColor"/>
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.9733 9.83366C13.0465 9.83379 14.1219 9.83379 15.1931 9.83366C15.4034 9.83364 15.6082 9.83361 15.7817 9.84778C15.9722 9.86335 16.197 9.90007 16.4232 10.0153C16.7368 10.1751 16.9917 10.4301 17.1515 10.7437C17.2668 10.9699 17.3035 11.1946 17.3191 11.3851C17.3332 11.5586 17.3332 11.7634 17.3332 11.9738V15.1936C17.3332 15.4039 17.3332 15.6087 17.3191 15.7822C17.3035 15.9727 17.2668 16.1975 17.1515 16.4236C16.9917 16.7373 16.7368 16.9922 16.4232 17.152C16.197 17.2673 15.9722 17.304 15.7817 17.3195C15.6082 17.3337 15.4034 17.3337 15.1931 17.3337H11.9733C11.763 17.3337 11.5581 17.3337 11.3847 17.3195C11.1942 17.304 10.9694 17.2673 10.7432 17.152C10.4296 16.9922 10.1746 16.7373 10.0148 16.4236C9.89958 16.1975 9.86286 15.9727 9.8473 15.7822C9.83312 15.6087 9.83315 15.4039 9.83317 15.1936C9.83317 15.1847 9.83317 15.1759 9.83317 15.167V12.0003C9.83317 11.9915 9.83317 11.9826 9.83317 11.9737C9.83315 11.7634 9.83312 11.5586 9.8473 11.3851C9.86286 11.1946 9.89958 10.9699 10.0148 10.7437C10.1746 10.4301 10.4296 10.1751 10.7432 10.0153C10.9694 9.90007 11.1942 9.86335 11.3847 9.84778C11.5581 9.83361 11.7629 9.83364 11.9733 9.83366Z" fill="currentColor"/>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
|
||||||
<symbol id="info-solid" viewBox="0 0 22 22" fill="none">
|
<symbol id="info-solid" viewBox="0 0 22 22" fill="none">
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11 0C4.92487 0 0 4.92487 0 11C0 17.0751 4.92487 22 11 22C17.0751 22 22 17.0751 22 11C22 4.92487 17.0751 0 11 0ZM11 6C10.4477 6 10 6.44772 10 7C10 7.55228 10.4477 8 11 8H11.01C11.5623 8 12.01 7.55228 12.01 7C12.01 6.44772 11.5623 6 11.01 6H11ZM12 11C12 10.4477 11.5523 10 11 10C10.4477 10 10 10.4477 10 11V15C10 15.5523 10.4477 16 11 16C11.5523 16 12 15.5523 12 15V11Z" fill="currentColor"/>
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M11 0C4.92487 0 0 4.92487 0 11C0 17.0751 4.92487 22 11 22C17.0751 22 22 17.0751 22 11C22 4.92487 17.0751 0 11 0ZM11 6C10.4477 6 10 6.44772 10 7C10 7.55228 10.4477 8 11 8H11.01C11.5623 8 12.01 7.55228 12.01 7C12.01 6.44772 11.5623 6 11.01 6H11ZM12 11C12 10.4477 11.5523 10 11 10C10.4477 10 10 10.4477 10 11V15C10 15.5523 10.4477 16 11 16C11.5523 16 12 15.5523 12 15V11Z" fill="currentColor"/>
|
||||||
</symbol>
|
</symbol>
|
||||||
<symbol id="info-outline" viewBox="0 0 22 22" fill="none">
|
<symbol id="info-outline" viewBox="0 0 22 22" fill="none">
|
||||||
<path d="M11 15V11M11 7H11.01M21 11C21 16.5228 16.5228 21 11 21C5.47715 21 1 16.5228 1 11C1 5.47715 5.47715 1 11 1C16.5228 1 21 5.47715 21 11Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M11 15V11M11 7H11.01M21 11C21 16.5228 16.5228 21 11 21C5.47715 21 1 16.5228 1 11C1 5.47715 5.47715 1 11 1C16.5228 1 21 5.47715 21 11Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
|
||||||
<symbol id="play-square-outline" viewBox="0 0 20 20" fill="none">
|
<symbol id="play-square-outline" viewBox="0 0 20 20" fill="none">
|
||||||
<path d="M7.5 6.96533C7.5 6.48805 7.5 6.24941 7.59974 6.11618C7.68666 6.00007 7.81971 5.92744 7.96438 5.9171C8.13038 5.90525 8.33112 6.03429 8.73261 6.29239L13.4532 9.32706C13.8016 9.55102 13.9758 9.663 14.0359 9.80539C14.0885 9.9298 14.0885 10.0702 14.0359 10.1946C13.9758 10.337 13.8016 10.449 13.4532 10.6729L8.73261 13.7076C8.33112 13.9657 8.13038 14.0948 7.96438 14.0829C7.81971 14.0726 7.68666 13.9999 7.59974 13.8838C7.5 13.7506 7.5 13.512 7.5 13.0347V6.96533Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M7.5 6.96533C7.5 6.48805 7.5 6.24941 7.59974 6.11618C7.68666 6.00007 7.81971 5.92744 7.96438 5.9171C8.13038 5.90525 8.33112 6.03429 8.73261 6.29239L13.4532 9.32706C13.8016 9.55102 13.9758 9.663 14.0359 9.80539C14.0885 9.9298 14.0885 10.0702 14.0359 10.1946C13.9758 10.337 13.8016 10.449 13.4532 10.6729L8.73261 13.7076C8.33112 13.9657 8.13038 14.0948 7.96438 14.0829C7.81971 14.0726 7.68666 13.9999 7.59974 13.8838C7.5 13.7506 7.5 13.512 7.5 13.0347V6.96533Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path d="M1 5.8C1 4.11984 1 3.27976 1.32698 2.63803C1.6146 2.07354 2.07354 1.6146 2.63803 1.32698C3.27976 1 4.11984 1 5.8 1H14.2C15.8802 1 16.7202 1 17.362 1.32698C17.9265 1.6146 18.3854 2.07354 18.673 2.63803C19 3.27976 19 4.11984 19 5.8V14.2C19 15.8802 19 16.7202 18.673 17.362C18.3854 17.9265 17.9265 18.3854 17.362 18.673C16.7202 19 15.8802 19 14.2 19H5.8C4.11984 19 3.27976 19 2.63803 18.673C2.07354 18.3854 1.6146 17.9265 1.32698 17.362C1 16.7202 1 15.8802 1 14.2V5.8Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M1 5.8C1 4.11984 1 3.27976 1.32698 2.63803C1.6146 2.07354 2.07354 1.6146 2.63803 1.32698C3.27976 1 4.11984 1 5.8 1H14.2C15.8802 1 16.7202 1 17.362 1.32698C17.9265 1.6146 18.3854 2.07354 18.673 2.63803C19 3.27976 19 4.11984 19 5.8V14.2C19 15.8802 19 16.7202 18.673 17.362C18.3854 17.9265 17.9265 18.3854 17.362 18.673C16.7202 19 15.8802 19 14.2 19H5.8C4.11984 19 3.27976 19 2.63803 18.673C2.07354 18.3854 1.6146 17.9265 1.32698 17.362C1 16.7202 1 15.8802 1 14.2V5.8Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
@ -453,16 +460,6 @@
|
|||||||
<symbol id="sats" viewBox="0 0 24 25" fill="none">
|
<symbol id="sats" viewBox="0 0 24 25" fill="none">
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M21 12.5C21 13.6819 20.7672 14.8522 20.3149 15.9442C19.8626 17.0361 19.1997 18.0282 18.364 18.864C17.5282 19.6997 16.5361 20.3626 15.4442 20.8149C14.3522 21.2672 13.1819 21.5 12 21.5C10.8181 21.5 9.64778 21.2672 8.55585 20.8149C7.46392 20.3626 6.47177 19.6997 5.63604 18.864C4.80031 18.0282 4.13738 17.0361 3.68508 15.9442C3.23279 14.8522 3 13.6819 3 12.5C3 10.1131 3.94821 7.82387 5.63604 6.13604C7.32387 4.44821 9.61305 3.5 12 3.5C14.3869 3.5 16.6761 4.44821 18.364 6.13604C20.0518 7.82387 21 10.1131 21 12.5ZM8.693 9.242L16.33 11.305L16.667 9.843L9.029 7.78L8.693 9.242ZM14.219 6.192L13.813 7.966L12.365 7.574L12.772 5.8L14.219 6.192ZM11.227 19.2L11.635 17.426L10.187 17.035L9.779 18.809L11.227 19.2ZM15.648 14.266L8.011 12.2L8.347 10.738L15.984 12.804L15.648 14.266ZM7.332 15.156L14.97 17.22L15.306 15.758L7.668 13.694L7.332 15.156Z" fill="currentColor"/>
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M21 12.5C21 13.6819 20.7672 14.8522 20.3149 15.9442C19.8626 17.0361 19.1997 18.0282 18.364 18.864C17.5282 19.6997 16.5361 20.3626 15.4442 20.8149C14.3522 21.2672 13.1819 21.5 12 21.5C10.8181 21.5 9.64778 21.2672 8.55585 20.8149C7.46392 20.3626 6.47177 19.6997 5.63604 18.864C4.80031 18.0282 4.13738 17.0361 3.68508 15.9442C3.23279 14.8522 3 13.6819 3 12.5C3 10.1131 3.94821 7.82387 5.63604 6.13604C7.32387 4.44821 9.61305 3.5 12 3.5C14.3869 3.5 16.6761 4.44821 18.364 6.13604C20.0518 7.82387 21 10.1131 21 12.5ZM8.693 9.242L16.33 11.305L16.667 9.843L9.029 7.78L8.693 9.242ZM14.219 6.192L13.813 7.966L12.365 7.574L12.772 5.8L14.219 6.192ZM11.227 19.2L11.635 17.426L10.187 17.035L9.779 18.809L11.227 19.2ZM15.648 14.266L8.011 12.2L8.347 10.738L15.984 12.804L15.648 14.266ZM7.332 15.156L14.97 17.22L15.306 15.758L7.668 13.694L7.332 15.156Z" fill="currentColor"/>
|
||||||
</symbol>
|
</symbol>
|
||||||
<symbol id="face-smile" viewBox="0 0 24 25" fill="none">
|
|
||||||
<path d="M8 14.5C8 14.5 9.5 16.5 12 16.5C14.5 16.5 16 14.5 16 14.5M15 9.5H15.01M9 9.5H9.01M22 12.5C22 18.0228 17.5228 22.5 12 22.5C6.47715 22.5 2 18.0228 2 12.5C2 6.97715 6.47715 2.5 12 2.5C17.5228 2.5 22 6.97715 22 12.5ZM15.5 9.5C15.5 9.77614 15.2761 10 15 10C14.7239 10 14.5 9.77614 14.5 9.5C14.5 9.22386 14.7239 9 15 9C15.2761 9 15.5 9.22386 15.5 9.5ZM9.5 9.5C9.5 9.77614 9.27614 10 9 10C8.72386 10 8.5 9.77614 8.5 9.5C8.5 9.22386 8.72386 9 9 9C9.27614 9 9.5 9.22386 9.5 9.5Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="bar-chart" viewBox="0 0 24 25" fill="none" >
|
|
||||||
<path d="M9 7.5H4.6C4.03995 7.5 3.75992 7.5 3.54601 7.60899C3.35785 7.70487 3.20487 7.85785 3.10899 8.04601C3 8.25992 3 8.53995 3 9.1V19.9C3 20.4601 3 20.7401 3.10899 20.954C3.20487 21.1422 3.35785 21.2951 3.54601 21.391C3.75992 21.5 4.03995 21.5 4.6 21.5H9M9 21.5H15M9 21.5L9 5.1C9 4.53995 9 4.25992 9.10899 4.04601C9.20487 3.85785 9.35785 3.70487 9.54601 3.60899C9.75992 3.5 10.0399 3.5 10.6 3.5L13.4 3.5C13.9601 3.5 14.2401 3.5 14.454 3.60899C14.6422 3.70487 14.7951 3.85785 14.891 4.04601C15 4.25992 15 4.53995 15 5.1V21.5M15 11.5H19.4C19.9601 11.5 20.2401 11.5 20.454 11.609C20.6422 11.7049 20.7951 11.8578 20.891 12.046C21 12.2599 21 12.5399 21 13.1V19.9C21 20.4601 21 20.7401 20.891 20.954C20.7951 21.1422 20.6422 21.2951 20.454 21.391C20.2401 21.5 19.9601 21.5 19.4 21.5H15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="expand" viewBox="0 0 24 24" fill="none">
|
|
||||||
<path d="M14 3C14 2.44772 14.4477 2 15 2H21C21.5523 2 22 2.44772 22 3V9C22 9.55229 21.5523 10 21 10C20.4477 10 20 9.55229 20 9V5.41421L14.7071 10.7071C14.3166 11.0976 13.6834 11.0976 13.2929 10.7071C12.9024 10.3166 12.9024 9.68342 13.2929 9.29289L18.5858 4H15C14.4477 4 14 3.55228 14 3Z" fill="currentColor" />
|
|
||||||
<path d="M5.41421 20L10.7071 14.7071C11.0976 14.3166 11.0976 13.6834 10.7071 13.2929C10.3166 12.9024 9.68342 12.9024 9.29289 13.2929L4 18.5858L4 15C4 14.4477 3.55229 14 3 14C2.44772 14 2 14.4477 2 15V21C2 21.5523 2.44772 22 3 22H9C9.55228 22 10 21.5523 10 21C10 20.4477 9.55228 20 9 20H5.41421Z" fill="currentColor" />
|
|
||||||
</symbol>
|
|
||||||
|
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 129 KiB |
@ -3,15 +3,6 @@ export const DefaultLocale = "en-US";
|
|||||||
export const getLocale = () => {
|
export const getLocale = () => {
|
||||||
return (navigator.languages && navigator.languages[0]) ?? navigator.language ?? DefaultLocale;
|
return (navigator.languages && navigator.languages[0]) ?? navigator.language ?? DefaultLocale;
|
||||||
};
|
};
|
||||||
export const getCurrency = () => {
|
|
||||||
const locale = navigator.language || navigator.languages[0];
|
|
||||||
const formatter = new Intl.NumberFormat(locale, {
|
|
||||||
style: "currency",
|
|
||||||
currency: "USD",
|
|
||||||
currencyDisplay: "code",
|
|
||||||
});
|
|
||||||
return formatter.formatToParts(1.2345).find(a => a.type === "currency")?.value ?? "USD";
|
|
||||||
};
|
|
||||||
export const AllLanguageCodes = [
|
export const AllLanguageCodes = [
|
||||||
"en",
|
"en",
|
||||||
"ja",
|
"ja",
|
||||||
|
@ -42,7 +42,7 @@ const InviteModal = () => {
|
|||||||
</p>
|
</p>
|
||||||
<Link to="/login/sign-up">
|
<Link to="/login/sign-up">
|
||||||
<button className="primary">
|
<button className="primary">
|
||||||
<FormattedMessage defaultMessage="Sign Up" />
|
<FormattedMessage defaultMessage="Sign Up" id="39AHJm" />
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
@ -11,10 +11,10 @@ export default function AccountName({ name = "", link = true }: AccountNameProps
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<FormattedMessage defaultMessage="Username" />: <b>{name}</b>
|
<FormattedMessage defaultMessage="Username" id="JCIgkj" />: <b>{name}</b>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<FormattedMessage defaultMessage="Short link" />:{" "}
|
<FormattedMessage defaultMessage="Short link" id="rx1i0i" />:{" "}
|
||||||
{link ? (
|
{link ? (
|
||||||
<a
|
<a
|
||||||
href={`https://iris.to/${name}`}
|
href={`https://iris.to/${name}`}
|
||||||
@ -29,7 +29,7 @@ export default function AccountName({ name = "", link = true }: AccountNameProps
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<FormattedMessage defaultMessage="Nostr address (nip05)" />: <b>{name}@iris.to</b>
|
<FormattedMessage defaultMessage="Nostr address (nip05)" id="BjNwZW" />: <b>{name}@iris.to</b>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -67,12 +67,12 @@ export default function ActiveAccount({ name = "", setAsPrimary = () => {} }: Ac
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="negative">
|
<div className="negative">
|
||||||
<FormattedMessage defaultMessage="You have an active iris.to account" />:
|
<FormattedMessage defaultMessage="You have an active iris.to account" id="UrKTqQ" />:
|
||||||
<AccountName name={name} />
|
<AccountName name={name} />
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
<button type="button" onClick={onClick}>
|
<button type="button" onClick={onClick}>
|
||||||
<FormattedMessage defaultMessage="Set as primary Nostr address (nip05)" />
|
<FormattedMessage defaultMessage="Set as primary Nostr address (nip05)" id="MiMipu" />
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -72,7 +72,7 @@ class IrisAccount extends Component<Props> {
|
|||||||
view = (
|
view = (
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
<FormattedMessage defaultMessage="Register an Iris username" /> (iris.to/username)
|
<FormattedMessage defaultMessage="Register an Iris username" id="kEZUR8" /> (iris.to/username)
|
||||||
</p>
|
</p>
|
||||||
<form onSubmit={e => this.showChallenge(e)}>
|
<form onSubmit={e => this.showChallenge(e)}>
|
||||||
<div className="flex g8">
|
<div className="flex g8">
|
||||||
@ -84,14 +84,14 @@ class IrisAccount extends Component<Props> {
|
|||||||
onInput={e => this.onNewUserNameChange(e)}
|
onInput={e => this.onNewUserNameChange(e)}
|
||||||
/>
|
/>
|
||||||
<button type="submit">
|
<button type="submit">
|
||||||
<FormattedMessage defaultMessage="Register" />
|
<FormattedMessage defaultMessage="Register" id="deEeEI" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{this.state.newUserNameValid ? (
|
{this.state.newUserNameValid ? (
|
||||||
<>
|
<>
|
||||||
<span className="success">
|
<span className="success">
|
||||||
<FormattedMessage defaultMessage="Username is available" />
|
<FormattedMessage defaultMessage="Username is available" id="EcfIwB" />
|
||||||
</span>
|
</span>
|
||||||
<AccountName name={this.state.newUserName} link={false} />
|
<AccountName name={this.state.newUserName} link={false} />
|
||||||
</>
|
</>
|
||||||
@ -107,7 +107,7 @@ class IrisAccount extends Component<Props> {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h3>
|
<h3>
|
||||||
<FormattedMessage defaultMessage="Iris.to account" />
|
<FormattedMessage defaultMessage="Iris.to account" id="Mzizei" />
|
||||||
</h3>
|
</h3>
|
||||||
{view}
|
{view}
|
||||||
<p>
|
<p>
|
||||||
|
@ -25,12 +25,12 @@ export default function ReservedAccount({
|
|||||||
<AccountName name={name} link={false} />
|
<AccountName name={name} link={false} />
|
||||||
<p>
|
<p>
|
||||||
<button className="btn btn-sm btn-primary" onClick={() => enableReserved()}>
|
<button className="btn btn-sm btn-primary" onClick={() => enableReserved()}>
|
||||||
<FormattedMessage defaultMessage="Yes please" />
|
<FormattedMessage defaultMessage="Yes please" id="VcwrfF" />
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<button className="btn btn-sm btn-neutral" onClick={() => declineReserved()}>
|
<button className="btn btn-sm btn-neutral" onClick={() => declineReserved()}>
|
||||||
<FormattedMessage defaultMessage="No thanks" />
|
<FormattedMessage defaultMessage="No thanks" id="c+JYNI" />
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,36 +1,18 @@
|
|||||||
import { NostrLink, TaggedNostrEvent } from "@snort/system";
|
import { NostrEvent, NostrLink } from "@snort/system";
|
||||||
import { lazy, Suspense, useState } from "react";
|
import { useState } from "react";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import Icon from "@/Components/Icons/Icon";
|
import Icon from "@/Components/Icons/Icon";
|
||||||
import { findTag } from "@/Utils";
|
import { findTag } from "@/Utils";
|
||||||
import { extractStreamInfo } from "@/Utils/stream";
|
|
||||||
|
|
||||||
import NoteAppHandler from "../Event/Note/NoteAppHandler";
|
|
||||||
import ProfileImage from "../User/ProfileImage";
|
import ProfileImage from "../User/ProfileImage";
|
||||||
const LiveKitRoom = lazy(() => import("./livekit"));
|
|
||||||
|
|
||||||
export function LiveEvent({ ev }: { ev: TaggedNostrEvent }) {
|
export function LiveEvent({ ev }: { ev: NostrEvent }) {
|
||||||
const service = ev.tags.find(a => a[0] === "streaming")?.at(1);
|
const title = findTag(ev, "title");
|
||||||
function inner() {
|
const status = findTag(ev, "status");
|
||||||
if (service?.endsWith(".m3u8")) {
|
const starts = Number(findTag(ev, "starts"));
|
||||||
return <LiveStreamEvent ev={ev} />;
|
const host = ev.tags.find(a => a[0] === "p" && a[3] === "host")?.[1] ?? ev.pubkey;
|
||||||
} else if (service?.startsWith("wss+livekit://")) {
|
|
||||||
return (
|
|
||||||
<Suspense>
|
|
||||||
<LiveKitRoom ev={ev} canJoin={true} />
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return <NoteAppHandler ev={ev} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return inner();
|
|
||||||
}
|
|
||||||
|
|
||||||
function LiveStreamEvent({ ev }: { ev: TaggedNostrEvent }) {
|
|
||||||
const { title, status, starts, host } = extractStreamInfo(ev);
|
|
||||||
const [play, setPlay] = useState(false);
|
const [play, setPlay] = useState(false);
|
||||||
|
|
||||||
function statusLine() {
|
function statusLine() {
|
||||||
@ -40,7 +22,7 @@ function LiveStreamEvent({ ev }: { ev: TaggedNostrEvent }) {
|
|||||||
<div className="flex g4 items-center">
|
<div className="flex g4 items-center">
|
||||||
<Icon name="signal-01" />
|
<Icon name="signal-01" />
|
||||||
<b className="uppercase">
|
<b className="uppercase">
|
||||||
<FormattedMessage defaultMessage="Live" />
|
<FormattedMessage defaultMessage="Live" id="Dn82AL" />
|
||||||
</b>
|
</b>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -48,7 +30,7 @@ function LiveStreamEvent({ ev }: { ev: TaggedNostrEvent }) {
|
|||||||
case "ended": {
|
case "ended": {
|
||||||
return (
|
return (
|
||||||
<b className="uppercase">
|
<b className="uppercase">
|
||||||
<FormattedMessage defaultMessage="Ended" />
|
<FormattedMessage defaultMessage="Ended" id="TP/cMX" />
|
||||||
</b>
|
</b>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -56,7 +38,7 @@ function LiveStreamEvent({ ev }: { ev: TaggedNostrEvent }) {
|
|||||||
return (
|
return (
|
||||||
<b className="uppercase">
|
<b className="uppercase">
|
||||||
{new Intl.DateTimeFormat(undefined, { dateStyle: "full", timeStyle: "short" }).format(
|
{new Intl.DateTimeFormat(undefined, { dateStyle: "full", timeStyle: "short" }).format(
|
||||||
new Date(Number(starts) * 1000),
|
new Date(starts * 1000),
|
||||||
)}
|
)}
|
||||||
</b>
|
</b>
|
||||||
);
|
);
|
||||||
@ -70,7 +52,7 @@ function LiveStreamEvent({ ev }: { ev: TaggedNostrEvent }) {
|
|||||||
case "live": {
|
case "live": {
|
||||||
return (
|
return (
|
||||||
<button className="nowrap" onClick={() => setPlay(true)}>
|
<button className="nowrap" onClick={() => setPlay(true)}>
|
||||||
<FormattedMessage defaultMessage="Watch Stream" />
|
<FormattedMessage defaultMessage="Watch Stream" id="furjvW" />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -79,7 +61,7 @@ function LiveStreamEvent({ ev }: { ev: TaggedNostrEvent }) {
|
|||||||
return (
|
return (
|
||||||
<Link to={link} target="_blank">
|
<Link to={link} target="_blank">
|
||||||
<button className="nowrap">
|
<button className="nowrap">
|
||||||
<FormattedMessage defaultMessage="Watch Replay" />
|
<FormattedMessage defaultMessage="Watch Replay" id="6/hB3S" />
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
@ -91,6 +73,8 @@ function LiveStreamEvent({ ev }: { ev: TaggedNostrEvent }) {
|
|||||||
const link = `https://zap.stream/embed/${NostrLink.fromEvent(ev).encode()}`;
|
const link = `https://zap.stream/embed/${NostrLink.fromEvent(ev).encode()}`;
|
||||||
return (
|
return (
|
||||||
<iframe
|
<iframe
|
||||||
|
// eslint-disable-next-line react/no-unknown-property
|
||||||
|
credentialless=""
|
||||||
src={link}
|
src={link}
|
||||||
width="100%"
|
width="100%"
|
||||||
style={{
|
style={{
|
||||||
@ -102,7 +86,7 @@ function LiveStreamEvent({ ev }: { ev: TaggedNostrEvent }) {
|
|||||||
return (
|
return (
|
||||||
<div className="sm:flex g12 br p24 bg-primary items-center">
|
<div className="sm:flex g12 br p24 bg-primary items-center">
|
||||||
<div>
|
<div>
|
||||||
<ProfileImage pubkey={host!} showUsername={false} size={56} />
|
<ProfileImage pubkey={host} showUsername={false} size={56} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col g8 grow">
|
<div className="flex flex-col g8 grow">
|
||||||
<div className="font-semibold text-3xl">{title}</div>
|
<div className="font-semibold text-3xl">{title}</div>
|
||||||
|
54
packages/app/src/Components/LiveStream/LiveStreams.css
Normal file
54
packages/app/src/Components/LiveStream/LiveStreams.css
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
.stream-list {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-list::-webkit-scrollbar {
|
||||||
|
height: 6.25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-event {
|
||||||
|
display: flex;
|
||||||
|
padding: 8px 12px;
|
||||||
|
gap: 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-event > div:first-of-type {
|
||||||
|
border-radius: 8px;
|
||||||
|
height: 49px;
|
||||||
|
width: 65px;
|
||||||
|
background-color: var(--gray-light);
|
||||||
|
background-image: var(--img);
|
||||||
|
background-position: center;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-event span.live {
|
||||||
|
display: flex;
|
||||||
|
padding: 4px 6px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
border-radius: 9px;
|
||||||
|
background: var(--live);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-event .details .reactions {
|
||||||
|
color: var(--font-secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-event .details > div:nth-of-type(2) {
|
||||||
|
width: 100px;
|
||||||
|
min-width: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
@ -1,67 +1,64 @@
|
|||||||
import { NostrEvent, NostrLink } from "@snort/system";
|
import "./LiveStreams.css";
|
||||||
import { useUserProfile } from "@snort/system-react";
|
|
||||||
import classNames from "classnames";
|
import { unixNow } from "@snort/shared";
|
||||||
import { CSSProperties } from "react";
|
import { EventKind, NostrEvent, NostrLink, RequestBuilder } from "@snort/system";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { useRequestBuilder } from "@snort/system-react";
|
||||||
|
import { CSSProperties, useMemo } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
import Icon from "@/Components/Icons/Icon";
|
||||||
|
import useFollowsControls from "@/Hooks/useFollowControls";
|
||||||
import useImgProxy from "@/Hooks/useImgProxy";
|
import useImgProxy from "@/Hooks/useImgProxy";
|
||||||
import useLiveStreams from "@/Hooks/useLiveStreams";
|
|
||||||
import { findTag } from "@/Utils";
|
import { findTag } from "@/Utils";
|
||||||
|
|
||||||
import Avatar from "../User/Avatar";
|
|
||||||
|
|
||||||
export function LiveStreams() {
|
export function LiveStreams() {
|
||||||
const streams = useLiveStreams();
|
const { followList } = useFollowsControls();
|
||||||
|
const sub = useMemo(() => {
|
||||||
|
const since = unixNow() - 60 * 60 * 24;
|
||||||
|
const rb = new RequestBuilder("follows:streams");
|
||||||
|
rb.withFilter().kinds([EventKind.LiveEvent]).authors(followList).since(since);
|
||||||
|
rb.withFilter().kinds([EventKind.LiveEvent]).tag("p", followList).since(since);
|
||||||
|
return rb;
|
||||||
|
}, [followList]);
|
||||||
|
|
||||||
|
const streams = useRequestBuilder(sub);
|
||||||
if (streams.length === 0) return null;
|
if (streams.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex mx-2 gap-4 overflow-x-auto sm-hide-scrollbar">
|
<div className="stream-list">
|
||||||
{streams.map(v => (
|
{streams.map(v => (
|
||||||
<LiveStreamEvent ev={v} key={`${v.kind}:${v.pubkey}:${findTag(v, "d")}`} className="h-[80px]" />
|
<LiveStreamEvent ev={v} key={`${v.kind}:${v.pubkey}:${findTag(v, "d")}`} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LiveStreamEvent({ ev, className }: { ev: NostrEvent; className?: string }) {
|
function LiveStreamEvent({ ev }: { ev: NostrEvent }) {
|
||||||
const { proxy } = useImgProxy();
|
const { proxy } = useImgProxy();
|
||||||
const title = findTag(ev, "title");
|
const title = findTag(ev, "title");
|
||||||
const image = findTag(ev, "image");
|
const image = findTag(ev, "image");
|
||||||
const status = findTag(ev, "status");
|
const status = findTag(ev, "status");
|
||||||
const viewers = findTag(ev, "current_participants");
|
|
||||||
const host = ev.tags.find(a => a[0] === "p" && a[3] === "host")?.[1] ?? ev.pubkey;
|
|
||||||
const hostProfile = useUserProfile(host);
|
|
||||||
|
|
||||||
const link = NostrLink.fromEvent(ev).encode();
|
const link = NostrLink.fromEvent(ev).encode(CONFIG.eventLinkPrefix);
|
||||||
const imageProxy = proxy(image ?? "");
|
const imageProxy = proxy(image ?? "");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link className={classNames("flex gap-2", className)} to={`https://zap.stream/${link}`} target="_blank">
|
<Link className="stream-event" to={`https://zap.stream/${link}`} target="_blank">
|
||||||
<div className="relative aspect-video">
|
<div
|
||||||
<div
|
style={
|
||||||
className="absolute h-full w-full bg-center bg-cover bg-gray-ultradark rounded-lg"
|
{
|
||||||
style={
|
"--img": `url(${imageProxy})`,
|
||||||
{
|
} as CSSProperties
|
||||||
backgroundImage: `url(${imageProxy})`,
|
}></div>
|
||||||
} as CSSProperties
|
<div className="flex flex-col details">
|
||||||
}></div>
|
<div className="flex g2">
|
||||||
<div className="absolute left-0 top-0 w-full overflow-hidden">
|
<span className="live">{status}</span>
|
||||||
<div
|
<div className="reaction-pill">
|
||||||
className="whitespace-nowrap px-1 text-ellipsis overflow-hidden text-xs font-medium bg-background opacity-70 text-center"
|
<Icon name="zap" size={24} />
|
||||||
title={title}>
|
<div className="reaction-pill-number">0</div>
|
||||||
{title}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute bottom-1 left-1 bg-heart rounded-md px-2 uppercase font-bold">{status}</div>
|
<div>{title}</div>
|
||||||
<div className="absolute right-1 bottom-1">
|
|
||||||
<Avatar pubkey={host} user={hostProfile} size={25} className="outline outline-2 outline-highlight" />
|
|
||||||
</div>
|
|
||||||
{viewers && (
|
|
||||||
<div className="absolute left-1 bottom-7 rounded-md px-2 py-1 text-xs bg-gray font-medium">
|
|
||||||
<FormattedMessage defaultMessage="{n} viewers" values={{ n: viewers }} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
@ -1,131 +0,0 @@
|
|||||||
import { LiveKitRoom as LiveKitRoomContext, RoomAudioRenderer, useParticipants } from "@livekit/components-react";
|
|
||||||
import { dedupe, unixNow } from "@snort/shared";
|
|
||||||
import { EventKind, NostrLink, RequestBuilder, TaggedNostrEvent } from "@snort/system";
|
|
||||||
import { useRequestBuilder, useUserProfile } from "@snort/system-react";
|
|
||||||
import { LocalParticipant, RemoteParticipant } from "livekit-client";
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
|
||||||
import { FormattedMessage } from "react-intl";
|
|
||||||
|
|
||||||
import useEventPublisher from "@/Hooks/useEventPublisher";
|
|
||||||
import { extractStreamInfo } from "@/Utils/stream";
|
|
||||||
|
|
||||||
import AsyncButton from "../Button/AsyncButton";
|
|
||||||
import { ProxyImg } from "../ProxyImg";
|
|
||||||
import Avatar from "../User/Avatar";
|
|
||||||
import { AvatarGroup } from "../User/AvatarGroup";
|
|
||||||
import DisplayName from "../User/DisplayName";
|
|
||||||
|
|
||||||
export default function LiveKitRoom({ ev, canJoin }: { ev: TaggedNostrEvent; canJoin?: boolean }) {
|
|
||||||
const { stream, service, id } = extractStreamInfo(ev);
|
|
||||||
const { publisher } = useEventPublisher();
|
|
||||||
const [join, setJoin] = useState(false);
|
|
||||||
const [token, setToken] = useState<string>();
|
|
||||||
|
|
||||||
async function getToken() {
|
|
||||||
if (!service || !publisher) return;
|
|
||||||
const url = `${service}/api/v1/nests/${id}`;
|
|
||||||
const auth = await publisher.generic(eb => {
|
|
||||||
eb.kind(EventKind.HttpAuthentication);
|
|
||||||
eb.tag(["url", url]);
|
|
||||||
eb.tag(["u", url]);
|
|
||||||
eb.tag(["method", "GET"]);
|
|
||||||
return eb;
|
|
||||||
});
|
|
||||||
const rsp = await fetch(url, {
|
|
||||||
headers: {
|
|
||||||
authorization: `Nostr ${window.btoa(JSON.stringify(auth))}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const text = await rsp.text();
|
|
||||||
if (rsp.ok) {
|
|
||||||
return JSON.parse(text) as { token: string };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (join && !token) {
|
|
||||||
getToken()
|
|
||||||
.then(t => setToken(t?.token))
|
|
||||||
.catch(console.error);
|
|
||||||
}
|
|
||||||
}, [join]);
|
|
||||||
|
|
||||||
if (!join) {
|
|
||||||
return (
|
|
||||||
<div className="p flex flex-col gap-2">
|
|
||||||
<RoomHeader ev={ev} />
|
|
||||||
{(canJoin ?? false) && (
|
|
||||||
<AsyncButton onClick={() => setJoin(true)}>
|
|
||||||
<FormattedMessage defaultMessage="Join Room" />
|
|
||||||
</AsyncButton>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<LiveKitRoomContext token={token} serverUrl={stream?.replace("wss+livekit://", "wss://")} connect={true}>
|
|
||||||
<RoomAudioRenderer volume={1} />
|
|
||||||
<ParticipantList ev={ev} />
|
|
||||||
</LiveKitRoomContext>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function RoomHeader({ ev }: { ev: TaggedNostrEvent }) {
|
|
||||||
const { image, title } = extractStreamInfo(ev);
|
|
||||||
return (
|
|
||||||
<div className="relative rounded-xl h-[140px] w-full overflow-hidden">
|
|
||||||
{image ? <ProxyImg src={image} className="w-full" /> : <div className="absolute bg-gray-dark w-full h-full" />}
|
|
||||||
<div className="absolute left-4 top-4 w-full flex justify-between pr-4">
|
|
||||||
<div className="text-2xl">{title}</div>
|
|
||||||
<div>
|
|
||||||
<NostrParticipants ev={ev} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ParticipantList({ ev }: { ev: TaggedNostrEvent }) {
|
|
||||||
const participants = useParticipants();
|
|
||||||
return (
|
|
||||||
<div className="p">
|
|
||||||
<RoomHeader ev={ev} />
|
|
||||||
<h3>
|
|
||||||
<FormattedMessage defaultMessage="Participants" />
|
|
||||||
</h3>
|
|
||||||
<div className="grid grid-cols-4">
|
|
||||||
{participants.map(a => (
|
|
||||||
<LiveKitUser p={a} key={a.identity} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function NostrParticipants({ ev }: { ev: TaggedNostrEvent }) {
|
|
||||||
const link = NostrLink.fromEvent(ev);
|
|
||||||
const sub = useMemo(() => {
|
|
||||||
const sub = new RequestBuilder(`livekit-participants:${link.tagKey}`);
|
|
||||||
sub
|
|
||||||
.withFilter()
|
|
||||||
.replyToLink([link])
|
|
||||||
.kinds([10_312 as EventKind])
|
|
||||||
.since(unixNow() - 600);
|
|
||||||
return sub;
|
|
||||||
}, [link.tagKey]);
|
|
||||||
|
|
||||||
const presense = useRequestBuilder(sub);
|
|
||||||
return <AvatarGroup ids={dedupe(presense.map(a => a.pubkey))} size={32} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function LiveKitUser({ p }: { p: RemoteParticipant | LocalParticipant }) {
|
|
||||||
const pubkey = p.identity.startsWith("guest-") ? "anon" : p.identity;
|
|
||||||
const profile = useUserProfile(pubkey);
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-2 items-center text-center">
|
|
||||||
<Avatar pubkey={pubkey} className={p.isSpeaking ? "outline" : ""} user={profile} size={48} />
|
|
||||||
<DisplayName pubkey={pubkey} user={pubkey === "anon" ? { name: "Anon" } : profile} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -283,7 +283,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
|||||||
: startBuy(handle, domain)
|
: startBuy(handle, domain)
|
||||||
}>
|
}>
|
||||||
{props.forSubscription ? (
|
{props.forSubscription ? (
|
||||||
<FormattedMessage defaultMessage="Claim Now" />
|
<FormattedMessage defaultMessage="Claim Now" id="FdhSU2" />
|
||||||
) : (
|
) : (
|
||||||
<FormattedMessage {...messages.BuyNow} />
|
<FormattedMessage {...messages.BuyNow} />
|
||||||
)}
|
)}
|
||||||
|
@ -10,11 +10,11 @@ export function Offline({ onRetry, className }: { onRetry?: () => void | Promise
|
|||||||
<div className={classNames("flex items-center g8", className)}>
|
<div className={classNames("flex items-center g8", className)}>
|
||||||
<Icon name="wifi-off" className="error" />
|
<Icon name="wifi-off" className="error" />
|
||||||
<div className="error">
|
<div className="error">
|
||||||
<FormattedMessage defaultMessage="Offline" />
|
<FormattedMessage defaultMessage="Offline" id="7UOvbT" />
|
||||||
</div>
|
</div>
|
||||||
{onRetry && (
|
{onRetry && (
|
||||||
<AsyncButton onClick={onRetry}>
|
<AsyncButton onClick={onRetry}>
|
||||||
<FormattedMessage defaultMessage="Retry" />
|
<FormattedMessage defaultMessage="Retry" id="62nsdy" />
|
||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -70,7 +70,7 @@ export function PinPrompt({
|
|||||||
}}>
|
}}>
|
||||||
<div className="flex flex-col g12">
|
<div className="flex flex-col g12">
|
||||||
<h2>
|
<h2>
|
||||||
<FormattedMessage defaultMessage="Enter Pin" />
|
<FormattedMessage defaultMessage="Enter Pin" id="KtsyO0" />
|
||||||
</h2>
|
</h2>
|
||||||
{subTitle ? <div>{subTitle}</div> : null}
|
{subTitle ? <div>{subTitle}</div> : null}
|
||||||
<input
|
<input
|
||||||
@ -84,10 +84,10 @@ export function PinPrompt({
|
|||||||
{error && <b className="error">{error}</b>}
|
{error && <b className="error">{error}</b>}
|
||||||
<div className="flex g8">
|
<div className="flex g8">
|
||||||
<button type="button" onClick={() => onCancel()}>
|
<button type="button" onClick={() => onCancel()}>
|
||||||
<FormattedMessage defaultMessage="Cancel" />
|
<FormattedMessage defaultMessage="Cancel" id="47FYwb" />
|
||||||
</button>
|
</button>
|
||||||
<AsyncButton ref={submitButtonRef} onClick={() => submitPin()} type="submit">
|
<AsyncButton ref={submitButtonRef} onClick={() => submitPin()} type="submit">
|
||||||
<FormattedMessage defaultMessage="Submit" />
|
<FormattedMessage defaultMessage="Submit" id="wSZR47" />
|
||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -168,7 +168,7 @@ export function LoginUnlock() {
|
|||||||
<PinPrompt
|
<PinPrompt
|
||||||
subTitle={
|
subTitle={
|
||||||
<p>
|
<p>
|
||||||
<FormattedMessage defaultMessage="Enter pin to unlock your private key" />
|
<FormattedMessage defaultMessage="Enter pin to unlock your private key" id="e7VmYP" />
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
onResult={unlockSession}
|
onResult={unlockSession}
|
||||||
|
@ -1,75 +1,65 @@
|
|||||||
import { OkResponse, TaggedNostrEvent } from "@snort/system";
|
import { TaggedNostrEvent } from "@snort/system";
|
||||||
import { SnortContext } from "@snort/system-react";
|
import { SnortContext } from "@snort/system-react";
|
||||||
import { useContext, useState } from "react";
|
import { useContext, useState } from "react";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
import AsyncButton from "@/Components/Button/AsyncButton";
|
|
||||||
import Modal from "@/Components/Modal/Modal";
|
import Modal from "@/Components/Modal/Modal";
|
||||||
import useRelays from "@/Hooks/useRelays";
|
import useRelays from "@/Hooks/useRelays";
|
||||||
|
|
||||||
|
import AsyncButton from "./Button/AsyncButton";
|
||||||
|
import messages from "./messages";
|
||||||
|
|
||||||
export function ReBroadcaster({ onClose, ev }: { onClose: () => void; ev: TaggedNostrEvent }) {
|
export function ReBroadcaster({ onClose, ev }: { onClose: () => void; ev: TaggedNostrEvent }) {
|
||||||
const [selected, setSelected] = useState<Array<string>>();
|
const [selected, setSelected] = useState<Array<string>>();
|
||||||
const [replies, setReplies] = useState<Array<OkResponse>>([]);
|
|
||||||
const [sending, setSending] = useState(false);
|
|
||||||
const system = useContext(SnortContext);
|
const system = useContext(SnortContext);
|
||||||
const relays = useRelays();
|
const relays = useRelays();
|
||||||
|
|
||||||
async function sendReBroadcast() {
|
async function sendReBroadcast() {
|
||||||
setSending(true);
|
if (selected) {
|
||||||
setReplies([]);
|
await Promise.all(selected.map(r => system.WriteOnceToRelay(r, ev)));
|
||||||
try {
|
} else {
|
||||||
if (selected) {
|
system.BroadcastEvent(ev);
|
||||||
await Promise.all(selected.map(r => system.WriteOnceToRelay(r, ev).then(o => setReplies(v => [...v, o]))));
|
|
||||||
} else {
|
|
||||||
const rsp = await system.BroadcastEvent(ev);
|
|
||||||
setReplies(rsp);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
} finally {
|
|
||||||
setSending(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderRelayCustomisation() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col g8">
|
||||||
|
{Object.keys(relays)
|
||||||
|
.filter(el => relays[el].write)
|
||||||
|
.map((r, i, a) => (
|
||||||
|
<div key={r} className="card flex justify-between">
|
||||||
|
<div>{r}</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={!selected || selected.includes(r)}
|
||||||
|
onChange={e =>
|
||||||
|
setSelected(
|
||||||
|
e.target.checked && selected && selected.length === a.length - 1
|
||||||
|
? undefined
|
||||||
|
: a.filter(el => (el === r ? e.target.checked : !selected || selected.includes(el))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal id="broadcaster" onClose={onClose}>
|
<Modal id="broadcaster" className="note-creator-modal" onClose={onClose}>
|
||||||
<div className="flex flex-col gap-4">
|
{renderRelayCustomisation()}
|
||||||
<div className="text-xl font-medium">
|
<div className="flex g8">
|
||||||
<FormattedMessage defaultMessage="Broadcast Event" />
|
<button className="secondary" onClick={onClose}>
|
||||||
</div>
|
<FormattedMessage {...messages.Cancel} />
|
||||||
{Object.keys(relays)
|
</button>
|
||||||
.filter(el => relays[el].write)
|
<AsyncButton onClick={sendReBroadcast}>
|
||||||
.map((r, i, a) => (
|
<FormattedMessage {...messages.ReBroadcast} />
|
||||||
<div key={r} className="flex justify-between">
|
</AsyncButton>
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<div>{r}</div>
|
|
||||||
<small>{replies.findLast(a => a.relay === r)?.message}</small>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
disabled={sending}
|
|
||||||
checked={!selected || selected.includes(r)}
|
|
||||||
onChange={e =>
|
|
||||||
setSelected(
|
|
||||||
e.target.checked && selected && selected.length === a.length - 1
|
|
||||||
? undefined
|
|
||||||
: a.filter(el => (el === r ? e.target.checked : !selected || selected.includes(el))),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<button className="secondary" onClick={onClose}>
|
|
||||||
<FormattedMessage defaultMessage="Cancel" />
|
|
||||||
</button>
|
|
||||||
<AsyncButton onClick={sendReBroadcast} disabled={sending}>
|
|
||||||
<FormattedMessage defaultMessage="Send" />
|
|
||||||
</AsyncButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
|
11
packages/app/src/Components/Relay/Relay.css
Normal file
11
packages/app/src/Components/Relay/Relay.css
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
.relay {
|
||||||
|
border-radius: 5px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: min-content auto;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
.relay > div {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
@ -1,48 +1,85 @@
|
|||||||
import { Link } from "react-router-dom";
|
import "./Relay.css";
|
||||||
|
|
||||||
|
import { RelaySettings } from "@snort/system";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
import { AsyncIcon } from "@/Components/Button/AsyncIcon";
|
||||||
import useRelayState from "@/Feed/RelayState";
|
import useRelayState from "@/Feed/RelayState";
|
||||||
import useLogin from "@/Hooks/useLogin";
|
import useLogin from "@/Hooks/useLogin";
|
||||||
import { getRelayName } from "@/Utils";
|
import { getRelayName } from "@/Utils";
|
||||||
|
|
||||||
import Icon from "../Icons/Icon";
|
import { RelayFavicon } from "./RelaysMetadata";
|
||||||
import RelayPermissions from "./permissions";
|
|
||||||
import RelayStatusLabel from "./status-label";
|
|
||||||
import RelayUptime from "./uptime";
|
|
||||||
|
|
||||||
export interface RelayProps {
|
export interface RelayProps {
|
||||||
addr: string;
|
addr: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Relay(props: RelayProps) {
|
export default function Relay(props: RelayProps) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const state = useLogin(s => s.state);
|
||||||
|
|
||||||
|
const name = useMemo(() => getRelayName(props.addr), [props.addr]);
|
||||||
const connection = useRelayState(props.addr);
|
const connection = useRelayState(props.addr);
|
||||||
const { state } = useLogin(s => ({ v: s.state.version, state: s.state }));
|
|
||||||
if (!connection) return;
|
const relaySettings = state.relays?.find(a => a.url === props.addr)?.settings;
|
||||||
const name = connection.info?.name ?? getRelayName(props.addr);
|
if (!relaySettings || !connection) return;
|
||||||
|
|
||||||
|
async function configure(o: RelaySettings) {
|
||||||
|
await state.updateRelay(props.addr, o);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr>
|
<>
|
||||||
<td className="text-ellipsis" title={props.addr}>
|
<div className="relay bg-dark">
|
||||||
<Link to={`/settings/relays/${encodeURIComponent(props.addr)}`}>
|
<div className={classNames("flex items-center", connection.isOpen ? "bg-success" : "bg-error")}>
|
||||||
{name.length > 20 ? <>{name.slice(0, 20)}...</> : name}
|
<RelayFavicon url={props.addr} />
|
||||||
</Link>
|
</div>
|
||||||
</td>
|
<div className="flex flex-col g8">
|
||||||
<td>
|
<div>
|
||||||
<RelayStatusLabel conn={connection} />
|
<b>{name}</b>
|
||||||
</td>
|
</div>
|
||||||
<td>
|
{!connection?.Ephemeral && (
|
||||||
<RelayPermissions conn={connection} />
|
<div className="flex g8">
|
||||||
</td>
|
<AsyncIcon
|
||||||
<td className="text-center">
|
iconName="write"
|
||||||
<RelayUptime url={props.addr} />
|
iconSize={16}
|
||||||
</td>
|
className={classNames("button-icon-sm transparent", { active: relaySettings.write })}
|
||||||
<td>
|
onClick={() =>
|
||||||
<Icon
|
configure({
|
||||||
name="trash"
|
write: !relaySettings.write,
|
||||||
className="text-gray-light cursor-pointer"
|
read: relaySettings.read,
|
||||||
onClick={() => {
|
})
|
||||||
state.removeRelay(props.addr, true);
|
}
|
||||||
}}
|
/>
|
||||||
/>
|
<AsyncIcon
|
||||||
</td>
|
iconName="read"
|
||||||
</tr>
|
iconSize={16}
|
||||||
|
className={classNames("button-icon-sm transparent", { active: relaySettings.read })}
|
||||||
|
onClick={() =>
|
||||||
|
configure({
|
||||||
|
write: relaySettings.write,
|
||||||
|
read: !relaySettings.read,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<AsyncIcon
|
||||||
|
iconName="trash"
|
||||||
|
iconSize={16}
|
||||||
|
className="button-icon-sm transparent trash-icon"
|
||||||
|
onClick={() => state.removeRelay(props.addr)}
|
||||||
|
/>
|
||||||
|
<AsyncIcon
|
||||||
|
iconName="gear"
|
||||||
|
iconSize={16}
|
||||||
|
className="button-icon-sm transparent"
|
||||||
|
onClick={() => navigate(connection?.Id ?? "")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
9
packages/app/src/Components/Relay/RelaysMetadata.css
Normal file
9
packages/app/src/Components/Relay/RelaysMetadata.css
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
.favicon {
|
||||||
|
width: 21px;
|
||||||
|
height: 21px;
|
||||||
|
max-width: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relay-active {
|
||||||
|
color: var(--highlight);
|
||||||
|
}
|
@ -1,10 +1,12 @@
|
|||||||
|
import "./RelaysMetadata.css";
|
||||||
|
|
||||||
import { FullRelaySettings } from "@snort/system";
|
import { FullRelaySettings } from "@snort/system";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import Nostrich from "@/assets/img/nostrich.webp";
|
import Nostrich from "@/assets/img/nostrich.webp";
|
||||||
import Icon from "@/Components/Icons/Icon";
|
import Icon from "@/Components/Icons/Icon";
|
||||||
|
|
||||||
export const RelayFavicon = ({ url, size }: { url: string; size?: number }) => {
|
export const RelayFavicon = ({ url }: { url: string }) => {
|
||||||
const cleanUrl = url
|
const cleanUrl = url
|
||||||
.replace(/^wss:\/\//, "https://")
|
.replace(/^wss:\/\//, "https://")
|
||||||
.replace(/^ws:\/\//, "http://")
|
.replace(/^ws:\/\//, "http://")
|
||||||
@ -12,12 +14,10 @@ export const RelayFavicon = ({ url, size }: { url: string; size?: number }) => {
|
|||||||
const [faviconUrl, setFaviconUrl] = useState(`${cleanUrl}/favicon.ico`);
|
const [faviconUrl, setFaviconUrl] = useState(`${cleanUrl}/favicon.ico`);
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
className="rounded-full object-cover"
|
className="circle favicon"
|
||||||
src={faviconUrl}
|
src={faviconUrl}
|
||||||
onError={() => setFaviconUrl(Nostrich)}
|
onError={() => setFaviconUrl(Nostrich)}
|
||||||
alt={`favicon for ${url}`}
|
alt={`favicon for ${url}`}
|
||||||
width={size ?? 20}
|
|
||||||
height={size ?? 20}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -35,8 +35,8 @@ const RelaysMetadata = ({ relays }: RelaysMetadataProps) => {
|
|||||||
<RelayFavicon url={url} />
|
<RelayFavicon url={url} />
|
||||||
<code className="grow f-ellipsis">{url}</code>
|
<code className="grow f-ellipsis">{url}</code>
|
||||||
<div className="flex g8">
|
<div className="flex g8">
|
||||||
<Icon name="read" className={settings.read ? "text-highlight" : "disabled"} />
|
<Icon name="read" className={settings.read ? "relay-active" : "disabled"} />
|
||||||
<Icon name="write" className={settings.write ? "text-highlight" : "disabled"} />
|
<Icon name="write" className={settings.write ? "relay-active" : "disabled"} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
import { RelayInfo } from "@snort/system";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import { FormattedMessage } from "react-intl";
|
|
||||||
|
|
||||||
export default function RelayPaymentLabel({ info }: { info: RelayInfo }) {
|
|
||||||
const isPaid = info?.limitation?.payment_required ?? false;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames("rounded-full px-2 py-1 font-medium", {
|
|
||||||
"bg-[var(--pro)] text-black": isPaid,
|
|
||||||
"bg-[var(--free)]": !isPaid,
|
|
||||||
})}>
|
|
||||||
{isPaid && <FormattedMessage defaultMessage="Paid" />}
|
|
||||||
{!isPaid && <FormattedMessage defaultMessage="Free" />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
import { ConnectionType } from "@snort/system/dist/connection-pool";
|
|
||||||
import { FormattedMessage } from "react-intl";
|
|
||||||
|
|
||||||
import useLogin from "@/Hooks/useLogin";
|
|
||||||
|
|
||||||
export default function RelayPermissions({ conn }: { conn: ConnectionType }) {
|
|
||||||
const { state } = useLogin(s => ({ v: s.state.version, state: s.state }));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex gap-2 cursor-pointer select-none">
|
|
||||||
<div
|
|
||||||
className={conn.settings.read ? "" : "text-gray"}
|
|
||||||
onClick={async () =>
|
|
||||||
await state.updateRelay(conn.address, {
|
|
||||||
read: !conn.settings.read,
|
|
||||||
write: conn.settings.write,
|
|
||||||
})
|
|
||||||
}>
|
|
||||||
<FormattedMessage defaultMessage="Read" />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={conn.settings.write ? "" : "text-gray"}
|
|
||||||
onClick={async () =>
|
|
||||||
await state.updateRelay(conn.address, {
|
|
||||||
read: conn.settings.read,
|
|
||||||
write: !conn.settings.write,
|
|
||||||
})
|
|
||||||
}>
|
|
||||||
<FormattedMessage defaultMessage="Write" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
import { Link } from "react-router-dom";
|
|
||||||
|
|
||||||
export default function RelaySoftware({ software }: { software: string }) {
|
|
||||||
if (software.includes("git")) {
|
|
||||||
const u = new URL(software);
|
|
||||||
return <Link to={software}>{u.pathname.split("/").at(-1)?.replace(".git", "")}</Link>;
|
|
||||||
}
|
|
||||||
return software;
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
import { ConnectionType } from "@snort/system/dist/connection-pool";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import { FormattedMessage } from "react-intl";
|
|
||||||
|
|
||||||
export default function RelayStatusLabel({ conn }: { conn: ConnectionType }) {
|
|
||||||
return (
|
|
||||||
<div className="flex gap-1 items-center">
|
|
||||||
<div
|
|
||||||
className={classNames("rounded-full w-4 h-4", {
|
|
||||||
"bg-success": conn.isOpen,
|
|
||||||
"bg-error": !conn.isOpen,
|
|
||||||
})}></div>
|
|
||||||
{conn.isOpen ? <FormattedMessage defaultMessage="Connected" /> : <FormattedMessage defaultMessage="Offline" />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
import classNames from "classnames";
|
|
||||||
import { FormattedMessage } from "react-intl";
|
|
||||||
|
|
||||||
export default function UptimeLabel({ avgPing }: { avgPing: number }) {
|
|
||||||
const idealPing = 500;
|
|
||||||
const badPing = idealPing * 2;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames("font-semibold", {
|
|
||||||
"text-error": isNaN(avgPing) || avgPing > badPing,
|
|
||||||
"text-warning": avgPing > idealPing && avgPing < badPing,
|
|
||||||
"text-success": avgPing < idealPing,
|
|
||||||
})}
|
|
||||||
title={`${avgPing.toFixed(0)} ms`}>
|
|
||||||
{isNaN(avgPing) && <FormattedMessage defaultMessage="Dead" />}
|
|
||||||
{avgPing > badPing && <FormattedMessage defaultMessage="Poor" />}
|
|
||||||
{avgPing > idealPing && avgPing < badPing && <FormattedMessage defaultMessage="Good" />}
|
|
||||||
{avgPing < idealPing && <FormattedMessage defaultMessage="Great" />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
import { sanitizeRelayUrl, unixNow } from "@snort/shared";
|
|
||||||
import { EventKind, RequestBuilder } from "@snort/system";
|
|
||||||
import { useRequestBuilder } from "@snort/system-react";
|
|
||||||
import { useMemo } from "react";
|
|
||||||
|
|
||||||
import { findTag } from "@/Utils";
|
|
||||||
import { Day, MonitorRelays } from "@/Utils/Const";
|
|
||||||
|
|
||||||
import UptimeLabel from "./uptime-label";
|
|
||||||
|
|
||||||
export default function RelayUptime({ url }: { url: string }) {
|
|
||||||
const sub = useMemo(() => {
|
|
||||||
const u = sanitizeRelayUrl(url);
|
|
||||||
const rb = new RequestBuilder(`uptime`);
|
|
||||||
if (u) {
|
|
||||||
rb.withFilter()
|
|
||||||
.kinds([30_166 as EventKind])
|
|
||||||
.tag("d", [u])
|
|
||||||
.since(unixNow() - Day)
|
|
||||||
.relay(MonitorRelays);
|
|
||||||
}
|
|
||||||
return rb;
|
|
||||||
}, [url]);
|
|
||||||
|
|
||||||
const data = useRequestBuilder(sub);
|
|
||||||
const myData = data.filter(a => findTag(a, "d") === url);
|
|
||||||
const ping = myData.reduce(
|
|
||||||
(acc, v) => {
|
|
||||||
const read = findTag(v, "rtt-read");
|
|
||||||
if (read) {
|
|
||||||
acc.n += 1;
|
|
||||||
acc.total += Number(read);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
n: 0,
|
|
||||||
total: 0,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const avgPing = ping.total / ping.n;
|
|
||||||
return <UptimeLabel avgPing={avgPing} />;
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user