Compare commits

...

22 Commits

Author SHA1 Message Date
7cba67e4c1
nip5 handles 2023-08-23 14:14:38 +01:00
c191a7684a
fix CSP for fonts 2023-08-22 22:54:10 +01:00
0e590feab4
formatting 2023-08-22 22:50:26 +01:00
16c54185bb
cleanup 2023-08-22 22:48:37 +01:00
d071736d4c Merge pull request 'Host fonts locally to limit user tracking by Google' (#82) from florian/stream:host-fonts-locally into main
Reviewed-on: Kieran/stream#82
Reviewed-by: Kieran <kieran@noreply.localhost>
2023-08-22 21:12:25 +00:00
f7517d7e1a Merge pull request 'Additional Zap amounts + larger text input for chat' (#83) from TheGrinder/zapstream:main into main
Reviewed-on: Kieran/stream#83
Reviewed-by: Kieran <kieran@noreply.localhost>
2023-08-22 21:09:36 +00:00
TheGrinder
9741eb6a83 change write-message from 32px to 40px to display
two lines of typed text instead of one.
2023-08-21 12:22:22 +02:00
TheGrinder
cb94afdc16 modified css for added zap amounts 2023-08-21 01:40:32 +02:00
TheGrinder
5a24671839 add additional zap amounts 2023-08-21 01:19:05 +02:00
6b328a1fde chore: Host fonts locally to limit tracking by google 2023-08-14 11:06:13 +02:00
5584eaea0a
Fix deploy cmd 2023-08-06 16:10:41 +01:00
d449c4aa57
Fix logged out mutelist 2023-08-06 16:03:44 +01:00
325e4be2ef
Redirect from non-xxx host 2023-08-06 15:36:50 +01:00
d44e4c1a94
Open NSFW streams on xxx host 2023-08-06 15:35:39 +01:00
b9f0d349ef
Define XXX 2023-08-06 15:02:09 +01:00
e607df56df Merge pull request 'feat: add following title' (#78) from following-title into main
Reviewed-on: Kieran/stream#78
Reviewed-by: Kieran <kieran@noreply.localhost>
2023-08-06 14:01:26 +00:00
c74b0e5b42 Merge pull request 'fix: only show awards from stream start' (#77) from awards-times into main
Reviewed-on: Kieran/stream#77
Reviewed-by: Kieran <kieran@noreply.localhost>
2023-08-06 14:00:49 +00:00
877be3d6b6 Merge pull request 'feat: in-chat mute button' (#76) from in-chat-mute into main
Reviewed-on: Kieran/stream#76
Reviewed-by: Kieran <kieran@noreply.localhost>
2023-08-06 13:58:37 +00:00
68218c5912 fix: zap content link color 2023-08-04 19:42:18 +02:00
7a0491f3b7 feat: add following title 2023-08-04 19:34:40 +02:00
443b1b1738 fix: only show awards from stream start
resolves #74
2023-08-04 18:51:49 +02:00
78d9a2779a feat: in-chat mute button
resolves #73
2023-08-04 18:29:03 +02:00
53 changed files with 564 additions and 274 deletions

View File

@ -22,4 +22,4 @@ steps:
volumes:
- name: cache
claim:
name: docker-cache
name: docker-cache

View File

@ -1,14 +1,19 @@
module.exports = {
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint"],
root: true,
ignorePatterns: ["build/", "*.test.ts", "*.js"],
env: {
browser: true,
worker: true,
commonjs: true,
node: false,
},
};
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint"],
root: true,
ignorePatterns: ["build/", "*.test.ts", "*.js"],
env: {
browser: true,
worker: true,
commonjs: true,
node: false,
},
rules: {
"@typescript-eslint/no-non-null-assertion": "error",
"require-await": "error",
eqeqeq: "error",
"object-shorthand": "warn",
},
};

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

@ -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

@ -1,29 +1,31 @@
#!/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";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = createRequire(absPnpApiPath);
const moduleWrapper = tsserver => {
const moduleWrapper = (tsserver) => {
if (!process.versions.pnp) {
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 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
@ -31,7 +33,11 @@ const moduleWrapper = tsserver => {
function toEditorPath(str) {
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) {
if (
isAbsolute(str) &&
!str.match(/^\^?(zip:|\/zip\/)/) &&
(str.match(/\.zip\//) || isVirtual(str))
) {
// We also take the opportunity to turn virtual paths into physical ones;
// this makes it much easier to work with workspaces that list peer
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
@ -45,7 +51,11 @@ 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;
}
}
@ -73,41 +83,55 @@ 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` ? `` : `/`);
@ -119,26 +143,35 @@ 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;
}
}
@ -150,8 +183,9 @@ 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);
};
@ -161,12 +195,13 @@ 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 (
@ -177,10 +212,12 @@ 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) {
@ -194,21 +231,31 @@ const moduleWrapper = tsserver => {
}
}
const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => {
return typeof value === 'string' ? fromEditorPath(value) : value;
});
const processedMessageJSON = JSON.stringify(
parsedMessage,
(key, value) => {
return typeof value === "string" ? fromEditorPath(value) : value;
}
);
return originalOnMessage.call(
this,
isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON)
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,29 +1,31 @@
#!/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";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = createRequire(absPnpApiPath);
const moduleWrapper = tsserver => {
const moduleWrapper = (tsserver) => {
if (!process.versions.pnp) {
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 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
@ -31,7 +33,11 @@ const moduleWrapper = tsserver => {
function toEditorPath(str) {
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) {
if (
isAbsolute(str) &&
!str.match(/^\^?(zip:|\/zip\/)/) &&
(str.match(/\.zip\//) || isVirtual(str))
) {
// We also take the opportunity to turn virtual paths into physical ones;
// this makes it much easier to work with workspaces that list peer
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
@ -45,7 +51,11 @@ 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;
}
}
@ -73,41 +83,55 @@ 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` ? `` : `/`);
@ -119,26 +143,35 @@ 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;
}
}
@ -150,8 +183,9 @@ 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);
};
@ -161,12 +195,13 @@ 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 (
@ -177,10 +212,12 @@ 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) {
@ -194,21 +231,31 @@ const moduleWrapper = tsserver => {
}
}
const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => {
return typeof value === 'string' ? fromEditorPath(value) : value;
});
const processedMessageJSON = JSON.stringify(
parsedMessage,
(key, value) => {
return typeof value === "string" ? fromEditorPath(value) : value;
}
);
return originalOnMessage.call(
this,
isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON)
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";

View File

@ -1,2 +1,2 @@
/*
Content-Security-Policy: default-src 'self'; manifest-src *; child-src 'none'; worker-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; connect-src *; img-src * data: blob:; font-src https://fonts.gstatic.com; media-src * blob:; script-src 'self';
Content-Security-Policy: default-src 'self'; manifest-src *; child-src 'none'; worker-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; connect-src *; img-src * data: blob:; font-src 'self'; media-src * blob:; script-src 'self';

View File

@ -52,7 +52,8 @@
"scripts": {
"start": "webpack serve",
"build": "webpack --node-env=production",
"deploy": "npx wrangler pages publish 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"
},
"eslintConfig": {
"extends": [

View File

@ -85,5 +85,8 @@
<symbol id="face-content" viewBox="0 0 24 24" fill="none">
<path d="M8 14C8 14 9.5 16 12 16C14.5 16 16 14 16 14M17 9.24C16.605 9.725 16.065 10 15.5 10C14.935 10 14.41 9.725 14 9.24M10 9.24C9.605 9.725 9.065 10 8.5 10C7.935 10 7.41 9.725 7 9.24M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</symbol>
<symbol id="user-x" viewBox="0 0 24 24" fill="none">
<path d="M16.5 16L21.5 21M21.5 16L16.5 21M15.5 3.29076C16.9659 3.88415 18 5.32131 18 7C18 8.67869 16.9659 10.1159 15.5 10.7092M12 15H8C6.13623 15 5.20435 15 4.46927 15.3045C3.48915 15.7105 2.71046 16.4892 2.30448 17.4693C2 18.2044 2 19.1362 2 21M13.5 7C13.5 9.20914 11.7091 11 9.5 11C7.29086 11 5.5 9.20914 5.5 7C5.5 4.79086 7.29086 3 9.5 3C11.7091 3 13.5 4.79086 13.5 7Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</symbol>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -1,21 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Nostr live streaming" />
<link rel="apple-touch-icon" href="/logo.png" />
<link rel="icon" href="/favicon.ico" />
<link rel="manifest" href="/manifest.json" />
<title>zap.stream</title>
</head>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Nostr live streaming" />
<link rel="apple-touch-icon" href="/logo.png" />
<link rel="icon" href="/favicon.ico" />
<link rel="manifest" href="/manifest.json" />
<title>zap.stream</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<div id="root"></div>
</body>
<body>
<div id="root"></div>
</body>
</html>

View File

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

View File

@ -1,5 +1,9 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/// <reference types="@webbtc/webln-types" />
declare const __XXX: boolean;
declare const __XXX_HOST: string;
declare module "*.jpg" {
const value: unknown;
export default value;

View File

@ -13,6 +13,7 @@ import { Icon } from "element/icon";
import { Emoji as EmojiComponent } from "element/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";
@ -55,6 +56,7 @@ export function ChatMessage({
const emojiRef = useRef(null);
const isTablet = useMediaQuery("(max-width: 1020px)");
const isHovering = useHover(ref);
const { mute } = useMute(ev.pubkey);
const [showZapDialog, setShowZapDialog] = useState(false);
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
const login = useLogin();
@ -62,6 +64,8 @@ export function ChatMessage({
System,
inView?.isIntersecting ? ev.pubkey : undefined
);
const shouldShowMuteButton =
ev.pubkey !== streamer && ev.pubkey !== login?.pubkey;
const zapTarget = profile?.lud16 ?? profile?.lud06;
const zaps = useMemo(() => {
return reactions
@ -107,8 +111,8 @@ export function ChatMessage({
const pub = login?.publisher();
if (emoji.native) {
reply = await pub?.react(ev, emoji.native || "+1");
} else {
const e = getEmojiById(emoji.id!);
} else if (emoji.id) {
const e = getEmojiById(emoji.id);
if (e) {
reply = await pub?.generic((eb) => {
return eb
@ -116,7 +120,7 @@ export function ChatMessage({
.content(`:${emoji.id}:`)
.tag(["e", ev.id])
.tag(["p", ev.pubkey])
.tag(["emoji", e.at(1)!, e.at(2)!]);
.tag(["emoji", e[1], e[2]]);
});
}
}
@ -132,11 +136,16 @@ export function ChatMessage({
const topOffset = ref.current?.getBoundingClientRect().top;
const leftOffset = ref.current?.getBoundingClientRect().left;
function pickEmoji(ev: React.MouseEvent) {
ev.stopPropagation();
function pickEmoji(e: React.MouseEvent) {
e.stopPropagation();
setShowEmojiPicker(!showEmojiPicker);
}
function muteUser(e: React.MouseEvent) {
e.stopPropagation();
mute();
}
return (
<>
<div
@ -185,7 +194,7 @@ export function ChatMessage({
<div className="message-reaction-container">
{isCustomEmojiReaction && emoji ? (
<span className="message-reaction">
<EmojiComponent name={emoji.at(1)!} url={emoji.at(2)!} />
<EmojiComponent name={emoji[1]} url={emoji[2]} />
</span>
) : (
<span className="message-reaction">{e}</span>
@ -229,6 +238,11 @@ export function ChatMessage({
<button className="message-zap-button" onClick={pickEmoji}>
<Icon name="face" className="message-zap-button-icon" />
</button>
{shouldShowMuteButton && (
<button className="message-zap-button" onClick={muteUser}>
<Icon name="user-x" className="message-zap-button-icon" />
</button>
)}
</div>
)}
</div>

View File

@ -12,7 +12,9 @@ export function LoggedInFollowButton({
value: string;
}) {
const login = useLogin();
const { tags, content, timestamp } = login!.follows;
if (!login) return;
const { tags, content, timestamp } = login.follows;
const follows = tags.filter((t) => t.at(0) === tag);
const isFollowing = follows.find((t) => t.at(1) === value);

View File

@ -52,7 +52,7 @@
}
.live-chat > .write-message > div:nth-child(1) {
height: 32px;
height: 40px;
flex-grow: 1;
}
@ -111,6 +111,10 @@
flex-wrap: wrap;
}
.live-chat .zap-content a {
color: var(--text-link);
}
.top-zappers {
display: flex;
flex-direction: column;
@ -321,8 +325,6 @@
text-transform: lowercase;
color: #fff;
font-size: 12px;
font-family: Outfit;
font-style: normal;
font-weight: 500;
line-height: 18px;
}

View File

@ -8,6 +8,7 @@ import {
parseZap,
encodeTLV,
} from "@snort/system";
import { unixNow, unwrap } from "@snort/shared";
import { useEffect, useMemo } from "react";
import uniqBy from "lodash.uniqby";
@ -28,7 +29,7 @@ import { useLogin } from "hooks/login";
import useTopZappers from "hooks/top-zappers";
import { useAddress } from "hooks/event";
import { formatSats } from "number";
import { LIVE_STREAM_CHAT } from "const";
import { WEEK, LIVE_STREAM_CHAT } from "const";
import { findTag, getTagValues, getHost } from "utils";
import { System } from "index";
@ -91,16 +92,20 @@ export function LiveChat({
height?: number;
}) {
const host = getHost(ev);
const { badges, awards } = useBadges(host);
const feed = useLiveChatFeed(link, goal ? [goal.id] : undefined);
const login = useLogin();
useEffect(() => {
const pubkeys = [
...new Set(feed.zaps.flatMap((a) => [a.pubkey, findTag(a, "p")!])),
...new Set(feed.zaps.flatMap((a) => [a.pubkey, unwrap(findTag(a, "p"))])),
];
System.ProfileLoader.TrackMetadata(pubkeys);
return () => System.ProfileLoader.UntrackMetadata(pubkeys);
}, [feed.zaps]);
const started = useMemo(() => {
const starts = findTag(ev, "starts");
return starts ? Number(starts) : unixNow() - WEEK;
}, [ev]);
const { badges, awards } = useBadges(host, started);
const mutedPubkeys = useMemo(() => {
return new Set(getTagValues(login?.muted.tags ?? [], "p"));
}, [login]);

View File

@ -1,13 +1,17 @@
import { useMemo } from "react";
import { useLogin } from "hooks/login";
import AsyncButton from "element/async-button";
import { Login, System } from "index";
import { MUTED } from "const";
export function LoggedInMuteButton({ pubkey }: { pubkey: string }) {
export function useMute(pubkey: string) {
const login = useLogin();
const { tags, content } = login!.muted;
const muted = tags.filter((t) => t.at(0) === "p");
const isMuted = muted.find((t) => t.at(1) === pubkey);
const { tags, content } = login?.muted ?? { tags: [] };
const muted = useMemo(() => tags.filter((t) => t.at(0) === "p"), [tags]);
const isMuted = useMemo(
() => muted.find((t) => t.at(1) === pubkey),
[pubkey, muted]
);
async function unmute() {
const pub = login?.publisher();
@ -43,6 +47,12 @@ export function LoggedInMuteButton({ pubkey }: { pubkey: string }) {
}
}
return { isMuted, mute, unmute };
}
export function LoggedInMuteButton({ pubkey }: { pubkey: string }) {
const { isMuted, mute, unmute } = useMute(pubkey);
return (
<AsyncButton
type="button"

View File

@ -7,8 +7,9 @@ import { StreamProvider, StreamProviders } from "providers";
import { useEffect, useState } from "react";
import { StreamEditor, StreamEditorProps } from "./stream-editor";
import { useNavigate } from "react-router-dom";
import { eventLink } from "utils";
import { eventLink, findTag } from "utils";
import { NostrProviderDialog } from "./nostr-provider-dialog";
import { unwrap } from "@snort/shared";
function NewStream({ ev, onFinish }: StreamEditorProps) {
const providers = useStreamProvider();
@ -19,7 +20,7 @@ function NewStream({ ev, onFinish }: StreamEditorProps) {
if (!currentProvider) {
setCurrentProvider(
ev !== undefined
? providers.find((a) => a.name.toLowerCase() === "manual")!
? unwrap(providers.find((a) => a.name.toLowerCase() === "manual"))
: providers.at(0)
);
}
@ -35,9 +36,17 @@ function NewStream({ ev, onFinish }: StreamEditorProps) {
onFinish={(ex) => {
currentProvider.updateStreamInfo(ex);
if (!ev) {
navigate(`/${eventLink(ex)}`, {
state: ev,
});
if (
findTag(ex, "content-warning") &&
__XXX_HOST &&
__XXX === false
) {
location.href = `${__XXX_HOST}/${eventLink(ex)}`;
} else {
navigate(`/${eventLink(ex)}`, {
state: ev,
});
}
} else {
onFinish?.(ev);
}

View File

@ -6,7 +6,7 @@
.send-zap .amounts {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-columns: repeat(6, 1fr);
justify-content: space-evenly;
gap: 8px;
}

View File

@ -43,10 +43,11 @@ export function SendZaps({
targetName,
onFinish,
}: SendZapsProps) {
const UsdRate = 30_000;
const UsdRate = 28_000;
const satsAmounts = [
100, 1_000, 5_000, 10_000, 50_000, 100_000, 500_000, 1_000_000,
21, 69, 121, 221, 420, 1_000, 2_100, 5_000, 6_666, 10_000, 21_000, 42_000,
69_000, 100_000, 210_000, 500_000, 1_000_000,
];
const usdAmounts = [0.05, 0.5, 2, 5, 10, 50, 100, 200];
const [isFiat, setIsFiat] = useState(false);

View File

@ -1,9 +1,11 @@
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 { Icon } from "./icon";
import { useState } from "react";
import { Textarea } from "./textarea";
import { NostrEvent, NostrPrefix, encodeTLV } from "@snort/system";
import { findTag } from "utils";
import AsyncButton from "./async-button";
import { useLogin } from "hooks/login";
@ -18,7 +20,7 @@ export function ShareMenu({ ev }: { ev: NostrEvent }) {
const naddr = encodeTLV(
NostrPrefix.Address,
findTag(ev, "d")!,
unwrap(findTag(ev, "d")),
undefined,
ev.kind,
ev.pubkey

View File

@ -100,7 +100,7 @@ function Card({ canEdit, ev, cards }: CardProps) {
);
function findTagByIdentifier(d: string) {
return tags.find((t) => t.at(1)!.endsWith(`:${d}`));
return tags.find((t) => t[1].endsWith(`:${d}`));
}
const [dropStyle, dropRef] = useDrop(
@ -293,7 +293,7 @@ function EditCard({ card, cards }: EditCardProps) {
async function onCancel() {
const pub = login?.publisher();
if (pub) {
const newTags = tags.filter((t) => !t.at(1)!.endsWith(`:${identifier}`));
const newTags = tags.filter((t) => !t[1].endsWith(`:${identifier}`));
const userCardsEv = await pub.generic((eb) => {
eb.kind(USER_CARDS).content("");
for (const tag of newTags) {
@ -408,7 +408,7 @@ export function StreamCardEditor({ pubkey, tags }: StreamCardEditorProps) {
<>
<div className="stream-cards">
{cards.map((ev) => (
<Card canEdit={isEditing} cards={cards} key={ev.id} ev={ev!} />
<Card canEdit={isEditing} cards={cards} key={ev.id} ev={ev} />
))}
{isEditing && <AddCard cards={cards} />}
</div>
@ -433,7 +433,7 @@ export function ReadOnlyStreamCards({ host }: StreamCardsProps) {
return (
<div className="stream-cards">
{cards.map((ev) => (
<Card cards={cards} key={ev!.id} ev={ev!} />
<Card cards={cards} key={ev.id} ev={ev} />
))}
</div>
);

View File

@ -53,11 +53,11 @@ export function Textarea({ emojis, ...props }: TextareaProps) {
const userDataProvider = async (token: string) => {
const cache = System.ProfileLoader.Cache;
if (cache instanceof UserProfileCache) {
return cache.search(token);
return await cache.search(token);
}
};
const emojiDataProvider = async (token: string) => {
const emojiDataProvider = (token: string) => {
const results = emojis
.map((t) => {
return {

View File

@ -0,0 +1,84 @@
/* latin-ext */
@font-face {
font-family: "Outfit";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(outfit_400_latin-ext.woff2) format("woff2");
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF,
U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: "Outfit";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(outfit_400_latin.woff2) format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: "Outfit";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(outfit_500_latin-ext.woff2) format("woff2");
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF,
U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: "Outfit";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(outfit_500_latin.woff2) format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: "Outfit";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(outfit_600_latin-ext.woff2) format("woff2");
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF,
U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: "Outfit";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(outfit_600_latin.woff2) format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: "Outfit";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(outfit_700_latin-ext.woff2) format("woff2");
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF,
U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: "Outfit";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(outfit_700_latin.woff2) format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -7,18 +7,16 @@ import {
RequestBuilder,
} from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
import { unixNow } from "@snort/shared";
import { findTag, toAddress, getTagValues } from "utils";
import { WEEK } from "const";
import { System } from "index";
import type { Badge } from "types";
export function useBadges(
pubkey: string,
since: number,
leaveOpen = true
): { badges: Badge[]; awards: TaggedRawEvent[] } {
const since = useMemo(() => unixNow() - WEEK, [pubkey]);
const rb = useMemo(() => {
const rb = new RequestBuilder(`badges:${pubkey.slice(0, 12)}`);
rb.withOptions({ leaveOpen });

View File

@ -29,7 +29,7 @@ export function useUserCards(
const subRelated = useMemo(() => {
if (!pubkey) return null;
const splitted = related.map((t) => t.at(1)!.split(":"));
const splitted = related.map((t) => t[1].split(":"));
const authors = splitted
.map((s) => s.at(1))
.filter((s) => s)
@ -58,7 +58,7 @@ export function useUserCards(
const cards = useMemo(() => {
return related
.map((t) => {
const [k, pubkey, identifier] = t.at(1)!.split(":");
const [k, pubkey, identifier] = t[1].split(":");
const kind = Number(k);
return (data ?? []).find(
(e) =>
@ -104,7 +104,7 @@ export function useCards(pubkey: string, leaveOpen = false): TaggedRawEvent[] {
const subRelated = useMemo(() => {
if (!pubkey) return null;
const splitted = related.map((t) => t.at(1)!.split(":"));
const splitted = related.map((t) => t[1].split(":"));
const authors = splitted
.map((s) => s.at(1))
.filter((s) => s)
@ -134,7 +134,7 @@ export function useCards(pubkey: string, leaveOpen = false): TaggedRawEvent[] {
const cards = useMemo(() => {
return related
.map((t) => {
const [k, pubkey, identifier] = t.at(1)!.split(":");
const [k, pubkey, identifier] = t[1].split(":");
const kind = Number(k);
return cardEvents.find(
(e) =>

View File

@ -45,7 +45,7 @@ export function useUserEmojiPacks(pubkey?: string, userEmoji?: Tags) {
const subRelated = useMemo(() => {
if (!pubkey) return null;
const splitted = related.map((t) => t.at(1)!.split(":"));
const splitted = related.map((t) => t[1].split(":"));
const authors = splitted
.map((s) => s.at(1))
.filter((s) => s)

View File

@ -9,6 +9,7 @@ import {
parseZap,
} from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
import { unwrap } from "@snort/shared";
import { GOAL } from "const";
import { System } from "index";
@ -36,14 +37,15 @@ export function useZaps(goal: NostrEvent, leaveOpen = false) {
);
}
export function useZapGoal(host: string, link: NostrLink, leaveOpen = false) {
export function useZapGoal(host: string, link?: NostrLink, leaveOpen = false) {
const sub = useMemo(() => {
if (!link) return null;
const b = new RequestBuilder(`goals:${host.slice(0, 12)}`);
b.withOptions({ leaveOpen });
b.withFilter()
.kinds([GOAL])
.authors([host])
.tag("a", [`${link.kind}:${link.author!}:${link.id}`]);
.tag("a", [`${link.kind}:${unwrap(link.author)}:${link.id}`]);
return b;
}, [link, leaveOpen]);

View File

@ -37,22 +37,32 @@ export function useStreamsFeed(tag?: string) {
const feed = useRequestBuilder<NoteCollection>(System, NoteCollection, rb);
const feedSorted = useMemo(() => {
if (feed.data) {
return [...feed.data];
if (__XXX) {
return [...feed.data].filter(
(a) => findTag(a, "content-warning") !== undefined
);
} else {
return [...feed.data].filter(
(a) => findTag(a, "content-warning") === undefined
);
}
}
return [];
}, [feed.data]);
const live = feedSorted.filter(
(a) => findTag(a, "status") === StreamState.Live
).sort(sortStarts);
const planned = feedSorted.filter(
(a) => findTag(a, "status") === StreamState.Planned
).sort(sortStarts);
const ended = feedSorted.filter((a) => {
const hasEnded = findTag(a, "status") === StreamState.Ended;
const recording = findTag(a, "recording") ?? "";
return hasEnded && recording?.length > 0;
}).sort(sortCreatedAt);
const live = feedSorted
.filter((a) => findTag(a, "status") === StreamState.Live)
.sort(sortStarts);
const planned = feedSorted
.filter((a) => findTag(a, "status") === StreamState.Planned)
.sort(sortStarts);
const ended = feedSorted
.filter((a) => {
const hasEnded = findTag(a, "status") === StreamState.Ended;
const recording = findTag(a, "recording") ?? "";
return hasEnded && recording?.length > 0;
})
.sort(sortCreatedAt);
return { live, planned, ended };
}

View File

@ -1,5 +1,6 @@
import "@szhsin/react-menu/dist/index.css";
import "./index.css";
import "./fonts/outfit/outfit.css";
import React from "react";
import ReactDOM from "react-dom/client";
@ -10,7 +11,7 @@ import { RootPage } from "pages/root";
import { TagPage } from "pages/tag";
import { LayoutPage } from "pages/layout";
import { ProfilePage } from "pages/profile-page";
import { StreamPage } from "pages/stream-page";
import { StreamPageHandler } from "pages/stream-page";
import { ChatPopout } from "pages/chat-popout";
import { LoginStore } from "login";
import { StreamProvidersPage } from "pages/providers";
@ -53,7 +54,7 @@ const router = createBrowserRouter([
},
{
path: "/:id",
element: <StreamPage />,
element: <StreamPageHandler />,
},
{
path: "/providers/:id?",

View File

@ -1,6 +1,6 @@
import { bytesToHex } from "@noble/curves/abstract/utils";
import { schnorr } from "@noble/curves/secp256k1";
import { ExternalStore } from "@snort/shared";
import { ExternalStore, unwrap } from "@snort/shared";
import { EventPublisher, Nip7Signer, PrivateKeySigner } from "@snort/system";
import type { EmojiPack, Tags } from "types";
@ -10,7 +10,7 @@ export enum LoginType {
}
interface ReplaceableTags {
tags: Array<string[]>;
tags: Tags;
content?: string;
timestamp: number;
}
@ -131,7 +131,7 @@ export function getPublisher(session: LoginSession) {
}
case LoginType.PrivateKey: {
return new EventPublisher(
new PrivateKeySigner(session.privateKey!),
new PrivateKeySigner(unwrap(session.privateKey)),
session.pubkey
);
}

View File

@ -2,12 +2,13 @@ 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";
export function ChatPopout() {
const params = useParams();
const link = parseNostrLink(params.id!);
const link = parseNostrLink(unwrap(params.id));
const ev = useCurrentStreamFeed(link, true);
const lnk = parseNostrLink(

View File

@ -10,6 +10,7 @@ import {
encodeTLV,
} 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";
@ -52,7 +53,7 @@ const defaultBanner = "https://void.cat/d/Hn1AdN5UKmceuDkgDW847q.webp";
export function ProfilePage() {
const navigate = useNavigate();
const params = useParams();
const link = parseNostrLink(params.npub!);
const link = parseNostrLink(unwrap(params.npub));
const placeholder = usePlaceholder(link.id);
const profile = useUserProfile(System, link.id);
const zapTarget = profile?.lud16 ?? profile?.lud06;

View File

@ -38,6 +38,12 @@
display: flex;
}
@media (max-width: 1020px) {
.divider {
padding: 0 8px;
}
}
.divider:after {
content: "";
flex: 1;

View File

@ -32,11 +32,14 @@ export function RootPage() {
return (
<div className="homepage">
{hasFollowingLive && (
<div className="video-grid">
{following.map((e) => (
<VideoTile ev={e} key={e.id} />
))}
</div>
<>
<h2 className="divider line one-line">Following</h2>
<div className="video-grid">
{following.map((e) => (
<VideoTile ev={e} key={e.id} />
))}
</div>
</>
)}
{!hasFollowingLive && (
<div className="video-grid">

View File

@ -1,5 +1,6 @@
import "./stream-page.css";
import { parseNostrLink, TaggedRawEvent } from "@snort/system";
import { NostrLink, NostrPrefix, TaggedRawEvent, tryParseNostrLink } from "@snort/system";
import { fetchNip05Pubkey } from "@snort/shared";
import { useLocation, useNavigate, useParams } from "react-router-dom";
import { Helmet } from "react-helmet";
@ -9,6 +10,7 @@ import {
findTag,
getEventFromLocationState,
getHost,
hexToBech32,
} from "utils";
import { Profile, getName } from "element/profile";
import { LiveChat } from "element/live-chat";
@ -31,6 +33,7 @@ import {
isContentWarningAccepted,
} from "element/content-warning";
import { useCurrentStreamFeed } from "hooks/current-stream-feed";
import { useEffect, useState } from "react";
function ProfileInfo({ ev, goal }: { ev?: NostrEvent; goal?: TaggedRawEvent }) {
const login = useLogin();
@ -110,14 +113,41 @@ function ProfileInfo({ ev, goal }: { ev?: NostrEvent; goal?: TaggedRawEvent }) {
);
}
export function StreamPage() {
export function StreamPageHandler() {
const params = useParams();
const location = useLocation();
const evPreload = getEventFromLocationState(location.state);
const link = parseNostrLink(params.id!);
const [link, setLink] = useState<NostrLink>();
useEffect(() => {
if (params.id) {
const parsedLink = tryParseNostrLink(params.id);
if (parsedLink) {
setLink(parsedLink);
} else {
const [handle, domain] = (params.id.includes("@") ? params.id : `${params.id}@zap.stream`).split("@");
fetchNip05Pubkey(handle, domain).then(d => {
if (d) {
setLink({
id: d,
type: NostrPrefix.PublicKey,
encode: () => hexToBech32(NostrPrefix.PublicKey, d)
} as NostrLink);
}
})
}
}
}, [params.id]);
if (link) {
return <StreamPage link={link} evPreload={evPreload} />
}
}
export function StreamPage({ link, evPreload }: { evPreload?: NostrEvent, link: NostrLink }) {
const ev = useCurrentStreamFeed(link, true, evPreload);
const host = getHost(ev);
const goal = useZapGoal(host, link, true);
const goal = useZapGoal(host, createNostrLink(ev), true);
const title = findTag(ev, "title");
const summary = findTag(ev, "summary");

View File

@ -1,5 +1,6 @@
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";
@ -12,7 +13,7 @@ export function TagPage() {
<div className="tag-page">
<div className="tag-page-header">
<h1>#{tag}</h1>
<FollowTagButton tag={tag!} />
<FollowTagButton tag={unwrap(tag)} />
</div>
<div className="video-grid">
{live.map((e) => (

View File

@ -58,7 +58,7 @@ export class OwncastProvider implements StreamProvider {
body?: unknown
): Promise<T> {
const rsp = await fetch(`${this.#url}${path}`, {
method: method,
method,
body: body ? JSON.stringify(body) : undefined,
headers: {
"content-type": "application/json",

View File

@ -103,7 +103,7 @@ export class Nip103StreamProvider implements StreamProvider {
.tag(["method", method]);
});
const rsp = await fetch(u, {
method: method,
method,
body: body ? JSON.stringify(body) : undefined,
headers: {
"content-type": "application/json",

View File

@ -109,7 +109,7 @@ export function getHost(ev?: NostrEvent) {
);
}
export async function openFile(): Promise<File | undefined> {
export function openFile(): Promise<File | undefined> {
return new Promise((resolve) => {
const elm = document.createElement("input");
elm.type = "file";

View File

@ -503,7 +503,7 @@ export class WISH extends TypedEventTarget {
},
});
const body = await resp.text();
if (resp.status != 201) {
if (resp.status !== 201) {
throw new Error(`Unexpected status code ${resp.status}: ${body}`);
}
@ -611,7 +611,7 @@ export class WISH extends TypedEventTarget {
throw new Error(`Unexpected status code ${resp.status}: ${body}`);
}
async WithEndpoint(endpoint: string, trickle: boolean) {
WithEndpoint(endpoint: string, trickle: boolean) {
if (endpoint === "") {
throw new Error("Endpoint cannot be empty");
}
@ -637,7 +637,7 @@ export class WISH extends TypedEventTarget {
method: "DELETE",
mode: "cors",
});
if (resp.status != 200) {
if (resp.status !== 200) {
const body = await resp.text();
throw new Error(`Unexpected status code ${resp.status}: ${body}`);
}

View File

@ -56,7 +56,7 @@ const config = {
new ESLintPlugin({
extensions: ["js", "mjs", "jsx", "ts", "tsx"],
eslintPath: require.resolve("eslint"),
failOnError: true,
failOnError: !isProduction,
cache: true,
}),
new MiniCssExtractPlugin({
@ -65,7 +65,11 @@ const config = {
new webpack.IgnorePlugin({
resourceRegExp: /^\.\/locale$/,
contextRegExp: /moment$/,
})
}),
new webpack.DefinePlugin({
__XXX: process.env["__XXX"] || JSON.stringify(false),
__XXX_HOST: JSON.stringify("https://xxzap.com"),
}),
],
module: {
rules: [
@ -85,9 +89,12 @@ const config = {
babelrc: false,
configFile: false,
presets: [
["@babel/preset-env", {
targets: "defaults"
}],
[
"@babel/preset-env",
{
targets: "defaults",
},
],
["@babel/preset-react", { runtime: "automatic" }],
"@babel/preset-typescript",
],
@ -102,7 +109,7 @@ const config = {
],
},
},
require.resolve("ts-loader")
require.resolve("ts-loader"),
],
},
{
@ -144,7 +151,7 @@ const config = {
aliasFields: ["browser"],
extensions: ["...", ".tsx", ".ts", ".jsx", ".js"],
modules: ["...", __dirname, path.resolve(__dirname, "src")],
fallback: { "crypto": false }
fallback: { crypto: false },
},
};