refactor: upgrade

This commit is contained in:
Kieran 2023-12-04 11:01:56 +00:00
parent 01eaf9996c
commit e2714c4274
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
105 changed files with 1894 additions and 4698 deletions

View File

@ -1,7 +1,15 @@
module.exports = {
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint"],
plugins: ["@typescript-eslint", "formatjs"],
rules: {
"formatjs/enforce-id": [
"error",
{
idInterpolationPattern: "[sha512:contenthash:base64:6]",
},
],
},
root: true,
ignorePatterns: ["build/", "*.test.ts", "*.js"],
env: {
@ -10,10 +18,4 @@ module.exports = {
commonjs: true,
node: false,
},
rules: {
"@typescript-eslint/no-non-null-assertion": "error",
"require-await": "error",
eqeqeq: "error",
"object-shorthand": "warn",
},
};

4
.gitignore vendored
View File

@ -29,4 +29,6 @@ yarn-error.log*
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
!.yarn/versions
dev-dist/

View File

@ -1,4 +1,6 @@
.yarn/
.vscode/
node_modules/
build/
build/
dist/
dev-dist/

View File

@ -1,3 +1,7 @@
{
"recommendations": ["arcanis.vscode-zipfs", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
"recommendations": [
"arcanis.vscode-zipfs",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}

View File

@ -1,8 +1,8 @@
#!/usr/bin/env node
const { existsSync } = require(`fs`);
const { createRequire } = require(`module`);
const { resolve } = require(`path`);
const {existsSync} = require(`fs`);
const {createRequire} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.cjs";

View File

@ -1,8 +1,8 @@
#!/usr/bin/env node
const { existsSync } = require(`fs`);
const { createRequire } = require(`module`);
const { resolve } = require(`path`);
const {existsSync} = require(`fs`);
const {createRequire} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.cjs";

View File

@ -0,0 +1,20 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = createRequire(absPnpApiPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require eslint/use-at-your-own-risk
require(absPnpApiPath).setup();
}
}
// Defer to the real eslint/use-at-your-own-risk your application uses
module.exports = absRequire(`eslint/use-at-your-own-risk`);

View File

@ -2,5 +2,13 @@
"name": "eslint",
"version": "8.48.0-sdk",
"main": "./lib/api.js",
"type": "commonjs"
"type": "commonjs",
"bin": {
"eslint": "./bin/eslint.js"
},
"exports": {
"./package.json": "./package.json",
".": "./lib/api.js",
"./use-at-your-own-risk": "./lib/unsupported-api.js"
}
}

20
.yarn/sdks/prettier/bin-prettier.js vendored Executable file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = createRequire(absPnpApiPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require prettier/bin-prettier.js
require(absPnpApiPath).setup();
}
}
// Defer to the real prettier/bin-prettier.js your application uses
module.exports = absRequire(`prettier/bin-prettier.js`);

6
.yarn/sdks/prettier/index.js vendored Executable file → Normal file
View File

@ -1,8 +1,8 @@
#!/usr/bin/env node
const { existsSync } = require(`fs`);
const { createRequire } = require(`module`);
const { resolve } = require(`path`);
const {existsSync} = require(`fs`);
const {createRequire} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../.pnp.cjs";

View File

@ -2,5 +2,6 @@
"name": "prettier",
"version": "2.8.8-sdk",
"main": "./index.js",
"type": "commonjs"
"type": "commonjs",
"bin": "./bin-prettier.js"
}

View File

@ -1,8 +1,8 @@
#!/usr/bin/env node
const { existsSync } = require(`fs`);
const { createRequire } = require(`module`);
const { resolve } = require(`path`);
const {existsSync} = require(`fs`);
const {createRequire} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.cjs";

View File

