145 changed files with 1 additions and 23430 deletions
@ -1,44 +0,0 @@ |
|||||||
// eslint-plugin-svelte |
|
||||||
// https://sveltejs.github.io/eslint-plugin-svelte/user-guide/ |
|
||||||
{ |
|
||||||
"root": true, |
|
||||||
"extends": [ |
|
||||||
// add more generic rule sets here, such as: |
|
||||||
// 'eslint:recommended', |
|
||||||
"plugin:@typescript-eslint/recommended", |
|
||||||
"prettier", |
|
||||||
"plugin:svelte/recommended" |
|
||||||
], |
|
||||||
"parser": "@typescript-eslint/parser", |
|
||||||
"parserOptions": { |
|
||||||
// ... |
|
||||||
"project": "./tsconfig.json", |
|
||||||
"extraFileExtensions": [".svelte"] // This is a required setting in `@typescript-eslint/parser` v4.24.0. |
|
||||||
}, |
|
||||||
"plugins": ["@typescript-eslint", "prettier"], |
|
||||||
"rules": { |
|
||||||
"no-console": 1, |
|
||||||
"prettier/prettier": 2, |
|
||||||
"comma-dangle": [2, "always-multiline"], |
|
||||||
"no-alert": "off", |
|
||||||
"no-unused-vars": "off", |
|
||||||
"@typescript-eslint/no-unused-vars": [ |
|
||||||
"error", |
|
||||||
{ |
|
||||||
"argsIgnorePattern": "^_", |
|
||||||
"varsIgnorePattern": "^_" |
|
||||||
} |
|
||||||
] |
|
||||||
// 'svelte/rule-name': 'error' |
|
||||||
}, |
|
||||||
"overrides": [ |
|
||||||
{ |
|
||||||
"files": ["*.svelte"], |
|
||||||
"parser": "svelte-eslint-parser", |
|
||||||
// Parse the `<script>` in `.svelte` as TypeScript by adding the following configuration. |
|
||||||
"parserOptions": { |
|
||||||
"parser": "@typescript-eslint/parser" |
|
||||||
} |
|
||||||
} |
|
||||||
] |
|
||||||
} |
|
||||||
@ -1,22 +0,0 @@ |
|||||||
.DS_Store |
|
||||||
node_modules |
|
||||||
/build |
|
||||||
/.svelte-kit |
|
||||||
/package |
|
||||||
.direnv |
|
||||||
.env |
|
||||||
.env.* |
|
||||||
!.env.example |
|
||||||
vite.config.js.timestamp-* |
|
||||||
vite.config.ts.timestamp-* |
|
||||||
/result |
|
||||||
storybook-static |
|
||||||
yarn-error.log |
|
||||||
tmp |
|
||||||
.pnp.* |
|
||||||
.yarn/* |
|
||||||
!.yarn/patches |
|
||||||
!.yarn/plugins |
|
||||||
!.yarn/releases |
|
||||||
!.yarn/sdks |
|
||||||
!.yarn/versions |
|
||||||
@ -1,9 +0,0 @@ |
|||||||
[general] |
|
||||||
regex-style-search=true |
|
||||||
|
|
||||||
# this ignore all body rules if there is a nostr link rather than just this line |
|
||||||
# until https://github.com/jorisroovers/gitlint/issues/255#issuecomment-1040868243 |
|
||||||
# is implemented with [ignore-body-lines] this skips validation of other body line lengths |
|
||||||
[ignore-by-body] |
|
||||||
regex = ^(nostr:[^\s]*|https://[^\s]*)$ |
|
||||||
ignore = body-max-line-length |
|
||||||
@ -1,16 +0,0 @@ |
|||||||
{ |
|
||||||
"trailingComma": "es5", |
|
||||||
"tabWidth": 2, |
|
||||||
"semi": false, |
|
||||||
"singleQuote": true, |
|
||||||
"printWidth": 80, |
|
||||||
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], |
|
||||||
"overrides": [ |
|
||||||
{ |
|
||||||
"files": "*.eslintrc", |
|
||||||
"options": { |
|
||||||
"trailingComma": "none" |
|
||||||
} |
|
||||||
} |
|
||||||
] |
|
||||||
} |
|
||||||
@ -1,21 +0,0 @@ |
|||||||
import type { StorybookConfig } from "@storybook/sveltekit"; |
|
||||||
|
|
||||||
const config: StorybookConfig = { |
|
||||||
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx|svelte)"], |
|
||||||
addons: [ |
|
||||||
"@storybook/addon-svelte-csf", |
|
||||||
"@storybook/addon-links", |
|
||||||
"@storybook/addon-essentials", |
|
||||||
"@storybook/addon-interactions", |
|
||||||
"@storybook/addon-a11y" |
|
||||||
], |
|
||||||
framework: { |
|
||||||
name: "@storybook/sveltekit", |
|
||||||
options: {}, |
|
||||||
}, |
|
||||||
staticDirs: ['test-assets'], |
|
||||||
docs: { |
|
||||||
autodocs: "tag", |
|
||||||
}, |
|
||||||
}; |
|
||||||
export default config; |
|
||||||
@ -1,16 +0,0 @@ |
|||||||
import type { Preview } from "@storybook/svelte"; |
|
||||||
import '../src/app.css' |
|
||||||
const preview: Preview = { |
|
||||||
parameters: { |
|
||||||
backgrounds: { default: 'dark' }, |
|
||||||
actions: { argTypesRegex: "^on[A-Z].*" }, |
|
||||||
controls: { |
|
||||||
matchers: { |
|
||||||
color: /(background|color)$/i, |
|
||||||
date: /Date$/, |
|
||||||
}, |
|
||||||
}, |
|
||||||
}, |
|
||||||
}; |
|
||||||
|
|
||||||
export default preview; |
|
||||||
|
Before Width: | Height: | Size: 76 KiB |
@ -1,31 +0,0 @@ |
|||||||
import type { TestRunnerConfig } from "@storybook/test-runner"; |
|
||||||
import { toMatchImageSnapshot } from 'jest-image-snapshot'; |
|
||||||
|
|
||||||
const customSnapshotsDir = `${process.cwd()}/__snapshots__`; |
|
||||||
|
|
||||||
|
|
||||||
const config: TestRunnerConfig = { |
|
||||||
setup() { |
|
||||||
expect.extend({ toMatchImageSnapshot }); |
|
||||||
}, |
|
||||||
async postVisit(page, context) { |
|
||||||
// DOM Snapshot
|
|
||||||
const elementHandler = await page.$('#storybook-root'); |
|
||||||
if (elementHandler) { |
|
||||||
const innerHTML = await elementHandler.innerHTML(); |
|
||||||
expect(innerHTML).toMatchSnapshot(); |
|
||||||
} |
|
||||||
else throw "cannot find storybook DOM root to take DOM screenshot" |
|
||||||
// Image Snapshop
|
|
||||||
const image = await page.screenshot(); |
|
||||||
expect(image).toMatchImageSnapshot({ |
|
||||||
customSnapshotsDir, |
|
||||||
customSnapshotIdentifier: context.id, |
|
||||||
comparisonMethod: 'ssim', |
|
||||||
failureThresholdType: 'percent', |
|
||||||
failureThreshold: 0.002, |
|
||||||
}); |
|
||||||
}, |
|
||||||
}; |
|
||||||
|
|
||||||
export default config; |
|
||||||
@ -1,6 +0,0 @@ |
|||||||
{ |
|
||||||
"recommendations": [ |
|
||||||
"svelte.svelte-vscode", |
|
||||||
"bradlc.vscode-tailwindcss" |
|
||||||
] |
|
||||||
} |
|
||||||
@ -1,13 +0,0 @@ |
|||||||
{ |
|
||||||
"eslint.validate": ["javascript", "javascriptreact", "svelte"], |
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode", |
|
||||||
"diffEditor.ignoreTrimWhitespace": false, |
|
||||||
"editor.formatOnPaste": true, |
|
||||||
"editor.formatOnSave": true, |
|
||||||
"files.associations": { |
|
||||||
"*.css": "tailwindcss" |
|
||||||
}, |
|
||||||
"editor.codeActionsOnSave": { |
|
||||||
"source.fixAll": "always" |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,5 +1,3 @@ |
|||||||
# GitCitadel Online |
# GitCitadel Online |
||||||
|
|
||||||
Repo containing the basic webpages for our [GitCitadel project](https://github.com/ShadowySupercode), including the Alexandria reader and publisher, the server monitoring page, and the possibility to submit PRs and issues to our software projects over [ngit events](https://gitcitadel.eu/ngit). |
Repo containing the basic webpages for our [GitCitadel company](https://gitcitadel.com). |
||||||
|
|
||||||
Feel free to contact us over [Nostr](https://nosta.me/nprofile1qqsggm4l0xs23qfjwnkfwf6fqcs66s3lz637gaxhl4nwd2vtle8rnfqprfmhxue69uhhg6r9vehhyetnwshxummnw3erztnrdaksz8mhwden5te0dehhxarj9ejkjmn4dej85ampdeaxjeewwdcxzcm9j3xeaa) or email us at [support@gitcitadel.eu](mailto:support@gitcitadel.eu). |
|
||||||
@ -1,82 +0,0 @@ |
|||||||
{ |
|
||||||
"nodes": { |
|
||||||
"flake-utils": { |
|
||||||
"inputs": { |
|
||||||
"systems": "systems" |
|
||||||
}, |
|
||||||
"locked": { |
|
||||||
"lastModified": 1726560853, |
|
||||||
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", |
|
||||||
"owner": "numtide", |
|
||||||
"repo": "flake-utils", |
|
||||||
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", |
|
||||||
"type": "github" |
|
||||||
}, |
|
||||||
"original": { |
|
||||||
"owner": "numtide", |
|
||||||
"repo": "flake-utils", |
|
||||||
"type": "github" |
|
||||||
} |
|
||||||
}, |
|
||||||
"gitignore": { |
|
||||||
"inputs": { |
|
||||||
"nixpkgs": [ |
|
||||||
"nixpkgs" |
|
||||||
] |
|
||||||
}, |
|
||||||
"locked": { |
|
||||||
"lastModified": 1709087332, |
|
||||||
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", |
|
||||||
"owner": "hercules-ci", |
|
||||||
"repo": "gitignore.nix", |
|
||||||
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394", |
|
||||||
"type": "github" |
|
||||||
}, |
|
||||||
"original": { |
|
||||||
"owner": "hercules-ci", |
|
||||||
"repo": "gitignore.nix", |
|
||||||
"type": "github" |
|
||||||
} |
|
||||||
}, |
|
||||||
"nixpkgs": { |
|
||||||
"locked": { |
|
||||||
"lastModified": 1726463316, |
|
||||||
"narHash": "sha256-gI9kkaH0ZjakJOKrdjaI/VbaMEo9qBbSUl93DnU7f4c=", |
|
||||||
"owner": "NixOS", |
|
||||||
"repo": "nixpkgs", |
|
||||||
"rev": "99dc8785f6a0adac95f5e2ab05cc2e1bf666d172", |
|
||||||
"type": "github" |
|
||||||
}, |
|
||||||
"original": { |
|
||||||
"owner": "NixOS", |
|
||||||
"ref": "nixos-unstable", |
|
||||||
"repo": "nixpkgs", |
|
||||||
"type": "github" |
|
||||||
} |
|
||||||
}, |
|
||||||
"root": { |
|
||||||
"inputs": { |
|
||||||
"flake-utils": "flake-utils", |
|
||||||
"gitignore": "gitignore", |
|
||||||
"nixpkgs": "nixpkgs" |
|
||||||
} |
|
||||||
}, |
|
||||||
"systems": { |
|
||||||
"locked": { |
|
||||||
"lastModified": 1681028828, |
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", |
|
||||||
"owner": "nix-systems", |
|
||||||
"repo": "default", |
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", |
|
||||||
"type": "github" |
|
||||||
}, |
|
||||||
"original": { |
|
||||||
"owner": "nix-systems", |
|
||||||
"repo": "default", |
|
||||||
"type": "github" |
|
||||||
} |
|
||||||
} |
|
||||||
}, |
|
||||||
"root": "root", |
|
||||||
"version": 7 |
|
||||||
} |
|
||||||
@ -1,64 +0,0 @@ |
|||||||
{ |
|
||||||
inputs = { |
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; |
|
||||||
flake-utils.url = "github:numtide/flake-utils"; |
|
||||||
gitignore = { |
|
||||||
url = "github:hercules-ci/gitignore.nix"; |
|
||||||
inputs.nixpkgs.follows = "nixpkgs"; |
|
||||||
}; |
|
||||||
}; |
|
||||||
|
|
||||||
outputs = |
|
||||||
{ nixpkgs |
|
||||||
, gitignore |
|
||||||
, flake-utils |
|
||||||
, ... |
|
||||||
}: |
|
||||||
flake-utils.lib.eachDefaultSystem ( |
|
||||||
system: |
|
||||||
let |
|
||||||
pkgs = nixpkgs.legacyPackages.${system}; |
|
||||||
packageJSON = pkgs.lib.importJSON ./package.json; |
|
||||||
gitignoreSource = gitignore.lib.gitignoreSource; |
|
||||||
in |
|
||||||
{ |
|
||||||
packages = rec { |
|
||||||
site-src = pkgs.mkYarnPackage rec { |
|
||||||
name = "${packageJSON.name}-site-${version}"; |
|
||||||
version = packageJSON.version; |
|
||||||
src = gitignoreSource ./.; |
|
||||||
packageJson = "${src}/package.json"; |
|
||||||
yarnLock = "${src}/yarn.lock"; |
|
||||||
buildPhase = '' |
|
||||||
yarn --offline build |
|
||||||
''; |
|
||||||
distPhase = "true"; |
|
||||||
}; |
|
||||||
|
|
||||||
default = pkgs.writeShellApplication { |
|
||||||
name = packageJSON.name; |
|
||||||
runtimeInputs = [ site-src pkgs.nodejs ]; |
|
||||||
text = '' |
|
||||||
node ${site-src}/libexec/${packageJSON.name}/deps/${packageJSON.name}/build |
|
||||||
''; |
|
||||||
}; |
|
||||||
}; |
|
||||||
|
|
||||||
devShell = pkgs.mkShell { |
|
||||||
buildInputs = [ |
|
||||||
pkgs.gitlint |
|
||||||
pkgs.nodejs |
|
||||||
pkgs.corepack |
|
||||||
]; |
|
||||||
|
|
||||||
shellHook = '' |
|
||||||
yarn set version 4.5.0 |
|
||||||
# auto-install git hooks |
|
||||||
dot_git="$(git rev-parse --git-common-dir)" |
|
||||||
if [[ ! -d "$dot_git/hooks" ]]; then mkdir "$dot_git/hooks"; fi |
|
||||||
for hook in git_hooks/* ; do ln -sf "$(pwd)/$hook" "$dot_git/hooks/" ; done |
|
||||||
''; |
|
||||||
}; |
|
||||||
} |
|
||||||
); |
|
||||||
} |
|
||||||
@ -1,35 +0,0 @@ |
|||||||
#!/bin/sh |
|
||||||
### gitlint commit-msg hook start ### |
|
||||||
|
|
||||||
# Determine whether we have a tty available by trying to access it. |
|
||||||
# This allows us to deal with UI based gitclient's like Atlassian SourceTree. |
|
||||||
# NOTE: "exec < /dev/tty" sets stdin to the keyboard |
|
||||||
stdin_available=1 |
|
||||||
(exec < /dev/tty) 2> /dev/null || stdin_available=0 |
|
||||||
|
|
||||||
if [ $stdin_available -eq 1 ]; then |
|
||||||
# Now that we know we have a functional tty, set stdin to it so we can ask the user questions :-) |
|
||||||
exec < /dev/tty |
|
||||||
|
|
||||||
# On Windows, we need to explicitly set our stdout to the tty to make terminal editing work (e.g. vim) |
|
||||||
# See SO for windows detection in bash (slight modified to work on plain shell (not bash)): |
|
||||||
# https://stackoverflow.com/questions/394230/how-to-detect-the-os-from-a-bash-script |
|
||||||
if [ "$OSTYPE" = "cygwin" ] || [ "$OSTYPE" = "msys" ] || [ "$OSTYPE" = "win32" ]; then |
|
||||||
exec > /dev/tty |
|
||||||
fi |
|
||||||
fi |
|
||||||
|
|
||||||
gitlint --staged --msg-filename "$1" run-hook |
|
||||||
exit_code=$? |
|
||||||
|
|
||||||
# If we fail to find the gitlint binary (command not found), let's retry by executing as a python module. |
|
||||||
# This is the case for Atlassian SourceTree, where $PATH deviates from the user's shell $PATH. |
|
||||||
if [ $exit_code -eq 127 ]; then |
|
||||||
echo "Fallback to python module execution" |
|
||||||
python -m gitlint.cli --staged --msg-filename "$1" run-hook |
|
||||||
exit_code=$? |
|
||||||
fi |
|
||||||
|
|
||||||
exit $exit_code |
|
||||||
|
|
||||||
### gitlint commit-msg hook end ### |
|
||||||
@ -1,11 +0,0 @@ |
|||||||
identifier: gitcitadel.eu |
|
||||||
maintainers: |
|
||||||
- npub1l5sga6xg72phsz5422ykujprejwud075ggrr3z2hwyrfgr7eylqstegx9z |
|
||||||
- npub1wqfzz2p880wq0tumuae9lfwyhs8uz35xd0kr34zrvrwyh3kvrzuskcqsyn |
|
||||||
- npub1qdjn8j4gwgmkj3k5un775nq6q3q7mguv5tvajstmkdsqdja2havq03fqm7 |
|
||||||
- npub1ecdlntvjzexlyfale2egzvvncc8tgqsaxkl5hw7xlgjv2cxs705s9qs735 |
|
||||||
- npub1m3xdppkd0njmrqe2ma8a6ys39zvgp5k8u22mev8xsnqp4nh80srqhqa5sf |
|
||||||
|
|
||||||
relays: |
|
||||||
- wss://thecitadel.nostr1.com |
|
||||||
- wss://theforest.nostr1.com |
|
||||||
@ -1,7 +0,0 @@ |
|||||||
[build] |
|
||||||
command = "yarn run build" |
|
||||||
publish = "build" |
|
||||||
[[headers]] |
|
||||||
for = "/.well-known/nostr.json" |
|
||||||
[headers.values] |
|
||||||
Access-Control-Allow-Origin = "*" |
|
||||||
@ -1,65 +0,0 @@ |
|||||||
{ |
|
||||||
"name": "gitcitadel.eu", |
|
||||||
"version": "0.0.1", |
|
||||||
"scripts": { |
|
||||||
"dev": "vite dev", |
|
||||||
"build": "vite build", |
|
||||||
"preview": "vite preview", |
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", |
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", |
|
||||||
"lint": "eslint 'src/**/*.{ts,svelte}' --fix", |
|
||||||
"format": "prettier 'src/**/*.{ts,svelte}' --write", |
|
||||||
"storybook": "storybook dev -p 6006", |
|
||||||
"test": "vitest" |
|
||||||
}, |
|
||||||
"license": "MIT", |
|
||||||
"devDependencies": { |
|
||||||
"@storybook/addon-a11y": "^8.3.2", |
|
||||||
"@storybook/addon-essentials": "^8.3.2", |
|
||||||
"@storybook/addon-interactions": "^8.3.2", |
|
||||||
"@storybook/addon-links": "^8.3.2", |
|
||||||
"@storybook/addon-svelte-csf": "^4.1.7", |
|
||||||
"@storybook/blocks": "^8.3.2", |
|
||||||
"@storybook/svelte": "^8.3.2", |
|
||||||
"@storybook/sveltekit": "^8.3.2", |
|
||||||
"@sveltejs/adapter-static": "^3.0.5", |
|
||||||
"@sveltejs/kit": "^2.5.28", |
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.1.2", |
|
||||||
"@tailwindcss/typography": "^0.5.15", |
|
||||||
"@types/ramda": "^0.30.2", |
|
||||||
"@typescript-eslint/eslint-plugin": "^8.6.0", |
|
||||||
"@typescript-eslint/parser": "^8.6.0", |
|
||||||
"autoprefixer": "^10.4.20", |
|
||||||
"eslint": "^8.0.0", |
|
||||||
"eslint-config-prettier": "^9.1.0", |
|
||||||
"eslint-plugin-prettier": "^5.2.1", |
|
||||||
"eslint-plugin-svelte": "^2.44.0", |
|
||||||
"postcss": "^8.4.47", |
|
||||||
"prettier": "^3.3.3", |
|
||||||
"prettier-plugin-svelte": "^3.2.6", |
|
||||||
"prettier-plugin-tailwindcss": "^0.6.6", |
|
||||||
"react": "^18.3.1", |
|
||||||
"react-dom": "^18.3.1", |
|
||||||
"storybook": "^8.3.2", |
|
||||||
"svelte": "^4.2.19", |
|
||||||
"svelte-check": "^4.0.2", |
|
||||||
"tailwindcss": "^3.4.12", |
|
||||||
"tslib": "^2.7.0", |
|
||||||
"typescript": "^5.6.2", |
|
||||||
"vite": "^5.4.6", |
|
||||||
"vitest": "^2.1.1" |
|
||||||
}, |
|
||||||
"type": "module", |
|
||||||
"dependencies": { |
|
||||||
"@nostr-dev-kit/ndk": "^2.10.0", |
|
||||||
"@nostr-dev-kit/ndk-svelte": "^2.2.18", |
|
||||||
"daisyui": "^4.12.10", |
|
||||||
"dayjs": "^1.11.13", |
|
||||||
"highlight.js": "^11.10.0", |
|
||||||
"nostr-tools": "^2.7.2", |
|
||||||
"parse-diff": "^0.11.1", |
|
||||||
"ramda": "^0.30.1", |
|
||||||
"svelte-markdown": "^0.4.1" |
|
||||||
}, |
|
||||||
"packageManager": "yarn@4.5.0" |
|
||||||
} |
|
||||||
@ -1,6 +0,0 @@ |
|||||||
export default { |
|
||||||
plugins: { |
|
||||||
tailwindcss: {}, |
|
||||||
autoprefixer: {}, |
|
||||||
}, |
|
||||||
} |
|
||||||
@ -1,21 +0,0 @@ |
|||||||
<!doctype html> |
|
||||||
<html lang="en"> |
|
||||||
<head> |
|
||||||
<meta charset="utf-8" /> |
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
|
||||||
|
|
||||||
<!-- Favicon --> |
|
||||||
<link |
|
||||||
rel="icon" |
|
||||||
href="/icons/icon-32x32.png?v=1" |
|
||||||
sizes="32x32" |
|
||||||
type="image/png" |
|
||||||
/> |
|
||||||
|
|
||||||
<!-- SvelteKit Head --> |
|
||||||
%sveltekit.head% |
|
||||||
</head> |
|
||||||
<body data-sveltekit-preload-data="hover"> |
|
||||||
<div style="display: contents">%sveltekit.body%</div> |
|
||||||
</body> |
|
||||||
</html> |
|
||||||
@ -1,3 +0,0 @@ |
|||||||
@tailwind base; |
|
||||||
@tailwind components; |
|
||||||
@tailwind utilities; |
|
||||||
@ -1,21 +0,0 @@ |
|||||||
<!doctype html> |
|
||||||
<html lang="en"> |
|
||||||
<head> |
|
||||||
<meta charset="utf-8" /> |
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
|
||||||
|
|
||||||
<!-- Favicon --> |
|
||||||
<link |
|
||||||
rel="icon" |
|
||||||
href="/icons/icon-32x32.png?v=1" |
|
||||||
sizes="32x32" |
|
||||||
type="image/png" |
|
||||||
/> |
|
||||||
|
|
||||||
<!-- SvelteKit Head --> |
|
||||||
%sveltekit.head% |
|
||||||
</head> |
|
||||||
<body data-sveltekit-preload-data="hover"> |
|
||||||
<div style="display: contents">%sveltekit.body%</div> |
|
||||||
</body> |
|
||||||
</html> |
|
||||||
@ -1,19 +0,0 @@ |
|||||||
<div role="alert" class="m-auto max-w-xl"> |
|
||||||
<div role="alert" class="alert alert-error m-auto mt-6"> |
|
||||||
<svg |
|
||||||
xmlns="http://www.w3.org/2000/svg" |
|
||||||
class="h-6 w-6 shrink-0 stroke-current" |
|
||||||
fill="none" |
|
||||||
viewBox="0 0 24 24" |
|
||||||
><path |
|
||||||
stroke-linecap="round" |
|
||||||
stroke-linejoin="round" |
|
||||||
stroke-width="2" |
|
||||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" |
|
||||||
/></svg |
|
||||||
> |
|
||||||
<div> |
|
||||||
<slot /> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
@ -1,18 +0,0 @@ |
|||||||
<div role="alert" class="m-auto max-w-xl"> |
|
||||||
<div role="alert" class="alert alert-warning m-auto mt-6 bg-yellow-300"> |
|
||||||
<!-- https://icon-sets.iconify.design/ph/warning-fill/ --> |
|
||||||
<svg |
|
||||||
xmlns="http://www.w3.org/2000/svg" |
|
||||||
class="h-6 w-6 shrink-0 stroke-current" |
|
||||||
viewBox="0 0 256 256" |
|
||||||
> |
|
||||||
<path |
|
||||||
fill="currentColor" |
|
||||||
d="M236.8 188.09L149.35 36.22a24.76 24.76 0 0 0-42.7 0L19.2 188.09a23.51 23.51 0 0 0 0 23.72A24.35 24.35 0 0 0 40.55 224h174.9a24.35 24.35 0 0 0 21.33-12.19a23.51 23.51 0 0 0 .02-23.72M120 104a8 8 0 0 1 16 0v40a8 8 0 0 1-16 0Zm8 88a12 12 0 1 1 12-12a12 12 0 0 1-12 12" |
|
||||||
/></svg |
|
||||||
> |
|
||||||
<div> |
|
||||||
<slot /> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
@ -1,13 +0,0 @@ |
|||||||
<script lang="ts"> |
|
||||||
export let no_wrap: boolean = false |
|
||||||
</script> |
|
||||||
|
|
||||||
<div class="mx-auto lg:container"> |
|
||||||
{#if no_wrap} |
|
||||||
<slot /> |
|
||||||
{:else} |
|
||||||
<div class="px-3"> |
|
||||||
<slot /> |
|
||||||
</div> |
|
||||||
{/if} |
|
||||||
</div> |
|
||||||
@ -1,77 +0,0 @@ |
|||||||
<script lang="ts"> |
|
||||||
import { icons_misc } from './icons' |
|
||||||
|
|
||||||
export let label: string = '' |
|
||||||
export let content: string = '' |
|
||||||
export let border_color = 'primary' |
|
||||||
export let no_border = false |
|
||||||
export let icon: undefined | string[] = undefined |
|
||||||
export let truncate: undefined | [number, number] = undefined |
|
||||||
const truncatedContent = () => { |
|
||||||
if (truncate && content.length > truncate[0] + truncate[1] + 3) { |
|
||||||
return `${content.substring(0, truncate[0])}...${content.substring(content.length - 1 - truncate[1])}` |
|
||||||
} |
|
||||||
return content |
|
||||||
} |
|
||||||
let copied = false |
|
||||||
</script> |
|
||||||
|
|
||||||
<!-- eslint-disable-next-line svelte/valid-compile --> |
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events --> |
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions --> |
|
||||||
<div |
|
||||||
class="group cursor-pointer" |
|
||||||
class:mt-3={!no_border} |
|
||||||
on:click={async () => { |
|
||||||
try { |
|
||||||
await navigator.clipboard.writeText(content) |
|
||||||
copied = true |
|
||||||
setTimeout(() => { |
|
||||||
copied = false |
|
||||||
}, 2000) |
|
||||||
} catch {} |
|
||||||
}} |
|
||||||
> |
|
||||||
{#if label.length > 0} |
|
||||||
{label} |
|
||||||
{#if copied}<span class="text-sm text-success opacity-50"> |
|
||||||
(copied to clipboard)</span |
|
||||||
>{/if} |
|
||||||
{/if} |
|
||||||
<div |
|
||||||
class="items flex w-full items-center rounded-lg border border-{border_color} opacity-50" |
|
||||||
class:mt-1={no_border && label.length === 0} |
|
||||||
class:border={!no_border} |
|
||||||
class:p-3={!no_border} |
|
||||||
class:text-success={copied} |
|
||||||
> |
|
||||||
{#if icon}<svg |
|
||||||
xmlns="http://www.w3.org/2000/svg" |
|
||||||
viewBox="0 0 16 16" |
|
||||||
class="mr-1 mt-1 inline h-4 w-4 flex-none fill-base-content opacity-50" |
|
||||||
class:fill-success={copied} |
|
||||||
> |
|
||||||
{#each icon as d} |
|
||||||
<path {d} /> |
|
||||||
{/each} |
|
||||||
</svg>{/if} |
|
||||||
<div |
|
||||||
class="truncate text-sm" |
|
||||||
class:flex-auto={!no_border} |
|
||||||
class:flex-none={no_border} |
|
||||||
> |
|
||||||
{truncatedContent()} |
|
||||||
</div> |
|
||||||
<svg |
|
||||||
xmlns="http://www.w3.org/2000/svg" |
|
||||||
viewBox="0 0 16 16" |
|
||||||
class="ml-1 inline h-4 w-4 flex-none fill-base-content opacity-50 group-hover:opacity-100" |
|
||||||
class:opacity-100={copied} |
|
||||||
class:fill-success={copied} |
|
||||||
> |
|
||||||
{#each icons_misc.copy as d} |
|
||||||
<path {d} /> |
|
||||||
{/each} |
|
||||||
</svg> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
@ -1,33 +0,0 @@ |
|||||||
<script lang="ts"> |
|
||||||
export let size: 'sm' | 'md' = 'md' |
|
||||||
const version = 'v1.5.2' |
|
||||||
</script> |
|
||||||
|
|
||||||
<div class="prose" class:text-sm={size === 'sm'}> |
|
||||||
<p> |
|
||||||
download binaries and add them to a directory from which they can be run |
|
||||||
globally: |
|
||||||
</p> |
|
||||||
<p> |
|
||||||
<a |
|
||||||
href="https://github.com/DanConwayDev/ngit-cli/releases/download/{version}/ngit-{version}-x86_64-unknown-linux-gnu.tar.gz" |
|
||||||
class="btn btn-neutral" |
|
||||||
class:btn-sm={size === 'sm'}>Linux</a |
|
||||||
> |
|
||||||
<a |
|
||||||
href="https://github.com/DanConwayDev/ngit-cli/releases/download/{version}/ngit-{version}-aarch64-apple-darwin.tar.gz" |
|
||||||
class="btn btn-neutral" |
|
||||||
class:btn-sm={size === 'sm'}>Mac</a |
|
||||||
> |
|
||||||
<a |
|
||||||
href="https://github.com/DanConwayDev/ngit-cli/releases/download/{version}/ngit-{version}-x86_64-pc-windows-msvc.zip" |
|
||||||
class="btn btn-neutral" |
|
||||||
class:btn-sm={size === 'sm'}>Windows</a |
|
||||||
> |
|
||||||
{version} |
|
||||||
</p> |
|
||||||
<p> |
|
||||||
alternatively, if you have cargo installed run<code>cargo install ngit</code |
|
||||||
> |
|
||||||
</p> |
|
||||||
</div> |
|
||||||
@ -1,32 +0,0 @@ |
|||||||
<script lang="ts" context="module"> |
|
||||||
import type { Meta } from '@storybook/svelte' |
|
||||||
import Navbar from '$lib/components/Navbar.svelte' |
|
||||||
import { Story, Template } from '@storybook/addon-svelte-csf' |
|
||||||
import { UserVectors } from '$lib/components/users/vectors' |
|
||||||
|
|
||||||
export const meta: Meta<Navbar> = { |
|
||||||
title: 'Navbar', |
|
||||||
component: Navbar, |
|
||||||
tags: ['autodocs'], |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<Template let:args> |
|
||||||
<Navbar {...args} /> |
|
||||||
</Template> |
|
||||||
|
|
||||||
<Story name="Default" /> |
|
||||||
|
|
||||||
<Story name="NIP07 Loading" args={{ nip07_plugin: undefined }} /> |
|
||||||
|
|
||||||
<Story name="NoNIP07" args={{ nip07_plugin: false }} /> |
|
||||||
|
|
||||||
<Story |
|
||||||
name="NIP07Exists" |
|
||||||
args={{ nip07_plugin: true, logged_in_user: undefined }} |
|
||||||
/> |
|
||||||
|
|
||||||
<Story |
|
||||||
name="Logged in" |
|
||||||
args={{ nip07_plugin: true, logged_in_user: { ...UserVectors.default } }} |
|
||||||
/> |
|
||||||
@ -1,68 +0,0 @@ |
|||||||
<script lang="ts"> |
|
||||||
import { logout } from '$lib/stores/users' |
|
||||||
import Container from './Container.svelte' |
|
||||||
import UserHeader from './users/UserHeader.svelte' |
|
||||||
import type { User } from './users/type' |
|
||||||
|
|
||||||
export let logged_in_user: User | undefined = undefined |
|
||||||
export let nip07_plugin: boolean | undefined = undefined |
|
||||||
export let login_function = () => {} |
|
||||||
export let singup_function = () => {} |
|
||||||
</script> |
|
||||||
|
|
||||||
<div class="bg-base-400"> |
|
||||||
<Container> |
|
||||||
<div class="navbar"> |
|
||||||
<div class="navbar-start"> |
|
||||||
<a href="/repos" class="btn btn-ghost btn-sm normal-case">Repos</a> |
|
||||||
</div> |
|
||||||
<div class="navbar-center"> |
|
||||||
<a class="align-middle text-lg" href="/"> |
|
||||||
GitCitadel |
|
||||||
</a> |
|
||||||
</div> |
|
||||||
<div class="navbar-end gap-4"> |
|
||||||
{#if logged_in_user} |
|
||||||
<div class="dropdown dropdown-end"> |
|
||||||
<div tabindex="0" role="button" class="m-1"> |
|
||||||
<UserHeader user={logged_in_user} link_to_profile={false} /> |
|
||||||
</div> |
|
||||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex --> |
|
||||||
<ul |
|
||||||
tabindex="0" |
|
||||||
class="menu dropdown-content z-[1] -mr-4 rounded-box bg-base-400 p-2 shadow" |
|
||||||
> |
|
||||||
<li><UserHeader user={logged_in_user} /></li> |
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events --> |
|
||||||
<!-- svelte-ignore a11y-missing-attribute --> |
|
||||||
<li> |
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions --> |
|
||||||
<a |
|
||||||
on:click={() => { |
|
||||||
logout() |
|
||||||
}}>Logout</a |
|
||||||
> |
|
||||||
</li> |
|
||||||
</ul> |
|
||||||
</div> |
|
||||||
{:else if nip07_plugin === undefined} |
|
||||||
<div class="skeleton h-8 w-20"></div> |
|
||||||
{:else if nip07_plugin} |
|
||||||
<button |
|
||||||
on:click={() => { |
|
||||||
login_function() |
|
||||||
}} |
|
||||||
class="btn btn-ghost btn-sm normal-case">Login</button |
|
||||||
> |
|
||||||
{:else} |
|
||||||
<button |
|
||||||
on:click={() => { |
|
||||||
singup_function() |
|
||||||
}} |
|
||||||
class="btn btn-ghost btn-sm normal-case">Sign up</button |
|
||||||
> |
|
||||||
{/if} |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</Container> |
|
||||||
</div> |
|
||||||
@ -1,28 +0,0 @@ |
|||||||
<script lang="ts" context="module"> |
|
||||||
import type { Meta } from '@storybook/svelte' |
|
||||||
import RepoSummaryCard from './RepoSummaryCard.svelte' |
|
||||||
import { Story, Template } from '@storybook/addon-svelte-csf' |
|
||||||
import { RepoSummaryCardArgsVectors as vectors } from './repo/vectors' |
|
||||||
|
|
||||||
export const meta: Meta<RepoSummaryCard> = { |
|
||||||
title: 'Repo/Summary/Card', |
|
||||||
component: RepoSummaryCard, |
|
||||||
tags: ['autodocs'], |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<Template let:args> |
|
||||||
<RepoSummaryCard {...args} /> |
|
||||||
</Template> |
|
||||||
|
|
||||||
<Story name="Short Details" args={vectors.Short} /> |
|
||||||
|
|
||||||
<Story name="Long Details" args={vectors.Long} /> |
|
||||||
|
|
||||||
<Story name="Long and No Spaces" args={vectors.LongNoSpaces} /> |
|
||||||
|
|
||||||
<Story name="No Details" args={{}} /> |
|
||||||
|
|
||||||
<Story name="loading" args={{ loading: true }} /> |
|
||||||
|
|
||||||
<Story name="Multiple Maintainers" args={vectors.MulipleMaintainers} /> |
|
||||||
@ -1,81 +0,0 @@ |
|||||||
<script lang="ts"> |
|
||||||
import { summary_defaults } from './repo/type' |
|
||||||
import UserHeader from './users/UserHeader.svelte' |
|
||||||
import type { User } from './users/type' |
|
||||||
|
|
||||||
export let { name, description, identifier, maintainers, naddr, loading } = |
|
||||||
summary_defaults |
|
||||||
let short_name: string |
|
||||||
$: { |
|
||||||
if (name && name.length > 45) short_name = name.slice(0, 45) + '...' |
|
||||||
else if (name && name.length >= 0) short_name = name |
|
||||||
else if (identifier && identifier.length > 45) |
|
||||||
short_name = identifier.slice(0, 45) + '...' |
|
||||||
else if (identifier && identifier.length >= 0) short_name = identifier |
|
||||||
else short_name = 'Untitled' |
|
||||||
} |
|
||||||
let additional_maintainers: User[] = [] |
|
||||||
let author: User | undefined = undefined |
|
||||||
|
|
||||||
$: short_descrption = |
|
||||||
description.length > 50 ? description.slice(0, 45) + '...' : description |
|
||||||
|
|
||||||
$: { |
|
||||||
additional_maintainers = (([_, ...xs]) => xs)(maintainers) |
|
||||||
author = maintainers[0] |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<div |
|
||||||
class="rounded-lg bg-base-200 p-4" |
|
||||||
style={`min-height: ${maintainers.length * 1.325 + 2}rem;`} |
|
||||||
> |
|
||||||
{#if loading} |
|
||||||
<div class="skeleton mb-2 h-5 w-40"></div> |
|
||||||
<div class="w-100 skeleton h-4"></div> |
|
||||||
{:else} |
|
||||||
<a class="link-primary break-words" href="/r/{naddr}">{short_name}</a> |
|
||||||
{#if short_descrption.length > 0} |
|
||||||
<p class="text-muted break-words pb-1 text-sm"> |
|
||||||
{short_descrption} |
|
||||||
</p> |
|
||||||
{/if} |
|
||||||
|
|
||||||
<div class="break-words text-right text-xs text-slate-400"> |
|
||||||
{#if author} |
|
||||||
<div |
|
||||||
class="inline" |
|
||||||
class:p-1={additional_maintainers.length > 0} |
|
||||||
class:rounded-md={additional_maintainers.length > 0} |
|
||||||
class:bg-base-400={additional_maintainers.length > 0} |
|
||||||
class:text-white={additional_maintainers.length > 0} |
|
||||||
> |
|
||||||
<UserHeader user={author} inline={true} size="xs" /> |
|
||||||
</div> |
|
||||||
{#if additional_maintainers.length > 0} |
|
||||||
<span>with</span> |
|
||||||
|
|
||||||
<ul class="reposummarycard inline"> |
|
||||||
{#each additional_maintainers as user} |
|
||||||
<li class="inline"> |
|
||||||
<UserHeader {user} inline={true} size="xs" /> |
|
||||||
</li> |
|
||||||
{/each} |
|
||||||
</ul> |
|
||||||
{/if} |
|
||||||
{/if} |
|
||||||
</div> |
|
||||||
{/if} |
|
||||||
</div> |
|
||||||
|
|
||||||
<style lang="postcss"> |
|
||||||
.reposummarycard li::before { |
|
||||||
content: ', '; |
|
||||||
} |
|
||||||
.reposummarycard li:last-child::before { |
|
||||||
content: ' and '; |
|
||||||
} |
|
||||||
.reposummarycard li:first-child::before { |
|
||||||
content: ''; |
|
||||||
} |
|
||||||
</style> |
|
||||||
@ -1,69 +0,0 @@ |
|||||||
<script lang="ts" context="module"> |
|
||||||
import type { Meta } from '@storybook/svelte' |
|
||||||
import ReposSummaryList from './ReposSummaryList.svelte' |
|
||||||
import { Story, Template } from '@storybook/addon-svelte-csf' |
|
||||||
import { RepoSummaryCardArgsVectors as vectors } from './repo/vectors' |
|
||||||
|
|
||||||
export const meta: Meta<ReposSummaryList> = { |
|
||||||
title: 'Repo/Summary/List', |
|
||||||
component: ReposSummaryList, |
|
||||||
tags: ['autodocs'], |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<Template let:args> |
|
||||||
<ReposSummaryList {...args} /> |
|
||||||
</Template> |
|
||||||
|
|
||||||
<Story |
|
||||||
name="Default" |
|
||||||
args={{ |
|
||||||
title: 'Featured Repositories', |
|
||||||
repos: [ |
|
||||||
vectors.Short, |
|
||||||
vectors.Long, |
|
||||||
vectors.LongNoSpaces, |
|
||||||
vectors.MulipleMaintainers, |
|
||||||
], |
|
||||||
}} |
|
||||||
/> |
|
||||||
|
|
||||||
<Story |
|
||||||
name="No Title" |
|
||||||
args={{ |
|
||||||
repos: [vectors.Short, vectors.Long], |
|
||||||
}} |
|
||||||
/> |
|
||||||
<Story |
|
||||||
name="Empty" |
|
||||||
args={{ |
|
||||||
title: 'Latest', |
|
||||||
repos: [], |
|
||||||
}} |
|
||||||
/> |
|
||||||
|
|
||||||
<Story |
|
||||||
name="Loading" |
|
||||||
args={{ |
|
||||||
title: 'Latest', |
|
||||||
repos: [], |
|
||||||
loading: true, |
|
||||||
}} |
|
||||||
/> |
|
||||||
|
|
||||||
<Story |
|
||||||
name="Partially Loaded" |
|
||||||
args={{ |
|
||||||
title: 'Latest', |
|
||||||
repos: [vectors.Short, vectors.Long], |
|
||||||
loading: true, |
|
||||||
}} |
|
||||||
/> |
|
||||||
|
|
||||||
<Story |
|
||||||
name="Multiple Maintainers" |
|
||||||
args={{ |
|
||||||
title: 'Multiple Maintainers', |
|
||||||
repos: [vectors.MulipleMaintainers, vectors.Long], |
|
||||||
}} |
|
||||||
/> |
|
||||||
@ -1,119 +0,0 @@ |
|||||||
<script lang="ts"> |
|
||||||
import type { RepoSummary } from './repo/type' |
|
||||||
import RepoSummaryCard from '$lib/components/RepoSummaryCard.svelte' |
|
||||||
|
|
||||||
export let title: string = '' |
|
||||||
export let repos: RepoSummary[] = [] |
|
||||||
export let loading: boolean = false |
|
||||||
export let group_by: 'name' | 'identifier' | undefined = undefined |
|
||||||
|
|
||||||
let grouped_repos: RepoSummary[][] = [] |
|
||||||
let selected_group: string | undefined = undefined |
|
||||||
$: { |
|
||||||
grouped_repos = [] |
|
||||||
repos.forEach((collection) => { |
|
||||||
if (!group_by) { |
|
||||||
grouped_repos.push([collection]) |
|
||||||
return |
|
||||||
} |
|
||||||
const added_to_group = grouped_repos.some((group, i) => { |
|
||||||
if (group.some((c) => c[group_by] === collection[group_by])) { |
|
||||||
grouped_repos[i].push(collection) |
|
||||||
return true |
|
||||||
} |
|
||||||
return false |
|
||||||
}) |
|
||||||
if (!added_to_group) grouped_repos.push([collection]) |
|
||||||
}) |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<div class="min-width"> |
|
||||||
{#if title.length > 0} |
|
||||||
<div class="prose mb-3"> |
|
||||||
<h3>{title}</h3> |
|
||||||
</div> |
|
||||||
{/if} |
|
||||||
{#if repos.length == 0 && !loading} |
|
||||||
<p class="prose">None</p> |
|
||||||
{:else} |
|
||||||
<div class="grid gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4"> |
|
||||||
{#each grouped_repos as group} |
|
||||||
{#if group.length === 0} |
|
||||||
<RepoSummaryCard loading={true} /> |
|
||||||
{:else if group.length === 1} |
|
||||||
{#each group as { name, description, identifier, maintainers, naddr }} |
|
||||||
<RepoSummaryCard |
|
||||||
{name} |
|
||||||
{description} |
|
||||||
{identifier} |
|
||||||
{maintainers} |
|
||||||
{naddr} |
|
||||||
/> |
|
||||||
{/each} |
|
||||||
{:else if group_by} |
|
||||||
<div class="stack"> |
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events --> |
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions --> |
|
||||||
<div |
|
||||||
class="flex min-h-28 cursor-pointer items-center rounded-lg border border-base-400 bg-base-200 p-4 hover:bg-base-300" |
|
||||||
on:click={() => { |
|
||||||
selected_group = group[0][group_by] |
|
||||||
}} |
|
||||||
> |
|
||||||
<div class="m-auto text-center"> |
|
||||||
<div class="">{group[0][group_by]}</div> |
|
||||||
<div class=" text-sm opacity-50">{group.length} Items</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
{#each group as { name, description, identifier, maintainers, naddr }} |
|
||||||
<div class="rounded-lg border border-base-400"> |
|
||||||
<RepoSummaryCard |
|
||||||
{name} |
|
||||||
{description} |
|
||||||
{identifier} |
|
||||||
{maintainers} |
|
||||||
{naddr} |
|
||||||
/> |
|
||||||
</div> |
|
||||||
{/each} |
|
||||||
</div> |
|
||||||
{/if} |
|
||||||
{/each} |
|
||||||
{#if loading} |
|
||||||
<RepoSummaryCard loading={true} /> |
|
||||||
{#if repos.length == 0} |
|
||||||
<RepoSummaryCard loading={true} /> |
|
||||||
<RepoSummaryCard loading={true} /> |
|
||||||
{/if} |
|
||||||
{/if} |
|
||||||
</div> |
|
||||||
{/if} |
|
||||||
</div> |
|
||||||
{#if selected_group} |
|
||||||
<div class="modal modal-open"> |
|
||||||
<div class="modal-box max-w-full text-wrap text-xs"> |
|
||||||
<div class="prose max-w-full"> |
|
||||||
<h3 class="mb-3 max-w-full text-center"> |
|
||||||
{group_by}: "{selected_group}" |
|
||||||
</h3> |
|
||||||
</div> |
|
||||||
<div class="grid gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4"> |
|
||||||
{#each repos.filter((summary) => group_by && summary[group_by] === selected_group) as { name, description, identifier, maintainers, naddr }} |
|
||||||
<RepoSummaryCard |
|
||||||
{name} |
|
||||||
{description} |
|
||||||
{identifier} |
|
||||||
{maintainers} |
|
||||||
{naddr} |
|
||||||
/> |
|
||||||
{/each} |
|
||||||
</div> |
|
||||||
<div class="modal-action"> |
|
||||||
<button class="btn btn-sm" on:click={() => (selected_group = undefined)} |
|
||||||
>Close</button |
|
||||||
> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
{/if} |
|
||||||
@ -1,49 +0,0 @@ |
|||||||
<script lang="ts"> |
|
||||||
import { logged_in_user, login } from '$lib/stores/users' |
|
||||||
import UserHeader from '../users/UserHeader.svelte' |
|
||||||
import { defaults as user_defaults } from '../users/type' |
|
||||||
|
|
||||||
export let sendReply: (content: string) => void = () => {} |
|
||||||
export let placeholder = 'reply...' |
|
||||||
export let submitting = false |
|
||||||
export let logged_in = false |
|
||||||
let submit = () => { |
|
||||||
if (!logged_in) login() |
|
||||||
sendReply(content) |
|
||||||
} |
|
||||||
let content = '' |
|
||||||
</script> |
|
||||||
|
|
||||||
<div class="flex pt-5"> |
|
||||||
<div class="mt-0 flex-none px-3"> |
|
||||||
<UserHeader |
|
||||||
avatar_only={true} |
|
||||||
user={$logged_in_user || { ...user_defaults, loading: false }} |
|
||||||
/> |
|
||||||
</div> |
|
||||||
<div class="flex-grow pt-2"> |
|
||||||
{#if !submitting} |
|
||||||
<textarea |
|
||||||
bind:value={content} |
|
||||||
class="textarea textarea-primary w-full" |
|
||||||
{placeholder} |
|
||||||
></textarea> |
|
||||||
{/if} |
|
||||||
<div class="flex"> |
|
||||||
<div class="flex-auto"></div> |
|
||||||
<button |
|
||||||
on:click={submit} |
|
||||||
disabled={submitting || content.length === 0} |
|
||||||
class="align-right btn btn-primary btn-sm mt-2 align-bottom" |
|
||||||
> |
|
||||||
{#if submitting} |
|
||||||
Sending |
|
||||||
{:else if !logged_in} |
|
||||||
Login before Sending |
|
||||||
{:else} |
|
||||||
Send |
|
||||||
{/if} |
|
||||||
</button> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
@ -1,157 +0,0 @@ |
|||||||
<script lang="ts"> |
|
||||||
import dayjs from 'dayjs' |
|
||||||
import UserHeader from '../users/UserHeader.svelte' |
|
||||||
import type { User } from '../users/type' |
|
||||||
import { defaults as user_defaults } from '../users/type' |
|
||||||
import ComposeReply from '$lib/wrappers/ComposeReply.svelte' |
|
||||||
import { logged_in_user } from '$lib/stores/users' |
|
||||||
import type { NDKEvent } from '@nostr-dev-kit/ndk' |
|
||||||
import CopyField from '../CopyField.svelte' |
|
||||||
import { ndkEventToNeventOrNaddr } from '../repo/utils' |
|
||||||
|
|
||||||
export let type: 'proposal' | 'issue' = 'proposal' |
|
||||||
export let author: User = { ...user_defaults } |
|
||||||
export let created_at: number | undefined |
|
||||||
export let event: NDKEvent |
|
||||||
let show_compose = false |
|
||||||
let show_raw_json_modal = false |
|
||||||
let show_share_modal = false |
|
||||||
let created_at_ago = '' |
|
||||||
$: created_at_ago = created_at ? dayjs(created_at * 1000).fromNow() : '' |
|
||||||
|
|
||||||
const replySent = () => { |
|
||||||
show_compose = false |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<div class="max-w-4xl border-b border-base-300 p-3 pl-3"> |
|
||||||
<div class="flex"> |
|
||||||
<div class="flex-auto"> |
|
||||||
<UserHeader user={author} in_event_header={true} /> |
|
||||||
</div> |
|
||||||
<span class="m-auto text-xs">{created_at_ago}</span> |
|
||||||
<div class="m-auto ml-2"> |
|
||||||
{#if event} |
|
||||||
<div class="tooltip align-middle" data-tip="event json"> |
|
||||||
<button |
|
||||||
on:click={() => { |
|
||||||
show_raw_json_modal = true |
|
||||||
}} |
|
||||||
class="btn btn-xs text-neutral-content" |
|
||||||
> |
|
||||||
<!-- https://icon-sets.iconify.design/ph/brackets-curly-bold --> |
|
||||||
<svg |
|
||||||
xmlns="http://www.w3.org/2000/svg" |
|
||||||
width="16" |
|
||||||
height="16" |
|
||||||
viewBox="0 0 256 256" |
|
||||||
><path |
|
||||||
fill="currentColor" |
|
||||||
d="M54.8 119.49a35.06 35.06 0 0 1-5.75 8.51a35.06 35.06 0 0 1 5.75 8.51C60 147.24 60 159.83 60 172c0 25.94 1.84 32 20 32a12 12 0 0 1 0 24c-19.14 0-32.2-6.9-38.8-20.51C36 196.76 36 184.17 36 172c0-25.94-1.84-32-20-32a12 12 0 0 1 0-24c18.16 0 20-6.06 20-32c0-12.17 0-24.76 5.2-35.49C47.8 34.9 60.86 28 80 28a12 12 0 0 1 0 24c-18.16 0-20 6.06-20 32c0 12.17 0 24.76-5.2 35.49M240 116c-18.16 0-20-6.06-20-32c0-12.17 0-24.76-5.2-35.49C208.2 34.9 195.14 28 176 28a12 12 0 0 0 0 24c18.16 0 20 6.06 20 32c0 12.17 0 24.76 5.2 35.49A35.06 35.06 0 0 0 207 128a35.06 35.06 0 0 0-5.75 8.51C196 147.24 196 159.83 196 172c0 25.94-1.84 32-20 32a12 12 0 0 0 0 24c19.14 0 32.2-6.9 38.8-20.51c5.2-10.73 5.2-23.32 5.2-35.49c0-25.94 1.84-32 20-32a12 12 0 0 0 0-24" |
|
||||||
/></svg |
|
||||||
></button |
|
||||||
> |
|
||||||
</div> |
|
||||||
{#if show_raw_json_modal} |
|
||||||
<div class="modal" class:modal-open={show_raw_json_modal}> |
|
||||||
<div class="modal-box max-w-full text-wrap text-xs"> |
|
||||||
<code class="w-full">{JSON.stringify(event.rawEvent())}</code> |
|
||||||
<div class="modal-action"> |
|
||||||
<button |
|
||||||
class="btn btn-sm" |
|
||||||
on:click={() => (show_raw_json_modal = false)}>Close</button |
|
||||||
> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
{/if} |
|
||||||
<div class="tooltip align-middle" data-tip="share"> |
|
||||||
<button |
|
||||||
on:click={() => { |
|
||||||
show_share_modal = true |
|
||||||
}} |
|
||||||
class="btn btn-xs text-neutral-content" |
|
||||||
> |
|
||||||
<!-- https://icon-sets.iconify.design/ph/share-network-bold/ --> |
|
||||||
<svg |
|
||||||
xmlns="http://www.w3.org/2000/svg" |
|
||||||
width="16" |
|
||||||
height="16" |
|
||||||
viewBox="0 0 256 256" |
|
||||||
><path |
|
||||||
fill="currentColor" |
|
||||||
d="M176 156a43.78 43.78 0 0 0-29.09 11l-40.81-26.2a44.07 44.07 0 0 0 0-25.6L146.91 89a43.83 43.83 0 1 0-13-20.17L93.09 95a44 44 0 1 0 0 65.94l40.81 26.26A44 44 0 1 0 176 156m0-120a20 20 0 1 1-20 20a20 20 0 0 1 20-20M64 148a20 20 0 1 1 20-20a20 20 0 0 1-20 20m112 72a20 20 0 1 1 20-20a20 20 0 0 1-20 20" |
|
||||||
/></svg |
|
||||||
></button |
|
||||||
> |
|
||||||
</div> |
|
||||||
{#if show_share_modal} |
|
||||||
<div class="modal" class:modal-open={show_share_modal}> |
|
||||||
<div class="modal-box max-w-lg text-wrap"> |
|
||||||
<div class="prose"><h3>Share</h3></div> |
|
||||||
<CopyField |
|
||||||
label="nostr address" |
|
||||||
content={`nostr:${ndkEventToNeventOrNaddr(event)}`} |
|
||||||
/> |
|
||||||
<CopyField |
|
||||||
label="njump" |
|
||||||
content={`https://njump.me/${ndkEventToNeventOrNaddr(event)}`} |
|
||||||
border_color="secondary" |
|
||||||
/> |
|
||||||
<CopyField |
|
||||||
label="raw event id" |
|
||||||
content={event.id} |
|
||||||
border_color="neutral-content" |
|
||||||
/> |
|
||||||
<div class="modal-action"> |
|
||||||
<button |
|
||||||
class="btn btn-sm" |
|
||||||
on:click={() => (show_share_modal = false)}>Close</button |
|
||||||
> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
{/if} |
|
||||||
{/if} |
|
||||||
{#if !show_compose && $logged_in_user} |
|
||||||
<div class="tooltip align-middle" data-tip="reply"> |
|
||||||
<button |
|
||||||
on:click={() => { |
|
||||||
show_compose = true |
|
||||||
}} |
|
||||||
class="btn btn-xs" |
|
||||||
><svg |
|
||||||
xmlns="http://www.w3.org/2000/svg" |
|
||||||
width="16" |
|
||||||
height="16" |
|
||||||
viewBox="0 0 16 16" |
|
||||||
><path |
|
||||||
fill="currentColor" |
|
||||||
d="M6.78 1.97a.75.75 0 0 1 0 1.06L3.81 6h6.44A4.75 4.75 0 0 1 15 10.75v2.5a.75.75 0 0 1-1.5 0v-2.5a3.25 3.25 0 0 0-3.25-3.25H3.81l2.97 2.97a.749.749 0 0 1-.326 1.275a.749.749 0 0 1-.734-.215L1.47 7.28a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0" |
|
||||||
/></svg |
|
||||||
></button |
|
||||||
> |
|
||||||
</div> |
|
||||||
{/if} |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
<div class="ml-11"> |
|
||||||
<slot /> |
|
||||||
{#if show_compose} |
|
||||||
<div class=""> |
|
||||||
<div class="flex"> |
|
||||||
<div class="flex-auto"></div> |
|
||||||
<button |
|
||||||
on:click={() => { |
|
||||||
show_compose = false |
|
||||||
}} |
|
||||||
class="btn btn-circle btn-ghost btn-sm right-2 top-2">✕</button |
|
||||||
> |
|
||||||
</div> |
|
||||||
<div class=""> |
|
||||||
<ComposeReply {type} {event} sentFunction={() => replySent()} /> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
{/if} |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
@ -1,23 +0,0 @@ |
|||||||
<script lang="ts"> |
|
||||||
import dayjs from 'dayjs' |
|
||||||
import UserHeader from '../users/UserHeader.svelte' |
|
||||||
import type { User } from '../users/type' |
|
||||||
import { defaults as user_defaults } from '../users/type' |
|
||||||
|
|
||||||
export let author: User = { ...user_defaults } |
|
||||||
export let created_at: number | undefined |
|
||||||
let created_at_ago = '' |
|
||||||
$: created_at_ago = created_at ? dayjs(created_at * 1000).fromNow() : '' |
|
||||||
</script> |
|
||||||
|
|
||||||
<div class="max-w-4xl border-b border-base-300 p-3 pl-3"> |
|
||||||
<div class="flex"> |
|
||||||
<div class="flex-auto"> |
|
||||||
<div class="inline text-neutral-400"><slot /></div> |
|
||||||
<div class="badge bg-base-400 text-neutral-400"> |
|
||||||
<UserHeader user={author} inline /> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
<span class="m-auto flex-none py-1 text-xs">{created_at_ago}</span> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
@ -1,48 +0,0 @@ |
|||||||
<script lang="ts"> |
|
||||||
import { icons_misc } from '../icons' |
|
||||||
|
|
||||||
let show_replies = true |
|
||||||
export let num_replies = 0 |
|
||||||
|
|
||||||
const toggle_replies = () => { |
|
||||||
show_replies = !show_replies |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<div class="border-l border-blue-500 pl-1"> |
|
||||||
{#if num_replies > 0} |
|
||||||
{#if show_replies} |
|
||||||
<div class="opacity-60 hover:opacity-90" class:relative={show_replies}> |
|
||||||
<button |
|
||||||
on:click={() => { |
|
||||||
toggle_replies() |
|
||||||
}} |
|
||||||
class="-ml-1 -mt-8" |
|
||||||
class:absolute={show_replies} |
|
||||||
> |
|
||||||
<svg |
|
||||||
xmlns="http://www.w3.org/2000/svg" |
|
||||||
viewBox="0 0 16 16" |
|
||||||
class="h-7 w-7 flex-none fill-blue-500 pt-1" |
|
||||||
> |
|
||||||
{#each show_replies ? icons_misc.chevron_up : icons_misc.chevron_down as p} |
|
||||||
<path d={p} /> |
|
||||||
{/each} |
|
||||||
</svg> |
|
||||||
</button> |
|
||||||
</div> |
|
||||||
{:else} |
|
||||||
<button |
|
||||||
on:click={() => { |
|
||||||
toggle_replies() |
|
||||||
}} |
|
||||||
class="w-full cursor-pointer bg-base-300 p-3 text-left hover:bg-base-400" |
|
||||||
> |
|
||||||
show {num_replies} hidden replies |
|
||||||
</button> |
|
||||||
{/if} |
|
||||||
{/if} |
|
||||||
<div class:hidden={!show_replies}> |
|
||||||
<slot /> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
@ -1,22 +0,0 @@ |
|||||||
<script lang="ts"> |
|
||||||
import { extractAReference } from '$lib/components/repo/utils' |
|
||||||
import { extractRepoAFromProposalEvent } from '$lib/stores/Proposals' |
|
||||||
import type { NDKEvent } from '@nostr-dev-kit/ndk' |
|
||||||
import { nip19 } from 'nostr-tools' |
|
||||||
import { extractIssueTitle } from './utils' |
|
||||||
|
|
||||||
export let event: NDKEvent |
|
||||||
let nevent = nip19.neventEncode({ |
|
||||||
id: event.id, |
|
||||||
relays: event.relay ? [event.relay.url] : undefined, |
|
||||||
}) |
|
||||||
let a_string = extractRepoAFromProposalEvent(event) |
|
||||||
let pointer = a_string ? extractAReference(a_string) : undefined |
|
||||||
let naddr = pointer ? nip19.naddrEncode(pointer) : undefined |
|
||||||
</script> |
|
||||||
|
|
||||||
<span> |
|
||||||
Git Issue for <a class="opacity-50" href={`/e/${naddr}`} |
|
||||||
>{pointer?.identifier}</a |
|
||||||
>: <a href={`/e/${nevent}`}>{extractIssueTitle(event)}</a> by |
|
||||||
</span> |
|
||||||
@ -1,51 +0,0 @@ |
|||||||
<script lang="ts"> |
|
||||||
import type { NDKTag } from '@nostr-dev-kit/ndk' |
|
||||||
import { |
|
||||||
isImage, |
|
||||||
isParsedLink, |
|
||||||
isParsedNaddr, |
|
||||||
isParsedNevent, |
|
||||||
isParsedNewLine, |
|
||||||
isParsedNote, |
|
||||||
isParsedNprofile, |
|
||||||
isParsedNpub, |
|
||||||
isParsedText, |
|
||||||
parseContent, |
|
||||||
type ParsedPart, |
|
||||||
} from './utils' |
|
||||||
import UserHeader from '$lib/components/users/UserHeader.svelte' |
|
||||||
import EventPreview from '$lib/wrappers/EventPreview.svelte' |
|
||||||
export let content: string = '' |
|
||||||
export let tags: NDKTag[] = [] |
|
||||||
|
|
||||||
let fullContent: ParsedPart[] = [] |
|
||||||
|
|
||||||
$: fullContent = parseContent(content, tags) |
|
||||||
</script> |
|
||||||
|
|
||||||
<div class="prose max-w-prose break-words"> |
|
||||||
{#each fullContent as part} |
|
||||||
{#if isParsedNewLine(part)} |
|
||||||
{#if part.value.length > 1} |
|
||||||
<br /> |
|
||||||
{/if} |
|
||||||
<br /> |
|
||||||
{:else if isParsedLink(part)} |
|
||||||
{#if isImage(part.url)} |
|
||||||
<img src={part.url} alt={part.imeta?.alt} /> |
|
||||||
{:else} |
|
||||||
<a href={part.url} target="_blank"> |
|
||||||
{part.url.replace(/https?:\/\/(www\.)?/, '')} |
|
||||||
</a> |
|
||||||
{/if} |
|
||||||
{:else if isParsedNpub(part) || isParsedNprofile(part)} |
|
||||||
<div class="badge badge-neutral"> |
|
||||||
<UserHeader user={part.hex} inline={true} size="sm" /> |
|
||||||
</div> |
|
||||||
{:else if isParsedNevent(part) || isParsedNote(part) || isParsedNaddr(part)} |
|
||||||
<EventPreview pointer={part.data} /> |
|
||||||
{:else if isParsedText(part)} |
|
||||||
{part.value} |
|
||||||
{/if} |
|
||||||
{/each} |
|
||||||
</div> |
|
||||||
@ -1,252 +0,0 @@ |
|||||||
<script lang="ts"> |
|
||||||
import type { NDKEvent, NDKTag } from '@nostr-dev-kit/ndk' |
|
||||||
import parseDiff from 'parse-diff' |
|
||||||
import hljs from 'highlight.js/lib/common' |
|
||||||
import 'highlight.js/styles/agate.min.css' |
|
||||||
import type { Change, AddChange, DeleteChange } from 'parse-diff' |
|
||||||
import ParsedContent from './ParsedContent.svelte' |
|
||||||
import { extractPatchMessage, extractTagContent } from './utils' |
|
||||||
import { nip19 } from 'nostr-tools' |
|
||||||
import { extractRepoAFromProposalEvent } from '$lib/stores/Proposals' |
|
||||||
import { extractAReference } from '$lib/components/repo/utils' |
|
||||||
|
|
||||||
export let event: NDKEvent |
|
||||||
export let preview = false |
|
||||||
|
|
||||||
let content: string = event ? event.content : '' |
|
||||||
let tags: NDKTag[] = event ? event.tags : [] |
|
||||||
|
|
||||||
let commit_id_shorthand = |
|
||||||
extractTagContent('commit', tags)?.substring(0, 8) || '[commit_id unknown]' |
|
||||||
let commit_message = |
|
||||||
extractTagContent('description', tags) || |
|
||||||
extractPatchMessage(content) || |
|
||||||
'[untitled]' |
|
||||||
let commit_title = commit_message.split('\n')[0] |
|
||||||
|
|
||||||
let files = parseDiff(content) |
|
||||||
let expand_files = files.map((file) => file.deletions + file.additions < 20) |
|
||||||
|
|
||||||
if ( |
|
||||||
files.reduce((acc, file) => acc + file.deletions + file.additions, 0) < 60 |
|
||||||
) { |
|
||||||
expand_files = expand_files.map((_) => true) |
|
||||||
} |
|
||||||
|
|
||||||
let expand_full_files = files.map((_) => false) |
|
||||||
|
|
||||||
let isAddChange = (change: Change): change is AddChange => |
|
||||||
change.type == 'add' |
|
||||||
let isDeleteChange = (change: Change): change is DeleteChange => |
|
||||||
change.type == 'del' |
|
||||||
let extractChangeLine = (change: Change, stage?: 'before' | 'after') => { |
|
||||||
if (isAddChange(change) || isDeleteChange(change)) { |
|
||||||
return change.ln |
|
||||||
} else { |
|
||||||
if (stage === 'before') return change.ln1 |
|
||||||
if (stage === 'after') return change.ln2 |
|
||||||
if (change.ln2 === change.ln2) return change.ln1 |
|
||||||
return '#' |
|
||||||
} |
|
||||||
} |
|
||||||
let getFortmattedDiffHtml = ( |
|
||||||
change: Change, |
|
||||||
language: string |
|
||||||
): string | undefined => { |
|
||||||
try { |
|
||||||
return hljs.highlight( |
|
||||||
change.type == 'normal' ? change.content : change.content.substring(1), |
|
||||||
{ language } |
|
||||||
).value |
|
||||||
} catch { |
|
||||||
return undefined |
|
||||||
} |
|
||||||
} |
|
||||||
let nevent = nip19.neventEncode({ |
|
||||||
id: event.id, |
|
||||||
relays: event.relay ? [event.relay.url] : undefined, |
|
||||||
}) |
|
||||||
|
|
||||||
let a_string = extractRepoAFromProposalEvent(event) |
|
||||||
let pointer = a_string ? extractAReference(a_string) : undefined |
|
||||||
let naddr = pointer ? nip19.naddrEncode(pointer) : undefined |
|
||||||
</script> |
|
||||||
|
|
||||||
{#if preview} |
|
||||||
<span> |
|
||||||
Git Patch for <a class="opacity-50" href={`/e/${naddr}`} |
|
||||||
>{pointer?.identifier}</a |
|
||||||
>: <a href={`/e/${nevent}`}>{commit_title}</a> by |
|
||||||
</span> |
|
||||||
{:else} |
|
||||||
<div class=""> |
|
||||||
<div class="flex rounded-t bg-base-300 p-1"> |
|
||||||
<article class="ml-2 flex-grow font-mono text-sm"> |
|
||||||
<ParsedContent content={commit_message} /> |
|
||||||
</article> |
|
||||||
<div class="flex-none p-1 align-middle text-xs text-neutral">commit</div> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="flex p-3"> |
|
||||||
<div class="flex-grow text-xs">Changes:</div> |
|
||||||
<div class="flex-none text-right font-mono text-xs"> |
|
||||||
{commit_id_shorthand} |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
|
|
||||||
{#each files as file, index} |
|
||||||
<div |
|
||||||
class="my-2 border border-base-300 {expand_full_files[index] |
|
||||||
? 'absolute left-0 z-10 w-screen bg-base-300 px-5' |
|
||||||
: ''}" |
|
||||||
> |
|
||||||
<div class="flex w-full bg-base-200"> |
|
||||||
<button |
|
||||||
class="flex shrink flex-grow p-3 text-sm" |
|
||||||
on:click={() => { |
|
||||||
if (expand_full_files[index]) { |
|
||||||
expand_full_files[index] = false |
|
||||||
expand_files[index] = false |
|
||||||
} else if (expand_files[index]) { |
|
||||||
expand_full_files[index] = true |
|
||||||
} else { |
|
||||||
expand_files[index] = true |
|
||||||
} |
|
||||||
}} |
|
||||||
><div class="shrink text-wrap text-left"> |
|
||||||
<span class="pr-3">{file.to || file.from}</span> |
|
||||||
<span |
|
||||||
class="text-middle flex-none align-middle font-mono text-xs opacity-70" |
|
||||||
>{#if file.new}<span>created file</span |
|
||||||
> {/if}{#if file.deleted}<span>deleted file</span |
|
||||||
> {/if}{#if !file.deleted}<span class="text-success" |
|
||||||
>+{file.additions}</span |
|
||||||
>{/if} {#if !file.new}<span class="text-error" |
|
||||||
>-{file.deletions}</span |
|
||||||
>{/if} |
|
||||||
</span> |
|
||||||
</div> |
|
||||||
<div class="flex-grow"></div> |
|
||||||
</button> |
|
||||||
<button |
|
||||||
class="flex-none p-3 text-right text-xs opacity-40" |
|
||||||
on:click={() => { |
|
||||||
expand_files[index] = !expand_files[index] |
|
||||||
expand_full_files[index] = false |
|
||||||
}} |
|
||||||
> |
|
||||||
{expand_files[index] ? 'colapse' : 'expand'} |
|
||||||
</button> |
|
||||||
<button |
|
||||||
class="flex-none p-3 text-right text-xs opacity-40" |
|
||||||
on:click={() => { |
|
||||||
expand_full_files[index] = !expand_full_files[index] |
|
||||||
if (expand_full_files[index]) expand_files[index] = true |
|
||||||
}} |
|
||||||
> |
|
||||||
full |
|
||||||
</button> |
|
||||||
</div> |
|
||||||
{#if expand_files[index]} |
|
||||||
<div class="border-t-1 flex border-base-300 font-mono text-xs"> |
|
||||||
<div class="flex-full select-none text-right"> |
|
||||||
{#each file.chunks as chunk, index} |
|
||||||
{#if index !== 0} |
|
||||||
<div class="flex w-full bg-base-200"> |
|
||||||
<div |
|
||||||
class="w-8 flex-none whitespace-pre pb-2 pr-2 pt-1 opacity-50" |
|
||||||
> |
|
||||||
... |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
{/if} |
|
||||||
{#each chunk.changes as change, i} |
|
||||||
<div class="flex w-full bg-base-100"> |
|
||||||
<div |
|
||||||
class="w-8 flex-none whitespace-pre {change.type == 'add' |
|
||||||
? 'bg-success/50' |
|
||||||
: change.type == 'del' |
|
||||||
? 'bg-error/50' |
|
||||||
: 'bg-slate-500/20'} pr-2 opacity-50" |
|
||||||
class:pt-3={index === 0 && i === 0} |
|
||||||
class:pb-3={index === file.chunks.length - 1 && |
|
||||||
i === chunk.changes.length - 1} |
|
||||||
> |
|
||||||
{isAddChange(change) && |
|
||||||
i !== 0 && |
|
||||||
isDeleteChange(chunk.changes[i - 1]) |
|
||||||
? ' ' |
|
||||||
: extractChangeLine(change)} |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
{/each} |
|
||||||
{/each} |
|
||||||
</div> |
|
||||||
<div class="flex-auto overflow-x-auto"> |
|
||||||
<div class="w-fit"> |
|
||||||
{#each file.chunks as chunk, index} |
|
||||||
{#if index !== 0} |
|
||||||
<div class="flex h-7 w-full bg-base-200"></div> |
|
||||||
{/if} |
|
||||||
{#each chunk.changes as change, i} |
|
||||||
<div class="flex w-full bg-base-100"> |
|
||||||
<div |
|
||||||
class="w-full flex-grow whitespace-pre {change.type == |
|
||||||
'add' |
|
||||||
? 'bg-success/20' |
|
||||||
: change.type == 'del' |
|
||||||
? 'bg-error/20' |
|
||||||
: ''}" |
|
||||||
class:pt-3={index === 0 && i === 0} |
|
||||||
class:pb-3={index === file.chunks.length - 1 && |
|
||||||
i === chunk.changes.length - 1} |
|
||||||
> |
|
||||||
{#if getFortmattedDiffHtml(change, (file.to || file.from) |
|
||||||
?.split('.') |
|
||||||
.pop() || '')} |
|
||||||
<!-- eslint-disable-next-line svelte/no-at-html-tags --> |
|
||||||
{@html getFortmattedDiffHtml( |
|
||||||
change, |
|
||||||
(file.to || file.from)?.split('.').pop() || '' |
|
||||||
)} |
|
||||||
{:else} |
|
||||||
{change.type == 'normal' |
|
||||||
? change.content |
|
||||||
: change.content.substring(1)} |
|
||||||
{/if} |
|
||||||
{#if (change.type == 'normal' ? change.content : change.content.substring(1)).length === 0} |
|
||||||
<!-- force empty line to have height --> |
|
||||||
<span></span> |
|
||||||
{/if} |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
{/each} |
|
||||||
{/each} |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
{/if} |
|
||||||
</div> |
|
||||||
<!-- vertical padding for full width so that content retains it space --> |
|
||||||
{#if expand_full_files[index]} |
|
||||||
<div class="w-full whitespace-pre font-mono text-xs"> |
|
||||||
<span class="block p-3 text-sm"> </span> |
|
||||||
{#each file.chunks as chunk, index} |
|
||||||
{#if index !== 0} |
|
||||||
<span class="block h-7 p-3"> </span> |
|
||||||
{/if} |
|
||||||
{#each chunk.changes as _, i} |
|
||||||
<span |
|
||||||
class="block" |
|
||||||
class:pt-3={index === 0 && i === 0} |
|
||||||
class:pb-3={index === file.chunks.length - 1 && |
|
||||||
i === chunk.changes.length - 1} |
|
||||||
> |
|
||||||
</span> |
|
||||||
{/each} |
|
||||||
{/each} |
|
||||||
</div> |
|
||||||
{/if} |
|
||||||
{/each} |
|
||||||
</div> |
|
||||||
{/if} |
|
||||||
@ -1,19 +0,0 @@ |
|||||||
<script lang="ts"> |
|
||||||
import type { RepoEvent } from '$lib/components/repo/type' |
|
||||||
import { eventToRepoEvent } from '$lib/stores/repos' |
|
||||||
import type { NDKEvent } from '@nostr-dev-kit/ndk' |
|
||||||
|
|
||||||
export let event: NDKEvent | RepoEvent |
|
||||||
|
|
||||||
const isRepoEvent = (event: NDKEvent | RepoEvent): event is RepoEvent => { |
|
||||||
return Object.keys(event).includes('web') |
|
||||||
} |
|
||||||
|
|
||||||
let repo = isRepoEvent(event) ? event : eventToRepoEvent(event) |
|
||||||
</script> |
|
||||||
|
|
||||||
{#if repo} |
|
||||||
<span class=""> |
|
||||||
Git Repository: <a href={`/r/${repo.naddr}`}>{repo.name}</a> by |
|
||||||
</span> |
|
||||||
{/if} |
|
||||||
@ -1,14 +0,0 @@ |
|||||||
<script lang="ts"> |
|
||||||
import Status from '$lib/components/proposals/Status.svelte' |
|
||||||
|
|
||||||
export let type: 'proposal' | 'issue' = 'proposal' |
|
||||||
export let status: number | undefined |
|
||||||
</script> |
|
||||||
|
|
||||||
<div class=""> |
|
||||||
{#if status} |
|
||||||
set status to <Status {type} {status} /> |
|
||||||
{:else} |
|
||||||
set status incorrectly |
|
||||||
{/if} |
|
||||||
</div> |
|
||||||
@ -1,54 +0,0 @@ |
|||||||
import { describe, expect, test } from 'vitest' |
|
||||||
import { extractPatchMessage } from './utils' |
|
||||||
|
|
||||||
// const simple =
|
|
||||||
// const example = `From 35ef1fe53b5a460266a1666709d886560d99cd67 Mon Sep 17 00:00:00 2001\nFrom: fiatjaf <fiatjaf@gmail.com>\nDate: Mon, 29 Jan 2024 09:41:27 -0300\nSubject: [PATCH] fix multi-attempt password prompt.\n\nthe print was doing nothing\nand the continue was missing\n---\nfound this bug while copying these functions to be used in nak\n\n helpers.go | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/helpers.go b/helpers.go\nindex 0b3790d..9b5c3da 100644\n--- a/helpers.go\n+++ b/helpers.go\n@@ -176,7 +176,7 @@ func promptDecrypt(ncryptsec1 string) (string, error) {\n \t\t}\n \t\tsec, err := nip49.Decrypt(ncryptsec1, password)\n \t\tif err != nil {\n-\t\t\tfmt.Fprintf(os.Stderr, "failed to decrypt: %s", err)\n+\t\t\tcontinue\n \t\t}\n \t\treturn sec, nil\n \t}\n--\n2.43.0\n', tags: (3) […], kind: 1617, id: "fd5d1be541bf2d20c51ca63265cc893eecb4be8720db9b42abec21b9ca9747de", sig: "d4733b8b32c05d1fb33a76105926fc537e4060df25405521b3f74f91ed7d65f345386260e8a825c79d67c3dd67f5e7eea7d532cda48cb8d45f09f9be19775289", pubkey: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", … }`
|
|
||||||
|
|
||||||
describe('extractPatchMessage', () => { |
|
||||||
test('extractPatchMessage - normal message end', () => { |
|
||||||
expect( |
|
||||||
extractPatchMessage( |
|
||||||
'From 5ec8fb38b7e4d7b2081e276be456519e2dc76d46 Mon Sep 17 00:00:00 2001\nFrom: fiatjaf <fiatjaf@gmail.com>\nDate: Mon, 29 Jan 2024 09:49:32 -0300\nSubject: [PATCH] invert alias order for `git str send --to`\n\n---\n send.go | 6 +++---\n 1 file changed, 3 insertions(+), 3 deletions(-)\n\ndiff --git a/send.go b/send.go\nindex bc81c00..d9017b6 100644\n--- a/send.go\n+++ b/send.go\n@@ -25,8 +25,8 @@ var send = &cli.Command{\n \t\t\tUsage: "if we should save the secret key to git config --local",\n \t\t},\n \t\t&cli.StringFlag{\n-\t\t\tName: "repository",\n-\t\t\tAliases: []string{"a", "to"},\n+\t\t\tName: "to",\n+\t\t\tAliases: []string{"a", "repository"},\n \t\t\tUsage: "repository reference, as an naddr1... code",\n \t\t},\n \t\t&cli.StringSliceFlag{\n@@ -170,7 +170,7 @@ func getAndApplyTargetRepository(\n \t\treturn nil, nil\n \t}\n \n-\ttarget := c.String("repository")\n+\ttarget := c.String("to")\n \tvar stored string\n \tif target == "" {\n \t\ttarget, _ = git("config", "--local", "str.upstream")\n-- \n2.43.0' |
|
||||||
) |
|
||||||
).toEqual('invert alias order for `git str send --to`') |
|
||||||
}) |
|
||||||
|
|
||||||
test('extractPatchMessage - unusual message end', () => { |
|
||||||
expect( |
|
||||||
extractPatchMessage( |
|
||||||
`From 35ef1fe53b5a460266a1666709d886560d99cd67 Mon Sep 17 00:00:00 2001\nFrom: fiatjaf <fiatjaf@gmail.com>\nDate: Mon, 29 Jan 2024 09:41:27 -0300\nSubject: [PATCH] fix multi-attempt password prompt.\n\nthe print was doing nothing\nand the continue was missing\n---\nfound this bug while copying these functions to be used in nak\n\n helpers.go | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/helpers.go b/helpers.go\nindex 0b3790d..9b5c3da 100644\n--- a/helpers.go\n+++ b/helpers.go\n@@ -176,7 +176,7 @@ func promptDecrypt(ncryptsec1 string) (string, error) {\n \t\t}\n \t\tsec, err := nip49.Decrypt(ncryptsec1, password)\n \t\tif err != nil {\n-\t\t\tfmt.Fprintf(os.Stderr, "failed to decrypt: %s", err)\n+\t\t\tcontinue\n \t\t}\n \t\treturn sec, nil\n \t}\n--\n2.43.0\n` |
|
||||||
) |
|
||||||
).toEqual( |
|
||||||
'fix multi-attempt password prompt.\n\nthe print was doing nothing\nand the continue was missing\n---\nfound this bug while copying these functions to be used in nak' |
|
||||||
) |
|
||||||
}) |
|
||||||
|
|
||||||
test('cover letter', () => { |
|
||||||
expect( |
|
||||||
extractPatchMessage( |
|
||||||
`From 8a45afcacd035de474e142e29cbdfa979d23f751 Mon Sep 17 00:00:00 2001\nSubject: [PATCH 0/2] testing multiple revisions of multi patch proposal with cover letter\n\nhere is the cover letter description` |
|
||||||
) |
|
||||||
).toEqual( |
|
||||||
'testing multiple revisions of multi patch proposal with cover letter\n\nhere is the cover letter description' |
|
||||||
) |
|
||||||
}) |
|
||||||
|
|
||||||
test('extractPatchMessage - returns undefined if not parsed', () => { |
|
||||||
expect( |
|
||||||
extractPatchMessage( |
|
||||||
`From 35ef1fe53b5a460266a1666709d886560d99cd67 Mon Sep 17 00:00:00 2001\nFrom: fiatjaf <fiatjaf@gmail.com>\nDate: Mon, 29 Jan 20` |
|
||||||
) |
|
||||||
).toBeUndefined() |
|
||||||
}) |
|
||||||
|
|
||||||
// TODO make this pass
|
|
||||||
test.skip('extractPatchMessage - anotherunusual message end', () => { |
|
||||||
expect( |
|
||||||
extractPatchMessage( |
|
||||||
`From 1263051aa4426937c5ef4f7616e06e9a8ea021e0 Mon Sep 17 00:00:00 2001\nFrom: William Casarin <jb55@jb55.com>\nDate: Mon, 22 Jan 2024 14:41:54 -0800\nSubject: [PATCH] Revert "mention: fix broken mentions when there is text is\n directly after"\n\nThis reverts commit af75eed83a2a1dd0eb33a0a27ded71c9f44dacbd.\n---\n damus/Views/PostView.swift | 7 -------\n damusTests/PostViewTests.swift | 22 ----------------------\n 2 files changed, 29 deletions(-)\n\ndiff --git a/damus/Views/PostView.swift b/damus/Views/PostView.swift\nindex 21ca0498..934ed7de 100644\n--- a/damus/Views/PostView.swift\n+++ b/damus/Views/PostView.swift\n@@ -619,13 +619,6 @@ func load_draft_for_post(drafts: Drafts, action: PostAction) -> DraftArtifacts?\n func build_post(state: DamusState, post: NSMutableAttributedString, action: PostAction, uploadedMedias: [UploadedMedia], references: [RefId]) -> NostrPost {\n post.enumerateAttributes(in: NSRange(location: 0, length: post.length), options: []) { attributes, range, stop in\n if let link = attributes[.link] as? String {\n- let nextCharIndex = range.upperBound\n- if nextCharIndex < post.length,\n- let nextChar = post.attributedSubstring(from: NSRange(location: nextCharIndex, length: 1)).string.first,\n- !nextChar.isWhitespace {\n- post.insert(NSAttributedString(string: " "), at: nextCharIndex)\n- }\n-\n let normalized_link: String\n if link.hasPrefix("damus:nostr:") {\n // Replace damus:nostr: URI prefix with nostr: since the former is for internal navigation and not meant to be posted.\ndiff --git a/damusTests/PostViewTests.swift b/damusTests/PostViewTests.swift\nindex 51976cad..ae78c3e6 100644\n--- a/damusTests/PostViewTests.swift\n+++ b/damusTests/PostViewTests.swift\n@@ -142,28 +142,6 @@ final class PostViewTests: XCTestCase {\n checkMentionLinkEditorHandling(content: content, replacementText: "", replacementRange: NSRange(location: 5, length: 28), shouldBeAbleToChangeAutomatically: true)\n \n }\n- \n- func testMentionLinkEditorHandling_noWhitespaceAfterLink1_addsWhitespace() {\n- var content: NSMutableAttributedString\n-\n- content = NSMutableAttributedString(string: "Hello @user ")\n- content.addAttribute(.link, value: "damus:1234", range: NSRange(location: 6, length: 5))\n- checkMentionLinkEditorHandling(content: content, replacementText: "up", replacementRange: NSRange(location: 11, length: 1), shouldBeAbleToChangeAutomatically: true, expectedNewCursorIndex: 13, handleNewContent: { newManuallyEditedContent in\n- XCTAssertEqual(newManuallyEditedContent.string, "Hello @user up")\n- XCTAssertNil(newManuallyEditedContent.attribute(.link, at: 11, effectiveRange: nil))\n- })\n- }\n- \n- func testMentionLinkEditorHandling_noWhitespaceAfterLink2_addsWhitespace() {\n- var content: NSMutableAttributedString\n-\n- content = NSMutableAttributedString(string: "Hello @user test")\n- content.addAttribute(.link, value: "damus:1234", range: NSRange(location: 6, length: 5))\n- checkMentionLinkEditorHandling(content: content, replacementText: "up", replacementRange: NSRange(location: 11, length: 1), shouldBeAbleToChangeAutomatically: true, expectedNewCursorIndex: 13, handleNewContent: { newManuallyEditedContent in\n- XCTAssertEqual(newManuallyEditedContent.string, "Hello @user uptest")\n- XCTAssertNil(newManuallyEditedContent.attribute(.link, at: 11, effectiveRange: nil))\n- })\n- }\n }\n \n func checkMentionLinkEditorHandling(\n\nbase-commit: c67741983e3f07f2386eaa388cb8a1475e8e0471\n-- \n2.42.0\n\n` |
|
||||||
) |
|
||||||
).toEqual( |
|
||||||
'Revert "mention: fix broken mentions when there is text is\n directly after"\n\nThis reverts commit af75eed83a2a1dd0eb33a0a27ded71c9f44dacbd.' |
|
||||||
) |
|
||||||
}) |
|
||||||
}) |
|
||||||
@ -1,318 +0,0 @@ |
|||||||
import type { NDKEvent, NDKTag } from '@nostr-dev-kit/ndk' |
|
||||||
import { nip19 } from 'nostr-tools' |
|
||||||
import type { AddressPointer, EventPointer } from 'nostr-tools/nip19' |
|
||||||
import last from 'ramda/src/last' |
|
||||||
|
|
||||||
export const TOPIC = 'topic' |
|
||||||
export const LINKCOLLECTION = 'link[]' |
|
||||||
export const HTML = 'html' |
|
||||||
export const INVOICE = 'invoice' |
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const first = (list: any) => (list ? list[0] : undefined) |
|
||||||
|
|
||||||
export const fromNostrURI = (s: string) => s.replace(/^[\w+]+:\/?\/?/, '') |
|
||||||
|
|
||||||
export const urlIsMedia = (url: string): boolean => |
|
||||||
(!url.match(/\.(apk|docx|xlsx|csv|dmg)/) && |
|
||||||
last(url.split('://'))?.includes('/')) || |
|
||||||
false |
|
||||||
|
|
||||||
export const isImage = (url: string) => |
|
||||||
url.match(/^.*\.(jpg|jpeg|png|webp|gif|avif|svg)/gi) |
|
||||||
export const isVideo = (url: string) => |
|
||||||
url.match(/^.*\.(mov|mkv|mp4|avi|m4v|webm)/gi) |
|
||||||
export const isAudio = (url: string) => url.match(/^.*\.(ogg|mp3|wav)/gi) |
|
||||||
|
|
||||||
export const NEWLINE = 'newline' |
|
||||||
type PartTypeNewLine = 'newline' |
|
||||||
export type ParsedNewLine = { |
|
||||||
type: PartTypeNewLine |
|
||||||
value: string |
|
||||||
} |
|
||||||
|
|
||||||
export const LINK = 'link' |
|
||||||
type PartTypeLink = 'link' |
|
||||||
export type ParsedLink = { |
|
||||||
type: PartTypeLink |
|
||||||
url: string |
|
||||||
is_media: boolean |
|
||||||
imeta: Imeta | undefined |
|
||||||
} |
|
||||||
type Imeta = { |
|
||||||
url: string |
|
||||||
m: string | undefined |
|
||||||
alt: string | undefined |
|
||||||
size: string | undefined |
|
||||||
dim: string | undefined |
|
||||||
x: string | undefined |
|
||||||
fallback: string[] |
|
||||||
blurhash: string | undefined |
|
||||||
} |
|
||||||
|
|
||||||
export const NOSTR_NPUB = 'nostr:npub' |
|
||||||
type PartTypeNpub = 'nostr:npub' |
|
||||||
export type ParsedNpub = { |
|
||||||
type: PartTypeNpub |
|
||||||
hex: string |
|
||||||
} |
|
||||||
export const NOSTR_NPROFILE = 'nostr:nprofile' |
|
||||||
type PartTypeNprofile = 'nostr:nprofile' |
|
||||||
export type ParsedNprofile = { |
|
||||||
type: PartTypeNprofile |
|
||||||
hex: string |
|
||||||
relays: string[] |
|
||||||
} |
|
||||||
|
|
||||||
export const NOSTR_NOTE = 'nostr:note' |
|
||||||
type PartTypeNote = 'nostr:note' |
|
||||||
export type ParsedNote = { |
|
||||||
type: PartTypeNote |
|
||||||
data: EventPointer |
|
||||||
} |
|
||||||
|
|
||||||
export const NOSTR_NEVENT = 'nostr:nevent' |
|
||||||
type PartTypeNevent = 'nostr:nevent' |
|
||||||
export type ParsedNevent = { |
|
||||||
type: PartTypeNevent |
|
||||||
data: EventPointer |
|
||||||
} |
|
||||||
|
|
||||||
export const NOSTR_NADDR = 'nostr:naddr' |
|
||||||
type PartTypeNaddr = 'nostr:naddr' |
|
||||||
export type ParsedNaddr = { |
|
||||||
type: PartTypeNaddr |
|
||||||
data: AddressPointer |
|
||||||
} |
|
||||||
|
|
||||||
export type ParsedNostrLink = |
|
||||||
| ParsedNpub |
|
||||||
| ParsedNprofile |
|
||||||
| ParsedNevent |
|
||||||
| ParsedNote |
|
||||||
| ParsedNaddr |
|
||||||
|
|
||||||
export const TEXT = 'text' |
|
||||||
type PartTypeText = 'text' |
|
||||||
export type ParsedText = { |
|
||||||
type: PartTypeText |
|
||||||
value: string |
|
||||||
} |
|
||||||
|
|
||||||
export type ParsedPart = |
|
||||||
| ParsedNewLine |
|
||||||
| ParsedText |
|
||||||
| ParsedNostrLink |
|
||||||
| ParsedLink |
|
||||||
|
|
||||||
export const isParsedNewLine = (part: ParsedPart): part is ParsedNewLine => |
|
||||||
part.type == NEWLINE |
|
||||||
|
|
||||||
export const isParsedLink = (part: ParsedPart): part is ParsedLink => |
|
||||||
part.type == LINK |
|
||||||
|
|
||||||
export const isParsedNostrLink = (part: ParsedPart): part is ParsedNostrLink => |
|
||||||
part.type == NOSTR_NPUB || |
|
||||||
part.type == NOSTR_NPROFILE || |
|
||||||
part.type == NOSTR_NEVENT || |
|
||||||
part.type == NOSTR_NOTE || |
|
||||||
part.type == NOSTR_NADDR |
|
||||||
|
|
||||||
export const isParsedNpub = (part: ParsedPart): part is ParsedNpub => |
|
||||||
part.type == NOSTR_NPUB |
|
||||||
|
|
||||||
export const isParsedNprofile = (part: ParsedPart): part is ParsedNprofile => |
|
||||||
part.type == NOSTR_NPROFILE |
|
||||||
|
|
||||||
export const isParsedNevent = (part: ParsedPart): part is ParsedNevent => |
|
||||||
part.type == NOSTR_NEVENT |
|
||||||
|
|
||||||
export const isParsedNote = (part: ParsedPart): part is ParsedNote => |
|
||||||
part.type == NOSTR_NOTE |
|
||||||
|
|
||||||
export const isParsedNaddr = (part: ParsedPart): part is ParsedNaddr => |
|
||||||
part.type == NOSTR_NADDR |
|
||||||
|
|
||||||
export const isParsedText = (part: ParsedPart): part is ParsedText => |
|
||||||
part.type == TEXT |
|
||||||
|
|
||||||
export const parseContent = (content: string, tags: NDKTag[]): ParsedPart[] => { |
|
||||||
const result: ParsedPart[] = [] |
|
||||||
let text = content.trim() |
|
||||||
let buffer = '' |
|
||||||
|
|
||||||
const getIMeta = (url: string): undefined | Imeta => { |
|
||||||
const imeta_tag_for_url = tags.find( |
|
||||||
(tag) => tag[0] === 'imeta' && tag.some((e) => e.includes(url)) |
|
||||||
) |
|
||||||
if (!imeta_tag_for_url) return undefined |
|
||||||
const pairs = imeta_tag_for_url.map((s) => [ |
|
||||||
s.split(' ')[0], |
|
||||||
s.substring(s.indexOf(' ') + 1), |
|
||||||
]) |
|
||||||
return { |
|
||||||
url, |
|
||||||
m: pairs.find((p) => p[0] === 'm')?.[1], |
|
||||||
alt: pairs.find((p) => p[0] === 'alt')?.[1], |
|
||||||
x: pairs.find((p) => p[0] === 'x')?.[1], |
|
||||||
size: pairs.find((p) => p[0] === 'size')?.[1], |
|
||||||
dim: pairs.find((p) => p[0] === 'dim')?.[1], |
|
||||||
blurhash: pairs.find((p) => p[0] === 'blurhash')?.[1], |
|
||||||
fallback: pairs.filter((p) => p[0] === 'fallback')?.map((p) => p[1]), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
const parseNewline = (): undefined | [string, ParsedNewLine] => { |
|
||||||
const newline: string = first(text.match(/^\n+/)) |
|
||||||
|
|
||||||
if (newline) { |
|
||||||
return [newline, { type: NEWLINE, value: newline }] |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
const parseUrl = (): undefined | [string, ParsedLink] => { |
|
||||||
const raw: string = first( |
|
||||||
text.match( |
|
||||||
/^([a-z\+:]{2,30}:\/\/)?[^<>\(\)\s]+\.[a-z]{2,6}[^\s]*[^<>"'\.!?,:\s\)\(]/gi |
|
||||||
) |
|
||||||
) |
|
||||||
|
|
||||||
// Skip url if it's just the end of a filepath
|
|
||||||
if (!raw) { |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
const prev = last(result) |
|
||||||
|
|
||||||
if (prev?.type === TEXT && prev.value.endsWith('/')) { |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
let url = raw |
|
||||||
|
|
||||||
// Skip ellipses and very short non-urls
|
|
||||||
if (url.match(/\.\./)) { |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
if (!url.match('://')) { |
|
||||||
url = 'https://' + url |
|
||||||
} |
|
||||||
|
|
||||||
return [ |
|
||||||
raw, |
|
||||||
{ type: LINK, url, is_media: urlIsMedia(url), imeta: getIMeta(url) }, |
|
||||||
] |
|
||||||
} |
|
||||||
|
|
||||||
const parseNostrLinks = (): undefined | [string, ParsedNostrLink] => { |
|
||||||
const bech32: string = first( |
|
||||||
text.match( |
|
||||||
/^(web\+)?(nostr:)?\/?\/?n(event|ote|profile|pub|addr)1[\d\w]+/i |
|
||||||
) |
|
||||||
) |
|
||||||
if (bech32) { |
|
||||||
try { |
|
||||||
const entity = fromNostrURI(bech32) |
|
||||||
const decoded = nip19.decode(entity) |
|
||||||
if (decoded.type === 'npub') { |
|
||||||
return [bech32, { type: NOSTR_NPUB, hex: decoded.data }] |
|
||||||
} |
|
||||||
if (decoded.type === 'nprofile') { |
|
||||||
return [bech32, { type: NOSTR_NPUB, hex: decoded.data.pubkey }] |
|
||||||
} |
|
||||||
if (decoded.type === 'note') { |
|
||||||
return [bech32, { type: NOSTR_NOTE, data: { id: decoded.data } }] |
|
||||||
} |
|
||||||
if (decoded.type === 'nevent') { |
|
||||||
return [bech32, { type: NOSTR_NEVENT, data: decoded.data }] |
|
||||||
} |
|
||||||
if (decoded.type === 'naddr') { |
|
||||||
return [bech32, { type: NOSTR_NADDR, data: decoded.data }] |
|
||||||
} |
|
||||||
} catch {} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
while (text) { |
|
||||||
// The order that this runs matters
|
|
||||||
const part = parseNewline() || parseUrl() || parseNostrLinks() |
|
||||||
|
|
||||||
if (part) { |
|
||||||
if (buffer) { |
|
||||||
result.push({ type: TEXT, value: buffer }) |
|
||||||
buffer = '' |
|
||||||
} |
|
||||||
|
|
||||||
const [raw, parsed] = part |
|
||||||
|
|
||||||
result.push(parsed) |
|
||||||
text = text.slice(raw.length) |
|
||||||
} else { |
|
||||||
// Instead of going character by character and re-running all the above regular expressions
|
|
||||||
// a million times, try to match the next word and add it to the buffer
|
|
||||||
const match = first(text.match(/^[\w\d]+ ?/i)) || text[0] |
|
||||||
|
|
||||||
buffer += match |
|
||||||
text = text.slice(match.length) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if (buffer) { |
|
||||||
result.push({ type: TEXT, value: buffer }) |
|
||||||
} |
|
||||||
|
|
||||||
return result |
|
||||||
} |
|
||||||
|
|
||||||
export const isCoverLetter = (s: string): boolean => { |
|
||||||
return s.indexOf('PATCH 0/') > 0 |
|
||||||
} |
|
||||||
|
|
||||||
export function extractTagContent( |
|
||||||
name: string, |
|
||||||
tags: NDKTag[] |
|
||||||
): string | undefined { |
|
||||||
const tag = tags.find((tag) => tag[0] === name) |
|
||||||
return tag ? tag[1] : undefined |
|
||||||
} |
|
||||||
|
|
||||||
/** this doesn't work for all patch formats and options */ |
|
||||||
export const extractPatchMessage = (s: string): string | undefined => { |
|
||||||
try { |
|
||||||
if (isCoverLetter(s)) { |
|
||||||
return s.substring(s.indexOf('] ') + 2) |
|
||||||
} |
|
||||||
const t = s.split('\nSubject: [')[1].split('] ')[1] |
|
||||||
if (t.split('\n\n---\n ').length > 1) return t.split('\n\n---\n ')[0] |
|
||||||
return t.split('\n\ndiff --git ')[0].split('\n\n ').slice(0, -1).join('') |
|
||||||
} catch { |
|
||||||
return undefined |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/** this doesn't work for all patch formats and options */ |
|
||||||
export const extractPatchTitle = (s: string): string | undefined => { |
|
||||||
const msg = extractPatchMessage(s) |
|
||||||
if (!msg) return undefined |
|
||||||
return s.split('\n')[0] |
|
||||||
} |
|
||||||
|
|
||||||
/** patch message without first line */ |
|
||||||
export const extractPatchDescription = (s: string): string | undefined => { |
|
||||||
const msg = extractPatchMessage(s) |
|
||||||
if (!msg) return '' |
|
||||||
const i = msg.indexOf('\n') |
|
||||||
if (i === -1) return '' |
|
||||||
return msg.substring(i).trim() |
|
||||||
} |
|
||||||
|
|
||||||
export const extractIssueTitle = (event: NDKEvent): string => { |
|
||||||
return event.tagValue('subject') || event.content.split('\n')[0] || '' |
|
||||||
} |
|
||||||
|
|
||||||
export const extractIssueDescription = (s: string): string => { |
|
||||||
const split = s.split('\n') |
|
||||||
if (split.length === 0) return '' |
|
||||||
return s.substring(split[0].length) || '' |
|
||||||
} |
|
||||||
@ -1,12 +0,0 @@ |
|||||||
import type { NDKEvent } from '@nostr-dev-kit/ndk' |
|
||||||
import type { User } from '../users/type' |
|
||||||
|
|
||||||
export interface Event { |
|
||||||
author: User |
|
||||||
content: unknown |
|
||||||
} |
|
||||||
|
|
||||||
export interface ThreadTreeNode { |
|
||||||
event: NDKEvent |
|
||||||
child_nodes: ThreadTreeNode[] |
|
||||||
} |
|
||||||
@ -1,27 +0,0 @@ |
|||||||
export const icons_misc = { |
|
||||||
chevron_down: [ |
|
||||||
'M6 8.825c-.2 0-.4-.1-.5-.2l-3.3-3.3c-.3-.3-.3-.8 0-1.1c.3-.3.8-.3 1.1 0l2.7 2.7l2.7-2.7c.3-.3.8-.3 1.1 0c.3.3.3.8 0 1.1l-3.2 3.2c-.2.2-.4.3-.6.3', |
|
||||||
], |
|
||||||
chevron_up: [ |
|
||||||
'M6 4c-.2 0-.4.1-.5.2L2.2 7.5c-.3.3-.3.8 0 1.1c.3.3.8.3 1.1 0L6 5.9l2.7 2.7c.3.3.8.3 1.1 0c.3-.3.3-.8 0-1.1L6.6 4.3C6.4 4.1 6.2 4 6 4', |
|
||||||
], |
|
||||||
// https://icon-sets.iconify.design/octicon/copy-16/
|
|
||||||
copy: [ |
|
||||||
'M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z', |
|
||||||
'M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z', |
|
||||||
], |
|
||||||
// https://icon-sets.iconify.design/octicon/key-16/ MIT licence
|
|
||||||
key: [ |
|
||||||
'M10.5 0a5.499 5.499 0 1 1-1.288 10.848l-.932.932a.75.75 0 0 1-.53.22H7v.75a.75.75 0 0 1-.22.53l-.5.5a.75.75 0 0 1-.53.22H5v.75a.75.75 0 0 1-.22.53l-.5.5a.75.75 0 0 1-.53.22h-2A1.75 1.75 0 0 1 0 14.25v-2c0-.199.079-.389.22-.53l4.932-4.932A5.5 5.5 0 0 1 10.5 0m-4 5.5c-.001.431.069.86.205 1.269a.75.75 0 0 1-.181.768L1.5 12.56v1.69c0 .138.112.25.25.25h1.69l.06-.06v-1.19a.75.75 0 0 1 .75-.75h1.19l.06-.06v-1.19a.75.75 0 0 1 .75-.75h1.19l1.023-1.025a.75.75 0 0 1 .768-.18A4 4 0 1 0 6.5 5.5M11 6a1 1 0 1 1 0-2a1 1 0 0 1 0 2', |
|
||||||
], |
|
||||||
// https://icon-sets.iconify.design/clarity/lightning-solid/ MIT licence
|
|
||||||
lightning: [ |
|
||||||
'M5.52.359A.5.5 0 0 1 6 0h4a.5.5 0 0 1 .474.658L8.694 6H12.5a.5.5 0 0 1 .395.807l-7 9a.5.5 0 0 1-.873-.454L6.823 9.5H3.5a.5.5 0 0 1-.48-.641z', |
|
||||||
], |
|
||||||
info: [ |
|
||||||
'M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8m8-6.5a6.5 6.5 0 1 0 0 13a6.5 6.5 0 0 0 0-13M6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75M8 6a1 1 0 1 1 0-2a1 1 0 0 1 0 2', |
|
||||||
], |
|
||||||
link: [ |
|
||||||
'm7.775 3.275l1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0a.75.75 0 0 1 .018-1.042a.75.75 0 0 1 1.042-.018a2 2 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.75.75 0 0 1-1.042-.018a.75.75 0 0 1-.018-1.042m-4.69 9.64a2 2 0 0 0 2.83 0l1.25-1.25a.75.75 0 0 1 1.042.018a.75.75 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0a.75.75 0 0 1-.018 1.042a.75.75 0 0 1-1.042.018a2 2 0 0 0-2.83 0l-2.5 2.5a2 2 0 0 0 0 2.83', |
|
||||||
], |
|
||||||
} |
|
||||||
@ -1,18 +0,0 @@ |
|||||||
// icon are MIT licenced
|
|
||||||
export const issue_icon_path = { |
|
||||||
// https://icon-sets.iconify.design/octicon/issue-opened-16/
|
|
||||||
open: [ |
|
||||||
'M8 9.5a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3', |
|
||||||
'M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0M1.5 8a6.5 6.5 0 1 0 13 0a6.5 6.5 0 0 0-13 0', |
|
||||||
], |
|
||||||
// https://icon-sets.iconify.design/octicon/issue-closed-16/
|
|
||||||
resolved: [ |
|
||||||
'M11.28 6.78a.75.75 0 0 0-1.06-1.06L7.25 8.69L5.78 7.22a.75.75 0 0 0-1.06 1.06l2 2a.75.75 0 0 0 1.06 0z', |
|
||||||
'M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-1.5 0a6.5 6.5 0 1 0-13 0a6.5 6.5 0 0 0 13 0', |
|
||||||
], |
|
||||||
// https://icon-sets.iconify.design/octicon/no-entry-16/
|
|
||||||
closed: [ |
|
||||||
'M4.25 7.25a.75.75 0 0 0 0 1.5h7.5a.75.75 0 0 0 0-1.5z', |
|
||||||
'M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-1.5 0a6.5 6.5 0 1 0-13 0a6.5 6.5 0 0 0 13 0', |
|
||||||
], |
|
||||||
} |
|
||||||
@ -1,60 +0,0 @@ |
|||||||
import type { User } from '../users/type' |
|
||||||
import { defaults as user_defaults } from '../users/type' |
|
||||||
import type { Event } from '../events/type' |
|
||||||
import type { NDKEvent } from '@nostr-dev-kit/ndk' |
|
||||||
|
|
||||||
export interface IssueSummary { |
|
||||||
type: 'issue' |
|
||||||
title: string |
|
||||||
descritpion: string |
|
||||||
repo_a: string |
|
||||||
id: string |
|
||||||
comments: number |
|
||||||
status: undefined | number |
|
||||||
status_date: number |
|
||||||
author: User |
|
||||||
created_at: number | undefined |
|
||||||
loading: boolean |
|
||||||
} |
|
||||||
|
|
||||||
export const summary_defaults: IssueSummary = { |
|
||||||
type: 'issue', |
|
||||||
title: '', |
|
||||||
descritpion: '', |
|
||||||
repo_a: '', |
|
||||||
id: '', |
|
||||||
comments: 0, |
|
||||||
status: undefined, |
|
||||||
status_date: 0, |
|
||||||
author: { ...user_defaults }, |
|
||||||
created_at: 0, |
|
||||||
loading: true, |
|
||||||
} |
|
||||||
|
|
||||||
export interface IssueSummaries { |
|
||||||
repo_a: string | undefined |
|
||||||
summaries: IssueSummary[] |
|
||||||
loading: boolean |
|
||||||
} |
|
||||||
|
|
||||||
export const summaries_defaults: IssueSummaries = { |
|
||||||
repo_a: '', |
|
||||||
summaries: [], |
|
||||||
loading: true, |
|
||||||
} |
|
||||||
|
|
||||||
export interface IssueFull { |
|
||||||
summary: IssueSummary |
|
||||||
issue_event: NDKEvent | undefined |
|
||||||
labels: string[] |
|
||||||
events: Event[] |
|
||||||
loading: boolean |
|
||||||
} |
|
||||||
|
|
||||||
export const full_defaults: IssueFull = { |
|
||||||
summary: { ...summary_defaults }, |
|
||||||
issue_event: undefined, |
|
||||||
labels: [], |
|
||||||
events: [], |
|
||||||
loading: true, |
|
||||||
} |
|
||||||
@ -1,50 +0,0 @@ |
|||||||
<script lang="ts"> |
|
||||||
import { full_defaults, summary_defaults, type ProposalSummary } from './type' |
|
||||||
import UserHeader from '../users/UserHeader.svelte' |
|
||||||
import StatusSelector from './StatusSelector.svelte' |
|
||||||
import type { IssueSummary } from '../issues/type' |
|
||||||
|
|
||||||
export let type: 'proposal' | 'issue' = 'proposal' |
|
||||||
export let summary: ProposalSummary | IssueSummary = { ...summary_defaults } |
|
||||||
export let { labels, loading } = { ...full_defaults } |
|
||||||
</script> |
|
||||||
|
|
||||||
<div class="max-w-md"> |
|
||||||
<div> |
|
||||||
{#if loading} |
|
||||||
<div class="skeleton my-3 h-5 w-20"></div> |
|
||||||
<div class="badge skeleton my-2 block w-60"></div> |
|
||||||
<div class="badge skeleton my-2 block w-40"></div> |
|
||||||
{:else} |
|
||||||
<h4>Author</h4> |
|
||||||
<UserHeader user={summary.author} /> |
|
||||||
{/if} |
|
||||||
</div> |
|
||||||
|
|
||||||
<div> |
|
||||||
{#if loading} |
|
||||||
<div class="skeleton my-3 h-5 w-20"></div> |
|
||||||
<div class="badge skeleton my-2 block w-60"></div> |
|
||||||
<div class="badge skeleton my-2 block w-40"></div> |
|
||||||
{:else} |
|
||||||
<h4>Status</h4> |
|
||||||
<StatusSelector |
|
||||||
{type} |
|
||||||
status={summary.status} |
|
||||||
proposal_or_issue_id={summary.id} |
|
||||||
/> |
|
||||||
{/if} |
|
||||||
</div> |
|
||||||
|
|
||||||
<div> |
|
||||||
{#if loading} |
|
||||||
<div class="badge skeleton w-20"></div> |
|
||||||
<div class="badge skeleton w-20"></div> |
|
||||||
{:else} |
|
||||||
<h4>Labels</h4> |
|
||||||
{#each labels as label} |
|
||||||
<div class="badge badge-secondary mr-2">{label}</div> |
|
||||||
{/each} |
|
||||||
{/if} |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
@ -1,26 +0,0 @@ |
|||||||
<script lang="ts" context="module"> |
|
||||||
import type { Meta } from '@storybook/svelte' |
|
||||||
import ProposalHeader from './ProposalHeader.svelte' |
|
||||||
import { Story, Template } from '@storybook/addon-svelte-csf' |
|
||||||
import { ProposalsListItemArgsVectors as vectors } from './vectors' |
|
||||||
|
|
||||||
export const meta: Meta<ProposalHeader> = { |
|
||||||
title: 'Proposals/Header', |
|
||||||
component: ProposalHeader, |
|
||||||
tags: ['autodocs'], |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<Template let:args> |
|
||||||
<ProposalHeader {...args} /> |
|
||||||
</Template> |
|
||||||
|
|
||||||
<Story name="Short Details" args={vectors.Short} /> |
|
||||||
|
|
||||||
<Story name="Long Details" args={vectors.Long} /> |
|
||||||
|
|
||||||
<Story name="Long and No Spaces" args={vectors.LongNoSpaces} /> |
|
||||||
|
|
||||||
<Story name="Author Loading" args={vectors.AuthorLoading} /> |
|
||||||
|
|
||||||
<Story name="loading" args={{ loading: true }} /> |
|
||||||
@ -1,104 +0,0 @@ |
|||||||
<script lang="ts" context="module"> |
|
||||||
</script> |
|
||||||
|
|
||||||
<script lang="ts"> |
|
||||||
import dayjs from 'dayjs' |
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime' |
|
||||||
import { summary_defaults } from './type' |
|
||||||
import { |
|
||||||
defaults as user_defaults, |
|
||||||
getName, |
|
||||||
type UserObject, |
|
||||||
} from '../users/type' |
|
||||||
import Container from '../Container.svelte' |
|
||||||
import Status from './Status.svelte' |
|
||||||
import { ensureUser, logged_in_user } from '$lib/stores/users' |
|
||||||
import type { Unsubscriber } from 'svelte/store' |
|
||||||
import { onDestroy } from 'svelte' |
|
||||||
import StatusSelector from './StatusSelector.svelte' |
|
||||||
|
|
||||||
dayjs.extend(relativeTime) |
|
||||||
export let type: 'proposal' | 'issue' = 'proposal' |
|
||||||
export let { |
|
||||||
title, |
|
||||||
descritpion, |
|
||||||
repo_a, |
|
||||||
id, |
|
||||||
comments, |
|
||||||
status, |
|
||||||
status_date, |
|
||||||
author, |
|
||||||
created_at, |
|
||||||
loading, |
|
||||||
} = summary_defaults |
|
||||||
let short_title: string |
|
||||||
let created_at_ago: string |
|
||||||
let author_name = '' |
|
||||||
let author_object: UserObject = { |
|
||||||
...user_defaults, |
|
||||||
} |
|
||||||
let unsubscriber: Unsubscriber |
|
||||||
$: { |
|
||||||
if (typeof author === 'string') { |
|
||||||
if (unsubscriber) unsubscriber() |
|
||||||
unsubscriber = ensureUser(author).subscribe((u) => { |
|
||||||
author_object = { ...u } |
|
||||||
}) |
|
||||||
} else author_object = author |
|
||||||
} |
|
||||||
onDestroy(() => { |
|
||||||
if (unsubscriber) unsubscriber() |
|
||||||
}) |
|
||||||
$: { |
|
||||||
author_name = getName(author_object) |
|
||||||
} |
|
||||||
$: { |
|
||||||
if (title.length > 70) short_title = title.slice(0, 65) + '...' |
|
||||||
else if (title.length == 0) short_title = 'Untitled' |
|
||||||
else short_title = title |
|
||||||
created_at_ago = created_at ? dayjs(created_at * 1000).fromNow() : '' |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<div |
|
||||||
class="grow border-b border-accent-content bg-base-200 pb-4 pt-2 text-xs text-neutral-content" |
|
||||||
> |
|
||||||
<Container> |
|
||||||
{#if loading} |
|
||||||
<div> |
|
||||||
<div class="skeleton h-7 w-60 pt-1"></div> |
|
||||||
<div class=""> |
|
||||||
<div class="skeleton mt-3 inline-block h-8 w-20 align-middle"></div> |
|
||||||
<div |
|
||||||
class="skeleton ml-3 mt-5 inline-block h-3 w-28 align-middle" |
|
||||||
></div> |
|
||||||
<div |
|
||||||
class="skeleton ml-3 mt-5 inline-block h-3 w-28 align-middle" |
|
||||||
></div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
{:else} |
|
||||||
<div class="mb-2 text-lg text-base-content"> |
|
||||||
{short_title} |
|
||||||
</div> |
|
||||||
<div class="pt-1"> |
|
||||||
<div class="mr-3 inline align-middle"> |
|
||||||
{#if !$logged_in_user} |
|
||||||
<Status {type} {status} edit_mode={false} /> |
|
||||||
{:else} |
|
||||||
<StatusSelector {type} {status} proposal_or_issue_id={id} />{/if} |
|
||||||
</div> |
|
||||||
<div class="mr-3 inline align-middle"> |
|
||||||
opened {created_at_ago} |
|
||||||
</div> |
|
||||||
<div class="inline align-middle"> |
|
||||||
{#if author_object.loading} |
|
||||||
<div class="skeleton inline-block h-3 w-20 pb-2"></div> |
|
||||||
{:else} |
|
||||||
{author_name} |
|
||||||
{/if} |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
{/if} |
|
||||||
</Container> |
|
||||||
</div> |
|
||||||
@ -1,56 +0,0 @@ |
|||||||
<script lang="ts" context="module"> |
|
||||||
import type { Meta } from '@storybook/svelte' |
|
||||||
import ProposalsList from './ProposalsList.svelte' |
|
||||||
import { Story, Template } from '@storybook/addon-svelte-csf' |
|
||||||
import { ProposalsListItemArgsVectors as vectors } from './vectors' |
|
||||||
|
|
||||||
export const meta: Meta<ProposalsList> = { |
|
||||||
title: 'Proposals/List/List', |
|
||||||
component: ProposalsList, |
|
||||||
tags: ['autodocs'], |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<Template let:args> |
|
||||||
<ProposalsList {...args} /> |
|
||||||
</Template> |
|
||||||
|
|
||||||
<Story |
|
||||||
name="Default" |
|
||||||
args={{ |
|
||||||
title: 'Open Proposals', |
|
||||||
proposals_or_issues: [vectors.Short, vectors.Long, vectors.LongNoSpaces], |
|
||||||
}} |
|
||||||
/> |
|
||||||
|
|
||||||
<Story |
|
||||||
name="No Title" |
|
||||||
args={{ |
|
||||||
proposals_or_issues: [vectors.Short, vectors.Long], |
|
||||||
}} |
|
||||||
/> |
|
||||||
<Story |
|
||||||
name="Empty" |
|
||||||
args={{ |
|
||||||
title: 'Open Proposals', |
|
||||||
proposals_or_issues: [], |
|
||||||
}} |
|
||||||
/> |
|
||||||
|
|
||||||
<Story |
|
||||||
name="Loading" |
|
||||||
args={{ |
|
||||||
title: 'Open Proposals', |
|
||||||
proposals_or_issues: [], |
|
||||||
loading: true, |
|
||||||
}} |
|
||||||
/> |
|
||||||
|
|
||||||
<Story |
|
||||||
name="Partially Loaded" |
|
||||||
args={{ |
|
||||||
title: 'Open Proposals', |
|
||||||
proposals_or_issues: [vectors.Short, vectors.Long], |
|
||||||
loading: true, |
|
||||||
}} |
|
||||||
/> |
|
||||||
@ -1,47 +0,0 @@ |
|||||||
<script lang="ts"> |
|
||||||
import ProposalsListItem from '$lib/components/proposals/ProposalsListItem.svelte' |
|
||||||
import type { IssueSummary } from '../issues/type' |
|
||||||
import type { ProposalSummary } from './type' |
|
||||||
|
|
||||||
export let title: string = '' |
|
||||||
export let proposals_or_issues: ProposalSummary[] | IssueSummary[] = [] |
|
||||||
export let repo_naddr_override: string | undefined = undefined |
|
||||||
export let loading: boolean = false |
|
||||||
export let show_repo: boolean = false |
|
||||||
export let limit: number = 0 |
|
||||||
export let allow_more = true |
|
||||||
export let sort_youngest_first = true |
|
||||||
let current_limit = limit |
|
||||||
</script> |
|
||||||
|
|
||||||
<div class=""> |
|
||||||
{#if title.length > 0} |
|
||||||
<div class="prose"> |
|
||||||
<h4>{title}</h4> |
|
||||||
</div> |
|
||||||
{/if} |
|
||||||
{#if proposals_or_issues.length == 0 && !loading} |
|
||||||
<p class="prose">None</p> |
|
||||||
{/if} |
|
||||||
<ul class=" divide-y divide-base-400"> |
|
||||||
{#each sort_youngest_first ? proposals_or_issues.sort((a, b) => (b.created_at || 0) - (a.created_at || 0)) : proposals_or_issues as proposal, index} |
|
||||||
{#if current_limit === 0 || index + 1 <= current_limit} |
|
||||||
<ProposalsListItem {...proposal} {repo_naddr_override} {show_repo} /> |
|
||||||
{/if} |
|
||||||
{/each} |
|
||||||
{#if loading} |
|
||||||
<ProposalsListItem loading={true} /> |
|
||||||
{#if proposals_or_issues.length == 0} |
|
||||||
<ProposalsListItem loading={true} /> |
|
||||||
<ProposalsListItem loading={true} /> |
|
||||||
{/if} |
|
||||||
{:else if allow_more && limit !== 0 && proposals_or_issues.length > current_limit} |
|
||||||
<button |
|
||||||
on:click={() => { |
|
||||||
current_limit = current_limit + 5 |
|
||||||
}} |
|
||||||
class="btn mt-3 p-3 font-normal">more</button |
|
||||||
> |
|
||||||
{/if} |
|
||||||
</ul> |
|
||||||
</div> |
|
||||||
@ -1,34 +0,0 @@ |
|||||||
<script lang="ts" context="module"> |
|
||||||
import type { Meta } from '@storybook/svelte' |
|
||||||
import ProposalsListItem from './ProposalsListItem.svelte' |
|
||||||
import { Story, Template } from '@storybook/addon-svelte-csf' |
|
||||||
import { ProposalsListItemArgsVectors as vectors } from './vectors' |
|
||||||
|
|
||||||
export const meta: Meta<ProposalsListItem> = { |
|
||||||
title: 'Proposals/List/Item', |
|
||||||
component: ProposalsListItem, |
|
||||||
tags: ['autodocs'], |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<Template let:args> |
|
||||||
<ProposalsListItem {...args} /> |
|
||||||
</Template> |
|
||||||
|
|
||||||
<Story name="Short Details" args={vectors.Short} /> |
|
||||||
|
|
||||||
<Story name="Long Details" args={vectors.Long} /> |
|
||||||
|
|
||||||
<Story name="Long and No Spaces" args={vectors.LongNoSpaces} /> |
|
||||||
|
|
||||||
<Story name="Author Loading" args={vectors.AuthorLoading} /> |
|
||||||
|
|
||||||
<Story name="Status Loading" args={vectors.StatusLoading} /> |
|
||||||
|
|
||||||
<Story name="Status Draft" args={vectors.StatusDraft} /> |
|
||||||
|
|
||||||
<Story name="Status Closed" args={vectors.StatusClosed} /> |
|
||||||
|
|
||||||
<Story name="Status Applied" args={vectors.StatusApplied} /> |
|
||||||
|
|
||||||
<Story name="loading" args={{ loading: true }} /> |
|
||||||
@ -1,167 +0,0 @@ |
|||||||
<script lang="ts" context="module"> |
|
||||||
</script> |
|
||||||
|
|
||||||
<script lang="ts"> |
|
||||||
import dayjs from 'dayjs' |
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime' |
|
||||||
import { summary_defaults } from './type' |
|
||||||
import { proposal_icon_path } from './icons' |
|
||||||
import UserHeader from '../users/UserHeader.svelte' |
|
||||||
import { |
|
||||||
proposal_status_applied, |
|
||||||
proposal_status_closed, |
|
||||||
proposal_status_draft, |
|
||||||
proposal_status_open, |
|
||||||
} from '$lib/kinds' |
|
||||||
import { issue_icon_path } from '../issues/icons' |
|
||||||
import { aToNaddr, naddrToPointer } from '../repo/utils' |
|
||||||
import { nip19 } from 'nostr-tools' |
|
||||||
|
|
||||||
dayjs.extend(relativeTime) |
|
||||||
export let type: 'issue' | 'proposal' = 'proposal' |
|
||||||
|
|
||||||
export let { |
|
||||||
title, |
|
||||||
descritpion, |
|
||||||
id, |
|
||||||
repo_a, |
|
||||||
comments, |
|
||||||
status, |
|
||||||
status_date, |
|
||||||
author, |
|
||||||
created_at, |
|
||||||
loading, |
|
||||||
} = summary_defaults |
|
||||||
export let show_repo: boolean = false |
|
||||||
export let repo_naddr_override: string | undefined = undefined |
|
||||||
let short_title: string |
|
||||||
let created_at_ago: string |
|
||||||
$: { |
|
||||||
if (title.length > 70) short_title = title.slice(0, 65) + '...' |
|
||||||
else if (title.length == 0) short_title = 'Untitled' |
|
||||||
else short_title = title |
|
||||||
created_at_ago = created_at ? dayjs(created_at * 1000).fromNow() : '' |
|
||||||
} |
|
||||||
let repo_naddr = '' |
|
||||||
let repo_identifier = '' |
|
||||||
$: { |
|
||||||
if (repo_a.length > 0) { |
|
||||||
repo_naddr = repo_naddr_override || aToNaddr(repo_a) || '' |
|
||||||
if (repo_naddr_override) { |
|
||||||
repo_identifier = naddrToPointer(repo_naddr)?.identifier || '' |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<li class="flex p-2 pt-4 {!loading ? 'cursor-pointer hover:bg-base-200' : ''}"> |
|
||||||
<!-- <figure class="p-4 pl-0 text-color-primary"> --> |
|
||||||
<!-- http://icon-sets.iconify.design/octicon/git-pull-request-16/ --> |
|
||||||
{#if loading || !status} |
|
||||||
<div class="skeleton h-5 w-5 flex-none pt-1"></div> |
|
||||||
{:else if status === proposal_status_open} |
|
||||||
<svg |
|
||||||
xmlns="http://www.w3.org/2000/svg" |
|
||||||
viewBox="0 0 16 16" |
|
||||||
class="h-5 w-5 flex-none fill-success pt-1" |
|
||||||
> |
|
||||||
{#if type === 'proposal'} |
|
||||||
<path d={proposal_icon_path.open_patch} /> |
|
||||||
{:else if type === 'issue'} |
|
||||||
{#each issue_icon_path.open as p} |
|
||||||
<path d={p} /> |
|
||||||
{/each} |
|
||||||
{/if} |
|
||||||
</svg> |
|
||||||
{:else if status === proposal_status_closed} |
|
||||||
<svg |
|
||||||
xmlns="http://www.w3.org/2000/svg" |
|
||||||
viewBox="0 0 16 16" |
|
||||||
class="h-5 w-5 flex-none fill-neutral-content pt-1" |
|
||||||
> |
|
||||||
{#if type === 'proposal'} |
|
||||||
<path d={proposal_icon_path.close} /> |
|
||||||
{:else if type === 'issue'} |
|
||||||
{#each issue_icon_path.closed as p} |
|
||||||
<path d={p} /> |
|
||||||
{/each} |
|
||||||
{/if} |
|
||||||
</svg> |
|
||||||
{:else if status === proposal_status_draft} |
|
||||||
<svg |
|
||||||
xmlns="http://www.w3.org/2000/svg" |
|
||||||
viewBox="0 0 16 16" |
|
||||||
class="h-5 w-5 flex-none fill-neutral-content pt-1" |
|
||||||
><path d={proposal_icon_path.draft} /></svg |
|
||||||
> |
|
||||||
{:else if status === proposal_status_applied} |
|
||||||
<svg |
|
||||||
xmlns="http://www.w3.org/2000/svg" |
|
||||||
viewBox="0 0 16 16" |
|
||||||
class="h-5 w-5 flex-none fill-primary pt-1" |
|
||||||
> |
|
||||||
{#if type === 'proposal'} |
|
||||||
<path d={proposal_icon_path.applied} /> |
|
||||||
{:else if type === 'issue'} |
|
||||||
{#each issue_icon_path.resolved as p} |
|
||||||
<path d={p} /> |
|
||||||
{/each} |
|
||||||
{/if} |
|
||||||
</svg> |
|
||||||
{/if} |
|
||||||
<a |
|
||||||
href="/r/{repo_naddr}/{type}s/{nip19.noteEncode(id) || ''}" |
|
||||||
class="ml-3 grow overflow-hidden text-xs text-neutral-content" |
|
||||||
class:pointer-events-none={loading} |
|
||||||
> |
|
||||||
{#if loading} |
|
||||||
<div class="skeleton h-5 w-60 flex-none pt-1"></div> |
|
||||||
<div class="skeleton mb-1 mt-3 h-3 w-40 flex-none"></div> |
|
||||||
{:else} |
|
||||||
<div class="text-sm text-base-content"> |
|
||||||
{short_title} |
|
||||||
</div> |
|
||||||
<!-- <div class="text-xs text-neutral-content"> |
|
||||||
{description} |
|
||||||
</div> --> |
|
||||||
<ul class="pt-2"> |
|
||||||
{#if comments > 0} |
|
||||||
<li class="mr-3 inline align-middle"> |
|
||||||
<!-- http://icon-sets.iconify.design/octicon/comment-16/ --> |
|
||||||
<svg |
|
||||||
xmlns="http://www.w3.org/2000/svg" |
|
||||||
class="inline-block h-3 w-3 flex-none fill-base-content pt-0" |
|
||||||
viewBox="0 0 16 16" |
|
||||||
><path |
|
||||||
d="M1 2.75C1 1.784 1.784 1 2.75 1h10.5c.966 0 1.75.784 1.75 1.75v7.5A1.75 1.75 0 0 1 13.25 12H9.06l-2.573 2.573A1.458 1.458 0 0 1 4 13.543V12H2.75A1.75 1.75 0 0 1 1 10.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h4.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z" |
|
||||||
/></svg |
|
||||||
> |
|
||||||
{comments} |
|
||||||
</li> |
|
||||||
{/if} |
|
||||||
<li class="mr-3 inline"> |
|
||||||
opened {created_at_ago} |
|
||||||
</li> |
|
||||||
<li class="inline"> |
|
||||||
<UserHeader user={author} inline={true} size="xs" /> |
|
||||||
</li> |
|
||||||
{#if show_repo && repo_identifier.length > 0} |
|
||||||
<li class="ml-3 inline"> |
|
||||||
<a class="link-primary z-10" href="/r/{repo_naddr}"> |
|
||||||
{repo_identifier} |
|
||||||
</a> |
|
||||||
</li> |
|
||||||
{/if} |
|
||||||
</ul> |
|
||||||
{/if} |
|
||||||
</a> |
|
||||||
<!-- <div class="flex-none text-xs pt-0 hidden md:block"> |
|
||||||
<div class="align-middle"> |
|
||||||
{#if loading} |
|
||||||
<div class="skeleton w-10 h-10"></div> |
|
||||||
{:else} |
|
||||||
<Avatar /> |
|
||||||
{/if} |
|
||||||
</div> |
|
||||||
</div> --> |
|
||||||
</li> |
|
||||||
@ -1,27 +0,0 @@ |
|||||||
<script lang="ts" context="module"> |
|
||||||
import type { Meta } from '@storybook/svelte' |
|
||||||
import Status from './Status.svelte' |
|
||||||
import { Story, Template } from '@storybook/addon-svelte-csf' |
|
||||||
|
|
||||||
export const meta: Meta<Status> = { |
|
||||||
title: 'Proposals/Status', |
|
||||||
component: Status, |
|
||||||
tags: ['autodocs'], |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<Template let:args> |
|
||||||
<Status {...args} /> |
|
||||||
</Template> |
|
||||||
|
|
||||||
<Story name="Open" args={{ status: 'Open' }} /> |
|
||||||
|
|
||||||
<Story name="Closed" args={{ status: 'Closed' }} /> |
|
||||||
|
|
||||||
<Story name="Draft" args={{ status: 'Draft' }} /> |
|
||||||
|
|
||||||
<Story name="Applied" args={{ status: 'Applied' }} /> |
|
||||||
|
|
||||||
<Story name="Open Edit Mode" args={{ edit_mode: true, status: 'Open' }} /> |
|
||||||
|
|
||||||
<Story name="Loading" args={{ status: undefined }} /> |
|
||||||
@ -1,110 +0,0 @@ |
|||||||
<script lang="ts"> |
|
||||||
import { |
|
||||||
proposal_status_applied, |
|
||||||
proposal_status_closed, |
|
||||||
proposal_status_draft, |
|
||||||
proposal_status_open, |
|
||||||
statusKindtoText, |
|
||||||
} from '$lib/kinds' |
|
||||||
import { issue_icon_path } from '../issues/icons' |
|
||||||
import { proposal_icon_path } from './icons' |
|
||||||
|
|
||||||
export let status: number | undefined = undefined |
|
||||||
export let type: 'proposal' | 'issue' = 'proposal' |
|
||||||
export let edit_mode = false |
|
||||||
</script> |
|
||||||
|
|
||||||
{#if !status} |
|
||||||
<div class="skeleton inline-block h-8 w-24 rounded-md align-middle"></div> |
|
||||||
{:else} |
|
||||||
<div |
|
||||||
tabIndex={0} |
|
||||||
role="button" |
|
||||||
class:btn-success={status && status === proposal_status_open} |
|
||||||
class:btn-primary={status && status === proposal_status_applied} |
|
||||||
class:btn-neutral={!status || |
|
||||||
status === proposal_status_draft || |
|
||||||
status === proposal_status_closed} |
|
||||||
class:cursor-default={!edit_mode} |
|
||||||
class:no-animation={!edit_mode} |
|
||||||
class:hover:bg-success={!edit_mode && |
|
||||||
status && |
|
||||||
status === proposal_status_open} |
|
||||||
class:hover:bg-primary={!edit_mode && |
|
||||||
status && |
|
||||||
status === proposal_status_applied} |
|
||||||
class:hover:bg-neutral={(!edit_mode && |
|
||||||
status && |
|
||||||
status === proposal_status_draft) || |
|
||||||
status === proposal_status_closed} |
|
||||||
class="btn btn-success btn-sm align-middle" |
|
||||||
> |
|
||||||
{#if status === proposal_status_open} |
|
||||||
<svg |
|
||||||
xmlns="http://www.w3.org/2000/svg" |
|
||||||
viewBox="0 0 18 18" |
|
||||||
class="h-5 w-5 flex-none fill-success-content pt-1" |
|
||||||
> |
|
||||||
{#if type === 'proposal'} |
|
||||||
<path d={proposal_icon_path.open_patch} /> |
|
||||||
{:else if type === 'issue'} |
|
||||||
{#each issue_icon_path.open as p} |
|
||||||
<path d={p} /> |
|
||||||
{/each} |
|
||||||
{/if} |
|
||||||
</svg> |
|
||||||
{statusKindtoText(proposal_status_open, type)} |
|
||||||
{:else if status === proposal_status_applied} |
|
||||||
<svg |
|
||||||
xmlns="http://www.w3.org/2000/svg" |
|
||||||
viewBox="0 0 16 16" |
|
||||||
class="h-5 w-5 flex-none fill-primary-content pt-1" |
|
||||||
> |
|
||||||
{#if type === 'proposal'} |
|
||||||
<path d={proposal_icon_path.applied} /> |
|
||||||
{:else if type === 'issue'} |
|
||||||
{#each issue_icon_path.resolved as p} |
|
||||||
<path d={p} /> |
|
||||||
{/each} |
|
||||||
{/if} |
|
||||||
</svg> |
|
||||||
{statusKindtoText(proposal_status_applied, type)} |
|
||||||
{:else if status === proposal_status_closed} |
|
||||||
<svg |
|
||||||
xmlns="http://www.w3.org/2000/svg" |
|
||||||
viewBox="0 0 16 16" |
|
||||||
class="h-5 w-5 flex-none fill-neutral-content pt-1" |
|
||||||
> |
|
||||||
{#if type === 'proposal'} |
|
||||||
<path d={proposal_icon_path.close} /> |
|
||||||
{:else if type === 'issue'} |
|
||||||
{#each issue_icon_path.closed as p} |
|
||||||
<path d={p} /> |
|
||||||
{/each} |
|
||||||
{/if} |
|
||||||
</svg> |
|
||||||
{statusKindtoText(proposal_status_closed, type)} |
|
||||||
{:else if status === proposal_status_draft} |
|
||||||
<svg |
|
||||||
xmlns="http://www.w3.org/2000/svg" |
|
||||||
viewBox="0 0 16 16" |
|
||||||
class="h-5 w-5 flex-none fill-neutral-content pt-1" |
|
||||||
><path d={proposal_icon_path.draft} /></svg |
|
||||||
> |
|
||||||
{statusKindtoText(proposal_status_draft, type)} |
|
||||||
{:else} |
|
||||||
{status} |
|
||||||
{/if} |
|
||||||
{#if edit_mode} |
|
||||||
<svg |
|
||||||
xmlns="http://www.w3.org/2000/svg" |
|
||||||
viewBox="0 0 24 24" |
|
||||||
class="h-5 w-5 flex-none fill-success-content" |
|
||||||
><path |
|
||||||
fill="currentColor" |
|
||||||
d="M11.646 15.146L5.854 9.354a.5.5 0 0 1 .353-.854h11.586a.5.5 0 0 1 .353.854l-5.793 5.792a.5.5 0 0 1-.707 0" |
|
||||||
/></svg |
|
||||||
> |
|
||||||
{/if} |
|
||||||
</div> |
|
||||||
{/if} |
|
||||||
@ -1,141 +0,0 @@ |
|||||||
<script lang="ts"> |
|
||||||
import { ndk } from '$lib/stores/ndk' |
|
||||||
import { NDKEvent, NDKRelaySet } from '@nostr-dev-kit/ndk' |
|
||||||
import { |
|
||||||
selected_proposal_full, |
|
||||||
selected_proposal_replies, |
|
||||||
} from '$lib/stores/Proposal' |
|
||||||
import { |
|
||||||
proposal_status_applied, |
|
||||||
proposal_status_closed, |
|
||||||
proposal_status_draft, |
|
||||||
proposal_status_open, |
|
||||||
statusKindtoText, |
|
||||||
} from '$lib/kinds' |
|
||||||
import { getUserRelays, logged_in_user } from '$lib/stores/users' |
|
||||||
import { selected_repo_event } from '$lib/stores/repo' |
|
||||||
import Status from '$lib/components/proposals/Status.svelte' |
|
||||||
|
|
||||||
export let status: number | undefined = undefined |
|
||||||
export let type: 'proposal' | 'issue' = 'proposal' |
|
||||||
export let proposal_or_issue_id: string = '' |
|
||||||
|
|
||||||
let loading = false |
|
||||||
|
|
||||||
let edit_mode = false |
|
||||||
$: { |
|
||||||
edit_mode = $logged_in_user !== undefined |
|
||||||
} |
|
||||||
|
|
||||||
async function changeStatus(new_status_kind: number) { |
|
||||||
if (!$logged_in_user) return |
|
||||||
let event = new NDKEvent(ndk) |
|
||||||
event.kind = new_status_kind |
|
||||||
// tag proposal event |
|
||||||
event.tags.push(['e', proposal_or_issue_id, 'root']) |
|
||||||
// tag proposal revision event |
|
||||||
$selected_proposal_replies |
|
||||||
.filter((reply) => |
|
||||||
reply.tags.some((t) => t.length > 1 && t[1] === 'revision-root') |
|
||||||
) |
|
||||||
.forEach((revision) => { |
|
||||||
event.tags.push(['e', revision.id, 'mention']) |
|
||||||
}) |
|
||||||
if ($selected_repo_event.unique_commit) |
|
||||||
event.tags.push(['r', $selected_repo_event.unique_commit]) |
|
||||||
loading = true |
|
||||||
let relays = [...$selected_repo_event.relays] |
|
||||||
try { |
|
||||||
event.sign() |
|
||||||
} catch { |
|
||||||
alert('failed to sign event') |
|
||||||
} |
|
||||||
try { |
|
||||||
let user_relays = await getUserRelays($logged_in_user.hexpubkey) |
|
||||||
relays = [ |
|
||||||
...relays, |
|
||||||
...(user_relays.ndk_relays |
|
||||||
? user_relays.ndk_relays.writeRelayUrls |
|
||||||
: []), |
|
||||||
// TODO: proposal event pubkey relays |
|
||||||
] |
|
||||||
} catch { |
|
||||||
alert('failed to get user relays') |
|
||||||
} |
|
||||||
try { |
|
||||||
let _ = await event.publish(NDKRelaySet.fromRelayUrls(relays, ndk)) |
|
||||||
selected_proposal_full.update((proposal_full) => { |
|
||||||
if (proposal_full.summary.id !== proposal_or_issue_id) |
|
||||||
return proposal_full |
|
||||||
return { |
|
||||||
...proposal_full, |
|
||||||
summary: { |
|
||||||
...proposal_full.summary, |
|
||||||
status: new_status_kind, |
|
||||||
status_date: event.created_at || 0, |
|
||||||
}, |
|
||||||
} |
|
||||||
}) |
|
||||||
loading = false |
|
||||||
} catch {} |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
{#if loading || !status} |
|
||||||
<Status {type} /> |
|
||||||
{:else} |
|
||||||
<div class="dropdown"> |
|
||||||
<Status {type} {edit_mode} {status} /> |
|
||||||
{#if edit_mode} |
|
||||||
<ul |
|
||||||
tabIndex={0} |
|
||||||
class="menu dropdown-content z-[1] ml-0 w-52 rounded-box bg-base-300 p-2 shadow" |
|
||||||
> |
|
||||||
{#if status !== proposal_status_draft && type !== 'issue'} |
|
||||||
<li class="my-2 pl-0"> |
|
||||||
<button |
|
||||||
on:click={() => { |
|
||||||
changeStatus(proposal_status_draft) |
|
||||||
}} |
|
||||||
class="btn btn-neutral btn-sm mx-2 align-middle" |
|
||||||
>{statusKindtoText(proposal_status_draft, type)}</button |
|
||||||
> |
|
||||||
</li> |
|
||||||
{/if} |
|
||||||
{#if status !== proposal_status_open} |
|
||||||
<li class="my-2 pl-0"> |
|
||||||
<button |
|
||||||
on:click={() => { |
|
||||||
changeStatus(proposal_status_open) |
|
||||||
}} |
|
||||||
class="btn btn-success btn-sm mx-2 align-middle" |
|
||||||
>{statusKindtoText(proposal_status_open, type)}</button |
|
||||||
> |
|
||||||
</li> |
|
||||||
{/if} |
|
||||||
{#if status !== proposal_status_applied} |
|
||||||
<li class="my-2 pl-0"> |
|
||||||
<button |
|
||||||
on:click={() => { |
|
||||||
changeStatus(proposal_status_applied) |
|
||||||
}} |
|
||||||
class="btn btn-primary btn-sm mx-2 align-middle" |
|
||||||
>{statusKindtoText(proposal_status_applied, type)}</button |
|
||||||
> |
|
||||||
</li> |
|
||||||
{/if} |
|
||||||
{#if status !== proposal_status_closed} |
|
||||||
<li class="my-2 pl-0"> |
|
||||||
<button |
|
||||||
on:click={() => { |
|
||||||
changeStatus(proposal_status_closed) |
|
||||||
}} |
|
||||||
class="btn btn-neutral btn-sm mx-2 align-middle" |
|
||||||
>{statusKindtoText(proposal_status_closed, type)}</button |
|
||||||
> |
|
||||||
</li> |
|
||||||
{/if} |
|
||||||
</ul> |
|
||||||
{/if} |
|
||||||
</div> |
|
||||||
{/if} |
|
||||||
@ -1,22 +0,0 @@ |
|||||||
// icon are MIT licenced
|
|
||||||
export const proposal_icon_path = { |
|
||||||
// http://icon-sets.iconify.design/octicon/git-pull-request-16/
|
|
||||||
open_pull: |
|
||||||
'M1.5 3.25a2.25 2.25 0 1 1 3 2.122v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.25 2.25 0 0 1 1.5 3.25m5.677-.177L9.573.677A.25.25 0 0 1 10 .854V2.5h1A2.5 2.5 0 0 1 13.5 5v5.628a2.251 2.251 0 1 1-1.5 0V5a1 1 0 0 0-1-1h-1v1.646a.25.25 0 0 1-.427.177L7.177 3.427a.25.25 0 0 1 0-.354M3.75 2.5a.75.75 0 1 0 0 1.5a.75.75 0 0 0 0-1.5m0 9.5a.75.75 0 1 0 0 1.5a.75.75 0 0 0 0-1.5m8.25.75a.75.75 0 1 0 1.5 0a.75.75 0 0 0-1.5 0', |
|
||||||
// https://icon-sets.iconify.design/octicon/git-pull-request-closed-16/
|
|
||||||
open_patch: |
|
||||||
'M3.75 4.5a1.25 1.25 0 1 0 0-2.5a1.25 1.25 0 0 0 0 2.5M3 7.75a.75.75 0 0 1 1.5 0v2.878a2.251 2.251 0 1 1-1.5 0Zm.75 5.75a.75.75 0 1 0 0-1.5a.75.75 0 0 0 0 1.5m5-7.75a1.25 1.25 0 1 1-2.5 0a1.25 1.25 0 0 1 2.5 0m5.75 2.5a2.25 2.25 0 1 1-4.5 0a2.25 2.25 0 0 1 4.5 0m-1.5 0a.75.75 0 1 0-1.5 0a.75.75 0 0 0 1.5 0', |
|
||||||
// https://icon-sets.iconify.design/octicon/git-pull-request-closed-16/
|
|
||||||
close: |
|
||||||
'M3.25 1A2.25 2.25 0 0 1 4 5.372v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.251 2.251 0 0 1 3.25 1m9.5 5.5a.75.75 0 0 1 .75.75v3.378a2.251 2.251 0 1 1-1.5 0V7.25a.75.75 0 0 1 .75-.75m-2.03-5.273a.75.75 0 0 1 1.06 0l.97.97l.97-.97a.748.748 0 0 1 1.265.332a.75.75 0 0 1-.205.729l-.97.97l.97.97a.751.751 0 0 1-.018 1.042a.751.751 0 0 1-1.042.018l-.97-.97l-.97.97a.749.749 0 0 1-1.275-.326a.749.749 0 0 1 .215-.734l.97-.97l-.97-.97a.75.75 0 0 1 0-1.06ZM2.5 3.25a.75.75 0 1 0 1.5 0a.75.75 0 0 0-1.5 0M3.25 12a.75.75 0 1 0 0 1.5a.75.75 0 0 0 0-1.5m9.5 0a.75.75 0 1 0 0 1.5a.75.75 0 0 0 0-1.5', |
|
||||||
// https://icon-sets.iconify.design/octicon/git-pull-request-draft-16/
|
|
||||||
draft: |
|
||||||
'M3.25 1A2.25 2.25 0 0 1 4 5.372v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.251 2.251 0 0 1 3.25 1m9.5 14a2.25 2.25 0 1 1 0-4.5a2.25 2.25 0 0 1 0 4.5M2.5 3.25a.75.75 0 1 0 1.5 0a.75.75 0 0 0-1.5 0M3.25 12a.75.75 0 1 0 0 1.5a.75.75 0 0 0 0-1.5m9.5 0a.75.75 0 1 0 0 1.5a.75.75 0 0 0 0-1.5M14 7.5a1.25 1.25 0 1 1-2.5 0a1.25 1.25 0 0 1 2.5 0m0-4.25a1.25 1.25 0 1 1-2.5 0a1.25 1.25 0 0 1 2.5 0', |
|
||||||
// https://icon-sets.iconify.design/octicon/git-merge-16/
|
|
||||||
merge: |
|
||||||
'M5.45 5.154A4.25 4.25 0 0 0 9.25 7.5h1.378a2.251 2.251 0 1 1 0 1.5H9.25A5.734 5.734 0 0 1 5 7.123v3.505a2.25 2.25 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.95-.218M4.25 13.5a.75.75 0 1 0 0-1.5a.75.75 0 0 0 0 1.5m8.5-4.5a.75.75 0 1 0 0-1.5a.75.75 0 0 0 0 1.5M5 3.25a.75.75 0 1 0 0 .005z', |
|
||||||
// adapted from https://icon-sets.iconify.design/octicon/git-pull-request-closed-16/
|
|
||||||
// TODO: centre in icon frame
|
|
||||||
applied: |
|
||||||
'M 3.25 1 A 2.25 2.25 0 0 1 4 5.372 v 5.256 a 2.251 2.251 0 1 1 -1.5 0 V 5.372 A 2.251 2.251 0 0 1 3.25 1 Z M 2.5 3.25 a 0.75 0.75 0 1 0 1.5 0 a 0.75 0.75 0 0 0 -1.5 0 M 3.25 12 a 0.75 0.75 0 1 0 0 1.5 a 0.75 0.75 0 0 0 0 -1.5', |
|
||||||
} |
|
||||||
@ -1,60 +0,0 @@ |
|||||||
import type { User } from '../users/type' |
|
||||||
import { defaults as user_defaults } from '../users/type' |
|
||||||
import type { Event } from '../events/type' |
|
||||||
import type { NDKEvent } from '@nostr-dev-kit/ndk' |
|
||||||
|
|
||||||
export interface ProposalSummary { |
|
||||||
type: 'proposal' |
|
||||||
title: string |
|
||||||
descritpion: string |
|
||||||
repo_a: string |
|
||||||
id: string |
|
||||||
comments: number |
|
||||||
status: undefined | number |
|
||||||
status_date: number |
|
||||||
author: User |
|
||||||
created_at: number | undefined |
|
||||||
loading: boolean |
|
||||||
} |
|
||||||
|
|
||||||
export const summary_defaults: ProposalSummary = { |
|
||||||
type: 'proposal', |
|
||||||
title: '', |
|
||||||
descritpion: '', |
|
||||||
repo_a: '', |
|
||||||
id: '', |
|
||||||
comments: 0, |
|
||||||
status: undefined, |
|
||||||
status_date: 0, |
|
||||||
author: { ...user_defaults }, |
|
||||||
created_at: 0, |
|
||||||
loading: true, |
|
||||||
} |
|
||||||
|
|
||||||
export interface ProposalSummaries { |
|
||||||
repo_a: string | undefined |
|
||||||
summaries: ProposalSummary[] |
|
||||||
loading: boolean |
|
||||||
} |
|
||||||
|
|
||||||
export const summaries_defaults: ProposalSummaries = { |
|
||||||
repo_a: '', |
|
||||||
summaries: [], |
|
||||||
loading: true, |
|
||||||
} |
|
||||||
|
|
||||||
export interface ProposalFull { |
|
||||||
summary: ProposalSummary |
|
||||||
proposal_event: NDKEvent | undefined |
|
||||||
labels: string[] |
|
||||||
events: Event[] |
|
||||||
loading: boolean |
|
||||||
} |
|
||||||
|
|
||||||
export const full_defaults: ProposalFull = { |
|
||||||
summary: { ...summary_defaults }, |
|
||||||
proposal_event: undefined, |
|
||||||
labels: [], |
|
||||||
events: [], |
|
||||||
loading: true, |
|
||||||
} |
|
||||||
@ -1,66 +0,0 @@ |
|||||||
import dayjs from 'dayjs' |
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime' |
|
||||||
import type { ProposalSummary } from './type' |
|
||||||
import { UserVectors } from '../users/vectors' |
|
||||||
import { |
|
||||||
proposal_status_applied, |
|
||||||
proposal_status_draft, |
|
||||||
proposal_status_open, |
|
||||||
} from '$lib/kinds' |
|
||||||
|
|
||||||
dayjs.extend(relativeTime) |
|
||||||
|
|
||||||
const Short = { |
|
||||||
title: 'short title', |
|
||||||
author: { ...UserVectors.default }, |
|
||||||
created_at: dayjs().subtract(7, 'days').unix(), |
|
||||||
comments: 2, |
|
||||||
status: proposal_status_open, |
|
||||||
loading: false, |
|
||||||
} as ProposalSummary |
|
||||||
|
|
||||||
export const ProposalsListItemArgsVectors = { |
|
||||||
Short, |
|
||||||
Long: { |
|
||||||
title: |
|
||||||
'rather long title that goes on and on and on and on and on and on and on and on and on and on and on and on and on and on and on', |
|
||||||
author: { ...UserVectors.default }, |
|
||||||
created_at: dayjs().subtract(1, 'minute').unix(), |
|
||||||
comments: 0, |
|
||||||
status: proposal_status_open, |
|
||||||
loading: false, |
|
||||||
} as ProposalSummary, |
|
||||||
LongNoSpaces: { |
|
||||||
title: |
|
||||||
'LongNameLongNameLongNameLongNameLongNameLongNameLongNameLongNameLongNameLongNameLongNameLongNameLongNameLongName', |
|
||||||
author: { ...UserVectors.default }, |
|
||||||
created_at: dayjs().subtract(3, 'month').subtract(3, 'days').unix(), |
|
||||||
comments: 1, |
|
||||||
status: proposal_status_open, |
|
||||||
loading: false, |
|
||||||
} as ProposalSummary, |
|
||||||
AuthorLoading: { |
|
||||||
title: 'short title', |
|
||||||
author: { ...UserVectors.loading }, |
|
||||||
created_at: dayjs().subtract(3, 'month').subtract(3, 'days').unix(), |
|
||||||
comments: 1, |
|
||||||
status: proposal_status_open, |
|
||||||
loading: false, |
|
||||||
} as ProposalSummary, |
|
||||||
StatusLoading: { |
|
||||||
...Short, |
|
||||||
status: undefined, |
|
||||||
} as ProposalSummary, |
|
||||||
StatusDraft: { |
|
||||||
...Short, |
|
||||||
status: proposal_status_draft, |
|
||||||
} as ProposalSummary, |
|
||||||
StatusClosed: { |
|
||||||
...Short, |
|
||||||
status: proposal_status_draft, |
|
||||||
} as ProposalSummary, |
|
||||||
StatusApplied: { |
|
||||||
...Short, |
|
||||||
status: proposal_status_applied, |
|
||||||
} as ProposalSummary, |
|
||||||
} |
|
||||||
@ -1,55 +0,0 @@ |
|||||||
<script lang="ts" context="module"> |
|
||||||
import type { Meta } from '@storybook/svelte' |
|
||||||
import RepoDetails from './RepoDetails.svelte' |
|
||||||
import { Story, Template } from '@storybook/addon-svelte-csf' |
|
||||||
import { RepoDetailsArgsVectors as vectors } from './vectors' |
|
||||||
|
|
||||||
export const meta: Meta<RepoDetails> = { |
|
||||||
title: 'Repo/Details', |
|
||||||
component: RepoDetails, |
|
||||||
tags: ['autodocs'], |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<Template let:args> |
|
||||||
<RepoDetails {...args} /> |
|
||||||
</Template> |
|
||||||
|
|
||||||
<Story name="Short Details" args={vectors.NoMaintainers} /> |
|
||||||
|
|
||||||
<Story name="Long Details" args={vectors.Long} /> |
|
||||||
|
|
||||||
<Story name="Long and No Spaces" args={vectors.LongNoSpaces} /> |
|
||||||
|
|
||||||
<Story name="No Name or Description" args={vectors.NoNameOrDescription} /> |
|
||||||
|
|
||||||
<Story name="No Description" args={vectors.NoDescription} /> |
|
||||||
|
|
||||||
<Story name="No Tags" args={vectors.NoTags} /> |
|
||||||
|
|
||||||
<Story name="No Clone" args={vectors.NoGitServer} /> |
|
||||||
|
|
||||||
<Story name="No Web" args={vectors.NoWeb} /> |
|
||||||
|
|
||||||
<Story name="No Maintainers" args={vectors.NoMaintainers} /> |
|
||||||
|
|
||||||
<Story |
|
||||||
name="One Maintainer's Profile Not Loaded" |
|
||||||
args={vectors.MaintainersOneProfileNotLoaded} |
|
||||||
/> |
|
||||||
|
|
||||||
<Story |
|
||||||
name="One Maintainer's Profile Only Has displayName But No Name" |
|
||||||
args={vectors.MaintainersOneProfileDisplayNameWithoutName} |
|
||||||
/> |
|
||||||
|
|
||||||
<Story |
|
||||||
name="One Maintainer's Profile Has No displayName or Name" |
|
||||||
args={vectors.MaintainersOneProfileNoNameOrDisplayNameBeingPresent} |
|
||||||
/> |
|
||||||
|
|
||||||
<Story name="No Relays" args={vectors.NoRelays} /> |
|
||||||
|
|
||||||
<Story name="No Maintainers or Relays" args={vectors.NoMaintainersOrRelays} /> |
|
||||||
|
|
||||||
<Story name="loading" args={{ loading: true }} /> |
|
||||||
@ -1,365 +0,0 @@ |
|||||||
<script lang="ts"> |
|
||||||
import UserHeader from '$lib/components/users/UserHeader.svelte' |
|
||||||
import { nip19 } from 'nostr-tools' |
|
||||||
import AlertWarning from '../AlertWarning.svelte' |
|
||||||
import { icons_misc } from '../icons' |
|
||||||
import InstallNgit from '../InstallNgit.svelte' |
|
||||||
import { event_defaults } from './type' |
|
||||||
|
|
||||||
export let { |
|
||||||
event_id, |
|
||||||
naddr, |
|
||||||
identifier, |
|
||||||
author, |
|
||||||
unique_commit, |
|
||||||
name, |
|
||||||
description, |
|
||||||
clone, |
|
||||||
web, |
|
||||||
tags, |
|
||||||
maintainers, |
|
||||||
relays, |
|
||||||
referenced_by, |
|
||||||
most_recent_reference_timestamp, |
|
||||||
created_at, |
|
||||||
loading, |
|
||||||
} = event_defaults |
|
||||||
$: short_descrption = |
|
||||||
!description && description.length > 500 |
|
||||||
? description.slice(0, 450) + '...' |
|
||||||
: description |
|
||||||
let naddr_copied = false |
|
||||||
const create_nostr_url = ( |
|
||||||
maintainers: string[], |
|
||||||
identifier: string, |
|
||||||
relays: string[] |
|
||||||
) => { |
|
||||||
if (identifier.length > 0 && maintainers.length > 0) { |
|
||||||
let npub = nip19.npubEncode(maintainers[0]) |
|
||||||
if (relays.length > 0) { |
|
||||||
let relay = relays[0] |
|
||||||
// remove trailing slash(es) |
|
||||||
.replace(/\/+$/, '') |
|
||||||
if (/^[a-zA-Z0-9.]+$/.test(relay.replace('wss://', ''))) { |
|
||||||
return `nostr://${npub}/${relay.replace('wss://', '')}/${identifier}` |
|
||||||
} |
|
||||||
return `nostr://${npub}/${encodeURIComponent(relay)}/${identifier}` |
|
||||||
} |
|
||||||
return `nostr://${npub}/${identifier}` |
|
||||||
} |
|
||||||
return '' |
|
||||||
} |
|
||||||
$: nostr_url = create_nostr_url(maintainers, identifier, relays) |
|
||||||
let nostr_url_copied = false |
|
||||||
let git_url_copied: false | string = false |
|
||||||
let maintainer_copied: false | string = false |
|
||||||
$: event_not_found = !loading && created_at == 0 |
|
||||||
</script> |
|
||||||
|
|
||||||
<div class="prose w-full max-w-md"> |
|
||||||
{#if event_not_found} |
|
||||||
<h4 class="mt-0 pt-1">identifier</h4> |
|
||||||
<p class="my-2 break-words text-sm">{identifier}</p> |
|
||||||
{:else} |
|
||||||
{#if name == identifier} |
|
||||||
{#if loading} |
|
||||||
<div class="skeleton my-3 h-5 w-20"></div> |
|
||||||
<div class="skeleton my-2 h-4"></div> |
|
||||||
<div class="skeleton my-2 mb-3 h-4 w-2/3"></div> |
|
||||||
{:else if !name || name.length == 0} |
|
||||||
<h4 class="mt-0 pt-1">name / identifier</h4> |
|
||||||
<div>none</div> |
|
||||||
{:else} |
|
||||||
<h4 class="mt-0 pt-1">name / identifier</h4> |
|
||||||
<p class="my-2 break-words text-sm">{name}</p> |
|
||||||
{/if} |
|
||||||
{:else} |
|
||||||
{#if loading} |
|
||||||
<div class="skeleton my-3 h-5 w-20"></div> |
|
||||||
<div class="skeleton my-2 h-4"></div> |
|
||||||
<div class="skeleton my-2 mb-3 h-4 w-2/3"></div> |
|
||||||
{:else if !name || name.length == 0} |
|
||||||
<h4>name</h4> |
|
||||||
<div>none</div> |
|
||||||
{:else} |
|
||||||
<h4>name</h4> |
|
||||||
<p class="my-2 break-words text-sm">{name}</p> |
|
||||||
{/if} |
|
||||||
{#if loading} |
|
||||||
<div class="skeleton my-3 h-5 w-20"></div> |
|
||||||
<div class="skeleton my-2 h-4"></div> |
|
||||||
<div class="skeleton my-2 mb-3 h-4 w-2/3"></div> |
|
||||||
{:else if !identifier || identifier.length == 0} |
|
||||||
<h4>identifier</h4> |
|
||||||
<div>none</div> |
|
||||||
{:else} |
|
||||||
<h4>identifier</h4> |
|
||||||
<p class="my-2 break-words text-sm">{identifier}</p> |
|
||||||
{/if} |
|
||||||
{/if} |
|
||||||
{#if !loading} |
|
||||||
<div class="dropdown dropdown-end mt-3"> |
|
||||||
<div tabIndex={0} class="btn btn-success btn-sm text-base-400"> |
|
||||||
clone |
|
||||||
<svg |
|
||||||
xmlns="http://www.w3.org/2000/svg" |
|
||||||
viewBox="0 0 24 24" |
|
||||||
class="h-5 w-5 flex-none fill-success-content" |
|
||||||
><path |
|
||||||
fill="currentColor" |
|
||||||
d="M11.646 15.146L5.854 9.354a.5.5 0 0 1 .353-.854h11.586a.5.5 0 0 1 .353.854l-5.793 5.792a.5.5 0 0 1-.707 0" |
|
||||||
/></svg |
|
||||||
> |
|
||||||
</div> |
|
||||||
<ul |
|
||||||
tabIndex={0} |
|
||||||
class="w-md menu dropdown-content z-[1] ml-0 rounded-box bg-base-300 p-2 shadow" |
|
||||||
> |
|
||||||
<li class="prose"> |
|
||||||
<div> |
|
||||||
<div> |
|
||||||
<h4 class="mt-0">1. install ngit and git-remote-nostr</h4> |
|
||||||
<InstallNgit size="sm" /> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</li> |
|
||||||
<li class="m-0 p-0"> |
|
||||||
<!-- eslint-disable-next-line svelte/valid-compile --> |
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events --> |
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions --> |
|
||||||
<div |
|
||||||
on:click={async () => { |
|
||||||
try { |
|
||||||
await navigator.clipboard.writeText(nostr_url) |
|
||||||
nostr_url_copied = true |
|
||||||
setTimeout(() => { |
|
||||||
nostr_url_copied = false |
|
||||||
}, 2000) |
|
||||||
} catch {} |
|
||||||
}} |
|
||||||
class="group cursor-pointer rounded-md" |
|
||||||
> |
|
||||||
<div> |
|
||||||
<h4 class="mt-0 pt-0"> |
|
||||||
2. copy git clone url |
|
||||||
<svg |
|
||||||
xmlns="http://www.w3.org/2000/svg" |
|
||||||
viewBox="0 0 16 16" |
|
||||||
class="ml-1 inline h-4 w-4 flex-none fill-base-content opacity-50 group-hover:opacity-100" |
|
||||||
class:fill-base-content={!nostr_url_copied} |
|
||||||
class:fill-success={nostr_url_copied} |
|
||||||
> |
|
||||||
{#each icons_misc.copy as d} |
|
||||||
<path {d} /> |
|
||||||
{/each} |
|
||||||
</svg> |
|
||||||
|
|
||||||
{#if nostr_url_copied}<span |
|
||||||
class="text-sm text-success opacity-50" |
|
||||||
> |
|
||||||
(copied to clipboard)</span |
|
||||||
>{/if} |
|
||||||
</h4> |
|
||||||
<p class="my-2 break-words border p-2 text-xs">{nostr_url}</p> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</li> |
|
||||||
</ul> |
|
||||||
</div> |
|
||||||
{/if} |
|
||||||
{#if loading} |
|
||||||
<div class="skeleton my-3 h-5 w-20"></div> |
|
||||||
<div class="skeleton my-2 h-4"></div> |
|
||||||
<div class="skeleton my-2 mb-3 h-4 w-2/3"></div> |
|
||||||
{:else if !short_descrption || description.length == 0} |
|
||||||
<h4>description</h4> |
|
||||||
<div>none</div> |
|
||||||
{:else} |
|
||||||
<h4>description</h4> |
|
||||||
<p class="my-2 break-words text-sm">{short_descrption}</p> |
|
||||||
{/if} |
|
||||||
<div> |
|
||||||
{#if loading} |
|
||||||
<div class="badge skeleton w-20"></div> |
|
||||||
<div class="badge skeleton w-20"></div> |
|
||||||
{:else} |
|
||||||
{#each tags as tag} |
|
||||||
<div class="badge badge-secondary mr-2">{tag}</div> |
|
||||||
{/each} |
|
||||||
{/if} |
|
||||||
</div> |
|
||||||
<div> |
|
||||||
{#if loading} |
|
||||||
<div class="skeleton my-3 h-5 w-20"></div> |
|
||||||
<div class="badge skeleton my-2 block w-60"></div> |
|
||||||
{:else if clone.length == 0} |
|
||||||
<div /> |
|
||||||
{:else} |
|
||||||
<h4> |
|
||||||
git servers {#if git_url_copied}<span |
|
||||||
class="text-sm text-success opacity-50" |
|
||||||
> |
|
||||||
(copied to clipboard)</span |
|
||||||
>{/if} |
|
||||||
</h4> |
|
||||||
{#each clone as git_url} |
|
||||||
<!-- eslint-disable-next-line svelte/valid-compile --> |
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events --> |
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions --> |
|
||||||
<div |
|
||||||
on:click={async () => { |
|
||||||
try { |
|
||||||
await navigator.clipboard.writeText(git_url) |
|
||||||
git_url_copied = git_url |
|
||||||
setTimeout(() => { |
|
||||||
git_url_copied = false |
|
||||||
}, 2000) |
|
||||||
} catch {} |
|
||||||
}} |
|
||||||
class="group my-2 mt-3 cursor-pointer break-words text-xs" |
|
||||||
class:text-success={git_url_copied === git_url} |
|
||||||
class:opacity-50={git_url_copied === git_url} |
|
||||||
> |
|
||||||
{git_url} |
|
||||||
<svg |
|
||||||
xmlns="http://www.w3.org/2000/svg" |
|
||||||
viewBox="0 0 16 16" |
|
||||||
class="ml-1 inline h-4 w-4 flex-none fill-base-content opacity-50" |
|
||||||
class:group-hover:opacity-100={git_url_copied !== git_url} |
|
||||||
class:fill-base-content={git_url_copied !== git_url} |
|
||||||
class:fill-success={git_url_copied === git_url} |
|
||||||
class:opacity-100={git_url_copied === git_url} |
|
||||||
> |
|
||||||
{#each icons_misc.copy as d} |
|
||||||
<path {d} /> |
|
||||||
{/each} |
|
||||||
</svg> |
|
||||||
</div> |
|
||||||
{/each} |
|
||||||
{/if} |
|
||||||
</div> |
|
||||||
<div> |
|
||||||
{#if loading} |
|
||||||
<div class="skeleton my-3 h-5 w-20"></div> |
|
||||||
<div class="badge skeleton my-2 block w-60"></div> |
|
||||||
<div class="badge skeleton my-2 block w-40"></div> |
|
||||||
{:else if web.length == 0} |
|
||||||
<h4>websites</h4> |
|
||||||
<div>none</div> |
|
||||||
{:else} |
|
||||||
<h4>websites</h4> |
|
||||||
{#each web as site} |
|
||||||
<a |
|
||||||
href={site} |
|
||||||
target="_blank" |
|
||||||
class="link link-primary my-2 break-words text-sm" |
|
||||||
> |
|
||||||
{site} |
|
||||||
</a> |
|
||||||
{/each} |
|
||||||
{/if} |
|
||||||
</div> |
|
||||||
{/if} |
|
||||||
|
|
||||||
<div> |
|
||||||
{#if loading} |
|
||||||
<div class="skeleton my-3 h-5 w-20"></div> |
|
||||||
<div class="badge skeleton my-2 block w-60"></div> |
|
||||||
<div class="badge skeleton my-2 block w-40"></div> |
|
||||||
{:else if maintainers.length == 0} |
|
||||||
<div /> |
|
||||||
{:else} |
|
||||||
<h4> |
|
||||||
{#if event_not_found}author{:else}maintainers{/if} |
|
||||||
{#if maintainer_copied}<span class="text-sm text-success opacity-50"> |
|
||||||
(copied to clipboard)</span |
|
||||||
>{/if} |
|
||||||
</h4> |
|
||||||
{#each maintainers as maintainer} |
|
||||||
<div class="my-2 mt-3 break-words text-xs"> |
|
||||||
<UserHeader user={maintainer} /> |
|
||||||
</div> |
|
||||||
{/each} |
|
||||||
{/if} |
|
||||||
</div> |
|
||||||
|
|
||||||
{#if !event_not_found} |
|
||||||
<div> |
|
||||||
{#if loading} |
|
||||||
<div class="skeleton my-3 h-5 w-20"></div> |
|
||||||
<div class="badge skeleton my-2 block w-60"></div> |
|
||||||
<div class="badge skeleton my-2 block w-40"></div> |
|
||||||
{:else if relays.length == 0} |
|
||||||
<h4>relays</h4> |
|
||||||
<div>none</div> |
|
||||||
{:else} |
|
||||||
<h4>relays</h4> |
|
||||||
{#each relays as relay} |
|
||||||
<div class="badge badge-secondary badge-sm my-2 block">{relay}</div> |
|
||||||
{/each} |
|
||||||
{/if} |
|
||||||
</div> |
|
||||||
|
|
||||||
{#if loading} |
|
||||||
<div class="skeleton my-3 h-5 w-20"></div> |
|
||||||
<div class="skeleton my-2 h-4"></div> |
|
||||||
<div class="skeleton my-2 mb-3 h-4 w-2/3"></div> |
|
||||||
{:else if !unique_commit || unique_commit.length == 0} |
|
||||||
<h4>earliest unique commit</h4> |
|
||||||
<p class="my-2 break-words text-xs">not specified</p> |
|
||||||
{:else} |
|
||||||
<h4>earliest unique commit</h4> |
|
||||||
<p class="my-2 break-words text-xs">{unique_commit}</p> |
|
||||||
{/if} |
|
||||||
{/if} |
|
||||||
|
|
||||||
{#if loading} |
|
||||||
<div class="skeleton my-3 h-5 w-20"></div> |
|
||||||
<div class="skeleton my-2 h-4"></div> |
|
||||||
<div class="skeleton my-2 mb-3 h-4 w-2/3"></div> |
|
||||||
{:else if naddr && naddr.length > 0} |
|
||||||
<!-- eslint-disable-next-line svelte/valid-compile --> |
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events --> |
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions --> |
|
||||||
<div |
|
||||||
on:click={async () => { |
|
||||||
try { |
|
||||||
await navigator.clipboard.writeText(naddr) |
|
||||||
naddr_copied = true |
|
||||||
setTimeout(() => { |
|
||||||
naddr_copied = false |
|
||||||
}, 2000) |
|
||||||
} catch {} |
|
||||||
}} |
|
||||||
class="group -ml-3 mt-3 cursor-pointer rounded-md p-3 hover:bg-base-300" |
|
||||||
> |
|
||||||
<h4 class="mt-0 pt-0"> |
|
||||||
naddr |
|
||||||
<svg |
|
||||||
xmlns="http://www.w3.org/2000/svg" |
|
||||||
viewBox="0 0 16 16" |
|
||||||
class="ml-1 inline h-4 w-4 flex-none fill-base-content opacity-50 group-hover:opacity-100" |
|
||||||
class:fill-base-content={!naddr_copied} |
|
||||||
class:fill-success={naddr_copied} |
|
||||||
> |
|
||||||
{#each icons_misc.copy as d} |
|
||||||
<path {d} /> |
|
||||||
{/each} |
|
||||||
</svg> |
|
||||||
|
|
||||||
{#if naddr_copied}<span class="text-sm text-success opacity-50"> |
|
||||||
(copied to clipboard)</span |
|
||||||
>{/if} |
|
||||||
</h4> |
|
||||||
<p class="my-2 break-words text-xs">{naddr}</p> |
|
||||||
</div> |
|
||||||
{/if} |
|
||||||
{#if event_not_found} |
|
||||||
<div class="text-xs"> |
|
||||||
<AlertWarning> |
|
||||||
<div class="pb-1 font-semibold">missing repository details</div> |
|
||||||
<div>cannot find referenced repository event</div> |
|
||||||
</AlertWarning> |
|
||||||
</div> |
|
||||||
{/if} |
|
||||||
</div> |
|
||||||
@ -1,24 +0,0 @@ |
|||||||
<script lang="ts" context="module"> |
|
||||||
import type { Meta } from '@storybook/svelte' |
|
||||||
import RepoHeader from './RepoHeader.svelte' |
|
||||||
import { Story, Template } from '@storybook/addon-svelte-csf' |
|
||||||
import { RepoDetailsArgsVectors as vectors } from './vectors' |
|
||||||
|
|
||||||
export const meta: Meta<RepoHeader> = { |
|
||||||
title: 'Repo/Header', |
|
||||||
component: RepoHeader, |
|
||||||
tags: ['autodocs'], |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<Template let:args> |
|
||||||
<RepoHeader {...args} /> |
|
||||||
</Template> |
|
||||||
|
|
||||||
<Story name="Short Name" args={vectors.NoMaintainers} /> |
|
||||||
|
|
||||||
<Story name="Long Name" args={vectors.Long} /> |
|
||||||
|
|
||||||
<Story name="No Name" args={vectors.NoNameOrDescription} /> |
|
||||||
|
|
||||||
<Story name="loading" args={{ loading: true }} /> |
|
||||||
@ -1,61 +0,0 @@ |
|||||||
<script lang="ts"> |
|
||||||
import RepoMenu from '$lib/wrappers/RepoMenu.svelte' |
|
||||||
import UserHeader from '$lib/components/users/UserHeader.svelte' |
|
||||||
import Container from '../Container.svelte' |
|
||||||
import { event_defaults, type RepoPage } from './type' |
|
||||||
|
|
||||||
export let { |
|
||||||
event_id, |
|
||||||
identifier, |
|
||||||
naddr, |
|
||||||
unique_commit, |
|
||||||
name, |
|
||||||
author, |
|
||||||
description, |
|
||||||
clone, |
|
||||||
web, |
|
||||||
tags, |
|
||||||
maintainers, |
|
||||||
relays, |
|
||||||
referenced_by, |
|
||||||
created_at, |
|
||||||
most_recent_reference_timestamp, |
|
||||||
loading, |
|
||||||
} = event_defaults |
|
||||||
export let selected_tab: RepoPage = 'about' |
|
||||||
let short_name: string |
|
||||||
$: { |
|
||||||
if (name && name.length > 45) short_name = name.slice(0, 45) + '...' |
|
||||||
else if (name && name.length >= 0) short_name = name |
|
||||||
else if (identifier && identifier.length > 45) |
|
||||||
short_name = identifier.slice(0, 45) + '...' |
|
||||||
else if (identifier && identifier.length >= 0) short_name = identifier |
|
||||||
else short_name = 'Untitled' |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<div class="border-b border-accent-content bg-base-300"> |
|
||||||
<Container no_wrap={true}> |
|
||||||
{#if loading} |
|
||||||
<div class="p-3"> |
|
||||||
<div class="skeleton h-6 w-28 bg-base-200"></div> |
|
||||||
</div> |
|
||||||
{:else} |
|
||||||
<a |
|
||||||
href={`/r/${naddr}`} |
|
||||||
class="strong btn btn-ghost mb-0 mt-0 break-words px-3 text-sm" |
|
||||||
>{short_name}</a |
|
||||||
> |
|
||||||
{#if created_at === 0 && name.length === 0} |
|
||||||
<span class="text-xs text-warning"> |
|
||||||
cannot find referenced repository event by <div |
|
||||||
class="badge bg-base-400 text-warning" |
|
||||||
> |
|
||||||
<UserHeader user={author} inline size="xs" /> |
|
||||||
</div> |
|
||||||
</span> |
|
||||||
{/if} |
|
||||||
{/if} |
|
||||||
<RepoMenu {selected_tab} /> |
|
||||||
</Container> |
|
||||||
</div> |
|
||||||
@ -1,129 +0,0 @@ |
|||||||
import { |
|
||||||
defaults as user_defaults, |
|
||||||
type User, |
|
||||||
type UserObject, |
|
||||||
} from '../users/type' |
|
||||||
|
|
||||||
export interface RepoEventBase { |
|
||||||
event_id: string |
|
||||||
naddr: string |
|
||||||
author: string // pubkey
|
|
||||||
identifier: string |
|
||||||
unique_commit: string | undefined |
|
||||||
name: string |
|
||||||
description: string |
|
||||||
clone: string[] |
|
||||||
web: string[] |
|
||||||
tags: string[] |
|
||||||
maintainers: string | User[] |
|
||||||
relays: string[] |
|
||||||
referenced_by: string[] |
|
||||||
// this is unreliable as relays dont return youngest first
|
|
||||||
most_recent_reference_timestamp: number |
|
||||||
created_at: number |
|
||||||
loading: boolean |
|
||||||
} |
|
||||||
export interface RepoEvent extends RepoEventBase { |
|
||||||
maintainers: string[] |
|
||||||
} |
|
||||||
|
|
||||||
export interface RepoEventWithMaintainersMetadata extends RepoEventBase { |
|
||||||
maintainers: UserObject[] |
|
||||||
} |
|
||||||
|
|
||||||
export const event_defaults: RepoEvent = { |
|
||||||
event_id: '', |
|
||||||
naddr: '', |
|
||||||
author: '', |
|
||||||
identifier: '', |
|
||||||
unique_commit: '', |
|
||||||
name: '', |
|
||||||
description: '', |
|
||||||
clone: [], |
|
||||||
web: [], |
|
||||||
tags: [], |
|
||||||
maintainers: [], |
|
||||||
relays: [], |
|
||||||
referenced_by: [], |
|
||||||
most_recent_reference_timestamp: 0, |
|
||||||
created_at: 0, |
|
||||||
loading: true, |
|
||||||
} |
|
||||||
|
|
||||||
export interface RepoCollectionBase { |
|
||||||
selected_a: string // <kind>:<pubkeyhex>:<identifier>
|
|
||||||
most_recent_index: number |
|
||||||
maintainers: string | User[] |
|
||||||
events: RepoEvent[] |
|
||||||
loading: boolean |
|
||||||
} |
|
||||||
|
|
||||||
export interface RepoCollection extends RepoCollectionBase { |
|
||||||
maintainers: string[] |
|
||||||
} |
|
||||||
|
|
||||||
export interface RepoCollectionWithMaintainersMetadata |
|
||||||
extends RepoCollectionBase { |
|
||||||
maintainers: UserObject[] |
|
||||||
} |
|
||||||
|
|
||||||
export const collection_defaults: RepoCollection = { |
|
||||||
selected_a: '', |
|
||||||
most_recent_index: -1, |
|
||||||
maintainers: [], |
|
||||||
events: [], |
|
||||||
loading: true, |
|
||||||
} |
|
||||||
|
|
||||||
export interface RepoSummary { |
|
||||||
name: string |
|
||||||
description: string |
|
||||||
identifier: string |
|
||||||
naddr: string |
|
||||||
unique_commit: string | undefined |
|
||||||
maintainers: User[] |
|
||||||
loading?: boolean |
|
||||||
created_at: number |
|
||||||
most_recent_reference_timestamp: number |
|
||||||
} |
|
||||||
export const summary_defaults: RepoSummary = { |
|
||||||
name: '', |
|
||||||
identifier: '', |
|
||||||
naddr: '', |
|
||||||
unique_commit: undefined, |
|
||||||
description: '', |
|
||||||
maintainers: [{ ...user_defaults }], |
|
||||||
loading: false, |
|
||||||
created_at: 0, |
|
||||||
most_recent_reference_timestamp: 0, |
|
||||||
} |
|
||||||
|
|
||||||
export interface SelectedPubkeyRepoCollections { |
|
||||||
pubkey: string |
|
||||||
collections: RepoCollection[] |
|
||||||
} |
|
||||||
|
|
||||||
export interface RepoDIdentiferCollection { |
|
||||||
d: string |
|
||||||
events: RepoEvent[] |
|
||||||
loading: boolean |
|
||||||
} |
|
||||||
|
|
||||||
export interface RepoRecentCollection { |
|
||||||
events: RepoEvent[] |
|
||||||
loading: boolean |
|
||||||
} |
|
||||||
|
|
||||||
export type RepoPage = 'about' | 'issues' | 'proposals' |
|
||||||
|
|
||||||
export interface RepoReadme { |
|
||||||
md: string |
|
||||||
loading: boolean |
|
||||||
failed: boolean |
|
||||||
} |
|
||||||
|
|
||||||
export const readme_defaults: RepoReadme = { |
|
||||||
md: '', |
|
||||||
loading: true, |
|
||||||
failed: false, |
|
||||||
} |
|
||||||
@ -1,100 +0,0 @@ |
|||||||
import { describe, expect, test } from 'vitest' |
|
||||||
import { cloneArrayToReadMeUrls } from './utils' |
|
||||||
|
|
||||||
describe('cloneArrayToReadMeUrls', () => { |
|
||||||
test('for each clone url returns url to /raw/HEAD/README.md and /raw/HEAD/readme.md', () => { |
|
||||||
expect( |
|
||||||
cloneArrayToReadMeUrls([ |
|
||||||
'https://gitea.com/orgname/reponame', |
|
||||||
'https://gitlab.com/orgname/reponame', |
|
||||||
]) |
|
||||||
).toEqual([ |
|
||||||
'https://gitea.com/orgname/reponame/raw/HEAD/README.md', |
|
||||||
'https://gitea.com/orgname/reponame/raw/HEAD/readme.md', |
|
||||||
'https://gitlab.com/orgname/reponame/raw/HEAD/README.md', |
|
||||||
'https://gitlab.com/orgname/reponame/raw/HEAD/readme.md', |
|
||||||
]) |
|
||||||
}) |
|
||||||
test('for github link use raw.githubusercontent.com/HEAD', () => { |
|
||||||
expect( |
|
||||||
cloneArrayToReadMeUrls(['https://github.com/orgname/reponame']) |
|
||||||
).toEqual([ |
|
||||||
'https://raw.githubusercontent.com/orgname/reponame/HEAD/README.md', |
|
||||||
'https://raw.githubusercontent.com/orgname/reponame/HEAD/readme.md', |
|
||||||
]) |
|
||||||
}) |
|
||||||
test('for sr.hr link to /blob/HEAD', () => { |
|
||||||
expect(cloneArrayToReadMeUrls(['https://sr.ht/~orgname/reponame'])).toEqual( |
|
||||||
[ |
|
||||||
'https://sr.ht/~orgname/reponame/blob/HEAD/README.md', |
|
||||||
'https://sr.ht/~orgname/reponame/blob/HEAD/readme.md', |
|
||||||
] |
|
||||||
) |
|
||||||
}) |
|
||||||
test('for git.launchpad.net link to /plain', () => { |
|
||||||
expect( |
|
||||||
cloneArrayToReadMeUrls(['https://git.launchpad.net/orgname/reponame']) |
|
||||||
).toEqual([ |
|
||||||
'https://git.launchpad.net/orgname/reponame/plain/README.md', |
|
||||||
'https://git.launchpad.net/orgname/reponame/plain/readme.md', |
|
||||||
]) |
|
||||||
}) |
|
||||||
test('for git.savannah.gnu.org link to /plain', () => { |
|
||||||
expect( |
|
||||||
cloneArrayToReadMeUrls(['https://git.savannah.gnu.org/orgname/reponame']) |
|
||||||
).toEqual([ |
|
||||||
'https://git.savannah.gnu.org/orgname/reponame/plain/README.md', |
|
||||||
'https://git.savannah.gnu.org/orgname/reponame/plain/readme.md', |
|
||||||
]) |
|
||||||
}) |
|
||||||
describe('transform clone address to url', () => { |
|
||||||
test('strips trailing / from address', () => { |
|
||||||
expect( |
|
||||||
cloneArrayToReadMeUrls(['https://codeberg.org/orgname/reponame/']) |
|
||||||
).toEqual([ |
|
||||||
'https://codeberg.org/orgname/reponame/raw/HEAD/README.md', |
|
||||||
'https://codeberg.org/orgname/reponame/raw/HEAD/readme.md', |
|
||||||
]) |
|
||||||
}) |
|
||||||
test('strips .git from address', () => { |
|
||||||
expect( |
|
||||||
cloneArrayToReadMeUrls(['https://codeberg.org/orgname/reponame.git']) |
|
||||||
).toEqual([ |
|
||||||
'https://codeberg.org/orgname/reponame/raw/HEAD/README.md', |
|
||||||
'https://codeberg.org/orgname/reponame/raw/HEAD/readme.md', |
|
||||||
]) |
|
||||||
}) |
|
||||||
test('git@codeberg.org:orgname/reponame.git to address', () => { |
|
||||||
expect( |
|
||||||
cloneArrayToReadMeUrls(['git@codeberg.org:orgname/reponame.git']) |
|
||||||
).toEqual([ |
|
||||||
'https://codeberg.org/orgname/reponame/raw/HEAD/README.md', |
|
||||||
'https://codeberg.org/orgname/reponame/raw/HEAD/readme.md', |
|
||||||
]) |
|
||||||
}) |
|
||||||
test('ssh://codeberg.org/orgname/reponame to address', () => { |
|
||||||
expect( |
|
||||||
cloneArrayToReadMeUrls(['ssh://codeberg.org/orgname/reponame']) |
|
||||||
).toEqual([ |
|
||||||
'https://codeberg.org/orgname/reponame/raw/HEAD/README.md', |
|
||||||
'https://codeberg.org/orgname/reponame/raw/HEAD/readme.md', |
|
||||||
]) |
|
||||||
}) |
|
||||||
test('strips port eg ssh://git@git.v0l.io:2222/Kieran/snort.git to address', () => { |
|
||||||
expect( |
|
||||||
cloneArrayToReadMeUrls(['ssh://git@git.v0l.io:2222/Kieran/snort.git']) |
|
||||||
).toEqual([ |
|
||||||
'https://git.v0l.io/Kieran/snort/raw/HEAD/README.md', |
|
||||||
'https://git.v0l.io/Kieran/snort/raw/HEAD/readme.md', |
|
||||||
]) |
|
||||||
}) |
|
||||||
test('https://custom.com/deep/deeper/deeper to address', () => { |
|
||||||
expect( |
|
||||||
cloneArrayToReadMeUrls(['https://custom.com/deep/deeper/deeper']) |
|
||||||
).toEqual([ |
|
||||||
'https://custom.com/deep/deeper/deeper/raw/HEAD/README.md', |
|
||||||
'https://custom.com/deep/deeper/deeper/raw/HEAD/readme.md', |
|
||||||
]) |
|
||||||
}) |
|
||||||
}) |
|
||||||
}) |
|
||||||
@ -1,135 +0,0 @@ |
|||||||
import type { RepoCollection, RepoEvent } from './type' |
|
||||||
import { nip19 } from 'nostr-tools' |
|
||||||
import { repo_kind } from '$lib/kinds' |
|
||||||
import type { NDKEvent } from '@nostr-dev-kit/ndk' |
|
||||||
import type { AddressPointer } from 'nostr-tools/nip19' |
|
||||||
|
|
||||||
export const selectRepoFromCollection = ( |
|
||||||
collection: RepoCollection |
|
||||||
): RepoEvent | undefined => { |
|
||||||
return collection.events[collection.most_recent_index] |
|
||||||
} |
|
||||||
|
|
||||||
/** most servers will produce a CORS error so a proxy should be used */ |
|
||||||
export const cloneArrayToReadMeUrls = (clone: string[]): string[] => { |
|
||||||
const addresses = clone.map(extractRepoAddress) |
|
||||||
/** |
|
||||||
* at the time of this commit these urls work for: |
|
||||||
* self-hosted gitea (or forgejo), gitlab |
|
||||||
* github.com |
|
||||||
* bitbucket.org |
|
||||||
* gitlab.org |
|
||||||
* gitea.com |
|
||||||
* codeberg.org (forgejo instance) |
|
||||||
* sourcehut (git.sr.ht) |
|
||||||
* launchpad.net |
|
||||||
* It doesnt work for: |
|
||||||
* self-hosted gogs (requires branch name repo/raw/master/README.md) |
|
||||||
* sourceforge.net (https://sourceforge.net/p/mingw/catgets/ci/master/tree/README?format=raw)
|
|
||||||
* notabug.org (requires branch name notabug.org/org/repo/raw/master/README.md) |
|
||||||
*/ |
|
||||||
return [ |
|
||||||
...addresses.flatMap((address) => { |
|
||||||
let prefix = 'raw/HEAD' |
|
||||||
if (address.includes('sr.ht')) prefix = 'blob/HEAD' |
|
||||||
if ( |
|
||||||
address.includes('git.launchpad.net') || |
|
||||||
address.includes('git.savannah.gnu.org') |
|
||||||
) |
|
||||||
prefix = 'plain' |
|
||||||
if (address.includes('github.com')) { |
|
||||||
// raw.githubusercontent.com can be used without CORS error
|
|
||||||
address = address.replace('github.com', 'raw.githubusercontent.com') |
|
||||||
prefix = 'HEAD' |
|
||||||
} |
|
||||||
return ['README.md', 'readme.md'].map( |
|
||||||
(filename) => `https://${address}/${prefix}/${filename}` |
|
||||||
) |
|
||||||
}), |
|
||||||
] |
|
||||||
} |
|
||||||
|
|
||||||
const extractRepoAddress = (clone_string: string): string => { |
|
||||||
let s = clone_string |
|
||||||
// remove trailing slash
|
|
||||||
if (s.endsWith('/')) s = s.substring(0, s.length - 1) |
|
||||||
// remove trailing .git
|
|
||||||
if (s.endsWith('.git')) s = s.substring(0, s.length - 4) |
|
||||||
// remove :// and anything before
|
|
||||||
if (s.includes('://')) s = s.split('://')[1] |
|
||||||
// remove @ and anything before
|
|
||||||
if (s.includes('@')) s = s.split('@')[1] |
|
||||||
// replace : with /
|
|
||||||
s = s.replace(/\s|:[0-9]+/g, '') |
|
||||||
s = s.replace(':', '/') |
|
||||||
return s |
|
||||||
} |
|
||||||
|
|
||||||
export const naddrToPointer = (s: string): AddressPointer | undefined => { |
|
||||||
const decoded = nip19.decode(s) |
|
||||||
if ( |
|
||||||
typeof decoded.data === 'string' || |
|
||||||
!Object.keys(decoded.data).includes('identifier') |
|
||||||
) |
|
||||||
return undefined |
|
||||||
return decoded.data as AddressPointer |
|
||||||
} |
|
||||||
|
|
||||||
export const extractAReference = (a: string): AddressPointer | undefined => { |
|
||||||
if (a.split(':').length !== 3) return undefined |
|
||||||
const [k, pubkey, identifier] = a.split(':') |
|
||||||
return { kind: Number(k), pubkey, identifier } |
|
||||||
} |
|
||||||
|
|
||||||
export const naddrToRepoA = (s: string): string | undefined => { |
|
||||||
const pointer = naddrToPointer(s) |
|
||||||
if (pointer && pointer.kind === repo_kind) |
|
||||||
return `${repo_kind}:${pointer.pubkey}:${pointer.identifier}` |
|
||||||
return undefined |
|
||||||
} |
|
||||||
|
|
||||||
export const aToNaddr = ( |
|
||||||
a: string | AddressPointer |
|
||||||
): `naddr1${string}` | undefined => { |
|
||||||
const a_ref = typeof a === 'string' ? extractAReference(a) : a |
|
||||||
if (!a_ref) return undefined |
|
||||||
return nip19.naddrEncode(a_ref) |
|
||||||
} |
|
||||||
|
|
||||||
export const neventOrNoteToHexId = (s: string): string | undefined => { |
|
||||||
try { |
|
||||||
const decoded = nip19.decode(s) |
|
||||||
if (decoded.type === 'note') return decoded.data |
|
||||||
else if (decoded.type === 'nevent') return decoded.data.id |
|
||||||
} catch {} |
|
||||||
return undefined |
|
||||||
} |
|
||||||
|
|
||||||
/** this functoin can be removed when ndk.encode includes kind in nevent */ |
|
||||||
export const ndkEventToNeventOrNaddr = ( |
|
||||||
event: NDKEvent |
|
||||||
): string | undefined => { |
|
||||||
let relays: string[] = [] |
|
||||||
if (event.onRelays.length > 0) { |
|
||||||
relays = event.onRelays.map((relay) => relay.url) |
|
||||||
} else if (event.relay) { |
|
||||||
relays = [event.relay.url] |
|
||||||
} |
|
||||||
if (event.kind && event.isParamReplaceable()) { |
|
||||||
return nip19.naddrEncode({ |
|
||||||
kind: event.kind, |
|
||||||
pubkey: event.pubkey, |
|
||||||
identifier: event.replaceableDTag(), |
|
||||||
relays, |
|
||||||
}) |
|
||||||
} else if (relays.length > 0) { |
|
||||||
return nip19.neventEncode({ |
|
||||||
kind: event.kind, |
|
||||||
id: event.tagId(), |
|
||||||
relays, |
|
||||||
author: event.pubkey, |
|
||||||
}) |
|
||||||
} else { |
|
||||||
return nip19.noteEncode(event.tagId()) |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,127 +0,0 @@ |
|||||||
import { UserVectors, withName } from '../users/vectors' |
|
||||||
import type { RepoEventWithMaintainersMetadata, RepoSummary } from './type' |
|
||||||
|
|
||||||
export const RepoSummaryCardArgsVectors = { |
|
||||||
Short: { |
|
||||||
name: 'Short Name', |
|
||||||
description: 'short description', |
|
||||||
maintainers: [withName(UserVectors.default, 'Will')], |
|
||||||
} as RepoSummary, |
|
||||||
Long: { |
|
||||||
name: 'Long Name that goes on and on and on and on and on and on and on and on and on', |
|
||||||
description: |
|
||||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis quis nisl eget turpis congue molestie. Nulla vitae purus nec augue accumsan facilisis sed sed ligula. Vestibulum sed risus lacinia risus lacinia molestie. Ut lorem quam, consequat eget tempus in, rhoncus vel nunc. Duis efficitur a leo vel sodales. Nam id fermentum lacus. Etiam nec placerat velit. Praesent ac consectetur est. Aenean iaculis commodo enim.', |
|
||||||
maintainers: [withName(UserVectors.default, 'Rather Long Display Name')], |
|
||||||
} as RepoSummary, |
|
||||||
LongNoSpaces: { |
|
||||||
name: 'LongNameLongNameLongNameLongNameLongNameLongNameLongNameLongName', |
|
||||||
description: |
|
||||||
'LoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsum>', |
|
||||||
maintainers: [ |
|
||||||
{ |
|
||||||
...UserVectors.default, |
|
||||||
}, |
|
||||||
], |
|
||||||
} as RepoSummary, |
|
||||||
MulipleMaintainers: { |
|
||||||
name: 'Short Name', |
|
||||||
description: 'short description', |
|
||||||
maintainers: [ |
|
||||||
withName(UserVectors.default, 'Will'), |
|
||||||
withName(UserVectors.default, 'DanConwayDev'), |
|
||||||
withName(UserVectors.default, 'sectore'), |
|
||||||
], |
|
||||||
} as RepoSummary, |
|
||||||
} |
|
||||||
const base: RepoEventWithMaintainersMetadata = { |
|
||||||
identifier: '9ee507fc4357d7ee16a5d8901bedcd103f23c17d', |
|
||||||
unique_commit: '9ee507fc4357d7ee16a5d8901bedcd103f23c17d', |
|
||||||
author: '', |
|
||||||
name: 'Short Name', |
|
||||||
description: 'short description', |
|
||||||
clone: ['github.com/example/example'], |
|
||||||
tags: ['svelte', 'nostr', 'code-collaboration', 'git'], |
|
||||||
relays: ['relay.damus.io', 'relay.snort.social', 'relayable.org'], |
|
||||||
maintainers: [ |
|
||||||
withName(UserVectors.default, 'carole'), |
|
||||||
withName(UserVectors.default, 'bob'), |
|
||||||
withName(UserVectors.default, 'steve'), |
|
||||||
], |
|
||||||
loading: false, |
|
||||||
event_id: '', |
|
||||||
naddr: '', |
|
||||||
web: ['https://gitcitadel.eu/repo/example', 'https://example.com'], |
|
||||||
referenced_by: [], |
|
||||||
most_recent_reference_timestamp: 0, |
|
||||||
created_at: 0, |
|
||||||
} |
|
||||||
|
|
||||||
export const RepoDetailsArgsVectors = { |
|
||||||
Short: { ...base } as RepoEventWithMaintainersMetadata, |
|
||||||
Long: { |
|
||||||
...base, |
|
||||||
name: 'Long Name that goes on and on and on and on and on and on and on and on and on', |
|
||||||
description: |
|
||||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis quis nisl eget turpis congue molestie. Nulla vitae purus nec augue accumsan facilisis sed sed ligula. Vestibulum sed risus lacinia risus lacinia molestie. Ut lorem quam, consequat eget tempus in, rhoncus vel nunc. Duis efficitur a leo vel sodales. Nam id fermentum lacus. Etiam nec placerat velit. Praesent ac consectetur est. Aenean iaculis commodo enim.\n Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis quis nisl eget turpis congue molestie.', |
|
||||||
} as RepoEventWithMaintainersMetadata, |
|
||||||
LongNoSpaces: { |
|
||||||
...base, |
|
||||||
name: 'LongNameLongNameLongNameLongNameLongNameLongNameLongNameLongName', |
|
||||||
description: |
|
||||||
'LoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsumLoremipsum', |
|
||||||
} as RepoEventWithMaintainersMetadata, |
|
||||||
NoNameOrDescription: { |
|
||||||
...base, |
|
||||||
name: '', |
|
||||||
description: '', |
|
||||||
} as RepoEventWithMaintainersMetadata, |
|
||||||
NoDescription: { |
|
||||||
...base, |
|
||||||
description: '', |
|
||||||
} as RepoEventWithMaintainersMetadata, |
|
||||||
NoTags: { ...base, tags: [] } as RepoEventWithMaintainersMetadata, |
|
||||||
NoGitServer: { ...base, clone: [''] } as RepoEventWithMaintainersMetadata, |
|
||||||
NoWeb: { ...base, web: [] } as RepoEventWithMaintainersMetadata, |
|
||||||
MaintainersOneProfileNotLoaded: { |
|
||||||
...base, |
|
||||||
maintainers: [ |
|
||||||
{ ...base.maintainers[0] }, |
|
||||||
{ ...UserVectors.loading }, |
|
||||||
{ ...base.maintainers[2] }, |
|
||||||
], |
|
||||||
} as RepoEventWithMaintainersMetadata, |
|
||||||
MaintainersOneProfileDisplayNameWithoutName: { |
|
||||||
...base, |
|
||||||
maintainers: [ |
|
||||||
{ ...base.maintainers[0] }, |
|
||||||
{ ...UserVectors.display_name_only }, |
|
||||||
{ ...base.maintainers[2] }, |
|
||||||
], |
|
||||||
} as RepoEventWithMaintainersMetadata, |
|
||||||
MaintainersOneProfileNameAndDisplayNamePresent: { |
|
||||||
...base, |
|
||||||
maintainers: [ |
|
||||||
{ ...base.maintainers[0] }, |
|
||||||
{ ...UserVectors.display_name_and_name }, |
|
||||||
{ ...base.maintainers[2] }, |
|
||||||
], |
|
||||||
} as RepoEventWithMaintainersMetadata, |
|
||||||
MaintainersOneProfileNoNameOrDisplayNameBeingPresent: { |
|
||||||
...base, |
|
||||||
maintainers: [ |
|
||||||
{ ...base.maintainers[0] }, |
|
||||||
{ ...UserVectors.no_profile }, |
|
||||||
{ ...base.maintainers[2] }, |
|
||||||
], |
|
||||||
} as RepoEventWithMaintainersMetadata, |
|
||||||
NoMaintainers: { |
|
||||||
...base, |
|
||||||
maintainers: [], |
|
||||||
} as RepoEventWithMaintainersMetadata, |
|
||||||
NoRelays: { ...base, relays: [] } as RepoEventWithMaintainersMetadata, |
|
||||||
NoMaintainersOrRelays: { |
|
||||||
...base, |
|
||||||
maintainers: [], |
|
||||||
relays: [], |
|
||||||
} as RepoEventWithMaintainersMetadata, |
|
||||||
} |
|
||||||
@ -1,141 +0,0 @@ |
|||||||
<script lang="ts" context="module"> |
|
||||||
import type { Meta } from '@storybook/svelte' |
|
||||||
import UserHeader from '$lib/components/users/UserHeader.svelte' |
|
||||||
import { Story, Template } from '@storybook/addon-svelte-csf' |
|
||||||
import { UserVectors as vectors } from './vectors' |
|
||||||
|
|
||||||
export const meta: Meta<UserHeader> = { |
|
||||||
title: 'Users/Header', |
|
||||||
component: UserHeader, |
|
||||||
tags: ['autodocs'], |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<Template let:args> |
|
||||||
<UserHeader {...args} /> |
|
||||||
</Template> |
|
||||||
|
|
||||||
<Story |
|
||||||
name="default" |
|
||||||
args={{ |
|
||||||
user: { |
|
||||||
...vectors.default, |
|
||||||
}, |
|
||||||
}} |
|
||||||
/> |
|
||||||
|
|
||||||
<Story |
|
||||||
name="small" |
|
||||||
args={{ |
|
||||||
user: { |
|
||||||
...vectors.default, |
|
||||||
}, |
|
||||||
size: 'sm', |
|
||||||
}} |
|
||||||
/> |
|
||||||
|
|
||||||
<Story |
|
||||||
name="extra small" |
|
||||||
args={{ |
|
||||||
user: { |
|
||||||
...vectors.default, |
|
||||||
}, |
|
||||||
size: 'xs', |
|
||||||
}} |
|
||||||
/> |
|
||||||
|
|
||||||
<Story |
|
||||||
name="no image" |
|
||||||
args={{ |
|
||||||
user: { |
|
||||||
...vectors.no_image, |
|
||||||
}, |
|
||||||
}} |
|
||||||
/> |
|
||||||
|
|
||||||
<Story |
|
||||||
name="long name truncated" |
|
||||||
args={{ |
|
||||||
user: { |
|
||||||
...vectors.long_name, |
|
||||||
}, |
|
||||||
}} |
|
||||||
/> |
|
||||||
|
|
||||||
<Story name="loading" args={{ user: { ...vectors.loading } }} /> |
|
||||||
|
|
||||||
<Story name="not found" args={{ user: { ...vectors.no_profile } }} /> |
|
||||||
|
|
||||||
<Story |
|
||||||
name="displayName without name" |
|
||||||
args={{ user: { ...vectors.display_name_only } }} |
|
||||||
/> |
|
||||||
|
|
||||||
<Story |
|
||||||
name="name and displayName shows name" |
|
||||||
args={{ user: { ...vectors.display_name_and_name } }} |
|
||||||
/> |
|
||||||
|
|
||||||
<Story |
|
||||||
name="inline" |
|
||||||
args={{ |
|
||||||
user: { |
|
||||||
...vectors.default, |
|
||||||
}, |
|
||||||
inline: true, |
|
||||||
}} |
|
||||||
/> |
|
||||||
|
|
||||||
<Story |
|
||||||
name="inline loading" |
|
||||||
args={{ |
|
||||||
user: { |
|
||||||
...vectors.loading, |
|
||||||
}, |
|
||||||
inline: true, |
|
||||||
}} |
|
||||||
/> |
|
||||||
|
|
||||||
<Story |
|
||||||
name="inline small" |
|
||||||
args={{ |
|
||||||
user: { |
|
||||||
...vectors.default, |
|
||||||
}, |
|
||||||
inline: true, |
|
||||||
size: 'sm', |
|
||||||
}} |
|
||||||
/> |
|
||||||
|
|
||||||
<Story |
|
||||||
name="inline small loading" |
|
||||||
args={{ |
|
||||||
user: { |
|
||||||
...vectors.loading, |
|
||||||
}, |
|
||||||
inline: true, |
|
||||||
size: 'sm', |
|
||||||
}} |
|
||||||
/> |
|
||||||
|
|
||||||
<Story |
|
||||||
name="inline extra small" |
|
||||||
args={{ |
|
||||||
user: { |
|
||||||
...vectors.default, |
|
||||||
}, |
|
||||||
inline: true, |
|
||||||
size: 'xs', |
|
||||||
}} |
|
||||||
/> |
|
||||||
|
|
||||||
<Story |
|
||||||
name="inline extra small loading" |
|
||||||
args={{ |
|
||||||
user: { |
|
||||||
...vectors.loading, |
|
||||||
}, |
|
||||||
inline: true, |
|
||||||
size: 'xs', |
|
||||||
}} |
|
||||||
/> |
|
||||||
@ -1,166 +0,0 @@ |
|||||||
<script lang="ts"> |
|
||||||
import { ensureUser } from '$lib/stores/users' |
|
||||||
import type { Unsubscriber } from 'svelte/store' |
|
||||||
import { defaults, getName, type User, type UserObject } from './type' |
|
||||||
import { onDestroy } from 'svelte' |
|
||||||
import { goto } from '$app/navigation' |
|
||||||
import ParsedContent from '../events/content/ParsedContent.svelte' |
|
||||||
import CopyField from '../CopyField.svelte' |
|
||||||
import { icons_misc } from '../icons' |
|
||||||
|
|
||||||
export let user: User = { |
|
||||||
...defaults, |
|
||||||
} |
|
||||||
|
|
||||||
export let inline = false |
|
||||||
export let size: 'xs' | 'sm' | 'md' | 'full' = 'md' |
|
||||||
export let avatar_only = false |
|
||||||
export let in_event_header = false |
|
||||||
export let link_to_profile = true |
|
||||||
|
|
||||||
let user_object: UserObject = { |
|
||||||
...defaults, |
|
||||||
} |
|
||||||
let unsubscriber: Unsubscriber |
|
||||||
$: { |
|
||||||
if (typeof user === 'string') { |
|
||||||
if (unsubscriber) unsubscriber() |
|
||||||
unsubscriber = ensureUser(user).subscribe((u) => { |
|
||||||
user_object = { ...u } |
|
||||||
}) |
|
||||||
} else user_object = user |
|
||||||
} |
|
||||||
onDestroy(() => { |
|
||||||
if (unsubscriber) unsubscriber() |
|
||||||
}) |
|
||||||
$: ({ profile, loading } = user_object) |
|
||||||
$: display_name = getName(user_object) |
|
||||||
</script> |
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events --> |
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions --> |
|
||||||
<div |
|
||||||
class:inline-block={inline} |
|
||||||
class:cursor-pointer={link_to_profile} |
|
||||||
on:click={() => { |
|
||||||
if (link_to_profile) goto(`/p/${user_object.npub}`) |
|
||||||
}} |
|
||||||
> |
|
||||||
<div |
|
||||||
class:my-2={!inline} |
|
||||||
class:text-xs={size === 'xs'} |
|
||||||
class:text-sm={size === 'sm'} |
|
||||||
class:text-md={size === 'md'} |
|
||||||
class:align-middle={inline} |
|
||||||
class:flex={!inline} |
|
||||||
class:items-center={!inline} |
|
||||||
> |
|
||||||
<div |
|
||||||
class="avatar" |
|
||||||
class:inline-block={inline} |
|
||||||
class:align-middle={inline} |
|
||||||
class:flex-none={!inline} |
|
||||||
> |
|
||||||
<div |
|
||||||
class:inline-block={inline} |
|
||||||
class:h-32={!inline && size === 'full'} |
|
||||||
class:w-32={!inline && size === 'full'} |
|
||||||
class:h-8={!inline && size === 'md'} |
|
||||||
class:w-8={!inline && size === 'md'} |
|
||||||
class:h-4={!inline && size === 'sm'} |
|
||||||
class:w-4={!inline && size === 'sm'} |
|
||||||
class:h-5={inline && size === 'md'} |
|
||||||
class:w-5={inline && size === 'md'} |
|
||||||
class:h-3.5={(inline && size === 'sm') || size === 'xs'} |
|
||||||
class:w-3.5={(inline && size === 'sm') || size === 'xs'} |
|
||||||
class="rounded" |
|
||||||
class:skeleton={!profile && loading} |
|
||||||
class:bg-neutral={!loading && (!profile || !profile.image)} |
|
||||||
> |
|
||||||
{#if profile && profile?.image} |
|
||||||
<img class="my-0" src={profile?.image} alt={display_name} /> |
|
||||||
{/if} |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
<div |
|
||||||
class:text-xl={size === 'full'} |
|
||||||
class:width-max-prose={size === 'full'} |
|
||||||
class:pl-4={!inline && size === 'full'} |
|
||||||
class:pl-3={!inline && size === 'md'} |
|
||||||
class:pl-2={!inline && (size === 'sm' || size === 'xs')} |
|
||||||
class:pl-0={inline} |
|
||||||
class:flex-auto={!inline} |
|
||||||
class:m-auto={!inline} |
|
||||||
class:inline-block={inline} |
|
||||||
class:hidden={avatar_only} |
|
||||||
class:opacity-40={in_event_header} |
|
||||||
> |
|
||||||
{#if loading} |
|
||||||
<div |
|
||||||
class="skeleton w-24" |
|
||||||
class:h-4={size === 'md'} |
|
||||||
class:h-3={size === 'sm'} |
|
||||||
class:h-2.5={size === 'xs'} |
|
||||||
></div> |
|
||||||
{:else} |
|
||||||
<span class:font-bold={in_event_header || size === 'full'} |
|
||||||
>{display_name}</span |
|
||||||
> |
|
||||||
{/if} |
|
||||||
{#if size === 'full'} |
|
||||||
<CopyField |
|
||||||
icon={icons_misc.key} |
|
||||||
content={user_object.npub} |
|
||||||
no_border |
|
||||||
truncate={[10, 10]} |
|
||||||
/> |
|
||||||
{#if profile && profile.lud16} |
|
||||||
<CopyField |
|
||||||
icon={icons_misc.lightning} |
|
||||||
content={profile.lud16} |
|
||||||
no_border |
|
||||||
/> |
|
||||||
{/if} |
|
||||||
{#if profile && profile.website} |
|
||||||
<a |
|
||||||
href={profile.website} |
|
||||||
target="_blank" |
|
||||||
class="items items-top mt-1 flex w-full opacity-60" |
|
||||||
> |
|
||||||
<svg |
|
||||||
xmlns="http://www.w3.org/2000/svg" |
|
||||||
viewBox="0 0 16 16" |
|
||||||
class="mr-1 inline h-4 w-4 flex-none fill-base-content opacity-50" |
|
||||||
> |
|
||||||
{#each icons_misc.link as d} |
|
||||||
<path {d} /> |
|
||||||
{/each} |
|
||||||
</svg> |
|
||||||
<div class="link-secondary text-sm">{profile.website}</div> |
|
||||||
</a> |
|
||||||
{/if} |
|
||||||
{#if size === 'full' && profile && profile.about} |
|
||||||
<div class="items items-top flex max-w-md opacity-60"> |
|
||||||
<svg |
|
||||||
xmlns="http://www.w3.org/2000/svg" |
|
||||||
viewBox="0 0 16 16" |
|
||||||
class="mr-1 mt-1 inline h-4 w-4 flex-none fill-base-content opacity-50" |
|
||||||
> |
|
||||||
{#each icons_misc.info as d} |
|
||||||
<path {d} /> |
|
||||||
{/each} |
|
||||||
</svg> |
|
||||||
|
|
||||||
{#if loading} |
|
||||||
<div class="w.max-lg skeleton h-3"></div> |
|
||||||
{:else} |
|
||||||
<div class="text-sm"> |
|
||||||
<ParsedContent content={profile?.about} /> |
|
||||||
</div> |
|
||||||
{/if} |
|
||||||
</div> |
|
||||||
{/if} |
|
||||||
{/if} |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
@ -1,38 +0,0 @@ |
|||||||
import type { NDKUserProfile } from '@nostr-dev-kit/ndk' |
|
||||||
|
|
||||||
export interface UserObject { |
|
||||||
loading: boolean |
|
||||||
hexpubkey: string |
|
||||||
npub: string |
|
||||||
profile?: NDKUserProfile |
|
||||||
} |
|
||||||
|
|
||||||
export const defaults: UserObject = { |
|
||||||
loading: true, |
|
||||||
hexpubkey: '', |
|
||||||
npub: '', |
|
||||||
} |
|
||||||
|
|
||||||
export type User = UserObject | string |
|
||||||
|
|
||||||
export function getName(user: UserObject, truncate_above = 25): string { |
|
||||||
return truncate( |
|
||||||
user.profile |
|
||||||
? user.profile.name |
|
||||||
? user.profile.name |
|
||||||
: user.profile.displayName |
|
||||||
? user.profile.displayName |
|
||||||
: truncateNpub(user.npub) |
|
||||||
: truncateNpub(user.npub), |
|
||||||
truncate_above |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
function truncateNpub(npub: string): string { |
|
||||||
return `${npub.substring(0, 9)}...` |
|
||||||
} |
|
||||||
|
|
||||||
function truncate(s: string, truncate_above = 20): string { |
|
||||||
if (s.length < truncate_above || truncate_above < 5) return s |
|
||||||
return `${s.substring(0, truncate_above - 3)}...` |
|
||||||
} |
|
||||||
@ -1,39 +0,0 @@ |
|||||||
import type { UserObject } from './type' |
|
||||||
|
|
||||||
// nsec1rg53qfv09az39dlw6j64ange3cx8sh5p8np29qcxtythplvplktsv93tnr
|
|
||||||
const base: UserObject = { |
|
||||||
hexpubkey: '3eb45c6f15752d796fa5465d0530a5a5feb79fb6f08c0a4176be9d73cc28c40d', |
|
||||||
npub: 'npub18669cmc4w5khjma9gews2v995hlt08ak7zxq5stkh6wh8npgcsxslt2xjn', |
|
||||||
loading: false, |
|
||||||
} |
|
||||||
|
|
||||||
const image = '../test-profile-image.jpg' |
|
||||||
|
|
||||||
export const UserVectors = { |
|
||||||
loading: { ...base, loading: true } as UserObject, |
|
||||||
default: { ...base, profile: { name: 'DanConwayDev', image } } as UserObject, |
|
||||||
display_name_only: { |
|
||||||
...base, |
|
||||||
profile: { displayName: 'DanConwayDev', image }, |
|
||||||
} as UserObject, |
|
||||||
display_name_and_name: { |
|
||||||
...base, |
|
||||||
profile: { name: 'Dan', displayName: 'DanConwayDev', image }, |
|
||||||
} as UserObject, |
|
||||||
no_image: { ...base, profile: { name: 'DanConwayDev' } } as UserObject, |
|
||||||
no_profile: { ...base } as UserObject, |
|
||||||
long_name: { |
|
||||||
...base, |
|
||||||
profile: { name: 'Really Really Long Long Name', image }, |
|
||||||
} as UserObject, |
|
||||||
} |
|
||||||
|
|
||||||
export function withName(base: UserObject, name: string): UserObject { |
|
||||||
return { |
|
||||||
...base, |
|
||||||
profile: { |
|
||||||
...base.profile, |
|
||||||
name, |
|
||||||
}, |
|
||||||
} as UserObject |
|
||||||
} |
|
||||||
@ -1,29 +0,0 @@ |
|||||||
export const reply_kind: number = 1 |
|
||||||
|
|
||||||
export const proposal_status_open: number = 1630 |
|
||||||
export const proposal_status_applied: number = 1631 |
|
||||||
export const proposal_status_closed: number = 1632 |
|
||||||
export const proposal_status_draft: number = 1633 |
|
||||||
export const proposal_status_kinds: number[] = [ |
|
||||||
proposal_status_open, |
|
||||||
proposal_status_applied, |
|
||||||
proposal_status_closed, |
|
||||||
proposal_status_draft, |
|
||||||
] |
|
||||||
|
|
||||||
export function statusKindtoText( |
|
||||||
kind: number, |
|
||||||
type: 'proposal' | 'issue' |
|
||||||
): string { |
|
||||||
if (kind === proposal_status_open) return 'Open' |
|
||||||
if (type === 'proposal' && kind === proposal_status_applied) return 'Applied' |
|
||||||
if (type === 'issue' && kind === proposal_status_applied) return 'Resolved' |
|
||||||
if (kind === proposal_status_closed) return 'Closed' |
|
||||||
return 'Draft' |
|
||||||
} |
|
||||||
|
|
||||||
export const repo_kind: number = 30617 |
|
||||||
|
|
||||||
export const patch_kind: number = 1617 |
|
||||||
|
|
||||||
export const issue_kind: number = 1621 |
|
||||||
@ -1,204 +0,0 @@ |
|||||||
import { NDKRelaySet, NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk' |
|
||||||
import { writable, type Writable } from 'svelte/store' |
|
||||||
import { base_relays, ndk } from './ndk' |
|
||||||
import { type IssueFull, full_defaults } from '$lib/components/issues/type' |
|
||||||
import { proposal_status_kinds, proposal_status_open } from '$lib/kinds' |
|
||||||
import { awaitSelectedRepoCollection } from './repo' |
|
||||||
import { |
|
||||||
extractIssueDescription, |
|
||||||
extractIssueTitle, |
|
||||||
} from '$lib/components/events/content/utils' |
|
||||||
import { selectRepoFromCollection } from '$lib/components/repo/utils' |
|
||||||
import { ignore_kinds } from './utils' |
|
||||||
|
|
||||||
export const selected_issue_full: Writable<IssueFull> = writable({ |
|
||||||
...full_defaults, |
|
||||||
}) |
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
let selected_issue_repo_a: string = '' |
|
||||||
let selected_issue_id: string = '' |
|
||||||
|
|
||||||
export const selected_issue_replies: Writable<NDKEvent[]> = writable([]) |
|
||||||
|
|
||||||
let selected_issue_status_date = 0 |
|
||||||
|
|
||||||
let sub: NDKSubscription |
|
||||||
|
|
||||||
let sub_replies: NDKSubscription |
|
||||||
|
|
||||||
const sub_replies_to_replies: NDKSubscription[] = [] |
|
||||||
|
|
||||||
export const ensureIssueFull = ( |
|
||||||
repo_a: string, |
|
||||||
issue_id_or_event: string | NDKEvent |
|
||||||
) => { |
|
||||||
const issue_id = |
|
||||||
typeof issue_id_or_event === 'string' |
|
||||||
? issue_id_or_event |
|
||||||
: issue_id_or_event.id |
|
||||||
if (selected_issue_id == issue_id) return |
|
||||||
if (issue_id == '') { |
|
||||||
selected_issue_full.set({ ...full_defaults }) |
|
||||||
selected_issue_replies.set([]) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
if (sub) sub.stop() |
|
||||||
if (sub_replies) sub_replies.stop() |
|
||||||
sub_replies_to_replies.forEach((sub) => sub.stop()) |
|
||||||
|
|
||||||
selected_issue_repo_a = repo_a |
|
||||||
selected_issue_id = issue_id |
|
||||||
selected_issue_status_date = 0 |
|
||||||
selected_issue_replies.set([]) |
|
||||||
|
|
||||||
selected_issue_full.set({ |
|
||||||
...full_defaults, |
|
||||||
summary: { |
|
||||||
...full_defaults.summary, |
|
||||||
id: issue_id, |
|
||||||
repo_a, |
|
||||||
loading: true, |
|
||||||
}, |
|
||||||
loading: true, |
|
||||||
}) |
|
||||||
|
|
||||||
new Promise(async (r) => { |
|
||||||
const repo_collection = await awaitSelectedRepoCollection(repo_a) |
|
||||||
const repo = selectRepoFromCollection(repo_collection) |
|
||||||
const relays_to_use = |
|
||||||
repo && repo.relays.length > 3 |
|
||||||
? repo.relays |
|
||||||
: [...base_relays].concat(repo ? repo.relays : []) |
|
||||||
|
|
||||||
const setEvent = (event: NDKEvent) => { |
|
||||||
try { |
|
||||||
selected_issue_full.update((full) => { |
|
||||||
return { |
|
||||||
...full, |
|
||||||
issue_event: event, |
|
||||||
summary: { |
|
||||||
...full.summary, |
|
||||||
title: extractIssueTitle(event), |
|
||||||
descritpion: extractIssueDescription(event.content), |
|
||||||
created_at: event.created_at, |
|
||||||
comments: 0, |
|
||||||
author: event.pubkey, |
|
||||||
loading: false, |
|
||||||
}, |
|
||||||
} |
|
||||||
}) |
|
||||||
} catch {} |
|
||||||
} |
|
||||||
if (typeof issue_id_or_event !== 'string') { |
|
||||||
setEvent(issue_id_or_event) |
|
||||||
} else { |
|
||||||
sub = ndk.subscribe( |
|
||||||
{ |
|
||||||
ids: [issue_id], |
|
||||||
limit: 100, |
|
||||||
}, |
|
||||||
{ |
|
||||||
closeOnEose: false, |
|
||||||
}, |
|
||||||
NDKRelaySet.fromRelayUrls(relays_to_use, ndk) |
|
||||||
) |
|
||||||
|
|
||||||
sub.on('event', (event: NDKEvent) => { |
|
||||||
if (event.id == issue_id) setEvent(event) |
|
||||||
}) |
|
||||||
|
|
||||||
sub.on('eose', () => { |
|
||||||
selected_issue_full.update((full) => { |
|
||||||
const updated = { |
|
||||||
...full, |
|
||||||
summary: { |
|
||||||
...full.summary, |
|
||||||
loading: false, |
|
||||||
}, |
|
||||||
} |
|
||||||
if (full.loading === false) { |
|
||||||
r({ ...updated }) |
|
||||||
} |
|
||||||
return updated |
|
||||||
}) |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
sub_replies = ndk.subscribe( |
|
||||||
{ |
|
||||||
'#e': [issue_id], |
|
||||||
}, |
|
||||||
{ |
|
||||||
closeOnEose: false, |
|
||||||
}, |
|
||||||
NDKRelaySet.fromRelayUrls(relays_to_use, ndk) |
|
||||||
) |
|
||||||
|
|
||||||
const process_replies = (event: NDKEvent) => { |
|
||||||
if (event.kind && ignore_kinds.includes(event.kind)) return false |
|
||||||
if ( |
|
||||||
event.kind && |
|
||||||
proposal_status_kinds.includes(event.kind) && |
|
||||||
event.created_at && |
|
||||||
selected_issue_status_date < event.created_at |
|
||||||
) { |
|
||||||
selected_issue_status_date = event.created_at |
|
||||||
selected_issue_full.update((full) => { |
|
||||||
return { |
|
||||||
...full, |
|
||||||
summary: { |
|
||||||
...full.summary, |
|
||||||
status: event.kind, |
|
||||||
// this wont be 0 as we are ensuring it is not undefined above
|
|
||||||
status_date: event.created_at || 0, |
|
||||||
}, |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
selected_issue_replies.update((replies) => { |
|
||||||
if (!replies.some((e) => e.id === event.id)) { |
|
||||||
const sub_replies_to_reply = ndk.subscribe( |
|
||||||
{ |
|
||||||
'#e': [event.id], |
|
||||||
}, |
|
||||||
{ |
|
||||||
groupable: true, |
|
||||||
groupableDelay: 300, |
|
||||||
closeOnEose: false, |
|
||||||
}, |
|
||||||
NDKRelaySet.fromRelayUrls(relays_to_use, ndk) |
|
||||||
) |
|
||||||
sub_replies_to_reply.on('event', (event: NDKEvent) => { |
|
||||||
process_replies(event) |
|
||||||
}) |
|
||||||
sub_replies_to_replies.push(sub_replies_to_reply) |
|
||||||
return [...replies, event] |
|
||||||
} |
|
||||||
return [...replies] |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
sub_replies.on('event', (event: NDKEvent) => { |
|
||||||
process_replies(event) |
|
||||||
}) |
|
||||||
|
|
||||||
sub_replies.on('eose', () => { |
|
||||||
selected_issue_full.update((full) => { |
|
||||||
const updated = { |
|
||||||
...full, |
|
||||||
summary: { |
|
||||||
...full.summary, |
|
||||||
status: full.summary.status || proposal_status_open, |
|
||||||
}, |
|
||||||
loading: false, |
|
||||||
} |
|
||||||
if (full.summary.loading === false) { |
|
||||||
r({ ...updated }) |
|
||||||
} |
|
||||||
return updated |
|
||||||
}) |
|
||||||
}) |
|
||||||
}) |
|
||||||
} |
|
||||||
@ -1,203 +0,0 @@ |
|||||||
import { |
|
||||||
NDKRelaySet, |
|
||||||
type NDKEvent, |
|
||||||
NDKSubscription, |
|
||||||
type NDKFilter, |
|
||||||
} from '@nostr-dev-kit/ndk' |
|
||||||
import { writable, type Writable } from 'svelte/store' |
|
||||||
import { base_relays, ndk } from './ndk' |
|
||||||
import { awaitSelectedRepoCollection } from './repo' |
|
||||||
import { |
|
||||||
issue_kind, |
|
||||||
proposal_status_kinds, |
|
||||||
proposal_status_open, |
|
||||||
repo_kind, |
|
||||||
} from '$lib/kinds' |
|
||||||
import { |
|
||||||
extractIssueDescription, |
|
||||||
extractIssueTitle, |
|
||||||
} from '$lib/components/events/content/utils' |
|
||||||
import { selectRepoFromCollection } from '$lib/components/repo/utils' |
|
||||||
import { |
|
||||||
summary_defaults, |
|
||||||
type IssueSummaries, |
|
||||||
} from '$lib/components/issues/type' |
|
||||||
|
|
||||||
export const issue_summaries: Writable<IssueSummaries> = writable({ |
|
||||||
repo_a: '', |
|
||||||
summaries: [], |
|
||||||
loading: false, |
|
||||||
}) |
|
||||||
|
|
||||||
let selected_repo_a: string | undefined = '' |
|
||||||
|
|
||||||
let sub: NDKSubscription |
|
||||||
|
|
||||||
export const ensureIssueSummaries = async (repo_a: string | undefined) => { |
|
||||||
if (selected_repo_a == repo_a) return |
|
||||||
issue_summaries.set({ |
|
||||||
repo_a, |
|
||||||
summaries: [], |
|
||||||
loading: repo_a !== '', |
|
||||||
}) |
|
||||||
|
|
||||||
if (sub) sub.stop() |
|
||||||
if (sub_statuses) sub_statuses.stop() |
|
||||||
|
|
||||||
selected_repo_a = repo_a |
|
||||||
|
|
||||||
setTimeout(() => { |
|
||||||
issue_summaries.update((summaries) => { |
|
||||||
return { |
|
||||||
...summaries, |
|
||||||
loading: false, |
|
||||||
} |
|
||||||
}) |
|
||||||
}, 6000) |
|
||||||
|
|
||||||
let relays_to_use = [...base_relays] |
|
||||||
let filter: NDKFilter = { |
|
||||||
kinds: [issue_kind], |
|
||||||
limit: 100, |
|
||||||
} |
|
||||||
|
|
||||||
if (repo_a) { |
|
||||||
const repo_collection = await awaitSelectedRepoCollection(repo_a) |
|
||||||
|
|
||||||
const repo = selectRepoFromCollection(repo_collection) |
|
||||||
if (!repo) { |
|
||||||
// TODO: display error info bar
|
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
relays_to_use = |
|
||||||
repo.relays.length > 3 |
|
||||||
? repo.relays |
|
||||||
: [...base_relays].concat(repo.relays) |
|
||||||
|
|
||||||
filter = { |
|
||||||
kinds: [issue_kind], |
|
||||||
'#a': repo.maintainers.map((m) => `${repo_kind}:${m}:${repo.identifier}`), |
|
||||||
limit: 100, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
sub = ndk.subscribe( |
|
||||||
filter, |
|
||||||
{ |
|
||||||
closeOnEose: false, |
|
||||||
}, |
|
||||||
NDKRelaySet.fromRelayUrls(relays_to_use, ndk) |
|
||||||
) |
|
||||||
|
|
||||||
sub.on('event', (event: NDKEvent) => { |
|
||||||
try { |
|
||||||
if (event.kind == issue_kind) { |
|
||||||
if (!extractRepoIdentiferFromIssueEvent(event) && !repo_a) { |
|
||||||
// link to issue will not work as it requires an identifier
|
|
||||||
return |
|
||||||
} |
|
||||||
issue_summaries.update((issues) => { |
|
||||||
return { |
|
||||||
...issues, |
|
||||||
summaries: [ |
|
||||||
...issues.summaries, |
|
||||||
{ |
|
||||||
...summary_defaults, |
|
||||||
id: event.id, |
|
||||||
repo_a: |
|
||||||
extractRepoIdentiferFromIssueEvent(event) || repo_a || '', |
|
||||||
title: extractIssueTitle(event), |
|
||||||
descritpion: extractIssueDescription(event.content), |
|
||||||
created_at: event.created_at, |
|
||||||
comments: 0, |
|
||||||
author: event.pubkey, |
|
||||||
loading: false, |
|
||||||
}, |
|
||||||
], |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
} catch {} |
|
||||||
}) |
|
||||||
sub.on('eose', () => { |
|
||||||
issue_summaries.update((issues) => { |
|
||||||
getAndUpdateIssueStatus(issues, relays_to_use) |
|
||||||
return { |
|
||||||
...issues, |
|
||||||
loading: false, |
|
||||||
} |
|
||||||
}) |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
let sub_statuses: NDKSubscription |
|
||||||
|
|
||||||
function getAndUpdateIssueStatus( |
|
||||||
issues: IssueSummaries, |
|
||||||
relays: string[] |
|
||||||
): void { |
|
||||||
if (sub_statuses) sub_statuses.stop() |
|
||||||
sub_statuses = ndk.subscribe( |
|
||||||
{ |
|
||||||
kinds: proposal_status_kinds, |
|
||||||
'#e': issues.summaries.map((issue) => issue.id), |
|
||||||
}, |
|
||||||
{ |
|
||||||
closeOnEose: false, |
|
||||||
}, |
|
||||||
NDKRelaySet.fromRelayUrls(relays, ndk) |
|
||||||
) |
|
||||||
sub_statuses.on('event', (event: NDKEvent) => { |
|
||||||
const tagged_issue_event = event.tagValue('e') |
|
||||||
if ( |
|
||||||
event.kind && |
|
||||||
proposal_status_kinds.includes(event.kind) && |
|
||||||
tagged_issue_event && |
|
||||||
event.created_at |
|
||||||
) { |
|
||||||
issue_summaries.update((issues) => { |
|
||||||
return { |
|
||||||
...issues, |
|
||||||
summaries: issues.summaries.map((o) => { |
|
||||||
if ( |
|
||||||
o.id === tagged_issue_event && |
|
||||||
event.created_at && |
|
||||||
o.status_date < event.created_at |
|
||||||
) { |
|
||||||
return { |
|
||||||
...o, |
|
||||||
status: event.kind as number, |
|
||||||
status_date: event.created_at, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return o |
|
||||||
}), |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
sub_statuses.on('eose', () => { |
|
||||||
issue_summaries.update((issues) => { |
|
||||||
return { |
|
||||||
...issues, |
|
||||||
summaries: issues.summaries.map((o) => ({ |
|
||||||
...o, |
|
||||||
status: o.status || proposal_status_open, |
|
||||||
})), |
|
||||||
} |
|
||||||
}) |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
export const extractRepoIdentiferFromIssueEvent = ( |
|
||||||
event: NDKEvent |
|
||||||
): string | undefined => { |
|
||||||
const value = event.tagValue('a') |
|
||||||
if (!value) return undefined |
|
||||||
const split = value.split(':') |
|
||||||
if (split.length < 3) return undefined |
|
||||||
return value |
|
||||||
} |
|
||||||
@ -1,210 +0,0 @@ |
|||||||
import { NDKRelaySet, type NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk' |
|
||||||
import { writable, type Writable } from 'svelte/store' |
|
||||||
import { base_relays, ndk } from './ndk' |
|
||||||
import { |
|
||||||
type ProposalFull, |
|
||||||
full_defaults, |
|
||||||
} from '$lib/components/proposals/type' |
|
||||||
import { proposal_status_kinds, proposal_status_open } from '$lib/kinds' |
|
||||||
import { awaitSelectedRepoCollection } from './repo' |
|
||||||
import { extractPatchMessage } from '$lib/components/events/content/utils' |
|
||||||
import { selectRepoFromCollection } from '$lib/components/repo/utils' |
|
||||||
import { ignore_kinds } from './utils' |
|
||||||
|
|
||||||
export const selected_proposal_full: Writable<ProposalFull> = writable({ |
|
||||||
...full_defaults, |
|
||||||
}) |
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
let selected_proposal_repo_a: string = '' |
|
||||||
let selected_proposal_id: string = '' |
|
||||||
|
|
||||||
export const selected_proposal_replies: Writable<NDKEvent[]> = writable([]) |
|
||||||
|
|
||||||
let selected_proposal_status_date = 0 |
|
||||||
|
|
||||||
let sub: NDKSubscription |
|
||||||
|
|
||||||
let sub_replies: NDKSubscription |
|
||||||
|
|
||||||
const sub_replies_to_replies: NDKSubscription[] = [] |
|
||||||
|
|
||||||
export const ensureProposalFull = ( |
|
||||||
repo_a: string, |
|
||||||
proposal_id_or_event: string | NDKEvent |
|
||||||
) => { |
|
||||||
const proposal_id = |
|
||||||
typeof proposal_id_or_event === 'string' |
|
||||||
? proposal_id_or_event |
|
||||||
: proposal_id_or_event.id |
|
||||||
if (selected_proposal_id == proposal_id) return |
|
||||||
if (proposal_id == '') { |
|
||||||
selected_proposal_full.set({ ...full_defaults }) |
|
||||||
selected_proposal_replies.set([]) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
if (sub) sub.stop() |
|
||||||
if (sub_replies) sub_replies.stop() |
|
||||||
sub_replies_to_replies.forEach((sub) => sub.stop()) |
|
||||||
|
|
||||||
selected_proposal_repo_a = repo_a |
|
||||||
selected_proposal_id = proposal_id |
|
||||||
selected_proposal_status_date = 0 |
|
||||||
selected_proposal_replies.set([]) |
|
||||||
|
|
||||||
selected_proposal_full.set({ |
|
||||||
...full_defaults, |
|
||||||
summary: { |
|
||||||
...full_defaults.summary, |
|
||||||
id: proposal_id, |
|
||||||
repo_a, |
|
||||||
loading: true, |
|
||||||
}, |
|
||||||
loading: true, |
|
||||||
}) |
|
||||||
|
|
||||||
new Promise(async (r) => { |
|
||||||
const repo_collection = await awaitSelectedRepoCollection(repo_a) |
|
||||||
const repo = selectRepoFromCollection(repo_collection) |
|
||||||
const relays_to_use = |
|
||||||
repo && repo.relays.length > 3 |
|
||||||
? repo.relays |
|
||||||
: [...base_relays].concat(repo ? repo.relays : []) |
|
||||||
|
|
||||||
const setEvent = (event: NDKEvent) => { |
|
||||||
try { |
|
||||||
selected_proposal_full.update((full) => { |
|
||||||
return { |
|
||||||
...full, |
|
||||||
proposal_event: event, |
|
||||||
summary: { |
|
||||||
...full.summary, |
|
||||||
title: ( |
|
||||||
event.tagValue('name') || |
|
||||||
event.tagValue('description') || |
|
||||||
extractPatchMessage(event.content) || |
|
||||||
'' |
|
||||||
).split('\n')[0], |
|
||||||
descritpion: event.tagValue('description') || '', |
|
||||||
created_at: event.created_at, |
|
||||||
comments: 0, |
|
||||||
author: event.pubkey, |
|
||||||
loading: false, |
|
||||||
}, |
|
||||||
} |
|
||||||
}) |
|
||||||
} catch {} |
|
||||||
} |
|
||||||
|
|
||||||
if (typeof proposal_id_or_event !== 'string') { |
|
||||||
setEvent(proposal_id_or_event) |
|
||||||
} else { |
|
||||||
sub = ndk.subscribe( |
|
||||||
{ |
|
||||||
ids: [proposal_id], |
|
||||||
limit: 100, |
|
||||||
}, |
|
||||||
{ |
|
||||||
closeOnEose: false, |
|
||||||
}, |
|
||||||
NDKRelaySet.fromRelayUrls(relays_to_use, ndk) |
|
||||||
) |
|
||||||
|
|
||||||
sub.on('event', (event: NDKEvent) => { |
|
||||||
if (event.id == proposal_id) setEvent(event) |
|
||||||
}) |
|
||||||
|
|
||||||
sub.on('eose', () => { |
|
||||||
selected_proposal_full.update((full) => { |
|
||||||
const updated = { |
|
||||||
...full, |
|
||||||
summary: { |
|
||||||
...full.summary, |
|
||||||
loading: false, |
|
||||||
}, |
|
||||||
} |
|
||||||
if (full.loading === false) { |
|
||||||
r({ ...updated }) |
|
||||||
} |
|
||||||
return updated |
|
||||||
}) |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
sub_replies = ndk.subscribe( |
|
||||||
{ |
|
||||||
'#e': [proposal_id], |
|
||||||
}, |
|
||||||
{ |
|
||||||
closeOnEose: false, |
|
||||||
}, |
|
||||||
NDKRelaySet.fromRelayUrls(relays_to_use, ndk) |
|
||||||
) |
|
||||||
|
|
||||||
const process_replies = (event: NDKEvent) => { |
|
||||||
if (event.kind && ignore_kinds.includes(event.kind)) return false |
|
||||||
if ( |
|
||||||
event.kind && |
|
||||||
proposal_status_kinds.includes(event.kind) && |
|
||||||
event.created_at && |
|
||||||
selected_proposal_status_date < event.created_at |
|
||||||
) { |
|
||||||
selected_proposal_status_date = event.created_at |
|
||||||
selected_proposal_full.update((full) => { |
|
||||||
return { |
|
||||||
...full, |
|
||||||
summary: { |
|
||||||
...full.summary, |
|
||||||
status: event.kind, |
|
||||||
// this wont be 0 as we are ensuring it is not undefined above
|
|
||||||
status_date: event.created_at || 0, |
|
||||||
}, |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
selected_proposal_replies.update((replies) => { |
|
||||||
if (!replies.some((e) => e.id === event.id)) { |
|
||||||
const sub_replies_to_reply = ndk.subscribe( |
|
||||||
{ |
|
||||||
'#e': [event.id], |
|
||||||
}, |
|
||||||
{ |
|
||||||
groupable: true, |
|
||||||
groupableDelay: 300, |
|
||||||
closeOnEose: false, |
|
||||||
}, |
|
||||||
NDKRelaySet.fromRelayUrls(relays_to_use, ndk) |
|
||||||
) |
|
||||||
sub_replies_to_reply.on('event', (event: NDKEvent) => { |
|
||||||
process_replies(event) |
|
||||||
}) |
|
||||||
sub_replies_to_replies.push(sub_replies_to_reply) |
|
||||||
return [...replies, event] |
|
||||||
} |
|
||||||
return [...replies] |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
sub_replies.on('event', (event: NDKEvent) => { |
|
||||||
process_replies(event) |
|
||||||
}) |
|
||||||
|
|
||||||
sub_replies.on('eose', () => { |
|
||||||
selected_proposal_full.update((full) => { |
|
||||||
const updated = { |
|
||||||
...full, |
|
||||||
summary: { |
|
||||||
...full.summary, |
|
||||||
status: full.summary.status || proposal_status_open, |
|
||||||
}, |
|
||||||
loading: false, |
|
||||||
} |
|
||||||
if (full.summary.loading === false) { |
|
||||||
r({ ...updated }) |
|
||||||
} |
|
||||||
return updated |
|
||||||
}) |
|
||||||
}) |
|
||||||
}) |
|
||||||
} |
|
||||||
@ -1,251 +0,0 @@ |
|||||||
import { |
|
||||||
NDKRelaySet, |
|
||||||
type NDKEvent, |
|
||||||
NDKSubscription, |
|
||||||
type NDKFilter, |
|
||||||
} from '@nostr-dev-kit/ndk' |
|
||||||
import { writable, type Writable } from 'svelte/store' |
|
||||||
import { base_relays, ndk } from './ndk' |
|
||||||
import { summary_defaults } from '$lib/components/proposals/type' |
|
||||||
import type { ProposalSummaries } from '$lib/components/proposals/type' |
|
||||||
import { awaitSelectedRepoCollection } from './repo' |
|
||||||
import { |
|
||||||
patch_kind, |
|
||||||
proposal_status_kinds, |
|
||||||
proposal_status_open, |
|
||||||
repo_kind, |
|
||||||
} from '$lib/kinds' |
|
||||||
import { extractPatchMessage } from '$lib/components/events/content/utils' |
|
||||||
import { selectRepoFromCollection } from '$lib/components/repo/utils' |
|
||||||
import { returnRepoCollection } from './repos' |
|
||||||
|
|
||||||
export const proposal_summaries: Writable<ProposalSummaries> = writable({ |
|
||||||
repo_a: '', |
|
||||||
summaries: [], |
|
||||||
loading: false, |
|
||||||
}) |
|
||||||
|
|
||||||
let selected_a: string | undefined = '' |
|
||||||
|
|
||||||
let sub: NDKSubscription |
|
||||||
|
|
||||||
export const ensureProposalSummaries = async (repo_a: string | undefined) => { |
|
||||||
if (selected_a == repo_a) return |
|
||||||
proposal_summaries.set({ |
|
||||||
repo_a, |
|
||||||
summaries: [], |
|
||||||
loading: repo_a !== '', |
|
||||||
}) |
|
||||||
|
|
||||||
if (sub) sub.stop() |
|
||||||
if (sub_statuses) sub_statuses.stop() |
|
||||||
|
|
||||||
selected_a = repo_a |
|
||||||
|
|
||||||
setTimeout(() => { |
|
||||||
proposal_summaries.update((summaries) => { |
|
||||||
return { |
|
||||||
...summaries, |
|
||||||
loading: false, |
|
||||||
} |
|
||||||
}) |
|
||||||
}, 6000) |
|
||||||
|
|
||||||
let relays_to_use = [...base_relays] |
|
||||||
let filter: NDKFilter = { |
|
||||||
kinds: [patch_kind], |
|
||||||
limit: 100, |
|
||||||
} |
|
||||||
|
|
||||||
if (repo_a) { |
|
||||||
const repo_collection = await awaitSelectedRepoCollection(repo_a) |
|
||||||
|
|
||||||
const repo = selectRepoFromCollection(repo_collection) |
|
||||||
if (!repo) { |
|
||||||
// TODO: display error info bar
|
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
relays_to_use = |
|
||||||
repo.relays.length > 3 |
|
||||||
? repo.relays |
|
||||||
: [...base_relays].concat(repo.relays) |
|
||||||
|
|
||||||
const without_root_tag = !repo.unique_commit |
|
||||||
|
|
||||||
if (without_root_tag) { |
|
||||||
filter = { |
|
||||||
kinds: [patch_kind], |
|
||||||
'#a': repo.maintainers.map( |
|
||||||
(m) => `${repo_kind}:${m}:${repo.identifier}` |
|
||||||
), |
|
||||||
limit: 100, |
|
||||||
} |
|
||||||
} else { |
|
||||||
filter = { |
|
||||||
kinds: [patch_kind], |
|
||||||
'#a': repo.maintainers.map( |
|
||||||
(m) => `${repo_kind}:${m}:${repo.identifier}` |
|
||||||
), |
|
||||||
'#t': ['root'], |
|
||||||
limit: 100, |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
sub = ndk.subscribe( |
|
||||||
filter, |
|
||||||
{ |
|
||||||
closeOnEose: false, |
|
||||||
}, |
|
||||||
NDKRelaySet.fromRelayUrls(relays_to_use, ndk) |
|
||||||
) |
|
||||||
|
|
||||||
sub.on('event', async (event: NDKEvent) => { |
|
||||||
try { |
|
||||||
if ( |
|
||||||
event.kind == patch_kind && |
|
||||||
event.content.length > 0 && |
|
||||||
!event.tags.some((t) => t.length > 1 && t[1] === 'revision-root') |
|
||||||
) { |
|
||||||
if (!extractRepoAFromProposalEvent(event) && !repo_a) { |
|
||||||
// link to proposal will not work as it requires an identifier
|
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
proposal_summaries.update((proposals) => { |
|
||||||
return { |
|
||||||
...proposals, |
|
||||||
summaries: [ |
|
||||||
...proposals.summaries, |
|
||||||
{ |
|
||||||
...summary_defaults, |
|
||||||
id: event.id, |
|
||||||
repo_a: extractRepoAFromProposalEvent(event) || repo_a || '', |
|
||||||
title: ( |
|
||||||
event.tagValue('name') || |
|
||||||
event.tagValue('description') || |
|
||||||
extractPatchMessage(event.content) || |
|
||||||
'' |
|
||||||
).split('\n')[0], |
|
||||||
descritpion: event.tagValue('description') || '', |
|
||||||
created_at: event.created_at, |
|
||||||
comments: 0, |
|
||||||
author: event.pubkey, |
|
||||||
loading: false, |
|
||||||
}, |
|
||||||
], |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
// filter out non root proposals if repo event supports nip34+ features
|
|
||||||
if (repo_a && repo_a.length > 0) { |
|
||||||
const repo_collection = await returnRepoCollection(repo_a) |
|
||||||
if ( |
|
||||||
selected_a === repo_a && |
|
||||||
repo_collection.events[repo_collection.most_recent_index] |
|
||||||
.unique_commit |
|
||||||
) { |
|
||||||
proposal_summaries.update((proposals) => { |
|
||||||
return { |
|
||||||
...proposals, |
|
||||||
summaries: [ |
|
||||||
...proposals.summaries.filter( |
|
||||||
(summary) => |
|
||||||
(event.tags.some( |
|
||||||
(t) => t.length > 1 && t[1] === 'root' |
|
||||||
) && |
|
||||||
!event.tags.some( |
|
||||||
(t) => t.length > 1 && t[1] === 'revision-root' |
|
||||||
)) || |
|
||||||
event.id !== summary.id |
|
||||||
), |
|
||||||
], |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} catch {} |
|
||||||
}) |
|
||||||
sub.on('eose', () => { |
|
||||||
proposal_summaries.update((proposals) => { |
|
||||||
getAndUpdateProposalStatus(proposals, relays_to_use) |
|
||||||
return { |
|
||||||
...proposals, |
|
||||||
loading: false, |
|
||||||
} |
|
||||||
}) |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
let sub_statuses: NDKSubscription |
|
||||||
|
|
||||||
function getAndUpdateProposalStatus( |
|
||||||
proposals: ProposalSummaries, |
|
||||||
relays: string[] |
|
||||||
): void { |
|
||||||
if (sub_statuses) sub_statuses.stop() |
|
||||||
sub_statuses = ndk.subscribe( |
|
||||||
{ |
|
||||||
kinds: proposal_status_kinds, |
|
||||||
'#e': proposals.summaries.map((proposal) => proposal.id), |
|
||||||
}, |
|
||||||
{ |
|
||||||
closeOnEose: false, |
|
||||||
}, |
|
||||||
NDKRelaySet.fromRelayUrls(relays, ndk) |
|
||||||
) |
|
||||||
sub_statuses.on('event', (event: NDKEvent) => { |
|
||||||
const tagged_proposal_event = event.tagValue('e') |
|
||||||
if ( |
|
||||||
event.kind && |
|
||||||
proposal_status_kinds.includes(event.kind) && |
|
||||||
tagged_proposal_event && |
|
||||||
event.created_at |
|
||||||
) { |
|
||||||
proposal_summaries.update((proposals) => { |
|
||||||
return { |
|
||||||
...proposals, |
|
||||||
summaries: proposals.summaries.map((o) => { |
|
||||||
if ( |
|
||||||
o.id === tagged_proposal_event && |
|
||||||
event.created_at && |
|
||||||
o.status_date < event.created_at |
|
||||||
) { |
|
||||||
return { |
|
||||||
...o, |
|
||||||
status: event.kind as number, |
|
||||||
status_date: event.created_at, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return o |
|
||||||
}), |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
sub_statuses.on('eose', () => { |
|
||||||
proposal_summaries.update((proposals) => { |
|
||||||
return { |
|
||||||
...proposals, |
|
||||||
summaries: proposals.summaries.map((o) => ({ |
|
||||||
...o, |
|
||||||
status: o.status || proposal_status_open, |
|
||||||
})), |
|
||||||
} |
|
||||||
}) |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
export const extractRepoAFromProposalEvent = ( |
|
||||||
event: NDKEvent |
|
||||||
): string | undefined => { |
|
||||||
const value = event.tagValue('a') |
|
||||||
if (!value) return undefined |
|
||||||
const split = value.split(':') |
|
||||||
if (split.length < 3) return undefined |
|
||||||
return value |
|
||||||
} |
|
||||||
@ -1,56 +0,0 @@ |
|||||||
import type { RepoDIdentiferCollection } from '$lib/components/repo/type' |
|
||||||
import { writable, type Writable } from 'svelte/store' |
|
||||||
import { ensureRepo, eventToRepoEvent } from './repos' |
|
||||||
import { base_relays, ndk } from './ndk' |
|
||||||
import { repo_kind } from '$lib/kinds' |
|
||||||
import { NDKEvent, NDKRelaySet } from '@nostr-dev-kit/ndk' |
|
||||||
|
|
||||||
export const repos_identifer: { |
|
||||||
[d: string]: Writable<RepoDIdentiferCollection> |
|
||||||
} = {} |
|
||||||
|
|
||||||
export const ensureIdentifierRepoCollection = ( |
|
||||||
identifier: string |
|
||||||
): Writable<RepoDIdentiferCollection> => { |
|
||||||
if (!Object.keys(repos_identifer).includes(identifier)) { |
|
||||||
repos_identifer[identifier] = writable({ |
|
||||||
d: '', |
|
||||||
events: [], |
|
||||||
loading: true, |
|
||||||
}) |
|
||||||
const sub = ndk.subscribe( |
|
||||||
{ kinds: [repo_kind], '#d': [identifier] }, |
|
||||||
{ closeOnEose: true }, |
|
||||||
NDKRelaySet.fromRelayUrls(base_relays, ndk) |
|
||||||
) |
|
||||||
sub.on('event', (event: NDKEvent) => { |
|
||||||
const repo_event = eventToRepoEvent(event) |
|
||||||
if (repo_event && repo_event.identifier === identifier) { |
|
||||||
ensureRepo(event).subscribe((repo_event) => { |
|
||||||
repos_identifer[identifier].update((collection) => { |
|
||||||
let events = collection.events |
|
||||||
let exists = false |
|
||||||
events.map((e) => { |
|
||||||
if (e.author === repo_event.author) { |
|
||||||
exists = true |
|
||||||
return repo_event |
|
||||||
} else return e |
|
||||||
}) |
|
||||||
if (!exists) events = [...events, repo_event] |
|
||||||
return { |
|
||||||
...collection, |
|
||||||
events, |
|
||||||
} |
|
||||||
}) |
|
||||||
}) |
|
||||||
} |
|
||||||
}) |
|
||||||
sub.on('eose', () => { |
|
||||||
repos_identifer[identifier].update((collection) => ({ |
|
||||||
...collection, |
|
||||||
loading: false, |
|
||||||
})) |
|
||||||
}) |
|
||||||
} |
|
||||||
return repos_identifer[identifier] |
|
||||||
} |
|
||||||
@ -1,78 +0,0 @@ |
|||||||
import type { SelectedPubkeyRepoCollections } from '$lib/components/repo/type' |
|
||||||
import { get, writable, type Unsubscriber, type Writable } from 'svelte/store' |
|
||||||
import { ensureRepoCollection, eventToRepoEvent } from './repos' |
|
||||||
import { base_relays, ndk } from './ndk' |
|
||||||
import { repo_kind } from '$lib/kinds' |
|
||||||
import { NDKEvent, NDKRelaySet } from '@nostr-dev-kit/ndk' |
|
||||||
import { extractAReference } from '$lib/components/repo/utils' |
|
||||||
|
|
||||||
export const selected_npub_repo_collections: Writable<SelectedPubkeyRepoCollections> = |
|
||||||
writable({ |
|
||||||
pubkey: '', |
|
||||||
collections: [], |
|
||||||
}) |
|
||||||
|
|
||||||
const unsubscribers: Unsubscriber[] = [] |
|
||||||
|
|
||||||
export const ensureSelectedPubkeyRepoCollection = ( |
|
||||||
pubkey: string |
|
||||||
): Writable<SelectedPubkeyRepoCollections> => { |
|
||||||
const collections = get(selected_npub_repo_collections) |
|
||||||
if (collections.pubkey === pubkey) return selected_npub_repo_collections |
|
||||||
// TODO call unsubscribers
|
|
||||||
selected_npub_repo_collections.set({ |
|
||||||
pubkey, |
|
||||||
collections: [], |
|
||||||
}) |
|
||||||
|
|
||||||
const sub = ndk.subscribe( |
|
||||||
{ kinds: [repo_kind], authors: [pubkey] }, |
|
||||||
{ closeOnEose: true }, |
|
||||||
NDKRelaySet.fromRelayUrls(base_relays, ndk) |
|
||||||
) |
|
||||||
const identifiers: string[] = [] |
|
||||||
sub.on('event', (event: NDKEvent) => { |
|
||||||
const repo_event = eventToRepoEvent(event) |
|
||||||
if ( |
|
||||||
repo_event && |
|
||||||
repo_event.author === pubkey && |
|
||||||
!identifiers.includes(repo_event.identifier) |
|
||||||
) |
|
||||||
identifiers.push(repo_event.identifier) |
|
||||||
}) |
|
||||||
sub.on('eose', () => { |
|
||||||
identifiers.forEach((identifier) => { |
|
||||||
unsubscribers.push( |
|
||||||
ensureRepoCollection(`${repo_kind}:${pubkey}:${identifier}`).subscribe( |
|
||||||
(c) => { |
|
||||||
if (!c.maintainers.includes(pubkey)) return |
|
||||||
|
|
||||||
selected_npub_repo_collections.update((selected_collections) => { |
|
||||||
if (selected_collections.pubkey !== pubkey) |
|
||||||
return { ...selected_collections } |
|
||||||
let collection_in_selected_collections = false |
|
||||||
const collections = selected_collections.collections.map( |
|
||||||
(old_c) => { |
|
||||||
const ref = extractAReference(old_c.selected_a) |
|
||||||
if (ref && ref.identifier === identifier) { |
|
||||||
collection_in_selected_collections = true |
|
||||||
return { |
|
||||||
...c, |
|
||||||
} |
|
||||||
} |
|
||||||
return { ...old_c } |
|
||||||
} |
|
||||||
) |
|
||||||
if (!collection_in_selected_collections) collections.push(c) |
|
||||||
return { |
|
||||||
...selected_collections, |
|
||||||
collections, |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
) |
|
||||||
) |
|
||||||
}) |
|
||||||
}) |
|
||||||
return selected_npub_repo_collections |
|
||||||
} |
|
||||||
@ -1,55 +0,0 @@ |
|||||||
import type { RepoRecentCollection } from '$lib/components/repo/type' |
|
||||||
import { writable, type Writable } from 'svelte/store' |
|
||||||
import { ensureRepo, eventToRepoEvent } from './repos' |
|
||||||
import { base_relays, ndk } from './ndk' |
|
||||||
import { repo_kind } from '$lib/kinds' |
|
||||||
import { NDKEvent, NDKRelaySet } from '@nostr-dev-kit/ndk' |
|
||||||
|
|
||||||
export const recent_repos: Writable<RepoRecentCollection> = writable({ |
|
||||||
events: [], |
|
||||||
loading: true, |
|
||||||
}) |
|
||||||
|
|
||||||
let started = false |
|
||||||
|
|
||||||
export const ensureRecentRepos = (): Writable<RepoRecentCollection> => { |
|
||||||
if (started) return recent_repos |
|
||||||
started = true |
|
||||||
const sub = ndk.subscribe( |
|
||||||
{ kinds: [repo_kind] }, |
|
||||||
{ closeOnEose: true }, |
|
||||||
NDKRelaySet.fromRelayUrls(base_relays, ndk) |
|
||||||
) |
|
||||||
sub.on('event', (event: NDKEvent) => { |
|
||||||
const repo_event = eventToRepoEvent(event) |
|
||||||
if (repo_event) { |
|
||||||
ensureRepo(event).subscribe((repo_event) => { |
|
||||||
recent_repos.update((collection) => { |
|
||||||
let events = collection.events |
|
||||||
let exists = false |
|
||||||
events.map((e) => { |
|
||||||
if ( |
|
||||||
e.author === repo_event.author && |
|
||||||
e.identifier === repo_event.identifier |
|
||||||
) { |
|
||||||
exists = true |
|
||||||
return repo_event |
|
||||||
} else return e |
|
||||||
}) |
|
||||||
if (!exists) events = [...events, repo_event] |
|
||||||
return { |
|
||||||
...collection, |
|
||||||
events, |
|
||||||
} |
|
||||||
}) |
|
||||||
}) |
|
||||||
} |
|
||||||
}) |
|
||||||
sub.on('eose', () => { |
|
||||||
recent_repos.update((collection) => ({ |
|
||||||
...collection, |
|
||||||
loading: false, |
|
||||||
})) |
|
||||||
}) |
|
||||||
return recent_repos |
|
||||||
} |
|
||||||
@ -1,21 +0,0 @@ |
|||||||
import NDKSvelte from '@nostr-dev-kit/ndk-svelte' |
|
||||||
|
|
||||||
// export let base_relays = import.meta.env.DEV
|
|
||||||
// ? ["ws://localhost:8080"]
|
|
||||||
// : [
|
|
||||||
|
|
||||||
export const base_relays = [ |
|
||||||
'wss://relay.damus.io', |
|
||||||
'wss://nos.lol', |
|
||||||
'wss://relay.nostr.band', |
|
||||||
'wss://purplerelay.com', // reliability untested
|
|
||||||
// 'wss://relayable.org', // free but not so reliable
|
|
||||||
] |
|
||||||
|
|
||||||
// TODO: fallback_relays for if profile cannot be found
|
|
||||||
|
|
||||||
export const ndk = new NDKSvelte({ |
|
||||||
explicitRelayUrls: [...base_relays], |
|
||||||
}) |
|
||||||
|
|
||||||
ndk.connect(5000) |
|
||||||
@ -1,128 +0,0 @@ |
|||||||
import { writable, type Unsubscriber, type Writable } from 'svelte/store' |
|
||||||
import type { |
|
||||||
RepoCollection, |
|
||||||
RepoEvent, |
|
||||||
RepoReadme, |
|
||||||
} from '$lib/components/repo/type' |
|
||||||
import { |
|
||||||
collection_defaults, |
|
||||||
event_defaults, |
|
||||||
readme_defaults, |
|
||||||
} from '$lib/components/repo/type' |
|
||||||
import { ensureRepoCollection } from './repos' |
|
||||||
import { |
|
||||||
cloneArrayToReadMeUrls, |
|
||||||
selectRepoFromCollection, |
|
||||||
} from '$lib/components/repo/utils' |
|
||||||
import { get } from 'svelte/store' |
|
||||||
|
|
||||||
export const selected_repo_collection: Writable<RepoCollection> = writable({ |
|
||||||
...collection_defaults, |
|
||||||
}) |
|
||||||
|
|
||||||
export const selected_repo_event: Writable<RepoEvent> = writable({ |
|
||||||
...event_defaults, |
|
||||||
}) |
|
||||||
|
|
||||||
selected_repo_collection.subscribe((collection) => { |
|
||||||
const selected_from_collection = selectRepoFromCollection(collection) |
|
||||||
if (selected_from_collection) |
|
||||||
selected_repo_event.set({ ...selected_from_collection }) |
|
||||||
}) |
|
||||||
|
|
||||||
let selected_repo_a: string = '' |
|
||||||
|
|
||||||
let selected_unsubscriber: Unsubscriber |
|
||||||
|
|
||||||
export const ensureSelectedRepoCollection = ( |
|
||||||
a: string, |
|
||||||
naddr_relays: string[] | undefined = undefined |
|
||||||
): Writable<RepoCollection> => { |
|
||||||
if (selected_repo_a !== a) { |
|
||||||
let loading = true |
|
||||||
selected_repo_a = a |
|
||||||
if (selected_unsubscriber) selected_unsubscriber() |
|
||||||
selected_unsubscriber = ensureRepoCollection(a, naddr_relays).subscribe( |
|
||||||
(repo_collection) => { |
|
||||||
selected_repo_collection.set({ ...repo_collection }) |
|
||||||
if (loading && !repo_collection.loading) { |
|
||||||
loading = false |
|
||||||
const repo_event = selectRepoFromCollection(repo_collection) |
|
||||||
if (repo_event) |
|
||||||
ensureRepoReadme(repo_event.clone, repo_collection.selected_a) |
|
||||||
} |
|
||||||
} |
|
||||||
) |
|
||||||
} |
|
||||||
return selected_repo_collection |
|
||||||
} |
|
||||||
|
|
||||||
export const awaitSelectedRepoCollection = async ( |
|
||||||
a: string |
|
||||||
): Promise<RepoCollection> => { |
|
||||||
return new Promise((r) => { |
|
||||||
const unsubscriber = ensureSelectedRepoCollection(a).subscribe( |
|
||||||
(repo_collection) => { |
|
||||||
if (selected_repo_a === a && !repo_collection.loading) { |
|
||||||
setTimeout(() => { |
|
||||||
if (unsubscriber) unsubscriber() |
|
||||||
}, 5) |
|
||||||
r({ ...repo_collection }) |
|
||||||
} |
|
||||||
} |
|
||||||
) |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
export const selected_repo_readme: Writable<RepoReadme> = writable({ |
|
||||||
...readme_defaults, |
|
||||||
}) |
|
||||||
|
|
||||||
const ensureRepoReadme = async (clone: string[], a: string): Promise<void> => { |
|
||||||
selected_repo_readme.set({ ...readme_defaults }) |
|
||||||
|
|
||||||
/** update writable unless selected readme has changed */ |
|
||||||
const update = (md: string | undefined = undefined): void => { |
|
||||||
const latest_collection = get(selected_repo_collection) |
|
||||||
if ( |
|
||||||
[latest_collection.selected_a, latest_collection.selected_a].includes(a) |
|
||||||
) { |
|
||||||
selected_repo_readme.set({ |
|
||||||
md: md || '', |
|
||||||
loading: false, |
|
||||||
failed: !md, |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
let text: string | undefined |
|
||||||
try { |
|
||||||
let readme_urls = cloneArrayToReadMeUrls(clone) |
|
||||||
// prioritise using github as it doesn't require a proxy
|
|
||||||
readme_urls = [ |
|
||||||
...readme_urls.filter((url) => url.includes('raw.githubusercontent.com')), |
|
||||||
...readme_urls.filter( |
|
||||||
(url) => !url.includes('raw.githubusercontent.com') |
|
||||||
), |
|
||||||
] |
|
||||||
for (let i = 0; i < readme_urls.length; i++) { |
|
||||||
try { |
|
||||||
const res = await fetch( |
|
||||||
readme_urls[i] |
|
||||||
// readme_urls[i].includes('raw.githubusercontent.com')
|
|
||||||
// ? readme_urls[i]
|
|
||||||
// : // use proxy as most servers produce a CORS error
|
|
||||||
// `/git_proxy/readme/${encodeURIComponent(readme_urls[i])}`
|
|
||||||
) |
|
||||||
if (res.ok) { |
|
||||||
text = await res.text() |
|
||||||
break |
|
||||||
} else { |
|
||||||
continue |
|
||||||
} |
|
||||||
} catch { |
|
||||||
continue |
|
||||||
} |
|
||||||
} |
|
||||||
} catch {} |
|
||||||
update(text) |
|
||||||
} |
|
||||||
@ -1,367 +0,0 @@ |
|||||||
import { |
|
||||||
event_defaults, |
|
||||||
collection_defaults, |
|
||||||
type RepoCollection, |
|
||||||
type RepoEvent, |
|
||||||
type RepoSummary, |
|
||||||
} from '$lib/components/repo/type' |
|
||||||
import { NDKRelaySet, NDKEvent } from '@nostr-dev-kit/ndk' |
|
||||||
import { get, writable, type Writable } from 'svelte/store' |
|
||||||
import { base_relays, ndk } from './ndk' |
|
||||||
import { repo_kind } from '$lib/kinds' |
|
||||||
import { |
|
||||||
aToNaddr, |
|
||||||
extractAReference, |
|
||||||
selectRepoFromCollection, |
|
||||||
} from '$lib/components/repo/utils' |
|
||||||
import { nip19 } from 'nostr-tools' |
|
||||||
|
|
||||||
export const repos: { |
|
||||||
[a: string]: Writable<RepoEvent> |
|
||||||
} = {} |
|
||||||
|
|
||||||
export const repo_collections: { |
|
||||||
[a: string]: Writable<RepoCollection> |
|
||||||
} = {} |
|
||||||
|
|
||||||
export const ensureRepo = ( |
|
||||||
a: string | NDKEvent, |
|
||||||
naddr_relays: string[] | undefined = undefined |
|
||||||
): Writable<RepoEvent> => { |
|
||||||
if (typeof a !== 'string') { |
|
||||||
const repo_event = eventToRepoEvent(a) |
|
||||||
if (repo_event) { |
|
||||||
const a = repoEventToARef(repo_event) |
|
||||||
repos[a] = writable({ ...repo_event, loading: true }) |
|
||||||
fetchReferencedBy(repo_event) |
|
||||||
return repos[a] |
|
||||||
} |
|
||||||
return repos[''] |
|
||||||
} |
|
||||||
if (!repos[a]) { |
|
||||||
const base: RepoEvent = { |
|
||||||
...event_defaults, |
|
||||||
} |
|
||||||
|
|
||||||
const a_ref = extractAReference(a) |
|
||||||
|
|
||||||
if (!a_ref) return writable(base) |
|
||||||
|
|
||||||
const { pubkey, identifier } = a_ref |
|
||||||
|
|
||||||
repos[a] = writable({ |
|
||||||
...base, |
|
||||||
identifier, |
|
||||||
author: pubkey, |
|
||||||
naddr: aToNaddr(a_ref) || '', |
|
||||||
maintainers: [pubkey], |
|
||||||
}) |
|
||||||
|
|
||||||
const sub = ndk.subscribe( |
|
||||||
{ kinds: [repo_kind], '#d': [identifier], authors: [pubkey] }, |
|
||||||
{ |
|
||||||
groupable: true, |
|
||||||
// default 100
|
|
||||||
groupableDelay: 200, |
|
||||||
closeOnEose: false, |
|
||||||
}, |
|
||||||
NDKRelaySet.fromRelayUrls([...base_relays, ...(naddr_relays || [])], ndk) |
|
||||||
) |
|
||||||
sub.on('event', (event: NDKEvent) => { |
|
||||||
const repo_event = eventToRepoEvent(event) |
|
||||||
|
|
||||||
if (repo_event) { |
|
||||||
if ( |
|
||||||
identifier === repo_event.identifier && |
|
||||||
pubkey === repo_event.author |
|
||||||
) |
|
||||||
repos[a].update(() => { |
|
||||||
return { |
|
||||||
...repo_event, |
|
||||||
} |
|
||||||
}) |
|
||||||
fetchReferencedBy(repo_event) |
|
||||||
// TODO fetch stargazers
|
|
||||||
} |
|
||||||
}) |
|
||||||
sub.on('eose', () => { |
|
||||||
// still awaiting reference_by at this point
|
|
||||||
repos[a].update((repo_event) => { |
|
||||||
return { |
|
||||||
...repo_event, |
|
||||||
loading: false, |
|
||||||
} |
|
||||||
}) |
|
||||||
}) |
|
||||||
} |
|
||||||
setTimeout(() => { |
|
||||||
repos[a].update((repo_event) => { |
|
||||||
return { |
|
||||||
...repo_event, |
|
||||||
loading: false, |
|
||||||
} |
|
||||||
}) |
|
||||||
}, 5000) |
|
||||||
return repos[a] |
|
||||||
} |
|
||||||
|
|
||||||
export const returnRepo = async ( |
|
||||||
a: string, |
|
||||||
naddr_relays: string[] | undefined = undefined |
|
||||||
): Promise<RepoEvent> => { |
|
||||||
return new Promise((r) => { |
|
||||||
const unsubscriber = ensureRepo(a, naddr_relays).subscribe((c) => { |
|
||||||
if (!c.loading) { |
|
||||||
setTimeout(() => { |
|
||||||
if (unsubscriber) unsubscriber() |
|
||||||
}, 5) |
|
||||||
r(c) |
|
||||||
} |
|
||||||
}) |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
export const ensureRepoCollection = ( |
|
||||||
a: string, |
|
||||||
naddr_relays: string[] | undefined = undefined |
|
||||||
): Writable<RepoCollection> => { |
|
||||||
if (!repo_collections[a]) { |
|
||||||
const base: RepoCollection = { |
|
||||||
...collection_defaults, |
|
||||||
selected_a: a, |
|
||||||
} |
|
||||||
|
|
||||||
repo_collections[a] = writable(base) |
|
||||||
|
|
||||||
const a_ref = extractAReference(a) |
|
||||||
|
|
||||||
if (!a_ref) return repo_collections[a] |
|
||||||
|
|
||||||
const { pubkey, identifier } = a_ref |
|
||||||
|
|
||||||
returnRepo(a, naddr_relays).then(async (repo_event) => { |
|
||||||
if (get(repo_collections[a]).events.length > 0) return |
|
||||||
repo_collections[a].update((collection) => { |
|
||||||
return { |
|
||||||
...collection, |
|
||||||
events: [repo_event], |
|
||||||
maintainers: repo_event.maintainers, |
|
||||||
most_recent_index: 0, |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
const new_maintainers: string[] = [] |
|
||||||
|
|
||||||
const addMaintainers = async (m: string) => { |
|
||||||
const m_repo_event = await returnRepo(`${repo_kind}:${m}:${identifier}`) |
|
||||||
repo_collections[a].update((collection) => { |
|
||||||
m_repo_event.maintainers.forEach((m) => { |
|
||||||
if ( |
|
||||||
![pubkey, ...collection.maintainers, ...new_maintainers].includes( |
|
||||||
m |
|
||||||
) |
|
||||||
) |
|
||||||
new_maintainers.push(m) |
|
||||||
}) |
|
||||||
const events = [...collection.events, m_repo_event] |
|
||||||
const most_recent = events.sort( |
|
||||||
(a, b) => b.created_at - a.created_at |
|
||||||
)[0] |
|
||||||
return { |
|
||||||
...collection, |
|
||||||
events, |
|
||||||
most_recent_index: events.findIndex( |
|
||||||
(e) => e.author === most_recent.author |
|
||||||
), |
|
||||||
maintainers: [...collection.maintainers, ...new_maintainers], |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
// add maintainer events
|
|
||||||
await Promise.all( |
|
||||||
repo_event.maintainers |
|
||||||
.filter((m) => m !== pubkey) |
|
||||||
.map((m) => addMaintainers(m)) |
|
||||||
) |
|
||||||
|
|
||||||
// also add maintainers included in their maintainer events
|
|
||||||
while (new_maintainers.length > 0) { |
|
||||||
await Promise.all(new_maintainers.map((m) => addMaintainers(m))) |
|
||||||
} |
|
||||||
|
|
||||||
repo_collections[a].update((repo_collection) => { |
|
||||||
return { |
|
||||||
...repo_collection, |
|
||||||
loading: false, |
|
||||||
} |
|
||||||
}) |
|
||||||
}) |
|
||||||
} |
|
||||||
setTimeout(() => { |
|
||||||
repo_collections[a].update((repo_collection) => { |
|
||||||
return { |
|
||||||
...repo_collection, |
|
||||||
loading: false, |
|
||||||
} |
|
||||||
}) |
|
||||||
}, 5000) |
|
||||||
return repo_collections[a] |
|
||||||
} |
|
||||||
|
|
||||||
export const returnRepoCollection = async ( |
|
||||||
a: string |
|
||||||
): Promise<RepoCollection> => { |
|
||||||
return new Promise((r) => { |
|
||||||
const unsubscriber = ensureRepoCollection(a).subscribe((c) => { |
|
||||||
if (!c.loading) { |
|
||||||
setTimeout(() => { |
|
||||||
if (unsubscriber) unsubscriber() |
|
||||||
}, 5) |
|
||||||
r(c) |
|
||||||
} |
|
||||||
}) |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
const repoEventToARef = (repo_event: RepoEvent): string => |
|
||||||
`${repo_kind}:${repo_event.author}:${repo_event.identifier}` |
|
||||||
|
|
||||||
const fetchReferencedBy = (repo_event: RepoEvent) => { |
|
||||||
const relays_to_use = |
|
||||||
repo_event.relays.length < 3 |
|
||||||
? repo_event.relays |
|
||||||
: [...base_relays].concat(repo_event.relays) |
|
||||||
|
|
||||||
const ref_sub = ndk.subscribe( |
|
||||||
{ |
|
||||||
'#a': [repoEventToARef(repo_event)], |
|
||||||
limit: 10, |
|
||||||
}, |
|
||||||
{ |
|
||||||
groupable: true, |
|
||||||
// default 100
|
|
||||||
groupableDelay: 200, |
|
||||||
closeOnEose: true, |
|
||||||
}, |
|
||||||
NDKRelaySet.fromRelayUrls(relays_to_use, ndk) |
|
||||||
) |
|
||||||
ref_sub.on('event', (ref_event: NDKEvent) => { |
|
||||||
repos[repoEventToARef(repo_event)].update((repo_event) => { |
|
||||||
return { |
|
||||||
...repo_event, |
|
||||||
referenced_by: repo_event.referenced_by.includes(ref_event.id) |
|
||||||
? [...repo_event.referenced_by] |
|
||||||
: [...repo_event.referenced_by, ref_event.id], |
|
||||||
most_recent_reference_timestamp: |
|
||||||
ref_event.created_at && |
|
||||||
repo_event.most_recent_reference_timestamp < ref_event.created_at |
|
||||||
? ref_event.created_at |
|
||||||
: repo_event.most_recent_reference_timestamp, |
|
||||||
} |
|
||||||
}) |
|
||||||
}) |
|
||||||
|
|
||||||
ref_sub.on('eose', () => { |
|
||||||
repos[repoEventToARef(repo_event)].update((repo_event) => { |
|
||||||
return { |
|
||||||
...repo_event, |
|
||||||
// finished loading repo_event as we have all referenced_by events
|
|
||||||
loading: false, |
|
||||||
} |
|
||||||
}) |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
export const eventToRepoEvent = (event: NDKEvent): RepoEvent | undefined => { |
|
||||||
if (event.kind !== repo_kind) return undefined |
|
||||||
|
|
||||||
const maintainers = [event.pubkey] |
|
||||||
event.getMatchingTags('maintainers').forEach((t: string[]) => { |
|
||||||
t.forEach((v, i) => { |
|
||||||
if (i > 0 && v !== maintainers[0]) { |
|
||||||
try { |
|
||||||
nip19.npubEncode(v) // will throw if invalid hex pubkey
|
|
||||||
maintainers.push(v) |
|
||||||
} catch {} |
|
||||||
} |
|
||||||
}) |
|
||||||
}) |
|
||||||
const relays: string[] = [] |
|
||||||
event.getMatchingTags('relays').forEach((t: string[]) => { |
|
||||||
t.forEach((v, i) => { |
|
||||||
if (i > 0) { |
|
||||||
relays.push(v) |
|
||||||
} |
|
||||||
}) |
|
||||||
}) |
|
||||||
const web: string[] = [] |
|
||||||
event.getMatchingTags('web').forEach((t: string[]) => { |
|
||||||
t.forEach((v, i) => { |
|
||||||
if (i > 0) { |
|
||||||
web.push(v) |
|
||||||
} |
|
||||||
}) |
|
||||||
}) |
|
||||||
const clone: string[] = [] |
|
||||||
event.getMatchingTags('clone').forEach((t: string[]) => { |
|
||||||
t.forEach((v, i) => { |
|
||||||
if (i > 0) { |
|
||||||
clone.push(v) |
|
||||||
} |
|
||||||
}) |
|
||||||
}) |
|
||||||
return { |
|
||||||
event_id: event.id, |
|
||||||
naddr: event.encode(), |
|
||||||
author: event.pubkey, |
|
||||||
identifier: event.replaceableDTag(), |
|
||||||
unique_commit: event.tagValue('r') || undefined, |
|
||||||
name: event.tagValue('name') || '', |
|
||||||
description: event.tagValue('description') || '', |
|
||||||
clone, |
|
||||||
web, |
|
||||||
tags: event.getMatchingTags('t').map((t) => t[1]) || [], |
|
||||||
maintainers, |
|
||||||
relays, |
|
||||||
referenced_by: [], |
|
||||||
most_recent_reference_timestamp: event.created_at || 0, |
|
||||||
created_at: event.created_at || 0, |
|
||||||
loading: true, // loading until references fetched
|
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
export const repoCollectionToSummary = ( |
|
||||||
collection: RepoCollection |
|
||||||
): RepoSummary | undefined => { |
|
||||||
const selected = selectRepoFromCollection(collection) |
|
||||||
if (!selected) return undefined |
|
||||||
return { |
|
||||||
name: selected.name, |
|
||||||
identifier: selected.identifier, |
|
||||||
naddr: selected.naddr, |
|
||||||
unique_commit: selected.unique_commit, |
|
||||||
description: selected.description, |
|
||||||
maintainers: selected.maintainers, |
|
||||||
loading: collection.loading, |
|
||||||
created_at: selected.created_at, |
|
||||||
most_recent_reference_timestamp: Math.max.apply( |
|
||||||
0, |
|
||||||
collection.events.map((e) => e.most_recent_reference_timestamp) |
|
||||||
), |
|
||||||
} as RepoSummary |
|
||||||
} |
|
||||||
|
|
||||||
export const repoEventToSummary = (event: RepoEvent): RepoSummary => { |
|
||||||
return { |
|
||||||
name: event.name, |
|
||||||
identifier: event.identifier, |
|
||||||
naddr: event.naddr, |
|
||||||
unique_commit: event.unique_commit, |
|
||||||
description: event.description, |
|
||||||
maintainers: event.maintainers, |
|
||||||
loading: event.loading, |
|
||||||
created_at: event.created_at, |
|
||||||
most_recent_reference_timestamp: event.most_recent_reference_timestamp, |
|
||||||
} as RepoSummary |
|
||||||
} |
|
||||||
@ -1,184 +0,0 @@ |
|||||||
import { |
|
||||||
defaults as user_defaults, |
|
||||||
type UserObject, |
|
||||||
} from '$lib/components/users/type' |
|
||||||
import { |
|
||||||
getRelayListForUser, |
|
||||||
NDKNip07Signer, |
|
||||||
NDKRelayList, |
|
||||||
} from '@nostr-dev-kit/ndk' |
|
||||||
import { get, writable, type Unsubscriber, type Writable } from 'svelte/store' |
|
||||||
import { ndk } from './ndk' |
|
||||||
|
|
||||||
export const users: { [hexpubkey: string]: Writable<UserObject> } = {} |
|
||||||
|
|
||||||
const empty_user: Writable<UserObject> = writable({ |
|
||||||
loading: true, |
|
||||||
hexpubkey: '', |
|
||||||
npub: 'npub...', |
|
||||||
}) |
|
||||||
|
|
||||||
export const ensureUser = (hexpubkey: string): Writable<UserObject> => { |
|
||||||
if (hexpubkey === '') return empty_user |
|
||||||
if (!users[hexpubkey]) { |
|
||||||
const u = ndk.getUser({ hexpubkey }) |
|
||||||
|
|
||||||
const base: UserObject = { |
|
||||||
loading: false, |
|
||||||
hexpubkey, |
|
||||||
npub: u.npub, |
|
||||||
} |
|
||||||
|
|
||||||
users[hexpubkey] = writable(base) |
|
||||||
getUserRelays(hexpubkey) |
|
||||||
const getProfile = () => { |
|
||||||
u.fetchProfile({ |
|
||||||
closeOnEose: true, |
|
||||||
groupable: true, |
|
||||||
// default 100
|
|
||||||
groupableDelay: 200, |
|
||||||
}).then( |
|
||||||
(p) => { |
|
||||||
users[hexpubkey].update((u) => ({ |
|
||||||
...u, |
|
||||||
loading: false, |
|
||||||
profile: p === null ? undefined : p, |
|
||||||
})) |
|
||||||
}, |
|
||||||
() => { |
|
||||||
users[hexpubkey].update((u) => ({ |
|
||||||
...u, |
|
||||||
loading: false, |
|
||||||
})) |
|
||||||
} |
|
||||||
) |
|
||||||
} |
|
||||||
let attempts = 1 |
|
||||||
const tryAgainin3s = () => { |
|
||||||
setTimeout( |
|
||||||
() => { |
|
||||||
if (!get(users[hexpubkey]).profile) { |
|
||||||
getProfile() |
|
||||||
attempts++ |
|
||||||
if (attempts < 5) tryAgainin3s() |
|
||||||
} |
|
||||||
}, |
|
||||||
(attempts ^ 2) * 1000 |
|
||||||
) |
|
||||||
} |
|
||||||
getProfile() |
|
||||||
tryAgainin3s() |
|
||||||
} |
|
||||||
return users[hexpubkey] |
|
||||||
} |
|
||||||
|
|
||||||
export const returnUser = async (hexpubkey: string): Promise<UserObject> => { |
|
||||||
return new Promise((r) => { |
|
||||||
const unsubscriber = ensureUser(hexpubkey).subscribe((u) => { |
|
||||||
if (!u.loading) { |
|
||||||
setTimeout(() => { |
|
||||||
if (unsubscriber) unsubscriber() |
|
||||||
}, 5) |
|
||||||
r(u) |
|
||||||
} |
|
||||||
}) |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
// nip07_plugin is set in Navbar component
|
|
||||||
export const nip07_plugin: Writable<undefined | boolean> = writable(undefined) |
|
||||||
|
|
||||||
export const checkForNip07Plugin = () => { |
|
||||||
if (window.nostr) { |
|
||||||
nip07_plugin.set(true) |
|
||||||
if (localStorage.getItem('nip07pubkey')) login() |
|
||||||
} else { |
|
||||||
let timerId: NodeJS.Timeout | undefined = undefined |
|
||||||
const intervalId = setInterval(() => { |
|
||||||
if (window.nostr) { |
|
||||||
clearTimeout(timerId) |
|
||||||
clearInterval(intervalId) |
|
||||||
nip07_plugin.set(true) |
|
||||||
if (localStorage.getItem('nip07pubkey')) login() |
|
||||||
} |
|
||||||
}, 100) |
|
||||||
timerId = setTimeout(() => { |
|
||||||
clearInterval(intervalId) |
|
||||||
nip07_plugin.set(false) |
|
||||||
}, 5000) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
const signer = new NDKNip07Signer(2000) |
|
||||||
|
|
||||||
export const logged_in_user: Writable<undefined | UserObject> = |
|
||||||
writable(undefined) |
|
||||||
|
|
||||||
export const login = async (): Promise<void> => { |
|
||||||
return new Promise(async (res, rej) => { |
|
||||||
const user = get(logged_in_user) |
|
||||||
if (user) return res() |
|
||||||
if (get(nip07_plugin)) { |
|
||||||
try { |
|
||||||
const ndk_user = await signer.blockUntilReady() |
|
||||||
localStorage.setItem('nip07pubkey', ndk_user.pubkey) |
|
||||||
logged_in_user.set({ |
|
||||||
...user_defaults, |
|
||||||
hexpubkey: ndk_user.pubkey, |
|
||||||
}) |
|
||||||
ndk.signer = signer |
|
||||||
ensureUser(ndk_user.pubkey).subscribe((user) => { |
|
||||||
logged_in_user.set({ ...user }) |
|
||||||
}) |
|
||||||
return res() |
|
||||||
} catch (e) { |
|
||||||
alert(e) |
|
||||||
rej() |
|
||||||
} |
|
||||||
} else { |
|
||||||
rej() |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
export const logout = async (): Promise<void> => { |
|
||||||
logged_in_user.set(undefined) |
|
||||||
localStorage.removeItem('nip07pubkey') |
|
||||||
ndk.signer = undefined |
|
||||||
} |
|
||||||
|
|
||||||
interface UserRelays { |
|
||||||
loading: boolean |
|
||||||
ndk_relays: NDKRelayList | undefined |
|
||||||
} |
|
||||||
|
|
||||||
export const user_relays: { [hexpubkey: string]: Writable<UserRelays> } = {} |
|
||||||
|
|
||||||
export const getUserRelays = async (hexpubkey: string): Promise<UserRelays> => { |
|
||||||
return new Promise(async (res, _) => { |
|
||||||
if (user_relays[hexpubkey]) { |
|
||||||
const unsubscriber: Unsubscriber = user_relays[hexpubkey].subscribe( |
|
||||||
(querying_user_relays) => { |
|
||||||
if (querying_user_relays && !querying_user_relays.loading) { |
|
||||||
res(querying_user_relays) |
|
||||||
setTimeout(() => { |
|
||||||
if (unsubscriber) unsubscriber() |
|
||||||
}, 5) |
|
||||||
} |
|
||||||
} |
|
||||||
) |
|
||||||
} else { |
|
||||||
user_relays[hexpubkey] = writable({ |
|
||||||
loading: true, |
|
||||||
ndk_relays: undefined, |
|
||||||
}) |
|
||||||
const relay_list = await getRelayListForUser(hexpubkey, ndk) |
|
||||||
const querying_user_relays = { |
|
||||||
loading: false, |
|
||||||
ndk_relays: relay_list, |
|
||||||
} |
|
||||||
user_relays[hexpubkey].set({ ...querying_user_relays }) |
|
||||||
res(querying_user_relays) |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
@ -1,4 +0,0 @@ |
|||||||
export const ignore_kinds = [ |
|
||||||
31234, // amethyst draft kind
|
|
||||||
9978, // confidence scoring event
|
|
||||||
] |
|
||||||
@ -1,136 +0,0 @@ |
|||||||
<script lang="ts"> |
|
||||||
import { base_relays, ndk } from '$lib/stores/ndk' |
|
||||||
import { NDKEvent, NDKRelaySet } from '@nostr-dev-kit/ndk' |
|
||||||
import { issue_kind, repo_kind } from '$lib/kinds' |
|
||||||
import { getUserRelays, logged_in_user, login } from '$lib/stores/users' |
|
||||||
import type { RepoEvent } from '$lib/components/repo/type' |
|
||||||
import { goto } from '$app/navigation' |
|
||||||
import { nip19 } from 'nostr-tools' |
|
||||||
|
|
||||||
export let repo_event: RepoEvent |
|
||||||
|
|
||||||
let submitting = false |
|
||||||
let submitted = false |
|
||||||
let edit_mode = false |
|
||||||
let title = '' |
|
||||||
let content = '' |
|
||||||
$: { |
|
||||||
edit_mode = !submitted |
|
||||||
} |
|
||||||
let submit_attempted = false |
|
||||||
|
|
||||||
async function sendIssue(title: string, content: string) { |
|
||||||
submit_attempted = true |
|
||||||
if (title.length < 10) return |
|
||||||
if (!$logged_in_user) await login() |
|
||||||
if (!$logged_in_user) return |
|
||||||
let event = new NDKEvent(ndk) |
|
||||||
event.kind = issue_kind |
|
||||||
event.tags.push(['subject', title]) |
|
||||||
event.tags.push(['alt', `git repository issue: ${title}`]) |
|
||||||
|
|
||||||
if (repo_event.unique_commit) { |
|
||||||
event.tags.push(['r', repo_event.unique_commit]) |
|
||||||
} |
|
||||||
event.tags.push([ |
|
||||||
'a', |
|
||||||
`${repo_kind}:${repo_event.maintainers[0]}:${repo_event.identifier}`, |
|
||||||
repo_event.relays[0] || '', |
|
||||||
'root', |
|
||||||
]) |
|
||||||
repo_event.maintainers.forEach((m) => event.tags.push(['p', m])) |
|
||||||
event.content = `${content}` |
|
||||||
submitting = true |
|
||||||
let relays = [ |
|
||||||
...(repo_event.relays.length > 3 |
|
||||||
? repo_event.relays |
|
||||||
: [...base_relays].concat(repo_event.relays)), |
|
||||||
] |
|
||||||
try { |
|
||||||
event.sign() |
|
||||||
} catch { |
|
||||||
alert('failed to sign event') |
|
||||||
} |
|
||||||
try { |
|
||||||
let user_relays = await getUserRelays($logged_in_user.hexpubkey) |
|
||||||
relays = [ |
|
||||||
...relays, |
|
||||||
...(user_relays.ndk_relays |
|
||||||
? user_relays.ndk_relays.writeRelayUrls |
|
||||||
: []), |
|
||||||
] |
|
||||||
} catch { |
|
||||||
alert('failed to get user relays') |
|
||||||
} |
|
||||||
try { |
|
||||||
let _ = await event.publish(NDKRelaySet.fromRelayUrls(relays, ndk)) |
|
||||||
submitting = false |
|
||||||
submitted = true |
|
||||||
setTimeout(() => { |
|
||||||
goto(`/r/${repo_event.naddr}/issues/${nip19.noteEncode(event.id)}`) |
|
||||||
}, 2000) |
|
||||||
} catch {} |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
{#if edit_mode} |
|
||||||
<div class="flex"> |
|
||||||
<div class="flex-grow"> |
|
||||||
<label class="form-control w-full"> |
|
||||||
<div class="label"> |
|
||||||
<span class="label-text text-sm">Title</span> |
|
||||||
</div> |
|
||||||
<input |
|
||||||
type="text" |
|
||||||
bind:value={title} |
|
||||||
class="input-neutral input input-sm input-bordered mb-3 w-full border-warning" |
|
||||||
class:border-warning={submit_attempted && title.length < 10} |
|
||||||
placeholder="title" |
|
||||||
/> |
|
||||||
{#if submit_attempted && title.length < 10} |
|
||||||
<div class="pr-3 align-middle text-sm text-warning"> |
|
||||||
title must be at least 10 characters |
|
||||||
</div> |
|
||||||
{/if} |
|
||||||
</label> |
|
||||||
<label class="form-control w-full"> |
|
||||||
<div class="label"> |
|
||||||
<span class="label-textarea text-sm">Description</span> |
|
||||||
</div> |
|
||||||
|
|
||||||
<textarea |
|
||||||
disabled={submitting} |
|
||||||
bind:value={content} |
|
||||||
class="textarea textarea-secondary w-full" |
|
||||||
placeholder="description" |
|
||||||
rows="10" |
|
||||||
></textarea> |
|
||||||
</label> |
|
||||||
|
|
||||||
<div class="mt-2 flex items-center"> |
|
||||||
<div class="flex-auto"></div> |
|
||||||
{#if submit_attempted && title.length < 10} |
|
||||||
<div class="pr-3 align-middle text-sm text-warning"> |
|
||||||
title must be at least 10 characters |
|
||||||
</div> |
|
||||||
{/if} |
|
||||||
<button |
|
||||||
on:click={() => sendIssue(title, content)} |
|
||||||
disabled={submitting || (submit_attempted && title.length < 10)} |
|
||||||
class="btn btn-primary btn-sm" |
|
||||||
> |
|
||||||
{#if submitting} |
|
||||||
Sending |
|
||||||
{:else if !$logged_in_user} |
|
||||||
Login before Sending |
|
||||||
{:else} |
|
||||||
Send |
|
||||||
{/if} |
|
||||||
</button> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
{/if} |
|
||||||
{#if submitted} |
|
||||||
<div>sent going to issue!</div> |
|
||||||
{/if} |
|
||||||
@ -1,159 +0,0 @@ |
|||||||
<script lang="ts"> |
|
||||||
import { base_relays, ndk } from '$lib/stores/ndk' |
|
||||||
import { NDKEvent, NDKRelaySet } from '@nostr-dev-kit/ndk' |
|
||||||
import { reply_kind, repo_kind } from '$lib/kinds' |
|
||||||
import { getUserRelays, logged_in_user, user_relays } from '$lib/stores/users' |
|
||||||
import { |
|
||||||
selected_repo_collection, |
|
||||||
selected_repo_event, |
|
||||||
} from '$lib/stores/repo' |
|
||||||
import Compose from '$lib/components/events/Compose.svelte' |
|
||||||
import { selected_proposal_full } from '$lib/stores/Proposal' |
|
||||||
import { selected_issue_full } from '$lib/stores/Issue' |
|
||||||
import type { IssueFull } from '$lib/components/issues/type' |
|
||||||
import type { ProposalFull } from '$lib/components/proposals/type' |
|
||||||
import { get } from 'svelte/store' |
|
||||||
|
|
||||||
export let type: 'proposal' | 'issue' = 'proposal' |
|
||||||
export let event: NDKEvent |
|
||||||
export let sentFunction = () => {} |
|
||||||
let repo_identifier: string |
|
||||||
let selected_proposal_or_issue: IssueFull | ProposalFull |
|
||||||
|
|
||||||
let submitting = false |
|
||||||
let submitted = false |
|
||||||
let edit_mode = false |
|
||||||
$: { |
|
||||||
repo_identifier = $selected_repo_event.identifier |
|
||||||
selected_proposal_or_issue = |
|
||||||
type === 'proposal' ? $selected_proposal_full : $selected_issue_full |
|
||||||
|
|
||||||
edit_mode = |
|
||||||
repo_identifier.length > 0 && |
|
||||||
selected_proposal_or_issue.summary.id.length > 0 && |
|
||||||
!submitted |
|
||||||
} |
|
||||||
/** to get the proposal revision id rather than the root proposal */ |
|
||||||
const getRootId = (event: NDKEvent): string => { |
|
||||||
// exclude 'a' references to repo events |
|
||||||
let root_tag = event.tags.find( |
|
||||||
(t) => t[0] === 'e' && t.length === 4 && t[3] === 'root' |
|
||||||
) |
|
||||||
if (root_tag) return root_tag[1] |
|
||||||
if (event.tags.some((t) => t[0] === 't' && t[1] === 'root')) return event.id |
|
||||||
return selected_proposal_or_issue.summary.id |
|
||||||
} |
|
||||||
|
|
||||||
async function sendReply(content: string) { |
|
||||||
if (!$logged_in_user) return |
|
||||||
let new_event = new NDKEvent(ndk) |
|
||||||
new_event.kind = reply_kind |
|
||||||
if (reply_kind !== 1) event.tags.push(['alt', `git reply`]) |
|
||||||
new_event.tags.push([ |
|
||||||
'e', |
|
||||||
getRootId(event), |
|
||||||
$selected_repo_event.relays[0] || '', |
|
||||||
'root', |
|
||||||
]) |
|
||||||
if (event.id.length > 0) { |
|
||||||
new_event.tags.push([ |
|
||||||
'e', |
|
||||||
event.id, |
|
||||||
$selected_repo_event.relays[0] || '', |
|
||||||
'reply', |
|
||||||
]) |
|
||||||
} |
|
||||||
if ($selected_repo_event.unique_commit) { |
|
||||||
new_event.tags.push(['r', $selected_repo_event.unique_commit]) |
|
||||||
} |
|
||||||
$selected_repo_collection.maintainers.forEach((m) => { |
|
||||||
new_event.tags.push(['a', `${repo_kind}:${m}:${repo_identifier}`]) |
|
||||||
}) |
|
||||||
let parent_event_user_relay = user_relays[event.pubkey] |
|
||||||
? get(user_relays[event.pubkey]).ndk_relays?.writeRelayUrls[0] |
|
||||||
: undefined |
|
||||||
|
|
||||||
if (event.pubkey !== $logged_in_user?.hexpubkey) |
|
||||||
new_event.tags.push( |
|
||||||
parent_event_user_relay |
|
||||||
? ['p', event.pubkey, parent_event_user_relay] |
|
||||||
: ['p', event.pubkey] |
|
||||||
) |
|
||||||
event.tags |
|
||||||
.filter((tag) => tag[0] === 'p') |
|
||||||
.forEach((tag) => { |
|
||||||
if ( |
|
||||||
// not duplicate |
|
||||||
!new_event.tags.some((t) => t[1] === tag[1]) && |
|
||||||
// not current user (dont tag self) |
|
||||||
tag[1] !== $logged_in_user?.hexpubkey |
|
||||||
) |
|
||||||
new_event.tags.push(tag) |
|
||||||
}) |
|
||||||
new_event.content = content |
|
||||||
submitting = true |
|
||||||
let relays = [ |
|
||||||
...($selected_repo_event.relays.length > 3 |
|
||||||
? $selected_repo_event.relays |
|
||||||
: [...base_relays].concat($selected_repo_event.relays)), |
|
||||||
] |
|
||||||
try { |
|
||||||
new_event.sign() |
|
||||||
} catch { |
|
||||||
alert('failed to sign event') |
|
||||||
} |
|
||||||
try { |
|
||||||
let user_relays = await getUserRelays($logged_in_user.hexpubkey) |
|
||||||
relays = [ |
|
||||||
...relays, |
|
||||||
...(user_relays.ndk_relays |
|
||||||
? user_relays.ndk_relays.writeRelayUrls |
|
||||||
: []), |
|
||||||
] |
|
||||||
} catch {} |
|
||||||
try { |
|
||||||
let root_event_user_relays = await getUserRelays(event.pubkey) |
|
||||||
relays = [ |
|
||||||
...relays, |
|
||||||
...(root_event_user_relays.ndk_relays |
|
||||||
? root_event_user_relays.ndk_relays.writeRelayUrls |
|
||||||
: []), |
|
||||||
] |
|
||||||
} catch {} |
|
||||||
// TODO root event user relays |
|
||||||
try { |
|
||||||
let _ = await new_event.publish( |
|
||||||
NDKRelaySet.fromRelayUrls([...new Set(relays)], ndk) |
|
||||||
) |
|
||||||
submitting = false |
|
||||||
submitted = true |
|
||||||
setTimeout(() => { |
|
||||||
submitted = false |
|
||||||
sentFunction() |
|
||||||
}, 3000) |
|
||||||
} catch {} |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
{#if edit_mode} |
|
||||||
<Compose {sendReply} {submitting} logged_in={!!$logged_in_user} /> |
|
||||||
{/if} |
|
||||||
{#if submitted} |
|
||||||
<div role="alert" class="alert mt-6"> |
|
||||||
<svg |
|
||||||
xmlns="http://www.w3.org/2000/svg" |
|
||||||
fill="none" |
|
||||||
viewBox="0 0 24 24" |
|
||||||
class="h-6 w-6 shrink-0 stroke-info" |
|
||||||
><path |
|
||||||
stroke-linecap="round" |
|
||||||
stroke-linejoin="round" |
|
||||||
stroke-width="2" |
|
||||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" |
|
||||||
></path></svg |
|
||||||
> |
|
||||||
<div> |
|
||||||
<h3 class="prose mb-2 text-sm font-bold">reply sent</h3> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
{/if} |
|
||||||
@ -1,89 +0,0 @@ |
|||||||
<script lang="ts"> |
|
||||||
import EventWrapper from '$lib/components/events/EventWrapper.svelte' |
|
||||||
import EventWrapperLite from '$lib/components/events/EventWrapperLite.svelte' |
|
||||||
import Status from '$lib/components/events/content/Status.svelte' |
|
||||||
import Patch from '$lib/components/events/content/Patch.svelte' |
|
||||||
import ParsedContent from '$lib/components/events/content/ParsedContent.svelte' |
|
||||||
import { defaults as user_defaults } from '$lib/components/users/type' |
|
||||||
import { |
|
||||||
issue_kind, |
|
||||||
patch_kind, |
|
||||||
proposal_status_kinds, |
|
||||||
repo_kind, |
|
||||||
} from '$lib/kinds' |
|
||||||
import { ensureUser } from '$lib/stores/users' |
|
||||||
import type { NDKEvent } from '@nostr-dev-kit/ndk' |
|
||||||
import { onDestroy } from 'svelte' |
|
||||||
import { writable, type Unsubscriber } from 'svelte/store' |
|
||||||
import { |
|
||||||
extractPatchMessage, |
|
||||||
isCoverLetter, |
|
||||||
} from '$lib/components/events/content/utils' |
|
||||||
import Repo from '$lib/components/events/content/Repo.svelte' |
|
||||||
import IssuePreview from '$lib/components/events/content/IssuePreview.svelte' |
|
||||||
|
|
||||||
export let event: NDKEvent |
|
||||||
export let type: 'proposal' | 'issue' = 'proposal' |
|
||||||
export let preview = false |
|
||||||
|
|
||||||
let author = writable({ ...user_defaults }) |
|
||||||
let author_unsubsriber: Unsubscriber |
|
||||||
$: { |
|
||||||
if (event && event.pubkey.length > 0) |
|
||||||
author_unsubsriber = ensureUser(event.pubkey).subscribe((u) => { |
|
||||||
if (u.hexpubkey == event.pubkey) author.set({ ...u }) |
|
||||||
}) |
|
||||||
} |
|
||||||
onDestroy(() => { |
|
||||||
if (author_unsubsriber) author_unsubsriber() |
|
||||||
}) |
|
||||||
const getDtag = (event: NDKEvent): undefined | string => { |
|
||||||
try { |
|
||||||
const tag = event.replaceableDTag() |
|
||||||
return tag |
|
||||||
} catch {} |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
{#if event.kind && [6, 16].includes(event.kind)} |
|
||||||
<EventWrapperLite author={$author} created_at={event.created_at}> |
|
||||||
reposted by |
|
||||||
</EventWrapperLite> |
|
||||||
{:else if event.kind && event.kind === 5} |
|
||||||
<EventWrapperLite author={$author} created_at={event.created_at}> |
|
||||||
deletion requested by |
|
||||||
</EventWrapperLite> |
|
||||||
{:else if event.kind && event.kind === 30001} |
|
||||||
<EventWrapperLite author={$author} created_at={event.created_at}> |
|
||||||
added to '{getDtag(event) || 'unknown'}' list by |
|
||||||
</EventWrapperLite> |
|
||||||
{:else if event.kind && event.kind == repo_kind} |
|
||||||
<EventWrapperLite author={$author} created_at={event.created_at}> |
|
||||||
<Repo {event} /> |
|
||||||
</EventWrapperLite> |
|
||||||
{:else if preview && event.kind && event.kind === patch_kind} |
|
||||||
<EventWrapperLite author={$author} created_at={event.created_at}> |
|
||||||
<Patch {event} {preview} /> |
|
||||||
</EventWrapperLite> |
|
||||||
{:else if preview && event.kind && event.kind === issue_kind} |
|
||||||
<EventWrapperLite author={$author} created_at={event.created_at}> |
|
||||||
<IssuePreview {event} /> |
|
||||||
</EventWrapperLite> |
|
||||||
{:else} |
|
||||||
<EventWrapper {type} author={$author} created_at={event.created_at} {event}> |
|
||||||
{#if event.kind == patch_kind} |
|
||||||
{#if isCoverLetter(event.content)} |
|
||||||
<ParsedContent |
|
||||||
content={extractPatchMessage(event.content)} |
|
||||||
tags={event.tags} |
|
||||||
/> |
|
||||||
{:else} |
|
||||||
<Patch {event} /> |
|
||||||
{/if} |
|
||||||
{:else if event.kind && proposal_status_kinds.includes(event.kind)} |
|
||||||
<Status {type} status={event.kind} /> |
|
||||||
{:else} |
|
||||||
<ParsedContent content={event.content} tags={event.tags} /> |
|
||||||
{/if} |
|
||||||
</EventWrapper> |
|
||||||
{/if} |
|
||||||
@ -1,69 +0,0 @@ |
|||||||
<script lang="ts"> |
|
||||||
import { NDKRelaySet, type NDKEvent } from '@nostr-dev-kit/ndk' |
|
||||||
import { onMount } from 'svelte' |
|
||||||
import { get, writable, type Writable } from 'svelte/store' |
|
||||||
import { base_relays, ndk } from '$lib/stores/ndk' |
|
||||||
import EventCard from './EventCard.svelte' |
|
||||||
import type { AddressPointer, EventPointer } from 'nostr-tools/nip19' |
|
||||||
import { repo_kind } from '$lib/kinds' |
|
||||||
import { ensureRepo } from '$lib/stores/repos' |
|
||||||
import EventWrapperLite from '$lib/components/events/EventWrapperLite.svelte' |
|
||||||
import Repo from '$lib/components/events/content/Repo.svelte' |
|
||||||
|
|
||||||
export let pointer: AddressPointer | EventPointer |
|
||||||
|
|
||||||
let cannot_find_event = false |
|
||||||
let event: Writable<undefined | NDKEvent> = writable(undefined) |
|
||||||
|
|
||||||
const isAddressPointer = ( |
|
||||||
pointer: AddressPointer | EventPointer |
|
||||||
): pointer is AddressPointer => { |
|
||||||
return Object.keys(pointer).includes('identifier') |
|
||||||
} |
|
||||||
let is_repo = isAddressPointer(pointer) && pointer.kind == repo_kind |
|
||||||
let repo = |
|
||||||
is_repo && isAddressPointer(pointer) |
|
||||||
? ensureRepo(`${pointer.kind}:${pointer.pubkey}:${pointer.identifier}`) |
|
||||||
: undefined |
|
||||||
|
|
||||||
onMount(() => { |
|
||||||
if (!is_repo) { |
|
||||||
let sub = ndk.subscribe( |
|
||||||
isAddressPointer(pointer) |
|
||||||
? { |
|
||||||
'#a': [`${pointer.kind}:${pointer.pubkey}:${pointer.identifier}`], |
|
||||||
} |
|
||||||
: { ids: [pointer.id] }, |
|
||||||
{ closeOnEose: true }, |
|
||||||
NDKRelaySet.fromRelayUrls( |
|
||||||
pointer.relays ? [...base_relays, ...pointer.relays] : base_relays, |
|
||||||
ndk |
|
||||||
) |
|
||||||
) |
|
||||||
|
|
||||||
sub.on('event', (e: NDKEvent) => { |
|
||||||
event.set(e) |
|
||||||
}) |
|
||||||
|
|
||||||
sub.on('eose', () => { |
|
||||||
if (!get(event)) cannot_find_event = true |
|
||||||
}) |
|
||||||
} |
|
||||||
}) |
|
||||||
</script> |
|
||||||
|
|
||||||
<div class="card my-3 border border-base-400 shadow-xl"> |
|
||||||
{#if repo && $repo} |
|
||||||
<EventWrapperLite author={$repo?.author} created_at={$repo?.created_at}> |
|
||||||
<Repo event={$repo} /> |
|
||||||
</EventWrapperLite> |
|
||||||
{:else if $event && $event.pubkey} |
|
||||||
<div class="p-2 pt-0"> |
|
||||||
<EventCard event={$event} preview={true} /> |
|
||||||
</div> |
|
||||||
{:else if cannot_find_event} |
|
||||||
<div class="m-3 text-center text-sm">cannot find event</div> |
|
||||||
{:else} |
|
||||||
<div class="m-3 text-center text-sm">loading...</div> |
|
||||||
{/if} |
|
||||||
</div> |
|
||||||
@ -1,23 +0,0 @@ |
|||||||
<script lang="ts"> |
|
||||||
import { |
|
||||||
checkForNip07Plugin, |
|
||||||
logged_in_user, |
|
||||||
login, |
|
||||||
nip07_plugin, |
|
||||||
} from '$lib/stores/users' |
|
||||||
import { onMount } from 'svelte' |
|
||||||
import Navbar from '$lib/components/Navbar.svelte' |
|
||||||
|
|
||||||
let singup_function = () => { |
|
||||||
alert('a NIP-07 browser extension is required. currently no signup page') |
|
||||||
} |
|
||||||
|
|
||||||
onMount(checkForNip07Plugin) |
|
||||||
</script> |
|
||||||
|
|
||||||
<Navbar |
|
||||||
logged_in_user={$logged_in_user} |
|
||||||
nip07_plugin={$nip07_plugin} |
|
||||||
login_function={login} |
|
||||||
{singup_function} |
|
||||||
/> |
|
||||||
@ -1,13 +0,0 @@ |
|||||||
<script lang="ts"> |
|
||||||
import RepoDetails from '$lib/components/repo/RepoDetails.svelte' |
|
||||||
import { |
|
||||||
ensureSelectedRepoCollection, |
|
||||||
selected_repo_event, |
|
||||||
} from '$lib/stores/repo' |
|
||||||
|
|
||||||
export let a = '' |
|
||||||
|
|
||||||
ensureSelectedRepoCollection(a) |
|
||||||
</script> |
|
||||||
|
|
||||||
<RepoDetails {...$selected_repo_event} /> |
|
||||||
@ -1,76 +0,0 @@ |
|||||||
<script lang="ts"> |
|
||||||
import { issue_icon_path } from '$lib/components/issues/icons' |
|
||||||
import { proposal_icon_path } from '$lib/components/proposals/icons' |
|
||||||
import type { RepoPage } from '$lib/components/repo/type' |
|
||||||
import { proposal_status_open } from '$lib/kinds' |
|
||||||
import { issue_summaries } from '$lib/stores/Issues' |
|
||||||
import { proposal_summaries } from '$lib/stores/Proposals' |
|
||||||
import { selected_repo_event, selected_repo_readme } from '$lib/stores/repo' |
|
||||||
|
|
||||||
export let selected_tab: RepoPage = 'about' |
|
||||||
</script> |
|
||||||
|
|
||||||
<div class="flex border-b border-base-400"> |
|
||||||
<div role="tablist" class="tabs tabs-bordered flex-none"> |
|
||||||
{#if !$selected_repo_readme.failed} |
|
||||||
<a |
|
||||||
href={`/r/${$selected_repo_event.naddr}`} |
|
||||||
class="tab" |
|
||||||
class:tab-active={selected_tab === 'about'} |
|
||||||
> |
|
||||||
About |
|
||||||
</a> |
|
||||||
{/if} |
|
||||||
<a |
|
||||||
href={`/r/${$selected_repo_event.naddr}/proposals`} |
|
||||||
class="tab" |
|
||||||
class:tab-active={selected_tab === 'proposals'} |
|
||||||
> |
|
||||||
<svg |
|
||||||
xmlns="http://www.w3.org/2000/svg" |
|
||||||
viewBox="0 0 16 16" |
|
||||||
class="mb-1 mr-1 h-4 w-4 flex-none fill-base-content pt-1 opacity-50" |
|
||||||
> |
|
||||||
<path d={proposal_icon_path.open_pull} /> |
|
||||||
</svg> |
|
||||||
Proposals |
|
||||||
{#if $proposal_summaries.loading} |
|
||||||
<span class="loading loading-spinner loading-xs ml-2 text-neutral" |
|
||||||
></span> |
|
||||||
{:else if $proposal_summaries.summaries.filter((s) => s.status === proposal_status_open).length > 0} |
|
||||||
<span class="badge badge-neutral badge-sm ml-2"> |
|
||||||
{$proposal_summaries.summaries.filter( |
|
||||||
(s) => s.status === proposal_status_open |
|
||||||
).length} |
|
||||||
</span> |
|
||||||
{/if} |
|
||||||
</a> |
|
||||||
<a |
|
||||||
href={`/r/${$selected_repo_event.naddr}/issues`} |
|
||||||
class="tab" |
|
||||||
class:tab-active={selected_tab === 'issues'} |
|
||||||
> |
|
||||||
<svg |
|
||||||
xmlns="http://www.w3.org/2000/svg" |
|
||||||
viewBox="0 0 16 16" |
|
||||||
class="mb-1 mr-1 h-4 w-4 flex-none fill-base-content pt-1 opacity-50" |
|
||||||
> |
|
||||||
{#each issue_icon_path.open as p} |
|
||||||
<path d={p} /> |
|
||||||
{/each} |
|
||||||
</svg> |
|
||||||
Issues |
|
||||||
{#if $issue_summaries.loading} |
|
||||||
<span class="loading loading-spinner loading-xs ml-2 text-neutral" |
|
||||||
></span> |
|
||||||
{:else if $issue_summaries.summaries.filter((s) => s.status === proposal_status_open).length > 0} |
|
||||||
<span class="badge badge-neutral badge-sm ml-2"> |
|
||||||
{$issue_summaries.summaries.filter( |
|
||||||
(s) => s.status === proposal_status_open |
|
||||||
).length} |
|
||||||
</span> |
|
||||||
{/if} |
|
||||||
</a> |
|
||||||
</div> |
|
||||||
<div class="flex-grow"></div> |
|
||||||
</div> |
|
||||||
@ -1,70 +0,0 @@ |
|||||||
<script lang="ts"> |
|
||||||
import RepoDetails from '$lib/wrappers/RepoDetails.svelte' |
|
||||||
import { |
|
||||||
ensureSelectedRepoCollection, |
|
||||||
selected_repo_event, |
|
||||||
} from '$lib/stores/repo' |
|
||||||
import RepoHeader from '$lib/components/repo/RepoHeader.svelte' |
|
||||||
import Container from '$lib/components/Container.svelte' |
|
||||||
import { ensureProposalSummaries } from '$lib/stores/Proposals' |
|
||||||
import { ensureIssueSummaries } from '$lib/stores/Issues' |
|
||||||
import type { RepoPage } from '$lib/components/repo/type' |
|
||||||
import { naddrToPointer, naddrToRepoA } from '$lib/components/repo/utils' |
|
||||||
import AlertError from '$lib/components/AlertError.svelte' |
|
||||||
|
|
||||||
export let repo_naddr = '' |
|
||||||
export let selected_tab: RepoPage = 'about' |
|
||||||
export let with_side_bar = true |
|
||||||
export let show_details_on_mobile = false |
|
||||||
|
|
||||||
let invalid_naddr = false |
|
||||||
let a = '' |
|
||||||
|
|
||||||
$: { |
|
||||||
const a_result = naddrToRepoA(repo_naddr) |
|
||||||
if (a_result) { |
|
||||||
a = a_result |
|
||||||
invalid_naddr = false |
|
||||||
ensureSelectedRepoCollection(a, naddrToPointer(repo_naddr)?.relays) |
|
||||||
ensureProposalSummaries(a) |
|
||||||
ensureIssueSummaries(a) |
|
||||||
} else { |
|
||||||
invalid_naddr = true |
|
||||||
} |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<RepoHeader {...$selected_repo_event} {selected_tab} /> |
|
||||||
{#if invalid_naddr} |
|
||||||
<Container> |
|
||||||
<AlertError> |
|
||||||
<div>Error! invalid naddr in url:</div> |
|
||||||
<div class="break-all">{repo_naddr}</div> |
|
||||||
</AlertError> |
|
||||||
</Container> |
|
||||||
<Container> |
|
||||||
<slot /> |
|
||||||
</Container> |
|
||||||
{/if} |
|
||||||
{#if with_side_bar} |
|
||||||
<Container> |
|
||||||
<div class="mt-2 md:flex"> |
|
||||||
<div class="md:mr-2 md:w-2/3"> |
|
||||||
<slot /> |
|
||||||
</div> |
|
||||||
<div |
|
||||||
class:hidden={!show_details_on_mobile} |
|
||||||
class=" rounded-lg border border-base-400 md:flex md:w-1/3 md:border-none" |
|
||||||
> |
|
||||||
<div class="border-b border-base-400 bg-base-300 px-6 py-3 md:hidden"> |
|
||||||
<h4 class="">Repository Details</h4> |
|
||||||
</div> |
|
||||||
<div class="prose my-3 w-full px-6 md:ml-2 md:px-0"> |
|
||||||
<RepoDetails {a} /> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</Container> |
|
||||||
{:else} |
|
||||||
<slot /> |
|
||||||
{/if} |
|
||||||
@ -1,24 +0,0 @@ |
|||||||
<script lang="ts"> |
|
||||||
import type { NDKEvent } from '@nostr-dev-kit/ndk' |
|
||||||
import ThreadTree from './ThreadTree.svelte' |
|
||||||
import { getThreadTrees } from './thread_tree' |
|
||||||
|
|
||||||
export let event: NDKEvent |
|
||||||
export let type: 'proposal' | 'issue' = 'proposal' |
|
||||||
export let show_compose = true |
|
||||||
|
|
||||||
export let replies: NDKEvent[] | undefined = undefined |
|
||||||
|
|
||||||
$: thread_trees = getThreadTrees(type, event, replies) |
|
||||||
</script> |
|
||||||
|
|
||||||
{#each thread_trees as tree, i} |
|
||||||
{#if i > 0} |
|
||||||
<div class="divider">new revision</div> |
|
||||||
{/if} |
|
||||||
<ThreadTree |
|
||||||
{type} |
|
||||||
{tree} |
|
||||||
show_compose={show_compose && thread_trees.length - 1 === i} |
|
||||||
/> |
|
||||||
{/each} |
|
||||||
@ -1,138 +0,0 @@ |
|||||||
<script lang="ts"> |
|
||||||
import EventCard from './EventCard.svelte' |
|
||||||
import ThreadWrapper from '$lib/components/events/ThreadWrapper.svelte' |
|
||||||
import type { ThreadTreeNode } from '$lib/components/events/type' |
|
||||||
import ComposeReply from './ComposeReply.svelte' |
|
||||||
|
|
||||||
export let tree: ThreadTreeNode | undefined |
|
||||||
export let type: 'proposal' | 'issue' = 'proposal' |
|
||||||
export let show_compose = false |
|
||||||
const countReplies = (tree: ThreadTreeNode, starting: number = 0): number => { |
|
||||||
return ( |
|
||||||
tree.child_nodes.length + |
|
||||||
tree.child_nodes.reduce((a, c) => a + countReplies(c), starting) |
|
||||||
) |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
{#if tree} |
|
||||||
<EventCard {type} event={tree.event} /> |
|
||||||
<ThreadWrapper num_replies={countReplies(tree)}> |
|
||||||
{#each tree.child_nodes as layer1} |
|
||||||
<EventCard {type} event={layer1.event} /> |
|
||||||
<ThreadWrapper num_replies={countReplies(layer1)}> |
|
||||||
{#each layer1.child_nodes as layer2} |
|
||||||
<EventCard {type} event={layer2.event} /> |
|
||||||
<ThreadWrapper num_replies={countReplies(layer2)}> |
|
||||||
{#each layer2.child_nodes as layer3} |
|
||||||
<EventCard {type} event={layer3.event} /> |
|
||||||
<ThreadWrapper num_replies={countReplies(layer3)}> |
|
||||||
{#each layer3.child_nodes as layer4} |
|
||||||
<EventCard {type} event={layer4.event} /> |
|
||||||
<ThreadWrapper num_replies={countReplies(layer4)}> |
|
||||||
{#each layer4.child_nodes as layer5} |
|
||||||
<EventCard {type} event={layer5.event} /> |
|
||||||
<ThreadWrapper num_replies={countReplies(layer5)}> |
|
||||||
{#each layer5.child_nodes as layer6} |
|
||||||
<EventCard {type} event={layer6.event} /> |
|
||||||
<ThreadWrapper num_replies={countReplies(layer6)}> |
|
||||||
{#each layer6.child_nodes as layer7} |
|
||||||
<EventCard {type} event={layer7.event} /> |
|
||||||
<ThreadWrapper num_replies={countReplies(layer7)}> |
|
||||||
{#each layer7.child_nodes as layer8} |
|
||||||
<EventCard {type} event={layer8.event} /> |
|
||||||
<ThreadWrapper |
|
||||||
num_replies={countReplies(layer8)} |
|
||||||
> |
|
||||||
{#each layer8.child_nodes as layer9} |
|
||||||
<EventCard {type} event={layer9.event} /> |
|
||||||
<ThreadWrapper |
|
||||||
num_replies={countReplies(layer9)} |
|
||||||
> |
|
||||||
{#each layer9.child_nodes as layer10} |
|
||||||
<EventCard |
|
||||||
{type} |
|
||||||
event={layer10.event} |
|
||||||
/> |
|
||||||
<ThreadWrapper |
|
||||||
num_replies={countReplies(layer10)} |
|
||||||
> |
|
||||||
{#each layer10.child_nodes as layer11} |
|
||||||
<EventCard |
|
||||||
{type} |
|
||||||
event={layer11.event} |
|
||||||
/> |
|
||||||
<ThreadWrapper |
|
||||||
num_replies={countReplies( |
|
||||||
layer11 |
|
||||||
)} |
|
||||||
> |
|
||||||
{#each layer11.child_nodes as layer12} |
|
||||||
<EventCard |
|
||||||
{type} |
|
||||||
event={layer12.event} |
|
||||||
/> |
|
||||||
<ThreadWrapper |
|
||||||
num_replies={countReplies( |
|
||||||
layer12 |
|
||||||
)} |
|
||||||
> |
|
||||||
{#each layer12.child_nodes as layer13} |
|
||||||
<EventCard |
|
||||||
{type} |
|
||||||
event={layer13.event} |
|
||||||
/> |
|
||||||
<ThreadWrapper |
|
||||||
num_replies={countReplies( |
|
||||||
layer13 |
|
||||||
)} |
|
||||||
> |
|
||||||
{#each layer13.child_nodes as layer14} |
|
||||||
<EventCard |
|
||||||
{type} |
|
||||||
event={layer14.event} |
|
||||||
/> |
|
||||||
<ThreadWrapper |
|
||||||
num_replies={countReplies( |
|
||||||
layer14 |
|
||||||
)} |
|
||||||
> |
|
||||||
{#each layer14.child_nodes as layer15} |
|
||||||
<EventCard |
|
||||||
{type} |
|
||||||
event={layer15.event} |
|
||||||
/> |
|
||||||
{/each} |
|
||||||
</ThreadWrapper> |
|
||||||
{/each} |
|
||||||
</ThreadWrapper> |
|
||||||
{/each} |
|
||||||
</ThreadWrapper> |
|
||||||
{/each} |
|
||||||
</ThreadWrapper> |
|
||||||
{/each} |
|
||||||
</ThreadWrapper> |
|
||||||
{/each} |
|
||||||
</ThreadWrapper> |
|
||||||
{/each} |
|
||||||
</ThreadWrapper> |
|
||||||
{/each} |
|
||||||
</ThreadWrapper> |
|
||||||
{/each} |
|
||||||
</ThreadWrapper> |
|
||||||
{/each} |
|
||||||
</ThreadWrapper> |
|
||||||
{/each} |
|
||||||
</ThreadWrapper> |
|
||||||
{/each} |
|
||||||
</ThreadWrapper> |
|
||||||
{/each} |
|
||||||
</ThreadWrapper> |
|
||||||
{/each} |
|
||||||
</ThreadWrapper> |
|
||||||
{/each} |
|
||||||
{#if show_compose} |
|
||||||
<ComposeReply {type} event={tree.event} /> |
|
||||||
{/if} |
|
||||||
</ThreadWrapper> |
|
||||||
{/if} |
|
||||||
@ -1,231 +0,0 @@ |
|||||||
import { describe, expect, test } from 'vitest' |
|
||||||
import { createThreadTree, getParentId, getThreadTrees } from './thread_tree' |
|
||||||
import NDK, { |
|
||||||
NDKEvent, |
|
||||||
NDKPrivateKeySigner, |
|
||||||
type NDKTag, |
|
||||||
} from '@nostr-dev-kit/ndk' |
|
||||||
import { reply_kind } from '$lib/kinds' |
|
||||||
|
|
||||||
const ndk = new NDK() |
|
||||||
ndk.signer = new NDKPrivateKeySigner( |
|
||||||
'08608a436aee4c07ea5c36f85cb17c58f52b3ad7094f9318cc777771f0bf218b' |
|
||||||
) |
|
||||||
const generateEventWithTags = async (tags: NDKTag[]): Promise<NDKEvent> => { |
|
||||||
const event = new NDKEvent(ndk) |
|
||||||
event.kind = reply_kind |
|
||||||
event.content = Math.random().toFixed(10) |
|
||||||
tags.forEach((tag) => { |
|
||||||
event.tags.push(tag) |
|
||||||
}) |
|
||||||
await event.sign() |
|
||||||
return event |
|
||||||
} |
|
||||||
|
|
||||||
describe('getParentId', () => { |
|
||||||
describe('when all types of e tag are present', () => { |
|
||||||
test('returns id of e reply tag', async () => { |
|
||||||
expect( |
|
||||||
getParentId( |
|
||||||
await generateEventWithTags([ |
|
||||||
['e', '012'], |
|
||||||
['e', '123', '', 'root'], |
|
||||||
['e', '789', '', 'mention'], |
|
||||||
['e', '456', '', 'reply'], |
|
||||||
]) |
|
||||||
) |
|
||||||
).toEqual('456') |
|
||||||
}) |
|
||||||
}) |
|
||||||
describe('when all types of e tag are present except reply', () => { |
|
||||||
test('returns id of e root tag', async () => { |
|
||||||
expect( |
|
||||||
getParentId( |
|
||||||
await generateEventWithTags([ |
|
||||||
['e', '012'], |
|
||||||
['e', '123', '', 'root'], |
|
||||||
['e', '789', '', 'mention'], |
|
||||||
]) |
|
||||||
) |
|
||||||
).toEqual('123') |
|
||||||
}) |
|
||||||
}) |
|
||||||
describe('when only mention and unmarked e tags are present', () => { |
|
||||||
test('returns id of unmarked e tag', async () => { |
|
||||||
expect( |
|
||||||
getParentId( |
|
||||||
await generateEventWithTags([ |
|
||||||
['e', '012'], |
|
||||||
['e', '789', '', 'mention'], |
|
||||||
]) |
|
||||||
) |
|
||||||
).toEqual('012') |
|
||||||
}) |
|
||||||
}) |
|
||||||
describe('when only mention e tag are present', () => { |
|
||||||
test('return undefined', async () => { |
|
||||||
expect( |
|
||||||
getParentId(await generateEventWithTags([['e', '789', '', 'mention']])) |
|
||||||
).toBeUndefined() |
|
||||||
}) |
|
||||||
}) |
|
||||||
}) |
|
||||||
|
|
||||||
describe('createThreadTree', () => { |
|
||||||
describe('only events without parents are returned as top level array items', () => { |
|
||||||
describe('1 parent, 1 child', () => { |
|
||||||
test('returns array with only parent at top level', async () => { |
|
||||||
const root = await generateEventWithTags([]) |
|
||||||
const reply_to_root = await generateEventWithTags([ |
|
||||||
['e', root.id, '', 'reply'], |
|
||||||
]) |
|
||||||
const tree = createThreadTree([root, reply_to_root]) |
|
||||||
expect(tree).to.have.length(1) |
|
||||||
expect(tree[0].event.id).to.eq(root.id) |
|
||||||
}) |
|
||||||
test('parent has child in child_nodes, child has empty child nodes', async () => { |
|
||||||
const root = await generateEventWithTags([]) |
|
||||||
const reply_to_root = await generateEventWithTags([ |
|
||||||
['e', root.id, '', 'reply'], |
|
||||||
]) |
|
||||||
const tree = createThreadTree([root, reply_to_root]) |
|
||||||
expect(tree[0].child_nodes).to.have.length(1) |
|
||||||
expect(tree[0].child_nodes[0].event.id).to.eq(reply_to_root.id) |
|
||||||
expect(tree[0].child_nodes[0].child_nodes).to.be.length(0) |
|
||||||
}) |
|
||||||
}) |
|
||||||
describe('1 grand parent, 1 parent, 1 child - out of order', () => { |
|
||||||
test('returns array with only grand parent at top level with parent as its child, and child as parents child', async () => { |
|
||||||
const grand_parent = await generateEventWithTags([]) |
|
||||||
const parent = await generateEventWithTags([ |
|
||||||
['e', grand_parent.id, '', 'reply'], |
|
||||||
]) |
|
||||||
const child = await generateEventWithTags([ |
|
||||||
['e', parent.id, '', 'reply'], |
|
||||||
]) |
|
||||||
const tree = createThreadTree([grand_parent, child, parent]) |
|
||||||
expect(tree).to.have.length(1) |
|
||||||
expect(tree[0].event.id).to.eq(grand_parent.id) |
|
||||||
expect(tree[0].child_nodes).to.have.length(1) |
|
||||||
expect(tree[0].child_nodes[0].event.id).to.eq(parent.id) |
|
||||||
expect(tree[0].child_nodes[0].child_nodes).to.have.length(1) |
|
||||||
expect(tree[0].child_nodes[0].child_nodes[0].event.id).to.eq(child.id) |
|
||||||
expect( |
|
||||||
tree[0].child_nodes[0].child_nodes[0].child_nodes |
|
||||||
).to.have.length(0) |
|
||||||
}) |
|
||||||
}) |
|
||||||
describe('2 roots, 1 child', () => { |
|
||||||
test('returns array with 2 roots at top level', async () => { |
|
||||||
const root = await generateEventWithTags([]) |
|
||||||
const root2 = await generateEventWithTags([]) |
|
||||||
const reply_to_root = await generateEventWithTags([ |
|
||||||
['e', root.id, '', 'reply'], |
|
||||||
]) |
|
||||||
const tree = createThreadTree([root, reply_to_root, root2]) |
|
||||||
expect(tree).to.have.length(2) |
|
||||||
expect(tree[0].event.id).to.eq(root.id) |
|
||||||
expect(tree[1].event.id).to.eq(root2.id) |
|
||||||
expect(tree[1].child_nodes).to.have.length(0) |
|
||||||
expect(tree[0].child_nodes).to.have.length(1) |
|
||||||
expect(tree[0].child_nodes[0].event.id).to.eq(reply_to_root.id) |
|
||||||
expect(tree[0].child_nodes[0].child_nodes).to.be.length(0) |
|
||||||
}) |
|
||||||
}) |
|
||||||
}) |
|
||||||
}) |
|
||||||
|
|
||||||
describe('getThreadTrees', () => { |
|
||||||
describe('issue', () => { |
|
||||||
describe('2 roots, 1 child', () => { |
|
||||||
test('array only contains node related to specified event and children', async () => { |
|
||||||
const root = await generateEventWithTags([]) |
|
||||||
const root2 = await generateEventWithTags([]) |
|
||||||
const reply_to_root = await generateEventWithTags([ |
|
||||||
['e', root.id, '', 'reply'], |
|
||||||
]) |
|
||||||
const trees = getThreadTrees('issue', root, [ |
|
||||||
root, |
|
||||||
reply_to_root, |
|
||||||
root2, |
|
||||||
]) |
|
||||||
expect(trees).to.have.length(1) |
|
||||||
expect(trees[0].event.id).to.eq(root.id) |
|
||||||
expect(trees[0].child_nodes).to.have.length(1) |
|
||||||
expect(trees[0].child_nodes[0].event.id).to.eq(reply_to_root.id) |
|
||||||
expect(trees[0].child_nodes[0].child_nodes).to.be.length(0) |
|
||||||
}) |
|
||||||
}) |
|
||||||
}) |
|
||||||
describe('proposal', () => { |
|
||||||
describe('2 roots, 1 child', () => { |
|
||||||
test('array only contains node related to specified event and children', async () => { |
|
||||||
const root = await generateEventWithTags([]) |
|
||||||
const root2 = await generateEventWithTags([]) |
|
||||||
const reply_to_root = await generateEventWithTags([ |
|
||||||
['e', root.id, '', 'reply'], |
|
||||||
]) |
|
||||||
const trees = getThreadTrees('proposal', root, [ |
|
||||||
root, |
|
||||||
reply_to_root, |
|
||||||
root2, |
|
||||||
]) |
|
||||||
expect(trees).to.have.length(1) |
|
||||||
expect(trees[0].event.id).to.eq(root.id) |
|
||||||
expect(trees[0].child_nodes).to.have.length(1) |
|
||||||
expect(trees[0].child_nodes[0].event.id).to.eq(reply_to_root.id) |
|
||||||
expect(trees[0].child_nodes[0].child_nodes).to.be.length(0) |
|
||||||
}) |
|
||||||
}) |
|
||||||
describe('2 roots, 1 reply, 1 revision', () => { |
|
||||||
test('array contains node related to specified event with reply, and revision', async () => { |
|
||||||
const root = await generateEventWithTags([]) |
|
||||||
const root2 = await generateEventWithTags([]) |
|
||||||
const reply_to_root = await generateEventWithTags([ |
|
||||||
['e', root.id, '', 'reply'], |
|
||||||
]) |
|
||||||
const revision_of_root = await generateEventWithTags([ |
|
||||||
['e', root.id, '', 'reply'], |
|
||||||
['t', 'revision-root'], |
|
||||||
]) |
|
||||||
const trees = getThreadTrees('proposal', root, [ |
|
||||||
root, |
|
||||||
reply_to_root, |
|
||||||
root2, |
|
||||||
revision_of_root, |
|
||||||
]) |
|
||||||
expect(trees).to.have.length(2) |
|
||||||
expect(trees[0].event.id).to.eq(root.id) |
|
||||||
expect(trees[0].child_nodes).to.have.length(1) |
|
||||||
expect(trees[0].child_nodes[0].event.id).to.eq(reply_to_root.id) |
|
||||||
expect(trees[1].event.id).to.eq(revision_of_root.id) |
|
||||||
}) |
|
||||||
}) |
|
||||||
}) |
|
||||||
describe('issue', () => { |
|
||||||
describe('2 roots, 1 reply, 1 revision', () => { |
|
||||||
test('array contains only node related to specified event with reply and revision as children', async () => { |
|
||||||
const root = await generateEventWithTags([]) |
|
||||||
const root2 = await generateEventWithTags([]) |
|
||||||
const reply_to_root = await generateEventWithTags([ |
|
||||||
['e', root.id, '', 'reply'], |
|
||||||
]) |
|
||||||
const revision_of_root = await generateEventWithTags([ |
|
||||||
['e', root.id, '', 'reply'], |
|
||||||
['t', 'revision-root'], |
|
||||||
]) |
|
||||||
const trees = getThreadTrees('issue', root, [ |
|
||||||
root, |
|
||||||
reply_to_root, |
|
||||||
root2, |
|
||||||
revision_of_root, |
|
||||||
]) |
|
||||||
expect(trees).to.have.length(1) |
|
||||||
expect(trees[0].event.id).to.eq(root.id) |
|
||||||
expect(trees[0].child_nodes).to.have.length(2) |
|
||||||
expect(trees[0].child_nodes[0].event.id).to.eq(reply_to_root.id) |
|
||||||
expect(trees[0].child_nodes[1].event.id).to.eq(revision_of_root.id) |
|
||||||
}) |
|
||||||
}) |
|
||||||
}) |
|
||||||
}) |
|
||||||
@ -1,70 +0,0 @@ |
|||||||
import type { ThreadTreeNode } from '$lib/components/events/type' |
|
||||||
import type { NDKEvent } from '@nostr-dev-kit/ndk' |
|
||||||
|
|
||||||
export const getParentId = (reply: NDKEvent): string | undefined => { |
|
||||||
const t = |
|
||||||
reply.tags.find((tag) => tag.length === 4 && tag[3] === 'reply') || |
|
||||||
reply.tags.find((tag) => tag.length === 4 && tag[3] === 'root') || |
|
||||||
// include events that don't use nip 10 markers
|
|
||||||
reply.tags.find((tag) => tag.length < 4 && tag[0] === 'e') |
|
||||||
return t ? t[1] : undefined |
|
||||||
} |
|
||||||
|
|
||||||
export const createThreadTree = (replies: NDKEvent[]): ThreadTreeNode[] => { |
|
||||||
const hashTable: { [key: string]: ThreadTreeNode } = Object.create(null) |
|
||||||
replies.forEach( |
|
||||||
(reply) => (hashTable[reply.id] = { event: reply, child_nodes: [] }) |
|
||||||
) |
|
||||||
const thread_tree: ThreadTreeNode[] = [] |
|
||||||
replies.forEach((reply) => { |
|
||||||
const reply_parent_id = getParentId(reply) |
|
||||||
if (reply_parent_id && hashTable[reply_parent_id]) { |
|
||||||
hashTable[reply_parent_id].child_nodes.push(hashTable[reply.id]) |
|
||||||
hashTable[reply_parent_id].child_nodes.sort( |
|
||||||
(a, b) => (a.event.created_at || 0) - (b.event.created_at || 0) |
|
||||||
) |
|
||||||
} else thread_tree.push(hashTable[reply.id]) |
|
||||||
}) |
|
||||||
return thread_tree |
|
||||||
} |
|
||||||
|
|
||||||
export const splitIntoRevisionThreadTrees = ( |
|
||||||
tree: ThreadTreeNode |
|
||||||
): ThreadTreeNode[] => { |
|
||||||
const thread_revision_trees: ThreadTreeNode[] = [ |
|
||||||
{ |
|
||||||
...tree, |
|
||||||
child_nodes: [...tree?.child_nodes], |
|
||||||
}, |
|
||||||
] |
|
||||||
thread_revision_trees[0].child_nodes = [ |
|
||||||
...thread_revision_trees[0].child_nodes.filter((n) => { |
|
||||||
if (n.event.tags.some((t) => t.length > 1 && t[1] === 'revision-root')) { |
|
||||||
thread_revision_trees.push(n) |
|
||||||
return false |
|
||||||
} |
|
||||||
return true |
|
||||||
}), |
|
||||||
] |
|
||||||
return thread_revision_trees.sort( |
|
||||||
(a, b) => (a.event.created_at || 0) - (b.event.created_at || 0) |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
export const getThreadTrees = ( |
|
||||||
type: 'proposal' | 'issue', |
|
||||||
event: NDKEvent | undefined, |
|
||||||
replies: NDKEvent[] | undefined |
|
||||||
): ThreadTreeNode[] => { |
|
||||||
if (event) { |
|
||||||
const all_trees = createThreadTree(replies ? [event, ...replies] : [event]) |
|
||||||
const event_tree = all_trees.find((t) => t.event.id === event.id) |
|
||||||
if (event_tree) { |
|
||||||
// TODO: add 'mentions' and secondary references with a 'metioned event wrapper'
|
|
||||||
if (type === 'proposal') return splitIntoRevisionThreadTrees(event_tree) |
|
||||||
return [event_tree] |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return [] |
|
||||||
} |
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue