Merge remote-tracking branch 'upstream/main' into feat/connect-wallet
This commit is contained in:
37
.drone.yaml
37
.drone.yaml
@ -23,3 +23,40 @@ volumes:
|
|||||||
- name: cache
|
- name: cache
|
||||||
claim:
|
claim:
|
||||||
name: docker-cache
|
name: docker-cache
|
||||||
|
---
|
||||||
|
kind: pipeline
|
||||||
|
type: kubernetes
|
||||||
|
name: crowdin
|
||||||
|
concurrency:
|
||||||
|
limit: 1
|
||||||
|
trigger:
|
||||||
|
branch:
|
||||||
|
- main
|
||||||
|
metadata:
|
||||||
|
namespace: git
|
||||||
|
steps:
|
||||||
|
- name: Push/Pull translations
|
||||||
|
image: node:current-bullseye
|
||||||
|
volumes:
|
||||||
|
- name: cache
|
||||||
|
path: /cache
|
||||||
|
environment:
|
||||||
|
YARN_CACHE_FOLDER: /cache/.yarn-translations
|
||||||
|
TOKEN:
|
||||||
|
from_secret: gitea
|
||||||
|
CTOKEN:
|
||||||
|
from_secret: crowdin
|
||||||
|
commands:
|
||||||
|
- git config --global user.email drone@v0l.io
|
||||||
|
- git config --global user.name "Drone CI"
|
||||||
|
- git remote set-url origin https://drone:$TOKEN@git.v0l.io/Kieran/stream.git
|
||||||
|
- yarn install
|
||||||
|
- npx @crowdin/cli upload sources -b main -T $CTOKEN
|
||||||
|
- npx @crowdin/cli pull -b main -T $CTOKEN
|
||||||
|
- git add .
|
||||||
|
- 'git commit -a -m "chore: Update translations"'
|
||||||
|
- git push -u origin main
|
||||||
|
volumes:
|
||||||
|
- name: cache
|
||||||
|
claim:
|
||||||
|
name: docker-cache
|
@ -1 +0,0 @@
|
|||||||
{}
|
|
6
.vscode/extensions.json
vendored
6
.vscode/extensions.json
vendored
@ -1,7 +1,3 @@
|
|||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": ["arcanis.vscode-zipfs", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
|
||||||
"arcanis.vscode-zipfs",
|
|
||||||
"dbaeumer.vscode-eslint",
|
|
||||||
"esbenp.prettier-vscode"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
File diff suppressed because one or more lines are too long
2
.yarn/sdks/eslint/package.json
vendored
2
.yarn/sdks/eslint/package.json
vendored
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "eslint",
|
"name": "eslint",
|
||||||
"version": "8.45.0-sdk",
|
"version": "8.48.0-sdk",
|
||||||
"main": "./lib/api.js",
|
"main": "./lib/api.js",
|
||||||
"type": "commonjs"
|
"type": "commonjs"
|
||||||
}
|
}
|
||||||
|
50
.yarn/sdks/typescript/lib/tsserver.js
vendored
50
.yarn/sdks/typescript/lib/tsserver.js
vendored
@ -9,7 +9,7 @@ const relPnpApiPath = "../../../../.pnp.cjs";
|
|||||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||||
const absRequire = createRequire(absPnpApiPath);
|
const absRequire = createRequire(absPnpApiPath);
|
||||||
|
|
||||||
const moduleWrapper = (tsserver) => {
|
const moduleWrapper = tsserver => {
|
||||||
if (!process.versions.pnp) {
|
if (!process.versions.pnp) {
|
||||||
return tsserver;
|
return tsserver;
|
||||||
}
|
}
|
||||||
@ -17,12 +17,12 @@ const moduleWrapper = (tsserver) => {
|
|||||||
const { isAbsolute } = require(`path`);
|
const { isAbsolute } = require(`path`);
|
||||||
const pnpApi = require(`pnpapi`);
|
const pnpApi = require(`pnpapi`);
|
||||||
|
|
||||||
const isVirtual = (str) => str.match(/\/(\$\$virtual|__virtual__)\//);
|
const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//);
|
||||||
const isPortal = (str) => str.startsWith("portal:/");
|
const isPortal = str => str.startsWith("portal:/");
|
||||||
const normalize = (str) => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
|
const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
|
||||||
|
|
||||||
const dependencyTreeRoots = new Set(
|
const dependencyTreeRoots = new Set(
|
||||||
pnpApi.getDependencyTreeRoots().map((locator) => {
|
pnpApi.getDependencyTreeRoots().map(locator => {
|
||||||
return `${locator.name}@${locator.reference}`;
|
return `${locator.name}@${locator.reference}`;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -33,11 +33,7 @@ const moduleWrapper = (tsserver) => {
|
|||||||
|
|
||||||
function toEditorPath(str) {
|
function toEditorPath(str) {
|
||||||
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
|
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
|
||||||
if (
|
if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) {
|
||||||
isAbsolute(str) &&
|
|
||||||
!str.match(/^\^?(zip:|\/zip\/)/) &&
|
|
||||||
(str.match(/\.zip\//) || isVirtual(str))
|
|
||||||
) {
|
|
||||||
// We also take the opportunity to turn virtual paths into physical ones;
|
// We also take the opportunity to turn virtual paths into physical ones;
|
||||||
// this makes it much easier to work with workspaces that list peer
|
// this makes it much easier to work with workspaces that list peer
|
||||||
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
|
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
|
||||||
@ -53,8 +49,7 @@ const moduleWrapper = (tsserver) => {
|
|||||||
const locator = pnpApi.findPackageLocator(resolved);
|
const locator = pnpApi.findPackageLocator(resolved);
|
||||||
if (
|
if (
|
||||||
locator &&
|
locator &&
|
||||||
(dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) ||
|
(dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))
|
||||||
isPortal(locator.reference))
|
|
||||||
) {
|
) {
|
||||||
str = resolved;
|
str = resolved;
|
||||||
}
|
}
|
||||||
@ -149,9 +144,7 @@ const moduleWrapper = (tsserver) => {
|
|||||||
// The path for coc-nvim is in format of /<pwd>/zipfile:/<pwd>/.yarn/...
|
// The path for coc-nvim is in format of /<pwd>/zipfile:/<pwd>/.yarn/...
|
||||||
// So in order to convert it back, we use .* to match all the thing
|
// So in order to convert it back, we use .* to match all the thing
|
||||||
// before `zipfile:`
|
// before `zipfile:`
|
||||||
return process.platform === `win32`
|
return process.platform === `win32` ? str.replace(/^.*zipfile:\//, ``) : str.replace(/^.*zipfile:/, ``);
|
||||||
? str.replace(/^.*zipfile:\//, ``)
|
|
||||||
: str.replace(/^.*zipfile:/, ``);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -166,10 +159,7 @@ const moduleWrapper = (tsserver) => {
|
|||||||
case `vscode`:
|
case `vscode`:
|
||||||
default:
|
default:
|
||||||
{
|
{
|
||||||
return str.replace(
|
return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`);
|
||||||
/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/,
|
|
||||||
process.platform === `win32` ? `` : `/`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -183,8 +173,7 @@ const moduleWrapper = (tsserver) => {
|
|||||||
// TypeScript already does local loads and if this code is running the user trusts the workspace
|
// TypeScript already does local loads and if this code is running the user trusts the workspace
|
||||||
// https://github.com/microsoft/vscode/issues/45856
|
// https://github.com/microsoft/vscode/issues/45856
|
||||||
const ConfiguredProject = tsserver.server.ConfiguredProject;
|
const ConfiguredProject = tsserver.server.ConfiguredProject;
|
||||||
const { enablePluginsWithOptions: originalEnablePluginsWithOptions } =
|
const { enablePluginsWithOptions: originalEnablePluginsWithOptions } = ConfiguredProject.prototype;
|
||||||
ConfiguredProject.prototype;
|
|
||||||
ConfiguredProject.prototype.enablePluginsWithOptions = function () {
|
ConfiguredProject.prototype.enablePluginsWithOptions = function () {
|
||||||
this.projectService.allowLocalPluginLoads = true;
|
this.projectService.allowLocalPluginLoads = true;
|
||||||
return originalEnablePluginsWithOptions.apply(this, arguments);
|
return originalEnablePluginsWithOptions.apply(this, arguments);
|
||||||
@ -195,8 +184,7 @@ const moduleWrapper = (tsserver) => {
|
|||||||
// like an absolute path of ours and normalize it.
|
// like an absolute path of ours and normalize it.
|
||||||
|
|
||||||
const Session = tsserver.server.Session;
|
const Session = tsserver.server.Session;
|
||||||
const { onMessage: originalOnMessage, send: originalSend } =
|
const { onMessage: originalOnMessage, send: originalSend } = Session.prototype;
|
||||||
Session.prototype;
|
|
||||||
let hostInfo = `unknown`;
|
let hostInfo = `unknown`;
|
||||||
|
|
||||||
Object.assign(Session.prototype, {
|
Object.assign(Session.prototype, {
|
||||||
@ -231,19 +219,11 @@ const moduleWrapper = (tsserver) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const processedMessageJSON = JSON.stringify(
|
const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => {
|
||||||
parsedMessage,
|
return typeof value === "string" ? fromEditorPath(value) : value;
|
||||||
(key, value) => {
|
});
|
||||||
return typeof value === "string" ? fromEditorPath(value) : value;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return originalOnMessage.call(
|
return originalOnMessage.call(this, isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON));
|
||||||
this,
|
|
||||||
isStringMessage
|
|
||||||
? processedMessageJSON
|
|
||||||
: JSON.parse(processedMessageJSON)
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
send(/** @type {any} */ msg) {
|
send(/** @type {any} */ msg) {
|
||||||
|
50
.yarn/sdks/typescript/lib/tsserverlibrary.js
vendored
50
.yarn/sdks/typescript/lib/tsserverlibrary.js
vendored
@ -9,7 +9,7 @@ const relPnpApiPath = "../../../../.pnp.cjs";
|
|||||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||||
const absRequire = createRequire(absPnpApiPath);
|
const absRequire = createRequire(absPnpApiPath);
|
||||||
|
|
||||||
const moduleWrapper = (tsserver) => {
|
const moduleWrapper = tsserver => {
|
||||||
if (!process.versions.pnp) {
|
if (!process.versions.pnp) {
|
||||||
return tsserver;
|
return tsserver;
|
||||||
}
|
}
|
||||||
@ -17,12 +17,12 @@ const moduleWrapper = (tsserver) => {
|
|||||||
const { isAbsolute } = require(`path`);
|
const { isAbsolute } = require(`path`);
|
||||||
const pnpApi = require(`pnpapi`);
|
const pnpApi = require(`pnpapi`);
|
||||||
|
|
||||||
const isVirtual = (str) => str.match(/\/(\$\$virtual|__virtual__)\//);
|
const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//);
|
||||||
const isPortal = (str) => str.startsWith("portal:/");
|
const isPortal = str => str.startsWith("portal:/");
|
||||||
const normalize = (str) => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
|
const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
|
||||||
|
|
||||||
const dependencyTreeRoots = new Set(
|
const dependencyTreeRoots = new Set(
|
||||||
pnpApi.getDependencyTreeRoots().map((locator) => {
|
pnpApi.getDependencyTreeRoots().map(locator => {
|
||||||
return `${locator.name}@${locator.reference}`;
|
return `${locator.name}@${locator.reference}`;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -33,11 +33,7 @@ const moduleWrapper = (tsserver) => {
|
|||||||
|
|
||||||
function toEditorPath(str) {
|
function toEditorPath(str) {
|
||||||
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
|
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
|
||||||
if (
|
if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) {
|
||||||
isAbsolute(str) &&
|
|
||||||
!str.match(/^\^?(zip:|\/zip\/)/) &&
|
|
||||||
(str.match(/\.zip\//) || isVirtual(str))
|
|
||||||
) {
|
|
||||||
// We also take the opportunity to turn virtual paths into physical ones;
|
// We also take the opportunity to turn virtual paths into physical ones;
|
||||||
// this makes it much easier to work with workspaces that list peer
|
// this makes it much easier to work with workspaces that list peer
|
||||||
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
|
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
|
||||||
@ -53,8 +49,7 @@ const moduleWrapper = (tsserver) => {
|
|||||||
const locator = pnpApi.findPackageLocator(resolved);
|
const locator = pnpApi.findPackageLocator(resolved);
|
||||||
if (
|
if (
|
||||||
locator &&
|
locator &&
|
||||||
(dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) ||
|
(dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))
|
||||||
isPortal(locator.reference))
|
|
||||||
) {
|
) {
|
||||||
str = resolved;
|
str = resolved;
|
||||||
}
|
}
|
||||||
@ -149,9 +144,7 @@ const moduleWrapper = (tsserver) => {
|
|||||||
// The path for coc-nvim is in format of /<pwd>/zipfile:/<pwd>/.yarn/...
|
// The path for coc-nvim is in format of /<pwd>/zipfile:/<pwd>/.yarn/...
|
||||||
// So in order to convert it back, we use .* to match all the thing
|
// So in order to convert it back, we use .* to match all the thing
|
||||||
// before `zipfile:`
|
// before `zipfile:`
|
||||||
return process.platform === `win32`
|
return process.platform === `win32` ? str.replace(/^.*zipfile:\//, ``) : str.replace(/^.*zipfile:/, ``);
|
||||||
? str.replace(/^.*zipfile:\//, ``)
|
|
||||||
: str.replace(/^.*zipfile:/, ``);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -166,10 +159,7 @@ const moduleWrapper = (tsserver) => {
|
|||||||
case `vscode`:
|
case `vscode`:
|
||||||
default:
|
default:
|
||||||
{
|
{
|
||||||
return str.replace(
|
return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`);
|
||||||
/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/,
|
|
||||||
process.platform === `win32` ? `` : `/`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -183,8 +173,7 @@ const moduleWrapper = (tsserver) => {
|
|||||||
// TypeScript already does local loads and if this code is running the user trusts the workspace
|
// TypeScript already does local loads and if this code is running the user trusts the workspace
|
||||||
// https://github.com/microsoft/vscode/issues/45856
|
// https://github.com/microsoft/vscode/issues/45856
|
||||||
const ConfiguredProject = tsserver.server.ConfiguredProject;
|
const ConfiguredProject = tsserver.server.ConfiguredProject;
|
||||||
const { enablePluginsWithOptions: originalEnablePluginsWithOptions } =
|
const { enablePluginsWithOptions: originalEnablePluginsWithOptions } = ConfiguredProject.prototype;
|
||||||
ConfiguredProject.prototype;
|
|
||||||
ConfiguredProject.prototype.enablePluginsWithOptions = function () {
|
ConfiguredProject.prototype.enablePluginsWithOptions = function () {
|
||||||
this.projectService.allowLocalPluginLoads = true;
|
this.projectService.allowLocalPluginLoads = true;
|
||||||
return originalEnablePluginsWithOptions.apply(this, arguments);
|
return originalEnablePluginsWithOptions.apply(this, arguments);
|
||||||
@ -195,8 +184,7 @@ const moduleWrapper = (tsserver) => {
|
|||||||
// like an absolute path of ours and normalize it.
|
// like an absolute path of ours and normalize it.
|
||||||
|
|
||||||
const Session = tsserver.server.Session;
|
const Session = tsserver.server.Session;
|
||||||
const { onMessage: originalOnMessage, send: originalSend } =
|
const { onMessage: originalOnMessage, send: originalSend } = Session.prototype;
|
||||||
Session.prototype;
|
|
||||||
let hostInfo = `unknown`;
|
let hostInfo = `unknown`;
|
||||||
|
|
||||||
Object.assign(Session.prototype, {
|
Object.assign(Session.prototype, {
|
||||||
@ -231,19 +219,11 @@ const moduleWrapper = (tsserver) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const processedMessageJSON = JSON.stringify(
|
const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => {
|
||||||
parsedMessage,
|
return typeof value === "string" ? fromEditorPath(value) : value;
|
||||||
(key, value) => {
|
});
|
||||||
return typeof value === "string" ? fromEditorPath(value) : value;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return originalOnMessage.call(
|
return originalOnMessage.call(this, isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON));
|
||||||
this,
|
|
||||||
isStringMessage
|
|
||||||
? processedMessageJSON
|
|
||||||
: JSON.parse(processedMessageJSON)
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
send(/** @type {any} */ msg) {
|
send(/** @type {any} */ msg) {
|
||||||
|
2
.yarn/sdks/typescript/package.json
vendored
2
.yarn/sdks/typescript/package.json
vendored
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "typescript",
|
"name": "typescript",
|
||||||
"version": "5.1.3-sdk",
|
"version": "5.2.2-sdk",
|
||||||
"main": "./lib/typescript.js",
|
"main": "./lib/typescript.js",
|
||||||
"type": "commonjs"
|
"type": "commonjs"
|
||||||
}
|
}
|
||||||
|
@ -1 +1 @@
|
|||||||
yarnPath: .yarn/releases/yarn-3.6.1.cjs
|
yarnPath: .yarn/releases/yarn-3.6.3.cjs
|
||||||
|
70
README.md
70
README.md
@ -1,70 +0,0 @@
|
|||||||
# Getting Started with Create React App
|
|
||||||
|
|
||||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
|
||||||
|
|
||||||
## Available Scripts
|
|
||||||
|
|
||||||
In the project directory, you can run:
|
|
||||||
|
|
||||||
### `yarn start`
|
|
||||||
|
|
||||||
Runs the app in the development mode.\
|
|
||||||
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
|
|
||||||
|
|
||||||
The page will reload when you make changes.\
|
|
||||||
You may also see any lint errors in the console.
|
|
||||||
|
|
||||||
### `yarn test`
|
|
||||||
|
|
||||||
Launches the test runner in the interactive watch mode.\
|
|
||||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
|
||||||
|
|
||||||
### `yarn build`
|
|
||||||
|
|
||||||
Builds the app for production to the `build` folder.\
|
|
||||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
|
||||||
|
|
||||||
The build is minified and the filenames include the hashes.\
|
|
||||||
Your app is ready to be deployed!
|
|
||||||
|
|
||||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
|
||||||
|
|
||||||
### `yarn eject`
|
|
||||||
|
|
||||||
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
|
|
||||||
|
|
||||||
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
|
||||||
|
|
||||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
|
|
||||||
|
|
||||||
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
|
|
||||||
|
|
||||||
## Learn More
|
|
||||||
|
|
||||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
|
||||||
|
|
||||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
|
||||||
|
|
||||||
### Code Splitting
|
|
||||||
|
|
||||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
|
||||||
|
|
||||||
### Analyzing the Bundle Size
|
|
||||||
|
|
||||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
|
||||||
|
|
||||||
### Making a Progressive Web App
|
|
||||||
|
|
||||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
|
||||||
|
|
||||||
### Advanced Configuration
|
|
||||||
|
|
||||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
|
||||||
|
|
||||||
### Deployment
|
|
||||||
|
|
||||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
|
||||||
|
|
||||||
### `yarn build` fails to minify
|
|
||||||
|
|
||||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
|
||||||
|
5
crowdin.yml
Normal file
5
crowdin.yml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
project_id: 610631
|
||||||
|
preserve_hierarchy: true
|
||||||
|
files:
|
||||||
|
- source: src/lang.json
|
||||||
|
translation: src/translations/%locale_with_underscore%.json
|
55
package.json
55
package.json
@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "stream_ui",
|
"name": "stream_ui",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emoji-mart/data": "^1.1.2",
|
"@emoji-mart/data": "^1.1.2",
|
||||||
"@emoji-mart/react": "^1.1.1",
|
"@emoji-mart/react": "^1.1.1",
|
||||||
@ -16,8 +15,8 @@
|
|||||||
"@react-hook/resize-observer": "^1.2.6",
|
"@react-hook/resize-observer": "^1.2.6",
|
||||||
"@scure/base": "^1.1.1",
|
"@scure/base": "^1.1.1",
|
||||||
"@snort/shared": "^1.0.4",
|
"@snort/shared": "^1.0.4",
|
||||||
"@snort/system": "^1.0.16",
|
"@snort/system": "^1.0.17",
|
||||||
"@snort/system-react": "^1.0.11",
|
"@snort/system-react": "^1.0.12",
|
||||||
"@szhsin/react-menu": "^4.0.2",
|
"@szhsin/react-menu": "^4.0.2",
|
||||||
"@testing-library/jest-dom": "^5.14.1",
|
"@testing-library/jest-dom": "^5.14.1",
|
||||||
"@testing-library/react": "^13.0.0",
|
"@testing-library/react": "^13.0.0",
|
||||||
@ -39,22 +38,26 @@
|
|||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"react-intersection-observer": "^9.5.1",
|
"react-intersection-observer": "^9.5.1",
|
||||||
|
"react-intl": "^6.4.4",
|
||||||
"react-markdown": "^8.0.7",
|
"react-markdown": "^8.0.7",
|
||||||
"react-router-dom": "^6.13.0",
|
"react-router-dom": "^6.13.0",
|
||||||
"react-tag-input-component": "^2.0.2",
|
"react-tag-input-component": "^2.0.2",
|
||||||
"semantic-sdp": "^3.26.2",
|
"semantic-sdp": "^3.26.3",
|
||||||
"usehooks-ts": "^2.9.1",
|
"usehooks-ts": "^2.9.1",
|
||||||
"web-vitals": "^2.1.0",
|
"web-vitals": "^2.1.0",
|
||||||
"webrtc-adapter": "^8.2.3",
|
"webrtc-adapter": "^8.2.3",
|
||||||
"workbox-core": "^7.0.0",
|
"workbox-core": "^7.0.0",
|
||||||
|
"workbox-precaching": "^7.0.0",
|
||||||
"workbox-routing": "^7.0.0",
|
"workbox-routing": "^7.0.0",
|
||||||
"workbox-strategies": "^7.0.0"
|
"workbox-strategies": "^7.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "webpack serve",
|
"start": "webpack serve --node-env=development --mode=development",
|
||||||
"build": "webpack --node-env=production",
|
"build": "webpack --node-env=production --mode=production",
|
||||||
"deploy": "__XXX='false' && yarn build && npx wrangler pages publish --project-name nostr-live build",
|
"deploy": "__XXX='false' && yarn build && npx wrangler pages publish --project-name nostr-live build",
|
||||||
"deploy:xxzap": "__XXX='true' && yarn build && npx wrangler pages publish --project-name xxzap build"
|
"deploy:xxzap": "__XXX='true' && yarn build && npx wrangler pages publish --project-name xxzap build",
|
||||||
|
"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"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"extends": [
|
||||||
@ -77,29 +80,26 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.22.9",
|
"@babel/core": "^7.22.11",
|
||||||
"@babel/plugin-syntax-import-assertions": "^7.20.0",
|
"@babel/plugin-syntax-import-assertions": "^7.20.0",
|
||||||
"@babel/preset-env": "^7.21.5",
|
"@babel/preset-env": "^7.21.5",
|
||||||
"@babel/preset-react": "^7.18.6",
|
"@babel/preset-react": "^7.18.6",
|
||||||
"@babel/preset-typescript": "^7.22.5",
|
"@formatjs/cli": "^6.1.3",
|
||||||
"@formatjs/cli": "^6.0.1",
|
"@formatjs/ts-transformer": "^3.13.3",
|
||||||
"@formatjs/ts-transformer": "^3.13.1",
|
|
||||||
"@testing-library/dom": "^9.3.1",
|
"@testing-library/dom": "^9.3.1",
|
||||||
"@types/lodash": "^4.14.195",
|
"@types/lodash": "^4.14.195",
|
||||||
"@types/lodash.uniqby": "^4.7.7",
|
"@types/lodash.uniqby": "^4.7.7",
|
||||||
"@types/react": "^18.2.15",
|
"@types/react": "^18.2.21",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^18.2.7",
|
||||||
"@types/react-helmet": "^6.1.6",
|
"@types/react-helmet": "^6.1.6",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.1.0",
|
"@typescript-eslint/eslint-plugin": "^6.4.1",
|
||||||
"@typescript-eslint/parser": "^6.1.0",
|
"@typescript-eslint/parser": "^6.4.1",
|
||||||
"@webbtc/webln-types": "^1.0.12",
|
"@webbtc/webln-types": "^1.0.12",
|
||||||
"babel-loader": "^9.1.2",
|
"babel-loader": "^9.1.3",
|
||||||
"babel-plugin-formatjs": "^10.5.3",
|
|
||||||
"copy-webpack-plugin": "^11.0.0",
|
"copy-webpack-plugin": "^11.0.0",
|
||||||
"css-loader": "^6.7.3",
|
"css-loader": "^6.8.1",
|
||||||
"css-minimizer-webpack-plugin": "^5.0.0",
|
"css-minimizer-webpack-plugin": "^5.0.0",
|
||||||
"eslint": "^8.45.0",
|
"eslint": "^8.48.0",
|
||||||
"eslint-plugin-formatjs": "^4.10.1",
|
|
||||||
"eslint-webpack-plugin": "^4.0.1",
|
"eslint-webpack-plugin": "^4.0.1",
|
||||||
"html-webpack-plugin": "^5.5.1",
|
"html-webpack-plugin": "^5.5.1",
|
||||||
"mini-css-extract-plugin": "^2.7.5",
|
"mini-css-extract-plugin": "^2.7.5",
|
||||||
@ -108,12 +108,17 @@
|
|||||||
"source-map-loader": "^4.0.1",
|
"source-map-loader": "^4.0.1",
|
||||||
"terser-webpack-plugin": "^5.3.9",
|
"terser-webpack-plugin": "^5.3.9",
|
||||||
"ts-loader": "^9.4.4",
|
"ts-loader": "^9.4.4",
|
||||||
"typescript": "^5.1.3",
|
"typescript": "^5.2.2",
|
||||||
"webpack": "^5.82.1",
|
"webpack": "^5.88.2",
|
||||||
"webpack-bundle-analyzer": "^4.8.0",
|
"webpack-bundle-analyzer": "^4.8.0",
|
||||||
"webpack-cli": "^5.1.1",
|
"webpack-cli": "^5.1.4",
|
||||||
"webpack-dev-server": "^4.15.0",
|
"webpack-dev-server": "^4.15.1",
|
||||||
"workbox-webpack-plugin": "^6.5.4"
|
"workbox-webpack-plugin": "^7.0.0"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@3.6.1"
|
"packageManager": "yarn@3.6.3",
|
||||||
|
"prettier": {
|
||||||
|
"printWidth": 120,
|
||||||
|
"bracketSameLine": true,
|
||||||
|
"arrowParens": "avoid"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
import "./event.css";
|
import "./event.css";
|
||||||
|
|
||||||
import {
|
import { type NostrLink, type NostrEvent as NostrEventType, EventKind } from "@snort/system";
|
||||||
type NostrLink,
|
|
||||||
type NostrEvent as NostrEventType,
|
|
||||||
EventKind,
|
|
||||||
} from "@snort/system";
|
|
||||||
|
|
||||||
import { Icon } from "element/icon";
|
import { Icon } from "element/icon";
|
||||||
import { Goal } from "element/goal";
|
import { Goal } from "element/goal";
|
||||||
|
@ -2,8 +2,7 @@ import "./async-button.css";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Spinner from "element/spinner";
|
import Spinner from "element/spinner";
|
||||||
|
|
||||||
interface AsyncButtonProps
|
interface AsyncButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onClick(e: React.MouseEvent): Promise<void> | void;
|
onClick(e: React.MouseEvent): Promise<void> | void;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
@ -29,15 +28,8 @@ export default function AsyncButton(props: AsyncButtonProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button type="button" disabled={loading || props.disabled} {...props} onClick={handle}>
|
||||||
type="button"
|
<span style={{ visibility: loading ? "hidden" : "visible" }}>{props.children}</span>
|
||||||
disabled={loading || props.disabled}
|
|
||||||
{...props}
|
|
||||||
onClick={handle}
|
|
||||||
>
|
|
||||||
<span style={{ visibility: loading ? "hidden" : "visible" }}>
|
|
||||||
{props.children}
|
|
||||||
</span>
|
|
||||||
{loading && (
|
{loading && (
|
||||||
<span className="spinner-wrapper">
|
<span className="spinner-wrapper">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
|
@ -1,17 +1,5 @@
|
|||||||
import { MetadataCache } from "@snort/system";
|
import { MetadataCache } from "@snort/system";
|
||||||
|
|
||||||
export function Avatar({
|
export function Avatar({ user, avatarClassname }: { user: MetadataCache; avatarClassname: string }) {
|
||||||
user,
|
return <img className={avatarClassname} alt={user?.name || user?.pubkey} src={user?.picture ?? ""} />;
|
||||||
avatarClassname,
|
|
||||||
}: {
|
|
||||||
user: MetadataCache;
|
|
||||||
avatarClassname: string;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
className={avatarClassname}
|
|
||||||
alt={user?.name || user?.pubkey}
|
|
||||||
src={user?.picture ?? ""}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -12,9 +12,7 @@ export function Badge({ ev }: { ev: NostrEvent }) {
|
|||||||
<img className="badge-thumbnail" src={thumb || image} alt={name} />
|
<img className="badge-thumbnail" src={thumb || image} alt={name} />
|
||||||
<div className="badge-details">
|
<div className="badge-details">
|
||||||
<h4 className="badge-name">{name}</h4>
|
<h4 className="badge-name">{name}</h4>
|
||||||
{description?.length > 0 && (
|
{description?.length > 0 && <p className="badge-description">{description}</p>}
|
||||||
<p className="badge-description">{description}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,12 +1,7 @@
|
|||||||
import { useUserProfile } from "@snort/system-react";
|
import { useUserProfile, SnortContext } from "@snort/system-react";
|
||||||
import { NostrEvent, parseZap, EventKind } from "@snort/system";
|
import { NostrEvent, parseZap, EventKind } from "@snort/system";
|
||||||
import React, { useRef, useState, useMemo } from "react";
|
import React, { useRef, useState, useMemo, useContext } from "react";
|
||||||
import {
|
import { useMediaQuery, useHover, useOnClickOutside, useIntersectionObserver } from "usehooks-ts";
|
||||||
useMediaQuery,
|
|
||||||
useHover,
|
|
||||||
useOnClickOutside,
|
|
||||||
useIntersectionObserver,
|
|
||||||
} from "usehooks-ts";
|
|
||||||
|
|
||||||
import { EmojiPicker } from "element/emoji-picker";
|
import { EmojiPicker } from "element/emoji-picker";
|
||||||
import { Icon } from "element/icon";
|
import { Icon } from "element/icon";
|
||||||
@ -20,7 +15,6 @@ import { useLogin } from "hooks/login";
|
|||||||
import { formatSats } from "number";
|
import { formatSats } from "number";
|
||||||
import { findTag } from "utils";
|
import { findTag } from "utils";
|
||||||
import type { Badge, Emoji, EmojiPack } from "types";
|
import type { Badge, Emoji, EmojiPack } from "types";
|
||||||
import { System } from "index";
|
|
||||||
|
|
||||||
function emojifyReaction(reaction: string) {
|
function emojifyReaction(reaction: string) {
|
||||||
if (reaction === "+") {
|
if (reaction === "+") {
|
||||||
@ -60,36 +54,31 @@ export function ChatMessage({
|
|||||||
const [showZapDialog, setShowZapDialog] = useState(false);
|
const [showZapDialog, setShowZapDialog] = useState(false);
|
||||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const profile = useUserProfile(
|
const profile = useUserProfile(inView?.isIntersecting ? ev.pubkey : undefined);
|
||||||
System,
|
const shouldShowMuteButton = ev.pubkey !== streamer && ev.pubkey !== login?.pubkey;
|
||||||
inView?.isIntersecting ? ev.pubkey : undefined
|
|
||||||
);
|
|
||||||
const shouldShowMuteButton =
|
|
||||||
ev.pubkey !== streamer && ev.pubkey !== login?.pubkey;
|
|
||||||
const zapTarget = profile?.lud16 ?? profile?.lud06;
|
const zapTarget = profile?.lud16 ?? profile?.lud06;
|
||||||
|
const system = useContext(SnortContext);
|
||||||
const zaps = useMemo(() => {
|
const zaps = useMemo(() => {
|
||||||
return reactions
|
return reactions
|
||||||
.filter((a) => a.kind === EventKind.ZapReceipt)
|
.filter(a => a.kind === EventKind.ZapReceipt)
|
||||||
.map((a) => parseZap(a, System.ProfileLoader.Cache))
|
.map(a => parseZap(a, system.ProfileLoader.Cache))
|
||||||
.filter((a) => a && a.valid);
|
.filter(a => a && a.valid);
|
||||||
}, [reactions]);
|
}, [reactions]);
|
||||||
const emojiReactions = useMemo(() => {
|
const emojiReactions = useMemo(() => {
|
||||||
const emojified = reactions
|
const emojified = reactions
|
||||||
.filter((e) => e.kind === EventKind.Reaction && findTag(e, "e") === ev.id)
|
.filter(e => e.kind === EventKind.Reaction && findTag(e, "e") === ev.id)
|
||||||
.map((ev) => emojifyReaction(ev.content));
|
.map(ev => emojifyReaction(ev.content));
|
||||||
return [...new Set(emojified)];
|
return [...new Set(emojified)];
|
||||||
}, [ev, reactions]);
|
}, [ev, reactions]);
|
||||||
const emojiNames = emojiPacks.map((p) => p.emojis).flat();
|
const emojiNames = emojiPacks.map(p => p.emojis).flat();
|
||||||
|
|
||||||
const hasReactions = emojiReactions.length > 0;
|
const hasReactions = emojiReactions.length > 0;
|
||||||
const totalZaps = useMemo(() => {
|
const totalZaps = useMemo(() => {
|
||||||
const messageZaps = zaps.filter((z) => z.event === ev.id);
|
const messageZaps = zaps.filter(z => z.event === ev.id);
|
||||||
return messageZaps.reduce((acc, z) => acc + z.amount, 0);
|
return messageZaps.reduce((acc, z) => acc + z.amount, 0);
|
||||||
}, [zaps, ev]);
|
}, [zaps, ev]);
|
||||||
const hasZaps = totalZaps > 0;
|
const hasZaps = totalZaps > 0;
|
||||||
const awardedBadges = badges.filter(
|
const awardedBadges = badges.filter(b => b.awardees.has(ev.pubkey) && b.accepted.has(ev.pubkey));
|
||||||
(b) => b.awardees.has(ev.pubkey) && b.accepted.has(ev.pubkey)
|
|
||||||
);
|
|
||||||
|
|
||||||
useOnClickOutside(ref, () => {
|
useOnClickOutside(ref, () => {
|
||||||
setShowZapDialog(false);
|
setShowZapDialog(false);
|
||||||
@ -100,7 +89,7 @@ export function ChatMessage({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function getEmojiById(id: string) {
|
function getEmojiById(id: string) {
|
||||||
return emojiNames.find((e) => e.at(1) === id);
|
return emojiNames.find(e => e.at(1) === id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onEmojiSelect(emoji: Emoji) {
|
async function onEmojiSelect(emoji: Emoji) {
|
||||||
@ -114,7 +103,7 @@ export function ChatMessage({
|
|||||||
} else if (emoji.id) {
|
} else if (emoji.id) {
|
||||||
const e = getEmojiById(emoji.id);
|
const e = getEmojiById(emoji.id);
|
||||||
if (e) {
|
if (e) {
|
||||||
reply = await pub?.generic((eb) => {
|
reply = await pub?.generic(eb => {
|
||||||
return eb
|
return eb
|
||||||
.kind(EventKind.Reaction)
|
.kind(EventKind.Reaction)
|
||||||
.content(`:${emoji.id}:`)
|
.content(`:${emoji.id}:`)
|
||||||
@ -126,7 +115,7 @@ export function ChatMessage({
|
|||||||
}
|
}
|
||||||
if (reply) {
|
if (reply) {
|
||||||
console.debug(reply);
|
console.debug(reply);
|
||||||
System.BroadcastEvent(reply);
|
system.BroadcastEvent(reply);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
//ignore
|
//ignore
|
||||||
@ -148,23 +137,15 @@ export function ChatMessage({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div className={`message${streamer === ev.pubkey ? " streamer" : ""}`} ref={ref}>
|
||||||
className={`message${streamer === ev.pubkey ? " streamer" : ""}`}
|
|
||||||
ref={ref}
|
|
||||||
>
|
|
||||||
<Profile
|
<Profile
|
||||||
icon={
|
icon={
|
||||||
ev.pubkey === streamer ? (
|
ev.pubkey === streamer ? (
|
||||||
<Icon name="signal" size={16} />
|
<Icon name="signal" size={16} />
|
||||||
) : (
|
) : (
|
||||||
awardedBadges.map((badge) => {
|
awardedBadges.map(badge => {
|
||||||
return (
|
return (
|
||||||
<img
|
<img key={badge.name} className="badge-icon" src={badge.thumb || badge.image} alt={badge.name} />
|
||||||
key={badge.name}
|
|
||||||
className="badge-icon"
|
|
||||||
src={badge.thumb || badge.image}
|
|
||||||
alt={badge.name}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@ -172,11 +153,7 @@ export function ChatMessage({
|
|||||||
pubkey={ev.pubkey}
|
pubkey={ev.pubkey}
|
||||||
profile={profile}
|
profile={profile}
|
||||||
/>
|
/>
|
||||||
<Text
|
<Text tags={ev.tags} content={ev.content} customComponents={customComponents} />
|
||||||
tags={ev.tags}
|
|
||||||
content={ev.content}
|
|
||||||
customComponents={customComponents}
|
|
||||||
/>
|
|
||||||
{(hasReactions || hasZaps) && (
|
{(hasReactions || hasZaps) && (
|
||||||
<div className="message-reactions">
|
<div className="message-reactions">
|
||||||
{hasZaps && (
|
{hasZaps && (
|
||||||
@ -185,9 +162,8 @@ export function ChatMessage({
|
|||||||
<span className="zap-pill-amount">{formatSats(totalZaps)}</span>
|
<span className="zap-pill-amount">{formatSats(totalZaps)}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{emojiReactions.map((e) => {
|
{emojiReactions.map(e => {
|
||||||
const isCustomEmojiReaction =
|
const isCustomEmojiReaction = e.length > 1 && e.startsWith(":") && e.endsWith(":");
|
||||||
e.length > 1 && e.startsWith(":") && e.endsWith(":");
|
|
||||||
const emojiName = e.replace(/:/g, "");
|
const emojiName = e.replace(/:/g, "");
|
||||||
const emoji = isCustomEmojiReaction && getEmojiById(emojiName);
|
const emoji = isCustomEmojiReaction && getEmojiById(emojiName);
|
||||||
return (
|
return (
|
||||||
@ -217,11 +193,9 @@ export function ChatMessage({
|
|||||||
top: topOffset ? topOffset - 12 : 0,
|
top: topOffset ? topOffset - 12 : 0,
|
||||||
left: leftOffset ? leftOffset - 32 : 0,
|
left: leftOffset ? leftOffset - 32 : 0,
|
||||||
opacity: showZapDialog || isHovering ? 1 : 0,
|
opacity: showZapDialog || isHovering ? 1 : 0,
|
||||||
pointerEvents:
|
pointerEvents: showZapDialog || isHovering ? "auto" : "none",
|
||||||
showZapDialog || isHovering ? "auto" : "none",
|
|
||||||
}
|
}
|
||||||
}
|
}>
|
||||||
>
|
|
||||||
{zapTarget && (
|
{zapTarget && (
|
||||||
<SendZapsDialog
|
<SendZapsDialog
|
||||||
lnurl={zapTarget}
|
lnurl={zapTarget}
|
||||||
|
@ -2,6 +2,7 @@ import "./collapsible.css";
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import * as Dialog from "@radix-ui/react-dialog";
|
import * as Dialog from "@radix-ui/react-dialog";
|
||||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||||
|
|
||||||
@ -31,7 +32,7 @@ export function MediaURL({ url, children }: MediaURLProps) {
|
|||||||
</div>
|
</div>
|
||||||
<Dialog.Close asChild>
|
<Dialog.Close asChild>
|
||||||
<button className="btn delete-button" aria-label="Close">
|
<button className="btn delete-button" aria-label="Close">
|
||||||
Close
|
<FormattedMessage defaultMessage="Close" />
|
||||||
</button>
|
</button>
|
||||||
</Dialog.Close>
|
</Dialog.Close>
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
@ -46,29 +47,19 @@ export function CollapsibleEvent({ link }: { link: NostrLink }) {
|
|||||||
const author = event?.pubkey || link.author;
|
const author = event?.pubkey || link.author;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Collapsible.Root
|
<Collapsible.Root className="collapsible" open={open} onOpenChange={setOpen}>
|
||||||
className="collapsible"
|
|
||||||
open={open}
|
|
||||||
onOpenChange={setOpen}
|
|
||||||
>
|
|
||||||
<div className="collapsed-event">
|
<div className="collapsed-event">
|
||||||
<div className="collapsed-event-header">
|
<div className="collapsed-event-header">
|
||||||
{event && <EventIcon kind={event.kind} />}
|
{event && <EventIcon kind={event.kind} />}
|
||||||
{author && <Mention pubkey={author} />}
|
{author && <Mention pubkey={author} />}
|
||||||
</div>
|
</div>
|
||||||
<Collapsible.Trigger asChild>
|
<Collapsible.Trigger asChild>
|
||||||
<button
|
<button className={`${open ? "btn btn-small delete-button" : "btn btn-small"}`}>
|
||||||
className={`${
|
{open ? <FormattedMessage defaultMessage="Hide" /> : <FormattedMessage defaultMessage="Show" />}
|
||||||
open ? "btn btn-small delete-button" : "btn btn-small"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{open ? "Hide" : "Show"}
|
|
||||||
</button>
|
</button>
|
||||||
</Collapsible.Trigger>
|
</Collapsible.Trigger>
|
||||||
</div>
|
</div>
|
||||||
<Collapsible.Content>
|
<Collapsible.Content>{open && event && <NostrEvent ev={event} />}</Collapsible.Content>
|
||||||
{open && event && <NostrEvent ev={event} />}
|
|
||||||
</Collapsible.Content>
|
|
||||||
</Collapsible.Root>
|
</Collapsible.Root>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
export function isContentWarningAccepted() {
|
export function isContentWarningAccepted() {
|
||||||
@ -17,14 +18,18 @@ export function ContentWarningOverlay() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fullscreen-exclusive age-check">
|
<div className="fullscreen-exclusive age-check">
|
||||||
<h1>Sexually explicit material ahead!</h1>
|
<h1>
|
||||||
<h2>Confirm your age</h2>
|
<FormattedMessage defaultMessage="Sexually explicit material ahead!" />
|
||||||
|
</h1>
|
||||||
|
<h2>
|
||||||
|
<FormattedMessage defaultMessage="Confirm your age" />
|
||||||
|
</h2>
|
||||||
<div className="flex g24">
|
<div className="flex g24">
|
||||||
<button className="btn btn-warning" onClick={grownUp}>
|
<button className="btn btn-warning" onClick={grownUp}>
|
||||||
Yes, I am over 18
|
<FormattedMessage defaultMessage="Yes, I am over 18" />
|
||||||
</button>
|
</button>
|
||||||
<button className="btn" onClick={() => navigate("/")}>
|
<button className="btn" onClick={() => navigate("/")}>
|
||||||
No, I am under 18
|
<FormattedMessage defaultMessage="No, I am under 18" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -11,26 +11,13 @@ export interface CopyProps {
|
|||||||
export default function Copy({ text, maxSize = 32, className, hideText }: CopyProps) {
|
export default function Copy({ text, maxSize = 32, className, hideText }: CopyProps) {
|
||||||
const { copy, copied } = useCopy();
|
const { copy, copied } = useCopy();
|
||||||
const sliceLength = maxSize / 2;
|
const sliceLength = maxSize / 2;
|
||||||
const trimmed =
|
const trimmed = text.length > maxSize ? `${text.slice(0, sliceLength)}...${text.slice(-sliceLength)}` : text;
|
||||||
text.length > maxSize
|
|
||||||
? `${text.slice(0, sliceLength)}...${text.slice(-sliceLength)}`
|
|
||||||
: text;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={`copy${className ? ` ${className}` : ""}`} onClick={() => copy(text)}>
|
||||||
className={`copy${className ? ` ${className}` : ""}`}
|
|
||||||
onClick={() => copy(text)}
|
|
||||||
>
|
|
||||||
{!hideText && <span className="body">{trimmed}</span>}
|
{!hideText && <span className="body">{trimmed}</span>}
|
||||||
<span
|
<span className="icon" style={{ color: copied ? "var(--success)" : "var(--highlight)" }}>
|
||||||
className="icon"
|
{copied ? <Icon name="check" size={14} /> : <Icon name="copy" size={14} />}
|
||||||
style={{ color: copied ? "var(--success)" : "var(--highlight)" }}
|
|
||||||
>
|
|
||||||
{copied ? (
|
|
||||||
<Icon name="check" size={14} />
|
|
||||||
) : (
|
|
||||||
<Icon name="copy" size={14} />
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -8,28 +8,24 @@ import { findTag } from "utils";
|
|||||||
import { USER_EMOJIS } from "const";
|
import { USER_EMOJIS } from "const";
|
||||||
import { Login, System } from "index";
|
import { Login, System } from "index";
|
||||||
import type { EmojiPack as EmojiPackType } from "types";
|
import type { EmojiPack as EmojiPackType } from "types";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
export function EmojiPack({ ev }: { ev: NostrEvent }) {
|
export function EmojiPack({ ev }: { ev: NostrEvent }) {
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const name = findTag(ev, "d");
|
const name = findTag(ev, "d");
|
||||||
const isUsed = login?.emojis.find(
|
const isUsed = login?.emojis.find(e => e.author === ev.pubkey && e.name === name);
|
||||||
(e) => e.author === ev.pubkey && e.name === name
|
const emoji = ev.tags.filter(e => e.at(0) === "emoji");
|
||||||
);
|
|
||||||
const emoji = ev.tags.filter((e) => e.at(0) === "emoji");
|
|
||||||
|
|
||||||
async function toggleEmojiPack() {
|
async function toggleEmojiPack() {
|
||||||
let newPacks = [] as EmojiPackType[];
|
let newPacks = [] as EmojiPackType[];
|
||||||
if (isUsed) {
|
if (isUsed) {
|
||||||
newPacks =
|
newPacks = login?.emojis.filter(e => e.author !== ev.pubkey && e.name !== name) ?? [];
|
||||||
login?.emojis.filter(
|
|
||||||
(e) => e.author !== ev.pubkey && e.name !== name
|
|
||||||
) ?? [];
|
|
||||||
} else {
|
} else {
|
||||||
newPacks = [...(login?.emojis ?? []), toEmojiPack(ev)];
|
newPacks = [...(login?.emojis ?? []), toEmojiPack(ev)];
|
||||||
}
|
}
|
||||||
const pub = login?.publisher();
|
const pub = login?.publisher();
|
||||||
if (pub) {
|
if (pub) {
|
||||||
const ev = await pub.generic((eb) => {
|
const ev = await pub.generic(eb => {
|
||||||
eb.kind(USER_EMOJIS).content("");
|
eb.kind(USER_EMOJIS).content("");
|
||||||
for (const e of newPacks) {
|
for (const e of newPacks) {
|
||||||
eb.tag(["a", e.address]);
|
eb.tag(["a", e.address]);
|
||||||
@ -48,17 +44,14 @@ export function EmojiPack({ ev }: { ev: NostrEvent }) {
|
|||||||
<h4>{name}</h4>
|
<h4>{name}</h4>
|
||||||
{login?.pubkey && (
|
{login?.pubkey && (
|
||||||
<AsyncButton
|
<AsyncButton
|
||||||
className={`btn btn-small btn-primary ${
|
className={`btn btn-small btn-primary ${isUsed ? "delete-button" : ""}`}
|
||||||
isUsed ? "delete-button" : ""
|
onClick={toggleEmojiPack}>
|
||||||
}`}
|
{isUsed ? <FormattedMessage defaultMessage="Remove" /> : <FormattedMessage defaultMessage="Add" />}
|
||||||
onClick={toggleEmojiPack}
|
|
||||||
>
|
|
||||||
{isUsed ? "Remove" : "Add"}
|
|
||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="emoji-pack-emojis">
|
<div className="emoji-pack-emojis">
|
||||||
{emoji.map((e) => {
|
{emoji.map(e => {
|
||||||
const [, name, image] = e;
|
const [, name, image] = e;
|
||||||
return (
|
return (
|
||||||
<div className="emoji-definition">
|
<div className="emoji-definition">
|
||||||
|
@ -22,11 +22,11 @@ export function EmojiPicker({
|
|||||||
height = 300,
|
height = 300,
|
||||||
ref,
|
ref,
|
||||||
}: EmojiPickerProps) {
|
}: EmojiPickerProps) {
|
||||||
const customEmojiList = emojiPacks.map((pack) => {
|
const customEmojiList = emojiPacks.map(pack => {
|
||||||
return {
|
return {
|
||||||
id: pack.address,
|
id: pack.address,
|
||||||
name: pack.name,
|
name: pack.name,
|
||||||
emojis: pack.emojis.map((e) => {
|
emojis: pack.emojis.map(e => {
|
||||||
const [, name, url] = e;
|
const [, name, url] = e;
|
||||||
return {
|
return {
|
||||||
id: name,
|
id: name,
|
||||||
@ -45,8 +45,7 @@ export function EmojiPicker({
|
|||||||
left: leftOffset,
|
left: leftOffset,
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
}}
|
}}
|
||||||
ref={ref}
|
ref={ref}>
|
||||||
>
|
|
||||||
<style>
|
<style>
|
||||||
{`
|
{`
|
||||||
em-emoji-picker { max-height: ${height}px; }
|
em-emoji-picker { max-height: ${height}px; }
|
||||||
|
@ -11,16 +11,10 @@ export function Emoji({ name, url }: EmojiProps) {
|
|||||||
return <img alt={name} src={url} className="emoji" />;
|
return <img alt={name} src={url} className="emoji" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Emojify({
|
export function Emojify({ content, emoji }: { content: string; emoji: EmojiTag[] }) {
|
||||||
content,
|
|
||||||
emoji,
|
|
||||||
}: {
|
|
||||||
content: string;
|
|
||||||
emoji: EmojiTag[];
|
|
||||||
}) {
|
|
||||||
const emojified = useMemo(() => {
|
const emojified = useMemo(() => {
|
||||||
return content.split(/:(\w+):/g).map((i) => {
|
return content.split(/:(\w+):/g).map(i => {
|
||||||
const t = emoji.find((t) => t[1] === i);
|
const t = emoji.find(t => t[1] === i);
|
||||||
if (t) {
|
if (t) {
|
||||||
return <Emoji name={t[1]} url={t[2]} />;
|
return <Emoji name={t[1]} url={t[2]} />;
|
||||||
} else {
|
} else {
|
||||||
|
@ -19,19 +19,10 @@ interface ExternalIconLinkProps extends Omit<ExternalLinkProps, "children"> {
|
|||||||
size?: number;
|
size?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ExternalIconLink({
|
export function ExternalIconLink({ size = 32, href, ...rest }: ExternalIconLinkProps) {
|
||||||
size = 32,
|
|
||||||
href,
|
|
||||||
...rest
|
|
||||||
}: ExternalIconLinkProps) {
|
|
||||||
return (
|
return (
|
||||||
<span style={{ cursor: "pointer" }}>
|
<span style={{ cursor: "pointer" }}>
|
||||||
<Icon
|
<Icon name="link" size={size} onClick={() => window.open(href, "_blank")} {...rest} />
|
||||||
name="link"
|
|
||||||
size={size}
|
|
||||||
onClick={() => window.open(href, "_blank")}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import "./file-uploader.css";
|
|||||||
import type { ChangeEvent } from "react";
|
import type { ChangeEvent } from "react";
|
||||||
import { VoidApi } from "@void-cat/api";
|
import { VoidApi } from "@void-cat/api";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
const voidCatHost = "https://void.cat";
|
const voidCatHost = "https://void.cat";
|
||||||
const fileExtensionRegex = /\.([\w]{1,7})$/i;
|
const fileExtensionRegex = /\.([\w]{1,7})$/i;
|
||||||
@ -23,9 +24,7 @@ async function voidCatUpload(file: File | Blob): Promise<UploadResult> {
|
|||||||
if (rsp.file?.metadata?.mimeType === "image/webp") {
|
if (rsp.file?.metadata?.mimeType === "image/webp") {
|
||||||
ext = ["", "webp"];
|
ext = ["", "webp"];
|
||||||
}
|
}
|
||||||
const resultUrl =
|
const resultUrl = rsp.file?.metadata?.url ?? `${voidCatHost}/d/${rsp.file?.id}${ext ? `.${ext[1]}` : ""}`;
|
||||||
rsp.file?.metadata?.url ??
|
|
||||||
`${voidCatHost}/d/${rsp.file?.id}${ext ? `.${ext[1]}` : ""}`;
|
|
||||||
|
|
||||||
const ret = {
|
const ret = {
|
||||||
url: resultUrl,
|
url: resultUrl,
|
||||||
@ -45,11 +44,7 @@ interface FileUploaderProps {
|
|||||||
onFileUpload(url: string): void;
|
onFileUpload(url: string): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FileUploader({
|
export function FileUploader({ defaultImage, onClear, onFileUpload }: FileUploaderProps) {
|
||||||
defaultImage,
|
|
||||||
onClear,
|
|
||||||
onFileUpload,
|
|
||||||
}: FileUploaderProps) {
|
|
||||||
const [img, setImg] = useState<string>(defaultImage ?? "");
|
const [img, setImg] = useState<string>(defaultImage ?? "");
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
|
|
||||||
@ -88,7 +83,7 @@ export function FileUploader({
|
|||||||
<div className="file-uploader-preview">
|
<div className="file-uploader-preview">
|
||||||
{img?.length > 0 && (
|
{img?.length > 0 && (
|
||||||
<button className="btn btn-primary clear-button" onClick={clearImage}>
|
<button className="btn btn-primary clear-button" onClick={clearImage}>
|
||||||
Clear
|
<FormattedMessage defaultMessage="Clear" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{img && <img className="image-preview" src={img} />}
|
{img && <img className="image-preview" src={img} />}
|
||||||
|
@ -3,26 +3,21 @@ import { EventKind } from "@snort/system";
|
|||||||
import { useLogin } from "hooks/login";
|
import { useLogin } from "hooks/login";
|
||||||
import AsyncButton from "element/async-button";
|
import AsyncButton from "element/async-button";
|
||||||
import { Login, System } from "index";
|
import { Login, System } from "index";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
export function LoggedInFollowButton({
|
export function LoggedInFollowButton({ tag, value }: { tag: "p" | "t"; value: string }) {
|
||||||
tag,
|
|
||||||
value,
|
|
||||||
}: {
|
|
||||||
tag: "p" | "t";
|
|
||||||
value: string;
|
|
||||||
}) {
|
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
if (!login) return;
|
if (!login) return;
|
||||||
|
|
||||||
const { tags, content, timestamp } = login.follows;
|
const { tags, content, timestamp } = login.follows;
|
||||||
const follows = tags.filter((t) => t.at(0) === tag);
|
const follows = tags.filter(t => t.at(0) === tag);
|
||||||
const isFollowing = follows.find((t) => t.at(1) === value);
|
const isFollowing = follows.find(t => t.at(1) === value);
|
||||||
|
|
||||||
async function unfollow() {
|
async function unfollow() {
|
||||||
const pub = login?.publisher();
|
const pub = login?.publisher();
|
||||||
if (pub) {
|
if (pub) {
|
||||||
const newFollows = tags.filter((t) => t.at(1) !== value);
|
const newFollows = tags.filter(t => t.at(1) !== value);
|
||||||
const ev = await pub.generic((eb) => {
|
const ev = await pub.generic(eb => {
|
||||||
eb.kind(EventKind.ContactList).content(content ?? "");
|
eb.kind(EventKind.ContactList).content(content ?? "");
|
||||||
for (const t of newFollows) {
|
for (const t of newFollows) {
|
||||||
eb.tag(t);
|
eb.tag(t);
|
||||||
@ -39,7 +34,7 @@ export function LoggedInFollowButton({
|
|||||||
const pub = login?.publisher();
|
const pub = login?.publisher();
|
||||||
if (pub) {
|
if (pub) {
|
||||||
const newFollows = [...tags, [tag, value]];
|
const newFollows = [...tags, [tag, value]];
|
||||||
const ev = await pub.generic((eb) => {
|
const ev = await pub.generic(eb => {
|
||||||
eb.kind(EventKind.ContactList).content(content ?? "");
|
eb.kind(EventKind.ContactList).content(content ?? "");
|
||||||
for (const tag of newFollows) {
|
for (const tag of newFollows) {
|
||||||
eb.tag(tag);
|
eb.tag(tag);
|
||||||
@ -57,9 +52,8 @@ export function LoggedInFollowButton({
|
|||||||
disabled={timestamp ? timestamp === 0 : true}
|
disabled={timestamp ? timestamp === 0 : true}
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
onClick={isFollowing ? unfollow : follow}
|
onClick={isFollowing ? unfollow : follow}>
|
||||||
>
|
{isFollowing ? <FormattedMessage defaultMessage="Unfollow" /> : <FormattedMessage defaultMessage="Follow" />}
|
||||||
{isFollowing ? "Unfollow" : "Follow"}
|
|
||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -71,7 +65,5 @@ export function FollowTagButton({ tag }: { tag: string }) {
|
|||||||
|
|
||||||
export function FollowButton({ pubkey }: { pubkey: string }) {
|
export function FollowButton({ pubkey }: { pubkey: string }) {
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
return login?.pubkey ? (
|
return login?.pubkey ? <LoggedInFollowButton tag={"p"} value={pubkey} /> : null;
|
||||||
<LoggedInFollowButton tag={"p"} value={pubkey} />
|
|
||||||
) : null;
|
|
||||||
}
|
}
|
||||||
|
@ -12,11 +12,11 @@ import usePreviousValue from "hooks/usePreviousValue";
|
|||||||
import { SendZapsDialog } from "element/send-zap";
|
import { SendZapsDialog } from "element/send-zap";
|
||||||
import { useZaps } from "hooks/goals";
|
import { useZaps } from "hooks/goals";
|
||||||
import { getName } from "element/profile";
|
import { getName } from "element/profile";
|
||||||
import { System } from "index";
|
|
||||||
import { Icon } from "./icon";
|
import { Icon } from "./icon";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
export function Goal({ ev }: { ev: NostrEvent }) {
|
export function Goal({ ev }: { ev: NostrEvent }) {
|
||||||
const profile = useUserProfile(System, ev.pubkey);
|
const profile = useUserProfile(ev.pubkey);
|
||||||
const zapTarget = profile?.lud16 ?? profile?.lud06;
|
const zapTarget = profile?.lud16 ?? profile?.lud06;
|
||||||
const zaps = useZaps(ev, true);
|
const zaps = useZaps(ev, true);
|
||||||
const goalAmount = useMemo(() => {
|
const goalAmount = useMemo(() => {
|
||||||
@ -29,9 +29,7 @@ export function Goal({ ev }: { ev: NostrEvent }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const soFar = useMemo(() => {
|
const soFar = useMemo(() => {
|
||||||
return zaps
|
return zaps.filter(z => z.receiver === ev.pubkey && z.event === ev.id).reduce((acc, z) => acc + z.amount, 0);
|
||||||
.filter((z) => z.receiver === ev.pubkey && z.event === ev.id)
|
|
||||||
.reduce((acc, z) => acc + z.amount, 0);
|
|
||||||
}, [zaps]);
|
}, [zaps]);
|
||||||
|
|
||||||
const progress = Math.max(0, Math.min(100, (soFar / goalAmount) * 100));
|
const progress = Math.max(0, Math.min(100, (soFar / goalAmount) * 100));
|
||||||
@ -43,26 +41,18 @@ export function Goal({ ev }: { ev: NostrEvent }) {
|
|||||||
{ev.content.length > 0 && <p>{ev.content}</p>}
|
{ev.content.length > 0 && <p>{ev.content}</p>}
|
||||||
<div className={`progress-container ${isFinished ? "finished" : ""}`}>
|
<div className={`progress-container ${isFinished ? "finished" : ""}`}>
|
||||||
<Progress.Root className="progress-root" value={progress}>
|
<Progress.Root className="progress-root" value={progress}>
|
||||||
<Progress.Indicator
|
<Progress.Indicator className="progress-indicator" style={{ transform: `translateX(-${100 - progress}%)` }}>
|
||||||
className="progress-indicator"
|
{!isFinished && <span className="amount so-far">{formatSats(soFar)}</span>}
|
||||||
style={{ transform: `translateX(-${100 - progress}%)` }}
|
|
||||||
>
|
|
||||||
{!isFinished && (
|
|
||||||
<span className="amount so-far">{formatSats(soFar)}</span>
|
|
||||||
)}
|
|
||||||
</Progress.Indicator>
|
</Progress.Indicator>
|
||||||
<span className="amount target">Goal: {formatSats(goalAmount)}</span>
|
<span className="amount target">
|
||||||
|
<FormattedMessage defaultMessage="Goal: {amount}" values={{ amount: formatSats(goalAmount) }} />
|
||||||
|
</span>
|
||||||
</Progress.Root>
|
</Progress.Root>
|
||||||
<div className="zap-circle">
|
<div className="zap-circle">
|
||||||
<Icon
|
<Icon name="zap-filled" className={isFinished ? "goal-finished" : "goal-unfinished"} />
|
||||||
name="zap-filled"
|
|
||||||
className={isFinished ? "goal-finished" : "goal-unfinished"}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isFinished && previousValue === false && (
|
{isFinished && previousValue === false && <Confetti numberOfPieces={2100} recycle={false} />}
|
||||||
<Confetti numberOfPieces={2100} recycle={false} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -12,8 +12,7 @@ interface HyperTextProps {
|
|||||||
export function HyperText({ link, children }: HyperTextProps) {
|
export function HyperText({ link, children }: HyperTextProps) {
|
||||||
try {
|
try {
|
||||||
const url = new URL(link);
|
const url = new URL(link);
|
||||||
const extension =
|
const extension = FileExtensionRegex.test(url.pathname.toLowerCase()) && RegExp.$1;
|
||||||
FileExtensionRegex.test(url.pathname.toLowerCase()) && RegExp.$1;
|
|
||||||
|
|
||||||
if (extension) {
|
if (extension) {
|
||||||
switch (extension) {
|
switch (extension) {
|
||||||
@ -25,11 +24,7 @@ export function HyperText({ link, children }: HyperTextProps) {
|
|||||||
case "webp": {
|
case "webp": {
|
||||||
return (
|
return (
|
||||||
<MediaURL url={url}>
|
<MediaURL url={url}>
|
||||||
<img
|
<img src={url.toString()} alt={url.toString()} style={{ objectFit: "contain" }} />
|
||||||
src={url.toString()}
|
|
||||||
alt={url.toString()}
|
|
||||||
style={{ objectFit: "contain" }}
|
|
||||||
/>
|
|
||||||
</MediaURL>
|
</MediaURL>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -12,12 +12,7 @@ export function Icon(props: Props) {
|
|||||||
const href = `/icons.svg#` + props.name;
|
const href = `/icons.svg#` + props.name;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg width={size} height={size} className={props.className} onClick={props.onClick}>
|
||||||
width={size}
|
|
||||||
height={size}
|
|
||||||
className={props.className}
|
|
||||||
onClick={props.onClick}
|
|
||||||
>
|
|
||||||
<use href={href} />
|
<use href={href} />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
@ -203,15 +203,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.zap-container.big-zap:before {
|
.zap-container.big-zap:before {
|
||||||
background: linear-gradient(
|
background: linear-gradient(60deg, #2bd9ff, #8c8ded, #f838d9, #f83838, #ff902b, #ddf838);
|
||||||
60deg,
|
|
||||||
#2bd9ff,
|
|
||||||
#8c8ded,
|
|
||||||
#f838d9,
|
|
||||||
#f83838,
|
|
||||||
#ff902b,
|
|
||||||
#ddf838
|
|
||||||
);
|
|
||||||
animation: animatedgradient 3s ease alternate infinite;
|
animation: animatedgradient 3s ease alternate infinite;
|
||||||
background-size: 300% 300%;
|
background-size: 300% 300%;
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,5 @@
|
|||||||
import "./live-chat.css";
|
import "./live-chat.css";
|
||||||
import {
|
import { EventKind, NostrPrefix, NostrLink, ParsedZap, NostrEvent, parseZap, encodeTLV } from "@snort/system";
|
||||||
EventKind,
|
|
||||||
NostrPrefix,
|
|
||||||
NostrLink,
|
|
||||||
ParsedZap,
|
|
||||||
NostrEvent,
|
|
||||||
parseZap,
|
|
||||||
encodeTLV,
|
|
||||||
} from "@snort/system";
|
|
||||||
import { unixNow, unwrap } from "@snort/shared";
|
import { unixNow, unwrap } from "@snort/shared";
|
||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import uniqBy from "lodash.uniqby";
|
import uniqBy from "lodash.uniqby";
|
||||||
@ -32,6 +24,7 @@ import { formatSats } from "number";
|
|||||||
import { WEEK, LIVE_STREAM_CHAT } from "const";
|
import { WEEK, LIVE_STREAM_CHAT } from "const";
|
||||||
import { findTag, getTagValues, getHost } from "utils";
|
import { findTag, getTagValues, getHost } from "utils";
|
||||||
import { System } from "index";
|
import { System } from "index";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
export interface LiveChatOptions {
|
export interface LiveChatOptions {
|
||||||
canWrite?: boolean;
|
canWrite?: boolean;
|
||||||
@ -48,7 +41,7 @@ function BadgeAward({ ev }: { ev: NostrEvent }) {
|
|||||||
{event && <Badge ev={event} />}
|
{event && <Badge ev={event} />}
|
||||||
<p>awarded to</p>
|
<p>awarded to</p>
|
||||||
<div className="badge-awardees">
|
<div className="badge-awardees">
|
||||||
{awardees.map((pk) => (
|
{awardees.map(pk => (
|
||||||
<Profile key={pk} pubkey={pk} />
|
<Profile key={pk} pubkey={pk} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -95,9 +88,7 @@ export function LiveChat({
|
|||||||
const feed = useLiveChatFeed(link, goal ? [goal.id] : undefined);
|
const feed = useLiveChatFeed(link, goal ? [goal.id] : undefined);
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const pubkeys = [
|
const pubkeys = [...new Set(feed.zaps.flatMap(a => [a.pubkey, unwrap(findTag(a, "p"))]))];
|
||||||
...new Set(feed.zaps.flatMap((a) => [a.pubkey, unwrap(findTag(a, "p"))])),
|
|
||||||
];
|
|
||||||
System.ProfileLoader.TrackMetadata(pubkeys);
|
System.ProfileLoader.TrackMetadata(pubkeys);
|
||||||
return () => System.ProfileLoader.UntrackMetadata(pubkeys);
|
return () => System.ProfileLoader.UntrackMetadata(pubkeys);
|
||||||
}, [feed.zaps]);
|
}, [feed.zaps]);
|
||||||
@ -116,54 +107,40 @@ export function LiveChat({
|
|||||||
return uniqBy(userEmojiPacks.concat(channelEmojiPacks), packId);
|
return uniqBy(userEmojiPacks.concat(channelEmojiPacks), packId);
|
||||||
}, [userEmojiPacks, channelEmojiPacks]);
|
}, [userEmojiPacks, channelEmojiPacks]);
|
||||||
|
|
||||||
const zaps = feed.zaps
|
const zaps = feed.zaps.map(ev => parseZap(ev, System.ProfileLoader.Cache)).filter(z => z && z.valid);
|
||||||
.map((ev) => parseZap(ev, System.ProfileLoader.Cache))
|
|
||||||
.filter((z) => z && z.valid);
|
|
||||||
const events = useMemo(() => {
|
const events = useMemo(() => {
|
||||||
return [...feed.messages, ...feed.zaps, ...awards].sort(
|
return [...feed.messages, ...feed.zaps, ...awards].sort((a, b) => b.created_at - a.created_at);
|
||||||
(a, b) => b.created_at - a.created_at
|
|
||||||
);
|
|
||||||
}, [feed.messages, feed.zaps, awards]);
|
}, [feed.messages, feed.zaps, awards]);
|
||||||
const streamer = getHost(ev);
|
const streamer = getHost(ev);
|
||||||
const naddr = useMemo(() => {
|
const naddr = useMemo(() => {
|
||||||
if (ev) {
|
if (ev) {
|
||||||
return encodeTLV(
|
return encodeTLV(NostrPrefix.Address, findTag(ev, "d") ?? "", undefined, ev.kind, ev.pubkey);
|
||||||
NostrPrefix.Address,
|
|
||||||
findTag(ev, "d") ?? "",
|
|
||||||
undefined,
|
|
||||||
ev.kind,
|
|
||||||
ev.pubkey
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}, [ev]);
|
}, [ev]);
|
||||||
const filteredEvents = useMemo(() => {
|
const filteredEvents = useMemo(() => {
|
||||||
return events.filter(
|
return events.filter(e => !mutedPubkeys.has(e.pubkey) && !hostMutedPubkeys.has(e.pubkey));
|
||||||
(e) => !mutedPubkeys.has(e.pubkey) && !hostMutedPubkeys.has(e.pubkey)
|
|
||||||
);
|
|
||||||
}, [events, mutedPubkeys, hostMutedPubkeys]);
|
}, [events, mutedPubkeys, hostMutedPubkeys]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="live-chat" style={height ? { height: `${height}px` } : {}}>
|
<div className="live-chat" style={height ? { height: `${height}px` } : {}}>
|
||||||
{(options?.showHeader ?? true) && (
|
{(options?.showHeader ?? true) && (
|
||||||
<div className="header">
|
<div className="header">
|
||||||
<h2 className="title">Stream Chat</h2>
|
<h2 className="title">
|
||||||
|
<FormattedMessage defaultMessage="Stream Chat" />
|
||||||
|
</h2>
|
||||||
<Icon
|
<Icon
|
||||||
name="link"
|
name="link"
|
||||||
className="secondary"
|
className="secondary"
|
||||||
size={32}
|
size={32}
|
||||||
onClick={() =>
|
onClick={() => window.open(`/chat/${naddr}?chat=true`, "_blank", "popup,width=400,height=800")}
|
||||||
window.open(
|
|
||||||
`/chat/${naddr}?chat=true`,
|
|
||||||
"_blank",
|
|
||||||
"popup,width=400,height=800"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{zaps.length > 0 && (
|
{zaps.length > 0 && (
|
||||||
<div className="top-zappers">
|
<div className="top-zappers">
|
||||||
<h3>Top zappers</h3>
|
<h3>
|
||||||
|
<FormattedMessage defaultMessage="Top zappers" />
|
||||||
|
</h3>
|
||||||
<div className="top-zappers-container">
|
<div className="top-zappers-container">
|
||||||
<TopZappers zaps={zaps} />
|
<TopZappers zaps={zaps} />
|
||||||
</div>
|
</div>
|
||||||
@ -172,7 +149,7 @@ export function LiveChat({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="messages">
|
<div className="messages">
|
||||||
{filteredEvents.map((a) => {
|
{filteredEvents.map(a => {
|
||||||
switch (a.kind) {
|
switch (a.kind) {
|
||||||
case EventKind.BadgeAward: {
|
case EventKind.BadgeAward: {
|
||||||
return <BadgeAward ev={a} />;
|
return <BadgeAward ev={a} />;
|
||||||
@ -190,9 +167,7 @@ export function LiveChat({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
case EventKind.ZapReceipt: {
|
case EventKind.ZapReceipt: {
|
||||||
const zap = zaps.find(
|
const zap = zaps.find(b => b.id === a.id && b.receiver === streamer);
|
||||||
(b) => b.id === a.id && b.receiver === streamer
|
|
||||||
);
|
|
||||||
if (zap) {
|
if (zap) {
|
||||||
return <ChatZap zap={zap} key={a.id} />;
|
return <ChatZap zap={zap} key={a.id} />;
|
||||||
}
|
}
|
||||||
@ -207,7 +182,9 @@ export function LiveChat({
|
|||||||
{login ? (
|
{login ? (
|
||||||
<WriteMessage emojiPacks={allEmojiPacks} link={link} />
|
<WriteMessage emojiPacks={allEmojiPacks} link={link} />
|
||||||
) : (
|
) : (
|
||||||
<p>Please login to write messages!</p>
|
<p>
|
||||||
|
<FormattedMessage defaultMessage="Please login to write messages!" />
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -227,16 +204,21 @@ function ChatZap({ zap }: { zap: ParsedZap }) {
|
|||||||
<div className={`zap-container ${isBig ? "big-zap" : ""}`}>
|
<div className={`zap-container ${isBig ? "big-zap" : ""}`}>
|
||||||
<div className="zap">
|
<div className="zap">
|
||||||
<Icon name="zap-filled" className="zap-icon" />
|
<Icon name="zap-filled" className="zap-icon" />
|
||||||
<Profile
|
<FormattedMessage
|
||||||
pubkey={zap.anonZap ? "anon" : zap.sender ?? "anon"}
|
defaultMessage="{person} zapped {amount} sats"
|
||||||
options={{
|
values={{
|
||||||
showAvatar: !zap.anonZap,
|
person: (
|
||||||
overrideName: zap.anonZap ? "Anon" : undefined,
|
<Profile
|
||||||
|
pubkey={zap.anonZap ? "anon" : zap.sender ?? "anon"}
|
||||||
|
options={{
|
||||||
|
showAvatar: !zap.anonZap,
|
||||||
|
overrideName: zap.anonZap ? "Anon" : undefined,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
amount: <span className="zap-amount">{formatSats(zap.amount)}</span>,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
zapped
|
|
||||||
<span className="zap-amount">{formatSats(zap.amount)}</span>
|
|
||||||
sats
|
|
||||||
</div>
|
</div>
|
||||||
{zap.content && (
|
{zap.content && (
|
||||||
<div className="zap-content">
|
<div className="zap-content">
|
||||||
|
@ -75,8 +75,7 @@ export function LiveVideoPlayer(props: VideoPlayerProps) {
|
|||||||
export function WebRTCPlayer(props: VideoPlayerProps) {
|
export function WebRTCPlayer(props: VideoPlayerProps) {
|
||||||
const video = useRef<HTMLVideoElement>(null);
|
const video = useRef<HTMLVideoElement>(null);
|
||||||
const streamCached = useMemo(
|
const streamCached = useMemo(
|
||||||
() =>
|
() => "https://customer-uu10flpvos4pfhgu.cloudflarestream.com/7634aee1af35a2de4ac13ca3d1718a8b/webRTC/play",
|
||||||
"https://customer-uu10flpvos4pfhgu.cloudflarestream.com/7634aee1af35a2de4ac13ca3d1718a8b/webRTC/play",
|
|
||||||
[props.stream]
|
[props.stream]
|
||||||
);
|
);
|
||||||
const [status] = useState<VideoStatus>();
|
const [status] = useState<VideoStatus>();
|
||||||
@ -90,7 +89,7 @@ export function WebRTCPlayer(props: VideoPlayerProps) {
|
|||||||
|
|
||||||
client
|
client
|
||||||
.Play()
|
.Play()
|
||||||
.then((s) => {
|
.then(s => {
|
||||||
if (video.current) {
|
if (video.current) {
|
||||||
video.current.srcObject = s;
|
video.current.srcObject = s;
|
||||||
}
|
}
|
||||||
@ -107,12 +106,7 @@ export function WebRTCPlayer(props: VideoPlayerProps) {
|
|||||||
<div className={status}>
|
<div className={status}>
|
||||||
<div>{status}</div>
|
<div>{status}</div>
|
||||||
</div>
|
</div>
|
||||||
<video
|
<video ref={video} autoPlay={true} poster={props.poster} controls={status === VideoStatus.Online} />
|
||||||
ref={video}
|
|
||||||
autoPlay={true}
|
|
||||||
poster={props.poster}
|
|
||||||
controls={status === VideoStatus.Online}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import Copy from "./copy";
|
|||||||
import { hexToBech32, openFile } from "utils";
|
import { hexToBech32, openFile } from "utils";
|
||||||
import { VoidApi } from "@void-cat/api";
|
import { VoidApi } from "@void-cat/api";
|
||||||
import { LoginType } from "login";
|
import { LoginType } from "login";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import { bech32 } from "@scure/base";
|
import { bech32 } from "@scure/base";
|
||||||
|
|
||||||
enum Stage {
|
enum Stage {
|
||||||
@ -87,8 +88,7 @@ export function LoginSignup({ close }: { close: () => void }) {
|
|||||||
"V-Strip-Metadata": "true",
|
"V-Strip-Metadata": "true",
|
||||||
});
|
});
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
const resultUrl =
|
const resultUrl = result.file?.metadata?.url ?? `${VoidCatHost}/d/${result.file?.id}`;
|
||||||
result.file?.metadata?.url ?? `${VoidCatHost}/d/${result.file?.id}`;
|
|
||||||
setAvatar(resultUrl);
|
setAvatar(resultUrl);
|
||||||
} else {
|
} else {
|
||||||
setError(result.errorMessage ?? "Upload failed");
|
setError(result.errorMessage ?? "Upload failed");
|
||||||
@ -115,22 +115,16 @@ export function LoginSignup({ close }: { close: () => void }) {
|
|||||||
case Stage.Login: {
|
case Stage.Login: {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h2>Login</h2>
|
<h2>
|
||||||
|
<FormattedMessage defaultMessage="Login" />
|
||||||
|
</h2>
|
||||||
{"nostr" in window && (
|
{"nostr" in window && (
|
||||||
<AsyncButton
|
<AsyncButton type="button" className="btn btn-primary" onClick={doLogin}>
|
||||||
type="button"
|
<FormattedMessage defaultMessage="Nostr Extension" />
|
||||||
className="btn btn-primary"
|
|
||||||
onClick={doLogin}
|
|
||||||
>
|
|
||||||
Nostr Extension
|
|
||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
)}
|
)}
|
||||||
<button
|
<button type="button" className="btn btn-primary" onClick={createAccount}>
|
||||||
type="button"
|
<FormattedMessage defaultMessage="Create Account" />
|
||||||
className="btn btn-primary"
|
|
||||||
onClick={createAccount}
|
|
||||||
>
|
|
||||||
Create Account
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -146,7 +140,9 @@ export function LoginSignup({ close }: { close: () => void }) {
|
|||||||
case Stage.Details: {
|
case Stage.Details: {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h2>Setup Profile</h2>
|
<h2>
|
||||||
|
<FormattedMessage defaultMessage="Setup Profile" />
|
||||||
|
</h2>
|
||||||
<div className="flex f-center">
|
<div className="flex f-center">
|
||||||
<div
|
<div
|
||||||
className="avatar-input"
|
className="avatar-input"
|
||||||
@ -155,28 +151,20 @@ export function LoginSignup({ close }: { close: () => void }) {
|
|||||||
{
|
{
|
||||||
"--img": `url(${avatar})`,
|
"--img": `url(${avatar})`,
|
||||||
} as CSSProperties
|
} as CSSProperties
|
||||||
}
|
}>
|
||||||
>
|
|
||||||
<Icon name="camera-plus" />
|
<Icon name="camera-plus" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="paper">
|
<div className="paper">
|
||||||
<input
|
<input type="text" placeholder="Username" value={username} onChange={e => setUsername(e.target.value)} />
|
||||||
type="text"
|
|
||||||
placeholder="Username"
|
|
||||||
value={username}
|
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<small>You can change this later</small>
|
<small>
|
||||||
|
<FormattedMessage defaultMessage="You can change this later" />
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<AsyncButton
|
<AsyncButton type="button" className="btn btn-primary" onClick={saveProfile}>
|
||||||
type="button"
|
<FormattedMessage defaultMessage="Save" />
|
||||||
className="btn btn-primary"
|
|
||||||
onClick={saveProfile}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -184,20 +172,17 @@ export function LoginSignup({ close }: { close: () => void }) {
|
|||||||
case Stage.SaveKey: {
|
case Stage.SaveKey: {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h2>Save Key</h2>
|
<h2>
|
||||||
|
<FormattedMessage defaultMessage="Save Key" />
|
||||||
|
</h2>
|
||||||
<p>
|
<p>
|
||||||
Nostr uses private keys, please save yours, if you lose this key you
|
<FormattedMessage defaultMessage="Nostr uses private keys, please save yours, if you lose this key you wont be able to login to your account anymore!" />
|
||||||
wont be able to login to your account anymore!
|
|
||||||
</p>
|
</p>
|
||||||
<div className="paper">
|
<div className="paper">
|
||||||
<Copy text={hexToBech32("nsec", key)} />
|
<Copy text={hexToBech32("nsec", key)} />
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button type="button" className="btn btn-primary" onClick={loginWithKey}>
|
||||||
type="button"
|
<FormattedMessage defaultMessage="Ok, it's safe" />
|
||||||
className="btn btn-primary"
|
|
||||||
onClick={loginWithKey}
|
|
||||||
>
|
|
||||||
Ok, it's safe
|
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { useUserProfile } from "@snort/system-react";
|
import { useUserProfile } from "@snort/system-react";
|
||||||
import { System } from "index";
|
|
||||||
import { hexToBech32 } from "utils";
|
import { hexToBech32 } from "utils";
|
||||||
|
|
||||||
interface MentionProps {
|
interface MentionProps {
|
||||||
@ -9,7 +8,7 @@ interface MentionProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Mention({ pubkey }: MentionProps) {
|
export function Mention({ pubkey }: MentionProps) {
|
||||||
const user = useUserProfile(System, pubkey);
|
const user = useUserProfile(pubkey);
|
||||||
const npub = hexToBech32("npub", pubkey);
|
const npub = hexToBech32("npub", pubkey);
|
||||||
return <Link to={`/p/${npub}`}>{user?.name || pubkey}</Link>;
|
return <Link to={`/p/${npub}`}>{user?.name || pubkey}</Link>;
|
||||||
}
|
}
|
||||||
|
@ -3,21 +3,19 @@ import { useLogin } from "hooks/login";
|
|||||||
import AsyncButton from "element/async-button";
|
import AsyncButton from "element/async-button";
|
||||||
import { Login, System } from "index";
|
import { Login, System } from "index";
|
||||||
import { MUTED } from "const";
|
import { MUTED } from "const";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
export function useMute(pubkey: string) {
|
export function useMute(pubkey: string) {
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const { tags, content } = login?.muted ?? { tags: [] };
|
const { tags, content } = login?.muted ?? { tags: [] };
|
||||||
const muted = useMemo(() => tags.filter((t) => t.at(0) === "p"), [tags]);
|
const muted = useMemo(() => tags.filter(t => t.at(0) === "p"), [tags]);
|
||||||
const isMuted = useMemo(
|
const isMuted = useMemo(() => muted.find(t => t.at(1) === pubkey), [pubkey, muted]);
|
||||||
() => muted.find((t) => t.at(1) === pubkey),
|
|
||||||
[pubkey, muted]
|
|
||||||
);
|
|
||||||
|
|
||||||
async function unmute() {
|
async function unmute() {
|
||||||
const pub = login?.publisher();
|
const pub = login?.publisher();
|
||||||
if (pub) {
|
if (pub) {
|
||||||
const newMuted = tags.filter((t) => t.at(1) !== pubkey);
|
const newMuted = tags.filter(t => t.at(1) !== pubkey);
|
||||||
const ev = await pub.generic((eb) => {
|
const ev = await pub.generic(eb => {
|
||||||
eb.kind(MUTED).content(content ?? "");
|
eb.kind(MUTED).content(content ?? "");
|
||||||
for (const t of newMuted) {
|
for (const t of newMuted) {
|
||||||
eb.tag(t);
|
eb.tag(t);
|
||||||
@ -34,7 +32,7 @@ export function useMute(pubkey: string) {
|
|||||||
const pub = login?.publisher();
|
const pub = login?.publisher();
|
||||||
if (pub) {
|
if (pub) {
|
||||||
const newMuted = [...tags, ["p", pubkey]];
|
const newMuted = [...tags, ["p", pubkey]];
|
||||||
const ev = await pub.generic((eb) => {
|
const ev = await pub.generic(eb => {
|
||||||
eb.kind(MUTED).content(content ?? "");
|
eb.kind(MUTED).content(content ?? "");
|
||||||
for (const tag of newMuted) {
|
for (const tag of newMuted) {
|
||||||
eb.tag(tag);
|
eb.tag(tag);
|
||||||
@ -54,12 +52,8 @@ export function LoggedInMuteButton({ pubkey }: { pubkey: string }) {
|
|||||||
const { isMuted, mute, unmute } = useMute(pubkey);
|
const { isMuted, mute, unmute } = useMute(pubkey);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AsyncButton
|
<AsyncButton type="button" className="btn delete-button" onClick={() => (isMuted ? unmute() : mute())}>
|
||||||
type="button"
|
{isMuted ? <FormattedMessage defaultMessage="Unmute" /> : <FormattedMessage defaultMessage="Mute" />}
|
||||||
className="btn delete-button"
|
|
||||||
onClick={() => (isMuted ? unmute() : mute())}
|
|
||||||
>
|
|
||||||
{isMuted ? "Unmute" : "Mute"}
|
|
||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import { useState } from "react";
|
|||||||
import { System } from "index";
|
import { System } from "index";
|
||||||
import { GOAL } from "const";
|
import { GOAL } from "const";
|
||||||
import { useLogin } from "hooks/login";
|
import { useLogin } from "hooks/login";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
interface NewGoalDialogProps {
|
interface NewGoalDialogProps {
|
||||||
link: NostrLink;
|
link: NostrLink;
|
||||||
@ -23,7 +24,7 @@ export function NewGoalDialog({ link }: NewGoalDialogProps) {
|
|||||||
async function publishGoal() {
|
async function publishGoal() {
|
||||||
const pub = login?.publisher();
|
const pub = login?.publisher();
|
||||||
if (pub) {
|
if (pub) {
|
||||||
const evNew = await pub.generic((eb) => {
|
const evNew = await pub.generic(eb => {
|
||||||
eb.kind(GOAL)
|
eb.kind(GOAL)
|
||||||
.tag(["a", `${link.kind}:${link.author}:${link.id}`])
|
.tag(["a", `${link.kind}:${link.author}:${link.id}`])
|
||||||
.tag(["amount", String(Number(goalAmount) * 1000)])
|
.tag(["amount", String(Number(goalAmount) * 1000)])
|
||||||
@ -48,7 +49,9 @@ export function NewGoalDialog({ link }: NewGoalDialogProps) {
|
|||||||
<button type="button" className="btn btn-primary">
|
<button type="button" className="btn btn-primary">
|
||||||
<span>
|
<span>
|
||||||
<Icon name="zap-filled" size={12} />
|
<Icon name="zap-filled" size={12} />
|
||||||
<span>Add stream goal</span>
|
<span>
|
||||||
|
<FormattedMessage defaultMessage="Add stream goal" />
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</Dialog.Trigger>
|
</Dialog.Trigger>
|
||||||
@ -57,26 +60,28 @@ export function NewGoalDialog({ link }: NewGoalDialogProps) {
|
|||||||
<Dialog.Content className="dialog-content">
|
<Dialog.Content className="dialog-content">
|
||||||
<div className="new-goal">
|
<div className="new-goal">
|
||||||
<div className="zap-goals">
|
<div className="zap-goals">
|
||||||
<Icon
|
<Icon name="zap-filled" className="stream-zap-goals-icon" size={16} />
|
||||||
name="zap-filled"
|
<h3>
|
||||||
className="stream-zap-goals-icon"
|
<FormattedMessage defaultMessage="Stream Zap Goals" />
|
||||||
size={16}
|
</h3>
|
||||||
/>
|
|
||||||
<h3>Stream Zap Goals</h3>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p>Name</p>
|
<p>
|
||||||
|
<FormattedMessage defaultMessage="Name" />
|
||||||
|
</p>
|
||||||
<div className="paper">
|
<div className="paper">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={goalName}
|
value={goalName}
|
||||||
placeholder="e.g. New Laptop"
|
placeholder="e.g. New Laptop"
|
||||||
onChange={(e) => setGoalName(e.target.value)}
|
onChange={e => setGoalName(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p>Amount</p>
|
<p>
|
||||||
|
<FormattedMessage defaultMessage="Amount" />
|
||||||
|
</p>
|
||||||
<div className="paper">
|
<div className="paper">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
@ -84,18 +89,13 @@ export function NewGoalDialog({ link }: NewGoalDialogProps) {
|
|||||||
min="1"
|
min="1"
|
||||||
max="2100000000000000"
|
max="2100000000000000"
|
||||||
value={goalAmount}
|
value={goalAmount}
|
||||||
onChange={(e) => setGoalAmount(e.target.value)}
|
onChange={e => setGoalAmount(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="create-goal">
|
<div className="create-goal">
|
||||||
<AsyncButton
|
<AsyncButton type="button" className="btn btn-primary wide" disabled={!isValid} onClick={publishGoal}>
|
||||||
type="button"
|
<FormattedMessage defaultMessage="Create Goal" />
|
||||||
className="btn btn-primary wide"
|
|
||||||
disabled={!isValid}
|
|
||||||
onClick={publishGoal}
|
|
||||||
>
|
|
||||||
Create goal
|
|
||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -10,6 +10,7 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { eventLink, findTag } from "utils";
|
import { eventLink, findTag } from "utils";
|
||||||
import { NostrProviderDialog } from "./nostr-provider-dialog";
|
import { NostrProviderDialog } from "./nostr-provider-dialog";
|
||||||
import { unwrap } from "@snort/shared";
|
import { unwrap } from "@snort/shared";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
function NewStream({ ev, onFinish }: StreamEditorProps) {
|
function NewStream({ ev, onFinish }: StreamEditorProps) {
|
||||||
const providers = useStreamProvider();
|
const providers = useStreamProvider();
|
||||||
@ -19,9 +20,7 @@ function NewStream({ ev, onFinish }: StreamEditorProps) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!currentProvider) {
|
if (!currentProvider) {
|
||||||
setCurrentProvider(
|
setCurrentProvider(
|
||||||
ev !== undefined
|
ev !== undefined ? unwrap(providers.find(a => a.name.toLowerCase() === "manual")) : providers.at(0)
|
||||||
? unwrap(providers.find((a) => a.name.toLowerCase() === "manual"))
|
|
||||||
: providers.at(0)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [providers, currentProvider]);
|
}, [providers, currentProvider]);
|
||||||
@ -33,14 +32,10 @@ function NewStream({ ev, onFinish }: StreamEditorProps) {
|
|||||||
case StreamProviders.Manual: {
|
case StreamProviders.Manual: {
|
||||||
return (
|
return (
|
||||||
<StreamEditor
|
<StreamEditor
|
||||||
onFinish={(ex) => {
|
onFinish={ex => {
|
||||||
currentProvider.updateStreamInfo(ex);
|
currentProvider.updateStreamInfo(ex);
|
||||||
if (!ev) {
|
if (!ev) {
|
||||||
if (
|
if (findTag(ex, "content-warning") && __XXX_HOST && __XXX === false) {
|
||||||
findTag(ex, "content-warning") &&
|
|
||||||
__XXX_HOST &&
|
|
||||||
__XXX === false
|
|
||||||
) {
|
|
||||||
location.href = `${__XXX_HOST}/${eventLink(ex)}`;
|
location.href = `${__XXX_HOST}/${eventLink(ex)}`;
|
||||||
} else {
|
} else {
|
||||||
navigate(`/${eventLink(ex)}`, {
|
navigate(`/${eventLink(ex)}`, {
|
||||||
@ -56,13 +51,7 @@ function NewStream({ ev, onFinish }: StreamEditorProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
case StreamProviders.NostrType: {
|
case StreamProviders.NostrType: {
|
||||||
return (
|
return <NostrProviderDialog provider={currentProvider} onFinish={onFinish} ev={ev} />;
|
||||||
<NostrProviderDialog
|
|
||||||
provider={currentProvider}
|
|
||||||
onFinish={onFinish}
|
|
||||||
ev={ev}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
case StreamProviders.Owncast: {
|
case StreamProviders.Owncast: {
|
||||||
return;
|
return;
|
||||||
@ -72,13 +61,12 @@ function NewStream({ ev, onFinish }: StreamEditorProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p>Stream Providers</p>
|
<p>
|
||||||
|
<FormattedMessage defaultMessage="Stream Providers" />
|
||||||
|
</p>
|
||||||
<div className="flex g12">
|
<div className="flex g12">
|
||||||
{providers.map((v) => (
|
{providers.map(v => (
|
||||||
<span
|
<span className={`pill${v === currentProvider ? " active" : ""}`} onClick={() => setCurrentProvider(v)}>
|
||||||
className={`pill${v === currentProvider ? " active" : ""}`}
|
|
||||||
onClick={() => setCurrentProvider(v)}
|
|
||||||
>
|
|
||||||
{v.name}
|
{v.name}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
@ -93,9 +81,7 @@ interface NewStreamDialogProps {
|
|||||||
btnClassName?: string;
|
btnClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NewStreamDialog(
|
export function NewStreamDialog(props: NewStreamDialogProps & StreamEditorProps) {
|
||||||
props: NewStreamDialogProps & StreamEditorProps
|
|
||||||
) {
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
return (
|
return (
|
||||||
<Dialog.Root open={open} onOpenChange={setOpen}>
|
<Dialog.Root open={open} onOpenChange={setOpen}>
|
||||||
@ -104,7 +90,9 @@ export function NewStreamDialog(
|
|||||||
{props.text && props.text}
|
{props.text && props.text}
|
||||||
{!props.text && (
|
{!props.text && (
|
||||||
<>
|
<>
|
||||||
<span className="hide-on-mobile">Stream</span>
|
<span className="hide-on-mobile">
|
||||||
|
<FormattedMessage defaultMessage="Stream" />
|
||||||
|
</span>
|
||||||
<Icon name="signal" />
|
<Icon name="signal" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -3,10 +3,7 @@ import { Mention } from "./mention";
|
|||||||
|
|
||||||
export function NostrLink({ link }: { link: string }) {
|
export function NostrLink({ link }: { link: string }) {
|
||||||
const nav = tryParseNostrLink(link);
|
const nav = tryParseNostrLink(link);
|
||||||
if (
|
if (nav?.type === NostrPrefix.PublicKey || nav?.type === NostrPrefix.Profile) {
|
||||||
nav?.type === NostrPrefix.PublicKey ||
|
|
||||||
nav?.type === NostrPrefix.Profile
|
|
||||||
) {
|
|
||||||
return <Mention pubkey={nav.id} relays={nav.relays} />;
|
return <Mention pubkey={nav.id} relays={nav.relays} />;
|
||||||
} else {
|
} else {
|
||||||
<a href={link} target="_blank" rel="noreferrer" className="ext">
|
<a href={link} target="_blank" rel="noreferrer" className="ext">
|
||||||
|
@ -1,19 +1,13 @@
|
|||||||
import { NostrEvent } from "@snort/system";
|
import { NostrEvent } from "@snort/system";
|
||||||
import {
|
import { StreamProvider, StreamProviderEndpoint, StreamProviderInfo } from "providers";
|
||||||
StreamProvider,
|
|
||||||
StreamProviderEndpoint,
|
|
||||||
StreamProviderInfo,
|
|
||||||
} from "providers";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { SendZaps } from "./send-zap";
|
import { SendZaps } from "./send-zap";
|
||||||
import { StreamEditor, StreamEditorProps } from "./stream-editor";
|
import { StreamEditor, StreamEditorProps } from "./stream-editor";
|
||||||
import Spinner from "./spinner";
|
import Spinner from "./spinner";
|
||||||
import AsyncButton from "./async-button";
|
import AsyncButton from "./async-button";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
export function NostrProviderDialog({
|
export function NostrProviderDialog({ provider, ...others }: { provider: StreamProvider } & StreamEditorProps) {
|
||||||
provider,
|
|
||||||
...others
|
|
||||||
}: { provider: StreamProvider } & StreamEditorProps) {
|
|
||||||
const [topup, setTopup] = useState(false);
|
const [topup, setTopup] = useState(false);
|
||||||
const [info, setInfo] = useState<StreamProviderInfo>();
|
const [info, setInfo] = useState<StreamProviderInfo>();
|
||||||
const [ep, setEndpoint] = useState<StreamProviderEndpoint>();
|
const [ep, setEndpoint] = useState<StreamProviderEndpoint>();
|
||||||
@ -24,7 +18,7 @@ export function NostrProviderDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
provider.info().then((v) => {
|
provider.info().then(v => {
|
||||||
setInfo(v);
|
setInfo(v);
|
||||||
setTos(v.tosAccepted ?? true);
|
setTos(v.tosAccepted ?? true);
|
||||||
setEndpoint(sortEndpoints(v.endpoints)[0]);
|
setEndpoint(sortEndpoints(v.endpoints)[0]);
|
||||||
@ -42,13 +36,13 @@ export function NostrProviderDialog({
|
|||||||
name: provider.name,
|
name: provider.name,
|
||||||
canZap: false,
|
canZap: false,
|
||||||
maxCommentLength: 0,
|
maxCommentLength: 0,
|
||||||
getInvoice: async (amount) => {
|
getInvoice: async amount => {
|
||||||
const pr = await provider.topup(amount);
|
const pr = await provider.topup(amount);
|
||||||
return { pr };
|
return { pr };
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
onFinish={() => {
|
onFinish={() => {
|
||||||
provider.info().then((v) => {
|
provider.info().then(v => {
|
||||||
setInfo(v);
|
setInfo(v);
|
||||||
setTopup(false);
|
setTopup(false);
|
||||||
});
|
});
|
||||||
@ -92,33 +86,27 @@ export function NostrProviderDialog({
|
|||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex g12">
|
<div className="flex g12">
|
||||||
<input
|
<input type="checkbox" checked={tos} onChange={e => setTos(e.target.checked)} />
|
||||||
type="checkbox"
|
|
||||||
checked={tos}
|
|
||||||
onChange={(e) => setTos(e.target.checked)}
|
|
||||||
/>
|
|
||||||
<p>
|
<p>
|
||||||
I have read and agree with {info.name}'s{" "}
|
<FormattedMessage
|
||||||
<span
|
defaultMessage="I have read and agree with {provider}'s {terms}."
|
||||||
className="tos-link"
|
values={{
|
||||||
onClick={() =>
|
provider: info.name,
|
||||||
window.open(info.tosLink, "popup", "width=400,height=800")
|
terms: (
|
||||||
}
|
<span
|
||||||
>
|
className="tos-link"
|
||||||
terms and conditions
|
onClick={() => window.open(info.tosLink, "popup", "width=400,height=800")}>
|
||||||
</span>
|
<FormattedMessage defaultMessage="terms and conditions" />
|
||||||
.
|
</span>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<AsyncButton
|
<AsyncButton type="button" className="btn btn-primary wide" disabled={!tos} onClick={acceptTos}>
|
||||||
type="button"
|
<FormattedMessage defaultMessage="Continue" />
|
||||||
className="btn btn-primary wide"
|
|
||||||
disabled={!tos}
|
|
||||||
onClick={acceptTos}
|
|
||||||
>
|
|
||||||
Continue
|
|
||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@ -129,13 +117,12 @@ export function NostrProviderDialog({
|
|||||||
<>
|
<>
|
||||||
{info.endpoints.length > 1 && (
|
{info.endpoints.length > 1 && (
|
||||||
<div>
|
<div>
|
||||||
<p>Endpoint</p>
|
<p>
|
||||||
|
<FormattedMessage defaultMessage="Endpoint" />
|
||||||
|
</p>
|
||||||
<div className="flex g12">
|
<div className="flex g12">
|
||||||
{sortEndpoints(info.endpoints).map((a) => (
|
{sortEndpoints(info.endpoints).map(a => (
|
||||||
<span
|
<span className={`pill${ep?.name === a.name ? " active" : ""}`} onClick={() => setEndpoint(a)}>
|
||||||
className={`pill${ep?.name === a.name ? " active" : ""}`}
|
|
||||||
onClick={() => setEndpoint(a)}
|
|
||||||
>
|
|
||||||
{a.name}
|
{a.name}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
@ -143,41 +130,48 @@ export function NostrProviderDialog({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<p>Stream Url</p>
|
<p>
|
||||||
|
<FormattedMessage defaultMessage="Server Url" />
|
||||||
|
</p>
|
||||||
<div className="paper">
|
<div className="paper">
|
||||||
<input type="text" value={ep?.url} disabled />
|
<input type="text" value={ep?.url} disabled />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p>Stream Key</p>
|
<p>
|
||||||
|
<FormattedMessage defaultMessage="Stream Key" />
|
||||||
|
</p>
|
||||||
<div className="flex g12">
|
<div className="flex g12">
|
||||||
<div className="paper f-grow">
|
<div className="paper f-grow">
|
||||||
<input type="password" value={ep?.key} disabled />
|
<input type="password" value={ep?.key} disabled />
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button className="btn btn-primary" onClick={() => window.navigator.clipboard.writeText(ep?.key ?? "")}>
|
||||||
className="btn btn-primary"
|
<FormattedMessage defaultMessage="Copy" />
|
||||||
onClick={() => window.navigator.clipboard.writeText(ep?.key ?? "")}
|
|
||||||
>
|
|
||||||
Copy
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p>Balance</p>
|
<p>
|
||||||
|
<FormattedMessage defaultMessage="Balance" />
|
||||||
|
</p>
|
||||||
<div className="flex g12">
|
<div className="flex g12">
|
||||||
<div className="paper f-grow">
|
<div className="paper f-grow">
|
||||||
{info.balance?.toLocaleString()} sats
|
<FormattedMessage defaultMessage="{amount} sats" values={{ amount: info.balance?.toLocaleString() }} />
|
||||||
</div>
|
</div>
|
||||||
<button className="btn btn-primary" onClick={() => setTopup(true)}>
|
<button className="btn btn-primary" onClick={() => setTopup(true)}>
|
||||||
Topup
|
<FormattedMessage defaultMessage="Topup" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<small>About {calcEstimate()}</small>
|
<small>
|
||||||
|
<FormattedMessage defaultMessage="About {estimate}" values={{ estimate: calcEstimate() }} />
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p>Resolutions</p>
|
<p>
|
||||||
|
<FormattedMessage defaultMessage="Resolutions" />
|
||||||
|
</p>
|
||||||
<div className="flex g12">
|
<div className="flex g12">
|
||||||
{ep?.capabilities?.map((a) => (
|
{ep?.capabilities?.map(a => (
|
||||||
<span className="pill">{parseCapability(a)}</span>
|
<span className="pill">{parseCapability(a)}</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -186,7 +180,7 @@ export function NostrProviderDialog({
|
|||||||
tosInput()
|
tosInput()
|
||||||
) : (
|
) : (
|
||||||
<StreamEditor
|
<StreamEditor
|
||||||
onFinish={(ex) => {
|
onFinish={ex => {
|
||||||
provider.updateStreamInfo(ex);
|
provider.updateStreamInfo(ex);
|
||||||
others.onFinish?.(ex);
|
others.onFinish?.(ex);
|
||||||
}}
|
}}
|
||||||
@ -196,10 +190,8 @@ export function NostrProviderDialog({
|
|||||||
["title", info.streamInfo?.title ?? ""],
|
["title", info.streamInfo?.title ?? ""],
|
||||||
["summary", info.streamInfo?.summary ?? ""],
|
["summary", info.streamInfo?.summary ?? ""],
|
||||||
["image", info.streamInfo?.image ?? ""],
|
["image", info.streamInfo?.image ?? ""],
|
||||||
...(info.streamInfo?.content_warning
|
...(info.streamInfo?.content_warning ? [["content-warning", info.streamInfo?.content_warning]] : []),
|
||||||
? [["content-warning", info.streamInfo?.content_warning]]
|
...(info.streamInfo?.tags?.map(a => ["t", a]) ?? []),
|
||||||
: []),
|
|
||||||
...(info.streamInfo?.tags?.map((a) => ["t", a]) ?? []),
|
|
||||||
],
|
],
|
||||||
} as NostrEvent
|
} as NostrEvent
|
||||||
}
|
}
|
||||||
|
@ -14,10 +14,7 @@ export function Note({ ev }: { ev: NostrEvent }) {
|
|||||||
<ExternalIconLink
|
<ExternalIconLink
|
||||||
size={24}
|
size={24}
|
||||||
className="note-link-icon"
|
className="note-link-icon"
|
||||||
href={`https://snort.social/e/${hexToBech32(
|
href={`https://snort.social/e/${hexToBech32(NostrPrefix.Event, ev.id)}`}
|
||||||
NostrPrefix.Event,
|
|
||||||
ev.id
|
|
||||||
)}`}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="note-content">
|
<div className="note-content">
|
||||||
|
@ -7,7 +7,6 @@ import { hexToBech32 } from "@snort/shared";
|
|||||||
|
|
||||||
import { Icon } from "element/icon";
|
import { Icon } from "element/icon";
|
||||||
import usePlaceholder from "hooks/placeholders";
|
import usePlaceholder from "hooks/placeholders";
|
||||||
import { System } from "index";
|
|
||||||
import { useInView } from "react-intersection-observer";
|
import { useInView } from "react-intersection-observer";
|
||||||
|
|
||||||
export interface ProfileOptions {
|
export interface ProfileOptions {
|
||||||
@ -45,8 +44,7 @@ export function Profile({
|
|||||||
linkToProfile?: boolean;
|
linkToProfile?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { inView, ref } = useInView();
|
const { inView, ref } = useInView();
|
||||||
const pLoaded =
|
const pLoaded = useUserProfile(inView && !profile ? pubkey : undefined) || profile;
|
||||||
useUserProfile(System, inView && !profile ? pubkey : undefined) || profile;
|
|
||||||
const showAvatar = options?.showAvatar ?? true;
|
const showAvatar = options?.showAvatar ?? true;
|
||||||
const showName = options?.showName ?? true;
|
const showName = options?.showName ?? true;
|
||||||
const placeholder = usePlaceholder(pubkey);
|
const placeholder = usePlaceholder(pubkey);
|
||||||
@ -64,13 +62,7 @@ export function Profile({
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{icon}
|
{icon}
|
||||||
{showName && (
|
{showName && <span>{options?.overrideName ?? pubkey === "anon" ? "Anon" : getName(pubkey, pLoaded)}</span>}
|
||||||
<span>
|
|
||||||
{options?.overrideName ?? pubkey === "anon"
|
|
||||||
? "Anon"
|
|
||||||
: getName(pubkey, pLoaded)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -79,11 +71,7 @@ export function Profile({
|
|||||||
{content}
|
{content}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Link
|
<Link to={`/p/${hexToBech32("npub", pubkey)}`} className="profile" ref={ref}>
|
||||||
to={`/p/${hexToBech32("npub", pubkey)}`}
|
|
||||||
className="profile"
|
|
||||||
ref={ref}
|
|
||||||
>
|
|
||||||
{content}
|
{content}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
@ -46,10 +46,5 @@ export default function QrCode(props: QrCodeProps) {
|
|||||||
}
|
}
|
||||||
}, [props.data, props.link, props.width, props.height, props.avatar]);
|
}, [props.data, props.link, props.width, props.height, props.avatar]);
|
||||||
|
|
||||||
return (
|
return <div className={`qr${props.className ? ` ${props.className}` : ""}`} ref={qrRef}></div>;
|
||||||
<div
|
|
||||||
className={`qr${props.className ? ` ${props.className}` : ""}`}
|
|
||||||
ref={qrRef}
|
|
||||||
></div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -13,16 +13,13 @@ import QrCode from "./qr-code";
|
|||||||
import { useLogin } from "hooks/login";
|
import { useLogin } from "hooks/login";
|
||||||
import Copy from "./copy";
|
import Copy from "./copy";
|
||||||
import { defaultRelays } from "const";
|
import { defaultRelays } from "const";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
export interface LNURLLike {
|
export interface LNURLLike {
|
||||||
get name(): string;
|
get name(): string;
|
||||||
get maxCommentLength(): number;
|
get maxCommentLength(): number;
|
||||||
get canZap(): boolean;
|
get canZap(): boolean;
|
||||||
getInvoice(
|
getInvoice(amountInSats: number, comment?: string, zap?: NostrEvent): Promise<{ pr?: string }>;
|
||||||
amountInSats: number,
|
|
||||||
comment?: string,
|
|
||||||
zap?: NostrEvent
|
|
||||||
): Promise<{ pr?: string }>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SendZapsProps {
|
export interface SendZapsProps {
|
||||||
@ -35,19 +32,12 @@ export interface SendZapsProps {
|
|||||||
button?: ReactNode;
|
button?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SendZaps({
|
export function SendZaps({ lnurl, pubkey, aTag, eTag, targetName, onFinish }: SendZapsProps) {
|
||||||
lnurl,
|
|
||||||
pubkey,
|
|
||||||
aTag,
|
|
||||||
eTag,
|
|
||||||
targetName,
|
|
||||||
onFinish,
|
|
||||||
}: SendZapsProps) {
|
|
||||||
const UsdRate = 28_000;
|
const UsdRate = 28_000;
|
||||||
|
|
||||||
const satsAmounts = [
|
const satsAmounts = [
|
||||||
21, 69, 121, 221, 420, 1_000, 2_100, 5_000, 6_666, 10_000, 21_000, 42_000,
|
21, 69, 121, 221, 420, 1_000, 2_100, 5_000, 6_666, 10_000, 21_000, 42_000, 69_000, 100_000, 210_000, 500_000,
|
||||||
69_000, 100_000, 210_000, 500_000, 1_000_000,
|
1_000_000,
|
||||||
];
|
];
|
||||||
const usdAmounts = [0.05, 0.5, 2, 5, 10, 50, 100, 200];
|
const usdAmounts = [0.05, 0.5, 2, 5, 10, 50, 100, 200];
|
||||||
const [isFiat, setIsFiat] = useState(false);
|
const [isFiat, setIsFiat] = useState(false);
|
||||||
@ -79,34 +69,25 @@ export function SendZaps({
|
|||||||
let pub = login?.publisher();
|
let pub = login?.publisher();
|
||||||
let isAnon = false;
|
let isAnon = false;
|
||||||
if (!pub) {
|
if (!pub) {
|
||||||
pub = EventPublisher.privateKey(
|
pub = EventPublisher.privateKey(bytesToHex(secp256k1.utils.randomPrivateKey()));
|
||||||
bytesToHex(secp256k1.utils.randomPrivateKey())
|
|
||||||
);
|
|
||||||
isAnon = true;
|
isAnon = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const amountInSats = isFiat ? Math.floor((amount / UsdRate) * 1e8) : amount;
|
const amountInSats = isFiat ? Math.floor((amount / UsdRate) * 1e8) : amount;
|
||||||
let zap: NostrEvent | undefined;
|
let zap: NostrEvent | undefined;
|
||||||
if (pubkey) {
|
if (pubkey) {
|
||||||
zap = await pub.zap(
|
zap = await pub.zap(amountInSats * 1000, pubkey, relays, undefined, comment, eb => {
|
||||||
amountInSats * 1000,
|
if (aTag) {
|
||||||
pubkey,
|
eb.tag(["a", aTag]);
|
||||||
relays,
|
|
||||||
undefined,
|
|
||||||
comment,
|
|
||||||
(eb) => {
|
|
||||||
if (aTag) {
|
|
||||||
eb.tag(["a", aTag]);
|
|
||||||
}
|
|
||||||
if (eTag) {
|
|
||||||
eb.tag(["e", eTag]);
|
|
||||||
}
|
|
||||||
if (isAnon) {
|
|
||||||
eb.tag(["anon", ""]);
|
|
||||||
}
|
|
||||||
return eb;
|
|
||||||
}
|
}
|
||||||
);
|
if (eTag) {
|
||||||
|
eb.tag(["e", eTag]);
|
||||||
|
}
|
||||||
|
if (isAnon) {
|
||||||
|
eb.tag(["anon", ""]);
|
||||||
|
}
|
||||||
|
return eb;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const invoice = await svc.getInvoice(amountInSats, comment, zap);
|
const invoice = await svc.getInvoice(amountInSats, comment, zap);
|
||||||
if (!invoice.pr) return;
|
if (!invoice.pr) return;
|
||||||
@ -134,8 +115,7 @@ export function SendZaps({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsFiat(false);
|
setIsFiat(false);
|
||||||
setAmount(satsAmounts[0]);
|
setAmount(satsAmounts[0]);
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
SATS
|
SATS
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
@ -143,20 +123,20 @@ export function SendZaps({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsFiat(true);
|
setIsFiat(true);
|
||||||
setAmount(usdAmounts[0]);
|
setAmount(usdAmounts[0]);
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
USD
|
USD
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<small>Zap amount in {isFiat ? "USD" : "sats"}</small>
|
<small>
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="Zap amount in {currency}"
|
||||||
|
values={{ amount: isFiat ? "USD" : "sats" }}
|
||||||
|
/>
|
||||||
|
</small>
|
||||||
<div className="amounts">
|
<div className="amounts">
|
||||||
{(isFiat ? usdAmounts : satsAmounts).map((a) => (
|
{(isFiat ? usdAmounts : satsAmounts).map(a => (
|
||||||
<span
|
<span key={a} className={`pill${a === amount ? " active" : ""}`} onClick={() => setAmount(a)}>
|
||||||
key={a}
|
|
||||||
className={`pill${a === amount ? " active" : ""}`}
|
|
||||||
onClick={() => setAmount(a)}
|
|
||||||
>
|
|
||||||
{isFiat ? `$${a.toLocaleString()}` : formatSats(a)}
|
{isFiat ? `$${a.toLocaleString()}` : formatSats(a)}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
@ -164,19 +144,17 @@ export function SendZaps({
|
|||||||
</div>
|
</div>
|
||||||
{svc && (svc.maxCommentLength > 0 || svc.canZap) && (
|
{svc && (svc.maxCommentLength > 0 || svc.canZap) && (
|
||||||
<div>
|
<div>
|
||||||
<small>Your comment for {name}</small>
|
<small>
|
||||||
|
<FormattedMessage defaultMessage="Your comment for {name}" values={{ name }} />
|
||||||
|
</small>
|
||||||
<div className="paper">
|
<div className="paper">
|
||||||
<textarea
|
<textarea placeholder="Nice!" value={comment} onChange={e => setComment(e.target.value)} />
|
||||||
placeholder="Nice!"
|
|
||||||
value={comment}
|
|
||||||
onChange={(e) => setComment(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<AsyncButton onClick={send} className="btn btn-primary">
|
<AsyncButton onClick={send} className="btn btn-primary">
|
||||||
Zap!
|
<FormattedMessage defaultMessage="Zap!" />
|
||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@ -194,7 +172,7 @@ export function SendZaps({
|
|||||||
<Copy text={invoice} />
|
<Copy text={invoice} />
|
||||||
</div>
|
</div>
|
||||||
<button className="btn btn-primary wide" onClick={() => onFinish()}>
|
<button className="btn btn-primary wide" onClick={() => onFinish()}>
|
||||||
Back
|
<FormattedMessage defaultMessage="Back" />
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -203,7 +181,7 @@ export function SendZaps({
|
|||||||
return (
|
return (
|
||||||
<div className="send-zap">
|
<div className="send-zap">
|
||||||
<h3>
|
<h3>
|
||||||
Zap {name}
|
<FormattedMessage defaultMessage="Zap {name}" values={{ name }} />
|
||||||
<Icon name="zap" />
|
<Icon name="zap" />
|
||||||
</h3>
|
</h3>
|
||||||
{input()}
|
{input()}
|
||||||
@ -221,7 +199,9 @@ export function SendZapsDialog(props: Omit<SendZapsProps, "onFinish">) {
|
|||||||
props.button
|
props.button
|
||||||
) : (
|
) : (
|
||||||
<button className="btn btn-primary zap">
|
<button className="btn btn-primary zap">
|
||||||
<span className="hide-on-mobile">Zap</span>
|
<span className="hide-on-mobile">
|
||||||
|
<FormattedMessage defaultMessage="Zap" />
|
||||||
|
</span>
|
||||||
<Icon name="zap-filled" size={16} />
|
<Icon name="zap-filled" size={16} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
@ -10,6 +10,7 @@ import { findTag } from "utils";
|
|||||||
import AsyncButton from "./async-button";
|
import AsyncButton from "./async-button";
|
||||||
import { useLogin } from "hooks/login";
|
import { useLogin } from "hooks/login";
|
||||||
import { System } from "index";
|
import { System } from "index";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
type ShareOn = "nostr" | "twitter";
|
type ShareOn = "nostr" | "twitter";
|
||||||
|
|
||||||
@ -18,13 +19,7 @@ export function ShareMenu({ ev }: { ev: NostrEvent }) {
|
|||||||
const [message, setMessage] = useState("");
|
const [message, setMessage] = useState("");
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
|
|
||||||
const naddr = encodeTLV(
|
const naddr = encodeTLV(NostrPrefix.Address, unwrap(findTag(ev, "d")), undefined, ev.kind, ev.pubkey);
|
||||||
NostrPrefix.Address,
|
|
||||||
unwrap(findTag(ev, "d")),
|
|
||||||
undefined,
|
|
||||||
ev.kind,
|
|
||||||
ev.pubkey
|
|
||||||
);
|
|
||||||
const link = `https://zap.stream/${naddr}`;
|
const link = `https://zap.stream/${naddr}`;
|
||||||
|
|
||||||
async function sendMessage() {
|
async function sendMessage() {
|
||||||
@ -45,35 +40,30 @@ export function ShareMenu({ ev }: { ev: NostrEvent }) {
|
|||||||
menuClassName="ctx-menu"
|
menuClassName="ctx-menu"
|
||||||
menuButton={
|
menuButton={
|
||||||
<button type="button" className="btn btn-secondary">
|
<button type="button" className="btn btn-secondary">
|
||||||
Share
|
<FormattedMessage defaultMessage="Share" />
|
||||||
</button>
|
</button>
|
||||||
}
|
}>
|
||||||
>
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setMessage(
|
setMessage(`Come check out my stream on zap.stream!\n\n${link}\n\nnostr:${naddr}`);
|
||||||
`Come check out my stream on zap.stream!\n\n${link}\n\nnostr:${naddr}`
|
|
||||||
);
|
|
||||||
setShare("nostr");
|
setShare("nostr");
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
<Icon name="nostrich" size={24} />
|
<Icon name="nostrich" size={24} />
|
||||||
Broadcast on Nostr
|
<FormattedMessage defaultMessage="Broadcast on Nostr" />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
<Dialog.Root
|
<Dialog.Root open={Boolean(share)} onOpenChange={() => setShare(undefined)}>
|
||||||
open={Boolean(share)}
|
|
||||||
onOpenChange={() => setShare(undefined)}
|
|
||||||
>
|
|
||||||
<Dialog.Portal>
|
<Dialog.Portal>
|
||||||
<Dialog.Overlay className="dialog-overlay" />
|
<Dialog.Overlay className="dialog-overlay" />
|
||||||
<Dialog.Content className="dialog-content">
|
<Dialog.Content className="dialog-content">
|
||||||
<h2>Share</h2>
|
<h2>
|
||||||
|
<FormattedMessage defaultMessage="Share" />
|
||||||
|
</h2>
|
||||||
<div className="paper">
|
<div className="paper">
|
||||||
<Textarea
|
<Textarea
|
||||||
emojis={[]}
|
emojis={[]}
|
||||||
value={message}
|
value={message}
|
||||||
onChange={(e) => setMessage(e.target.value)}
|
onChange={e => setMessage(e.target.value)}
|
||||||
onKeyDown={() => {
|
onKeyDown={() => {
|
||||||
//noop
|
//noop
|
||||||
}}
|
}}
|
||||||
@ -81,7 +71,7 @@ export function ShareMenu({ ev }: { ev: NostrEvent }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<AsyncButton className="btn btn-primary" onClick={sendMessage}>
|
<AsyncButton className="btn btn-primary" onClick={sendMessage}>
|
||||||
Send
|
<FormattedMessage defaultMessage="Send" />
|
||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
</Dialog.Portal>
|
</Dialog.Portal>
|
||||||
|
@ -7,13 +7,7 @@ export interface IconProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Spinner = (props: IconProps) => (
|
const Spinner = (props: IconProps) => (
|
||||||
<svg
|
<svg width="20" height="20" stroke="currentColor" viewBox="0 0 20 20" {...props}>
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<g className="spinner_V8m1">
|
<g className="spinner_V8m1">
|
||||||
<circle cx="10" cy="10" r="7.5" fill="none" strokeWidth="3"></circle>
|
<circle cx="10" cy="10" r="7.5" fill="none" strokeWidth="3"></circle>
|
||||||
</g>
|
</g>
|
||||||
|
@ -2,9 +2,5 @@ import "./state-pill.css";
|
|||||||
import { StreamState } from "index";
|
import { StreamState } from "index";
|
||||||
|
|
||||||
export function StatePill({ state }: { state: StreamState }) {
|
export function StatePill({ state }: { state: StreamState }) {
|
||||||
return (
|
return <span className={`state pill${state === StreamState.Live ? " live" : ""}`}>{state}</span>;
|
||||||
<span className={`state pill${state === StreamState.Live ? " live" : ""}`}>
|
|
||||||
{state}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import "./stream-cards.css";
|
import "./stream-cards.css";
|
||||||
|
|
||||||
import { useState, forwardRef } from "react";
|
import { useState, forwardRef } from "react";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import * as Dialog from "@radix-ui/react-dialog";
|
import * as Dialog from "@radix-ui/react-dialog";
|
||||||
import { DndProvider, useDrag, useDrop } from "react-dnd";
|
import { DndProvider, useDrag, useDrop } from "react-dnd";
|
||||||
import { HTML5Backend } from "react-dnd-html5-backend";
|
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||||
|
|
||||||
import type { TaggedRawEvent } from "@snort/system";
|
import { TaggedNostrEvent } from "@snort/system";
|
||||||
|
|
||||||
import { Toggle } from "element/toggle";
|
import { Toggle } from "element/toggle";
|
||||||
import { Icon } from "element/icon";
|
import { Icon } from "element/icon";
|
||||||
@ -37,35 +38,32 @@ interface CardPreviewProps extends NewCard {
|
|||||||
style: object;
|
style: object;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CardPreview = forwardRef(
|
const CardPreview = forwardRef(({ style, title, link, image, content }: CardPreviewProps, ref) => {
|
||||||
({ style, title, link, image, content }: CardPreviewProps, ref) => {
|
const isImageOnly = !isEmpty(image) && isEmpty(content) && isEmpty(title);
|
||||||
const isImageOnly = !isEmpty(image) && isEmpty(content) && isEmpty(title);
|
return (
|
||||||
return (
|
<div
|
||||||
<div
|
className={`stream-card ${isImageOnly ? "image-card" : ""}`}
|
||||||
className={`stream-card ${isImageOnly ? "image-card" : ""}`}
|
// @ts-expect-error: Type 'ForwardRef<unknown>'
|
||||||
// @ts-expect-error: Type 'ForwardRef<unknown>'
|
ref={ref}
|
||||||
ref={ref}
|
style={style}>
|
||||||
style={style}
|
{title && <h1 className="card-title">{title}</h1>}
|
||||||
>
|
{image &&
|
||||||
{title && <h1 className="card-title">{title}</h1>}
|
(link && link?.length > 0 ? (
|
||||||
{image &&
|
<ExternalLink href={link}>
|
||||||
(link && link?.length > 0 ? (
|
|
||||||
<ExternalLink href={link}>
|
|
||||||
<img className="card-image" src={image} alt={title} />
|
|
||||||
</ExternalLink>
|
|
||||||
) : (
|
|
||||||
<img className="card-image" src={image} alt={title} />
|
<img className="card-image" src={image} alt={title} />
|
||||||
))}
|
</ExternalLink>
|
||||||
<Markdown content={content} />
|
) : (
|
||||||
</div>
|
<img className="card-image" src={image} alt={title} />
|
||||||
);
|
))}
|
||||||
}
|
<Markdown content={content} />
|
||||||
);
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
interface CardProps {
|
interface CardProps {
|
||||||
canEdit?: boolean;
|
canEdit?: boolean;
|
||||||
ev: TaggedRawEvent;
|
ev: TaggedNostrEvent;
|
||||||
cards: TaggedRawEvent[];
|
cards: TaggedNostrEvent[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CardItem {
|
interface CardItem {
|
||||||
@ -88,7 +86,7 @@ function Card({ canEdit, ev, cards }: CardProps) {
|
|||||||
canDrag: () => {
|
canDrag: () => {
|
||||||
return Boolean(canEdit);
|
return Boolean(canEdit);
|
||||||
},
|
},
|
||||||
collect: (monitor) => {
|
collect: monitor => {
|
||||||
const isDragging = monitor.isDragging();
|
const isDragging = monitor.isDragging();
|
||||||
return {
|
return {
|
||||||
opacity: isDragging ? 0.1 : 1,
|
opacity: isDragging ? 0.1 : 1,
|
||||||
@ -100,7 +98,7 @@ function Card({ canEdit, ev, cards }: CardProps) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
function findTagByIdentifier(d: string) {
|
function findTagByIdentifier(d: string) {
|
||||||
return tags.find((t) => t[1].endsWith(`:${d}`));
|
return tags.find(t => t[1].endsWith(`:${d}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
const [dropStyle, dropRef] = useDrop(
|
const [dropStyle, dropRef] = useDrop(
|
||||||
@ -109,7 +107,7 @@ function Card({ canEdit, ev, cards }: CardProps) {
|
|||||||
canDrop: () => {
|
canDrop: () => {
|
||||||
return Boolean(canEdit);
|
return Boolean(canEdit);
|
||||||
},
|
},
|
||||||
collect: (monitor) => {
|
collect: monitor => {
|
||||||
const isOvering = monitor.isOver({ shallow: true });
|
const isOvering = monitor.isOver({ shallow: true });
|
||||||
return {
|
return {
|
||||||
opacity: isOvering ? 0.3 : 1,
|
opacity: isOvering ? 0.3 : 1,
|
||||||
@ -123,7 +121,7 @@ function Card({ canEdit, ev, cards }: CardProps) {
|
|||||||
}
|
}
|
||||||
const newItem = findTagByIdentifier(typed.identifier);
|
const newItem = findTagByIdentifier(typed.identifier);
|
||||||
const oldItem = findTagByIdentifier(identifier);
|
const oldItem = findTagByIdentifier(identifier);
|
||||||
const newTags = tags.map((t) => {
|
const newTags = tags.map(t => {
|
||||||
if (t === oldItem) {
|
if (t === oldItem) {
|
||||||
return newItem;
|
return newItem;
|
||||||
}
|
}
|
||||||
@ -134,7 +132,7 @@ function Card({ canEdit, ev, cards }: CardProps) {
|
|||||||
}) as Tags;
|
}) as Tags;
|
||||||
const pub = login?.publisher();
|
const pub = login?.publisher();
|
||||||
if (pub) {
|
if (pub) {
|
||||||
const userCardsEv = await pub.generic((eb) => {
|
const userCardsEv = await pub.generic(eb => {
|
||||||
eb.kind(USER_CARDS).content("");
|
eb.kind(USER_CARDS).content("");
|
||||||
for (const tag of newTags) {
|
for (const tag of newTags) {
|
||||||
eb.tag(tag);
|
eb.tag(tag);
|
||||||
@ -151,14 +149,7 @@ function Card({ canEdit, ev, cards }: CardProps) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const card = (
|
const card = (
|
||||||
<CardPreview
|
<CardPreview ref={dropRef} title={title} link={link} image={image} content={content} style={dropStyle} />
|
||||||
ref={dropRef}
|
|
||||||
title={title}
|
|
||||||
link={link}
|
|
||||||
image={image}
|
|
||||||
content={content}
|
|
||||||
style={dropStyle}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
const editor = canEdit && (
|
const editor = canEdit && (
|
||||||
<div className="editor-buttons">
|
<div className="editor-buttons">
|
||||||
@ -184,14 +175,7 @@ interface CardDialogProps {
|
|||||||
onCancel(): void;
|
onCancel(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardDialog({
|
function CardDialog({ header, cta, cancelCta, card, onSave, onCancel }: CardDialogProps) {
|
||||||
header,
|
|
||||||
cta,
|
|
||||||
cancelCta,
|
|
||||||
card,
|
|
||||||
onSave,
|
|
||||||
onCancel,
|
|
||||||
}: CardDialogProps) {
|
|
||||||
const [title, setTitle] = useState(card?.title ?? "");
|
const [title, setTitle] = useState(card?.title ?? "");
|
||||||
const [image, setImage] = useState(card?.image ?? "");
|
const [image, setImage] = useState(card?.image ?? "");
|
||||||
const [content, setContent] = useState(card?.content ?? "");
|
const [content, setContent] = useState(card?.content ?? "");
|
||||||
@ -199,58 +183,63 @@ function CardDialog({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="new-card">
|
<div className="new-card">
|
||||||
<h3>{header || "Add card"}</h3>
|
<h3>
|
||||||
|
{header || <FormattedMessage defaultMessage="Add card" />}
|
||||||
|
</h3>
|
||||||
<div className="form-control">
|
<div className="form-control">
|
||||||
<label htmlFor="card-title">Title</label>
|
<label htmlFor="card-title">
|
||||||
|
<FormattedMessage defaultMessage="Title" />
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
id="card-title"
|
id="card-title"
|
||||||
type="text"
|
type="text"
|
||||||
value={title}
|
value={title}
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
onChange={e => setTitle(e.target.value)}
|
||||||
placeholder="e.g. about me"
|
placeholder="e.g. about me"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-control">
|
<div className="form-control">
|
||||||
<label htmlFor="card-image">Image</label>
|
<label htmlFor="card-image">
|
||||||
<FileUploader
|
<FormattedMessage defaultMessage="Image" />
|
||||||
defaultImage={image}
|
</label>
|
||||||
onFileUpload={setImage}
|
<FileUploader defaultImage={image} onFileUpload={setImage} onClear={() => setImage("")} />
|
||||||
onClear={() => setImage("")}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-control">
|
<div className="form-control">
|
||||||
<label htmlFor="card-image-link">Image Link</label>
|
<label htmlFor="card-image-link">
|
||||||
|
<FormattedMessage defaultMessage="Image Link" />
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
id="card-image-link"
|
id="card-image-link"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="https://"
|
placeholder="https://"
|
||||||
value={link}
|
value={link}
|
||||||
onChange={(e) => setLink(e.target.value)}
|
onChange={e => setLink(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-control">
|
<div className="form-control">
|
||||||
<label htmlFor="card-content">Content</label>
|
<label htmlFor="card-content">
|
||||||
<textarea
|
<FormattedMessage defaultMessage="Content" />
|
||||||
placeholder="Start typing..."
|
</label>
|
||||||
value={content}
|
<textarea placeholder="Start typing..." value={content} onChange={e => setContent(e.target.value)} />
|
||||||
onChange={(e) => setContent(e.target.value)}
|
|
||||||
/>
|
|
||||||
<span className="help-text">
|
<span className="help-text">
|
||||||
Supports{" "}
|
<FormattedMessage
|
||||||
<ExternalLink href="https://www.markdownguide.org/cheat-sheet">
|
defaultMessage="Supports {markdown}"
|
||||||
Markdown
|
values={{
|
||||||
</ExternalLink>
|
markdown: (
|
||||||
|
<ExternalLink href="https://www.markdownguide.org/cheat-sheet">
|
||||||
|
<FormattedMessage defaultMessage="Markdown" />
|
||||||
|
</ExternalLink>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="new-card-buttons">
|
<div className="new-card-buttons">
|
||||||
<button
|
<button className="btn btn-primary add-button" onClick={() => onSave({ title, image, content, link })}>
|
||||||
className="btn btn-primary add-button"
|
{cta || <FormattedMessage defaultMessage="Add Card" />}
|
||||||
onClick={() => onSave({ title, image, content, link })}
|
|
||||||
>
|
|
||||||
{cta || "Add Card"}
|
|
||||||
</button>
|
</button>
|
||||||
<button className="btn delete-button" onClick={onCancel}>
|
<button className="btn delete-button" onClick={onCancel}>
|
||||||
{cancelCta || "Cancel"}
|
{cancelCta || <FormattedMessage defaultMessage="Cancel" />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -259,7 +248,7 @@ function CardDialog({
|
|||||||
|
|
||||||
interface EditCardProps {
|
interface EditCardProps {
|
||||||
card: CardType;
|
card: CardType;
|
||||||
cards: TaggedRawEvent[];
|
cards: TaggedNostrEvent[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditCard({ card, cards }: EditCardProps) {
|
function EditCard({ card, cards }: EditCardProps) {
|
||||||
@ -267,11 +256,12 @@ function EditCard({ card, cards }: EditCardProps) {
|
|||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const identifier = card.identifier;
|
const identifier = card.identifier;
|
||||||
const tags = cards.map(toTag);
|
const tags = cards.map(toTag);
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
async function editCard({ title, image, link, content }: CardType) {
|
async function editCard({ title, image, link, content }: CardType) {
|
||||||
const pub = login?.publisher();
|
const pub = login?.publisher();
|
||||||
if (pub) {
|
if (pub) {
|
||||||
const ev = await pub.generic((eb) => {
|
const ev = await pub.generic(eb => {
|
||||||
eb.kind(CARD).content(content).tag(["d", card.identifier]);
|
eb.kind(CARD).content(content).tag(["d", card.identifier]);
|
||||||
if (title && title?.length > 0) {
|
if (title && title?.length > 0) {
|
||||||
eb.tag(["title", title]);
|
eb.tag(["title", title]);
|
||||||
@ -293,8 +283,8 @@ function EditCard({ card, cards }: EditCardProps) {
|
|||||||
async function onCancel() {
|
async function onCancel() {
|
||||||
const pub = login?.publisher();
|
const pub = login?.publisher();
|
||||||
if (pub) {
|
if (pub) {
|
||||||
const newTags = tags.filter((t) => !t[1].endsWith(`:${identifier}`));
|
const newTags = tags.filter(t => !t[1].endsWith(`:${identifier}`));
|
||||||
const userCardsEv = await pub.generic((eb) => {
|
const userCardsEv = await pub.generic(eb => {
|
||||||
eb.kind(USER_CARDS).content("");
|
eb.kind(USER_CARDS).content("");
|
||||||
for (const tag of newTags) {
|
for (const tag of newTags) {
|
||||||
eb.tag(tag);
|
eb.tag(tag);
|
||||||
@ -312,15 +302,17 @@ function EditCard({ card, cards }: EditCardProps) {
|
|||||||
return (
|
return (
|
||||||
<Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<Dialog.Trigger asChild>
|
<Dialog.Trigger asChild>
|
||||||
<button className="btn btn-primary">Edit</button>
|
<button className="btn btn-primary">
|
||||||
|
<FormattedMessage defaultMessage="Edit" />
|
||||||
|
</button>
|
||||||
</Dialog.Trigger>
|
</Dialog.Trigger>
|
||||||
<Dialog.Portal>
|
<Dialog.Portal>
|
||||||
<Dialog.Overlay className="dialog-overlay" />
|
<Dialog.Overlay className="dialog-overlay" />
|
||||||
<Dialog.Content className="dialog-content">
|
<Dialog.Content className="dialog-content">
|
||||||
<CardDialog
|
<CardDialog
|
||||||
header="Edit card"
|
header={formatMessage({ defaultMessage: "Edit card" })}
|
||||||
cta="Save Card"
|
cta={formatMessage({ defaultMessage: "Save card" })}
|
||||||
cancelCta="Delete"
|
cancelCta={formatMessage({ defaultMessage: "Delete" })}
|
||||||
card={card}
|
card={card}
|
||||||
onSave={editCard}
|
onSave={editCard}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
@ -332,7 +324,7 @@ function EditCard({ card, cards }: EditCardProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface AddCardProps {
|
interface AddCardProps {
|
||||||
cards: TaggedRawEvent[];
|
cards: TaggedNostrEvent[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function AddCard({ cards }: AddCardProps) {
|
function AddCard({ cards }: AddCardProps) {
|
||||||
@ -343,7 +335,7 @@ function AddCard({ cards }: AddCardProps) {
|
|||||||
async function createCard({ title, image, link, content }: NewCard) {
|
async function createCard({ title, image, link, content }: NewCard) {
|
||||||
const pub = login?.publisher();
|
const pub = login?.publisher();
|
||||||
if (pub) {
|
if (pub) {
|
||||||
const ev = await pub.generic((eb) => {
|
const ev = await pub.generic(eb => {
|
||||||
const d = String(Date.now());
|
const d = String(Date.now());
|
||||||
eb.kind(CARD).content(content).tag(["d", d]);
|
eb.kind(CARD).content(content).tag(["d", d]);
|
||||||
if (title && title?.length > 0) {
|
if (title && title?.length > 0) {
|
||||||
@ -357,7 +349,7 @@ function AddCard({ cards }: AddCardProps) {
|
|||||||
}
|
}
|
||||||
return eb;
|
return eb;
|
||||||
});
|
});
|
||||||
const userCardsEv = await pub.generic((eb) => {
|
const userCardsEv = await pub.generic(eb => {
|
||||||
eb.kind(USER_CARDS).content("");
|
eb.kind(USER_CARDS).content("");
|
||||||
for (const tag of tags) {
|
for (const tag of tags) {
|
||||||
eb.tag(tag);
|
eb.tag(tag);
|
||||||
@ -407,18 +399,13 @@ export function StreamCardEditor({ pubkey, tags }: StreamCardEditorProps) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="stream-cards">
|
<div className="stream-cards">
|
||||||
{cards.map((ev) => (
|
{cards.map(ev => (
|
||||||
<Card canEdit={isEditing} cards={cards} key={ev.id} ev={ev} />
|
<Card canEdit={isEditing} cards={cards} key={ev.id} ev={ev} />
|
||||||
))}
|
))}
|
||||||
{isEditing && <AddCard cards={cards} />}
|
{isEditing && <AddCard cards={cards} />}
|
||||||
</div>
|
</div>
|
||||||
<div className="edit-container">
|
<div className="edit-container">
|
||||||
<Toggle
|
<Toggle pressed={isEditing} onPressedChange={setIsEditing} label="Toggle edit mode" text="Edit cards" />
|
||||||
pressed={isEditing}
|
|
||||||
onPressedChange={setIsEditing}
|
|
||||||
label="Toggle edit mode"
|
|
||||||
text="Edit cards"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -432,7 +419,7 @@ export function ReadOnlyStreamCards({ host }: StreamCardsProps) {
|
|||||||
const cards = useCards(host);
|
const cards = useCards(host);
|
||||||
return (
|
return (
|
||||||
<div className="stream-cards">
|
<div className="stream-cards">
|
||||||
{cards.map((ev) => (
|
{cards.map(ev => (
|
||||||
<Card cards={cards} key={ev.id} ev={ev} />
|
<Card cards={cards} key={ev.id} ev={ev} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,6 +8,7 @@ import AsyncButton from "./async-button";
|
|||||||
import { StreamState } from "../index";
|
import { StreamState } from "../index";
|
||||||
import { findTag } from "../utils";
|
import { findTag } from "../utils";
|
||||||
import { useLogin } from "hooks/login";
|
import { useLogin } from "hooks/login";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
|
||||||
export interface StreamEditorProps {
|
export interface StreamEditorProps {
|
||||||
ev?: NostrEvent;
|
ev?: NostrEvent;
|
||||||
@ -34,6 +35,7 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
|
|||||||
const [contentWarning, setContentWarning] = useState(false);
|
const [contentWarning, setContentWarning] = useState(false);
|
||||||
const [isValid, setIsValid] = useState(false);
|
const [isValid, setIsValid] = useState(false);
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTitle(findTag(ev, "title") ?? "");
|
setTitle(findTag(ev, "title") ?? "");
|
||||||
@ -42,7 +44,7 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
|
|||||||
setStream(findTag(ev, "streaming") ?? "");
|
setStream(findTag(ev, "streaming") ?? "");
|
||||||
setStatus(findTag(ev, "status") ?? StreamState.Live);
|
setStatus(findTag(ev, "status") ?? StreamState.Live);
|
||||||
setStart(findTag(ev, "starts"));
|
setStart(findTag(ev, "starts"));
|
||||||
setTags(ev?.tags.filter((a) => a[0] === "t").map((a) => a[1]) ?? []);
|
setTags(ev?.tags.filter(a => a[0] === "t").map(a => a[1]) ?? []);
|
||||||
setContentWarning(findTag(ev, "content-warning") !== undefined);
|
setContentWarning(findTag(ev, "content-warning") !== undefined);
|
||||||
}, [ev?.id]);
|
}, [ev?.id]);
|
||||||
|
|
||||||
@ -66,7 +68,7 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
|
|||||||
async function publishStream() {
|
async function publishStream() {
|
||||||
const pub = login?.publisher();
|
const pub = login?.publisher();
|
||||||
if (pub) {
|
if (pub) {
|
||||||
const evNew = await pub.generic((eb) => {
|
const evNew = await pub.generic(eb => {
|
||||||
const now = unixNow();
|
const now = unixNow();
|
||||||
const dTag = findTag(ev, "d") ?? now.toString();
|
const dTag = findTag(ev, "d") ?? now.toString();
|
||||||
const starts = start ?? now.toString();
|
const starts = start ?? now.toString();
|
||||||
@ -108,85 +110,81 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
|
|||||||
<h3>{ev ? "Edit Stream" : "New Stream"}</h3>
|
<h3>{ev ? "Edit Stream" : "New Stream"}</h3>
|
||||||
{(options?.canSetTitle ?? true) && (
|
{(options?.canSetTitle ?? true) && (
|
||||||
<div>
|
<div>
|
||||||
<p>Title</p>
|
<p>
|
||||||
|
<FormattedMessage defaultMessage="Title" />
|
||||||
|
</p>
|
||||||
<div className="paper">
|
<div className="paper">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="What are we steaming today?"
|
placeholder={formatMessage({ defaultMessage: "What are we steaming today?" })}
|
||||||
value={title}
|
value={title}
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
onChange={e => setTitle(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(options?.canSetSummary ?? true) && (
|
{(options?.canSetSummary ?? true) && (
|
||||||
<div>
|
<div>
|
||||||
<p>Summary</p>
|
<p>
|
||||||
|
<FormattedMessage defaultMessage="Summary" />
|
||||||
|
</p>
|
||||||
<div className="paper">
|
<div className="paper">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="A short description of the content"
|
placeholder={formatMessage({ defaultMessage: "A short description of the content" })}
|
||||||
value={summary}
|
value={summary}
|
||||||
onChange={(e) => setSummary(e.target.value)}
|
onChange={e => setSummary(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(options?.canSetImage ?? true) && (
|
{(options?.canSetImage ?? true) && (
|
||||||
<div>
|
<div>
|
||||||
<p>Cover image</p>
|
<p>
|
||||||
|
<FormattedMessage defaultMessage="Cover Image" />
|
||||||
|
</p>
|
||||||
<div className="paper">
|
<div className="paper">
|
||||||
<input
|
<input type="text" placeholder="https://" value={image} onChange={e => setImage(e.target.value)} />
|
||||||
type="text"
|
|
||||||
placeholder="https://"
|
|
||||||
value={image}
|
|
||||||
onChange={(e) => setImage(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(options?.canSetStream ?? true) && (
|
{(options?.canSetStream ?? true) && (
|
||||||
<div>
|
<div>
|
||||||
<p>Stream Url</p>
|
<p>
|
||||||
|
<FormattedMessage defaultMessage="Stream URL" />
|
||||||
|
</p>
|
||||||
<div className="paper">
|
<div className="paper">
|
||||||
<input
|
<input type="text" placeholder="https://" value={stream} onChange={e => setStream(e.target.value)} />
|
||||||
type="text"
|
|
||||||
placeholder="https://"
|
|
||||||
value={stream}
|
|
||||||
onChange={(e) => setStream(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<small>Stream type should be HLS</small>
|
<small>
|
||||||
|
<FormattedMessage defaultMessage="Stream type should be HLS" />
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(options?.canSetStatus ?? true) && (
|
{(options?.canSetStatus ?? true) && (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<p>Status</p>
|
<p>
|
||||||
|
<FormattedMessage defaultMessage="Status" />
|
||||||
|
</p>
|
||||||
<div className="flex g12">
|
<div className="flex g12">
|
||||||
{[StreamState.Live, StreamState.Planned, StreamState.Ended].map(
|
{[StreamState.Live, StreamState.Planned, StreamState.Ended].map(v => (
|
||||||
(v) => (
|
<span className={`pill${status === v ? " active" : ""}`} onClick={() => setStatus(v)} key={v}>
|
||||||
<span
|
{v}
|
||||||
className={`pill${status === v ? " active" : ""}`}
|
</span>
|
||||||
onClick={() => setStatus(v)}
|
))}
|
||||||
key={v}
|
|
||||||
>
|
|
||||||
{v}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{status === StreamState.Planned && (
|
{status === StreamState.Planned && (
|
||||||
<div>
|
<div>
|
||||||
<p>Start Time</p>
|
<p>
|
||||||
|
<FormattedMessage defaultMessage="Start Time" />
|
||||||
|
</p>
|
||||||
<div className="paper">
|
<div className="paper">
|
||||||
<input
|
<input
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
value={toDateTimeString(Number(start ?? "0"))}
|
value={toDateTimeString(Number(start ?? "0"))}
|
||||||
onChange={(e) =>
|
onChange={e => setStart(fromDateTimeString(e.target.value).toString())}
|
||||||
setStart(fromDateTimeString(e.target.value).toString())
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -195,40 +193,30 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
|
|||||||
)}
|
)}
|
||||||
{(options?.canSetTags ?? true) && (
|
{(options?.canSetTags ?? true) && (
|
||||||
<div>
|
<div>
|
||||||
<p>Tags</p>
|
<p>
|
||||||
|
<FormattedMessage defaultMessage="Tags" />
|
||||||
|
</p>
|
||||||
<div className="paper">
|
<div className="paper">
|
||||||
<TagsInput
|
<TagsInput value={tags} onChange={setTags} placeHolder="Music,DJ,English" separators={["Enter", ","]} />
|
||||||
value={tags}
|
|
||||||
onChange={setTags}
|
|
||||||
placeHolder="Music,DJ,English"
|
|
||||||
separators={["Enter", ","]}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(options?.canSetContentWarning ?? true) && (
|
{(options?.canSetContentWarning ?? true) && (
|
||||||
<div className="flex g12 content-warning">
|
<div className="flex g12 content-warning">
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input type="checkbox" checked={contentWarning} onChange={e => setContentWarning(e.target.checked)} />
|
||||||
type="checkbox"
|
|
||||||
checked={contentWarning}
|
|
||||||
onChange={(e) => setContentWarning(e.target.checked)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="warning">NSFW Content</div>
|
<div className="warning">
|
||||||
Check here if this stream contains nudity or pornographic content.
|
<FormattedMessage defaultMessage="NSFW Content" />
|
||||||
|
</div>
|
||||||
|
<FormattedMessage defaultMessage="Check here if this stream contains nudity or pornographic content." />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<AsyncButton
|
<AsyncButton type="button" className="btn btn-primary wide" disabled={!isValid} onClick={publishStream}>
|
||||||
type="button"
|
{ev ? <FormattedMessage defaultMessage="Save" /> : <FormattedMessage defaultMessage="Start Stream" />}
|
||||||
className="btn btn-primary wide"
|
|
||||||
disabled={!isValid}
|
|
||||||
onClick={publishStream}
|
|
||||||
>
|
|
||||||
{ev ? "Save" : "Start Stream"}
|
|
||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -11,9 +11,7 @@ export function StreamTimer({ ev }: { ev?: NostrEvent }) {
|
|||||||
const diff = unixNow() - starts;
|
const diff = unixNow() - starts;
|
||||||
const hours = Number(diff / 60.0 / 60.0);
|
const hours = Number(diff / 60.0 / 60.0);
|
||||||
const mins = Number((diff / 60) % 60);
|
const mins = Number((diff / 60) % 60);
|
||||||
setTime(
|
setTime(`${hours.toFixed(0).padStart(2, "0")}:${mins.toFixed(0).padStart(2, "0")}`);
|
||||||
`${hours.toFixed(0).padStart(2, "0")}:${mins.toFixed(0).padStart(2, "0")}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
|
||||||
import { NostrEvent } from "@snort/system";
|
import { NostrEvent } from "@snort/system";
|
||||||
@ -6,15 +7,7 @@ import { NostrEvent } from "@snort/system";
|
|||||||
import { StreamState } from "index";
|
import { StreamState } from "index";
|
||||||
import { findTag, getTagValues } from "utils";
|
import { findTag, getTagValues } from "utils";
|
||||||
|
|
||||||
export function Tags({
|
export function Tags({ children, max, ev }: { children?: ReactNode; max?: number; ev: NostrEvent }) {
|
||||||
children,
|
|
||||||
max,
|
|
||||||
ev,
|
|
||||||
}: {
|
|
||||||
children?: ReactNode;
|
|
||||||
max?: number;
|
|
||||||
ev: NostrEvent;
|
|
||||||
}) {
|
|
||||||
const status = findTag(ev, "status");
|
const status = findTag(ev, "status");
|
||||||
const start = findTag(ev, "starts");
|
const start = findTag(ev, "starts");
|
||||||
const hashtags = getTagValues(ev.tags, "t");
|
const hashtags = getTagValues(ev.tags, "t");
|
||||||
@ -25,11 +18,11 @@ export function Tags({
|
|||||||
{children}
|
{children}
|
||||||
{status === StreamState.Planned && (
|
{status === StreamState.Planned && (
|
||||||
<span className="pill">
|
<span className="pill">
|
||||||
{status === StreamState.Planned ? "Starts " : ""}
|
{status === StreamState.Planned ? <FormattedMessage defaultMessage="Starts " /> : ""}
|
||||||
{moment(Number(start) * 1000).fromNow()}
|
{moment(Number(start) * 1000).fromNow()}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{tags.map((a) => (
|
{tags.map(a => (
|
||||||
<a href={`/t/${encodeURIComponent(a)}`} className="pill" key={a}>
|
<a href={`/t/${encodeURIComponent(a)}`} className="pill" key={a}>
|
||||||
{a}
|
{a}
|
||||||
</a>
|
</a>
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
import { useMemo, type ReactNode, type FunctionComponent } from "react";
|
import { useMemo, type ReactNode, type FunctionComponent } from "react";
|
||||||
|
|
||||||
import {
|
import { type NostrLink, parseNostrLink, validateNostrLink } from "@snort/system";
|
||||||
type NostrLink,
|
|
||||||
parseNostrLink,
|
|
||||||
validateNostrLink,
|
|
||||||
} from "@snort/system";
|
|
||||||
|
|
||||||
import { Event } from "element/Event";
|
import { Event } from "element/Event";
|
||||||
import { Mention } from "element/mention";
|
import { Mention } from "element/mention";
|
||||||
@ -20,23 +16,17 @@ const EmojiRegex = /:([\w-]+):/g;
|
|||||||
|
|
||||||
function extractLinks(fragments: Fragment[]) {
|
function extractLinks(fragments: Fragment[]) {
|
||||||
return fragments
|
return fragments
|
||||||
.map((f) => {
|
.map(f => {
|
||||||
if (typeof f === "string") {
|
if (typeof f === "string") {
|
||||||
return splitByUrl(f).map((a) => {
|
return splitByUrl(f).map(a => {
|
||||||
const validateLink = () => {
|
const validateLink = () => {
|
||||||
const normalizedStr = a.toLowerCase();
|
const normalizedStr = a.toLowerCase();
|
||||||
|
|
||||||
if (
|
if (normalizedStr.startsWith("web+nostr:") || normalizedStr.startsWith("nostr:")) {
|
||||||
normalizedStr.startsWith("web+nostr:") ||
|
|
||||||
normalizedStr.startsWith("nostr:")
|
|
||||||
) {
|
|
||||||
return validateNostrLink(normalizedStr);
|
return validateNostrLink(normalizedStr);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return normalizedStr.startsWith("http:") || normalizedStr.startsWith("https:");
|
||||||
normalizedStr.startsWith("http:") ||
|
|
||||||
normalizedStr.startsWith("https:")
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (validateLink()) {
|
if (validateLink()) {
|
||||||
@ -52,10 +42,10 @@ function extractLinks(fragments: Fragment[]) {
|
|||||||
|
|
||||||
function extractEmoji(fragments: Fragment[], tags: string[][]) {
|
function extractEmoji(fragments: Fragment[], tags: string[][]) {
|
||||||
return fragments
|
return fragments
|
||||||
.map((f) => {
|
.map(f => {
|
||||||
if (typeof f === "string") {
|
if (typeof f === "string") {
|
||||||
return f.split(EmojiRegex).map((i) => {
|
return f.split(EmojiRegex).map(i => {
|
||||||
const t = tags.find((a) => a[0] === "emoji" && a[1] === i);
|
const t = tags.find(a => a[0] === "emoji" && a[1] === i);
|
||||||
if (t) {
|
if (t) {
|
||||||
return <Emoji name={t[1]} url={t[2]} />;
|
return <Emoji name={t[1]} url={t[2]} />;
|
||||||
} else {
|
} else {
|
||||||
@ -70,9 +60,9 @@ function extractEmoji(fragments: Fragment[], tags: string[][]) {
|
|||||||
|
|
||||||
function extractNprofiles(fragments: Fragment[]) {
|
function extractNprofiles(fragments: Fragment[]) {
|
||||||
return fragments
|
return fragments
|
||||||
.map((f) => {
|
.map(f => {
|
||||||
if (typeof f === "string") {
|
if (typeof f === "string") {
|
||||||
return f.split(/(nostr:nprofile1[a-z0-9]+)/g).map((i) => {
|
return f.split(/(nostr:nprofile1[a-z0-9]+)/g).map(i => {
|
||||||
if (i.startsWith("nostr:nprofile1")) {
|
if (i.startsWith("nostr:nprofile1")) {
|
||||||
try {
|
try {
|
||||||
const link = parseNostrLink(i.replace(NostrPrefixRegex, ""));
|
const link = parseNostrLink(i.replace(NostrPrefixRegex, ""));
|
||||||
@ -92,9 +82,9 @@ function extractNprofiles(fragments: Fragment[]) {
|
|||||||
|
|
||||||
function extractNpubs(fragments: Fragment[]) {
|
function extractNpubs(fragments: Fragment[]) {
|
||||||
return fragments
|
return fragments
|
||||||
.map((f) => {
|
.map(f => {
|
||||||
if (typeof f === "string") {
|
if (typeof f === "string") {
|
||||||
return f.split(/(nostr:npub1[a-z0-9]+)/g).map((i) => {
|
return f.split(/(nostr:npub1[a-z0-9]+)/g).map(i => {
|
||||||
if (i.startsWith("nostr:npub1")) {
|
if (i.startsWith("nostr:npub1")) {
|
||||||
try {
|
try {
|
||||||
const link = parseNostrLink(i.replace(NostrPrefixRegex, ""));
|
const link = parseNostrLink(i.replace(NostrPrefixRegex, ""));
|
||||||
@ -114,9 +104,9 @@ function extractNpubs(fragments: Fragment[]) {
|
|||||||
|
|
||||||
function extractNevents(fragments: Fragment[], Event: NostrComponent) {
|
function extractNevents(fragments: Fragment[], Event: NostrComponent) {
|
||||||
return fragments
|
return fragments
|
||||||
.map((f) => {
|
.map(f => {
|
||||||
if (typeof f === "string") {
|
if (typeof f === "string") {
|
||||||
return f.split(/(nostr:nevent1[a-z0-9]+)/g).map((i) => {
|
return f.split(/(nostr:nevent1[a-z0-9]+)/g).map(i => {
|
||||||
if (i.startsWith("nostr:nevent1")) {
|
if (i.startsWith("nostr:nevent1")) {
|
||||||
try {
|
try {
|
||||||
const link = parseNostrLink(i.replace(NostrPrefixRegex, ""));
|
const link = parseNostrLink(i.replace(NostrPrefixRegex, ""));
|
||||||
@ -136,9 +126,9 @@ function extractNevents(fragments: Fragment[], Event: NostrComponent) {
|
|||||||
|
|
||||||
function extractNaddrs(fragments: Fragment[], Address: NostrComponent) {
|
function extractNaddrs(fragments: Fragment[], Address: NostrComponent) {
|
||||||
return fragments
|
return fragments
|
||||||
.map((f) => {
|
.map(f => {
|
||||||
if (typeof f === "string") {
|
if (typeof f === "string") {
|
||||||
return f.split(/(nostr:naddr1[a-z0-9]+)/g).map((i) => {
|
return f.split(/(nostr:naddr1[a-z0-9]+)/g).map(i => {
|
||||||
if (i.startsWith("nostr:naddr1")) {
|
if (i.startsWith("nostr:naddr1")) {
|
||||||
try {
|
try {
|
||||||
const link = parseNostrLink(i.replace(NostrPrefixRegex, ""));
|
const link = parseNostrLink(i.replace(NostrPrefixRegex, ""));
|
||||||
@ -159,9 +149,9 @@ function extractNaddrs(fragments: Fragment[], Address: NostrComponent) {
|
|||||||
|
|
||||||
function extractNoteIds(fragments: Fragment[], Event: NostrComponent) {
|
function extractNoteIds(fragments: Fragment[], Event: NostrComponent) {
|
||||||
return fragments
|
return fragments
|
||||||
.map((f) => {
|
.map(f => {
|
||||||
if (typeof f === "string") {
|
if (typeof f === "string") {
|
||||||
return f.split(/(nostr:note1[a-z0-9]+)/g).map((i) => {
|
return f.split(/(nostr:note1[a-z0-9]+)/g).map(i => {
|
||||||
if (i.startsWith("nostr:note1")) {
|
if (i.startsWith("nostr:note1")) {
|
||||||
try {
|
try {
|
||||||
const link = parseNostrLink(i.replace(NostrPrefixRegex, ""));
|
const link = parseNostrLink(i.replace(NostrPrefixRegex, ""));
|
||||||
@ -189,11 +179,7 @@ const components: NostrComponents = {
|
|||||||
Event,
|
Event,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function transformText(
|
export function transformText(ps: Fragment[], tags: Array<string[]>, customComponents = components) {
|
||||||
ps: Fragment[],
|
|
||||||
tags: Array<string[]>,
|
|
||||||
customComponents = components
|
|
||||||
) {
|
|
||||||
let fragments = extractEmoji(ps, tags);
|
let fragments = extractEmoji(ps, tags);
|
||||||
fragments = extractNprofiles(fragments);
|
fragments = extractNprofiles(fragments);
|
||||||
fragments = extractNevents(fragments, customComponents.Event);
|
fragments = extractNevents(fragments, customComponents.Event);
|
||||||
@ -214,11 +200,7 @@ interface TextProps {
|
|||||||
export function Text({ content, tags, customComponents }: TextProps) {
|
export function Text({ content, tags, customComponents }: TextProps) {
|
||||||
// todo: RTL langugage support
|
// todo: RTL langugage support
|
||||||
const element = useMemo(() => {
|
const element = useMemo(() => {
|
||||||
return (
|
return <span className="text">{transformText([content], tags, customComponents)}</span>;
|
||||||
<span className="text">
|
|
||||||
{transformText([content], tags, customComponents)}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}, [content, tags]);
|
}, [content, tags]);
|
||||||
|
|
||||||
return <>{element}</>;
|
return <>{element}</>;
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import "./textarea.css";
|
import "./textarea.css";
|
||||||
import type { KeyboardEvent, ChangeEvent } from "react";
|
import type { KeyboardEvent, ChangeEvent } from "react";
|
||||||
import ReactTextareaAutocomplete, {
|
import ReactTextareaAutocomplete, { TriggerType } from "@webscopeio/react-textarea-autocomplete";
|
||||||
TriggerType,
|
|
||||||
} from "@webscopeio/react-textarea-autocomplete";
|
|
||||||
import "@webscopeio/react-textarea-autocomplete/style.css";
|
import "@webscopeio/react-textarea-autocomplete/style.css";
|
||||||
import uniqWith from "lodash/uniqWith";
|
import uniqWith from "lodash/uniqWith";
|
||||||
import isEqual from "lodash/isEqual";
|
import isEqual from "lodash/isEqual";
|
||||||
@ -59,7 +57,7 @@ export function Textarea({ emojis, ...props }: TextareaProps) {
|
|||||||
|
|
||||||
const emojiDataProvider = (token: string) => {
|
const emojiDataProvider = (token: string) => {
|
||||||
const results = emojis
|
const results = emojis
|
||||||
.map((t) => {
|
.map(t => {
|
||||||
return {
|
return {
|
||||||
name: t.at(1) || "",
|
name: t.at(1) || "",
|
||||||
url: t.at(2) || "",
|
url: t.at(2) || "",
|
||||||
@ -78,11 +76,8 @@ export function Textarea({ emojis, ...props }: TextareaProps) {
|
|||||||
"@": {
|
"@": {
|
||||||
afterWhitespace: true,
|
afterWhitespace: true,
|
||||||
dataProvider: userDataProvider,
|
dataProvider: userDataProvider,
|
||||||
component: (props: { entity: MetadataCache }) => (
|
component: (props: { entity: MetadataCache }) => <UserItem {...props.entity} />,
|
||||||
<UserItem {...props.entity} />
|
output: (item: { pubkey: string }) => `@${hexToBech32(NostrPrefix.PublicKey, item.pubkey)}`,
|
||||||
),
|
|
||||||
output: (item: { pubkey: string }) =>
|
|
||||||
`@${hexToBech32(NostrPrefix.PublicKey, item.pubkey)}`,
|
|
||||||
},
|
},
|
||||||
} as TriggerType<string | object>;
|
} as TriggerType<string | object>;
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ import { formatSats } from "number";
|
|||||||
import ZapStream from "../../public/zap-stream.svg";
|
import ZapStream from "../../public/zap-stream.svg";
|
||||||
import { isContentWarningAccepted } from "./content-warning";
|
import { isContentWarningAccepted } from "./content-warning";
|
||||||
import { Tags } from "element/tags";
|
import { Tags } from "element/tags";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
export function VideoTile({
|
export function VideoTile({
|
||||||
ev,
|
ev,
|
||||||
@ -26,37 +27,22 @@ export function VideoTile({
|
|||||||
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 viewers = findTag(ev, "current_participants");
|
||||||
const contentWarning =
|
const contentWarning = findTag(ev, "content-warning") && !isContentWarningAccepted();
|
||||||
findTag(ev, "content-warning") && !isContentWarningAccepted();
|
|
||||||
const host = getHost(ev);
|
const host = getHost(ev);
|
||||||
|
|
||||||
const link = encodeTLV(
|
const link = encodeTLV(NostrPrefix.Address, id, undefined, ev.kind, ev.pubkey);
|
||||||
NostrPrefix.Address,
|
|
||||||
id,
|
|
||||||
undefined,
|
|
||||||
ev.kind,
|
|
||||||
ev.pubkey
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<div className="video-tile-container">
|
<div className="video-tile-container">
|
||||||
<Link
|
<Link to={`/${link}`} className={`video-tile${contentWarning ? " nsfw" : ""}`} ref={ref} state={ev}>
|
||||||
to={`/${link}`}
|
|
||||||
className={`video-tile${contentWarning ? " nsfw" : ""}`}
|
|
||||||
ref={ref}
|
|
||||||
state={ev}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `url(${
|
backgroundImage: `url(${inView ? ((image?.length ?? 0) > 0 ? image : ZapStream) : ""})`,
|
||||||
inView ? ((image?.length ?? 0) > 0 ? image : ZapStream) : ""
|
}}></div>
|
||||||
})`,
|
|
||||||
}}
|
|
||||||
></div>
|
|
||||||
<span className="pill-box">
|
<span className="pill-box">
|
||||||
{showStatus && <StatePill state={status as StreamState} />}
|
{showStatus && <StatePill state={status as StreamState} />}
|
||||||
{viewers && (
|
{viewers && (
|
||||||
<span className="pill viewers">
|
<span className="pill viewers">
|
||||||
{formatSats(Number(viewers))} viewers
|
<FormattedMessage defaultMessage="{n} viewers" values={{ n: formatSats(Number(viewers)) }} />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { NostrLink, EventKind } from "@snort/system";
|
import { NostrLink, EventKind } from "@snort/system";
|
||||||
import React, { useRef, useState } from "react";
|
import React, { useRef, useState } from "react";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
import { useLogin } from "hooks/login";
|
import { useLogin } from "hooks/login";
|
||||||
import AsyncButton from "element/async-button";
|
import AsyncButton from "element/async-button";
|
||||||
@ -10,20 +11,14 @@ import type { EmojiPack, Emoji } from "types";
|
|||||||
import { System } from "index";
|
import { System } from "index";
|
||||||
import { LIVE_STREAM_CHAT } from "const";
|
import { LIVE_STREAM_CHAT } from "const";
|
||||||
|
|
||||||
export function WriteMessage({
|
export function WriteMessage({ link, emojiPacks }: { link: NostrLink; emojiPacks: EmojiPack[] }) {
|
||||||
link,
|
|
||||||
emojiPacks,
|
|
||||||
}: {
|
|
||||||
link: NostrLink;
|
|
||||||
emojiPacks: EmojiPack[];
|
|
||||||
}) {
|
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
const emojiRef = useRef(null);
|
const emojiRef = useRef(null);
|
||||||
const [chat, setChat] = useState("");
|
const [chat, setChat] = useState("");
|
||||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const emojis = emojiPacks.map((pack) => pack.emojis).flat();
|
const emojis = emojiPacks.map(pack => pack.emojis).flat();
|
||||||
const names = emojis.map((t) => t.at(1));
|
const names = emojis.map(t => t.at(1));
|
||||||
|
|
||||||
const topOffset = ref.current?.getBoundingClientRect().top;
|
const topOffset = ref.current?.getBoundingClientRect().top;
|
||||||
const leftOffset = ref.current?.getBoundingClientRect().left;
|
const leftOffset = ref.current?.getBoundingClientRect().left;
|
||||||
@ -39,10 +34,8 @@ export function WriteMessage({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const reply = await pub?.generic((eb) => {
|
const reply = await pub?.generic(eb => {
|
||||||
const emoji = [...emojiNames].map((name) =>
|
const emoji = [...emojiNames].map(name => emojis.find(e => e.at(1) === name));
|
||||||
emojis.find((e) => e.at(1) === name)
|
|
||||||
);
|
|
||||||
eb.kind(LIVE_STREAM_CHAT as EventKind)
|
eb.kind(LIVE_STREAM_CHAT as EventKind)
|
||||||
.content(chat)
|
.content(chat)
|
||||||
.tag(["a", `${link.kind}:${link.author}:${link.id}`, "", "root"])
|
.tag(["a", `${link.kind}:${link.author}:${link.id}`, "", "root"])
|
||||||
@ -86,12 +79,7 @@ export function WriteMessage({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="paper" ref={ref}>
|
<div className="paper" ref={ref}>
|
||||||
<Textarea
|
<Textarea emojis={emojis} value={chat} onKeyDown={onKeyDown} onChange={e => setChat(e.target.value)} />
|
||||||
emojis={emojis}
|
|
||||||
value={chat}
|
|
||||||
onKeyDown={onKeyDown}
|
|
||||||
onChange={(e) => setChat(e.target.value)}
|
|
||||||
/>
|
|
||||||
<div onClick={pickEmoji}>
|
<div onClick={pickEmoji}>
|
||||||
<Icon name="face" className="write-emoji-button" />
|
<Icon name="face" className="write-emoji-button" />
|
||||||
</div>
|
</div>
|
||||||
@ -107,7 +95,7 @@ export function WriteMessage({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<AsyncButton onClick={sendChatMessage} className="btn btn-border">
|
<AsyncButton onClick={sendChatMessage} className="btn btn-border">
|
||||||
Send
|
<FormattedMessage defaultMessage="Send" />
|
||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -5,8 +5,8 @@
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(outfit_400_latin-ext.woff2) format("woff2");
|
src: url(outfit_400_latin-ext.woff2) format("woff2");
|
||||||
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF,
|
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113,
|
||||||
U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
U+2C60-2C7F, U+A720-A7FF;
|
||||||
}
|
}
|
||||||
/* latin */
|
/* latin */
|
||||||
@font-face {
|
@font-face {
|
||||||
@ -15,9 +15,8 @@
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(outfit_400_latin.woff2) format("woff2");
|
src: url(outfit_400_latin.woff2) format("woff2");
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
|
||||||
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
||||||
}
|
}
|
||||||
/* latin-ext */
|
/* latin-ext */
|
||||||
@font-face {
|
@font-face {
|
||||||
@ -26,8 +25,8 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(outfit_500_latin-ext.woff2) format("woff2");
|
src: url(outfit_500_latin-ext.woff2) format("woff2");
|
||||||
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF,
|
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113,
|
||||||
U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
U+2C60-2C7F, U+A720-A7FF;
|
||||||
}
|
}
|
||||||
/* latin */
|
/* latin */
|
||||||
@font-face {
|
@font-face {
|
||||||
@ -36,9 +35,8 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(outfit_500_latin.woff2) format("woff2");
|
src: url(outfit_500_latin.woff2) format("woff2");
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
|
||||||
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
||||||
}
|
}
|
||||||
/* latin-ext */
|
/* latin-ext */
|
||||||
@font-face {
|
@font-face {
|
||||||
@ -47,8 +45,8 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(outfit_600_latin-ext.woff2) format("woff2");
|
src: url(outfit_600_latin-ext.woff2) format("woff2");
|
||||||
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF,
|
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113,
|
||||||
U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
U+2C60-2C7F, U+A720-A7FF;
|
||||||
}
|
}
|
||||||
/* latin */
|
/* latin */
|
||||||
@font-face {
|
@font-face {
|
||||||
@ -57,9 +55,8 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(outfit_600_latin.woff2) format("woff2");
|
src: url(outfit_600_latin.woff2) format("woff2");
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
|
||||||
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
||||||
}
|
}
|
||||||
/* latin-ext */
|
/* latin-ext */
|
||||||
@font-face {
|
@font-face {
|
||||||
@ -68,8 +65,8 @@
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(outfit_700_latin-ext.woff2) format("woff2");
|
src: url(outfit_700_latin-ext.woff2) format("woff2");
|
||||||
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF,
|
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113,
|
||||||
U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
U+2C60-2C7F, U+A720-A7FF;
|
||||||
}
|
}
|
||||||
/* latin */
|
/* latin */
|
||||||
@font-face {
|
@font-face {
|
||||||
@ -78,7 +75,6 @@
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(outfit_700_latin.woff2) format("woff2");
|
src: url(outfit_700_latin.woff2) format("woff2");
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
|
||||||
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
||||||
}
|
}
|
||||||
|
@ -1,50 +1,35 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
import {
|
import { TaggedNostrEvent, EventKind, NoteCollection, RequestBuilder } from "@snort/system";
|
||||||
TaggedRawEvent,
|
|
||||||
EventKind,
|
|
||||||
NoteCollection,
|
|
||||||
RequestBuilder,
|
|
||||||
} from "@snort/system";
|
|
||||||
import { useRequestBuilder } from "@snort/system-react";
|
import { useRequestBuilder } from "@snort/system-react";
|
||||||
|
|
||||||
import { findTag, toAddress, getTagValues } from "utils";
|
import { findTag, toAddress, getTagValues } from "utils";
|
||||||
import { System } from "index";
|
|
||||||
import type { Badge } from "types";
|
import type { Badge } from "types";
|
||||||
|
|
||||||
export function useBadges(
|
export function useBadges(
|
||||||
pubkey: string,
|
pubkey: string,
|
||||||
since: number,
|
since: number,
|
||||||
leaveOpen = true
|
leaveOpen = true
|
||||||
): { badges: Badge[]; awards: TaggedRawEvent[] } {
|
): { badges: Badge[]; awards: TaggedNostrEvent[] } {
|
||||||
const rb = useMemo(() => {
|
const rb = useMemo(() => {
|
||||||
const rb = new RequestBuilder(`badges:${pubkey.slice(0, 12)}`);
|
const rb = new RequestBuilder(`badges:${pubkey.slice(0, 12)}`);
|
||||||
rb.withOptions({ leaveOpen });
|
rb.withOptions({ leaveOpen });
|
||||||
rb.withFilter().authors([pubkey]).kinds([EventKind.Badge]);
|
rb.withFilter().authors([pubkey]).kinds([EventKind.Badge]);
|
||||||
rb.withFilter()
|
rb.withFilter().authors([pubkey]).kinds([EventKind.BadgeAward]).since(since);
|
||||||
.authors([pubkey])
|
|
||||||
.kinds([EventKind.BadgeAward])
|
|
||||||
.since(since);
|
|
||||||
return rb;
|
return rb;
|
||||||
}, [pubkey, since]);
|
}, [pubkey, since]);
|
||||||
|
|
||||||
const { data: badgeEvents } = useRequestBuilder<NoteCollection>(
|
const { data: badgeEvents } = useRequestBuilder(NoteCollection, rb);
|
||||||
System,
|
|
||||||
NoteCollection,
|
|
||||||
rb
|
|
||||||
);
|
|
||||||
|
|
||||||
const rawBadges = useMemo(() => {
|
const rawBadges = useMemo(() => {
|
||||||
if (badgeEvents) {
|
if (badgeEvents) {
|
||||||
return badgeEvents
|
return badgeEvents.filter(e => e.kind === EventKind.Badge).sort((a, b) => b.created_at - a.created_at);
|
||||||
.filter((e) => e.kind === EventKind.Badge)
|
|
||||||
.sort((a, b) => b.created_at - a.created_at);
|
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}, [badgeEvents]);
|
}, [badgeEvents]);
|
||||||
const badgeAwards = useMemo(() => {
|
const badgeAwards = useMemo(() => {
|
||||||
if (badgeEvents) {
|
if (badgeEvents) {
|
||||||
return badgeEvents.filter((e) => e.kind === EventKind.BadgeAward);
|
return badgeEvents.filter(e => e.kind === EventKind.BadgeAward);
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}, [badgeEvents]);
|
}, [badgeEvents]);
|
||||||
@ -52,37 +37,24 @@ export function useBadges(
|
|||||||
const acceptedSub = useMemo(() => {
|
const acceptedSub = useMemo(() => {
|
||||||
if (rawBadges.length === 0) return null;
|
if (rawBadges.length === 0) return null;
|
||||||
const rb = new RequestBuilder(`accepted-badges:${pubkey.slice(0, 12)}`);
|
const rb = new RequestBuilder(`accepted-badges:${pubkey.slice(0, 12)}`);
|
||||||
rb.withFilter()
|
rb.withFilter().kinds([EventKind.ProfileBadges]).tag("d", ["profile_badges"]).tag("a", rawBadges.map(toAddress));
|
||||||
.kinds([EventKind.ProfileBadges])
|
|
||||||
.tag("d", ["profile_badges"])
|
|
||||||
.tag("a", rawBadges.map(toAddress));
|
|
||||||
return rb;
|
return rb;
|
||||||
}, [rawBadges]);
|
}, [rawBadges]);
|
||||||
|
|
||||||
const acceptedStream = useRequestBuilder<NoteCollection>(
|
const acceptedStream = useRequestBuilder(NoteCollection, acceptedSub);
|
||||||
System,
|
|
||||||
NoteCollection,
|
|
||||||
acceptedSub
|
|
||||||
);
|
|
||||||
const acceptedEvents = acceptedStream.data ?? [];
|
const acceptedEvents = acceptedStream.data ?? [];
|
||||||
|
|
||||||
const badges = useMemo(() => {
|
const badges = useMemo(() => {
|
||||||
return rawBadges.map((e) => {
|
return rawBadges.map(e => {
|
||||||
const name = findTag(e, "d") ?? "";
|
const name = findTag(e, "d") ?? "";
|
||||||
const address = toAddress(e);
|
const address = toAddress(e);
|
||||||
const awardEvents = badgeAwards.filter(
|
const awardEvents = badgeAwards.filter(b => findTag(b, "a") === address);
|
||||||
(b) => findTag(b, "a") === address
|
const awardees = new Set(awardEvents.map(e => getTagValues(e.tags, "p")).flat());
|
||||||
);
|
|
||||||
const awardees = new Set(
|
|
||||||
awardEvents.map((e) => getTagValues(e.tags, "p")).flat()
|
|
||||||
);
|
|
||||||
const accepted = new Set(
|
const accepted = new Set(
|
||||||
acceptedEvents
|
acceptedEvents
|
||||||
.filter((pb) => awardees.has(pb.pubkey))
|
.filter(pb => awardees.has(pb.pubkey))
|
||||||
.filter((pb) =>
|
.filter(pb => pb.tags.find(t => t.at(0) === "a" && t.at(1) === address))
|
||||||
pb.tags.find((t) => t.at(0) === "a" && t.at(1) === address)
|
.map(pb => pb.pubkey)
|
||||||
)
|
|
||||||
.map((pb) => pb.pubkey)
|
|
||||||
);
|
);
|
||||||
const thumb = findTag(e, "thumb");
|
const thumb = findTag(e, "thumb");
|
||||||
const image = findTag(e, "image");
|
const image = findTag(e, "image");
|
||||||
|
@ -1,80 +1,55 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
import {
|
import { TaggedNostrEvent, ReplaceableNoteStore, NoteCollection, RequestBuilder } from "@snort/system";
|
||||||
TaggedRawEvent,
|
|
||||||
ReplaceableNoteStore,
|
|
||||||
NoteCollection,
|
|
||||||
RequestBuilder,
|
|
||||||
} from "@snort/system";
|
|
||||||
import { useRequestBuilder } from "@snort/system-react";
|
import { useRequestBuilder } from "@snort/system-react";
|
||||||
|
|
||||||
import { USER_CARDS, CARD } from "const";
|
import { USER_CARDS, CARD } from "const";
|
||||||
import { findTag } from "utils";
|
import { findTag } from "utils";
|
||||||
import { System } from "index";
|
|
||||||
|
|
||||||
export function useUserCards(
|
export function useUserCards(pubkey: string, userCards: Array<string[]>, leaveOpen = false): TaggedNostrEvent[] {
|
||||||
pubkey: string,
|
|
||||||
userCards: Array<string[]>,
|
|
||||||
leaveOpen = false
|
|
||||||
): TaggedRawEvent[] {
|
|
||||||
const related = useMemo(() => {
|
const related = useMemo(() => {
|
||||||
// filtering to only show CARD kinds for now, but in the future we could link and render anything
|
// filtering to only show CARD kinds for now, but in the future we could link and render anything
|
||||||
if (userCards?.length > 0) {
|
if (userCards?.length > 0) {
|
||||||
return userCards.filter(
|
return userCards.filter(t => t.at(0) === "a" && t.at(1)?.startsWith(`${CARD}:`));
|
||||||
(t) => t.at(0) === "a" && t.at(1)?.startsWith(`${CARD}:`)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}, [userCards]);
|
}, [userCards]);
|
||||||
|
|
||||||
const subRelated = useMemo(() => {
|
const subRelated = useMemo(() => {
|
||||||
if (!pubkey) return null;
|
if (!pubkey) return null;
|
||||||
const splitted = related.map((t) => t[1].split(":"));
|
const splitted = related.map(t => t[1].split(":"));
|
||||||
const authors = splitted
|
const authors = splitted
|
||||||
.map((s) => s.at(1))
|
.map(s => s.at(1))
|
||||||
.filter((s) => s)
|
.filter(s => s)
|
||||||
.map((s) => s as string);
|
.map(s => s as string);
|
||||||
const identifiers = splitted
|
const identifiers = splitted
|
||||||
.map((s) => s.at(2))
|
.map(s => s.at(2))
|
||||||
.filter((s) => s)
|
.filter(s => s)
|
||||||
.map((s) => s as string);
|
.map(s => s as string);
|
||||||
|
|
||||||
const rb = new RequestBuilder(`cards:${pubkey}`);
|
const rb = new RequestBuilder(`cards:${pubkey}`);
|
||||||
rb.withOptions({ leaveOpen })
|
rb.withOptions({ leaveOpen }).withFilter().kinds([CARD]).authors(authors).tag("d", identifiers);
|
||||||
.withFilter()
|
|
||||||
.kinds([CARD])
|
|
||||||
.authors(authors)
|
|
||||||
.tag("d", identifiers);
|
|
||||||
|
|
||||||
return rb;
|
return rb;
|
||||||
}, [pubkey, related]);
|
}, [pubkey, related]);
|
||||||
|
|
||||||
const { data } = useRequestBuilder<NoteCollection>(
|
const { data } = useRequestBuilder(NoteCollection, subRelated);
|
||||||
System,
|
|
||||||
NoteCollection,
|
|
||||||
subRelated
|
|
||||||
);
|
|
||||||
|
|
||||||
const cards = useMemo(() => {
|
const cards = useMemo(() => {
|
||||||
return related
|
return related
|
||||||
.map((t) => {
|
.map(t => {
|
||||||
const [k, pubkey, identifier] = t[1].split(":");
|
const [k, pubkey, identifier] = t[1].split(":");
|
||||||
const kind = Number(k);
|
const kind = Number(k);
|
||||||
return (data ?? []).find(
|
return (data ?? []).find(e => e.kind === kind && e.pubkey === pubkey && findTag(e, "d") === identifier);
|
||||||
(e) =>
|
|
||||||
e.kind === kind &&
|
|
||||||
e.pubkey === pubkey &&
|
|
||||||
findTag(e, "d") === identifier
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.filter((e) => e)
|
.filter(e => e)
|
||||||
.map((e) => e as TaggedRawEvent);
|
.map(e => e as TaggedNostrEvent);
|
||||||
}, [related, data]);
|
}, [related, data]);
|
||||||
|
|
||||||
return cards;
|
return cards;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCards(pubkey: string, leaveOpen = false): TaggedRawEvent[] {
|
export function useCards(pubkey: string, leaveOpen = false): TaggedNostrEvent[] {
|
||||||
const sub = useMemo(() => {
|
const sub = useMemo(() => {
|
||||||
const b = new RequestBuilder(`user-cards:${pubkey.slice(0, 12)}`);
|
const b = new RequestBuilder(`user-cards:${pubkey.slice(0, 12)}`);
|
||||||
b.withOptions({
|
b.withOptions({
|
||||||
@ -86,65 +61,46 @@ export function useCards(pubkey: string, leaveOpen = false): TaggedRawEvent[] {
|
|||||||
return b;
|
return b;
|
||||||
}, [pubkey, leaveOpen]);
|
}, [pubkey, leaveOpen]);
|
||||||
|
|
||||||
const { data: userCards } = useRequestBuilder<ReplaceableNoteStore>(
|
const { data: userCards } = useRequestBuilder(ReplaceableNoteStore, sub);
|
||||||
System,
|
|
||||||
ReplaceableNoteStore,
|
|
||||||
sub
|
|
||||||
);
|
|
||||||
|
|
||||||
const related = useMemo(() => {
|
const related = useMemo(() => {
|
||||||
// filtering to only show CARD kinds for now, but in the future we could link and render anything
|
// filtering to only show CARD kinds for now, but in the future we could link and render anything
|
||||||
if (userCards) {
|
if (userCards) {
|
||||||
return userCards.tags.filter(
|
return userCards.tags.filter(t => t.at(0) === "a" && t.at(1)?.startsWith(`${CARD}:`));
|
||||||
(t) => t.at(0) === "a" && t.at(1)?.startsWith(`${CARD}:`)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}, [userCards]);
|
}, [userCards]);
|
||||||
|
|
||||||
const subRelated = useMemo(() => {
|
const subRelated = useMemo(() => {
|
||||||
if (!pubkey) return null;
|
if (!pubkey) return null;
|
||||||
const splitted = related.map((t) => t[1].split(":"));
|
const splitted = related.map(t => t[1].split(":"));
|
||||||
const authors = splitted
|
const authors = splitted
|
||||||
.map((s) => s.at(1))
|
.map(s => s.at(1))
|
||||||
.filter((s) => s)
|
.filter(s => s)
|
||||||
.map((s) => s as string);
|
.map(s => s as string);
|
||||||
const identifiers = splitted
|
const identifiers = splitted
|
||||||
.map((s) => s.at(2))
|
.map(s => s.at(2))
|
||||||
.filter((s) => s)
|
.filter(s => s)
|
||||||
.map((s) => s as string);
|
.map(s => s as string);
|
||||||
|
|
||||||
const rb = new RequestBuilder(`cards:${pubkey}`);
|
const rb = new RequestBuilder(`cards:${pubkey}`);
|
||||||
rb.withOptions({ leaveOpen })
|
rb.withOptions({ leaveOpen }).withFilter().kinds([CARD]).authors(authors).tag("d", identifiers);
|
||||||
.withFilter()
|
|
||||||
.kinds([CARD])
|
|
||||||
.authors(authors)
|
|
||||||
.tag("d", identifiers);
|
|
||||||
|
|
||||||
return rb;
|
return rb;
|
||||||
}, [pubkey, related]);
|
}, [pubkey, related]);
|
||||||
|
|
||||||
const { data } = useRequestBuilder<NoteCollection>(
|
const { data } = useRequestBuilder(NoteCollection, subRelated);
|
||||||
System,
|
|
||||||
NoteCollection,
|
|
||||||
subRelated
|
|
||||||
);
|
|
||||||
const cardEvents = data ?? [];
|
const cardEvents = data ?? [];
|
||||||
|
|
||||||
const cards = useMemo(() => {
|
const cards = useMemo(() => {
|
||||||
return related
|
return related
|
||||||
.map((t) => {
|
.map(t => {
|
||||||
const [k, pubkey, identifier] = t[1].split(":");
|
const [k, pubkey, identifier] = t[1].split(":");
|
||||||
const kind = Number(k);
|
const kind = Number(k);
|
||||||
return cardEvents.find(
|
return cardEvents.find(e => e.kind === kind && e.pubkey === pubkey && findTag(e, "d") === identifier);
|
||||||
(e) =>
|
|
||||||
e.kind === kind &&
|
|
||||||
e.pubkey === pubkey &&
|
|
||||||
findTag(e, "d") === identifier
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.filter((e) => e)
|
.filter(e => e)
|
||||||
.map((e) => e as TaggedRawEvent);
|
.map(e => e as TaggedNostrEvent);
|
||||||
}, [related, cardEvents]);
|
}, [related, cardEvents]);
|
||||||
|
|
||||||
return cards;
|
return cards;
|
||||||
|
@ -1,33 +1,17 @@
|
|||||||
import { unwrap } from "@snort/shared";
|
import { unwrap } from "@snort/shared";
|
||||||
import {
|
import { NostrEvent, NostrLink, NostrPrefix, NoteCollection, RequestBuilder, TaggedNostrEvent } from "@snort/system";
|
||||||
NostrEvent,
|
|
||||||
NostrLink,
|
|
||||||
NostrPrefix,
|
|
||||||
NoteCollection,
|
|
||||||
RequestBuilder,
|
|
||||||
TaggedRawEvent,
|
|
||||||
} from "@snort/system";
|
|
||||||
import { useRequestBuilder } from "@snort/system-react";
|
import { useRequestBuilder } from "@snort/system-react";
|
||||||
import { LIVE_STREAM } from "const";
|
import { LIVE_STREAM } from "const";
|
||||||
import { System } from "index";
|
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
export function useCurrentStreamFeed(
|
export function useCurrentStreamFeed(link: NostrLink, leaveOpen = false, evPreload?: NostrEvent) {
|
||||||
link: NostrLink,
|
const author = link.type === NostrPrefix.Address ? unwrap(link.author) : link.id;
|
||||||
leaveOpen = false,
|
|
||||||
evPreload?: NostrEvent
|
|
||||||
) {
|
|
||||||
const author =
|
|
||||||
link.type === NostrPrefix.Address ? unwrap(link.author) : link.id;
|
|
||||||
const sub = useMemo(() => {
|
const sub = useMemo(() => {
|
||||||
const b = new RequestBuilder(`current-event:${link.id}`);
|
const b = new RequestBuilder(`current-event:${link.id}`);
|
||||||
b.withOptions({
|
b.withOptions({
|
||||||
leaveOpen,
|
leaveOpen,
|
||||||
});
|
});
|
||||||
if (
|
if (link.type === NostrPrefix.PublicKey || link.type === NostrPrefix.Profile) {
|
||||||
link.type === NostrPrefix.PublicKey ||
|
|
||||||
link.type === NostrPrefix.Profile
|
|
||||||
) {
|
|
||||||
b.withFilter().authors([link.id]).kinds([LIVE_STREAM]).limit(1);
|
b.withFilter().authors([link.id]).kinds([LIVE_STREAM]).limit(1);
|
||||||
b.withFilter().tag("p", [link.id]).kinds([LIVE_STREAM]).limit(1);
|
b.withFilter().tag("p", [link.id]).kinds([LIVE_STREAM]).limit(1);
|
||||||
} else if (link.type === NostrPrefix.Address) {
|
} else if (link.type === NostrPrefix.Address) {
|
||||||
@ -42,20 +26,16 @@ export function useCurrentStreamFeed(
|
|||||||
return b;
|
return b;
|
||||||
}, [link.id, leaveOpen]);
|
}, [link.id, leaveOpen]);
|
||||||
|
|
||||||
const q = useRequestBuilder(System, NoteCollection, sub);
|
const q = useRequestBuilder(NoteCollection, sub);
|
||||||
|
|
||||||
if (evPreload) {
|
if (evPreload) {
|
||||||
q.add(evPreload as TaggedRawEvent);
|
q.add(evPreload as TaggedNostrEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const hosting = q.data?.filter(
|
const hosting = q.data?.filter(
|
||||||
(a) =>
|
a => a.pubkey === author || a.tags.some(b => b[0] === "p" && b[1] === author && b[3] === "host")
|
||||||
a.pubkey === author ||
|
|
||||||
a.tags.some((b) => b[0] === "p" && b[1] === author && b[3] === "host")
|
|
||||||
);
|
);
|
||||||
return [...(hosting ?? [])]
|
return [...(hosting ?? [])].sort((a, b) => (b.created_at > a.created_at ? 1 : -1)).at(0);
|
||||||
.sort((a, b) => (b.created_at > a.created_at ? 1 : -1))
|
|
||||||
.at(0);
|
|
||||||
}, [q.data]);
|
}, [q.data]);
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,8 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import uniqBy from "lodash.uniqby";
|
import uniqBy from "lodash.uniqby";
|
||||||
|
|
||||||
import {
|
import { RequestBuilder, ReplaceableNoteStore, NoteCollection, NostrEvent } from "@snort/system";
|
||||||
RequestBuilder,
|
|
||||||
ReplaceableNoteStore,
|
|
||||||
NoteCollection,
|
|
||||||
NostrEvent,
|
|
||||||
} from "@snort/system";
|
|
||||||
import { useRequestBuilder } from "@snort/system-react";
|
import { useRequestBuilder } from "@snort/system-react";
|
||||||
import { System } from "index";
|
|
||||||
import { findTag } from "utils";
|
import { findTag } from "utils";
|
||||||
import { EMOJI_PACK, USER_EMOJIS } from "const";
|
import { EMOJI_PACK, USER_EMOJIS } from "const";
|
||||||
import type { EmojiPack, Tags, EmojiTag } from "types";
|
import type { EmojiPack, Tags, EmojiTag } from "types";
|
||||||
@ -24,8 +18,8 @@ export function toEmojiPack(ev: NostrEvent): EmojiPack {
|
|||||||
name: d,
|
name: d,
|
||||||
author: ev.pubkey,
|
author: ev.pubkey,
|
||||||
emojis: ev.tags
|
emojis: ev.tags
|
||||||
.filter((t) => t.at(0) === "emoji")
|
.filter(t => t.at(0) === "emoji")
|
||||||
.map((t) => ["emoji", cleanShortcode(t.at(1)), t.at(2)]) as EmojiTag[],
|
.map(t => ["emoji", cleanShortcode(t.at(1)), t.at(2)]) as EmojiTag[],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,24 +30,22 @@ export function packId(pack: EmojiPack): string {
|
|||||||
export function useUserEmojiPacks(pubkey?: string, userEmoji?: Tags) {
|
export function useUserEmojiPacks(pubkey?: string, userEmoji?: Tags) {
|
||||||
const related = useMemo(() => {
|
const related = useMemo(() => {
|
||||||
if (userEmoji) {
|
if (userEmoji) {
|
||||||
return userEmoji?.filter(
|
return userEmoji?.filter(t => t.at(0) === "a" && t.at(1)?.startsWith(`${EMOJI_PACK}:`));
|
||||||
(t) => t.at(0) === "a" && t.at(1)?.startsWith(`${EMOJI_PACK}:`)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}, [userEmoji]);
|
}, [userEmoji]);
|
||||||
|
|
||||||
const subRelated = useMemo(() => {
|
const subRelated = useMemo(() => {
|
||||||
if (!pubkey) return null;
|
if (!pubkey) return null;
|
||||||
const splitted = related.map((t) => t[1].split(":"));
|
const splitted = related.map(t => t[1].split(":"));
|
||||||
const authors = splitted
|
const authors = splitted
|
||||||
.map((s) => s.at(1))
|
.map(s => s.at(1))
|
||||||
.filter((s) => s)
|
.filter(s => s)
|
||||||
.map((s) => s as string);
|
.map(s => s as string);
|
||||||
const identifiers = splitted
|
const identifiers = splitted
|
||||||
.map((s) => s.at(2))
|
.map(s => s.at(2))
|
||||||
.filter((s) => s)
|
.filter(s => s)
|
||||||
.map((s) => s as string);
|
.map(s => s as string);
|
||||||
|
|
||||||
const rb = new RequestBuilder(`emoji-related:${pubkey}`);
|
const rb = new RequestBuilder(`emoji-related:${pubkey}`);
|
||||||
|
|
||||||
@ -64,11 +56,7 @@ export function useUserEmojiPacks(pubkey?: string, userEmoji?: Tags) {
|
|||||||
return rb;
|
return rb;
|
||||||
}, [pubkey, related]);
|
}, [pubkey, related]);
|
||||||
|
|
||||||
const { data: relatedData } = useRequestBuilder<NoteCollection>(
|
const { data: relatedData } = useRequestBuilder(NoteCollection, subRelated);
|
||||||
System,
|
|
||||||
NoteCollection,
|
|
||||||
subRelated
|
|
||||||
);
|
|
||||||
|
|
||||||
const emojiPacks = useMemo(() => {
|
const emojiPacks = useMemo(() => {
|
||||||
return relatedData ?? [];
|
return relatedData ?? [];
|
||||||
@ -92,11 +80,7 @@ export default function useEmoji(pubkey?: string) {
|
|||||||
return rb;
|
return rb;
|
||||||
}, [pubkey]);
|
}, [pubkey]);
|
||||||
|
|
||||||
const { data: userEmoji } = useRequestBuilder<ReplaceableNoteStore>(
|
const { data: userEmoji } = useRequestBuilder(ReplaceableNoteStore, sub);
|
||||||
System,
|
|
||||||
ReplaceableNoteStore,
|
|
||||||
sub
|
|
||||||
);
|
|
||||||
|
|
||||||
const emojis = useUserEmojiPacks(pubkey, userEmoji?.tags ?? []);
|
const emojis = useUserEmojiPacks(pubkey, userEmoji?.tags ?? []);
|
||||||
return emojis;
|
return emojis;
|
||||||
|
@ -1,14 +1,7 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import {
|
import { NostrPrefix, RequestBuilder, ReplaceableNoteStore, NostrLink } from "@snort/system";
|
||||||
NostrPrefix,
|
|
||||||
RequestBuilder,
|
|
||||||
ReplaceableNoteStore,
|
|
||||||
NostrLink,
|
|
||||||
} from "@snort/system";
|
|
||||||
import { useRequestBuilder } from "@snort/system-react";
|
import { useRequestBuilder } from "@snort/system-react";
|
||||||
|
|
||||||
import { System } from "index";
|
|
||||||
|
|
||||||
export default function useEventFeed(link: NostrLink, leaveOpen = false) {
|
export default function useEventFeed(link: NostrLink, leaveOpen = false) {
|
||||||
const sub = useMemo(() => {
|
const sub = useMemo(() => {
|
||||||
const b = new RequestBuilder(`event:${link.id.slice(0, 12)}`);
|
const b = new RequestBuilder(`event:${link.id.slice(0, 12)}`);
|
||||||
@ -26,7 +19,7 @@ export default function useEventFeed(link: NostrLink, leaveOpen = false) {
|
|||||||
} else {
|
} else {
|
||||||
const f = b.withFilter().ids([link.id]);
|
const f = b.withFilter().ids([link.id]);
|
||||||
if (link.relays) {
|
if (link.relays) {
|
||||||
link.relays.slice(0, 2).forEach((r) => f.relay(r));
|
link.relays.slice(0, 2).forEach(r => f.relay(r));
|
||||||
}
|
}
|
||||||
if (link.author) {
|
if (link.author) {
|
||||||
f.authors([link.author]);
|
f.authors([link.author]);
|
||||||
@ -35,9 +28,5 @@ export default function useEventFeed(link: NostrLink, leaveOpen = false) {
|
|||||||
return b;
|
return b;
|
||||||
}, [link, leaveOpen]);
|
}, [link, leaveOpen]);
|
||||||
|
|
||||||
return useRequestBuilder<ReplaceableNoteStore>(
|
return useRequestBuilder(ReplaceableNoteStore, sub);
|
||||||
System,
|
|
||||||
ReplaceableNoteStore,
|
|
||||||
sub
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,8 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
import {
|
import { NostrPrefix, ReplaceableNoteStore, RequestBuilder, type NostrLink } from "@snort/system";
|
||||||
NostrPrefix,
|
|
||||||
ReplaceableNoteStore,
|
|
||||||
RequestBuilder,
|
|
||||||
type NostrLink,
|
|
||||||
} from "@snort/system";
|
|
||||||
import { useRequestBuilder } from "@snort/system-react";
|
import { useRequestBuilder } from "@snort/system-react";
|
||||||
|
|
||||||
import { System } from "index";
|
|
||||||
|
|
||||||
export function useAddress(kind: number, pubkey: string, identifier: string) {
|
export function useAddress(kind: number, pubkey: string, identifier: string) {
|
||||||
const sub = useMemo(() => {
|
const sub = useMemo(() => {
|
||||||
const b = new RequestBuilder(`event:${kind}:${identifier}`);
|
const b = new RequestBuilder(`event:${kind}:${identifier}`);
|
||||||
@ -17,11 +10,7 @@ export function useAddress(kind: number, pubkey: string, identifier: string) {
|
|||||||
return b;
|
return b;
|
||||||
}, [kind, pubkey, identifier]);
|
}, [kind, pubkey, identifier]);
|
||||||
|
|
||||||
const { data } = useRequestBuilder<ReplaceableNoteStore>(
|
const { data } = useRequestBuilder(ReplaceableNoteStore, sub);
|
||||||
System,
|
|
||||||
ReplaceableNoteStore,
|
|
||||||
sub
|
|
||||||
);
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@ -40,7 +29,7 @@ export function useEvent(link: NostrLink) {
|
|||||||
} else {
|
} else {
|
||||||
const f = b.withFilter().ids([link.id]);
|
const f = b.withFilter().ids([link.id]);
|
||||||
if (link.relays) {
|
if (link.relays) {
|
||||||
link.relays.slice(0, 2).forEach((r) => f.relay(r));
|
link.relays.slice(0, 2).forEach(r => f.relay(r));
|
||||||
}
|
}
|
||||||
if (link.author) {
|
if (link.author) {
|
||||||
f.authors([link.author]);
|
f.authors([link.author]);
|
||||||
@ -49,11 +38,7 @@ export function useEvent(link: NostrLink) {
|
|||||||
return b;
|
return b;
|
||||||
}, [link]);
|
}, [link]);
|
||||||
|
|
||||||
const { data } = useRequestBuilder<ReplaceableNoteStore>(
|
const { data } = useRequestBuilder(ReplaceableNoteStore, sub);
|
||||||
System,
|
|
||||||
ReplaceableNoteStore,
|
|
||||||
sub
|
|
||||||
);
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
@ -17,24 +17,13 @@ export function useZaps(goal: NostrEvent, leaveOpen = false) {
|
|||||||
const sub = useMemo(() => {
|
const sub = useMemo(() => {
|
||||||
const b = new RequestBuilder(`goal-zaps:${goal.id.slice(0, 12)}`);
|
const b = new RequestBuilder(`goal-zaps:${goal.id.slice(0, 12)}`);
|
||||||
b.withOptions({ leaveOpen });
|
b.withOptions({ leaveOpen });
|
||||||
b.withFilter()
|
b.withFilter().kinds([EventKind.ZapReceipt]).tag("e", [goal.id]).since(goal.created_at);
|
||||||
.kinds([EventKind.ZapReceipt])
|
|
||||||
.tag("e", [goal.id])
|
|
||||||
.since(goal.created_at);
|
|
||||||
return b;
|
return b;
|
||||||
}, [goal, leaveOpen]);
|
}, [goal, leaveOpen]);
|
||||||
|
|
||||||
const { data } = useRequestBuilder<NoteCollection>(
|
const { data } = useRequestBuilder(NoteCollection, sub);
|
||||||
System,
|
|
||||||
NoteCollection,
|
|
||||||
sub
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return data?.map(ev => parseZap(ev, System.ProfileLoader.Cache)).filter(z => z && z.valid) ?? [];
|
||||||
data
|
|
||||||
?.map((ev) => parseZap(ev, System.ProfileLoader.Cache))
|
|
||||||
.filter((z) => z && z.valid) ?? []
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useZapGoal(host: string, link?: NostrLink, leaveOpen = false) {
|
export function useZapGoal(host: string, link?: NostrLink, leaveOpen = false) {
|
||||||
@ -49,11 +38,7 @@ export function useZapGoal(host: string, link?: NostrLink, leaveOpen = false) {
|
|||||||
return b;
|
return b;
|
||||||
}, [link, leaveOpen]);
|
}, [link, leaveOpen]);
|
||||||
|
|
||||||
const { data } = useRequestBuilder<ReplaceableNoteStore>(
|
const { data } = useRequestBuilder(ReplaceableNoteStore, sub);
|
||||||
System,
|
|
||||||
ReplaceableNoteStore,
|
|
||||||
sub
|
|
||||||
);
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ import { useRequestBuilder } from "@snort/system-react";
|
|||||||
|
|
||||||
import { MUTED } from "const";
|
import { MUTED } from "const";
|
||||||
import { getTagValues } from "utils";
|
import { getTagValues } from "utils";
|
||||||
import { System } from "index";
|
|
||||||
|
|
||||||
export function useMutedPubkeys(host?: string, leaveOpen = false) {
|
export function useMutedPubkeys(host?: string, leaveOpen = false) {
|
||||||
const mutedSub = useMemo(() => {
|
const mutedSub = useMemo(() => {
|
||||||
@ -16,11 +15,7 @@ export function useMutedPubkeys(host?: string, leaveOpen = false) {
|
|||||||
return rb;
|
return rb;
|
||||||
}, [host]);
|
}, [host]);
|
||||||
|
|
||||||
const { data: muted } = useRequestBuilder<ReplaceableNoteStore>(
|
const { data: muted } = useRequestBuilder(ReplaceableNoteStore, mutedSub);
|
||||||
System,
|
|
||||||
ReplaceableNoteStore,
|
|
||||||
mutedSub
|
|
||||||
);
|
|
||||||
const mutedPubkeys = useMemo(() => {
|
const mutedPubkeys = useMemo(() => {
|
||||||
return new Set(getTagValues(muted?.tags ?? [], "p"));
|
return new Set(getTagValues(muted?.tags ?? [], "p"));
|
||||||
}, [muted]);
|
}, [muted]);
|
||||||
|
@ -1,12 +1,6 @@
|
|||||||
import {
|
import { NostrLink, RequestBuilder, EventKind, NoteCollection } from "@snort/system";
|
||||||
NostrLink,
|
|
||||||
RequestBuilder,
|
|
||||||
EventKind,
|
|
||||||
FlatNoteStore,
|
|
||||||
} from "@snort/system";
|
|
||||||
import { useRequestBuilder } from "@snort/system-react";
|
import { useRequestBuilder } from "@snort/system-react";
|
||||||
import { unixNow } from "@snort/shared";
|
import { unixNow } from "@snort/shared";
|
||||||
import { System } from "index";
|
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { LIVE_STREAM_CHAT, WEEK } from "const";
|
import { LIVE_STREAM_CHAT, WEEK } from "const";
|
||||||
|
|
||||||
@ -27,17 +21,17 @@ export function useLiveChatFeed(link: NostrLink, eZaps?: Array<string>) {
|
|||||||
return rb;
|
return rb;
|
||||||
}, [link.id, since, eZaps]);
|
}, [link.id, since, eZaps]);
|
||||||
|
|
||||||
const feed = useRequestBuilder<FlatNoteStore>(System, FlatNoteStore, sub);
|
const feed = useRequestBuilder(NoteCollection, sub);
|
||||||
|
|
||||||
const messages = useMemo(() => {
|
const messages = useMemo(() => {
|
||||||
return (feed.data ?? []).filter((ev) => ev.kind === LIVE_STREAM_CHAT);
|
return (feed.data ?? []).filter(ev => ev.kind === LIVE_STREAM_CHAT);
|
||||||
}, [feed.data]);
|
}, [feed.data]);
|
||||||
const zaps = useMemo(() => {
|
const zaps = useMemo(() => {
|
||||||
return (feed.data ?? []).filter((ev) => ev.kind === EventKind.ZapReceipt);
|
return (feed.data ?? []).filter(ev => ev.kind === EventKind.ZapReceipt);
|
||||||
}, [feed.data]);
|
}, [feed.data]);
|
||||||
|
|
||||||
const etags = useMemo(() => {
|
const etags = useMemo(() => {
|
||||||
return messages.map((e) => e.id);
|
return messages.map(e => e.id);
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
const esub = useMemo(() => {
|
const esub = useMemo(() => {
|
||||||
@ -46,17 +40,11 @@ export function useLiveChatFeed(link: NostrLink, eZaps?: Array<string>) {
|
|||||||
rb.withOptions({
|
rb.withOptions({
|
||||||
leaveOpen: true,
|
leaveOpen: true,
|
||||||
});
|
});
|
||||||
rb.withFilter()
|
rb.withFilter().kinds([EventKind.Reaction, EventKind.ZapReceipt]).tag("e", etags);
|
||||||
.kinds([EventKind.Reaction, EventKind.ZapReceipt])
|
|
||||||
.tag("e", etags);
|
|
||||||
return rb;
|
return rb;
|
||||||
}, [etags]);
|
}, [etags]);
|
||||||
|
|
||||||
const reactionsSub = useRequestBuilder<FlatNoteStore>(
|
const reactionsSub = useRequestBuilder(NoteCollection, esub);
|
||||||
System,
|
|
||||||
FlatNoteStore,
|
|
||||||
esub
|
|
||||||
);
|
|
||||||
|
|
||||||
const reactions = reactionsSub.data ?? [];
|
const reactions = reactionsSub.data ?? [];
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import { useRequestBuilder } from "@snort/system-react";
|
|||||||
|
|
||||||
import { unixNow } from "@snort/shared";
|
import { unixNow } from "@snort/shared";
|
||||||
import { LIVE_STREAM } from "const";
|
import { LIVE_STREAM } from "const";
|
||||||
import { System, StreamState } from "index";
|
import { StreamState } from "index";
|
||||||
import { findTag } from "utils";
|
import { findTag } from "utils";
|
||||||
import { WEEK } from "const";
|
import { WEEK } from "const";
|
||||||
|
|
||||||
@ -34,30 +34,22 @@ export function useStreamsFeed(tag?: string) {
|
|||||||
return bStart > aStart ? 1 : -1;
|
return bStart > aStart ? 1 : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const feed = useRequestBuilder<NoteCollection>(System, NoteCollection, rb);
|
const feed = useRequestBuilder(NoteCollection, rb);
|
||||||
const feedSorted = useMemo(() => {
|
const feedSorted = useMemo(() => {
|
||||||
if (feed.data) {
|
if (feed.data) {
|
||||||
if (__XXX) {
|
if (__XXX) {
|
||||||
return [...feed.data].filter(
|
return [...feed.data].filter(a => findTag(a, "content-warning") !== undefined);
|
||||||
(a) => findTag(a, "content-warning") !== undefined
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
return [...feed.data].filter(
|
return [...feed.data].filter(a => findTag(a, "content-warning") === undefined);
|
||||||
(a) => findTag(a, "content-warning") === undefined
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}, [feed.data]);
|
}, [feed.data]);
|
||||||
|
|
||||||
const live = feedSorted
|
const live = feedSorted.filter(a => findTag(a, "status") === StreamState.Live).sort(sortStarts);
|
||||||
.filter((a) => findTag(a, "status") === StreamState.Live)
|
const planned = feedSorted.filter(a => findTag(a, "status") === StreamState.Planned).sort(sortStarts);
|
||||||
.sort(sortStarts);
|
|
||||||
const planned = feedSorted
|
|
||||||
.filter((a) => findTag(a, "status") === StreamState.Planned)
|
|
||||||
.sort(sortStarts);
|
|
||||||
const ended = feedSorted
|
const ended = feedSorted
|
||||||
.filter((a) => {
|
.filter(a => {
|
||||||
const hasEnded = findTag(a, "status") === StreamState.Ended;
|
const hasEnded = findTag(a, "status") === StreamState.Ended;
|
||||||
const recording = findTag(a, "recording") ?? "";
|
const recording = findTag(a, "recording") ?? "";
|
||||||
return hasEnded && recording?.length > 0;
|
return hasEnded && recording?.length > 0;
|
||||||
|
@ -6,12 +6,12 @@ import { useRequestBuilder } from "@snort/system-react";
|
|||||||
import { useUserEmojiPacks } from "hooks/emoji";
|
import { useUserEmojiPacks } from "hooks/emoji";
|
||||||
import { MUTED, USER_CARDS, USER_EMOJIS } from "const";
|
import { MUTED, USER_CARDS, USER_EMOJIS } from "const";
|
||||||
import type { Tags } from "types";
|
import type { Tags } from "types";
|
||||||
import { System, Login } from "index";
|
|
||||||
import { getPublisher } from "login";
|
import { getPublisher } from "login";
|
||||||
|
import { Login } from "index";
|
||||||
|
|
||||||
export function useLogin() {
|
export function useLogin() {
|
||||||
const session = useSyncExternalStore(
|
const session = useSyncExternalStore(
|
||||||
(c) => Login.hook(c),
|
c => Login.hook(c),
|
||||||
() => Login.snapshot()
|
() => Login.snapshot()
|
||||||
);
|
);
|
||||||
if (!session) return;
|
if (!session) return;
|
||||||
@ -26,7 +26,7 @@ export function useLogin() {
|
|||||||
export function useLoginEvents(pubkey?: string, leaveOpen = false) {
|
export function useLoginEvents(pubkey?: string, leaveOpen = false) {
|
||||||
const [userEmojis, setUserEmojis] = useState<Tags>([]);
|
const [userEmojis, setUserEmojis] = useState<Tags>([]);
|
||||||
const session = useSyncExternalStore(
|
const session = useSyncExternalStore(
|
||||||
(c) => Login.hook(c),
|
c => Login.hook(c),
|
||||||
() => Login.snapshot()
|
() => Login.snapshot()
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -42,11 +42,7 @@ export function useLoginEvents(pubkey?: string, leaveOpen = false) {
|
|||||||
return b;
|
return b;
|
||||||
}, [pubkey, leaveOpen]);
|
}, [pubkey, leaveOpen]);
|
||||||
|
|
||||||
const { data } = useRequestBuilder<NoteCollection>(
|
const { data } = useRequestBuilder(NoteCollection, sub);
|
||||||
System,
|
|
||||||
NoteCollection,
|
|
||||||
sub
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
export default function usePlaceholder(pubkey: string) {
|
export default function usePlaceholder(pubkey: string) {
|
||||||
const url = useMemo(
|
const url = useMemo(() => `https://robohash.v0l.io/${pubkey}.png?set=2`, [pubkey]);
|
||||||
() => `https://robohash.v0l.io/${pubkey}.png?set=2`,
|
|
||||||
[pubkey]
|
|
||||||
);
|
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,5 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import {
|
import { RequestBuilder, NoteCollection, NostrLink, EventKind, parseZap } from "@snort/system";
|
||||||
RequestBuilder,
|
|
||||||
FlatNoteStore,
|
|
||||||
NoteCollection,
|
|
||||||
NostrLink,
|
|
||||||
EventKind,
|
|
||||||
parseZap,
|
|
||||||
} from "@snort/system";
|
|
||||||
import { useRequestBuilder } from "@snort/system-react";
|
import { useRequestBuilder } from "@snort/system-react";
|
||||||
import { LIVE_STREAM } from "const";
|
import { LIVE_STREAM } from "const";
|
||||||
import { findTag } from "utils";
|
import { findTag } from "utils";
|
||||||
@ -27,16 +20,12 @@ export function useProfile(link: NostrLink, leaveOpen = false) {
|
|||||||
return b;
|
return b;
|
||||||
}, [link, leaveOpen]);
|
}, [link, leaveOpen]);
|
||||||
|
|
||||||
const { data: streamsData } = useRequestBuilder<NoteCollection>(
|
const { data: streamsData } = useRequestBuilder(NoteCollection, sub);
|
||||||
System,
|
|
||||||
NoteCollection,
|
|
||||||
sub
|
|
||||||
);
|
|
||||||
const streams = streamsData ?? [];
|
const streams = streamsData ?? [];
|
||||||
|
|
||||||
const addresses = useMemo(() => {
|
const addresses = useMemo(() => {
|
||||||
if (streamsData) {
|
if (streamsData) {
|
||||||
return streamsData.map((e) => `${e.kind}:${e.pubkey}:${findTag(e, "d")}`);
|
return streamsData.map(e => `${e.kind}:${e.pubkey}:${findTag(e, "d")}`);
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}, [streamsData]);
|
}, [streamsData]);
|
||||||
@ -52,14 +41,10 @@ export function useProfile(link: NostrLink, leaveOpen = false) {
|
|||||||
return b;
|
return b;
|
||||||
}, [link, addresses, leaveOpen]);
|
}, [link, addresses, leaveOpen]);
|
||||||
|
|
||||||
const { data: zapsData } = useRequestBuilder<FlatNoteStore>(
|
const { data: zapsData } = useRequestBuilder(NoteCollection, zapsSub);
|
||||||
System,
|
|
||||||
FlatNoteStore,
|
|
||||||
zapsSub
|
|
||||||
);
|
|
||||||
const zaps = (zapsData ?? [])
|
const zaps = (zapsData ?? [])
|
||||||
.map((ev) => parseZap(ev, System.ProfileLoader.Cache))
|
.map(ev => parseZap(ev, System.ProfileLoader.Cache))
|
||||||
.filter((z) => z && z.valid && z.receiver === link.id);
|
.filter(z => z && z.valid && z.receiver === link.id);
|
||||||
|
|
||||||
const sortedStreams = useMemo(() => {
|
const sortedStreams = useMemo(() => {
|
||||||
const sorted = [...streams];
|
const sorted = [...streams];
|
||||||
|
@ -3,7 +3,7 @@ import { useSyncExternalStore } from "react";
|
|||||||
|
|
||||||
export function useStreamProvider() {
|
export function useStreamProvider() {
|
||||||
return useSyncExternalStore(
|
return useSyncExternalStore(
|
||||||
(c) => StreamProviderStore.hook(c),
|
c => StreamProviderStore.hook(c),
|
||||||
() => StreamProviderStore.snapshot()
|
() => StreamProviderStore.snapshot()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,19 +2,15 @@ import { useMemo } from "react";
|
|||||||
import { ParsedZap } from "@snort/system";
|
import { ParsedZap } from "@snort/system";
|
||||||
|
|
||||||
function totalZapped(pubkey: string, zaps: ParsedZap[]) {
|
function totalZapped(pubkey: string, zaps: ParsedZap[]) {
|
||||||
return zaps
|
return zaps.filter(z => (z.anonZap ? pubkey === "anon" : z.sender === pubkey)).reduce((acc, z) => acc + z.amount, 0);
|
||||||
.filter((z) => (z.anonZap ? pubkey === "anon" : z.sender === pubkey))
|
|
||||||
.reduce((acc, z) => acc + z.amount, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function useTopZappers(zaps: ParsedZap[]) {
|
export default function useTopZappers(zaps: ParsedZap[]) {
|
||||||
const zappers = zaps
|
const zappers = zaps.map(z => (z.anonZap ? "anon" : z.sender)).map(p => p as string);
|
||||||
.map((z) => (z.anonZap ? "anon" : z.sender))
|
|
||||||
.map((p) => p as string);
|
|
||||||
|
|
||||||
const sorted = useMemo(() => {
|
const sorted = useMemo(() => {
|
||||||
const pubkeys = [...new Set([...zappers])];
|
const pubkeys = [...new Set([...zappers])];
|
||||||
const result = pubkeys.map((pubkey) => {
|
const result = pubkeys.map(pubkey => {
|
||||||
return { pubkey, total: totalZapped(pubkey, zaps) };
|
return { pubkey, total: totalZapped(pubkey, zaps) };
|
||||||
});
|
});
|
||||||
result.sort((a, b) => b.total - a.total);
|
result.sort((a, b) => b.total - a.total);
|
||||||
|
@ -19,11 +19,7 @@ body {
|
|||||||
--border: #171717;
|
--border: #171717;
|
||||||
--gradient-purple: linear-gradient(135deg, #882bff 0%, #f83838 100%);
|
--gradient-purple: linear-gradient(135deg, #882bff 0%, #f83838 100%);
|
||||||
--gradient-yellow: linear-gradient(270deg, #adff27 0%, #ffd027 100%);
|
--gradient-yellow: linear-gradient(270deg, #adff27 0%, #ffd027 100%);
|
||||||
--gradient-orange: linear-gradient(
|
--gradient-orange: linear-gradient(270deg, #ff5b27 0%, rgba(255, 182, 39, 0.99) 100%);
|
||||||
270deg,
|
|
||||||
#ff5b27 0%,
|
|
||||||
rgba(255, 182, 39, 0.99) 100%
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1020px) {
|
@media (max-width: 1020px) {
|
||||||
@ -35,8 +31,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
|
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||||
monospace;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
@ -119,14 +114,12 @@ a {
|
|||||||
.btn-border {
|
.btn-border {
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
background: linear-gradient(black, black) padding-box,
|
background: linear-gradient(black, black) padding-box, linear-gradient(94.73deg, #2bd9ff 0%, #f838d9 100%) border-box;
|
||||||
linear-gradient(94.73deg, #2bd9ff 0%, #f838d9 100%) border-box;
|
|
||||||
transition: 0.3s;
|
transition: 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-border:hover {
|
.btn-border:hover {
|
||||||
background: linear-gradient(black, black) padding-box,
|
background: linear-gradient(black, black) padding-box, linear-gradient(94.73deg, #14b4d8 0%, #ba179f 100%) border-box;
|
||||||
linear-gradient(94.73deg, #14b4d8 0%, #ba179f 100%) border-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
|
@ -5,6 +5,7 @@ import "./fonts/outfit/outfit.css";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import { NostrSystem } from "@snort/system";
|
import { NostrSystem } from "@snort/system";
|
||||||
|
import { SnortContext } from "@snort/system-react";
|
||||||
import { RouterProvider, createBrowserRouter } from "react-router-dom";
|
import { RouterProvider, createBrowserRouter } from "react-router-dom";
|
||||||
|
|
||||||
import { RootPage } from "pages/root";
|
import { RootPage } from "pages/root";
|
||||||
@ -18,6 +19,8 @@ import { StreamProvidersPage } from "pages/providers";
|
|||||||
import { defaultRelays } from "const";
|
import { defaultRelays } from "const";
|
||||||
import { CatchAllRoutePage } from "pages/catch-all";
|
import { CatchAllRoutePage } from "pages/catch-all";
|
||||||
import { SettingsPage } from "pages/settings-page";
|
import { SettingsPage } from "pages/settings-page";
|
||||||
|
import { register } from "serviceWorker";
|
||||||
|
import { IntlProvider } from "intl";
|
||||||
|
|
||||||
export enum StreamState {
|
export enum StreamState {
|
||||||
Live = "live",
|
Live = "live",
|
||||||
@ -28,7 +31,9 @@ export enum StreamState {
|
|||||||
export const System = new NostrSystem({});
|
export const System = new NostrSystem({});
|
||||||
export const Login = new LoginStore();
|
export const Login = new LoginStore();
|
||||||
|
|
||||||
Object.entries(defaultRelays).forEach((params) => {
|
register();
|
||||||
|
|
||||||
|
Object.entries(defaultRelays).forEach(params => {
|
||||||
const [relay, settings] = params;
|
const [relay, settings] = params;
|
||||||
System.ConnectToRelay(relay, settings);
|
System.ConnectToRelay(relay, settings);
|
||||||
});
|
});
|
||||||
@ -76,11 +81,13 @@ const router = createBrowserRouter([
|
|||||||
element: <ChatPopout />,
|
element: <ChatPopout />,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
const root = ReactDOM.createRoot(
|
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLDivElement);
|
||||||
document.getElementById("root") as HTMLDivElement
|
|
||||||
);
|
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<RouterProvider router={router} />
|
<SnortContext.Provider value={System}>
|
||||||
|
<IntlProvider>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</IntlProvider>
|
||||||
|
</SnortContext.Provider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
40
src/intl.tsx
Normal file
40
src/intl.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { useEffect, useState, type ReactNode } from "react";
|
||||||
|
import { IntlProvider as ReactIntlProvider } from "react-intl";
|
||||||
|
|
||||||
|
import enMessages from "translations/en.json";
|
||||||
|
|
||||||
|
const DefaultLocale = "en-US";
|
||||||
|
|
||||||
|
const getMessages = (locale: string) => {
|
||||||
|
const truncatedLocale = locale.toLowerCase().split(/[_-]+/)[0];
|
||||||
|
|
||||||
|
const matchLang = (lng: string) => {
|
||||||
|
switch (lng) {
|
||||||
|
case DefaultLocale:
|
||||||
|
case "en":
|
||||||
|
return enMessages;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return matchLang(locale) ?? matchLang(truncatedLocale) ?? enMessages;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IntlProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
const locale = getLocale();
|
||||||
|
const [messages, setMessages] = useState<Record<string, string>>(enMessages);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const msg = getMessages(locale);
|
||||||
|
setMessages(msg);
|
||||||
|
}, [locale]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactIntlProvider locale={locale} messages={messages}>
|
||||||
|
{children}
|
||||||
|
</ReactIntlProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getLocale = () => {
|
||||||
|
return (navigator.languages && navigator.languages[0]) ?? navigator.language ?? DefaultLocale;
|
||||||
|
};
|
281
src/lang.json
Normal file
281
src/lang.json
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
{
|
||||||
|
"+0zv6g": {
|
||||||
|
"defaultMessage": "Image"
|
||||||
|
},
|
||||||
|
"+vVZ/G": {
|
||||||
|
"defaultMessage": "Connect"
|
||||||
|
},
|
||||||
|
"/0TOL5": {
|
||||||
|
"defaultMessage": "Amount"
|
||||||
|
},
|
||||||
|
"/GCoTA": {
|
||||||
|
"defaultMessage": "Clear"
|
||||||
|
},
|
||||||
|
"04lmFi": {
|
||||||
|
"defaultMessage": "Save Key"
|
||||||
|
},
|
||||||
|
"0GfNiL": {
|
||||||
|
"defaultMessage": "Stream Zap Goals"
|
||||||
|
},
|
||||||
|
"1EYCdR": {
|
||||||
|
"defaultMessage": "Tags"
|
||||||
|
},
|
||||||
|
"2/2yg+": {
|
||||||
|
"defaultMessage": "Add"
|
||||||
|
},
|
||||||
|
"2CGh/0": {
|
||||||
|
"defaultMessage": "live"
|
||||||
|
},
|
||||||
|
"3HwrQo": {
|
||||||
|
"defaultMessage": "Zap!"
|
||||||
|
},
|
||||||
|
"3adEeb": {
|
||||||
|
"defaultMessage": "{n} viewers"
|
||||||
|
},
|
||||||
|
"47FYwb": {
|
||||||
|
"defaultMessage": "Cancel"
|
||||||
|
},
|
||||||
|
"4l6vz1": {
|
||||||
|
"defaultMessage": "Copy"
|
||||||
|
},
|
||||||
|
"4uI538": {
|
||||||
|
"defaultMessage": "Resolutions"
|
||||||
|
},
|
||||||
|
"5JcXdV": {
|
||||||
|
"defaultMessage": "Create Account"
|
||||||
|
},
|
||||||
|
"5QYdPU": {
|
||||||
|
"defaultMessage": "Start Time"
|
||||||
|
},
|
||||||
|
"5kx+2v": {
|
||||||
|
"defaultMessage": "Server Url"
|
||||||
|
},
|
||||||
|
"6Z2pvJ": {
|
||||||
|
"defaultMessage": "Stream Providers"
|
||||||
|
},
|
||||||
|
"9WRlF4": {
|
||||||
|
"defaultMessage": "Send"
|
||||||
|
},
|
||||||
|
"9a9+ww": {
|
||||||
|
"defaultMessage": "Title"
|
||||||
|
},
|
||||||
|
"9anxhq": {
|
||||||
|
"defaultMessage": "Starts"
|
||||||
|
},
|
||||||
|
"AIHaPH": {
|
||||||
|
"defaultMessage": "{person} zapped {amount} sats"
|
||||||
|
},
|
||||||
|
"Atr2p4": {
|
||||||
|
"defaultMessage": "NSFW Content"
|
||||||
|
},
|
||||||
|
"AyGauy": {
|
||||||
|
"defaultMessage": "Login"
|
||||||
|
},
|
||||||
|
"BGxpTN": {
|
||||||
|
"defaultMessage": "Stream Chat"
|
||||||
|
},
|
||||||
|
"C81/uG": {
|
||||||
|
"defaultMessage": "Logout"
|
||||||
|
},
|
||||||
|
"ESyhzp": {
|
||||||
|
"defaultMessage": "Your comment for {name}"
|
||||||
|
},
|
||||||
|
"G/yZLu": {
|
||||||
|
"defaultMessage": "Remove"
|
||||||
|
},
|
||||||
|
"Gq6x9o": {
|
||||||
|
"defaultMessage": "Cover Image"
|
||||||
|
},
|
||||||
|
"H5+NAX": {
|
||||||
|
"defaultMessage": "Balance"
|
||||||
|
},
|
||||||
|
"HAlOn1": {
|
||||||
|
"defaultMessage": "Name"
|
||||||
|
},
|
||||||
|
"I1kjHI": {
|
||||||
|
"defaultMessage": "Supports {markdown}"
|
||||||
|
},
|
||||||
|
"IJDKz3": {
|
||||||
|
"defaultMessage": "Zap amount in {currency}"
|
||||||
|
},
|
||||||
|
"Jq3FDz": {
|
||||||
|
"defaultMessage": "Content"
|
||||||
|
},
|
||||||
|
"K3r6DQ": {
|
||||||
|
"defaultMessage": "Delete"
|
||||||
|
},
|
||||||
|
"K3uH1C": {
|
||||||
|
"defaultMessage": "offline"
|
||||||
|
},
|
||||||
|
"K7AkdL": {
|
||||||
|
"defaultMessage": "Show"
|
||||||
|
},
|
||||||
|
"KkIL3s": {
|
||||||
|
"defaultMessage": "No, I am under 18"
|
||||||
|
},
|
||||||
|
"Ld5LAE": {
|
||||||
|
"defaultMessage": "Nostr uses private keys, please save yours, if you lose this key you wont be able to login to your account anymore!"
|
||||||
|
},
|
||||||
|
"LknBsU": {
|
||||||
|
"defaultMessage": "Stream Key"
|
||||||
|
},
|
||||||
|
"My6HwN": {
|
||||||
|
"defaultMessage": "Ok, it's safe"
|
||||||
|
},
|
||||||
|
"O2Cy6m": {
|
||||||
|
"defaultMessage": "Yes, I am over 18"
|
||||||
|
},
|
||||||
|
"OKhRC6": {
|
||||||
|
"defaultMessage": "Share"
|
||||||
|
},
|
||||||
|
"OWgHbg": {
|
||||||
|
"defaultMessage": "Edit card"
|
||||||
|
},
|
||||||
|
"Q3au2v": {
|
||||||
|
"defaultMessage": "About {estimate}"
|
||||||
|
},
|
||||||
|
"QRHNuF": {
|
||||||
|
"defaultMessage": "What are we steaming today?"
|
||||||
|
},
|
||||||
|
"QRRCp0": {
|
||||||
|
"defaultMessage": "Stream URL"
|
||||||
|
},
|
||||||
|
"QceMQZ": {
|
||||||
|
"defaultMessage": "Goal: {amount}"
|
||||||
|
},
|
||||||
|
"RJOmzk": {
|
||||||
|
"defaultMessage": "I have read and agree with {provider}''s {terms}."
|
||||||
|
},
|
||||||
|
"RXQdxR": {
|
||||||
|
"defaultMessage": "Please login to write messages!"
|
||||||
|
},
|
||||||
|
"RrCui3": {
|
||||||
|
"defaultMessage": "Summary"
|
||||||
|
},
|
||||||
|
"TaTRKo": {
|
||||||
|
"defaultMessage": "Start Stream"
|
||||||
|
},
|
||||||
|
"UJBFYK": {
|
||||||
|
"defaultMessage": "Add Card"
|
||||||
|
},
|
||||||
|
"UfSot5": {
|
||||||
|
"defaultMessage": "Past Streams"
|
||||||
|
},
|
||||||
|
"VA/Z1S": {
|
||||||
|
"defaultMessage": "Hide"
|
||||||
|
},
|
||||||
|
"W9355R": {
|
||||||
|
"defaultMessage": "Unmute"
|
||||||
|
},
|
||||||
|
"X2PZ7D": {
|
||||||
|
"defaultMessage": "Create Goal"
|
||||||
|
},
|
||||||
|
"ZmqxZs": {
|
||||||
|
"defaultMessage": "You can change this later"
|
||||||
|
},
|
||||||
|
"acrOoz": {
|
||||||
|
"defaultMessage": "Continue"
|
||||||
|
},
|
||||||
|
"cvAsEh": {
|
||||||
|
"defaultMessage": "Streamed on {date}"
|
||||||
|
},
|
||||||
|
"cyR7Kh": {
|
||||||
|
"defaultMessage": "Back"
|
||||||
|
},
|
||||||
|
"dVD/AR": {
|
||||||
|
"defaultMessage": "Top Zappers"
|
||||||
|
},
|
||||||
|
"ebmhes": {
|
||||||
|
"defaultMessage": "Nostr Extension"
|
||||||
|
},
|
||||||
|
"fBI91o": {
|
||||||
|
"defaultMessage": "Zap"
|
||||||
|
},
|
||||||
|
"hGQqkW": {
|
||||||
|
"defaultMessage": "Schedule"
|
||||||
|
},
|
||||||
|
"ieGrWo": {
|
||||||
|
"defaultMessage": "Follow"
|
||||||
|
},
|
||||||
|
"itPgxd": {
|
||||||
|
"defaultMessage": "Profile"
|
||||||
|
},
|
||||||
|
"izWS4J": {
|
||||||
|
"defaultMessage": "Unfollow"
|
||||||
|
},
|
||||||
|
"jr4+vD": {
|
||||||
|
"defaultMessage": "Markdown"
|
||||||
|
},
|
||||||
|
"jvo0vs": {
|
||||||
|
"defaultMessage": "Save"
|
||||||
|
},
|
||||||
|
"lZpRMR": {
|
||||||
|
"defaultMessage": "Check here if this stream contains nudity or pornographic content."
|
||||||
|
},
|
||||||
|
"ljmS5P": {
|
||||||
|
"defaultMessage": "Endpoint"
|
||||||
|
},
|
||||||
|
"mtNGwh": {
|
||||||
|
"defaultMessage": "A short description of the content"
|
||||||
|
},
|
||||||
|
"nBCvvJ": {
|
||||||
|
"defaultMessage": "Topup"
|
||||||
|
},
|
||||||
|
"nOaArs": {
|
||||||
|
"defaultMessage": "Setup Profile"
|
||||||
|
},
|
||||||
|
"nwA8Os": {
|
||||||
|
"defaultMessage": "Add card"
|
||||||
|
},
|
||||||
|
"oHPB8Q": {
|
||||||
|
"defaultMessage": "Zap {name}"
|
||||||
|
},
|
||||||
|
"oZrFyI": {
|
||||||
|
"defaultMessage": "Stream type should be HLS"
|
||||||
|
},
|
||||||
|
"pO/lPX": {
|
||||||
|
"defaultMessage": "Scheduled for {date}"
|
||||||
|
},
|
||||||
|
"rWBFZA": {
|
||||||
|
"defaultMessage": "Sexually explicit material ahead!"
|
||||||
|
},
|
||||||
|
"rbrahO": {
|
||||||
|
"defaultMessage": "Close"
|
||||||
|
},
|
||||||
|
"rfC1Zq": {
|
||||||
|
"defaultMessage": "Save card"
|
||||||
|
},
|
||||||
|
"s5ksS7": {
|
||||||
|
"defaultMessage": "Image Link"
|
||||||
|
},
|
||||||
|
"s7V+5p": {
|
||||||
|
"defaultMessage": "Confirm your age"
|
||||||
|
},
|
||||||
|
"thsiMl": {
|
||||||
|
"defaultMessage": "terms and conditions"
|
||||||
|
},
|
||||||
|
"tzMNF3": {
|
||||||
|
"defaultMessage": "Status"
|
||||||
|
},
|
||||||
|
"uYw2LD": {
|
||||||
|
"defaultMessage": "Stream"
|
||||||
|
},
|
||||||
|
"vrTOHJ": {
|
||||||
|
"defaultMessage": "{amount} sats"
|
||||||
|
},
|
||||||
|
"wCIL7o": {
|
||||||
|
"defaultMessage": "Broadcast on Nostr"
|
||||||
|
},
|
||||||
|
"wEQDC6": {
|
||||||
|
"defaultMessage": "Edit"
|
||||||
|
},
|
||||||
|
"wOy57k": {
|
||||||
|
"defaultMessage": "Add stream goal"
|
||||||
|
},
|
||||||
|
"wzWWzV": {
|
||||||
|
"defaultMessage": "Top zappers"
|
||||||
|
},
|
||||||
|
"x82IOl": {
|
||||||
|
"defaultMessage": "Mute"
|
||||||
|
}
|
||||||
|
}
|
@ -130,10 +130,7 @@ export function getPublisher(session: LoginSession) {
|
|||||||
return new EventPublisher(new Nip7Signer(), session.pubkey);
|
return new EventPublisher(new Nip7Signer(), session.pubkey);
|
||||||
}
|
}
|
||||||
case LoginType.PrivateKey: {
|
case LoginType.PrivateKey: {
|
||||||
return new EventPublisher(
|
return new EventPublisher(new PrivateKeySigner(unwrap(session.privateKey)), session.pubkey);
|
||||||
new PrivateKeySigner(unwrap(session.privateKey)),
|
|
||||||
session.pubkey
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,15 +11,7 @@ export function ChatPopout() {
|
|||||||
const link = parseNostrLink(unwrap(params.id));
|
const link = parseNostrLink(unwrap(params.id));
|
||||||
const ev = useCurrentStreamFeed(link, true);
|
const ev = useCurrentStreamFeed(link, true);
|
||||||
|
|
||||||
const lnk = parseNostrLink(
|
const lnk = parseNostrLink(encodeTLV(NostrPrefix.Address, findTag(ev, "d") ?? "", undefined, ev?.kind, ev?.pubkey));
|
||||||
encodeTLV(
|
|
||||||
NostrPrefix.Address,
|
|
||||||
findTag(ev, "d") ?? "",
|
|
||||||
undefined,
|
|
||||||
ev?.kind,
|
|
||||||
ev?.pubkey
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const chat = Boolean(new URL(window.location.href).searchParams.get("chat"));
|
const chat = Boolean(new URL(window.location.href).searchParams.get("chat"));
|
||||||
return (
|
return (
|
||||||
<div className={`popout-chat${chat ? "" : " embed"}`}>
|
<div className={`popout-chat${chat ? "" : " embed"}`}>
|
||||||
|
@ -12,6 +12,7 @@ import { LoginSignup } from "element/login-signup";
|
|||||||
import { Menu, MenuItem } from "@szhsin/react-menu";
|
import { Menu, MenuItem } from "@szhsin/react-menu";
|
||||||
import { hexToBech32 } from "@snort/shared";
|
import { hexToBech32 } from "@snort/shared";
|
||||||
import { Login } from "index";
|
import { Login } from "index";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
export function LayoutPage() {
|
export function LayoutPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -40,13 +41,10 @@ export function LayoutPage() {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
align="end"
|
align="end"
|
||||||
gap={5}
|
gap={5}>
|
||||||
>
|
<MenuItem onClick={() => navigate(`/p/${hexToBech32("npub", login.pubkey)}`)}>
|
||||||
<MenuItem
|
|
||||||
onClick={() => navigate(`/p/${hexToBech32("npub", login.pubkey)}`)}
|
|
||||||
>
|
|
||||||
<Icon name="user" size={24} />
|
<Icon name="user" size={24} />
|
||||||
Profile
|
<FormattedMessage defaultMessage="Profile" />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem onClick={() => navigate("/settings")}>
|
<MenuItem onClick={() => navigate("/settings")}>
|
||||||
<Icon name="settings" size={24} />
|
<Icon name="settings" size={24} />
|
||||||
@ -54,7 +52,7 @@ export function LayoutPage() {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem onClick={() => Login.logout()}>
|
<MenuItem onClick={() => Login.logout()}>
|
||||||
<Icon name="logout" size={24} />
|
<Icon name="logout" size={24} />
|
||||||
Logout
|
<FormattedMessage defaultMessage="Logout" />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
</>
|
</>
|
||||||
@ -67,12 +65,8 @@ export function LayoutPage() {
|
|||||||
return (
|
return (
|
||||||
<Dialog.Root open={showLogin} onOpenChange={setShowLogin}>
|
<Dialog.Root open={showLogin} onOpenChange={setShowLogin}>
|
||||||
<Dialog.Trigger asChild>
|
<Dialog.Trigger asChild>
|
||||||
<button
|
<button type="button" className="btn btn-border" onClick={() => setShowLogin(true)}>
|
||||||
type="button"
|
<FormattedMessage defaultMessage="Login" />
|
||||||
className="btn btn-border"
|
|
||||||
onClick={() => setShowLogin(true)}
|
|
||||||
>
|
|
||||||
Login
|
|
||||||
<Icon name="login" />
|
<Icon name="login" />
|
||||||
</button>
|
</button>
|
||||||
</Dialog.Trigger>
|
</Dialog.Trigger>
|
||||||
@ -87,11 +81,7 @@ export function LayoutPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={`page${location.pathname.startsWith("/naddr1") ? " stream" : ""}`}>
|
||||||
className={`page${
|
|
||||||
location.pathname.startsWith("/naddr1") ? " stream" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>Home - zap.stream</title>
|
<title>Home - zap.stream</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
|
@ -162,12 +162,7 @@
|
|||||||
|
|
||||||
.tabs-tab[data-state="active"] .tab-border {
|
.tabs-tab[data-state="active"] .tab-border {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background: linear-gradient(
|
background: linear-gradient(94.73deg, #2bd9ff 0%, #8c8ded 47.4%, #f838d9 100%);
|
||||||
94.73deg,
|
|
||||||
#2bd9ff 0%,
|
|
||||||
#8c8ded 47.4%,
|
|
||||||
#f838d9 100%
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs-content {
|
.tabs-content {
|
||||||
|
@ -3,12 +3,7 @@ import { useMemo } from "react";
|
|||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import * as Tabs from "@radix-ui/react-tabs";
|
import * as Tabs from "@radix-ui/react-tabs";
|
||||||
import {
|
import { parseNostrLink, NostrPrefix, ParsedZap, encodeTLV } from "@snort/system";
|
||||||
parseNostrLink,
|
|
||||||
NostrPrefix,
|
|
||||||
ParsedZap,
|
|
||||||
encodeTLV,
|
|
||||||
} from "@snort/system";
|
|
||||||
import { useUserProfile } from "@snort/system-react";
|
import { useUserProfile } from "@snort/system-react";
|
||||||
import { unwrap } from "@snort/shared";
|
import { unwrap } from "@snort/shared";
|
||||||
import { Profile } from "element/profile";
|
import { Profile } from "element/profile";
|
||||||
@ -21,9 +16,10 @@ import { useProfile } from "hooks/profile";
|
|||||||
import useTopZappers from "hooks/top-zappers";
|
import useTopZappers from "hooks/top-zappers";
|
||||||
import usePlaceholder from "hooks/placeholders";
|
import usePlaceholder from "hooks/placeholders";
|
||||||
import { Text } from "element/text";
|
import { Text } from "element/text";
|
||||||
import { StreamState, System } from "index";
|
import { StreamState } from "index";
|
||||||
import { findTag } from "utils";
|
import { findTag } from "utils";
|
||||||
import { formatSats } from "number";
|
import { formatSats } from "number";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
function Zapper({ pubkey, total }: { pubkey: string; total: number }) {
|
function Zapper({ pubkey, total }: { pubkey: string; total: number }) {
|
||||||
return (
|
return (
|
||||||
@ -41,7 +37,7 @@ function TopZappers({ zaps }: { zaps: ParsedZap[] }) {
|
|||||||
const zappers = useTopZappers(zaps);
|
const zappers = useTopZappers(zaps);
|
||||||
return (
|
return (
|
||||||
<section className="profile-top-zappers">
|
<section className="profile-top-zappers">
|
||||||
{zappers.map((z) => (
|
{zappers.map(z => (
|
||||||
<Zapper key={z.pubkey} pubkey={z.pubkey} total={z.total} />
|
<Zapper key={z.pubkey} pubkey={z.pubkey} total={z.total} />
|
||||||
))}
|
))}
|
||||||
</section>
|
</section>
|
||||||
@ -55,32 +51,24 @@ export function ProfilePage() {
|
|||||||
const params = useParams();
|
const params = useParams();
|
||||||
const link = parseNostrLink(unwrap(params.npub));
|
const link = parseNostrLink(unwrap(params.npub));
|
||||||
const placeholder = usePlaceholder(link.id);
|
const placeholder = usePlaceholder(link.id);
|
||||||
const profile = useUserProfile(System, link.id);
|
const profile = useUserProfile(link.id);
|
||||||
const zapTarget = profile?.lud16 ?? profile?.lud06;
|
const zapTarget = profile?.lud16 ?? profile?.lud06;
|
||||||
const { streams, zaps } = useProfile(link, true);
|
const { streams, zaps } = useProfile(link, true);
|
||||||
const liveEvent = useMemo(() => {
|
const liveEvent = useMemo(() => {
|
||||||
return streams.find((ev) => findTag(ev, "status") === StreamState.Live);
|
return streams.find(ev => findTag(ev, "status") === StreamState.Live);
|
||||||
}, [streams]);
|
}, [streams]);
|
||||||
const pastStreams = useMemo(() => {
|
const pastStreams = useMemo(() => {
|
||||||
return streams.filter((ev) => findTag(ev, "status") === StreamState.Ended);
|
return streams.filter(ev => findTag(ev, "status") === StreamState.Ended);
|
||||||
}, [streams]);
|
}, [streams]);
|
||||||
const futureStreams = useMemo(() => {
|
const futureStreams = useMemo(() => {
|
||||||
return streams.filter(
|
return streams.filter(ev => findTag(ev, "status") === StreamState.Planned);
|
||||||
(ev) => findTag(ev, "status") === StreamState.Planned
|
|
||||||
);
|
|
||||||
}, [streams]);
|
}, [streams]);
|
||||||
const isLive = Boolean(liveEvent);
|
const isLive = Boolean(liveEvent);
|
||||||
|
|
||||||
function goToLive() {
|
function goToLive() {
|
||||||
if (liveEvent) {
|
if (liveEvent) {
|
||||||
const d = findTag(liveEvent, "d") || "";
|
const d = findTag(liveEvent, "d") || "";
|
||||||
const naddr = encodeTLV(
|
const naddr = encodeTLV(NostrPrefix.Address, d, undefined, liveEvent.kind, liveEvent.pubkey);
|
||||||
NostrPrefix.Address,
|
|
||||||
d,
|
|
||||||
undefined,
|
|
||||||
liveEvent.kind,
|
|
||||||
liveEvent.pubkey
|
|
||||||
);
|
|
||||||
navigate(`/${naddr}`);
|
navigate(`/${naddr}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -88,52 +76,39 @@ export function ProfilePage() {
|
|||||||
return (
|
return (
|
||||||
<div className="profile-page">
|
<div className="profile-page">
|
||||||
<div className="profile-container">
|
<div className="profile-container">
|
||||||
<img
|
<img className="banner" alt={profile?.name || link.id} src={profile?.banner || defaultBanner} />
|
||||||
className="banner"
|
|
||||||
alt={profile?.name || link.id}
|
|
||||||
src={profile?.banner || defaultBanner}
|
|
||||||
/>
|
|
||||||
<div className="profile-content">
|
<div className="profile-content">
|
||||||
{profile?.picture ? (
|
{profile?.picture ? (
|
||||||
<img
|
<img className="avatar" alt={profile.name || link.id} src={profile.picture} />
|
||||||
className="avatar"
|
|
||||||
alt={profile.name || link.id}
|
|
||||||
src={profile.picture}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<img
|
<img className="avatar" alt={profile?.name || link.id} src={placeholder} />
|
||||||
className="avatar"
|
|
||||||
alt={profile?.name || link.id}
|
|
||||||
src={placeholder}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
<div className="status-indicator">
|
<div className="status-indicator">
|
||||||
{isLive ? (
|
{isLive ? (
|
||||||
<div className="live-button pill live" onClick={goToLive}>
|
<div className="live-button pill live" onClick={goToLive}>
|
||||||
<Icon name="signal" />
|
<Icon name="signal" />
|
||||||
<span>live</span>
|
<span>
|
||||||
|
<FormattedMessage defaultMessage="live" />
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<span className="pill offline">offline</span>
|
<span className="pill offline">
|
||||||
|
<FormattedMessage defaultMessage="offline" />
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="profile-actions">
|
<div className="profile-actions">
|
||||||
{zapTarget && (
|
{zapTarget && (
|
||||||
<SendZapsDialog
|
<SendZapsDialog
|
||||||
aTag={
|
aTag={liveEvent ? `${liveEvent.kind}:${liveEvent.pubkey}:${findTag(liveEvent, "d")}` : undefined}
|
||||||
liveEvent
|
|
||||||
? `${liveEvent.kind}:${liveEvent.pubkey}:${findTag(
|
|
||||||
liveEvent,
|
|
||||||
"d"
|
|
||||||
)}`
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
lnurl={zapTarget}
|
lnurl={zapTarget}
|
||||||
button={
|
button={
|
||||||
<button className="btn">
|
<button className="btn">
|
||||||
<div className="zap-button">
|
<div className="zap-button">
|
||||||
<Icon name="zap-filled" className="zap-button-icon" />
|
<Icon name="zap-filled" className="zap-button-icon" />
|
||||||
<span>Zap</span>
|
<span>
|
||||||
|
<FormattedMessage defaultMessage="Zap" />
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@ -152,22 +127,17 @@ export function ProfilePage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Tabs.Root className="tabs-root" defaultValue="top-zappers">
|
<Tabs.Root className="tabs-root" defaultValue="top-zappers">
|
||||||
<Tabs.List
|
<Tabs.List className="tabs-list" aria-label={`Information about ${profile ? profile.name : link.id}`}>
|
||||||
className="tabs-list"
|
|
||||||
aria-label={`Information about ${
|
|
||||||
profile ? profile.name : link.id
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Tabs.Trigger className="tabs-tab" value="top-zappers">
|
<Tabs.Trigger className="tabs-tab" value="top-zappers">
|
||||||
Top Zappers
|
<FormattedMessage defaultMessage="Top Zappers" />
|
||||||
<div className="tab-border"></div>
|
<div className="tab-border"></div>
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
<Tabs.Trigger className="tabs-tab" value="past-streams">
|
<Tabs.Trigger className="tabs-tab" value="past-streams">
|
||||||
Past Streams
|
<FormattedMessage defaultMessage="Past Streams" />
|
||||||
<div className="tab-border"></div>
|
<div className="tab-border"></div>
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
<Tabs.Trigger className="tabs-tab" value="schedule">
|
<Tabs.Trigger className="tabs-tab" value="schedule">
|
||||||
Schedule
|
<FormattedMessage defaultMessage="Schedule" />
|
||||||
<div className="tab-border"></div>
|
<div className="tab-border"></div>
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
@ -176,14 +146,16 @@ export function ProfilePage() {
|
|||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
<Tabs.Content className="tabs-content" value="past-streams">
|
<Tabs.Content className="tabs-content" value="past-streams">
|
||||||
<div className="stream-list">
|
<div className="stream-list">
|
||||||
{pastStreams.map((ev) => (
|
{pastStreams.map(ev => (
|
||||||
<div key={ev.id} className="stream-item">
|
<div key={ev.id} className="stream-item">
|
||||||
<VideoTile ev={ev} showAuthor={false} showStatus={false} />
|
<VideoTile ev={ev} showAuthor={false} showStatus={false} />
|
||||||
<span className="timestamp">
|
<span className="timestamp">
|
||||||
Streamed on{" "}
|
<FormattedMessage
|
||||||
{moment(Number(ev.created_at) * 1000).format(
|
defaultMessage="Streamed on {date}"
|
||||||
"MMM DD, YYYY"
|
values={{
|
||||||
)}
|
date: moment(Number(ev.created_at) * 1000).format("MMM DD, YYYY"),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@ -191,14 +163,16 @@ export function ProfilePage() {
|
|||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
<Tabs.Content className="tabs-content" value="schedule">
|
<Tabs.Content className="tabs-content" value="schedule">
|
||||||
<div className="stream-list">
|
<div className="stream-list">
|
||||||
{futureStreams.map((ev) => (
|
{futureStreams.map(ev => (
|
||||||
<div key={ev.id} className="stream-item">
|
<div key={ev.id} className="stream-item">
|
||||||
<VideoTile ev={ev} showAuthor={false} showStatus={false} />
|
<VideoTile ev={ev} showAuthor={false} showStatus={false} />
|
||||||
<span className="timestamp">
|
<span className="timestamp">
|
||||||
Scheduled for{" "}
|
<FormattedMessage
|
||||||
{moment(Number(ev.created_at) * 1000).format(
|
defaultMessage="Scheduled for {date}"
|
||||||
"MMM DD, YYYY h:mm:ss a"
|
values={{
|
||||||
)}
|
date: moment(Number(ev.created_at) * 1000).format("MMM DD, YYYY h:mm:ss a"),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
@ -48,16 +48,9 @@ export function StreamProvidersPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="stream-providers-page">
|
<div className="stream-providers-page">
|
||||||
<h1>Providers</h1>
|
<h1>Providers</h1>
|
||||||
<p>
|
<p>Stream providers streamline the process of streaming on Nostr, some event accept lightning payments!</p>
|
||||||
Stream providers streamline the process of streaming on Nostr, some
|
|
||||||
event accept lightning payments!
|
|
||||||
</p>
|
|
||||||
<div className="stream-providers-grid">
|
<div className="stream-providers-grid">
|
||||||
{[
|
{[StreamProviders.NostrType, StreamProviders.Owncast, StreamProviders.Cloudflare].map(v => providerLink(v))}
|
||||||
StreamProviders.NostrType,
|
|
||||||
StreamProviders.Owncast,
|
|
||||||
StreamProviders.Cloudflare,
|
|
||||||
].map((v) => providerLink(v))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -6,6 +6,7 @@ import { StatePill } from "element/state-pill";
|
|||||||
import { StreamState } from "index";
|
import { StreamState } from "index";
|
||||||
import { StreamProviderInfo, StreamProviderStore } from "providers";
|
import { StreamProviderInfo, StreamProviderStore } from "providers";
|
||||||
import { Nip103StreamProvider } from "providers/zsz";
|
import { Nip103StreamProvider } from "providers/zsz";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
export function ConfigureNostrType() {
|
export function ConfigureNostrType() {
|
||||||
const [url, setUrl] = useState("");
|
const [url, setUrl] = useState("");
|
||||||
@ -59,9 +60,8 @@ export function ConfigureNostrType() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
StreamProviderStore.add(new Nip103StreamProvider(url));
|
StreamProviderStore.add(new Nip103StreamProvider(url));
|
||||||
navigate("/");
|
navigate("/");
|
||||||
}}
|
}}>
|
||||||
>
|
<FormattedMessage defaultMessage="Save" />
|
||||||
Save
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@ -74,16 +74,11 @@ export function ConfigureNostrType() {
|
|||||||
<div>
|
<div>
|
||||||
<p>Nostr streaming provider URL</p>
|
<p>Nostr streaming provider URL</p>
|
||||||
<div className="paper">
|
<div className="paper">
|
||||||
<input
|
<input type="text" placeholder="https://" value={url} onChange={e => setUrl(e.target.value)} />
|
||||||
type="text"
|
|
||||||
placeholder="https://"
|
|
||||||
value={url}
|
|
||||||
onChange={(e) => setUrl(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AsyncButton className="btn btn-primary" onClick={tryConnect}>
|
<AsyncButton className="btn btn-primary" onClick={tryConnect}>
|
||||||
Connect
|
<FormattedMessage defaultMessage="Connect" />
|
||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
</div>
|
</div>
|
||||||
<div>{status()}</div>
|
<div>{status()}</div>
|
||||||
|
@ -59,8 +59,7 @@ export function ConfigureOwncast() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
StreamProviderStore.add(new OwncastProvider(url, token));
|
StreamProviderStore.add(new OwncastProvider(url, token));
|
||||||
navigate("/");
|
navigate("/");
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -74,22 +73,13 @@ export function ConfigureOwncast() {
|
|||||||
<div>
|
<div>
|
||||||
<p>Owncast instance url</p>
|
<p>Owncast instance url</p>
|
||||||
<div className="paper">
|
<div className="paper">
|
||||||
<input
|
<input type="text" placeholder="https://" value={url} onChange={e => setUrl(e.target.value)} />
|
||||||
type="text"
|
|
||||||
placeholder="https://"
|
|
||||||
value={url}
|
|
||||||
onChange={(e) => setUrl(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p>API token</p>
|
<p>API token</p>
|
||||||
<div className="paper">
|
<div className="paper">
|
||||||
<input
|
<input type="password" value={token} onChange={e => setToken(e.target.value)} />
|
||||||
type="password"
|
|
||||||
value={token}
|
|
||||||
onChange={(e) => setToken(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AsyncButton className="btn btn-primary" onClick={tryConnect}>
|
<AsyncButton className="btn btn-primary" onClick={tryConnect}>
|
||||||
|
@ -15,19 +15,17 @@ export function RootPage() {
|
|||||||
const tags = login?.follows.tags ?? [];
|
const tags = login?.follows.tags ?? [];
|
||||||
const followsHost = useCallback(
|
const followsHost = useCallback(
|
||||||
(ev: NostrEvent) => {
|
(ev: NostrEvent) => {
|
||||||
return tags.find((t) => t.at(1) === getHost(ev));
|
return tags.find(t => t.at(1) === getHost(ev));
|
||||||
},
|
},
|
||||||
[tags]
|
[tags]
|
||||||
);
|
);
|
||||||
const hashtags = getTagValues(tags, "t");
|
const hashtags = getTagValues(tags, "t");
|
||||||
const following = live.filter(followsHost);
|
const following = live.filter(followsHost);
|
||||||
const liveNow = live.filter((e) => !following.includes(e));
|
const liveNow = live.filter(e => !following.includes(e));
|
||||||
const hasFollowingLive = following.length > 0;
|
const hasFollowingLive = following.length > 0;
|
||||||
|
|
||||||
const plannedEvents = planned
|
const plannedEvents = planned.filter(e => !mutedHosts.has(getHost(e))).filter(followsHost);
|
||||||
.filter((e) => !mutedHosts.has(getHost(e)))
|
const endedEvents = ended.filter(e => !mutedHosts.has(getHost(e)));
|
||||||
.filter(followsHost);
|
|
||||||
const endedEvents = ended.filter((e) => !mutedHosts.has(getHost(e)));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="homepage">
|
<div className="homepage">
|
||||||
@ -35,7 +33,7 @@ export function RootPage() {
|
|||||||
<>
|
<>
|
||||||
<h2 className="divider line one-line">Following</h2>
|
<h2 className="divider line one-line">Following</h2>
|
||||||
<div className="video-grid">
|
<div className="video-grid">
|
||||||
{following.map((e) => (
|
{following.map(e => (
|
||||||
<VideoTile ev={e} key={e.id} />
|
<VideoTile ev={e} key={e.id} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -44,23 +42,23 @@ export function RootPage() {
|
|||||||
{!hasFollowingLive && (
|
{!hasFollowingLive && (
|
||||||
<div className="video-grid">
|
<div className="video-grid">
|
||||||
{live
|
{live
|
||||||
.filter((e) => !mutedHosts.has(getHost(e)))
|
.filter(e => !mutedHosts.has(getHost(e)))
|
||||||
.map((e) => (
|
.map(e => (
|
||||||
<VideoTile ev={e} key={e.id} />
|
<VideoTile ev={e} key={e.id} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{hashtags.map((t) => (
|
{hashtags.map(t => (
|
||||||
<>
|
<>
|
||||||
<h2 className="divider line one-line">#{t}</h2>
|
<h2 className="divider line one-line">#{t}</h2>
|
||||||
<div className="video-grid">
|
<div className="video-grid">
|
||||||
{live
|
{live
|
||||||
.filter((e) => !mutedHosts.has(getHost(e)))
|
.filter(e => !mutedHosts.has(getHost(e)))
|
||||||
.filter((e) => {
|
.filter(e => {
|
||||||
const evTags = getTagValues(e.tags, "t");
|
const evTags = getTagValues(e.tags, "t");
|
||||||
return evTags.includes(t);
|
return evTags.includes(t);
|
||||||
})
|
})
|
||||||
.map((e) => (
|
.map(e => (
|
||||||
<VideoTile ev={e} key={e.id} />
|
<VideoTile ev={e} key={e.id} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -71,8 +69,8 @@ export function RootPage() {
|
|||||||
<h2 className="divider line one-line">Live</h2>
|
<h2 className="divider line one-line">Live</h2>
|
||||||
<div className="video-grid">
|
<div className="video-grid">
|
||||||
{liveNow
|
{liveNow
|
||||||
.filter((e) => !mutedHosts.has(getHost(e)))
|
.filter(e => !mutedHosts.has(getHost(e)))
|
||||||
.map((e) => (
|
.map(e => (
|
||||||
<VideoTile ev={e} key={e.id} />
|
<VideoTile ev={e} key={e.id} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -82,7 +80,7 @@ export function RootPage() {
|
|||||||
<>
|
<>
|
||||||
<h2 className="divider line one-line">Planned</h2>
|
<h2 className="divider line one-line">Planned</h2>
|
||||||
<div className="video-grid">
|
<div className="video-grid">
|
||||||
{plannedEvents.map((e) => (
|
{plannedEvents.map(e => (
|
||||||
<VideoTile ev={e} key={e.id} />
|
<VideoTile ev={e} key={e.id} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -92,7 +90,7 @@ export function RootPage() {
|
|||||||
<>
|
<>
|
||||||
<h2 className="divider line one-line">Ended</h2>
|
<h2 className="divider line one-line">Ended</h2>
|
||||||
<div className="video-grid">
|
<div className="video-grid">
|
||||||
{endedEvents.map((e) => (
|
{endedEvents.map(e => (
|
||||||
<VideoTile ev={e} key={e.id} />
|
<VideoTile ev={e} key={e.id} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,9 +2,7 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto 450px;
|
grid-template-columns: auto 450px;
|
||||||
gap: var(--gap-m);
|
gap: var(--gap-m);
|
||||||
height: calc(
|
height: calc(100vh - var(--header-page-padding) - var(--header-height) - var(--gap-s));
|
||||||
100vh - var(--header-page-padding) - var(--header-height) - var(--gap-s)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.stream-page .video-content {
|
.stream-page .video-content {
|
||||||
@ -33,19 +31,14 @@
|
|||||||
padding: 24px 16px 8px 24px;
|
padding: 24px 16px 8px 24px;
|
||||||
border: 1px solid #171717;
|
border: 1px solid #171717;
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
height: calc(
|
height: calc(100vh - var(--header-page-padding) - var(--header-height) - var(--gap-s) - 24px - 8px);
|
||||||
100vh - var(--header-page-padding) - var(--header-height) - var(--gap-s) -
|
|
||||||
24px - 8px
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1020px) {
|
@media (max-width: 1020px) {
|
||||||
.stream-page {
|
.stream-page {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: calc(
|
height: calc(100vh - var(--header-page-padding) - var(--header-height) - var(--gap-s));
|
||||||
100vh - var(--header-page-padding) - var(--header-height) - var(--gap-s)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.stream-page .video-content {
|
.stream-page .video-content {
|
||||||
|
@ -1,17 +1,11 @@
|
|||||||
import "./stream-page.css";
|
import "./stream-page.css";
|
||||||
import { NostrLink, NostrPrefix, TaggedRawEvent, tryParseNostrLink } from "@snort/system";
|
import { NostrLink, NostrPrefix, TaggedNostrEvent, tryParseNostrLink } from "@snort/system";
|
||||||
import { fetchNip05Pubkey } from "@snort/shared";
|
import { fetchNip05Pubkey } from "@snort/shared";
|
||||||
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
|
|
||||||
import { LiveVideoPlayer } from "element/live-video-player";
|
import { LiveVideoPlayer } from "element/live-video-player";
|
||||||
import {
|
import { createNostrLink, findTag, getEventFromLocationState, getHost, hexToBech32 } from "utils";
|
||||||
createNostrLink,
|
|
||||||
findTag,
|
|
||||||
getEventFromLocationState,
|
|
||||||
getHost,
|
|
||||||
hexToBech32,
|
|
||||||
} from "utils";
|
|
||||||
import { Profile, getName } from "element/profile";
|
import { Profile, getName } from "element/profile";
|
||||||
import { LiveChat } from "element/live-chat";
|
import { LiveChat } from "element/live-chat";
|
||||||
import AsyncButton from "element/async-button";
|
import AsyncButton from "element/async-button";
|
||||||
@ -28,18 +22,15 @@ import { StreamCards } from "element/stream-cards";
|
|||||||
import { formatSats } from "number";
|
import { formatSats } from "number";
|
||||||
import { StreamTimer } from "element/stream-time";
|
import { StreamTimer } from "element/stream-time";
|
||||||
import { ShareMenu } from "element/share-menu";
|
import { ShareMenu } from "element/share-menu";
|
||||||
import {
|
import { ContentWarningOverlay, isContentWarningAccepted } from "element/content-warning";
|
||||||
ContentWarningOverlay,
|
|
||||||
isContentWarningAccepted,
|
|
||||||
} from "element/content-warning";
|
|
||||||
import { useCurrentStreamFeed } from "hooks/current-stream-feed";
|
import { useCurrentStreamFeed } from "hooks/current-stream-feed";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
function ProfileInfo({ ev, goal }: { ev?: NostrEvent; goal?: TaggedRawEvent }) {
|
function ProfileInfo({ ev, goal }: { ev?: NostrEvent; goal?: TaggedNostrEvent }) {
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const host = getHost(ev);
|
const host = getHost(ev);
|
||||||
const profile = useUserProfile(System, host);
|
const profile = useUserProfile(host);
|
||||||
const zapTarget = profile?.lud16 ?? profile?.lud06;
|
const zapTarget = profile?.lud16 ?? profile?.lud06;
|
||||||
|
|
||||||
const status = findTag(ev, "status") ?? "";
|
const status = findTag(ev, "status") ?? "";
|
||||||
@ -64,11 +55,7 @@ function ProfileInfo({ ev, goal }: { ev?: NostrEvent; goal?: TaggedRawEvent }) {
|
|||||||
<p>{findTag(ev, "summary")}</p>
|
<p>{findTag(ev, "summary")}</p>
|
||||||
<div className="tags">
|
<div className="tags">
|
||||||
<StatePill state={status as StreamState} />
|
<StatePill state={status as StreamState} />
|
||||||
{viewers > 0 && (
|
{viewers > 0 && <span className="pill viewers">{formatSats(viewers)} viewers</span>}
|
||||||
<span className="pill viewers">
|
|
||||||
{formatSats(viewers)} viewers
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{status === StreamState.Live && (
|
{status === StreamState.Live && (
|
||||||
<span className="pill">
|
<span className="pill">
|
||||||
<StreamTimer ev={ev} />
|
<StreamTimer ev={ev} />
|
||||||
@ -79,11 +66,7 @@ function ProfileInfo({ ev, goal }: { ev?: NostrEvent; goal?: TaggedRawEvent }) {
|
|||||||
{isMine && (
|
{isMine && (
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
{ev && <NewStreamDialog text="Edit" ev={ev} btnClassName="btn" />}
|
{ev && <NewStreamDialog text="Edit" ev={ev} btnClassName="btn" />}
|
||||||
<AsyncButton
|
<AsyncButton type="button" className="btn btn-warning" onClick={deleteStream}>
|
||||||
type="button"
|
|
||||||
className="btn btn-warning"
|
|
||||||
onClick={deleteStream}
|
|
||||||
>
|
|
||||||
Delete
|
Delete
|
||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
</div>
|
</div>
|
||||||
@ -131,20 +114,20 @@ export function StreamPageHandler() {
|
|||||||
setLink({
|
setLink({
|
||||||
id: d,
|
id: d,
|
||||||
type: NostrPrefix.PublicKey,
|
type: NostrPrefix.PublicKey,
|
||||||
encode: () => hexToBech32(NostrPrefix.PublicKey, d)
|
encode: () => hexToBech32(NostrPrefix.PublicKey, d),
|
||||||
} as NostrLink);
|
} as NostrLink);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [params.id]);
|
}, [params.id]);
|
||||||
|
|
||||||
if (link) {
|
if (link) {
|
||||||
return <StreamPage link={link} evPreload={evPreload} />
|
return <StreamPage link={link} evPreload={evPreload} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StreamPage({ link, evPreload }: { evPreload?: NostrEvent, link: NostrLink }) {
|
export function StreamPage({ link, evPreload }: { evPreload?: NostrEvent; link: NostrLink }) {
|
||||||
const ev = useCurrentStreamFeed(link, true, evPreload);
|
const ev = useCurrentStreamFeed(link, true, evPreload);
|
||||||
const host = getHost(ev);
|
const host = getHost(ev);
|
||||||
const goal = useZapGoal(host, createNostrLink(ev), true);
|
const goal = useZapGoal(host, createNostrLink(ev), true);
|
||||||
@ -153,31 +136,21 @@ export function StreamPage({ link, evPreload }: { evPreload?: NostrEvent, link:
|
|||||||
const summary = findTag(ev, "summary");
|
const summary = findTag(ev, "summary");
|
||||||
const image = findTag(ev, "image");
|
const image = findTag(ev, "image");
|
||||||
const status = findTag(ev, "status");
|
const status = findTag(ev, "status");
|
||||||
const stream =
|
const stream = status === StreamState.Live ? findTag(ev, "streaming") : findTag(ev, "recording");
|
||||||
status === StreamState.Live
|
|
||||||
? findTag(ev, "streaming")
|
|
||||||
: findTag(ev, "recording");
|
|
||||||
const contentWarning = findTag(ev, "content-warning");
|
const contentWarning = findTag(ev, "content-warning");
|
||||||
const tags = ev?.tags.filter((a) => a[0] === "t").map((a) => a[1]) ?? [];
|
const tags = ev?.tags.filter(a => a[0] === "t").map(a => a[1]) ?? [];
|
||||||
|
|
||||||
if (contentWarning && !isContentWarningAccepted()) {
|
if (contentWarning && !isContentWarningAccepted()) {
|
||||||
return <ContentWarningOverlay />;
|
return <ContentWarningOverlay />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const descriptionContent = [
|
const descriptionContent = [title, (summary?.length ?? 0) > 0 ? summary : "Nostr live streaming", ...tags].join(", ");
|
||||||
title,
|
|
||||||
(summary?.length ?? 0) > 0 ? summary : "Nostr live streaming",
|
|
||||||
...tags,
|
|
||||||
].join(", ");
|
|
||||||
return (
|
return (
|
||||||
<div className="stream-page">
|
<div className="stream-page">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{`${title} - zap.stream`}</title>
|
<title>{`${title} - zap.stream`}</title>
|
||||||
<meta name="description" content={descriptionContent} />
|
<meta name="description" content={descriptionContent} />
|
||||||
<meta
|
<meta property="og:url" content={`https://${window.location.host}/${link.encode()}`} />
|
||||||
property="og:url"
|
|
||||||
content={`https://${window.location.host}/${link.encode()}`}
|
|
||||||
/>
|
|
||||||
<meta property="og:type" content="video" />
|
<meta property="og:type" content="video" />
|
||||||
<meta property="og:title" content={title} />
|
<meta property="og:title" content={title} />
|
||||||
<meta property="og:description" content={descriptionContent} />
|
<meta property="og:description" content={descriptionContent} />
|
||||||
|
@ -16,7 +16,7 @@ export function TagPage() {
|
|||||||
<FollowTagButton tag={unwrap(tag)} />
|
<FollowTagButton tag={unwrap(tag)} />
|
||||||
</div>
|
</div>
|
||||||
<div className="video-grid">
|
<div className="video-grid">
|
||||||
{live.map((e) => (
|
{live.map(e => (
|
||||||
<VideoTile ev={e} key={e.id} />
|
<VideoTile ev={e} key={e.id} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -80,8 +80,7 @@ export class ProviderStore extends ExternalStore<Array<StreamProvider>> {
|
|||||||
super();
|
super();
|
||||||
const cache = window.localStorage.getItem("providers");
|
const cache = window.localStorage.getItem("providers");
|
||||||
if (cache) {
|
if (cache) {
|
||||||
const cached: Array<{ type: StreamProviders } & Record<string, unknown>> =
|
const cached: Array<{ type: StreamProviders } & Record<string, unknown>> = JSON.parse(cache);
|
||||||
JSON.parse(cache);
|
|
||||||
for (const c of cached) {
|
for (const c of cached) {
|
||||||
switch (c.type) {
|
switch (c.type) {
|
||||||
case StreamProviders.Manual: {
|
case StreamProviders.Manual: {
|
||||||
@ -93,9 +92,7 @@ export class ProviderStore extends ExternalStore<Array<StreamProvider>> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case StreamProviders.Owncast: {
|
case StreamProviders.Owncast: {
|
||||||
this.#providers.push(
|
this.#providers.push(new OwncastProvider(c.url as string, c.token as string));
|
||||||
new OwncastProvider(c.url as string, c.token as string)
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -110,14 +107,12 @@ export class ProviderStore extends ExternalStore<Array<StreamProvider>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
takeSnapshot() {
|
takeSnapshot() {
|
||||||
const defaultProvider = new Nip103StreamProvider(
|
const defaultProvider = new Nip103StreamProvider("https://api.zap.stream/api/nostr/");
|
||||||
"https://api.zap.stream/api/nostr/"
|
|
||||||
);
|
|
||||||
return [defaultProvider, new ManualProvider(), ...this.#providers];
|
return [defaultProvider, new ManualProvider(), ...this.#providers];
|
||||||
}
|
}
|
||||||
|
|
||||||
#save() {
|
#save() {
|
||||||
const cfg = this.#providers.map((a) => a.createConfig());
|
const cfg = this.#providers.map(a => a.createConfig());
|
||||||
window.localStorage.setItem("providers", JSON.stringify(cfg));
|
window.localStorage.setItem("providers", JSON.stringify(cfg));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,11 +52,7 @@ export class OwncastProvider implements StreamProvider {
|
|||||||
throw new Error("Method not implemented.");
|
throw new Error("Method not implemented.");
|
||||||
}
|
}
|
||||||
|
|
||||||
async #getJson<T>(
|
async #getJson<T>(method: "GET" | "POST", path: string, body?: unknown): Promise<T> {
|
||||||
method: "GET" | "POST",
|
|
||||||
path: string,
|
|
||||||
body?: unknown
|
|
||||||
): Promise<T> {
|
|
||||||
const rsp = await fetch(`${this.#url}${path}`, {
|
const rsp = await fetch(`${this.#url}${path}`, {
|
||||||
method,
|
method,
|
||||||
body: body ? JSON.stringify(body) : undefined,
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
@ -36,7 +36,7 @@ export class Nip103StreamProvider implements StreamProvider {
|
|||||||
balance: rsp.balance,
|
balance: rsp.balance,
|
||||||
tosAccepted: rsp.tos?.accepted,
|
tosAccepted: rsp.tos?.accepted,
|
||||||
tosLink: rsp.tos?.link,
|
tosLink: rsp.tos?.link,
|
||||||
endpoints: rsp.endpoints.map((a) => {
|
endpoints: rsp.endpoints.map(a => {
|
||||||
return {
|
return {
|
||||||
name: a.name,
|
name: a.name,
|
||||||
url: a.url,
|
url: a.url,
|
||||||
@ -60,7 +60,7 @@ export class Nip103StreamProvider implements StreamProvider {
|
|||||||
const title = findTag(ev, "title");
|
const title = findTag(ev, "title");
|
||||||
const summary = findTag(ev, "summary");
|
const summary = findTag(ev, "summary");
|
||||||
const image = findTag(ev, "image");
|
const image = findTag(ev, "image");
|
||||||
const tags = ev?.tags.filter((a) => a[0] === "t").map((a) => a[1]);
|
const tags = ev?.tags.filter(a => a[0] === "t").map(a => a[1]);
|
||||||
const contentWarning = findTag(ev, "content-warning");
|
const contentWarning = findTag(ev, "content-warning");
|
||||||
await this.#getJson("PATCH", "event", {
|
await this.#getJson("PATCH", "event", {
|
||||||
title,
|
title,
|
||||||
@ -72,10 +72,7 @@ export class Nip103StreamProvider implements StreamProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async topup(amount: number): Promise<string> {
|
async topup(amount: number): Promise<string> {
|
||||||
const rsp = await this.#getJson<TopUpResponse>(
|
const rsp = await this.#getJson<TopUpResponse>("GET", `topup?amount=${amount}`);
|
||||||
"GET",
|
|
||||||
`topup?amount=${amount}`
|
|
||||||
);
|
|
||||||
return rsp.pr;
|
return rsp.pr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,22 +82,14 @@ export class Nip103StreamProvider implements StreamProvider {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async #getJson<T>(
|
async #getJson<T>(method: "GET" | "POST" | "PATCH", path: string, body?: unknown): Promise<T> {
|
||||||
method: "GET" | "POST" | "PATCH",
|
|
||||||
path: string,
|
|
||||||
body?: unknown
|
|
||||||
): Promise<T> {
|
|
||||||
const login = Login.snapshot();
|
const login = Login.snapshot();
|
||||||
const pub = login && getPublisher(login);
|
const pub = login && getPublisher(login);
|
||||||
if (!pub) throw new Error("No signer");
|
if (!pub) throw new Error("No signer");
|
||||||
|
|
||||||
const u = `${this.#url}${path}`;
|
const u = `${this.#url}${path}`;
|
||||||
const token = await pub.generic((eb) => {
|
const token = await pub.generic(eb => {
|
||||||
return eb
|
return eb.kind(EventKind.HttpAuthentication).content("").tag(["u", u]).tag(["method", method]);
|
||||||
.kind(EventKind.HttpAuthentication)
|
|
||||||
.content("")
|
|
||||||
.tag(["u", u])
|
|
||||||
.tag(["method", method]);
|
|
||||||
});
|
});
|
||||||
const rsp = await fetch(u, {
|
const rsp = await fetch(u, {
|
||||||
method,
|
method,
|
||||||
|
@ -1,40 +1,15 @@
|
|||||||
/// <reference lib="webworker" />
|
/// <reference lib="webworker" />
|
||||||
import {} from ".";
|
declare const self: ServiceWorkerGlobalScope & {
|
||||||
declare const self: ServiceWorkerGlobalScope;
|
__WB_MANIFEST: (string | PrecacheEntry)[];
|
||||||
|
};
|
||||||
|
|
||||||
import { clientsClaim } from "workbox-core";
|
import { clientsClaim } from "workbox-core";
|
||||||
import { registerRoute } from "workbox-routing";
|
import { PrecacheEntry, precacheAndRoute } from "workbox-precaching";
|
||||||
import { CacheFirst } from "workbox-strategies";
|
|
||||||
|
|
||||||
|
precacheAndRoute(self.__WB_MANIFEST);
|
||||||
clientsClaim();
|
clientsClaim();
|
||||||
|
|
||||||
const staticTypes = ["image", "video", "audio", "script", "style", "font"];
|
self.addEventListener("message", event => {
|
||||||
registerRoute(
|
|
||||||
({ request, url }) =>
|
|
||||||
url.origin === self.location.origin &&
|
|
||||||
staticTypes.includes(request.destination),
|
|
||||||
new CacheFirst({
|
|
||||||
cacheName: "static-content",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// External media domains which have unique urls (never changing content) and can be cached forever
|
|
||||||
const externalMediaHosts = [
|
|
||||||
"void.cat",
|
|
||||||
"nostr.build",
|
|
||||||
"imgur.com",
|
|
||||||
"i.imgur.com",
|
|
||||||
"pbs.twimg.com",
|
|
||||||
"i.ibb.co",
|
|
||||||
];
|
|
||||||
registerRoute(
|
|
||||||
({ url }) => externalMediaHosts.includes(url.host),
|
|
||||||
new CacheFirst({
|
|
||||||
cacheName: "ext-content-hosts",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
self.addEventListener("message", (event) => {
|
|
||||||
if (event.data && event.data.type === "SKIP_WAITING") {
|
if (event.data && event.data.type === "SKIP_WAITING") {
|
||||||
self.skipWaiting();
|
self.skipWaiting();
|
||||||
}
|
}
|
||||||
|
30
src/serviceWorker.ts
Normal file
30
src/serviceWorker.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
export function register() {
|
||||||
|
if ("serviceWorker" in navigator) {
|
||||||
|
window.addEventListener("load", () => {
|
||||||
|
registerValidSW("/service-worker.js");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function registerValidSW(swUrl: string) {
|
||||||
|
try {
|
||||||
|
const registration = await navigator.serviceWorker.register(swUrl);
|
||||||
|
registration.onupdatefound = () => {
|
||||||
|
const installingWorker = registration.installing;
|
||||||
|
if (installingWorker === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
installingWorker.onstatechange = () => {
|
||||||
|
if (installingWorker.state === "installed") {
|
||||||
|
if (navigator.serviceWorker.controller) {
|
||||||
|
console.log("Service worker updated, pending reload");
|
||||||
|
} else {
|
||||||
|
console.log("Content is cached for offline use.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error during service worker registration:", e);
|
||||||
|
}
|
||||||
|
}
|
95
src/translations/en.json
Normal file
95
src/translations/en.json
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
{
|
||||||
|
"+0zv6g": "Image",
|
||||||
|
"+vVZ/G": "Connect",
|
||||||
|
"/0TOL5": "Amount",
|
||||||
|
"/GCoTA": "Clear",
|
||||||
|
"04lmFi": "Save Key",
|
||||||
|
"0GfNiL": "Stream Zap Goals",
|
||||||
|
"1EYCdR": "Tags",
|
||||||
|
"2/2yg+": "Add",
|
||||||
|
"2CGh/0": "live",
|
||||||
|
"3HwrQo": "Zap!",
|
||||||
|
"3adEeb": "{n} viewers",
|
||||||
|
"47FYwb": "Cancel",
|
||||||
|
"4l6vz1": "Copy",
|
||||||
|
"4uI538": "Resolutions",
|
||||||
|
"5JcXdV": "Create Account",
|
||||||
|
"5QYdPU": "Start Time",
|
||||||
|
"5kx+2v": "Server Url",
|
||||||
|
"6Z2pvJ": "Stream Providers",
|
||||||
|
"9WRlF4": "Send",
|
||||||
|
"9a9+ww": "Title",
|
||||||
|
"9anxhq": "Starts",
|
||||||
|
"AIHaPH": "{person} zapped {amount} sats",
|
||||||
|
"Atr2p4": "NSFW Content",
|
||||||
|
"AyGauy": "Login",
|
||||||
|
"BGxpTN": "Stream Chat",
|
||||||
|
"C81/uG": "Logout",
|
||||||
|
"ESyhzp": "Your comment for {name}",
|
||||||
|
"G/yZLu": "Remove",
|
||||||
|
"Gq6x9o": "Cover Image",
|
||||||
|
"H5+NAX": "Balance",
|
||||||
|
"HAlOn1": "Name",
|
||||||
|
"I1kjHI": "Supports {markdown}",
|
||||||
|
"IJDKz3": "Zap amount in {currency}",
|
||||||
|
"Jq3FDz": "Content",
|
||||||
|
"K3r6DQ": "Delete",
|
||||||
|
"K3uH1C": "offline",
|
||||||
|
"K7AkdL": "Show",
|
||||||
|
"KkIL3s": "No, I am under 18",
|
||||||
|
"Ld5LAE": "Nostr uses private keys, please save yours, if you lose this key you wont be able to login to your account anymore!",
|
||||||
|
"LknBsU": "Stream Key",
|
||||||
|
"My6HwN": "Ok, it's safe",
|
||||||
|
"O2Cy6m": "Yes, I am over 18",
|
||||||
|
"OKhRC6": "Share",
|
||||||
|
"OWgHbg": "Edit card",
|
||||||
|
"Q3au2v": "About {estimate}",
|
||||||
|
"QRHNuF": "What are we steaming today?",
|
||||||
|
"QRRCp0": "Stream URL",
|
||||||
|
"QceMQZ": "Goal: {amount}",
|
||||||
|
"RJOmzk": "I have read and agree with {provider}''s {terms}.",
|
||||||
|
"RXQdxR": "Please login to write messages!",
|
||||||
|
"RrCui3": "Summary",
|
||||||
|
"TaTRKo": "Start Stream",
|
||||||
|
"UJBFYK": "Add Card",
|
||||||
|
"UfSot5": "Past Streams",
|
||||||
|
"VA/Z1S": "Hide",
|
||||||
|
"W9355R": "Unmute",
|
||||||
|
"X2PZ7D": "Create Goal",
|
||||||
|
"ZmqxZs": "You can change this later",
|
||||||
|
"acrOoz": "Continue",
|
||||||
|
"cvAsEh": "Streamed on {date}",
|
||||||
|
"cyR7Kh": "Back",
|
||||||
|
"dVD/AR": "Top Zappers",
|
||||||
|
"ebmhes": "Nostr Extension",
|
||||||
|
"fBI91o": "Zap",
|
||||||
|
"hGQqkW": "Schedule",
|
||||||
|
"ieGrWo": "Follow",
|
||||||
|
"itPgxd": "Profile",
|
||||||
|
"izWS4J": "Unfollow",
|
||||||
|
"jr4+vD": "Markdown",
|
||||||
|
"jvo0vs": "Save",
|
||||||
|
"lZpRMR": "Check here if this stream contains nudity or pornographic content.",
|
||||||
|
"ljmS5P": "Endpoint",
|
||||||
|
"mtNGwh": "A short description of the content",
|
||||||
|
"nBCvvJ": "Topup",
|
||||||
|
"nOaArs": "Setup Profile",
|
||||||
|
"nwA8Os": "Add card",
|
||||||
|
"oHPB8Q": "Zap {name}",
|
||||||
|
"oZrFyI": "Stream type should be HLS",
|
||||||
|
"pO/lPX": "Scheduled for {date}",
|
||||||
|
"rWBFZA": "Sexually explicit material ahead!",
|
||||||
|
"rbrahO": "Close",
|
||||||
|
"rfC1Zq": "Save card",
|
||||||
|
"s5ksS7": "Image Link",
|
||||||
|
"s7V+5p": "Confirm your age",
|
||||||
|
"thsiMl": "terms and conditions",
|
||||||
|
"tzMNF3": "Status",
|
||||||
|
"uYw2LD": "Stream",
|
||||||
|
"vrTOHJ": "{amount} sats",
|
||||||
|
"wCIL7o": "Broadcast on Nostr",
|
||||||
|
"wEQDC6": "Edit",
|
||||||
|
"wOy57k": "Add stream goal",
|
||||||
|
"wzWWzV": "Top zappers",
|
||||||
|
"x82IOl": "Mute"
|
||||||
|
}
|
53
src/utils.ts
53
src/utils.ts
@ -1,10 +1,4 @@
|
|||||||
import {
|
import { NostrEvent, NostrPrefix, TaggedNostrEvent, encodeTLV, parseNostrLink } from "@snort/system";
|
||||||
NostrEvent,
|
|
||||||
NostrPrefix,
|
|
||||||
TaggedRawEvent,
|
|
||||||
encodeTLV,
|
|
||||||
parseNostrLink,
|
|
||||||
} from "@snort/system";
|
|
||||||
import * as utils from "@noble/curves/abstract/utils";
|
import * as utils from "@noble/curves/abstract/utils";
|
||||||
import { bech32 } from "@scure/base";
|
import { bech32 } from "@scure/base";
|
||||||
import type { Tag, Tags } from "types";
|
import type { Tag, Tags } from "types";
|
||||||
@ -39,7 +33,7 @@ export function toTag(e: NostrEvent): Tag {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function findTag(e: NostrEvent | undefined, tag: string) {
|
export function findTag(e: NostrEvent | undefined, tag: string) {
|
||||||
const maybeTag = e?.tags.find((evTag) => {
|
const maybeTag = e?.tags.find(evTag => {
|
||||||
return evTag[0] === tag;
|
return evTag[0] === tag;
|
||||||
});
|
});
|
||||||
return maybeTag && maybeTag[1];
|
return maybeTag && maybeTag[1];
|
||||||
@ -54,11 +48,7 @@ export function hexToBech32(hrp: string, hex?: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (
|
if (hrp === NostrPrefix.Note || hrp === NostrPrefix.PrivateKey || hrp === NostrPrefix.PublicKey) {
|
||||||
hrp === NostrPrefix.Note ||
|
|
||||||
hrp === NostrPrefix.PrivateKey ||
|
|
||||||
hrp === NostrPrefix.PublicKey
|
|
||||||
) {
|
|
||||||
const buf = utils.hexToBytes(hex);
|
const buf = utils.hexToBytes(hex);
|
||||||
return bech32.encode(hrp, bech32.toWords(buf));
|
return bech32.encode(hrp, bech32.toWords(buf));
|
||||||
} else {
|
} else {
|
||||||
@ -77,22 +67,12 @@ export function splitByUrl(str: string) {
|
|||||||
return str.split(urlRegex);
|
return str.split(urlRegex);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function eventLink(ev: NostrEvent | TaggedRawEvent) {
|
export function eventLink(ev: NostrEvent | TaggedNostrEvent) {
|
||||||
if (ev.kind && ev.kind >= 30000 && ev.kind <= 40000) {
|
if (ev.kind && ev.kind >= 30000 && ev.kind <= 40000) {
|
||||||
const d = findTag(ev, "d") ?? "";
|
const d = findTag(ev, "d") ?? "";
|
||||||
return encodeTLV(
|
return encodeTLV(NostrPrefix.Address, d, "relays" in ev ? ev.relays : undefined, ev.kind, ev.pubkey);
|
||||||
NostrPrefix.Address,
|
|
||||||
d,
|
|
||||||
"relays" in ev ? ev.relays : undefined,
|
|
||||||
ev.kind,
|
|
||||||
ev.pubkey
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
return encodeTLV(
|
return encodeTLV(NostrPrefix.Event, ev.id, "relays" in ev ? ev.relays : undefined);
|
||||||
NostrPrefix.Event,
|
|
||||||
ev.id,
|
|
||||||
"relays" in ev ? ev.relays : undefined
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,15 +82,11 @@ export function createNostrLink(ev?: NostrEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getHost(ev?: NostrEvent) {
|
export function getHost(ev?: NostrEvent) {
|
||||||
return (
|
return ev?.tags.find(a => a[0] === "p" && a[3] === "host")?.[1] ?? ev?.pubkey ?? "";
|
||||||
ev?.tags.find((a) => a[0] === "p" && a[3] === "host")?.[1] ??
|
|
||||||
ev?.pubkey ??
|
|
||||||
""
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function openFile(): Promise<File | undefined> {
|
export function openFile(): Promise<File | undefined> {
|
||||||
return new Promise((resolve) => {
|
return new Promise(resolve => {
|
||||||
const elm = document.createElement("input");
|
const elm = document.createElement("input");
|
||||||
elm.type = "file";
|
elm.type = "file";
|
||||||
elm.onchange = (e: Event) => {
|
elm.onchange = (e: Event) => {
|
||||||
@ -127,17 +103,14 @@ export function openFile(): Promise<File | undefined> {
|
|||||||
|
|
||||||
export function getTagValues(tags: Tags, tag: string): Array<string> {
|
export function getTagValues(tags: Tags, tag: string): Array<string> {
|
||||||
return tags
|
return tags
|
||||||
.filter((t) => t.at(0) === tag)
|
.filter(t => t.at(0) === tag)
|
||||||
.map((t) => t.at(1))
|
.map(t => t.at(1))
|
||||||
.filter((t) => t)
|
.filter(t => t)
|
||||||
.map((t) => t as string);
|
.map(t => t as string);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getEventFromLocationState(state: unknown | undefined | null) {
|
export function getEventFromLocationState(state: unknown | undefined | null) {
|
||||||
return state &&
|
return state && typeof state === "object" && "kind" in state && state.kind === LIVE_STREAM
|
||||||
typeof state === "object" &&
|
|
||||||
"kind" in state &&
|
|
||||||
state.kind === LIVE_STREAM
|
|
||||||
? (state as NostrEvent)
|
? (state as NostrEvent)
|
||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,7 @@ import { CandidateInfo, SDPInfo } from "semantic-sdp";
|
|||||||
import { TypedEventTarget, type StatusEvent, type LogEvent } from "./events";
|
import { TypedEventTarget, type StatusEvent, type LogEvent } from "./events";
|
||||||
import { parserLinkHeader } from "./parser";
|
import { parserLinkHeader } from "./parser";
|
||||||
|
|
||||||
export const DEFAULT_ICE_SERVERS = [
|
export const DEFAULT_ICE_SERVERS = ["stun:stun.cloudflare.com:3478", "stun:stun.l.google.com:19302"];
|
||||||
"stun:stun.cloudflare.com:3478",
|
|
||||||
"stun:stun.l.google.com:19302",
|
|
||||||
];
|
|
||||||
|
|
||||||
export const TRICKLE_BATCH_INTERVAL = 50;
|
export const TRICKLE_BATCH_INTERVAL = 50;
|
||||||
|
|
||||||
@ -49,9 +46,7 @@ export class WISH extends TypedEventTarget {
|
|||||||
if (iceServers) {
|
if (iceServers) {
|
||||||
this.iceServers = iceServers ? iceServers : DEFAULT_ICE_SERVERS;
|
this.iceServers = iceServers ? iceServers : DEFAULT_ICE_SERVERS;
|
||||||
}
|
}
|
||||||
this.logMessage(
|
this.logMessage(`Enabling webrtc-adapter for ${adapter.browserDetails.browser}@${adapter.browserDetails.version}`);
|
||||||
`Enabling webrtc-adapter for ${adapter.browserDetails.browser}@${adapter.browserDetails.version}`
|
|
||||||
);
|
|
||||||
this.newResolvers();
|
this.newResolvers();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,7 +94,7 @@ export class WISH extends TypedEventTarget {
|
|||||||
this.connectedResolver = resolve;
|
this.connectedResolver = resolve;
|
||||||
this.connectedRejector = reject;
|
this.connectedRejector = reject;
|
||||||
});
|
});
|
||||||
this.gatherPromise = new Promise((resolve) => {
|
this.gatherPromise = new Promise(resolve => {
|
||||||
this.gatherResolver = resolve;
|
this.gatherResolver = resolve;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -108,36 +103,19 @@ export class WISH extends TypedEventTarget {
|
|||||||
if (!this.peerConnection) {
|
if (!this.peerConnection) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.peerConnection.addEventListener(
|
this.peerConnection.addEventListener("connectionstatechange", this.onConnectionStateChange.bind(this));
|
||||||
"connectionstatechange",
|
this.peerConnection.addEventListener("iceconnectionstatechange", this.onICEConnectionStateChange.bind(this));
|
||||||
this.onConnectionStateChange.bind(this)
|
this.peerConnection.addEventListener("icegatheringstatechange", this.onGatheringStateChange.bind(this));
|
||||||
);
|
this.peerConnection.addEventListener("icecandidate", this.onICECandidate.bind(this));
|
||||||
this.peerConnection.addEventListener(
|
|
||||||
"iceconnectionstatechange",
|
|
||||||
this.onICEConnectionStateChange.bind(this)
|
|
||||||
);
|
|
||||||
this.peerConnection.addEventListener(
|
|
||||||
"icegatheringstatechange",
|
|
||||||
this.onGatheringStateChange.bind(this)
|
|
||||||
);
|
|
||||||
this.peerConnection.addEventListener(
|
|
||||||
"icecandidate",
|
|
||||||
this.onICECandidate.bind(this)
|
|
||||||
);
|
|
||||||
this.peerConnection.addEventListener("track", this.onTrack.bind(this));
|
this.peerConnection.addEventListener("track", this.onTrack.bind(this));
|
||||||
this.peerConnection.addEventListener(
|
this.peerConnection.addEventListener("signalingstatechange", this.onSignalingStateChange.bind(this));
|
||||||
"signalingstatechange",
|
|
||||||
this.onSignalingStateChange.bind(this)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private onGatheringStateChange() {
|
private onGatheringStateChange() {
|
||||||
if (!this.peerConnection) {
|
if (!this.peerConnection) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.logMessage(
|
this.logMessage(`ICE Gathering State changed: ${this.peerConnection.iceGatheringState}`);
|
||||||
`ICE Gathering State changed: ${this.peerConnection.iceGatheringState}`
|
|
||||||
);
|
|
||||||
switch (this.peerConnection.iceGatheringState) {
|
switch (this.peerConnection.iceGatheringState) {
|
||||||
case "complete":
|
case "complete":
|
||||||
this.gatherResolver();
|
this.gatherResolver();
|
||||||
@ -149,13 +127,8 @@ export class WISH extends TypedEventTarget {
|
|||||||
if (!this.peerConnection) {
|
if (!this.peerConnection) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.logMessage(
|
this.logMessage(`Peer Connection State changed: ${this.peerConnection.connectionState}`);
|
||||||
`Peer Connection State changed: ${this.peerConnection.connectionState}`
|
const transportHandler = (track: MediaStreamTrack, transport: RTCDtlsTransport) => {
|
||||||
);
|
|
||||||
const transportHandler = (
|
|
||||||
track: MediaStreamTrack,
|
|
||||||
transport: RTCDtlsTransport
|
|
||||||
) => {
|
|
||||||
const ice = transport.iceTransport;
|
const ice = transport.iceTransport;
|
||||||
if (!ice) {
|
if (!ice) {
|
||||||
return;
|
return;
|
||||||
@ -217,9 +190,7 @@ export class WISH extends TypedEventTarget {
|
|||||||
if (!candidate.candidate) {
|
if (!candidate.candidate) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.logMessage(
|
this.logMessage(`Got ICE candidate: ${candidate.candidate.replace("candidate:", "")}`);
|
||||||
`Got ICE candidate: ${candidate.candidate.replace("candidate:", "")}`
|
|
||||||
);
|
|
||||||
if (!this.parsedOffer) {
|
if (!this.parsedOffer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -240,13 +211,8 @@ export class WISH extends TypedEventTarget {
|
|||||||
if (this.trickleBatchingJob) {
|
if (this.trickleBatchingJob) {
|
||||||
clearInterval(this.trickleBatchingJob);
|
clearInterval(this.trickleBatchingJob);
|
||||||
}
|
}
|
||||||
this.logMessage(
|
this.logMessage(`Starting batching job to trickle candidates every ${TRICKLE_BATCH_INTERVAL}ms`);
|
||||||
`Starting batching job to trickle candidates every ${TRICKLE_BATCH_INTERVAL}ms`
|
this.trickleBatchingJob = setInterval(this.trickleBatch.bind(this), TRICKLE_BATCH_INTERVAL);
|
||||||
);
|
|
||||||
this.trickleBatchingJob = setInterval(
|
|
||||||
this.trickleBatch.bind(this),
|
|
||||||
TRICKLE_BATCH_INTERVAL
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private stopTrickleBatching() {
|
private stopTrickleBatching() {
|
||||||
@ -281,8 +247,7 @@ export class WISH extends TypedEventTarget {
|
|||||||
type: candidate.type || "host",
|
type: candidate.type || "host",
|
||||||
relAddr: candidate.relatedAddress || undefined,
|
relAddr: candidate.relatedAddress || undefined,
|
||||||
relPort:
|
relPort:
|
||||||
typeof candidate.relatedPort !== "undefined" &&
|
typeof candidate.relatedPort !== "undefined" && candidate.relatedPort !== null
|
||||||
candidate.relatedPort !== null
|
|
||||||
? candidate.relatedPort.toString()
|
? candidate.relatedPort.toString()
|
||||||
: undefined,
|
: undefined,
|
||||||
});
|
});
|
||||||
@ -307,18 +272,14 @@ export class WISH extends TypedEventTarget {
|
|||||||
if (!this.peerConnection) {
|
if (!this.peerConnection) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.logMessage(
|
this.logMessage(`Signaling State changed: ${this.peerConnection.signalingState}`);
|
||||||
`Signaling State changed: ${this.peerConnection.signalingState}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private onICEConnectionStateChange() {
|
private onICEConnectionStateChange() {
|
||||||
if (!this.peerConnection) {
|
if (!this.peerConnection) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.logMessage(
|
this.logMessage(`ICE Connection State changed: ${this.peerConnection.iceConnectionState}`);
|
||||||
`ICE Connection State changed: ${this.peerConnection.iceConnectionState}`
|
|
||||||
);
|
|
||||||
switch (this.peerConnection.iceConnectionState) {
|
switch (this.peerConnection.iceConnectionState) {
|
||||||
case "checking":
|
case "checking":
|
||||||
this.iceStartTime = performance.now();
|
this.iceStartTime = performance.now();
|
||||||
@ -327,19 +288,11 @@ export class WISH extends TypedEventTarget {
|
|||||||
const connected = performance.now();
|
const connected = performance.now();
|
||||||
if (this.connectStartTime) {
|
if (this.connectStartTime) {
|
||||||
const delta = connected - this.connectStartTime;
|
const delta = connected - this.connectStartTime;
|
||||||
this.logMessage(
|
this.logMessage(`Took ${(delta / 1000).toFixed(2)} seconds to establish PeerConnection (end-to-end)`);
|
||||||
`Took ${(delta / 1000).toFixed(
|
|
||||||
2
|
|
||||||
)} seconds to establish PeerConnection (end-to-end)`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (this.iceStartTime) {
|
if (this.iceStartTime) {
|
||||||
const delta = connected - this.iceStartTime;
|
const delta = connected - this.iceStartTime;
|
||||||
this.logMessage(
|
this.logMessage(`Took ${(delta / 1000).toFixed(2)} seconds to establish PeerConnection (ICE)`);
|
||||||
`Took ${(delta / 1000).toFixed(
|
|
||||||
2
|
|
||||||
)} seconds to establish PeerConnection (ICE)`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent<StatusEvent>("status", {
|
new CustomEvent<StatusEvent>("status", {
|
||||||
@ -421,19 +374,12 @@ export class WISH extends TypedEventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private setVideoCodecPreference(transceiver: RTCRtpTransceiver) {
|
private setVideoCodecPreference(transceiver: RTCRtpTransceiver) {
|
||||||
if (
|
if (typeof RTCRtpSender.getCapabilities === "undefined" || typeof transceiver.setCodecPreferences === "undefined") {
|
||||||
typeof RTCRtpSender.getCapabilities === "undefined" ||
|
|
||||||
typeof transceiver.setCodecPreferences === "undefined"
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const capability = RTCRtpSender.getCapabilities("video");
|
const capability = RTCRtpSender.getCapabilities("video");
|
||||||
const codecs = capability ? capability.codecs : [];
|
const codecs = capability ? capability.codecs : [];
|
||||||
this.logMessage(
|
this.logMessage(`Available codecs for outbound video: ${codecs.map(c => c.mimeType).join(", ")}`);
|
||||||
`Available codecs for outbound video: ${codecs
|
|
||||||
.map((c) => c.mimeType)
|
|
||||||
.join(", ")}`
|
|
||||||
);
|
|
||||||
for (let i = 0; i < codecs.length; i++) {
|
for (let i = 0; i < codecs.length; i++) {
|
||||||
const codec = codecs[i];
|
const codec = codecs[i];
|
||||||
if (codec.mimeType === "video/VP9") {
|
if (codec.mimeType === "video/VP9") {
|
||||||
@ -486,10 +432,7 @@ export class WISH extends TypedEventTarget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async doSignalingPOST(
|
private async doSignalingPOST(sdp: string, useLink?: boolean): Promise<string> {
|
||||||
sdp: string,
|
|
||||||
useLink?: boolean
|
|
||||||
): Promise<string> {
|
|
||||||
if (!this.endpoint) {
|
if (!this.endpoint) {
|
||||||
throw new Error("No WHIP/WHEP endpoint has been set");
|
throw new Error("No WHIP/WHEP endpoint has been set");
|
||||||
}
|
}
|
||||||
@ -528,14 +471,10 @@ export class WISH extends TypedEventTarget {
|
|||||||
if (resp.headers.get("accept-post") || resp.headers.get("accept-patch")) {
|
if (resp.headers.get("accept-post") || resp.headers.get("accept-patch")) {
|
||||||
switch (this.mode) {
|
switch (this.mode) {
|
||||||
case Mode.Publisher:
|
case Mode.Publisher:
|
||||||
this.logMessage(
|
this.logMessage(`WHIP version draft-ietf-wish-whip-05 (Accept-Post/Accept-Patch)`);
|
||||||
`WHIP version draft-ietf-wish-whip-05 (Accept-Post/Accept-Patch)`
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
case Mode.Player:
|
case Mode.Player:
|
||||||
this.logMessage(
|
this.logMessage(`WHEP version draft-murillo-whep-01 (Accept-Post/Accept-Patch)`);
|
||||||
`WHEP version draft-murillo-whep-01 (Accept-Post/Accept-Patch)`
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -560,9 +499,7 @@ export class WISH extends TypedEventTarget {
|
|||||||
|
|
||||||
const signaled = performance.now();
|
const signaled = performance.now();
|
||||||
const delta = signaled - signalStartTime;
|
const delta = signaled - signalStartTime;
|
||||||
this.logMessage(
|
this.logMessage(`Took ${(delta / 1000).toFixed(2)} seconds to exchange SDP`);
|
||||||
`Took ${(delta / 1000).toFixed(2)} seconds to exchange SDP`
|
|
||||||
);
|
|
||||||
|
|
||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
@ -8,16 +8,14 @@ const ESLintPlugin = require("eslint-webpack-plugin");
|
|||||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||||
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
|
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
|
||||||
const CopyPlugin = require("copy-webpack-plugin");
|
const CopyPlugin = require("copy-webpack-plugin");
|
||||||
|
const WorkboxPlugin = require("workbox-webpack-plugin");
|
||||||
|
const IntlTsTransformer = require("@formatjs/ts-transformer");
|
||||||
|
|
||||||
const isProduction = process.env.NODE_ENV == "production";
|
const isProduction = process.env.NODE_ENV == "production";
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
entry: {
|
entry: {
|
||||||
main: "./src/index.tsx",
|
main: "./src/index.tsx",
|
||||||
sw: {
|
|
||||||
import: "./src/service-worker.ts",
|
|
||||||
filename: "service-worker.js",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
target: "browserslist",
|
target: "browserslist",
|
||||||
mode: isProduction ? "production" : "development",
|
mode: isProduction ? "production" : "development",
|
||||||
@ -25,12 +23,7 @@ const config = {
|
|||||||
output: {
|
output: {
|
||||||
publicPath: "/",
|
publicPath: "/",
|
||||||
path: path.resolve(__dirname, "build"),
|
path: path.resolve(__dirname, "build"),
|
||||||
filename: ({ runtime }) => {
|
filename: isProduction ? "[name].[chunkhash].js" : "[name].js",
|
||||||
if (runtime === "sw") {
|
|
||||||
return "[name].js";
|
|
||||||
}
|
|
||||||
return isProduction ? "[name].[chunkhash].js" : "[name].js";
|
|
||||||
},
|
|
||||||
clean: isProduction,
|
clean: isProduction,
|
||||||
},
|
},
|
||||||
devServer: {
|
devServer: {
|
||||||
@ -51,12 +44,11 @@ const config = {
|
|||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
template: "public/index.html",
|
template: "public/index.html",
|
||||||
favicon: "public/favicon.ico",
|
favicon: "public/favicon.ico",
|
||||||
excludeChunks: ["sw"],
|
|
||||||
}),
|
}),
|
||||||
new ESLintPlugin({
|
new ESLintPlugin({
|
||||||
extensions: ["js", "mjs", "jsx", "ts", "tsx"],
|
extensions: ["js", "mjs", "jsx", "ts", "tsx"],
|
||||||
eslintPath: require.resolve("eslint"),
|
eslintPath: require.resolve("eslint"),
|
||||||
failOnError: !isProduction,
|
failOnError: true,
|
||||||
cache: true,
|
cache: true,
|
||||||
}),
|
}),
|
||||||
new MiniCssExtractPlugin({
|
new MiniCssExtractPlugin({
|
||||||
@ -70,6 +62,9 @@ const config = {
|
|||||||
__XXX: process.env["__XXX"] || JSON.stringify(false),
|
__XXX: process.env["__XXX"] || JSON.stringify(false),
|
||||||
__XXX_HOST: JSON.stringify("https://xxzap.com"),
|
__XXX_HOST: JSON.stringify("https://xxzap.com"),
|
||||||
}),
|
}),
|
||||||
|
new WorkboxPlugin.InjectManifest({
|
||||||
|
swSrc: "./src/service-worker.ts",
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
@ -97,28 +92,24 @@ const config = {
|
|||||||
options: {
|
options: {
|
||||||
babelrc: false,
|
babelrc: false,
|
||||||
configFile: false,
|
configFile: false,
|
||||||
presets: [
|
presets: [["@babel/preset-env"], ["@babel/preset-react", { runtime: "automatic" }]],
|
||||||
[
|
},
|
||||||
"@babel/preset-env",
|
},
|
||||||
{
|
{
|
||||||
targets: "defaults",
|
loader: require.resolve("ts-loader"),
|
||||||
},
|
options: {
|
||||||
],
|
getCustomTransformers() {
|
||||||
["@babel/preset-react", { runtime: "automatic" }],
|
return {
|
||||||
"@babel/preset-typescript",
|
before: [
|
||||||
],
|
IntlTsTransformer.transform({
|
||||||
plugins: [
|
overrideIdFn: "[sha512:contenthash:base64:6]",
|
||||||
[
|
ast: true,
|
||||||
"formatjs",
|
}),
|
||||||
{
|
],
|
||||||
idInterpolationPattern: "[sha512:contenthash:base64:6]",
|
};
|
||||||
ast: true,
|
},
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
require.resolve("ts-loader"),
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
Reference in New Issue
Block a user