@ -1,8 +1,8 @@
#!/usr/bin/env node
const { existsSync } = require(`fs`);
const { createRequire } = require(`module`);
const { resolve } = require(`path`);
const {existsSync} = require(`fs`);
const {createRequire} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.cjs";
@ -14,18 +14,16 @@ const moduleWrapper = tsserver => {
return tsserver;
}
const { isAbsolute } = require(`path`);
const {isAbsolute} = require(`path`);
const pnpApi = require(`pnpapi`);
const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//);
const isPortal = str => str.startsWith("portal:/");
const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
const dependencyTreeRoots = new Set(
pnpApi.getDependencyTreeRoots().map(locator => {
return `${locator.name}@${locator.reference}`;
})
);
const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => {
return `${locator.name}@${locator.reference}`;
}));
// VSCode sends the zip paths to TS using the "zip://" prefix, that TS
// doesn't understand. This layer makes sure to remove the protocol
@ -47,10 +45,7 @@ const moduleWrapper = tsserver => {
const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str;
if (resolved) {
const locator = pnpApi.findPackageLocator(resolved);
if (
locator &&
(dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))
) {
if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) {
str = resolved;
}
}
@ -78,55 +73,41 @@ const moduleWrapper = tsserver => {
// Before | ^/zip/c:/foo/bar.zip/package.json
// After | ^/zip//c:/foo/bar.zip/package.json
//
case `vscode <1.61`:
{
str = `^zip:${str}`;
}
break;
case `vscode <1.61`: {
str = `^zip:${str}`;
} break;
case `vscode <1.66`:
{
str = `^/zip/${str}`;
}
break;
case `vscode <1.66`: {
str = `^/zip/${str}`;
} break;
case `vscode <1.68`:
{
str = `^/zip${str}`;
}
break;
case `vscode <1.68`: {
str = `^/zip${str}`;
} break;
case `vscode`:
{
str = `^/zip/${str}`;
}
break;
case `vscode`: {
str = `^/zip/${str}`;
} break;
// To make "go to definition" work,
// We have to resolve the actual file system path from virtual path
// and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip)
case `coc-nvim`:
{
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
str = resolve(`zipfile:${str}`);
}
break;
case `coc-nvim`: {
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
str = resolve(`zipfile:${str}`);
} break;
// Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server)
// We have to resolve the actual file system path from virtual path,
// everything else is up to neovim
case `neovim`:
{
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
str = `zipfile://${str}`;
}
break;
case `neovim`: {
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
str = `zipfile://${str}`;
} break;
default:
{
str = `zip:${str}`;
}
break;
default: {
str = `zip:${str}`;
} break;
}
} else {
str = str.replace(/^\/?/, process.platform === `win32` ? `` : `/`);
@ -138,30 +119,26 @@ const moduleWrapper = tsserver => {
function fromEditorPath(str) {
switch (hostInfo) {
case `coc-nvim`:
{
str = str.replace(/\.zip::/, `.zip/`);
// 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
// before `zipfile:`
return process.platform === `win32` ? str.replace(/^.*zipfile:\//, ``) : str.replace(/^.*zipfile:/, ``);
}
break;
case `coc-nvim`: {
str = str.replace(/\.zip::/, `.zip/`);
// 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
// before `zipfile:`
return process.platform === `win32`
? str.replace(/^.*zipfile:\//, ``)
: str.replace(/^.*zipfile:/, ``);
} break;
case `neovim`:
{
str = str.replace(/\.zip::/, `.zip/`);
// The path for neovim is in format of zipfile:///<pwd>/.yarn/...
return str.replace(/^zipfile:\/\//, ``);
}
break;
case `neovim`: {
str = str.replace(/\.zip::/, `.zip/`);
// The path for neovim is in format of zipfile:///<pwd>/.yarn/...
return str.replace(/^zipfile:\/\//, ``);
} break;
case `vscode`:
default:
{
return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`);
}
break;
default: {
return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`)
} break;
}
}
@ -173,8 +150,8 @@ const moduleWrapper = tsserver => {
// TypeScript already does local loads and if this code is running the user trusts the workspace
// https://github.com/microsoft/vscode/issues/45856
const ConfiguredProject = tsserver.server.ConfiguredProject;
const { enablePluginsWithOptions: originalEnablePluginsWithOptions } = ConfiguredProject.prototype;
ConfiguredProject.prototype.enablePluginsWithOptions = function () {
const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype;
ConfiguredProject.prototype.enablePluginsWithOptions = function() {
this.projectService.allowLocalPluginLoads = true;
return originalEnablePluginsWithOptions.apply(this, arguments);
};
@ -184,12 +161,12 @@ const moduleWrapper = tsserver => {
// like an absolute path of ours and normalize it.
const Session = tsserver.server.Session;
const { onMessage: originalOnMessage, send: originalSend } = Session.prototype;
const {onMessage: originalOnMessage, send: originalSend} = Session.prototype;
let hostInfo = `unknown`;
Object.assign(Session.prototype, {
onMessage(/** @type {string | object} */ message) {
const isStringMessage = typeof message === "string";
const isStringMessage = typeof message === 'string';
const parsedMessage = isStringMessage ? JSON.parse(message) : message;
if (
@ -200,12 +177,10 @@ const moduleWrapper = tsserver => {
) {
hostInfo = parsedMessage.arguments.hostInfo;
if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) {
const [, major, minor] = (
process.env.VSCODE_IPC_HOOK.match(
// The RegExp from https://semver.org/ but without the caret at the start
/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
) ?? []
).map(Number);
const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match(
// The RegExp from https://semver.org/ but without the caret at the start
/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
) ?? []).map(Number)
if (major === 1) {
if (minor < 61) {
@ -220,22 +195,20 @@ const moduleWrapper = tsserver => {
}
const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => {
return typeof value === "string" ? fromEditorPath(value) : value;
return typeof value === 'string' ? fromEditorPath(value) : value;
});
return originalOnMessage.call(this, isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON));
return originalOnMessage.call(
this,
isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON)
);
},
send(/** @type {any} */ msg) {
return originalSend.call(
this,
JSON.parse(
JSON.stringify(msg, (key, value) => {
return typeof value === `string` ? toEditorPath(value) : value;
})
)
);
},
return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => {
return typeof value === `string` ? toEditorPath(value) : value;
})));
}
});
return tsserver;

View File

@ -1,8 +1,8 @@
#!/usr/bin/env node
const { existsSync } = require(`fs`);
const { createRequire } = require(`module`);
const { resolve } = require(`path`);
const {existsSync} = require(`fs`);
const {createRequire} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.cjs";
@ -14,18 +14,16 @@ const moduleWrapper = tsserver => {
return tsserver;
}
const { isAbsolute } = require(`path`);
const {isAbsolute} = require(`path`);
const pnpApi = require(`pnpapi`);
const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//);
const isPortal = str => str.startsWith("portal:/");
const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
const dependencyTreeRoots = new Set(
pnpApi.getDependencyTreeRoots().map(locator => {
return `${locator.name}@${locator.reference}`;
})
);
const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => {
return `${locator.name}@${locator.reference}`;
}));
// VSCode sends the zip paths to TS using the "zip://" prefix, that TS
// doesn't understand. This layer makes sure to remove the protocol
@ -47,10 +45,7 @@ const moduleWrapper = tsserver => {
const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str;
if (resolved) {
const locator = pnpApi.findPackageLocator(resolved);
if (
locator &&
(dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))
) {
if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) {
str = resolved;
}
}
@ -78,55 +73,41 @@ const moduleWrapper = tsserver => {
// Before | ^/zip/c:/foo/bar.zip/package.json
// After | ^/zip//c:/foo/bar.zip/package.json
//
case `vscode <1.61`:
{
str = `^zip:${str}`;
}
break;
case `vscode <1.61`: {
str = `^zip:${str}`;
} break;
case `vscode <1.66`:
{
str = `^/zip/${str}`;
}
break;
case `vscode <1.66`: {
str = `^/zip/${str}`;
} break;
case `vscode <1.68`:
{
str = `^/zip${str}`;
}
break;
case `vscode <1.68`: {
str = `^/zip${str}`;
} break;
case `vscode`:
{
str = `^/zip/${str}`;
}
break;
case `vscode`: {
str = `^/zip/${str}`;
} break;
// To make "go to definition" work,
// We have to resolve the actual file system path from virtual path
// and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip)
case `coc-nvim`:
{
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
str = resolve(`zipfile:${str}`);
}
break;
case `coc-nvim`: {
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
str = resolve(`zipfile:${str}`);
} break;
// Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server)
// We have to resolve the actual file system path from virtual path,
// everything else is up to neovim
case `neovim`:
{
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
str = `zipfile://${str}`;
}
break;
case `neovim`: {
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
str = `zipfile://${str}`;
} break;
default:
{
str = `zip:${str}`;
}
break;
default: {
str = `zip:${str}`;
} break;
}
} else {
str = str.replace(/^\/?/, process.platform === `win32` ? `` : `/`);
@ -138,30 +119,26 @@ const moduleWrapper = tsserver => {
function fromEditorPath(str) {
switch (hostInfo) {
case `coc-nvim`:
{
str = str.replace(/\.zip::/, `.zip/`);
// 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
// before `zipfile:`
return process.platform === `win32` ? str.replace(/^.*zipfile:\//, ``) : str.replace(/^.*zipfile:/, ``);
}
break;
case `coc-nvim`: {
str = str.replace(/\.zip::/, `.zip/`);
// 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
// before `zipfile:`
return process.platform === `win32`
? str.replace(/^.*zipfile:\//, ``)
: str.replace(/^.*zipfile:/, ``);
} break;
case `neovim`:
{
str = str.replace(/\.zip::/, `.zip/`);
// The path for neovim is in format of zipfile:///<pwd>/.yarn/...
return str.replace(/^zipfile:\/\//, ``);
}
break;
case `neovim`: {
str = str.replace(/\.zip::/, `.zip/`);
// The path for neovim is in format of zipfile:///<pwd>/.yarn/...
return str.replace(/^zipfile:\/\//, ``);
} break;
case `vscode`:
default:
{
return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`);
}
break;
default: {
return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`)
} break;
}
}
@ -173,8 +150,8 @@ const moduleWrapper = tsserver => {
// TypeScript already does local loads and if this code is running the user trusts the workspace
// https://github.com/microsoft/vscode/issues/45856
const ConfiguredProject = tsserver.server.ConfiguredProject;
const { enablePluginsWithOptions: originalEnablePluginsWithOptions } = ConfiguredProject.prototype;
ConfiguredProject.prototype.enablePluginsWithOptions = function () {
const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype;
ConfiguredProject.prototype.enablePluginsWithOptions = function() {
this.projectService.allowLocalPluginLoads = true;
return originalEnablePluginsWithOptions.apply(this, arguments);
};
@ -184,12 +161,12 @@ const moduleWrapper = tsserver => {
// like an absolute path of ours and normalize it.
const Session = tsserver.server.Session;
const { onMessage: originalOnMessage, send: originalSend } = Session.prototype;
const {onMessage: originalOnMessage, send: originalSend} = Session.prototype;
let hostInfo = `unknown`;
Object.assign(Session.prototype, {
onMessage(/** @type {string | object} */ message) {
const isStringMessage = typeof message === "string";
const isStringMessage = typeof message === 'string';
const parsedMessage = isStringMessage ? JSON.parse(message) : message;
if (
@ -200,12 +177,10 @@ const moduleWrapper = tsserver => {
) {
hostInfo = parsedMessage.arguments.hostInfo;
if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) {
const [, major, minor] = (
process.env.VSCODE_IPC_HOOK.match(
// The RegExp from https://semver.org/ but without the caret at the start
/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
) ?? []
).map(Number);
const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match(
// The RegExp from https://semver.org/ but without the caret at the start
/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
) ?? []).map(Number)
if (major === 1) {
if (minor < 61) {
@ -220,22 +195,20 @@ const moduleWrapper = tsserver => {
}
const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => {
return typeof value === "string" ? fromEditorPath(value) : value;
return typeof value === 'string' ? fromEditorPath(value) : value;
});
return originalOnMessage.call(this, isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON));
return originalOnMessage.call(
this,
isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON)
);
},
send(/** @type {any} */ msg) {
return originalSend.call(
this,
JSON.parse(
JSON.stringify(msg, (key, value) => {
return typeof value === `string` ? toEditorPath(value) : value;
})
)
);
},
return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => {
return typeof value === `string` ? toEditorPath(value) : value;
})));
}
});
return tsserver;

View File

@ -1,8 +1,8 @@
#!/usr/bin/env node
const { existsSync } = require(`fs`);
const { createRequire } = require(`module`);
const { resolve } = require(`path`);
const {existsSync} = require(`fs`);
const {createRequire} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.cjs";
@ -11,10 +11,10 @@ const absRequire = createRequire(absPnpApiPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require typescript/lib/typescript.js
// Setup the environment to be able to require typescript
require(absPnpApiPath).setup();
}
}
// Defer to the real typescript/lib/typescript.js your application uses
module.exports = absRequire(`typescript/lib/typescript.js`);
// Defer to the real typescript your application uses
module.exports = absRequire(`typescript`);

View File

@ -2,5 +2,9 @@
"name": "typescript",
"version": "5.2.2-sdk",
"main": "./lib/typescript.js",
"type": "commonjs"
"type": "commonjs",
"bin": {
"tsc": "./bin/tsc",
"tsserver": "./bin/tsserver"
}
}

View File

@ -13,5 +13,6 @@
<body>
<div id="root"></div>
<script src="src/index.tsx" type="module"></script>
</body>
</html>

View File

@ -14,14 +14,12 @@
"@radix-ui/react-toggle": "^1.0.3",
"@react-hook/resize-observer": "^1.2.6",
"@scure/base": "^1.1.1",
"@snort/shared": "^1.0.9",
"@snort/system": "^1.1.4",
"@snort/system-react": "^1.1.4",
"@snort/shared": "^1.0.10",
"@snort/system": "^1.1.5",
"@snort/system-react": "^1.1.5",
"@snort/system-wasm": "^1.0.1",
"@snort/system-web": "^1.0.2",
"@szhsin/react-menu": "^4.0.2",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^13.0.0",
"@testing-library/user-event": "^13.2.1",
"@types/webscopeio__react-textarea-autocomplete": "^4.7.2",
"@void-cat/api": "^1.0.7",
"@webscopeio/react-textarea-autocomplete": "^4.9.2",
@ -32,7 +30,6 @@
"lodash": "^4.17.21",
"lodash.uniqby": "^4.7.0",
"marked": "^9.1.2",
"moment": "^2.29.4",
"qr-code-styling": "^1.6.0-rc.1",
"react": "^18.2.0",
"react-confetti": "^6.1.0",
@ -55,8 +52,8 @@
"workbox-strategies": "^7.0.0"
},
"scripts": {
"start": "webpack serve --node-env=development --mode=development",
"build": "webpack --node-env=production --mode=production",
"start": "vite",
"build": "vite 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",
"intl-extract": "formatjs extract 'src/**/*.ts*' --ignore='**/*.d.ts' --out-file src/lang.json --flatten true",
@ -83,40 +80,27 @@
]
},
"devDependencies": {
"@babel/core": "^7.22.11",
"@babel/plugin-syntax-import-assertions": "^7.20.0",
"@babel/preset-env": "^7.21.5",
"@babel/preset-react": "^7.18.6",
"@formatjs/cli": "^6.1.3",
"@formatjs/ts-transformer": "^3.13.3",
"@testing-library/dom": "^9.3.1",
"@types/lodash": "^4.14.195",
"@types/lodash.uniqby": "^4.7.7",
"@types/node": "^20.10.3",
"@types/react": "^18.2.21",
"@types/react-dom": "^18.2.7",
"@types/react-helmet": "^6.1.6",
"@typescript-eslint/eslint-plugin": "^6.4.1",
"@typescript-eslint/parser": "^6.4.1",
"@vitejs/plugin-react": "^4.2.0",
"@webbtc/webln-types": "^1.0.12",
"babel-loader": "^9.1.3",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.8.1",
"css-minimizer-webpack-plugin": "^5.0.0",
"eslint": "^8.48.0",
"eslint-webpack-plugin": "^4.0.1",
"html-webpack-plugin": "^5.5.1",
"mini-css-extract-plugin": "^2.7.5",
"eslint-plugin-formatjs": "^4.11.3",
"prettier": "^2.8.8",
"prop-types": "^15.8.1",
"source-map-loader": "^4.0.1",
"terser-webpack-plugin": "^5.3.9",
"ts-loader": "^9.4.4",
"typescript": "^5.2.2",
"webpack": "^5.88.2",
"webpack-bundle-analyzer": "^4.8.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1",
"workbox-webpack-plugin": "^7.0.0"
"vite": "^5.0.5",
"vite-plugin-pwa": "^0.17.2",
"vite-plugin-version-mark": "^0.0.10"
},
"packageManager": "yarn@3.6.3",
"prettier": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -1 +0,0 @@
[{ "id": "nsfw", "text": "NSFW" }]

View File

@ -1,14 +1,14 @@
import "./event.css";
import { type NostrLink, type NostrEvent as NostrEventType, EventKind } from "@snort/system";
import { EventKind, type NostrEvent as NostrEventType, type NostrLink } from "@snort/system";
import { Icon } from "element/icon";
import { Goal } from "element/goal";
import { Note } from "element/note";
import { EmojiPack } from "element/emoji-pack";
import { Badge } from "element/badge";
import { useEvent } from "hooks/event";
import { GOAL, EMOJI_PACK } from "const";
import { Icon } from "./icon";
import { Goal } from "./goal";
import { Note } from "./note";
import { EmojiPack } from "./emoji-pack";
import { Badge } from "./badge";
import { useEvent } from "@/hooks/event";
import { EMOJI_PACK, GOAL } from "@/const";
interface EventProps {
link: NostrLink;

View File

@ -1,6 +1,6 @@
import "./async-button.css";
import { useState } from "react";
import Spinner from "element/spinner";
import Spinner from "./spinner";
interface AsyncButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
disabled?: boolean;

View File

@ -1,6 +1,6 @@
import "./badge.css";
import type { NostrEvent } from "@snort/system";
import { findTag } from "utils";
import { findTag } from "@/utils";
export function Badge({ ev }: { ev: NostrEvent }) {
const name = findTag(ev, "name") || findTag(ev, "d");

View File

@ -1,20 +1,21 @@
import { useUserProfile, SnortContext, useEventReactions } from "@snort/system-react";
import { SnortContext, useEventReactions, useUserProfile } from "@snort/system-react";
import { EventKind, NostrLink, TaggedNostrEvent } from "@snort/system";
import React, { useRef, useState, useMemo, useContext } from "react";
import { useMediaQuery, useHover, useOnClickOutside, useIntersectionObserver } from "usehooks-ts";
import React, { useContext, useMemo, useRef, useState } from "react";
import { useHover, useIntersectionObserver, useMediaQuery, useOnClickOutside } from "usehooks-ts";
import { dedupe } from "@snort/shared";
import { EmojiPicker } from "element/emoji-picker";
import { Icon } from "element/icon";
import { Emoji as EmojiComponent } from "element/emoji";
import { EmojiPicker } from "./emoji-picker";
import { Icon } from "./icon";
import { Emoji as EmojiComponent } from "./emoji";
import { Profile } from "./profile";
import { Text } from "element/text";
import { useMute } from "element/mute-button";
import { SendZapsDialog } from "element/send-zap";
import { CollapsibleEvent } from "element/collapsible";
import { useLogin } from "hooks/login";
import { formatSats } from "number";
import type { Badge, Emoji, EmojiPack } from "types";
import { Text } from "./text";
import { useMute } from "./mute-button";
import { SendZapsDialog } from "./send-zap";
import { CollapsibleEvent } from "./collapsible";
import { useLogin } from "@/hooks/login";
import { formatSats } from "@/number";
import type { Badge, Emoji, EmojiPack } from "@/types";
function emojifyReaction(reaction: string) {
if (reaction === "+") {

View File

@ -8,10 +8,10 @@ import * as Collapsible from "@radix-ui/react-collapsible";
import type { NostrLink } from "@snort/system";
import { Mention } from "element/mention";
import { NostrEvent, EventIcon } from "element/Event";
import { ExternalLink } from "element/external-link";
import { useEvent } from "hooks/event";
import { Mention } from "./mention";
import { EventIcon, NostrEvent } from "./Event";
import { ExternalLink } from "./external-link";
import { useEvent } from "@/hooks/event";
interface MediaURLProps {
url: URL;
@ -32,7 +32,7 @@ export function MediaURL({ url, children }: MediaURLProps) {
</div>
<Dialog.Close asChild>
<button className="btn delete-button" aria-label="Close">
<FormattedMessage defaultMessage="Close" />
<FormattedMessage defaultMessage="Close" id="rbrahO" />
</button>
</Dialog.Close>
</Dialog.Content>
@ -55,7 +55,11 @@ export function CollapsibleEvent({ link }: { link: NostrLink }) {
</div>
<Collapsible.Trigger asChild>
<button className={`${open ? "btn btn-small delete-button" : "btn btn-small"}`}>
{open ? <FormattedMessage defaultMessage="Hide" /> : <FormattedMessage defaultMessage="Show" />}
{open ? (
<FormattedMessage defaultMessage="Hide" id="VA/Z1S" />
) : (
<FormattedMessage defaultMessage="Show" id="K7AkdL" />
)}
</button>
</Collapsible.Trigger>
</div>

View File

@ -19,17 +19,17 @@ export function ContentWarningOverlay() {
return (
<div className="fullscreen-exclusive age-check">
<h1>
<FormattedMessage defaultMessage="Sexually explicit material ahead!" />
<FormattedMessage defaultMessage="Sexually explicit material ahead!" id="rWBFZA" />
</h1>
<h2>
<FormattedMessage defaultMessage="Confirm your age" />
<FormattedMessage defaultMessage="Confirm your age" id="s7V+5p" />
</h2>
<div className="flex g24">
<button className="btn btn-warning" onClick={grownUp}>
<FormattedMessage defaultMessage="Yes, I am over 18" />
<FormattedMessage defaultMessage="Yes, I am over 18" id="O2Cy6m" />
</button>
<button className="btn" onClick={() => navigate("/")}>
<FormattedMessage defaultMessage="No, I am under 18" />
<FormattedMessage defaultMessage="No, I am under 18" id="KkIL3s" />
</button>
</div>
</div>

View File

@ -1,5 +1,5 @@
import "./copy.css";
import { useCopy } from "hooks/copy";
import { useCopy } from "@/hooks/copy";
import { Icon } from "./icon";
export interface CopyProps {

View File

@ -1,17 +1,17 @@
import "./emoji-pack.css";
import { type NostrEvent } from "@snort/system";
import { useLogin } from "hooks/login";
import { toEmojiPack } from "hooks/emoji";
import AsyncButton from "element/async-button";
import { findTag } from "utils";
import { USER_EMOJIS } from "const";
import { Login } from "index";
import type { EmojiPack as EmojiPackType } from "types";
import { FormattedMessage } from "react-intl";
import { useContext } from "react";
import { SnortContext } from "@snort/system-react";
import { useLogin } from "@/hooks/login";
import { toEmojiPack } from "@/hooks/emoji";
import AsyncButton from "./async-button";
import { findTag } from "@/utils";
import { USER_EMOJIS } from "@/const";
import { Login } from "@/index";
import type { EmojiPack as EmojiPackType } from "@/types";
export function EmojiPack({ ev }: { ev: NostrEvent }) {
const system = useContext(SnortContext);
const login = useLogin();
@ -49,7 +49,11 @@ export function EmojiPack({ ev }: { ev: NostrEvent }) {
<AsyncButton
className={`btn btn-small btn-primary ${isUsed ? "delete-button" : ""}`}
onClick={toggleEmojiPack}>
{isUsed ? <FormattedMessage defaultMessage="Remove" /> : <FormattedMessage defaultMessage="Add" />}
{isUsed ? (
<FormattedMessage defaultMessage="Remove" id="G/yZLu" />
) : (
<FormattedMessage defaultMessage="Add" id="2/2yg+" />
)}
</AsyncButton>
)}
</div>

View File

@ -1,7 +1,7 @@
import data, { Emoji } from "@emoji-mart/data";
import Picker from "@emoji-mart/react";
import { RefObject } from "react";
import { EmojiPack } from "types";
import { EmojiPack } from "@/types";
interface EmojiPickerProps {
topOffset: number;

View File

@ -1,6 +1,6 @@
import "./emoji.css";
import { useMemo } from "react";
import { EmojiTag } from "types";
import { EmojiTag } from "@/types";
export type EmojiProps = {
name: string;

View File

@ -1,5 +1,5 @@
import type { ReactNode } from "react";
import { Icon } from "element/icon";
import { Icon } from "./icon";
interface ExternalLinkProps {
href: string;

View File

@ -79,15 +79,15 @@ export function FileUploader({ defaultImage, onClear, onFileUpload }: FileUpload
<label className="file-uploader">
<input type="file" onChange={onFileChange} />
{isUploading ? (
<FormattedMessage defaultMessage="Uploading..." />
<FormattedMessage defaultMessage="Uploading..." id="JEsxDw" />
) : (
<FormattedMessage defaultMessage="Add File" />
<FormattedMessage defaultMessage="Add File" id="fc2iho" />
)}
</label>
<div className="file-uploader-preview">
{img?.length > 0 && (
<button className="btn btn-primary clear-button" onClick={clearImage}>
<FormattedMessage defaultMessage="Clear" />
<FormattedMessage defaultMessage="Clear" id="/GCoTA" />
</button>
)}
{img && <img className="image-preview" src={img} />}

View File

@ -1,12 +1,12 @@
import { EventKind } from "@snort/system";
import { useLogin } from "hooks/login";
import AsyncButton from "element/async-button";
import { Login } from "index";
import { FormattedMessage } from "react-intl";
import { useContext } from "react";
import { SnortContext } from "@snort/system-react";
import { useLogin } from "@/hooks/login";
import AsyncButton from "./async-button";
import { Login } from "@/index";
export function LoggedInFollowButton({ tag, value }: { tag: "p" | "t"; value: string }) {
const system = useContext(SnortContext);
const login = useLogin();
@ -56,7 +56,11 @@ export function LoggedInFollowButton({ tag, value }: { tag: "p" | "t"; value: st
type="button"
className="btn btn-primary"
onClick={isFollowing ? unfollow : follow}>
{isFollowing ? <FormattedMessage defaultMessage="Unfollow" /> : <FormattedMessage defaultMessage="Follow" />}
{isFollowing ? (
<FormattedMessage defaultMessage="Unfollow" id="izWS4J" />
) : (
<FormattedMessage defaultMessage="Follow" id="ieGrWo" />
)}
</AsyncButton>
);
}

View File

@ -2,18 +2,18 @@ import "./goal.css";
import { useMemo } from "react";
import * as Progress from "@radix-ui/react-progress";
import Confetti from "react-confetti";
import { FormattedMessage } from "react-intl";
import { NostrLink, type NostrEvent } from "@snort/system";
import { type NostrEvent, NostrLink } from "@snort/system";
import { useUserProfile } from "@snort/system-react";
import { findTag } from "utils";
import { formatSats } from "number";
import usePreviousValue from "hooks/usePreviousValue";
import { SendZapsDialog } from "element/send-zap";
import { getName } from "element/profile";
import { findTag } from "@/utils";
import { formatSats } from "@/number";
import usePreviousValue from "@/hooks/usePreviousValue";
import { SendZapsDialog } from "./send-zap";
import { getName } from "./profile";
import { Icon } from "./icon";
import { FormattedMessage } from "react-intl";
import { useZaps } from "hooks/zaps";
import { useZaps } from "@/hooks/zaps";
export function Goal({ ev }: { ev: NostrEvent }) {
const profile = useUserProfile(ev.pubkey);
@ -48,7 +48,7 @@ export function Goal({ ev }: { ev: NostrEvent }) {
{!isFinished && <span className="amount so-far">{formatSats(soFar)}</span>}
</Progress.Indicator>
<span className="amount target">
<FormattedMessage defaultMessage="Goal: {amount}" values={{ amount: formatSats(goalAmount) }} />
<FormattedMessage defaultMessage="Goal: {amount}" id="QceMQZ" values={{ amount: formatSats(goalAmount) }} />
</span>
</Progress.Root>
<div className="zap-circle">

View File

@ -1,6 +1,6 @@
import type { ReactNode } from "react";
import { NostrLink } from "element/nostr-link";
import { MediaURL } from "element/collapsible";
import { NostrLink } from "./nostr-link";
import { MediaURL } from "./collapsible";
const FileExtensionRegex = /\.([\w]+)$/i;

View File

@ -1,29 +1,29 @@
import "./live-chat.css";
import { FormattedMessage } from "react-intl";
import { EventKind, NostrLink, ParsedZap, NostrEvent } from "@snort/system";
import { EventKind, NostrEvent, NostrLink, ParsedZap } from "@snort/system";
import { useEventReactions } from "@snort/system-react";
import { unixNow } from "@snort/shared";
import { useMemo } from "react";
import uniqBy from "lodash.uniqby";
import { Icon } from "element/icon";
import Spinner from "element/spinner";
import { Text } from "element/text";
import { Profile } from "element/profile";
import { ChatMessage } from "element/chat-message";
import { Goal } from "element/goal";
import { Badge } from "element/badge";
import { WriteMessage } from "element/write-message";
import useEmoji, { packId } from "hooks/emoji";
import { useLiveChatFeed } from "hooks/live-chat";
import { useMutedPubkeys } from "hooks/lists";
import { useBadges } from "hooks/badges";
import { useLogin } from "hooks/login";
import { useAddress } from "hooks/event";
import { formatSats } from "number";
import { WEEK, LIVE_STREAM_CHAT } from "const";
import { findTag, getTagValues, getHost } from "utils";
import { TopZappers } from "element/top-zappers";
import { Icon } from "./icon";
import Spinner from "./spinner";
import { Text } from "./text";
import { Profile } from "./profile";
import { ChatMessage } from "./chat-message";
import { Goal } from "./goal";
import { Badge } from "./badge";
import { WriteMessage } from "./write-message";
import useEmoji, { packId } from "@/hooks/emoji";
import { useLiveChatFeed } from "@/hooks/live-chat";
import { useMutedPubkeys } from "@/hooks/lists";
import { useBadges } from "@/hooks/badges";
import { useLogin } from "@/hooks/login";
import { useAddress } from "@/hooks/event";
import { formatSats } from "@/number";
import { LIVE_STREAM_CHAT, WEEK } from "@/const";
import { findTag, getHost, getTagValues } from "@/utils";
import { TopZappers } from "./top-zappers";
export interface LiveChatOptions {
canWrite?: boolean;
@ -95,7 +95,7 @@ export function LiveChat({
{(options?.showHeader ?? true) && (
<div className="header">
<h2 className="title">
<FormattedMessage defaultMessage="Stream Chat" />
<FormattedMessage defaultMessage="Stream Chat" id="BGxpTN" />
</h2>
<Icon
name="link"
@ -108,7 +108,7 @@ export function LiveChat({
{reactions.zaps.length > 0 && (
<div className="top-zappers">
<h3>
<FormattedMessage defaultMessage="Top zappers" />
<FormattedMessage defaultMessage="Top zappers" id="wzWWzV" />
</h3>
<div className="top-zappers-container">
<TopZappers zaps={reactions.zaps} />
@ -151,7 +151,7 @@ export function LiveChat({
<WriteMessage emojiPacks={allEmojiPacks} link={link} />
) : (
<p>
<FormattedMessage defaultMessage="Please login to write messages!" />
<FormattedMessage defaultMessage="Please login to write messages!" id="RXQdxR" />
</p>
)}
</div>
@ -174,6 +174,7 @@ function ChatZap({ zap }: { zap: ParsedZap }) {
<Icon name="zap-filled" className="zap-icon" />
<FormattedMessage
defaultMessage="{person} zapped {amount} sats"
id="AIHaPH"
values={{
person: (
<Profile

View File

@ -1,6 +1,6 @@
import Hls from "hls.js";
import { useEffect, useMemo, useRef, useState } from "react";
import { WISH } from "wish";
import { WISH } from "@/wish";
export enum VideoStatus {
Online = "online",

View File

@ -20,13 +20,13 @@ import { VoidApi } from "@void-cat/api";
import { SnortContext } from "@snort/system-react";
import AsyncButton from "./async-button";
import { Login } from "index";
import { Login } from "@/index";
import { Icon } from "./icon";
import Copy from "./copy";
import { openFile } from "utils";
import { LoginType } from "login";
import { DefaultProvider, StreamProviderInfo } from "providers";
import { Nip103StreamProvider } from "providers/zsz";
import { openFile } from "@/utils";
import { LoginType } from "@/login";
import { DefaultProvider, StreamProviderInfo } from "@/providers";
import { Nip103StreamProvider } from "@/providers/zsz";
enum Stage {
Login = 0,
@ -126,6 +126,7 @@ export function LoginSignup({ close }: { close: () => void }) {
throw new Error(
formatMessage({
defaultMessage: "Hmm, your lightning address looks wrong",
id: "4l69eO",
})
);
}
@ -157,29 +158,29 @@ export function LoginSignup({ close }: { close: () => void }) {
<img src={LoginHeader as string} srcSet={`${LoginHeader2x} 2x`} className="header-image" />
<div className="content-inner">
<h2>
<FormattedMessage defaultMessage="Create an Account" />
<FormattedMessage defaultMessage="Create an Account" id="u6uD94" />
</h2>
<h3>
<FormattedMessage defaultMessage="No emails, just awesomeness!" />
<FormattedMessage defaultMessage="No emails, just awesomeness!" id="+AcVD+" />
</h3>
<button type="button" className="btn btn-primary btn-block" onClick={createAccount}>
<FormattedMessage defaultMessage="Create Account" />
<FormattedMessage defaultMessage="Create Account" id="5JcXdV" />
</button>
<div className="or-divider">
<hr />
<FormattedMessage defaultMessage="OR" />
<FormattedMessage defaultMessage="OR" id="INlWvJ" />
<hr />
</div>
{hasNostrExtension && (
<>
<AsyncButton type="button" className="btn btn-primary btn-block" onClick={loginNip7}>
<FormattedMessage defaultMessage="Nostr Extension" />
<FormattedMessage defaultMessage="Nostr Extension" id="ebmhes" />
</AsyncButton>
</>
)}
<button type="button" className="btn btn-primary btn-block" onClick={() => setStage(Stage.LoginInput)}>
<FormattedMessage defaultMessage="Login with Private Key (insecure)" />
<FormattedMessage defaultMessage="Login with Private Key (insecure)" id="feZ/kG" />
</button>
{error && <b className="error">{error}</b>}
</div>
@ -192,15 +193,16 @@ export function LoginSignup({ close }: { close: () => void }) {
<img src={LoginVault as string} srcSet={`${LoginVault2x} 2x`} className="header-image" />
<div className="content-inner">
<h2>
<FormattedMessage defaultMessage="Login with private key" />
<FormattedMessage defaultMessage="Login with private key" id="3df560" />
</h2>
<p>
<FormattedMessage
defaultMessage="This method is insecure. We recommend using a {nostrlink}"
id="Z8ZOEY"
values={{
nostrlink: (
<a href="">
<FormattedMessage defaultMessage="nostr signer extension" />
<FormattedMessage defaultMessage="nostr signer extension" id="/EvlqN" />
</a>
),
}}
@ -211,7 +213,7 @@ export function LoginSignup({ close }: { close: () => void }) {
type="text"
value={key}
onChange={e => setNewKey(e.target.value)}
placeholder={formatMessage({ defaultMessage: "eg. nsec1xyz" })}
placeholder={formatMessage({ defaultMessage: "eg. nsec1xyz", id: "yzKwBQ" })}
/>
</div>
<div className="flex f-space">
@ -224,10 +226,10 @@ export function LoginSignup({ close }: { close: () => void }) {
setNewKey("");
setStage(Stage.Login);
}}>
<FormattedMessage defaultMessage="Cancel" />
<FormattedMessage defaultMessage="Cancel" id="47FYwb" />
</button>
<AsyncButton onClick={doLoginNsec} className="btn btn-primary">
<FormattedMessage defaultMessage="Log In" />
<FormattedMessage defaultMessage="Log In" id="r2Jjms" />
</AsyncButton>
</div>
</div>
@ -242,7 +244,7 @@ export function LoginSignup({ close }: { close: () => void }) {
<img src={LoginProfile as string} srcSet={`${LoginProfile2x} 2x`} className="header-image" />
<div className="content-inner">
<h2>
<FormattedMessage defaultMessage="Setup Profile" />
<FormattedMessage defaultMessage="Setup Profile" id="nOaArs" />
</h2>
<div className="flex f-center">
<div
@ -266,11 +268,11 @@ export function LoginSignup({ close }: { close: () => void }) {
/>
</div>
<small>
<FormattedMessage defaultMessage="You can change this later" />
<FormattedMessage defaultMessage="You can change this later" id="ZmqxZs" />
</small>
</div>
<AsyncButton type="button" className="btn btn-primary" onClick={setupProfile}>
<FormattedMessage defaultMessage="Save" />
<FormattedMessage defaultMessage="Save" id="jvo0vs" />
</AsyncButton>
</div>
</>
@ -282,15 +284,19 @@ export function LoginSignup({ close }: { close: () => void }) {
<img src={LoginWallet as string} srcSet={`${LoginWallet2x} 2x`} className="header-image" />
<div className="content-inner">
<h2>
<FormattedMessage defaultMessage="Get paid by viewers" />
<FormattedMessage defaultMessage="Get paid by viewers" id="Fodi9+" />
</h2>
<p>
<FormattedMessage defaultMessage="We hooked you up with a lightning wallet so you can get paid by viewers right away!" />
<FormattedMessage
defaultMessage="We hooked you up with a lightning wallet so you can get paid by viewers right away!"
id="Oxqtyf"
/>
</p>
{providerInfo?.balance && (
<p>
<FormattedMessage
defaultMessage="Oh, and you have {n} sats of free streaming on us! 💜"
id="f6biFA"
values={{
n: <FormattedNumber value={providerInfo.balance} />,
}}
@ -301,18 +307,18 @@ export function LoginSignup({ close }: { close: () => void }) {
<div className="paper">
<input
type="text"
placeholder={formatMessage({ defaultMessage: "eg. name@wallet.com" })}
placeholder={formatMessage({ defaultMessage: "eg. name@wallet.com", id: "1qsXCO" })}
value={lnAddress}
onChange={e => setLnAddress(e.target.value)}
/>
</div>
<small>
<FormattedMessage defaultMessage="You can always replace it with your own address later." />
<FormattedMessage defaultMessage="You can always replace it with your own address later." id="FjDlus" />
</small>
</div>
{error && <b className="error">{error}</b>}
<AsyncButton type="button" className="btn btn-primary" onClick={saveProfile}>
<FormattedMessage defaultMessage="Amazing! Continue.." />
<FormattedMessage defaultMessage="Amazing! Continue.." id="tM6fNW" />
</AsyncButton>
</div>
</>
@ -324,16 +330,19 @@ export function LoginSignup({ close }: { close: () => void }) {
<img src={LoginKey as string} srcSet={`${LoginKey2x} 2x`} className="header-image" />
<div className="content-inner">
<h2>
<FormattedMessage defaultMessage="Save Key" />
<FormattedMessage defaultMessage="Save Key" id="04lmFi" />
</h2>
<p>
<FormattedMessage defaultMessage="Save this and keep it safe! If you lose this key, you won't be able to access your account ever again. Yep, it's that serious!" />
<FormattedMessage
defaultMessage="Save this and keep it safe! If you lose this key, you won't be able to access your account ever again. Yep, it's that serious!"
id="H/bNs9"
/>
</p>
<div className="paper">
<Copy text={hexToBech32("nsec", key)} />
</div>
<button type="button" className="btn btn-primary" onClick={loginWithKey}>
<FormattedMessage defaultMessage="Ok, it's safe" />
<FormattedMessage defaultMessage="Ok, it's safe" id="My6HwN" />
</button>
</div>
</>

View File

@ -1,7 +1,7 @@
import "./markdown.css";
import { ReactNode, forwardRef, useMemo } from "react";
import { marked, Token } from "marked";
import { Token, marked } from "marked";
import { HyperText } from "./hypertext";
import { Text } from "./text";

View File

@ -1,11 +1,12 @@
import { useContext, useMemo } from "react";
import { useLogin } from "hooks/login";
import AsyncButton from "element/async-button";
import { Login } from "index";
import { MUTED } from "const";
import { FormattedMessage } from "react-intl";
import { SnortContext } from "@snort/system-react";
import { useLogin } from "@/hooks/login";
import AsyncButton from "./async-button";
import { Login } from "@/index";
import { MUTED } from "@/const";
export function useMute(pubkey: string) {
const system = useContext(SnortContext);
const login = useLogin();
@ -55,7 +56,11 @@ export function LoggedInMuteButton({ pubkey }: { pubkey: string }) {
return (
<AsyncButton type="button" className="btn delete-button" onClick={() => (isMuted ? unmute() : mute())}>
{isMuted ? <FormattedMessage defaultMessage="Unmute" /> : <FormattedMessage defaultMessage="Mute" />}
{isMuted ? (
<FormattedMessage defaultMessage="Unmute" id="W9355R" />
) : (
<FormattedMessage defaultMessage="Mute" id="x82IOl" />
)}
</AsyncButton>
);
}

View File

@ -1,14 +1,14 @@
import "./new-goal.css";
import * as Dialog from "@radix-ui/react-dialog";
import { FormattedMessage } from "react-intl";
import { useContext, useState } from "react";
import { SnortContext } from "@snort/system-react";
import AsyncButton from "./async-button";
import { Icon } from "element/icon";
import { useContext, useState } from "react";
import { GOAL } from "const";
import { useLogin } from "hooks/login";
import { FormattedMessage } from "react-intl";
import { defaultRelays } from "const";
import { SnortContext } from "@snort/system-react";
import { Icon } from "./icon";
import { GOAL } from "@/const";
import { useLogin } from "@/hooks/login";
import { defaultRelays } from "@/const";
export function NewGoalDialog() {
const system = useContext(SnortContext);
@ -44,7 +44,7 @@ export function NewGoalDialog() {
<span>
<Icon name="zap-filled" size={12} />
<span>
<FormattedMessage defaultMessage="Add stream goal" />
<FormattedMessage defaultMessage="Add stream goal" id="wOy57k" />
</span>
</span>
</button>
@ -56,12 +56,12 @@ export function NewGoalDialog() {
<div className="zap-goals">
<Icon name="zap-filled" className="stream-zap-goals-icon" size={16} />
<h3>
<FormattedMessage defaultMessage="Stream Zap Goals" />
<FormattedMessage defaultMessage="Stream Zap Goals" id="0GfNiL" />
</h3>
</div>
<div>
<p>
<FormattedMessage defaultMessage="Name" />
<FormattedMessage defaultMessage="Name" id="HAlOn1" />
</p>
<div className="paper">
<input
@ -74,7 +74,7 @@ export function NewGoalDialog() {
</div>
<div>
<p>
<FormattedMessage defaultMessage="Amount" />
<FormattedMessage defaultMessage="Amount" id="/0TOL5" />
</p>
<div className="paper">
<input
@ -89,7 +89,7 @@ export function NewGoalDialog() {
</div>
<div className="create-goal">
<AsyncButton type="button" className="btn btn-primary wide" disabled={!isValid} onClick={publishGoal}>
<FormattedMessage defaultMessage="Create Goal" />
<FormattedMessage defaultMessage="Create Goal" id="X2PZ7D" />
</AsyncButton>
</div>
</div>

View File

@ -1,18 +1,18 @@
import "./new-stream.css";
import * as Dialog from "@radix-ui/react-dialog";
import { Icon } from "element/icon";
import { useStreamProvider } from "hooks/stream-provider";
import { StreamProvider, StreamProviders } from "providers";
import { useContext, useEffect, useState } from "react";
import { StreamEditor, StreamEditorProps } from "./stream-editor";
import { useNavigate } from "react-router-dom";
import { eventLink, findTag } from "utils";
import { NostrProviderDialog } from "./nostr-provider-dialog";
import { unwrap } from "@snort/shared";
import { FormattedMessage } from "react-intl";
import { SnortContext } from "@snort/system-react";
import { Icon } from "./icon";
import { useStreamProvider } from "@/hooks/stream-provider";
import { StreamProvider, StreamProviders } from "@/providers";
import { StreamEditor, StreamEditorProps } from "./stream-editor";
import { eventLink, findTag } from "@/utils";
import { NostrProviderDialog } from "./nostr-provider-dialog";
function NewStream({ ev, onFinish }: StreamEditorProps) {
const system = useContext(SnortContext);
const providers = useStreamProvider();
@ -65,7 +65,7 @@ function NewStream({ ev, onFinish }: StreamEditorProps) {
return (
<>
<p>
<FormattedMessage defaultMessage="Stream Providers" />
<FormattedMessage defaultMessage="Stream Providers" id="6Z2pvJ" />
</p>
<div className="flex g12">
{providers.map(v => (
@ -94,7 +94,7 @@ export function NewStreamDialog(props: NewStreamDialogProps & StreamEditorProps)
{!props.text && (
<>
<span className="hide-on-mobile">
<FormattedMessage defaultMessage="Stream" />
<FormattedMessage defaultMessage="Stream" id="uYw2LD" />
</span>
<Icon name="signal" />
</>

View File

@ -1,12 +1,13 @@
import { NostrEvent } from "@snort/system";
import { StreamProvider, StreamProviderEndpoint, StreamProviderInfo } from "providers";
import { useContext, useEffect, useState } from "react";
import { FormattedMessage } from "react-intl";
import { SnortContext } from "@snort/system-react";
import { StreamProvider, StreamProviderEndpoint, StreamProviderInfo } from "@/providers";
import { SendZaps } from "./send-zap";
import { StreamEditor, StreamEditorProps } from "./stream-editor";
import Spinner from "./spinner";
import AsyncButton from "./async-button";
import { FormattedMessage } from "react-intl";
import { SnortContext } from "@snort/system-react";
export function NostrProviderDialog({ provider, ...others }: { provider: StreamProvider } & StreamEditorProps) {
const system = useContext(SnortContext);
@ -92,13 +93,14 @@ export function NostrProviderDialog({ provider, ...others }: { provider: StreamP
<p>
<FormattedMessage
defaultMessage="I have read and agree with {provider}'s {terms}."
id="RJOmzk"
values={{
provider: info.name,
terms: (
<span
className="tos-link"
onClick={() => window.open(info.tosLink, "popup", "width=400,height=800")}>
<FormattedMessage defaultMessage="terms and conditions" />
<FormattedMessage defaultMessage="terms and conditions" id="thsiMl" />
</span>
),
}}
@ -108,7 +110,7 @@ export function NostrProviderDialog({ provider, ...others }: { provider: StreamP
</div>
<div>
<AsyncButton type="button" className="btn btn-primary wide" disabled={!tos} onClick={acceptTos}>
<FormattedMessage defaultMessage="Continue" />
<FormattedMessage defaultMessage="Continue" id="acrOoz" />
</AsyncButton>
</div>
</>
@ -120,7 +122,7 @@ export function NostrProviderDialog({ provider, ...others }: { provider: StreamP
{info.endpoints.length > 1 && (
<div>
<p>
<FormattedMessage defaultMessage="Endpoint" />
<FormattedMessage defaultMessage="Endpoint" id="ljmS5P" />
</p>
<div className="flex g12">
{sortEndpoints(info.endpoints).map(a => (
@ -133,7 +135,7 @@ export function NostrProviderDialog({ provider, ...others }: { provider: StreamP
)}
<div>
<p>
<FormattedMessage defaultMessage="Server Url" />
<FormattedMessage defaultMessage="Server Url" id="5kx+2v" />
</p>
<div className="paper">
<input type="text" value={ep?.url} disabled />
@ -141,36 +143,40 @@ export function NostrProviderDialog({ provider, ...others }: { provider: StreamP
</div>
<div>
<p>
<FormattedMessage defaultMessage="Stream Key" />
<FormattedMessage defaultMessage="Stream Key" id="LknBsU" />
</p>
<div className="flex g12">
<div className="paper f-grow">
<input type="password" value={ep?.key} disabled />
</div>
<button className="btn btn-primary" onClick={() => window.navigator.clipboard.writeText(ep?.key ?? "")}>
<FormattedMessage defaultMessage="Copy" />
<FormattedMessage defaultMessage="Copy" id="4l6vz1" />
</button>
</div>
</div>
<div>
<p>
<FormattedMessage defaultMessage="Balance" />
<FormattedMessage defaultMessage="Balance" id="H5+NAX" />
</p>
<div className="flex g12">
<div className="paper f-grow">
<FormattedMessage defaultMessage="{amount} sats" values={{ amount: info.balance?.toLocaleString() }} />
<FormattedMessage
defaultMessage="{amount} sats"
id="vrTOHJ"
values={{ amount: info.balance?.toLocaleString() }}
/>
</div>
<button className="btn btn-primary" onClick={() => setTopup(true)}>
<FormattedMessage defaultMessage="Topup" />
<FormattedMessage defaultMessage="Topup" id="nBCvvJ" />
</button>
</div>
<small>
<FormattedMessage defaultMessage="About {estimate}" values={{ estimate: calcEstimate() }} />
<FormattedMessage defaultMessage="About {estimate}" id="Q3au2v" values={{ estimate: calcEstimate() }} />
</small>
</div>
<div>
<p>
<FormattedMessage defaultMessage="Resolutions" />
<FormattedMessage defaultMessage="Resolutions" id="4uI538" />
</p>
<div className="flex g12">
{ep?.capabilities?.map(a => (

View File

@ -1,11 +1,11 @@
import "./note.css";
import { type NostrEvent, NostrPrefix } from "@snort/system";
import { Markdown } from "element/markdown";
import { ExternalIconLink } from "element/external-link";
import { Profile } from "element/profile";
import { hexToBech32 } from "@snort/shared";
import { Markdown } from "./markdown";
import { ExternalIconLink } from "./external-link";
import { Profile } from "./profile";
export function Note({ ev }: { ev: NostrEvent }) {
return (
<div className="surface note">

View File

@ -4,11 +4,11 @@ import { Link } from "react-router-dom";
import { useUserProfile } from "@snort/system-react";
import { UserMetadata } from "@snort/system";
import { hexToBech32 } from "@snort/shared";
import { Icon } from "element/icon";
import usePlaceholder from "hooks/placeholders";
import { useInView } from "react-intersection-observer";
import { Icon } from "./icon";
import usePlaceholder from "@/hooks/placeholders";
export interface ProfileOptions {
showName?: boolean;
showAvatar?: boolean;

View File

@ -1,20 +1,20 @@
import "./send-zap.css";
import * as Dialog from "@radix-ui/react-dialog";
import { useEffect, useState, type ReactNode } from "react";
import { type ReactNode, useEffect, useState } from "react";
import { LNURL } from "@snort/shared";
import { NostrEvent, EventPublisher } from "@snort/system";
import { EventPublisher, NostrEvent } from "@snort/system";
import { secp256k1 } from "@noble/curves/secp256k1";
import { bytesToHex } from "@noble/curves/abstract/utils";
import { FormattedMessage, FormattedNumber } from "react-intl";
import { formatSats } from "../number";
import { Icon } from "./icon";
import AsyncButton from "./async-button";
import QrCode from "./qr-code";
import { useLogin } from "hooks/login";
import { useLogin } from "@/hooks/login";
import Copy from "./copy";
import { defaultRelays } from "const";
import { FormattedMessage, FormattedNumber } from "react-intl";
import { useRates } from "hooks/rates";
import { defaultRelays } from "@/const";
import { useRates } from "@/hooks/rates";
export interface LNURLLike {
get name(): string;
@ -131,6 +131,7 @@ export function SendZaps({ lnurl, pubkey, aTag, eTag, targetName, onFinish }: Se
<small>
<FormattedMessage
defaultMessage="Zap amount in {currency}"
id="IJDKz3"
values={{ currency: isFiat ? "USD" : "SATS" }}
/>
{isFiat && (
@ -138,6 +139,7 @@ export function SendZaps({ lnurl, pubkey, aTag, eTag, targetName, onFinish }: Se
&nbsp;
<FormattedMessage
defaultMessage="@ {rate}"
id="YPh5Nq"
description="Showing zap amount in USD @ rate"
values={{
rate: <FormattedNumber value={usdRate} />,
@ -157,7 +159,7 @@ export function SendZaps({ lnurl, pubkey, aTag, eTag, targetName, onFinish }: Se
{svc && (svc.maxCommentLength > 0 || svc.canZap) && (
<div>
<small>
<FormattedMessage defaultMessage="Your comment for {name}" values={{ name }} />
<FormattedMessage defaultMessage="Your comment for {name}" id="ESyhzp" values={{ name }} />
</small>
<div className="paper">
<textarea placeholder="Nice!" value={comment} onChange={e => setComment(e.target.value)} />
@ -166,7 +168,7 @@ export function SendZaps({ lnurl, pubkey, aTag, eTag, targetName, onFinish }: Se
)}
<div>
<AsyncButton onClick={send} className="btn btn-primary">
<FormattedMessage defaultMessage="Zap!" />
<FormattedMessage defaultMessage="Zap!" id="3HwrQo" />
</AsyncButton>
</div>
</>
@ -184,7 +186,7 @@ export function SendZaps({ lnurl, pubkey, aTag, eTag, targetName, onFinish }: Se
<Copy text={invoice} />
</div>
<button className="btn btn-primary wide" onClick={() => onFinish()}>
<FormattedMessage defaultMessage="Back" />
<FormattedMessage defaultMessage="Back" id="cyR7Kh" />
</button>
</>
);
@ -193,7 +195,7 @@ export function SendZaps({ lnurl, pubkey, aTag, eTag, targetName, onFinish }: Se
return (
<div className="send-zap">
<h3>
<FormattedMessage defaultMessage="Zap {name}" values={{ name }} />
<FormattedMessage defaultMessage="Zap {name}" id="oHPB8Q" values={{ name }} />
<Icon name="zap" />
</h3>
{input()}
@ -212,7 +214,7 @@ export function SendZapsDialog(props: Omit<SendZapsProps, "onFinish">) {
) : (
<button className="btn btn-primary zap">
<span className="hide-on-mobile">
<FormattedMessage defaultMessage="Zap" />
<FormattedMessage defaultMessage="Zap" id="fBI91o" />
</span>
<Icon name="zap-filled" size={16} />
</button>

View File

@ -2,15 +2,15 @@ import { Menu, MenuItem } from "@szhsin/react-menu";
import * as Dialog from "@radix-ui/react-dialog";
import { unwrap } from "@snort/shared";
import { NostrEvent, NostrPrefix, encodeTLV } from "@snort/system";
import { FormattedMessage } from "react-intl";
import { useContext, useState } from "react";
import { SnortContext } from "@snort/system-react";
import { Icon } from "./icon";
import { useContext, useState } from "react";
import { Textarea } from "./textarea";
import { findTag } from "utils";
import { findTag } from "@/utils";
import AsyncButton from "./async-button";
import { useLogin } from "hooks/login";
import { FormattedMessage } from "react-intl";
import { SnortContext } from "@snort/system-react";
import { useLogin } from "@/hooks/login";
type ShareOn = "nostr" | "twitter";
@ -41,7 +41,7 @@ export function ShareMenu({ ev }: { ev: NostrEvent }) {
menuClassName="ctx-menu"
menuButton={
<button type="button" className="btn btn-secondary">
<FormattedMessage defaultMessage="Share" />
<FormattedMessage defaultMessage="Share" id="OKhRC6" />
</button>
}>
<MenuItem
@ -50,7 +50,7 @@ export function ShareMenu({ ev }: { ev: NostrEvent }) {
setShare("nostr");
}}>
<Icon name="nostrich" size={24} />
<FormattedMessage defaultMessage="Broadcast on Nostr" />
<FormattedMessage defaultMessage="Broadcast on Nostr" id="wCIL7o" />
</MenuItem>
</Menu>
<Dialog.Root open={Boolean(share)} onOpenChange={() => setShare(undefined)}>
@ -59,7 +59,7 @@ export function ShareMenu({ ev }: { ev: NostrEvent }) {
<Dialog.Content className="dialog-content">
<div className="content-inner">
<h2>
<FormattedMessage defaultMessage="Share" />
<FormattedMessage defaultMessage="Share" id="OKhRC6" />
</h2>
<div className="paper">
<Textarea
@ -73,7 +73,7 @@ export function ShareMenu({ ev }: { ev: NostrEvent }) {
/>
</div>
<AsyncButton className="btn btn-primary" onClick={sendMessage}>
<FormattedMessage defaultMessage="Send" />
<FormattedMessage defaultMessage="Send" id="9WRlF4" />
</AsyncButton>
</div>
</Dialog.Content>

View File

@ -1,5 +1,5 @@
import "./state-pill.css";
import { StreamState } from "index";
import { StreamState } from "@/index";
export function StatePill({ state }: { state: StreamState }) {
return <span className={`state pill${state === StreamState.Live ? " live" : ""}`}>{state}</span>;

View File

@ -1,6 +1,6 @@
import "./stream-cards.css";
import { useState, forwardRef, useContext } from "react";
import { forwardRef, useContext, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import * as Dialog from "@radix-ui/react-dialog";
import { DndProvider, useDrag, useDrop } from "react-dnd";
@ -8,20 +8,20 @@ import { HTML5Backend } from "react-dnd-html5-backend";
import { removeUndefined, unwrap } from "@snort/shared";
import { NostrLink, TaggedNostrEvent } from "@snort/system";
import { Toggle } from "element/toggle";
import { Icon } from "element/icon";
import { ExternalLink } from "element/external-link";
import { FileUploader } from "element/file-uploader";
import { Markdown } from "element/markdown";
import { useLogin } from "hooks/login";
import { useCards, useUserCards } from "hooks/cards";
import { CARD, USER_CARDS } from "const";
import { findTag } from "utils";
import { Login } from "index";
import type { Tags } from "types";
import { SnortContext } from "@snort/system-react";
import { Toggle } from "./toggle";
import { Icon } from "./icon";
import { ExternalLink } from "./external-link";
import { FileUploader } from "./file-uploader";
import { Markdown } from "./markdown";
import { useLogin } from "@/hooks/login";
import { useCards, useUserCards } from "@/hooks/cards";
import { CARD, USER_CARDS } from "@/const";
import { findTag } from "@/utils";
import { Login } from "@/index";
import type { Tags } from "@/types";
interface CardType {
identifier: string;
content: string;
@ -187,28 +187,28 @@ function CardDialog({ header, cta, cancelCta, card, onSave, onCancel }: CardDial
return (
<div className="new-card">
<h3>{header || <FormattedMessage defaultMessage="Add card" />}</h3>
<h3>{header || <FormattedMessage defaultMessage="Add card" id="nwA8Os" />}</h3>
<div className="form-control">
<label htmlFor="card-title">
<FormattedMessage defaultMessage="Title" />
<FormattedMessage defaultMessage="Title" id="9a9+ww" />
</label>
<input
id="card-title"
type="text"
value={title}
onChange={e => setTitle(e.target.value)}
placeholder={formatMessage({ defaultMessage: "e.g. about me" })}
placeholder={formatMessage({ defaultMessage: "e.g. about me", id: "k21gTS" })}
/>
</div>
<div className="form-control">
<label htmlFor="card-image">
<FormattedMessage defaultMessage="Image" />
<FormattedMessage defaultMessage="Image" id="+0zv6g" />
</label>
<FileUploader defaultImage={image} onFileUpload={setImage} onClear={() => setImage("")} />
</div>
<div className="form-control">
<label htmlFor="card-image-link">
<FormattedMessage defaultMessage="Image Link" />
<FormattedMessage defaultMessage="Image Link" id="s5ksS7" />
</label>
<input
id="card-image-link"
@ -220,20 +220,21 @@ function CardDialog({ header, cta, cancelCta, card, onSave, onCancel }: CardDial
</div>
<div className="form-control">
<label htmlFor="card-content">
<FormattedMessage defaultMessage="Content" />
<FormattedMessage defaultMessage="Content" id="Jq3FDz" />
</label>
<textarea
placeholder={formatMessage({ defaultMessage: "Start typing" })}
placeholder={formatMessage({ defaultMessage: "Start typing", id: "w0Xm2F" })}
value={content}
onChange={e => setContent(e.target.value)}
/>
<span className="help-text">
<FormattedMessage
defaultMessage="Supports {markdown}"
id="I1kjHI"
values={{
markdown: (
<ExternalLink href="https://www.markdownguide.org/cheat-sheet">
<FormattedMessage defaultMessage="Markdown" />
<FormattedMessage defaultMessage="Markdown" id="jr4+vD" />
</ExternalLink>
),
}}
@ -242,10 +243,10 @@ function CardDialog({ header, cta, cancelCta, card, onSave, onCancel }: CardDial
</div>
<div className="new-card-buttons">
<button className="btn btn-primary add-button" onClick={() => onSave({ title, image, content, link })}>
{cta || <FormattedMessage defaultMessage="Add Card" />}
{cta || <FormattedMessage defaultMessage="Add Card" id="UJBFYK" />}
</button>
<button className="btn delete-button" onClick={onCancel}>
{cancelCta || <FormattedMessage defaultMessage="Cancel" />}
{cancelCta || <FormattedMessage defaultMessage="Cancel" id="47FYwb" />}
</button>
</div>
</div>
@ -310,7 +311,7 @@ function EditCard({ card, cards }: EditCardProps) {
<Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
<Dialog.Trigger asChild>
<button className="btn btn-primary">
<FormattedMessage defaultMessage="Edit" />
<FormattedMessage defaultMessage="Edit" id="wEQDC6" />
</button>
</Dialog.Trigger>
<Dialog.Portal>
@ -318,9 +319,9 @@ function EditCard({ card, cards }: EditCardProps) {
<Dialog.Content className="dialog-content">
<div className="content-inner">
<CardDialog
header={formatMessage({ defaultMessage: "Edit card" })}
cta={formatMessage({ defaultMessage: "Save card" })}
cancelCta={formatMessage({ defaultMessage: "Delete" })}
header={formatMessage({ defaultMessage: "Edit card", id: "OWgHbg" })}
cta={formatMessage({ defaultMessage: "Save card", id: "rfC1Zq" })}
cancelCta={formatMessage({ defaultMessage: "Delete", id: "K3r6DQ" })}
card={card}
onSave={editCard}
onCancel={onCancel}

View File

@ -1,16 +1,16 @@
import "./stream-editor.css";
import { useEffect, useState, useCallback } from "react";
import { useCallback, useEffect, useState } from "react";
import { NostrEvent } from "@snort/system";
import { unixNow } from "@snort/shared";
import { TagsInput } from "react-tag-input-component";
import { FormattedMessage, useIntl } from "react-intl";
import AsyncButton from "./async-button";
import { StreamState } from "../index";
import { findTag } from "../utils";
import { useLogin } from "hooks/login";
import { NewGoalDialog } from "element/new-goal";
import { useGoals } from "hooks/goals";
import { StreamState } from "@/index";
import { findTag } from "@/utils";
import { useLogin } from "@/hooks/login";
import { NewGoalDialog } from "./new-goal";
import { useGoals } from "@/hooks/goals";
export interface StreamEditorProps {
ev?: NostrEvent;
@ -37,9 +37,9 @@ function GoalSelector({ goal, pubkey, onGoalSelect }: GoalSelectorProps) {
const { formatMessage } = useIntl();
return (
<select onChange={ev => onGoalSelect(ev.target.value)}>
<option value="">{formatMessage({ defaultMessage: "Select a goal..." })}</option>
<option value={goal}>{formatMessage({ defaultMessage: "Select a goal...", id: "I/TubD" })}</option>
{goals?.map(x => (
<option key={x.id} value={x.id} selected={goal === x.id}>
<option key={x.id} value={x.id}>
{x.content}
</option>
))}
@ -139,12 +139,12 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
{(options?.canSetTitle ?? true) && (
<div>
<p>
<FormattedMessage defaultMessage="Title" />
<FormattedMessage defaultMessage="Title" id="9a9+ww" />
</p>
<div className="paper">
<input
type="text"
placeholder={formatMessage({ defaultMessage: "What are we steaming today?" })}
placeholder={formatMessage({ defaultMessage: "What are we steaming today?", id: "QRHNuF" })}
value={title}
onChange={e => setTitle(e.target.value)}
/>
@ -154,12 +154,12 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
{(options?.canSetSummary ?? true) && (
<div>
<p>
<FormattedMessage defaultMessage="Summary" />
<FormattedMessage defaultMessage="Summary" id="RrCui3" />
</p>
<div className="paper">
<input
type="text"
placeholder={formatMessage({ defaultMessage: "A short description of the content" })}
placeholder={formatMessage({ defaultMessage: "A short description of the content", id: "mtNGwh" })}
value={summary}
onChange={e => setSummary(e.target.value)}
/>
@ -169,7 +169,7 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
{(options?.canSetImage ?? true) && (
<div>
<p>
<FormattedMessage defaultMessage="Cover Image" />
<FormattedMessage defaultMessage="Cover Image" id="Gq6x9o" />
</p>
<div className="paper">
<input type="text" placeholder="https://" value={image} onChange={e => setImage(e.target.value)} />
@ -179,13 +179,13 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
{(options?.canSetStream ?? true) && (
<div>
<p>
<FormattedMessage defaultMessage="Stream URL" />
<FormattedMessage defaultMessage="Stream URL" id="QRRCp0" />
</p>
<div className="paper">
<input type="text" placeholder="https://" value={stream} onChange={e => setStream(e.target.value)} />
</div>
<small>
<FormattedMessage defaultMessage="Stream type should be HLS" />
<FormattedMessage defaultMessage="Stream type should be HLS" id="oZrFyI" />
</small>
</div>
)}
@ -193,7 +193,7 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
<>
<div>
<p>
<FormattedMessage defaultMessage="Status" />
<FormattedMessage defaultMessage="Status" id="tzMNF3" />
</p>
<div className="flex g12">
{[StreamState.Live, StreamState.Planned, StreamState.Ended].map(v => (
@ -206,7 +206,7 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
{status === StreamState.Planned && (
<div>
<p>
<FormattedMessage defaultMessage="Start Time" />
<FormattedMessage defaultMessage="Start Time" id="5QYdPU" />
</p>
<div className="paper">
<input
@ -222,7 +222,7 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
{(options?.canSetTags ?? true) && (
<div>
<p>
<FormattedMessage defaultMessage="Tags" />
<FormattedMessage defaultMessage="Tags" id="1EYCdR" />
</p>
<div className="paper">
<TagsInput value={tags} onChange={setTags} placeHolder="Music,DJ,English" separators={["Enter", ","]} />
@ -233,7 +233,7 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
<>
<div>
<p>
<FormattedMessage defaultMessage="Goal" />
<FormattedMessage defaultMessage="Goal" id="0VV/sK" />
</p>
<div className="paper">
<GoalSelector goal={goal} pubkey={login?.pubkey} onGoalSelect={setGoal} />
@ -249,15 +249,22 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
</div>
<div>
<div className="warning">
<FormattedMessage defaultMessage="NSFW Content" />
<FormattedMessage defaultMessage="NSFW Content" id="Atr2p4" />
</div>
<FormattedMessage defaultMessage="Check here if this stream contains nudity or pornographic content." />
<FormattedMessage
defaultMessage="Check here if this stream contains nudity or pornographic content."
id="lZpRMR"
/>
</div>
</div>
)}
<div>
<AsyncButton type="button" className="btn btn-primary wide" disabled={!isValid} onClick={publishStream}>
{ev ? <FormattedMessage defaultMessage="Save" /> : <FormattedMessage defaultMessage="Start Stream" />}
{ev ? (
<FormattedMessage defaultMessage="Save" id="jvo0vs" />
) : (
<FormattedMessage defaultMessage="Start Stream" id="TaTRKo" />
)}
</AsyncButton>
</div>
</>

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from "react";
import { NostrEvent } from "@snort/system";
import { unixNow } from "@snort/shared";
import { findTag } from "../utils";
import { findTag } from "@/utils";
export function StreamTimer({ ev }: { ev?: NostrEvent }) {
const [time, setTime] = useState("");

View File

@ -1,15 +1,13 @@
import type { ReactNode } from "react";
import { FormattedMessage } from "react-intl";
import moment from "moment";
import { NostrEvent } from "@snort/system";
import { StreamState } from "index";
import { findTag, getTagValues } from "utils";
import { StreamState } from "@/index";
import { findTag, getTagValues } from "@/utils";
export function Tags({ children, max, ev }: { children?: ReactNode; max?: number; ev: NostrEvent }) {
const status = findTag(ev, "status");
const start = findTag(ev, "starts");
const hashtags = getTagValues(ev.tags, "t");
const tags = max ? hashtags.slice(0, max) : hashtags;
@ -18,8 +16,7 @@ export function Tags({ children, max, ev }: { children?: ReactNode; max?: number
{children}
{status === StreamState.Planned && (
<span className="pill">
{status === StreamState.Planned ? <FormattedMessage defaultMessage="Starts " /> : ""}
{moment(Number(start) * 1000).fromNow()}
{status === StreamState.Planned ? <FormattedMessage defaultMessage="Starts " id="0hNxBy" /> : ""}
</span>
)}
{tags.map(a => (

View File

@ -1,5 +1,5 @@
import "./textarea.css";
import { type KeyboardEvent, type ChangeEvent, useContext } from "react";
import { type ChangeEvent, type KeyboardEvent, useContext } from "react";
import ReactTextareaAutocomplete, { TriggerType } from "@webscopeio/react-textarea-autocomplete";
import "@webscopeio/react-textarea-autocomplete/style.css";
import uniqWith from "lodash/uniqWith";
@ -9,9 +9,9 @@ import { hexToBech32 } from "@snort/shared";
import { SnortContext } from "@snort/system-react";
import { MetadataCache, NostrPrefix, UserProfileCache } from "@snort/system";
import { Emoji } from "element/emoji";
import { Avatar } from "element/avatar";
import type { EmojiTag } from "types";
import { Emoji } from "./emoji";
import { Avatar } from "./avatar";
import type { EmojiTag } from "@/types";
interface EmojiItemProps {
name: string;

View File

@ -1,6 +1,6 @@
import * as BaseToggle from "@radix-ui/react-toggle";
import "./toggle.css";
import { Icon } from "element/icon";
import { Icon } from "./icon";
interface ToggleProps {
label: string;

View File

@ -1,6 +1,6 @@
import { ParsedZap } from "@snort/system";
import useTopZappers from "hooks/top-zappers";
import { formatSats } from "number";
import useTopZappers from "@/hooks/top-zappers";
import { formatSats } from "@/number";
import { Icon } from "./icon";
import { Profile } from "./profile";

View File

@ -1,17 +1,18 @@
import "./video-tile.css";
import { Link } from "react-router-dom";
import { Profile } from "./profile";
import "./video-tile.css";
import { NostrEvent, encodeTLV, NostrPrefix } from "@snort/system";
import { NostrEvent, NostrPrefix, encodeTLV } from "@snort/system";
import { useInView } from "react-intersection-observer";
import { StatePill } from "./state-pill";
import { StreamState } from "index";
import { findTag, getHost } from "utils";
import { formatSats } from "number";
import ZapStream from "../../public/zap-stream.svg";
import { isContentWarningAccepted } from "./content-warning";
import { Tags } from "element/tags";
import { FormattedMessage } from "react-intl";
import { StatePill } from "./state-pill";
import { StreamState } from "@/index";
import { findTag, getHost } from "@/utils";
import { formatSats } from "@/number";
import ZapStream from "/zap-stream.svg";
import { isContentWarningAccepted } from "./content-warning";
import { Tags } from "./tags";
export function VideoTile({
ev,
showAuthor = true,
@ -42,7 +43,7 @@ export function VideoTile({
{showStatus && <StatePill state={status as StreamState} />}
{viewers && (
<span className="pill viewers">
<FormattedMessage defaultMessage="{n} viewers" values={{ n: formatSats(Number(viewers)) }} />
<FormattedMessage defaultMessage="{n} viewers" id="3adEeb" values={{ n: formatSats(Number(viewers)) }} />
</span>
)}
</span>

View File

@ -1,17 +1,17 @@
import { NostrLink, EventKind } from "@snort/system";
import { EventKind, NostrLink } from "@snort/system";
import React, { useContext, useRef, useState } from "react";
import { FormattedMessage } from "react-intl";
import { useLogin } from "hooks/login";
import AsyncButton from "element/async-button";
import { Icon } from "element/icon";
import { Textarea } from "element/textarea";
import { EmojiPicker } from "element/emoji-picker";
import type { EmojiPack, Emoji } from "types";
import { LIVE_STREAM_CHAT } from "const";
import { SnortContext } from "@snort/system-react";
import { unixNowMs } from "@snort/shared";
import { TimeSync } from "index";
import { useLogin } from "@/hooks/login";
import AsyncButton from "./async-button";
import { Icon } from "./icon";
import { Textarea } from "./textarea";
import { EmojiPicker } from "./emoji-picker";
import type { Emoji, EmojiPack } from "@/types";
import { LIVE_STREAM_CHAT } from "@/const";
import { TimeSync } from "@/index";
export function WriteMessage({ link, emojiPacks }: { link: NostrLink; emojiPacks: EmojiPack[] }) {
const system = useContext(SnortContext);
@ -99,7 +99,7 @@ export function WriteMessage({ link, emojiPacks }: { link: NostrLink; emojiPacks
)}
</div>
<AsyncButton onClick={sendChatMessage} className="btn btn-border">
<FormattedMessage defaultMessage="Send" />
<FormattedMessage defaultMessage="Send" id="9WRlF4" />
</AsyncButton>
</>
);

View File

@ -1,10 +1,10 @@
import { useMemo } from "react";
import { TaggedNostrEvent, EventKind, NoteCollection, RequestBuilder } from "@snort/system";
import { EventKind, NoteCollection, RequestBuilder, TaggedNostrEvent } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
import { findTag, toAddress, getTagValues } from "utils";
import type { Badge } from "types";
import { findTag, getTagValues, toAddress } from "@/utils";
import type { Badge } from "@/types";
export function useBadges(
pubkey: string,

View File

@ -1,10 +1,10 @@
import { useMemo } from "react";
import { TaggedNostrEvent, ReplaceableNoteStore, NoteCollection, RequestBuilder } from "@snort/system";
import { NoteCollection, ReplaceableNoteStore, RequestBuilder, TaggedNostrEvent } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
import { USER_CARDS, CARD } from "const";
import { findTag } from "utils";
import { CARD, USER_CARDS } from "@/const";
import { findTag } from "@/utils";
export function useUserCards(pubkey: string, userCards: Array<string[]>, leaveOpen = false): TaggedNostrEvent[] {
const related = useMemo(() => {

View File

@ -1,9 +1,10 @@
import { unwrap } from "@snort/shared";
import { NostrEvent, NostrLink, NostrPrefix, NoteCollection, RequestBuilder, TaggedNostrEvent } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
import { LIVE_STREAM } from "const";
import { useMemo } from "react";
import { LIVE_STREAM } from "@/const";
export function useCurrentStreamFeed(link: NostrLink, leaveOpen = false, evPreload?: NostrEvent) {
const author = link.type === NostrPrefix.Address ? unwrap(link.author) : link.id;
const sub = useMemo(() => {

View File

@ -1,11 +1,11 @@
import { useMemo } from "react";
import uniqBy from "lodash.uniqby";
import { RequestBuilder, ReplaceableNoteStore, NoteCollection, NostrEvent } from "@snort/system";
import { NostrEvent, NoteCollection, ReplaceableNoteStore, RequestBuilder } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
import { findTag } from "utils";
import { EMOJI_PACK, USER_EMOJIS } from "const";
import type { EmojiPack, Tags, EmojiTag } from "types";
import { findTag } from "@/utils";
import { EMOJI_PACK, USER_EMOJIS } from "@/const";
import type { EmojiPack, EmojiTag, Tags } from "@/types";
function cleanShortcode(shortcode?: string) {
return shortcode?.replace(/\s+/g, "_").replace(/_$/, "");

View File

@ -1,5 +1,5 @@
import { useMemo } from "react";
import { NostrPrefix, RequestBuilder, ReplaceableNoteStore, NostrLink } from "@snort/system";
import { NostrLink, NostrPrefix, ReplaceableNoteStore, RequestBuilder } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
export default function useEventFeed(link: NostrLink, leaveOpen = false) {

View File

@ -1,6 +1,6 @@
import { useMemo } from "react";
import { NostrPrefix, ReplaceableNoteStore, RequestBuilder, type NostrLink } from "@snort/system";
import { type NostrLink, NostrPrefix, ReplaceableNoteStore, RequestBuilder } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
export function useAddress(kind: number, pubkey: string, identifier: string) {

View File

@ -1,7 +1,7 @@
import { useMemo } from "react";
import { RequestBuilder, FlatNoteStore, ReplaceableNoteStore } from "@snort/system";
import { FlatNoteStore, ReplaceableNoteStore, RequestBuilder } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
import { GOAL } from "const";
import { GOAL } from "@/const";
export function useZapGoal(id?: string) {
const sub = useMemo(() => {

View File

@ -1,10 +1,10 @@
import { useMemo } from "react";
import { RequestBuilder, ReplaceableNoteStore } from "@snort/system";
import { ReplaceableNoteStore, RequestBuilder } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
import { MUTED } from "const";
import { getTagValues } from "utils";
import { MUTED } from "@/const";
import { getTagValues } from "@/utils";
export function useMutedPubkeys(host?: string, leaveOpen = false) {
const mutedSub = useMemo(() => {

View File

@ -1,8 +1,8 @@
import { NostrLink, RequestBuilder, NoteCollection } from "@snort/system";
import { NostrLink, NoteCollection, RequestBuilder } from "@snort/system";
import { useReactions, useRequestBuilder } from "@snort/system-react";
import { unixNow } from "@snort/shared";
import { useMemo } from "react";
import { LIVE_STREAM_CHAT, WEEK } from "const";
import { LIVE_STREAM_CHAT, WEEK } from "@/const";
export function useLiveChatFeed(link?: NostrLink, eZaps?: Array<string>, limit = 100) {
const since = useMemo(() => unixNow() - WEEK, [link?.id]);

View File

@ -4,10 +4,10 @@ import { NostrEvent, NoteCollection, RequestBuilder } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
import { unixNow } from "@snort/shared";
import { LIVE_STREAM } from "const";
import { StreamState } from "index";
import { findTag, getHost } from "utils";
import { WEEK } from "const";
import { LIVE_STREAM } from "@/const";
import { StreamState } from "@/index";
import { findTag, getHost } from "@/utils";
import { WEEK } from "@/const";
export function useStreamsFeed(tag?: string) {
const since = useMemo(() => unixNow() - WEEK, [tag]);

View File

@ -1,13 +1,13 @@
import { useSyncExternalStore, useMemo, useState, useEffect } from "react";
import { useEffect, useMemo, useState, useSyncExternalStore } from "react";
import { EventKind, NoteCollection, RequestBuilder } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
import { useUserEmojiPacks } from "hooks/emoji";
import { MUTED, USER_CARDS, USER_EMOJIS } from "const";
import type { Tags } from "types";
import { getPublisher } from "login";
import { Login } from "index";
import { useUserEmojiPacks } from "@/hooks/emoji";
import { MUTED, USER_CARDS, USER_EMOJIS } from "@/const";
import type { Tags } from "@/types";
import { getPublisher } from "@/login";
import { Login } from "@/index";
export function useLogin() {
const session = useSyncExternalStore(

View File

@ -1,7 +1,7 @@
import { useMemo } from "react";
import { RequestBuilder, NoteCollection, NostrLink } from "@snort/system";
import { NostrLink, NoteCollection, RequestBuilder } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
import { LIVE_STREAM } from "const";
import { LIVE_STREAM } from "@/const";
import { useZaps } from "./zaps";
export function useProfile(link: NostrLink, leaveOpen = false) {

View File

@ -1,6 +1,6 @@
import { fetchNip05Pubkey } from "@snort/shared";
import { NostrLink, tryParseNostrLink, NostrPrefix } from "@snort/system";
import { useState, useEffect } from "react";
import { NostrLink, NostrPrefix, tryParseNostrLink } from "@snort/system";
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
export function useStreamLink() {

View File

@ -1,4 +1,4 @@
import { StreamProviderStore } from "providers";
import { StreamProviderStore } from "@/providers";
import { useSyncExternalStore } from "react";
export function useStreamProvider() {

View File

@ -1,5 +1,5 @@
import { useMemo } from "react";
import { NostrLink, RequestBuilder, EventKind, NoteCollection, parseZap } from "@snort/system";
import { EventKind, NostrLink, NoteCollection, RequestBuilder, parseZap } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
export function useZaps(link?: NostrLink, leaveOpen = false) {

View File

@ -8,24 +8,24 @@ import { NostrSystem } from "@snort/system";
import { SnortContext } from "@snort/system-react";
import { SnortSystemDb } from "@snort/system-web";
import { RouterProvider, createBrowserRouter } from "react-router-dom";
import { RootPage } from "pages/root";
import { TagPage } from "pages/tag";
import { LayoutPage } from "pages/layout";
import { ProfilePage } from "pages/profile-page";
import { StreamPageHandler } from "pages/stream-page";
import { ChatPopout } from "pages/chat-popout";
import { LoginStore } from "login";
import { StreamProvidersPage } from "pages/providers";
import { defaultRelays } from "const";
import { CatchAllRoutePage } from "pages/catch-all";
import { SettingsPage } from "pages/settings-page";
import { register } from "serviceWorker";
import { IntlProvider } from "intl";
import { WidgetsPage } from "pages/widgets";
import { AlertsPage } from "pages/alerts";
import { unixNowMs } from "@snort/shared";
import { StreamSummaryPage } from "pages/summary";
import { RootPage } from "@/pages/root";
import { TagPage } from "@/pages/tag";
import { LayoutPage } from "@/pages/layout";
import { ProfilePage } from "@/pages/profile-page";
import { StreamPageHandler } from "@/pages/stream-page";
import { ChatPopout } from "@/pages/chat-popout";
import { LoginStore } from "@/login";
import { StreamProvidersPage } from "@/pages/providers";
import { defaultRelays } from "@/const";
import { CatchAllRoutePage } from "@/pages/catch-all";
import { SettingsPage } from "@/pages/settings-page";
import { register } from "@/serviceWorker";
import { IntlProvider } from "@/intl";
import { WidgetsPage } from "@/pages/widgets";
import { AlertsPage } from "@/pages/alerts";
import { StreamSummaryPage } from "@/pages/summary";
export enum StreamState {
Live = "live",

View File

@ -1,15 +1,8 @@
import { DefaultLocale, useLang } from "hooks/lang";
import { useEffect, useState, type ReactNode } from "react";
import { DefaultLocale, useLang } from "@/hooks/lang";
import { type ReactNode, useEffect, useState } from "react";
import { IntlProvider as ReactIntlProvider } from "react-intl";
import enMessages from "translations/en.json";
async function importLang(code: string) {
const src = await import(`translations/${code}.json`);
const typed = src.default as Record<string, { defaultMessage: string }>;
const ent = Object.entries(typed).map(([k, v]) => [k, v.defaultMessage]);
return Object.fromEntries(ent) as Record<string, string>;
}
import enMessages from "@/translations/en.json";
export const AllLocales = [
DefaultLocale,
@ -29,7 +22,13 @@ export const AllLocales = [
"fr-FR",
"pt-BR",
"ru-RU",
];
] as const;
function importLang(src: any) {
const typed = src.default as Record<string, { defaultMessage: string }>;
const ent = Object.entries(typed).map(([k, v]) => [k, v.defaultMessage]);
return Object.fromEntries(ent) as Record<string, string>;
}
const getMessages = (locale: string) => {
const truncatedLocale = locale.toLowerCase().split(/[_-]+/)[0];
@ -38,50 +37,50 @@ const getMessages = (locale: string) => {
switch (lng) {
case "de":
case "de-DE":
return await importLang("de_DE");
return importLang(await import("@/translations/de_DE.json"));
case "es":
case "es-ES":
return await importLang("es_ES");
return importLang(await import("@/translations/es_ES.json"));
case "th":
case "th-TH":
return await importLang("th_TH");
return importLang(await import("@/translations/th_TH.json"));
case "nl":
case "nl-NL":
return await importLang("nl_NL");
return importLang(await import("@/translations/nl_NL.json"));
case "ja":
case "ja-JP":
return await importLang("ja_JP");
return importLang(await import("@/translations/ja_JP.json"));
case "fa":
case "fa-IR":
return await importLang("fa_IR");
return importLang(await import("@/translations/fa_IR.json"));
case "sw":
case "sw-KE":
return await importLang("sw_KE");
return importLang(await import("@/translations/sw_KE.json"));
case "sv":
case "sv-SE":
return await importLang("sv_SE");
return importLang(await import("@/translations/sv_SE.json"));
case "bn":
case "bn-BD":
return await importLang("bn_BD");
return importLang(await import("@/translations/bn_BD.json"));
case "bg":
case "bg-BG":
return await importLang("bg_BG");
return importLang(await import("@/translations/bg_BG.json"));
case "zh":
case "zh-CN":
return await importLang("zh_CN");
return importLang(await import("@/translations/zh_CN.json"));
case "zh-TW":
return await importLang("zh_TW");
return importLang(await import("@/translations/zh_TW.json"));
case "fi":
case "fi-FI":
return await importLang("fi_FI");
return importLang(await import("@/translations/fi_FI.json"));
case "fr":
case "fr-FR":
return await importLang("fr_FR");
return importLang(await import("@/translations/fr_FR.json"));
case "pt-BR":
return await importLang("pt_BR");
return importLang(await import("@/translations/pt_BR.json"));
case "ru":
case "ru-RU":
return await importLang("ru_RU");
return importLang(await import("@/translations/ru_RU.json"));
case DefaultLocale:
case "en":
return enMessages;

View File

@ -26,6 +26,9 @@
"0VV/sK": {
"defaultMessage": "Goal"
},
"0hNxBy": {
"defaultMessage": "Starts"
},
"1EYCdR": {
"defaultMessage": "Tags"
},
@ -74,6 +77,9 @@
"6pr6hJ": {
"defaultMessage": "Minimum amount for text to speech"
},
"79lLl+": {
"defaultMessage": "Music"
},
"8YT6ja": {
"defaultMessage": "Insert text to speak"
},
@ -83,9 +89,6 @@
"9a9+ww": {
"defaultMessage": "Title"
},
"9anxhq": {
"defaultMessage": "Starts"
},
"AIHaPH": {
"defaultMessage": "{person} zapped {amount} sats"
},
@ -107,6 +110,9 @@
"C81/uG": {
"defaultMessage": "Logout"
},
"CsCUYo": {
"defaultMessage": "{n} sats"
},
"D3idYv": {
"defaultMessage": "Settings"
},
@ -128,6 +134,9 @@
"G/yZLu": {
"defaultMessage": "Remove"
},
"GGaJMU": {
"defaultMessage": "Top Chatters"
},
"Gq6x9o": {
"defaultMessage": "Cover Image"
},
@ -152,6 +161,9 @@
"INlWvJ": {
"defaultMessage": "OR"
},
"J/+m9y": {
"defaultMessage": "Stream Duration {duration} mins"
},
"JEsxDw": {
"defaultMessage": "Uploading..."
},
@ -239,6 +251,9 @@
"X2PZ7D": {
"defaultMessage": "Create Goal"
},
"XgWvGA": {
"defaultMessage": "Reactions"
},
"YPh5Nq": {
"defaultMessage": "@ {rate}",
"description": "Showing zap amount in USD @ rate"
@ -282,9 +297,15 @@
"feZ/kG": {
"defaultMessage": "Login with Private Key (insecure)"
},
"gzsn7k": {
"defaultMessage": "{n} messages"
},
"hGQqkW": {
"defaultMessage": "Schedule"
},
"hMzcSq": {
"defaultMessage": "Messages"
},
"heyxZL": {
"defaultMessage": "Enable text to speech"
},
@ -300,6 +321,9 @@
"izWS4J": {
"defaultMessage": "Unfollow"
},
"jgOqxt": {
"defaultMessage": "Widgets"
},
"jr4+vD": {
"defaultMessage": "Markdown"
},

View File

@ -1,7 +1,8 @@
import "./alerts.css";
import Spinner from "element/spinner";
import { useStreamLink } from "hooks/stream-link";
import { useParams } from "react-router-dom";
import Spinner from "@/element/spinner";
import { useStreamLink } from "@/hooks/stream-link";
import { ZapAlerts } from "./widgets/zaps";
import { Views } from "./widgets/views";
import { TopZappersWidget } from "./widgets/top-zappers";

View File

@ -1,11 +1,12 @@
import "./chat-popout.css";
import { LiveChat } from "element/live-chat";
import { useParams } from "react-router-dom";
import { NostrPrefix, encodeTLV, parseNostrLink } from "@snort/system";
import { unwrap } from "@snort/shared";
import { useCurrentStreamFeed } from "hooks/current-stream-feed";
import { findTag } from "utils";
import { useZapGoal } from "hooks/goals";
import { LiveChat } from "@/element/live-chat";
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
import { findTag } from "@/utils";
import { useZapGoal } from "@/hooks/goals";
export function ChatPopout() {
const params = useParams();

View File

@ -34,7 +34,7 @@ header {
}
header .logo {
background: url("public/logo.png") no-repeat #171717;
background: url("/logo.png") no-repeat #171717;
background-size: cover;
border-radius: 16px;
width: 48px;
@ -148,7 +148,7 @@ header .profile img {
.age-check::after {
content: " ";
background: url("public/zap-stream.svg") no-repeat;
background: url("/zap-stream.svg") no-repeat;
background-position: center;
background-size: contain;
position: absolute;

View File

@ -8,14 +8,14 @@ import { FormattedMessage } from "react-intl";
import { Menu, MenuItem } from "@szhsin/react-menu";
import { hexToBech32 } from "@snort/shared";
import { Icon } from "element/icon";
import { useLogin, useLoginEvents } from "hooks/login";
import { Profile } from "element/profile";
import { NewStreamDialog } from "element/new-stream";
import { LoginSignup } from "element/login-signup";
import { Login } from "index";
import { useLang } from "hooks/lang";
import { AllLocales } from "intl";
import { Icon } from "@/element/icon";
import { useLogin, useLoginEvents } from "@/hooks/login";
import { Profile } from "@/element/profile";
import { NewStreamDialog } from "@/element/new-stream";
import { LoginSignup } from "@/element/login-signup";
import { Login } from "@/index";
import { useLang } from "@/hooks/lang";
import { AllLocales } from "@/intl";
export function LayoutPage() {
const navigate = useNavigate();
@ -76,19 +76,19 @@ export function LayoutPage() {
gap={5}>
<MenuItem onClick={() => navigate(`/p/${hexToBech32("npub", login.pubkey)}`)}>
<Icon name="user" size={24} />
<FormattedMessage defaultMessage="Profile" />
<FormattedMessage defaultMessage="Profile" id="itPgxd" />
</MenuItem>
<MenuItem onClick={() => navigate("/settings")}>
<Icon name="settings" size={24} />
<FormattedMessage defaultMessage="Settings" />
<FormattedMessage defaultMessage="Settings" id="D3idYv" />
</MenuItem>
<MenuItem onClick={() => navigate("/widgets")}>
<Icon name="widget" size={24} />
<FormattedMessage defaultMessage="Widgets" />
<FormattedMessage defaultMessage="Widgets" id="jgOqxt" />
</MenuItem>
<MenuItem onClick={() => Login.logout()}>
<Icon name="logout" size={24} />
<FormattedMessage defaultMessage="Logout" />
<FormattedMessage defaultMessage="Logout" id="C81/uG" />
</MenuItem>
</Menu>
</>
@ -105,7 +105,7 @@ export function LayoutPage() {
return (
<Dialog.Root open={showLogin} onOpenChange={setShowLogin}>
<button type="button" className="btn btn-border" onClick={handleLogin}>
<FormattedMessage defaultMessage="Login" />
<FormattedMessage defaultMessage="Login" id="AyGauy" />
<Icon name="login" />
</button>
<Dialog.Portal>

View File

@ -1,26 +1,26 @@
import "./profile-page.css";
import { useMemo } from "react";
import moment from "moment";
import { useNavigate, useParams } from "react-router-dom";
import * as Tabs from "@radix-ui/react-tabs";
import { parseNostrLink, NostrPrefix, ParsedZap, encodeTLV } from "@snort/system";
import { NostrPrefix, ParsedZap, encodeTLV, parseNostrLink } from "@snort/system";
import { useUserProfile } from "@snort/system-react";
import { unwrap } from "@snort/shared";
import { Profile } from "element/profile";
import { Icon } from "element/icon";
import { SendZapsDialog } from "element/send-zap";
import { VideoTile } from "element/video-tile";
import { FollowButton } from "element/follow-button";
import { MuteButton } from "element/mute-button";
import { useProfile } from "hooks/profile";
import useTopZappers from "hooks/top-zappers";
import usePlaceholder from "hooks/placeholders";
import { Text } from "element/text";
import { StreamState } from "index";
import { findTag } from "utils";
import { formatSats } from "number";
import { FormattedMessage } from "react-intl";
import { Profile } from "@/element/profile";
import { Icon } from "@/element/icon";
import { SendZapsDialog } from "@/element/send-zap";
import { VideoTile } from "@/element/video-tile";
import { FollowButton } from "@/element/follow-button";
import { MuteButton } from "@/element/mute-button";
import { useProfile } from "@/hooks/profile";
import useTopZappers from "@/hooks/top-zappers";
import usePlaceholder from "@/hooks/placeholders";
import { Text } from "@/element/text";
import { StreamState } from "@/index";
import { findTag } from "@/utils";
import { formatSats } from "@/number";
function Zapper({ pubkey, total }: { pubkey: string; total: number }) {
return (
<div className="zapper">
@ -92,12 +92,12 @@ export function ProfilePage() {
<div className="live-button pill live" onClick={goToLive}>
<Icon name="signal" />
<span>
<FormattedMessage defaultMessage="live" />
<FormattedMessage defaultMessage="live" id="2CGh/0" />
</span>
</div>
) : (
<span className="pill offline">
<FormattedMessage defaultMessage="offline" />
<FormattedMessage defaultMessage="offline" id="K3uH1C" />
</span>
)}
</div>
@ -111,7 +111,7 @@ export function ProfilePage() {
<div className="zap-button">
<Icon name="zap-filled" className="zap-button-icon" />
<span>
<FormattedMessage defaultMessage="Zap" />
<FormattedMessage defaultMessage="Zap" id="fBI91o" />
</span>
</div>
</button>
@ -133,15 +133,15 @@ export function ProfilePage() {
<Tabs.Root className="tabs-root" defaultValue="top-zappers">
<Tabs.List className="tabs-list" aria-label={`Information about ${profile ? profile.name : link.id}`}>
<Tabs.Trigger className="tabs-tab" value="top-zappers">
<FormattedMessage defaultMessage="Top Zappers" />
<FormattedMessage defaultMessage="Top Zappers" id="dVD/AR" />
<div className="tab-border"></div>
</Tabs.Trigger>
<Tabs.Trigger className="tabs-tab" value="past-streams">
<FormattedMessage defaultMessage="Past Streams" />
<FormattedMessage defaultMessage="Past Streams" id="UfSot5" />
<div className="tab-border"></div>
</Tabs.Trigger>
<Tabs.Trigger className="tabs-tab" value="schedule">
<FormattedMessage defaultMessage="Schedule" />
<FormattedMessage defaultMessage="Schedule" id="hGQqkW" />
<div className="tab-border"></div>
</Tabs.Trigger>
</Tabs.List>
@ -156,8 +156,9 @@ export function ProfilePage() {
<span className="timestamp">
<FormattedMessage
defaultMessage="Streamed on {date}"
id="cvAsEh"
values={{
date: moment(Number(ev.created_at) * 1000).format("MMM DD, YYYY"),
date: new Date(ev.created_at * 1000).toLocaleDateString(),
}}
/>
</span>
@ -173,8 +174,9 @@ export function ProfilePage() {
<span className="timestamp">
<FormattedMessage
defaultMessage="Scheduled for {date}"
id="pO/lPX"
values={{
date: moment(Number(ev.created_at) * 1000).format("MMM DD, YYYY h:mm:ss a"),
date: new Date(ev.created_at * 1000).toLocaleDateString(),
}}
/>
</span>

View File

@ -1,9 +1,9 @@
import "./index.css";
import { StreamProviders } from "providers";
import Owncast from "owncast.png";
import Cloudflare from "cloudflare.png";
import { useNavigate, useParams } from "react-router-dom";
import { StreamProviders } from "@/providers";
import Owncast from "@/owncast.png";
import Cloudflare from "@/cloudflare.png";
import { ConfigureOwncast } from "./owncast";
import { ConfigureNostrType } from "./nostr";

View File

@ -1,13 +1,13 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import AsyncButton from "element/async-button";
import { StatePill } from "element/state-pill";
import { StreamState } from "index";
import { StreamProviderInfo, StreamProviderStore } from "providers";
import { Nip103StreamProvider } from "providers/zsz";
import { FormattedMessage } from "react-intl";
import AsyncButton from "@/element/async-button";
import { StatePill } from "@/element/state-pill";
import { StreamState } from "@/index";
import { StreamProviderInfo, StreamProviderStore } from "@/providers";
import { Nip103StreamProvider } from "@/providers/zsz";
export function ConfigureNostrType() {
const [url, setUrl] = useState("");
const [info, setInfo] = useState<StreamProviderInfo>();
@ -61,7 +61,7 @@ export function ConfigureNostrType() {
StreamProviderStore.add(new Nip103StreamProvider(new URL(url).host, url));
navigate("/");
}}>
<FormattedMessage defaultMessage="Save" />
<FormattedMessage defaultMessage="Save" id="jvo0vs" />
</button>
</div>
</>
@ -78,7 +78,7 @@ export function ConfigureNostrType() {
</div>
</div>
<AsyncButton className="btn btn-primary" onClick={tryConnect}>
<FormattedMessage defaultMessage="Connect" />
<FormattedMessage defaultMessage="Connect" id="+vVZ/G" />
</AsyncButton>
</div>
<div>{status()}</div>

View File

@ -1,11 +1,12 @@
import AsyncButton from "element/async-button";
import { StatePill } from "element/state-pill";
import { StreamState } from "index";
import { StreamProviderInfo, StreamProviderStore } from "providers";
import { OwncastProvider } from "providers/owncast";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import AsyncButton from "@/element/async-button";
import { StatePill } from "@/element/state-pill";
import { StreamState } from "@/index";
import { StreamProviderInfo, StreamProviderStore } from "@/providers";
import { OwncastProvider } from "@/providers/owncast";
export function ConfigureOwncast() {
const [url, setUrl] = useState("");
const [token, setToken] = useState("");

View File

@ -3,10 +3,10 @@ import { FormattedMessage } from "react-intl";
import { useCallback } from "react";
import type { NostrEvent } from "@snort/system";
import { VideoTile } from "element/video-tile";
import { useLogin } from "hooks/login";
import { getHost, getTagValues } from "utils";
import { useStreamsFeed } from "hooks/live-streams";
import { VideoTile } from "@/element/video-tile";
import { useLogin } from "@/hooks/login";
import { getHost, getTagValues } from "@/utils";
import { useStreamsFeed } from "@/hooks/live-streams";
export function RootPage() {
const login = useLogin();
@ -33,7 +33,7 @@ export function RootPage() {
{hasFollowingLive && (
<>
<h2 className="divider line one-line">
<FormattedMessage defaultMessage="Following" />
<FormattedMessage defaultMessage="Following" id="cPIKU2" />
</h2>
<div className="video-grid">
{following.map(e => (
@ -70,7 +70,7 @@ export function RootPage() {
{hasFollowingLive && liveNow.length > 0 && (
<>
<h2 className="divider line one-line">
<FormattedMessage defaultMessage="Live" />
<FormattedMessage defaultMessage="Live" id="Dn82AL" />
</h2>
<div className="video-grid">
{liveNow
@ -84,7 +84,7 @@ export function RootPage() {
{plannedEvents.length > 0 && (
<>
<h2 className="divider line one-line">
<FormattedMessage defaultMessage="Planned" />
<FormattedMessage defaultMessage="Planned" id="kp0NPF" />
</h2>
<div className="video-grid">
{plannedEvents.map(e => (
@ -96,7 +96,7 @@ export function RootPage() {
{endedEvents.length > 0 && (
<>
<h2 className="divider line one-line">
<FormattedMessage defaultMessage="Ended" />
<FormattedMessage defaultMessage="Ended" id="TP/cMX" />
</h2>
<div className="video-grid">
{endedEvents.map(e => (

View File

@ -5,8 +5,8 @@ import { FormattedMessage } from "react-intl";
import { Button as AlbyZapsButton } from "@getalby/bitcoin-connect-react";
import { hexToBech32 } from "@snort/shared";
import { useLogin } from "hooks/login";
import Copy from "element/copy";
import { useLogin } from "@/hooks/login";
import Copy from "@/element/copy";
const enum Tab {
Account,
@ -30,12 +30,12 @@ export function SettingsPage() {
return (
<>
<h1>
<FormattedMessage defaultMessage="Account" />
<FormattedMessage defaultMessage="Account" id="TwyMau" />
</h1>
{login?.pubkey && (
<div className="public-key">
<p>
<FormattedMessage defaultMessage="Logged in as" />
<FormattedMessage defaultMessage="Logged in as" id="DZKuuP" />
</p>
<Copy text={hexToBech32("npub", login.pubkey)} />
</div>
@ -43,13 +43,13 @@ export function SettingsPage() {
{login?.privateKey && (
<div className="private-key">
<p>
<FormattedMessage defaultMessage="Private key" />
<FormattedMessage defaultMessage="Private key" id="Bep/gA" />
</p>
<Copy text={hexToBech32("nsec", login.privateKey)} />
</div>
)}
<h1>
<FormattedMessage defaultMessage="Zaps" />
<FormattedMessage defaultMessage="Zaps" id="OEW7yJ" />
</h1>
<AlbyZapsButton />
</>
@ -61,12 +61,12 @@ export function SettingsPage() {
<div className="settings-page">
<div className="flex f-col g48">
<h1>
<FormattedMessage defaultMessage="Settings" />
<FormattedMessage defaultMessage="Settings" id="D3idYv" />
</h1>
<div className="flex g24 f-col-mobile">
<div className="flex f-col g24 tab-options">
<div onClick={() => setTab(Tab.Account)}>
<FormattedMessage defaultMessage="Account" />
<FormattedMessage defaultMessage="Account" id="TwyMau" />
</div>
</div>
<div className="tab-content">{tabContent()}</div>

View File

@ -2,31 +2,31 @@ import "./stream-page.css";
import { NostrLink, TaggedNostrEvent } from "@snort/system";
import { useLocation, useNavigate } from "react-router-dom";
import { Helmet } from "react-helmet";
import { LiveVideoPlayer } from "element/live-video-player";
import { findTag, getEventFromLocationState, getHost } from "utils";
import { Profile, getName } from "element/profile";
import { LiveChat } from "element/live-chat";
import AsyncButton from "element/async-button";
import { useLogin } from "hooks/login";
import { useZapGoal } from "hooks/goals";
import { StreamState } from "index";
import { SendZapsDialog } from "element/send-zap";
import { NostrEvent } from "@snort/system";
import { SnortContext, useUserProfile } from "@snort/system-react";
import { NewStreamDialog } from "element/new-stream";
import { Tags } from "element/tags";
import { StatePill } from "element/state-pill";
import { StreamCards } from "element/stream-cards";
import { formatSats } from "number";
import { StreamTimer } from "element/stream-time";
import { ShareMenu } from "element/share-menu";
import { ContentWarningOverlay, isContentWarningAccepted } from "element/content-warning";
import { useCurrentStreamFeed } from "hooks/current-stream-feed";
import { useStreamLink } from "hooks/stream-link";
import { FormattedMessage } from "react-intl";
import { useContext } from "react";
import { FollowButton } from "element/follow-button";
import { LiveVideoPlayer } from "@/element/live-video-player";
import { findTag, getEventFromLocationState, getHost } from "@/utils";
import { Profile, getName } from "@/element/profile";
import { LiveChat } from "@/element/live-chat";
import AsyncButton from "@/element/async-button";
import { useLogin } from "@/hooks/login";
import { useZapGoal } from "@/hooks/goals";
import { StreamState } from "@/index";
import { SendZapsDialog } from "@/element/send-zap";
import { NewStreamDialog } from "@/element/new-stream";
import { Tags } from "@/element/tags";
import { StatePill } from "@/element/state-pill";
import { StreamCards } from "@/element/stream-cards";
import { formatSats } from "@/number";
import { StreamTimer } from "@/element/stream-time";
import { ShareMenu } from "@/element/share-menu";
import { ContentWarningOverlay, isContentWarningAccepted } from "@/element/content-warning";
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
import { useStreamLink } from "@/hooks/stream-link";
import { FollowButton } from "@/element/follow-button";
function ProfileInfo({ ev, goal }: { ev?: NostrEvent; goal?: TaggedNostrEvent }) {
const system = useContext(SnortContext);
@ -60,7 +60,7 @@ function ProfileInfo({ ev, goal }: { ev?: NostrEvent; goal?: TaggedNostrEvent })
<StatePill state={status as StreamState} />
{viewers > 0 && (
<span className="pill viewers">
<FormattedMessage defaultMessage="{n} viewers" values={{ n: formatSats(viewers) }} />
<FormattedMessage defaultMessage="{n} viewers" id="3adEeb" values={{ n: formatSats(viewers) }} />
</span>
)}
{status === StreamState.Live && (
@ -74,7 +74,7 @@ function ProfileInfo({ ev, goal }: { ev?: NostrEvent; goal?: TaggedNostrEvent })
<div className="actions">
{ev && <NewStreamDialog text="Edit" ev={ev} btnClassName="btn" />}
<AsyncButton type="button" className="btn btn-warning" onClick={deleteStream}>
<FormattedMessage defaultMessage="Delete" />
<FormattedMessage defaultMessage="Delete" id="K3r6DQ" />
</AsyncButton>
</div>
)}

View File

@ -6,15 +6,15 @@ import { useEventReactions } from "@snort/system-react";
import { FormattedDate, FormattedMessage, FormattedNumber } from "react-intl";
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
import { LIVE_STREAM_CHAT } from "const";
import { Profile } from "element/profile";
import { StatePill } from "element/state-pill";
import { useCurrentStreamFeed } from "hooks/current-stream-feed";
import { useLiveChatFeed } from "hooks/live-chat";
import { useStreamLink } from "hooks/stream-link";
import { StreamState } from "index";
import { formatSats } from "number";
import { findTag, getEventFromLocationState } from "utils";
import { LIVE_STREAM_CHAT } from "@/const";
import { Profile } from "@/element/profile";
import { StatePill } from "@/element/state-pill";
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
import { useLiveChatFeed } from "@/hooks/live-chat";
import { useStreamLink } from "@/hooks/stream-link";
import { StreamState } from "@/index";
import { formatSats } from "@/number";
import { findTag, getEventFromLocationState } from "@/utils";
export function StreamSummaryPage() {
const location = useLocation();

View File

@ -2,9 +2,9 @@ import "./tag.css";
import { useParams } from "react-router-dom";
import { unwrap } from "@snort/shared";
import { VideoTile } from "element/video-tile";
import { FollowTagButton } from "element/follow-button";
import { useStreamsFeed } from "hooks/live-streams";
import { VideoTile } from "@/element/video-tile";
import { FollowTagButton } from "@/element/follow-button";
import { useStreamsFeed } from "@/hooks/live-streams";
export function TagPage() {
const { tag } = useParams();

View File

@ -1,13 +1,13 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import "./widgets.css";
import { useState, useMemo } from "react";
import { useIntl, FormattedMessage } from "react-intl";
import { useMemo, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { NostrLink, NostrPrefix } from "@snort/system";
import Copy from "element/copy";
import { useCurrentStreamFeed } from "hooks/current-stream-feed";
import { getVoices, speak, toTextToSpeechParams } from "text2speech";
import { useLogin } from "hooks/login";
import Copy from "@/element/copy";
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
import { getVoices, speak, toTextToSpeechParams } from "@/text2speech";
import { useLogin } from "@/hooks/login";
import { ZapAlertItem } from "./widgets/zaps";
import { TopZappersWidget } from "./widgets/top-zappers";
import { Views } from "./widgets/views";
@ -64,7 +64,7 @@ function ZapAlertConfiguration({ npub, baseUrl }: ZapAlertConfigurationProps) {
return (
<>
<h3>
<FormattedMessage defaultMessage="Zap Alert" />
<FormattedMessage defaultMessage="Zap Alert" id="zVDHAu" />
</h3>
<Copy text={`${baseUrl}/alert/${npub}/zaps${query}`} />
<ZapAlertItem
@ -91,13 +91,13 @@ function ZapAlertConfiguration({ npub, baseUrl }: ZapAlertConfigurationProps) {
checked={textToSpeech}
onChange={ev => setTextToSpeech(ev.target.checked)}
/>
<FormattedMessage defaultMessage="Enable text to speech" />
<FormattedMessage defaultMessage="Enable text to speech" id="heyxZL" />
</div>
{isTextToSpeechEnabled && (
<>
<div className="paper labeled-input">
<label htmlFor="minimum-sats">
<FormattedMessage defaultMessage="Minimum amount for text to speech" />
<FormattedMessage defaultMessage="Minimum amount for text to speech" id="6pr6hJ" />
</label>
<input
id="minimum-sats"
@ -109,7 +109,7 @@ function ZapAlertConfiguration({ npub, baseUrl }: ZapAlertConfigurationProps) {
</div>
<div className="paper labeled-input">
<label htmlFor="volume">
<FormattedMessage defaultMessage="Volume" />
<FormattedMessage defaultMessage="Volume" id="y867Vs" />
</label>
<input
id="volume"
@ -123,11 +123,11 @@ function ZapAlertConfiguration({ npub, baseUrl }: ZapAlertConfigurationProps) {
</div>
<div className="paper labeled-input">
<label htmlFor="voice-selector">
<FormattedMessage defaultMessage="Voice" />
<FormattedMessage defaultMessage="Voice" id="mnJYBQ" />
</label>
<select id="voice-selector" onChange={ev => setVoice(ev.target.value)}>
<option value="">
<FormattedMessage defaultMessage="Select voice..." />
<FormattedMessage defaultMessage="Select voice..." id="wMKVFz" />
</option>
{languages.map(l => (
<optgroup label={formatDisplayName(l, { type: "language" })}>
@ -142,17 +142,17 @@ function ZapAlertConfiguration({ npub, baseUrl }: ZapAlertConfigurationProps) {
<>
<div className="paper labeled-input">
<label htmlFor="zap-alert-text">
<FormattedMessage defaultMessage="Zap message" />
<FormattedMessage defaultMessage="Zap message" id="sInm1h" />
</label>
<textarea
id="zap-alert-text"
placeholder={formatMessage({ defaultMessage: "Insert text to speak" })}
placeholder={formatMessage({ defaultMessage: "Insert text to speak", id: "8YT6ja" })}
value={testText}
onChange={ev => setTestText(ev.target.value)}
/>
</div>
<button disabled={testText.length === 0} className="btn" onClick={testVoice}>
<FormattedMessage defaultMessage="Test voice" />
<FormattedMessage defaultMessage="Test voice" id="d5zWyh" />
</button>
</>
)}
@ -176,7 +176,7 @@ export function WidgetsPage() {
<div className="widgets g8">
<div className="flex f-col g8">
<h3>
<FormattedMessage defaultMessage="Chat Widget" />
<FormattedMessage defaultMessage="Chat Widget" id="hpl4BP" />
</h3>
<Copy text={`${baseUrl}/chat/${npub}`} />
</div>
@ -185,21 +185,21 @@ export function WidgetsPage() {
</div>
<div className="flex f-col g8">
<h3>
<FormattedMessage defaultMessage="Top Zappers" />
<FormattedMessage defaultMessage="Top Zappers" id="dVD/AR" />
</h3>
<Copy text={`${baseUrl}/alert/${npub}/top-zappers`} />
{currentLink && <TopZappersWidget link={currentLink} />}
</div>
<div className="flex f-col g8">
<h3>
<FormattedMessage defaultMessage="Current Viewers" />
<FormattedMessage defaultMessage="Current Viewers" id="rgsbu9" />
</h3>
<Copy text={`${baseUrl}/alert/${npub}/views`} />
{currentLink && <Views link={currentLink} />}
</div>
<div className="flex f-col g8">
<h3>
<FormattedMessage defaultMessage="Music" />
<FormattedMessage defaultMessage="Music" id="79lLl+" />
</h3>
<Copy text={`${baseUrl}/alert/${npub}/music`} />
{currentLink && <Music link={currentLink} />}

View File

@ -1,8 +1,9 @@
import { NostrLink } from "@snort/system";
import { unixNow } from "@snort/shared";
import { useCurrentStreamFeed } from "hooks/current-stream-feed";
import { useStatus } from "hooks/status";
import { getHost, findTag } from "utils";
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
import { useStatus } from "@/hooks/status";
import { findTag, getHost } from "@/utils";
export function Music({ link }: { link: NostrLink }) {
const currentEvent = useCurrentStreamFeed(link, true);

View File

@ -1,16 +1,17 @@
import { NostrLink } from "@snort/system";
import { TopZappers } from "element/top-zappers";
import { useCurrentStreamFeed } from "hooks/current-stream-feed";
import { useZaps } from "hooks/zaps";
import { FormattedMessage } from "react-intl";
import { TopZappers } from "@/element/top-zappers";
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
import { useZaps } from "@/hooks/zaps";
export function TopZappersWidget({ link }: { link: NostrLink }) {
const currentEvent = useCurrentStreamFeed(link, true);
const zaps = useZaps(currentEvent ? NostrLink.fromEvent(currentEvent) : undefined, true);
return (
<div className="top-zappers-widget">
<div>
<FormattedMessage defaultMessage="Top Zappers" />
<FormattedMessage defaultMessage="Top Zappers" id="dVD/AR" />
</div>
<div className="flex g8">
<TopZappers zaps={zaps} limit={3} />

View File

@ -1,7 +1,8 @@
import { NostrLink } from "@snort/system";
import { useCurrentStreamFeed } from "hooks/current-stream-feed";
import { FormattedMessage } from "react-intl";
import { findTag } from "utils";
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
import { findTag } from "@/utils";
export function Views({ link }: { link: NostrLink }) {
const current = useCurrentStreamFeed(link, true);
@ -11,9 +12,9 @@ export function Views({ link }: { link: NostrLink }) {
return (
<div className="views">
{isNaN(n) ? (
<FormattedMessage defaultMessage="No viewer data available" />
<FormattedMessage defaultMessage="No viewer data available" id="AukrPM" />
) : (
<FormattedMessage defaultMessage="{n} viewers" values={{ n }} />
<FormattedMessage defaultMessage="{n} viewers" id="3adEeb" values={{ n }} />
)}
</div>
);

View File

@ -1,16 +1,16 @@
import { useMemo, useState, useEffect } from "react";
import { useEffect, useMemo, useState } from "react";
import { FormattedMessage } from "react-intl";
import { hexToBech32 } from "@snort/shared";
import { NostrLink, ParsedZap } from "@snort/system";
import { useUserProfile } from "@snort/system-react";
import { useCurrentStreamFeed } from "hooks/current-stream-feed";
import { useZaps } from "hooks/zaps";
import { useMutedPubkeys } from "hooks/lists";
import { formatSats } from "number";
import { useTextToSpeechParams, getVoices, speak } from "text2speech";
import { FormattedMessage } from "react-intl";
import { getHost } from "utils";
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
import { useZaps } from "@/hooks/zaps";
import { useMutedPubkeys } from "@/hooks/lists";
import { formatSats } from "@/number";
import { getVoices, speak, useTextToSpeechParams } from "@/text2speech";
import { getHost } from "@/utils";
function useZapQueue(zapStream: ParsedZap[], zapTime = 10_000) {
const zaps = useMemo(() => {
@ -76,11 +76,12 @@ export function ZapAlertItem({ item }: { item: ParsedZap }) {
<>
<div className="zap-alert">
<div className="zap-alert-title">
<FormattedMessage defaultMessage="Incoming Zap" />
<FormattedMessage defaultMessage="Incoming Zap" id="tG1ST3" />
</div>
<div className="zap-alert-header">
<FormattedMessage
defaultMessage="{name} with {amount}"
id="Qe1MJu"
values={{
name: (
<span className="highlight">

View File

@ -1,4 +1,4 @@
import { StreamState } from "index";
import { StreamState } from "@/index";
import { NostrEvent, SystemInterface } from "@snort/system";
import { ExternalStore } from "@snort/shared";
import { Nip103StreamProvider } from "./zsz";

View File

@ -1,5 +1,5 @@
import { NostrEvent, SystemInterface } from "@snort/system";
import { StreamProvider, StreamProviderInfo, StreamProviders } from "providers";
import { StreamProvider, StreamProviderInfo, StreamProviders } from "@/providers";
export class ManualProvider implements StreamProvider {
get name(): string {

View File

@ -1,5 +1,5 @@
import { StreamState } from "index";
import { StreamProvider, StreamProviderInfo, StreamProviders } from "providers";
import { StreamState } from "@/index";
import { StreamProvider, StreamProviderInfo, StreamProviders } from "@/providers";
export class OwncastProvider implements StreamProvider {
#url: string;

View File

@ -7,9 +7,9 @@ import {
StreamProviders,
} from ".";
import { EventKind, EventPublisher, NostrEvent, SystemInterface } from "@snort/system";
import { Login, StreamState } from "index";
import { getPublisher } from "login";
import { findTag } from "utils";
import { Login, StreamState } from "@/index";
import { getPublisher } from "@/login";
import { findTag } from "@/utils";
export class Nip103StreamProvider implements StreamProvider {
#publisher?: EventPublisher;

View File

@ -1,7 +1,7 @@
import { NostrEvent, NostrLink, TaggedNostrEvent } from "@snort/system";
import type { Tags } from "types";
import { LIVE_STREAM } from "const";
import type { Tags } from "@/types";
import { LIVE_STREAM } from "@/const";
export function toAddress(e: NostrEvent): string {
if (e.kind && e.kind >= 30000 && e.kind <= 40000) {

View File

@ -1,6 +1,6 @@
import adapter from "webrtc-adapter";
import { CandidateInfo, SDPInfo } from "semantic-sdp";
import { TypedEventTarget, type StatusEvent, type LogEvent } from "./events";
import { type LogEvent, type StatusEvent, TypedEventTarget } from "./events";
import { parserLinkHeader } from "./parser";
export const DEFAULT_ICE_SERVERS = ["stun:stun.cloudflare.com:3478", "stun:stun.l.google.com:19302"];

Some files were not shown because too many files have changed in this diff Show More