This commit is contained in:
Untone 2024-07-26 18:49:15 +03:00
parent 219e3e2325
commit 1e4138e40e
36 changed files with 875 additions and 1058 deletions

1
.gitignore vendored
View File

@ -28,3 +28,4 @@ target
.output .output
.vinxi .vinxi
*.pem *.pem
edge.*

14
.vscode/launch.json vendored
View File

@ -1,14 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch browser against localhost",
"type": "chrome",
"request": "launch",
"url": "https://localhost:3000",
"webRoot": "${workspaceFolder}/src",
"sourceMaps": true,
"trace": true
}
]
}

View File

@ -30,12 +30,11 @@ export default defineConfig({
https: true https: true
}, },
devOverlay: true, devOverlay: true,
vite: {
build: { build: {
sourcemap: true,
chunkSizeWarningLimit: 1024, chunkSizeWarningLimit: 1024,
target: 'esnext' target: 'esnext'
}, },
vite: {
envPrefix: 'PUBLIC_', envPrefix: 'PUBLIC_',
plugins: [!isVercel && mkcert(), nodePolyfills(polyfillOptions), sassDts()], plugins: [!isVercel && mkcert(), nodePolyfills(polyfillOptions), sassDts()],
css: { css: {

View File

@ -11,7 +11,7 @@ generates:
skipTypename: true skipTypename: true
useTypeImports: true useTypeImports: true
outputPath: './src/graphql/types/chat.gen.ts' outputPath: './src/graphql/types/chat.gen.ts'
# namingConvention: lodash#pascalCase # namingConvention: change-case#CamelCase # for generated types
# Generate types for core # Generate types for core
src/graphql/schema/core.gen.ts: src/graphql/schema/core.gen.ts:
@ -24,7 +24,7 @@ generates:
skipTypename: true skipTypename: true
useTypeImports: true useTypeImports: true
outputPath: './src/graphql/types/core.gen.ts' outputPath: './src/graphql/types/core.gen.ts'
# namingConvention: lodash#pascalCase # namingConvention: change-case#CamelCase # for generated types
hooks: hooks:
afterAllFileWrite: afterAllFileWrite:
- prettier --ignore-path .gitignore --write --plugin-search-dir=. src/graphql/schema/*.gen.ts - prettier --ignore-path .gitignore --write --plugin-search-dir=. src/graphql/schema/*.gen.ts

380
package-lock.json generated
View File

@ -21,7 +21,7 @@
"@graphql-codegen/typescript-operations": "^4.2.3", "@graphql-codegen/typescript-operations": "^4.2.3",
"@graphql-codegen/typescript-urql": "^4.0.0", "@graphql-codegen/typescript-urql": "^4.0.0",
"@hocuspocus/provider": "^2.13.5", "@hocuspocus/provider": "^2.13.5",
"@playwright/test": "^1.45.2", "@playwright/test": "^1.45.3",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"@solid-primitives/media": "^2.2.9", "@solid-primitives/media": "^2.2.9",
"@solid-primitives/memo": "^1.3.9", "@solid-primitives/memo": "^1.3.9",
@ -33,35 +33,35 @@
"@solidjs/meta": "^0.29.4", "@solidjs/meta": "^0.29.4",
"@solidjs/router": "^0.14.1", "@solidjs/router": "^0.14.1",
"@solidjs/start": "^1.0.6", "@solidjs/start": "^1.0.6",
"@tiptap/core": "^2.5.4", "@tiptap/core": "^2.5.5",
"@tiptap/extension-blockquote": "^2.5.4", "@tiptap/extension-blockquote": "^2.5.5",
"@tiptap/extension-bold": "^2.5.4", "@tiptap/extension-bold": "^2.5.5",
"@tiptap/extension-bubble-menu": "^2.5.4", "@tiptap/extension-bubble-menu": "^2.5.5",
"@tiptap/extension-bullet-list": "^2.5.4", "@tiptap/extension-bullet-list": "^2.5.5",
"@tiptap/extension-character-count": "^2.5.4", "@tiptap/extension-character-count": "^2.5.5",
"@tiptap/extension-collaboration": "^2.5.4", "@tiptap/extension-collaboration": "^2.5.5",
"@tiptap/extension-collaboration-cursor": "^2.5.4", "@tiptap/extension-collaboration-cursor": "^2.5.5",
"@tiptap/extension-document": "^2.5.4", "@tiptap/extension-document": "^2.5.5",
"@tiptap/extension-dropcursor": "^2.5.4", "@tiptap/extension-dropcursor": "^2.5.5",
"@tiptap/extension-floating-menu": "^2.5.4", "@tiptap/extension-floating-menu": "^2.5.5",
"@tiptap/extension-focus": "^2.5.4", "@tiptap/extension-focus": "^2.5.5",
"@tiptap/extension-gapcursor": "^2.5.4", "@tiptap/extension-gapcursor": "^2.5.5",
"@tiptap/extension-hard-break": "^2.5.4", "@tiptap/extension-hard-break": "^2.5.5",
"@tiptap/extension-heading": "^2.5.4", "@tiptap/extension-heading": "^2.5.5",
"@tiptap/extension-highlight": "^2.5.4", "@tiptap/extension-highlight": "^2.5.5",
"@tiptap/extension-history": "^2.5.4", "@tiptap/extension-history": "^2.5.5",
"@tiptap/extension-horizontal-rule": "^2.5.4", "@tiptap/extension-horizontal-rule": "^2.5.5",
"@tiptap/extension-image": "^2.5.4", "@tiptap/extension-image": "^2.5.5",
"@tiptap/extension-italic": "^2.5.4", "@tiptap/extension-italic": "^2.5.5",
"@tiptap/extension-link": "^2.5.4", "@tiptap/extension-link": "^2.5.5",
"@tiptap/extension-list-item": "^2.5.4", "@tiptap/extension-list-item": "^2.5.5",
"@tiptap/extension-ordered-list": "^2.5.4", "@tiptap/extension-ordered-list": "^2.5.5",
"@tiptap/extension-paragraph": "^2.5.4", "@tiptap/extension-paragraph": "^2.5.5",
"@tiptap/extension-placeholder": "^2.5.4", "@tiptap/extension-placeholder": "^2.5.5",
"@tiptap/extension-strike": "^2.5.4", "@tiptap/extension-strike": "^2.5.5",
"@tiptap/extension-text": "^2.5.4", "@tiptap/extension-text": "^2.5.5",
"@tiptap/extension-underline": "^2.5.4", "@tiptap/extension-underline": "^2.5.5",
"@tiptap/extension-youtube": "^2.5.4", "@tiptap/extension-youtube": "^2.5.5",
"@types/cookie": "^0.6.0", "@types/cookie": "^0.6.0",
"@types/cookie-signature": "^1.1.2", "@types/cookie-signature": "^1.1.2",
"@types/node": "^20.14.11", "@types/node": "^20.14.11",
@ -98,7 +98,7 @@
"swiper": "^11.1.5", "swiper": "^11.1.5",
"throttle-debounce": "^5.0.2", "throttle-debounce": "^5.0.2",
"tslib": "^2.6.3", "tslib": "^2.6.3",
"typescript": "^5.5.3", "typescript": "^5.5.4",
"typograf": "^7.4.1", "typograf": "^7.4.1",
"uniqolor": "^1.1.1", "uniqolor": "^1.1.1",
"vinxi": "^0.4.1", "vinxi": "^0.4.1",
@ -2604,9 +2604,9 @@
} }
}, },
"node_modules/@graphql-tools/delegate": { "node_modules/@graphql-tools/delegate": {
"version": "10.0.14", "version": "10.0.15",
"resolved": "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-10.0.14.tgz", "resolved": "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-10.0.15.tgz",
"integrity": "sha512-mYrLtwVKTHg5F4OFrJbiL5F7dzopzGiac5ezkVrnlGNPBQ8GNCr1zo32c1rYyIbsa8fJSUvAJfJfFj6ipnutnw==", "integrity": "sha512-18R4vcJWz/6pk6K9SslijR0jCSe0mAnSs0sd1eioTvSSCWiagPdCOOhaM9dPNfEnxp3TRHg3cnYqywRtJgKHvw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -4148,13 +4148,13 @@
} }
}, },
"node_modules/@playwright/test": { "node_modules/@playwright/test": {
"version": "1.45.2", "version": "1.45.3",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.2.tgz", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.3.tgz",
"integrity": "sha512-JxG9eq92ET75EbVi3s+4sYbcG7q72ECeZNbdBlaMkGcNbiDQ4cAi8U2QP5oKkOx+1gpaiL1LDStmzCaEM1Z6fQ==", "integrity": "sha512-UKF4XsBfy+u3MFWEH44hva1Q8Da28G6RFtR2+5saw+jgAFQV5yYnB1fu68Mz7fO+5GJF3wgwAIs0UelU8TxFrA==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright": "1.45.2" "playwright": "1.45.3"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -4956,9 +4956,9 @@
} }
}, },
"node_modules/@tiptap/core": { "node_modules/@tiptap/core": {
"version": "2.5.4", "version": "2.5.5",
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.5.4.tgz", "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.5.5.tgz",
"integrity": "sha512-Zs/hShr4+W02+0nOlpmr5cS2YjDRLqd+XMt+jsiQH0QNr3s1Lc82pfF6C3CjgLEZtdUzImZrW2ABtLlpvbogaA==", "integrity": "sha512-VnAnyWnsqN65QijtUFHbe7EPSJCkhNEAwlatsG/HvrZvUv9KmoWWbMsHAU73wozKzPXR3nHRbCxN+LuxP5bADg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@ -4966,13 +4966,13 @@
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/pm": "^2.5.4" "@tiptap/pm": "^2.5.5"
} }
}, },
"node_modules/@tiptap/extension-blockquote": { "node_modules/@tiptap/extension-blockquote": {
"version": "2.5.4", "version": "2.5.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-2.5.4.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-2.5.5.tgz",
"integrity": "sha512-UqeJunZM3IiCQGZE0X5YNUOWYkuIieqrwPgOEghAIjnhDcQizQcouRQ5R7cwwv/scNr2JvZHncOTLrALV3Janw==", "integrity": "sha512-K+fc++ASlgDRHN6i3j3JBGzWiDhhoZv0jCUB/l7Jzut4UfjIoWqKhmJajnp95Qu9tmwQUy9LMzHqG4G5wUsIsQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@ -4980,13 +4980,13 @@
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "^2.5.4" "@tiptap/core": "^2.5.5"
} }
}, },
"node_modules/@tiptap/extension-bold": { "node_modules/@tiptap/extension-bold": {
"version": "2.5.4", "version": "2.5.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-2.5.4.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-2.5.5.tgz",
"integrity": "sha512-H5sjqloFMjq7VOSfE+U4T7dqGoflOiF6RW6/gZm/U6KYeHG2/bG0ktq7mWAnnhbiKiy7gUcxyJCV+ILdGX9C5g==", "integrity": "sha512-vXqaeTKy4nf4X+s7NkFt0OsuS1eKMQhrdt7SzACf0gWi3M761WGkaKHy8XUlo7zhWhqHtkgey53Gaw0nbEY54Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@ -4994,13 +4994,13 @@
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "^2.5.4" "@tiptap/core": "^2.5.5"
} }
}, },
"node_modules/@tiptap/extension-bubble-menu": { "node_modules/@tiptap/extension-bubble-menu": {
"version": "2.5.4", "version": "2.5.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.5.4.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.5.5.tgz",
"integrity": "sha512-GHwef912K1yd75pp9JGDnKSp1DvdOHH8BcHQv0no+a3q2ePFPYcgaSwVRR59jHRX9WzdVfoLcqDSAeoNGOrISw==", "integrity": "sha512-7k0HqrnhQGVZk86MEc5vt8stNRxIY65AMjZfszY/mQw0Dza7EQig/9b/AEmi9n+TNW5/8Qu+OMJD9ln92d/Eog==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -5011,14 +5011,14 @@
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "^2.5.4", "@tiptap/core": "^2.5.5",
"@tiptap/pm": "^2.5.4" "@tiptap/pm": "^2.5.5"
} }
}, },
"node_modules/@tiptap/extension-bullet-list": { "node_modules/@tiptap/extension-bullet-list": {
"version": "2.5.4", "version": "2.5.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.5.4.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.5.5.tgz",
"integrity": "sha512-aAfpALeD6OxymkbtrzDqbgkAkzVVHudxOb8GsK1N6m42nFL7Q9JzHJ5/8KzB+xi25CcIbS+HmXJkRIQJXgNbSA==", "integrity": "sha512-p89cTmGUoq3OEFzcS49iQ/tyQjDoKW1J0c7EghS7eU3wHVxeo/Ke110cY2W5o1e4KMFowo3a4jVsxKuCQJkWrA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@ -5026,13 +5026,13 @@
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "^2.5.4" "@tiptap/core": "^2.5.5"
} }
}, },
"node_modules/@tiptap/extension-character-count": { "node_modules/@tiptap/extension-character-count": {
"version": "2.5.4", "version": "2.5.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-character-count/-/extension-character-count-2.5.4.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-character-count/-/extension-character-count-2.5.5.tgz",
"integrity": "sha512-6qwt+81I+y+t3eoFPmCG2ouQce2RccwyiUC0ZOPTG1eUB+5yXmyIwBYI4aOM4TEfxNizyaZtQw32CDdAhMr3YA==", "integrity": "sha512-rh6q3YeuLV8PnaKUqQbnOQ16obXPcqsqnQ+y1XLWH74lHwdvbOvE1BCvSZD0ULPI9EcOtvhdZEZkDxlqQ9H3jg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@ -5040,14 +5040,14 @@
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "^2.5.4", "@tiptap/core": "^2.5.5",
"@tiptap/pm": "^2.5.4" "@tiptap/pm": "^2.5.5"
} }
}, },
"node_modules/@tiptap/extension-collaboration": { "node_modules/@tiptap/extension-collaboration": {
"version": "2.5.4", "version": "2.5.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-collaboration/-/extension-collaboration-2.5.4.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-collaboration/-/extension-collaboration-2.5.5.tgz",
"integrity": "sha512-CpQdbr7XpQaVqRFo/A1DchrQZMDb8vrkP+FcUIgvHN0b8hwKDmXRAHDtuk8yTTEatW1EqpX8lx8UxaUTcDNbIg==", "integrity": "sha512-HpDW+1VTKdtK7BglQNLFv2UzJIxtzZ9zvT+wdYDWPB3ZstoL8drpp4wGP2xt3tbki6wzGpUFkDCpVNl0oOunXQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@ -5055,15 +5055,15 @@
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "^2.5.4", "@tiptap/core": "^2.5.5",
"@tiptap/pm": "^2.5.4", "@tiptap/pm": "^2.5.5",
"y-prosemirror": "^1.2.6" "y-prosemirror": "^1.2.6"
} }
}, },
"node_modules/@tiptap/extension-collaboration-cursor": { "node_modules/@tiptap/extension-collaboration-cursor": {
"version": "2.5.4", "version": "2.5.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-collaboration-cursor/-/extension-collaboration-cursor-2.5.4.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-collaboration-cursor/-/extension-collaboration-cursor-2.5.5.tgz",
"integrity": "sha512-M32JChnP5RVdr1n+Tf0gF9bxx0gHvc0uV4SDxCMN3uaNH5YpcofmvKElS60rDGVfCdRTId/aj7P3AtwrvRlYdQ==", "integrity": "sha512-DWX3eOplWyLegOWeZa0CAVbb9/UYbngiZyKjVMpDlx5qzhUuLL+Df54/UGKqB1ZrBZrxKCVQE3APMyXkxI/2VQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@ -5071,14 +5071,14 @@
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "^2.5.4", "@tiptap/core": "^2.5.5",
"y-prosemirror": "^1.2.6" "y-prosemirror": "^1.2.6"
} }
}, },
"node_modules/@tiptap/extension-document": { "node_modules/@tiptap/extension-document": {
"version": "2.5.4", "version": "2.5.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.5.4.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.5.5.tgz",
"integrity": "sha512-4RDrhASxCTOZETYhIhEW1TfZqx3Tm+LQxouvBMFyODmT1PSgsg5Xz1FYpDPr+J49bGAK0Pr9ae0XcGW011L3sA==", "integrity": "sha512-MIjYO63JepcJW37PQuKVmYuZFqkQOZ/12tV0YLU4o6gmGVdqJS0+3md9CdnyUFUDIo7x6TBh8r5i5L2xQpm3Sg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@ -5086,13 +5086,13 @@
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "^2.5.4" "@tiptap/core": "^2.5.5"
} }
}, },
"node_modules/@tiptap/extension-dropcursor": { "node_modules/@tiptap/extension-dropcursor": {
"version": "2.5.4", "version": "2.5.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.5.4.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.5.5.tgz",
"integrity": "sha512-jzSnuuYhlc0SsHvAteWkE9TJy3eRwkxQs4MO2JxALOzJECN4G82nlX8vciihBD6xf7lVgVSBACejK9+rsTHqCg==", "integrity": "sha512-+K/qd115c3zFgHdvxtOkZhSTKNyPpjM0Np2v4cehqn0j+/3stOMGlAH2Jm/b2L8RylFKGtQP1b/1wsKY5feuAg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@ -5100,14 +5100,14 @@
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "^2.5.4", "@tiptap/core": "^2.5.5",
"@tiptap/pm": "^2.5.4" "@tiptap/pm": "^2.5.5"
} }
}, },
"node_modules/@tiptap/extension-floating-menu": { "node_modules/@tiptap/extension-floating-menu": {
"version": "2.5.4", "version": "2.5.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.5.4.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.5.5.tgz",
"integrity": "sha512-EqD4rgi3UhnDcV3H1+ndAS4Ue2zpsU7hFKoevOIV6GS7xVnWN70AGt6swH24QzuHKKISFtWoLpKjrwRORNIxuA==", "integrity": "sha512-1mgpxZGfy1ziNSvWz6m1nGb9ZF9fVVz4X4XwrIqwGw1Vqt9oXflm6puglnzwVLDeaMDT014VUfczJ4My3wDZzA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -5118,14 +5118,14 @@
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "^2.5.4", "@tiptap/core": "^2.5.5",
"@tiptap/pm": "^2.5.4" "@tiptap/pm": "^2.5.5"
} }
}, },
"node_modules/@tiptap/extension-focus": { "node_modules/@tiptap/extension-focus": {
"version": "2.5.4", "version": "2.5.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-focus/-/extension-focus-2.5.4.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-focus/-/extension-focus-2.5.5.tgz",
"integrity": "sha512-/Iq++93f9S+bNJzj3OmgOydCO58VfAhmnsImbGK/GmxV39hHbgJdazxMugwdQlvrY/oe3+Y+WY8ZI1WlWwTJ4g==", "integrity": "sha512-c5ul5PNl/2HcYwEPu1kjjs/u8N5BtLnreeUyb223y8i4BEcjydVlnCfVVUdonQIWnj0mKQ8KZbyLTSYdijDsVA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@ -5133,14 +5133,14 @@
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "^2.5.4", "@tiptap/core": "^2.5.5",
"@tiptap/pm": "^2.5.4" "@tiptap/pm": "^2.5.5"
} }
}, },
"node_modules/@tiptap/extension-gapcursor": { "node_modules/@tiptap/extension-gapcursor": {
"version": "2.5.4", "version": "2.5.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.5.4.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.5.5.tgz",
"integrity": "sha512-wzTh1piODZBS0wmuDgPjjg8PQwclYa5LssnxDIo9pDSnt4l3AfHSAJIJSGIfgt96KnzF1wqRTRpe08qNa1n7/g==", "integrity": "sha512-An/HwTheUP+D4UU1GVy2e4ypqA1TanZ7haNcm5WB+wSZQo6UNPIszIa49TTGenkk86hP2DH9cQSlTREsyAW6wg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@ -5148,14 +5148,14 @@
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "^2.5.4", "@tiptap/core": "^2.5.5",
"@tiptap/pm": "^2.5.4" "@tiptap/pm": "^2.5.5"
} }
}, },
"node_modules/@tiptap/extension-hard-break": { "node_modules/@tiptap/extension-hard-break": {
"version": "2.5.4", "version": "2.5.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.5.4.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.5.5.tgz",
"integrity": "sha512-nLn6HP9tqgdGGwbMORXVtcY30DTGctYFaWADRthvBjVgacYSeKlhUcsSu3YgaxtbxZp6BhfRvD2kKrxyQsSjnQ==", "integrity": "sha512-VtrwKU0LYS/0rfH5rGz8ztKwA0bsHRyBF53G7aP2FS4BiN8aOEu8t7VkvBZAewXDITDah9K6rqfXk+MNwoul2Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@ -5163,13 +5163,13 @@
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "^2.5.4" "@tiptap/core": "^2.5.5"
} }
}, },
"node_modules/@tiptap/extension-heading": { "node_modules/@tiptap/extension-heading": {
"version": "2.5.4", "version": "2.5.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-2.5.4.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-2.5.5.tgz",
"integrity": "sha512-DuAB58/e7eho1rkyad0Z/SjW+EB+H2hRqHlswEeZZYhBTjzey5UmBwkMWTGC/SQiRisx1xYQYTd8T0fiABi5hw==", "integrity": "sha512-NDnXOR6HmnkBA68oZTVf0BT5t8ikVFv9X6Ft/O5oU6IuzCswS8BUb5MJIhKBWQXJTsCNbC6EYl5jhJ3hukLcHw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@ -5177,13 +5177,13 @@
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "^2.5.4" "@tiptap/core": "^2.5.5"
} }
}, },
"node_modules/@tiptap/extension-highlight": { "node_modules/@tiptap/extension-highlight": {
"version": "2.5.4", "version": "2.5.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-highlight/-/extension-highlight-2.5.4.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-highlight/-/extension-highlight-2.5.5.tgz",
"integrity": "sha512-TSYnFBluZu1YQdTCyXl2wuxFuhFUYFzbaV0f1wq2P2Nc8U2OiiuaNz+QggHw5Hf3ILzkRxQCUQnq97Q/5smMwQ==", "integrity": "sha512-NqMmL9/82288DI1trnuxB3hcf61x+iDKFvNAE+thW6MmY6ZWi47bEnfUQGwDeInxH81NfMhTTSxuXmnuO10noQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@ -5191,13 +5191,13 @@
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "^2.5.4" "@tiptap/core": "^2.5.5"
} }
}, },
"node_modules/@tiptap/extension-history": { "node_modules/@tiptap/extension-history": {
"version": "2.5.4", "version": "2.5.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.5.4.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.5.5.tgz",
"integrity": "sha512-WB1fZYGIlpahAD6Ba+mj9vIb1tk8S3TsADXDFKxLVpZWZPQ+B7duGJP7g/vRH2XAXEs836JzC2oxjKeaop3k7A==", "integrity": "sha512-CYxFpE9wayc+iZQIlXd3cbq47WP+KqjDhprbKF5Tb7+WoWLS2FB5WK3n+r/SrcoIaslIt5SYDRQPzx4fS3N7LA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@ -5205,14 +5205,14 @@
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "^2.5.4", "@tiptap/core": "^2.5.5",
"@tiptap/pm": "^2.5.4" "@tiptap/pm": "^2.5.5"
} }
}, },
"node_modules/@tiptap/extension-horizontal-rule": { "node_modules/@tiptap/extension-horizontal-rule": {
"version": "2.5.4", "version": "2.5.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.5.4.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.5.5.tgz",
"integrity": "sha512-uXLDe/iyzQbyfDkJ8kE5XaAkY3EOcbTFLjbueqGlkbWtjJgy+3LysGvh8fQj8PAOaIBMaFRFhTq7GMbW2ebRog==", "integrity": "sha512-8oV0oLgGwJqr44wk7+bHxTAenR0bvk9aVdmE/owg1oy2tkSX0bwtvQEOnwwxtfPJGTwq8JGhefUGYcpHfG2YYg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@ -5220,14 +5220,14 @@
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "^2.5.4", "@tiptap/core": "^2.5.5",
"@tiptap/pm": "^2.5.4" "@tiptap/pm": "^2.5.5"
} }
}, },
"node_modules/@tiptap/extension-image": { "node_modules/@tiptap/extension-image": {
"version": "2.5.4", "version": "2.5.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-2.5.4.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-2.5.5.tgz",
"integrity": "sha512-4ySSP7iPsbbo1SlPJYj546TKettuO6FGY5MQKxH8AGnZWyQGZYl89GpU1iGFAaeHq4dKUemM5D3ikgSynEQLow==", "integrity": "sha512-DvnKf3XCGf/2GQrqtwgKwgaeqIn2dXgHTire0E2aPj8T939jA4ApX5qLPumndHX0rAckX5VAbnJjQeoxtEmMFw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@ -5235,13 +5235,13 @@
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "^2.5.4" "@tiptap/core": "^2.5.5"
} }
}, },
"node_modules/@tiptap/extension-italic": { "node_modules/@tiptap/extension-italic": {
"version": "2.5.4", "version": "2.5.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.5.4.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.5.5.tgz",
"integrity": "sha512-TAhtl/fNBgv1elzF3HWES8uwVdpKBSYrq1e6yeYfj74mQn//3ksvdhWQrLzc1e+zcoHbk1PeOp/5ODdPuZ6tkg==", "integrity": "sha512-PEeI68/u7Bm4n4xIcxVAV12jPhEa72fpHRnYfJe4CGp4x8mJfz/dowKN/P0/6CfjROB7Q8rY26u5E9fS+Cg73w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@ -5249,13 +5249,13 @@
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "^2.5.4" "@tiptap/core": "^2.5.5"
} }
}, },
"node_modules/@tiptap/extension-link": { "node_modules/@tiptap/extension-link": {
"version": "2.5.4", "version": "2.5.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-2.5.4.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-2.5.5.tgz",
"integrity": "sha512-xTB/+T6SHHCXInJni8WdqOfF40a/MiFUf5OoWW9cPrApx3I7TzJ9j8/WDshM0BOnDDw80w1bl9F2zkUQjC0Y2A==", "integrity": "sha512-zVpNvMD8R9uW1SX1PJoj3fLyOHwuFWqiqEHN2KWfLbEnbL/KXNnpIyKdpHnI9lqFrsMf2dmyZCS3R6xIrynviQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -5266,14 +5266,14 @@
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "^2.5.4", "@tiptap/core": "^2.5.5",
"@tiptap/pm": "^2.5.4" "@tiptap/pm": "^2.5.5"
} }
}, },
"node_modules/@tiptap/extension-list-item": { "node_modules/@tiptap/extension-list-item": {
"version": "2.5.4", "version": "2.5.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.5.4.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.5.5.tgz",
"integrity": "sha512-bPxUCFt9HnAfoaZQgwqCfRAZ6L3QlYhIRDDbOvZag7IxCdQuZmeY4k5OZfQIGijNDTag7CN9cdL4fl9rnm6/sQ==", "integrity": "sha512-CfNVCP8Pqqgr7fAQAuRvZikzXT9vCEogcW7/C16cyGykbUJBqBmpsyHcAlj7XwsBFUuJ5MCeULtk/0frUI5fMQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@ -5281,13 +5281,13 @@
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "^2.5.4" "@tiptap/core": "^2.5.5"
} }
}, },
"node_modules/@tiptap/extension-ordered-list": { "node_modules/@tiptap/extension-ordered-list": {
"version": "2.5.4", "version": "2.5.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.5.4.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.5.5.tgz",
"integrity": "sha512-cl3cTJitY6yDUmxqgjDUtDWCyX1VVsZNJ6i9yiPeARcxvzFc81KmUJxTGl8WPT5TjqmM+TleRkZjsxgvXX57+Q==", "integrity": "sha512-wElnGQJhKznayP7tVGl/r42mj1dLEeU+Ln1Y3wF/m+nFwKl2Gpsy01PjBy5sXPUgskGSWgMlOgJrQyMvH9AuAw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@ -5295,13 +5295,13 @@
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "^2.5.4" "@tiptap/core": "^2.5.5"
} }
}, },
"node_modules/@tiptap/extension-paragraph": { "node_modules/@tiptap/extension-paragraph": {
"version": "2.5.4", "version": "2.5.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.5.4.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.5.5.tgz",
"integrity": "sha512-pC1YIkkRPXoU0eDrhfAf8ZrFJQzvw2ftP6KRhLnnSw/Ot1DOjT1r95l7zsFefS9oCDMT/L4HghTAiPZ4rcpPbg==", "integrity": "sha512-XZO1rqsU1vlt9qeG2pVVAt2gXjD0twl2D+uxy4Nw6gxqbhSgfbNq3RP72mmtcS4KyFJi7ETANpcRpb8ZNvXfmg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@ -5309,13 +5309,13 @@
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "^2.5.4" "@tiptap/core": "^2.5.5"
} }
}, },
"node_modules/@tiptap/extension-placeholder": { "node_modules/@tiptap/extension-placeholder": {
"version": "2.5.4", "version": "2.5.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-2.5.4.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-2.5.5.tgz",
"integrity": "sha512-mcj4j2Z/L1H5dzWHbbWChuAdJK9F2p06fcjqL4iyJtVx38QQFzCdVmGaTAim8CLp/EynbAOYJ5gk9w2PTdv7+w==", "integrity": "sha512-SwWLYdyrMeoVUQdivkIJ4kkAcb38pykxSetlrXitfUmnkwv0/fi+p76Rickf+roudWPsfzqvgvJ4gT6OAOJrGA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@ -5323,14 +5323,14 @@
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "^2.5.4", "@tiptap/core": "^2.5.5",
"@tiptap/pm": "^2.5.4" "@tiptap/pm": "^2.5.5"
} }
}, },
"node_modules/@tiptap/extension-strike": { "node_modules/@tiptap/extension-strike": {
"version": "2.5.4", "version": "2.5.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-2.5.4.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-2.5.5.tgz",
"integrity": "sha512-OSN6ePbCwEhi3hYZZOPow/P9Ym2Kv3NhVbUvasjZCiqQuk8TGc33xirPWl9DTjb/BLfL66TtJ2tKUEVOKl5dKg==", "integrity": "sha512-xnVdSsP7+4yQ1E+rI77ZHvzDH1Gwe2Ty1tgXeOaLjt3RfeVx4xy75o09yHzab6J4hgPebonoXKbZV0JVTGnjtQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@ -5338,13 +5338,13 @@
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "^2.5.4" "@tiptap/core": "^2.5.5"
} }
}, },
"node_modules/@tiptap/extension-text": { "node_modules/@tiptap/extension-text": {
"version": "2.5.4", "version": "2.5.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.5.4.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.5.5.tgz",
"integrity": "sha512-+3x/hYqhmCYbvedCcQzQHFtZ5MAcMOlKuczomZtygf8AfDfuQVrG1m4GoJyNzJdqxjN80/xq4e2vDVvqQxYTCw==", "integrity": "sha512-8c/hxcw7t/S3iKGSFwGNxC2I6AkKpRiySQJ95ML2miwSOAxWhnltoYYV7gobWCRgm25lnvzX/Z6BdpFzXBrBKA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@ -5352,13 +5352,13 @@
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "^2.5.4" "@tiptap/core": "^2.5.5"
} }
}, },
"node_modules/@tiptap/extension-underline": { "node_modules/@tiptap/extension-underline": {
"version": "2.5.4", "version": "2.5.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-2.5.4.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-2.5.5.tgz",
"integrity": "sha512-o8T3oWbniA3rLo6LkslPRF8pwdjsaHXJCeK4KmKeCyYhTpMfjypT3uptd+VSSJ4iQkaiFInKeIUOBqqEQ9cADw==", "integrity": "sha512-3uog8d4G/AdqaJC8qutIIgkYnU2TfXW3QbtEy0Yg2WdjCz97bWXkFkNhhVZM/hvXjFCbYboRN5HLcIHl8+Zgmg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@ -5366,13 +5366,13 @@
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "^2.5.4" "@tiptap/core": "^2.5.5"
} }
}, },
"node_modules/@tiptap/extension-youtube": { "node_modules/@tiptap/extension-youtube": {
"version": "2.5.4", "version": "2.5.5",
"resolved": "https://registry.npmjs.org/@tiptap/extension-youtube/-/extension-youtube-2.5.4.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-youtube/-/extension-youtube-2.5.5.tgz",
"integrity": "sha512-iHcvXOA32MZsVJTT7mvZ1CWKUo2quQMQXfBniizLm0lUG1ftSioqnDuXy4kEjeCBR2cnZr3yph6tbG/pF0RcHg==", "integrity": "sha512-dPLSLsEiMdXB5q0YDRJKWiiTqdFiSeyaC5qWLio4SHYfyTYT1+M2Wwox+5Dm/OSgCHpxpT2W8JRt+H4+P38t9A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@ -5380,13 +5380,13 @@
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "^2.5.4" "@tiptap/core": "^2.5.5"
} }
}, },
"node_modules/@tiptap/pm": { "node_modules/@tiptap/pm": {
"version": "2.5.4", "version": "2.5.5",
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.5.4.tgz", "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.5.5.tgz",
"integrity": "sha512-oFIsuniptdUXn93x4aM2sVN3hYKo9Fj55zAkYrWhwxFYUYcPxd5ibra2we+wRK5TaiPu098wpC+yMSTZ/KKMpA==", "integrity": "sha512-ppePiLaeG6IKkm8Yq+mRENT4LIAS4qQyLT8EnKadznaTL6SNj/72mm0MjD44URkM38ySzIyvt/vqHDapNK0Hww==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true, "peer": true,
@ -7057,9 +7057,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001642", "version": "1.0.30001643",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001642.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001643.tgz",
"integrity": "sha512-3XQ0DoRgLijXJErLSl+bLnJ+Et4KqV1PY6JJBGAFlsNsz31zeAIncyeZfLCabHK/jtSh+671RM9YMldxjUPZtA==", "integrity": "sha512-ERgWGNleEilSrHM6iUz/zJNSQTP8Mr21wDWpdgvRwcTXGAq6jMtOUPP4dqFPTdKqZ2wKTdtB+uucZ3MRpAUSmg==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -8441,9 +8441,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.4.832", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.832.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.0.tgz",
"integrity": "sha512-cTen3SB0H2SGU7x467NRe1eVcQgcuS6jckKfWJHia2eo0cHIGOqHoAxevIYZD4eRHcWjkvFzo93bi3vJ9W+1lA==", "integrity": "sha512-Vb3xHHYnLseK8vlMJQKJYXJ++t4u1/qJ3vykuVrVjvdiOEhYyT1AuP4x03G8EnPmYvYOhe9T+dADTmthjRQMkA==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
@ -11898,9 +11898,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/node-releases": { "node_modules/node-releases": {
"version": "2.0.17", "version": "2.0.18",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.17.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
"integrity": "sha512-Ww6ZlOiEQfPfXM45v17oabk77Z7mg5bOt7AjDyzy7RjK9OrLrLC8dyZQoAPEOtFX9SaNf1Tdvr5gRJWdTJj7GA==", "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -12682,13 +12682,13 @@
} }
}, },
"node_modules/playwright": { "node_modules/playwright": {
"version": "1.45.2", "version": "1.45.3",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.2.tgz", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.3.tgz",
"integrity": "sha512-ReywF2t/0teRvNBpfIgh5e4wnrI/8Su8ssdo5XsQKpjxJj+jspm00jSoz9BTg91TT0c9HRjXO7LBNVrgYj9X0g==", "integrity": "sha512-QhVaS+lpluxCaioejDZ95l4Y4jSFCsBvl2UZkpeXlzxmqS+aABr5c82YmfMHrL6x27nvrvykJAFpkzT2eWdJww==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.45.2" "playwright-core": "1.45.3"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -12701,9 +12701,9 @@
} }
}, },
"node_modules/playwright-core": { "node_modules/playwright-core": {
"version": "1.45.2", "version": "1.45.3",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.2.tgz", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.3.tgz",
"integrity": "sha512-ha175tAWb0dTK0X4orvBIqi3jGEt701SMxMhyujxNrgd8K0Uy5wMSwwcQHtyB4om7INUkfndx02XnQ2p6dvLDw==", "integrity": "sha512-+ym0jNbcjikaOwwSZycFbwkWgfruWvYlJfThKYAlImbxUgdWFO2oW70ojPm4OpE4t6TAo2FY/smM+hpVTtkhDA==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
@ -13891,9 +13891,9 @@
} }
}, },
"node_modules/sass/node_modules/immutable": { "node_modules/sass/node_modules/immutable": {
"version": "4.3.6", "version": "4.3.7",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz",
"integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==", "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -15196,9 +15196,9 @@
} }
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.5.3", "version": "5.5.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
"integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {

View File

@ -1,7 +1,7 @@
{ {
"name": "discoursio-webapp", "name": "discoursio-webapp",
"private": true, "private": true,
"version": "0.9.6", "version": "0.9.5",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vinxi dev", "dev": "vinxi dev",
@ -25,7 +25,7 @@
"@graphql-codegen/typescript-operations": "^4.2.3", "@graphql-codegen/typescript-operations": "^4.2.3",
"@graphql-codegen/typescript-urql": "^4.0.0", "@graphql-codegen/typescript-urql": "^4.0.0",
"@hocuspocus/provider": "^2.13.5", "@hocuspocus/provider": "^2.13.5",
"@playwright/test": "^1.45.2", "@playwright/test": "^1.45.3",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"@solid-primitives/media": "^2.2.9", "@solid-primitives/media": "^2.2.9",
"@solid-primitives/memo": "^1.3.9", "@solid-primitives/memo": "^1.3.9",
@ -37,35 +37,35 @@
"@solidjs/meta": "^0.29.4", "@solidjs/meta": "^0.29.4",
"@solidjs/router": "^0.14.1", "@solidjs/router": "^0.14.1",
"@solidjs/start": "^1.0.6", "@solidjs/start": "^1.0.6",
"@tiptap/core": "^2.5.4", "@tiptap/core": "^2.5.5",
"@tiptap/extension-blockquote": "^2.5.4", "@tiptap/extension-blockquote": "^2.5.5",
"@tiptap/extension-bold": "^2.5.4", "@tiptap/extension-bold": "^2.5.5",
"@tiptap/extension-bubble-menu": "^2.5.4", "@tiptap/extension-bubble-menu": "^2.5.5",
"@tiptap/extension-bullet-list": "^2.5.4", "@tiptap/extension-bullet-list": "^2.5.5",
"@tiptap/extension-character-count": "^2.5.4", "@tiptap/extension-character-count": "^2.5.5",
"@tiptap/extension-collaboration": "^2.5.4", "@tiptap/extension-collaboration": "^2.5.5",
"@tiptap/extension-collaboration-cursor": "^2.5.4", "@tiptap/extension-collaboration-cursor": "^2.5.5",
"@tiptap/extension-document": "^2.5.4", "@tiptap/extension-document": "^2.5.5",
"@tiptap/extension-dropcursor": "^2.5.4", "@tiptap/extension-dropcursor": "^2.5.5",
"@tiptap/extension-floating-menu": "^2.5.4", "@tiptap/extension-floating-menu": "^2.5.5",
"@tiptap/extension-focus": "^2.5.4", "@tiptap/extension-focus": "^2.5.5",
"@tiptap/extension-gapcursor": "^2.5.4", "@tiptap/extension-gapcursor": "^2.5.5",
"@tiptap/extension-hard-break": "^2.5.4", "@tiptap/extension-hard-break": "^2.5.5",
"@tiptap/extension-heading": "^2.5.4", "@tiptap/extension-heading": "^2.5.5",
"@tiptap/extension-highlight": "^2.5.4", "@tiptap/extension-highlight": "^2.5.5",
"@tiptap/extension-history": "^2.5.4", "@tiptap/extension-history": "^2.5.5",
"@tiptap/extension-horizontal-rule": "^2.5.4", "@tiptap/extension-horizontal-rule": "^2.5.5",
"@tiptap/extension-image": "^2.5.4", "@tiptap/extension-image": "^2.5.5",
"@tiptap/extension-italic": "^2.5.4", "@tiptap/extension-italic": "^2.5.5",
"@tiptap/extension-link": "^2.5.4", "@tiptap/extension-link": "^2.5.5",
"@tiptap/extension-list-item": "^2.5.4", "@tiptap/extension-list-item": "^2.5.5",
"@tiptap/extension-ordered-list": "^2.5.4", "@tiptap/extension-ordered-list": "^2.5.5",
"@tiptap/extension-paragraph": "^2.5.4", "@tiptap/extension-paragraph": "^2.5.5",
"@tiptap/extension-placeholder": "^2.5.4", "@tiptap/extension-placeholder": "^2.5.5",
"@tiptap/extension-strike": "^2.5.4", "@tiptap/extension-strike": "^2.5.5",
"@tiptap/extension-text": "^2.5.4", "@tiptap/extension-text": "^2.5.5",
"@tiptap/extension-underline": "^2.5.4", "@tiptap/extension-underline": "^2.5.5",
"@tiptap/extension-youtube": "^2.5.4", "@tiptap/extension-youtube": "^2.5.5",
"@types/cookie": "^0.6.0", "@types/cookie": "^0.6.0",
"@types/cookie-signature": "^1.1.2", "@types/cookie-signature": "^1.1.2",
"@types/node": "^20.14.11", "@types/node": "^20.14.11",
@ -102,7 +102,7 @@
"swiper": "^11.1.5", "swiper": "^11.1.5",
"throttle-debounce": "^5.0.2", "throttle-debounce": "^5.0.2",
"tslib": "^2.6.3", "tslib": "^2.6.3",
"typescript": "^5.5.3", "typescript": "^5.5.4",
"typograf": "^7.4.1", "typograf": "^7.4.1",
"uniqolor": "^1.1.1", "uniqolor": "^1.1.1",
"vinxi": "^0.4.1", "vinxi": "^0.4.1",

View File

@ -19,8 +19,7 @@ import {
import { AuthorLink } from '../../Author/AuthorLink' import { AuthorLink } from '../../Author/AuthorLink'
import { Userpic } from '../../Author/Userpic' import { Userpic } from '../../Author/Userpic'
import { CommentDate } from '../CommentDate' import { CommentDate } from '../CommentDate'
import { RatingControl as CommentRatingControl } from '../RatingControl' import { CommentRatingControl } from '../CommentRatingControl'
import styles from './Comment.module.scss' import styles from './Comment.module.scss'
const SimplifiedEditor = lazy(() => import('../../Editor/SimplifiedEditor')) const SimplifiedEditor = lazy(() => import('../../Editor/SimplifiedEditor'))

View File

@ -0,0 +1,122 @@
import { clsx } from 'clsx'
import { createMemo } from 'solid-js'
import { useFeed } from '~/context/feed'
import { useLocalize } from '~/context/localize'
import { useReactions } from '~/context/reactions'
import { useSession } from '~/context/session'
import { useSnackbar } from '~/context/ui'
import { Reaction, ReactionKind } from '~/graphql/schema/core.gen'
import { Popup } from '../_shared/Popup'
import { VotersList } from '../_shared/VotersList'
import styles from './CommentRatingControl.module.scss'
type Props = {
comment: Reaction
}
export const CommentRatingControl = (props: Props) => {
const { t } = useLocalize()
const { loadShout } = useFeed()
const { session } = useSession()
const uid = createMemo<number>(() => session()?.user?.app_data?.profile?.id || 0)
const { showSnackbar } = useSnackbar()
const { reactionEntities, createReaction, deleteReaction, loadReactionsBy } = useReactions()
const checkReaction = (reactionKind: ReactionKind) =>
Object.values(reactionEntities).some(
(r) =>
r.kind === reactionKind &&
r.created_by.id === uid() &&
r.shout.id === props.comment.shout.id &&
r.reply_to === props.comment.id
)
const isUpvoted = createMemo(() => checkReaction(ReactionKind.Like))
const isDownvoted = createMemo(() => checkReaction(ReactionKind.Dislike))
const canVote = createMemo(() => uid() !== props.comment.created_by.id)
const commentRatingReactions = createMemo(() =>
Object.values(reactionEntities).filter(
(r) =>
[ReactionKind.Like, ReactionKind.Dislike].includes(r.kind) &&
r.shout.id === props.comment.shout.id &&
r.reply_to === props.comment.id
)
)
const deleteCommentReaction = async (reactionKind: ReactionKind) => {
const reactionToDelete = Object.values(reactionEntities).find(
(r) =>
r.kind === reactionKind &&
r.created_by.id === uid() &&
r.shout.id === props.comment.shout.id &&
r.reply_to === props.comment.id
)
if (reactionToDelete) return deleteReaction(reactionToDelete.id)
}
const handleRatingChange = async (isUpvote: boolean) => {
try {
if (isUpvoted()) {
await deleteCommentReaction(ReactionKind.Like)
} else if (isDownvoted()) {
await deleteCommentReaction(ReactionKind.Dislike)
} else {
await createReaction({
reaction: {
kind: isUpvote ? ReactionKind.Like : ReactionKind.Dislike,
shout: props.comment.shout.id,
reply_to: props.comment.id
}
})
}
} catch {
showSnackbar({ type: 'error', body: t('Error') })
}
await loadShout(props.comment.shout.slug)
await loadReactionsBy({
by: { shout: props.comment.shout.slug }
})
}
return (
<div class={styles.commentRating}>
<button
role="button"
disabled={!(canVote() && uid())}
onClick={() => handleRatingChange(true)}
class={clsx(styles.commentRatingControl, styles.commentRatingControlUp, {
[styles.voted]: isUpvoted()
})}
/>
<Popup
trigger={
<div
class={clsx(styles.commentRatingValue, {
[styles.commentRatingPositive]: (props.comment?.stat?.rating || 0) > 0,
[styles.commentRatingNegative]: (props.comment?.stat?.rating || 0) < 0
})}
>
{props.comment?.stat?.rating || 0}
</div>
}
variant="tiny"
>
<VotersList
reactions={commentRatingReactions()}
fallbackMessage={t('This comment has not yet been rated')}
/>
</Popup>
<button
role="button"
disabled={!(canVote() && uid())}
onClick={() => handleRatingChange(false)}
class={clsx(styles.commentRatingControl, styles.commentRatingControlDown, {
[styles.voted]: isDownvoted()
})}
/>
</div>
)
}

View File

@ -1,15 +1,14 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { For, Show, createMemo, createSignal, lazy, onMount } from 'solid-js' import { For, Show, createMemo, createSignal, lazy, onMount } from 'solid-js'
import { useFeed } from '~/context/feed' import { useFeed } from '~/context/feed'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { COMMENTS_PER_PAGE, useReactions } from '~/context/reactions' import { useReactions } from '~/context/reactions'
import { useSession } from '~/context/session' import { useSession } from '~/context/session'
import { Reaction, ReactionKind, ReactionSort, Shout } from '~/graphql/schema/core.gen' import { Author, Reaction, ReactionKind, ReactionSort } from '~/graphql/schema/core.gen'
import { byCreated, byStat } from '~/lib/sort' import { byCreated, byStat } from '~/lib/sort'
import { SortFunction } from '~/types/common' import { SortFunction } from '~/types/common'
import { Button } from '../_shared/Button' import { Button } from '../_shared/Button'
import { InlineLoader } from '../_shared/InlineLoader'
import { LoadMoreItems, LoadMoreWrapper } from '../_shared/LoadMoreWrapper'
import { ShowIfAuthenticated } from '../_shared/ShowIfAuthenticated' import { ShowIfAuthenticated } from '../_shared/ShowIfAuthenticated'
import styles from './Article.module.scss' import styles from './Article.module.scss'
import { Comment } from './Comment' import { Comment } from './Comment'
@ -17,21 +16,21 @@ import { Comment } from './Comment'
const SimplifiedEditor = lazy(() => import('../Editor/SimplifiedEditor')) const SimplifiedEditor = lazy(() => import('../Editor/SimplifiedEditor'))
type Props = { type Props = {
shout: Shout articleAuthors: Author[]
shoutSlug: string
shoutId: number
} }
export const CommentsTree = (props: Props) => { export const CommentsTree = (props: Props) => {
const { session } = useSession() const { session } = useSession()
const { t } = useLocalize() const { t } = useLocalize()
const { reactionEntities, createReaction, loadShoutComments } = useReactions()
const { seen } = useFeed()
const [commentsOrder, setCommentsOrder] = createSignal<ReactionSort>(ReactionSort.Newest) const [commentsOrder, setCommentsOrder] = createSignal<ReactionSort>(ReactionSort.Newest)
const [onlyNew, setOnlyNew] = createSignal(false) const [onlyNew, setOnlyNew] = createSignal(false)
const [newReactions, setNewReactions] = createSignal<Reaction[]>([]) const [newReactions, setNewReactions] = createSignal<Reaction[]>([])
const [clearEditor, setClearEditor] = createSignal(false) const [clearEditor, setClearEditor] = createSignal(false)
const [clickedReplyId, setClickedReplyId] = createSignal<number>() const [clickedReplyId, setClickedReplyId] = createSignal<number>()
const { reactionEntities, createReaction, loadReactionsBy } = useReactions()
const shoutLastSeen = createMemo(() => seen()[props.shout.slug] ?? 0)
const comments = createMemo(() => const comments = createMemo(() =>
Object.values(reactionEntities).filter((reaction) => reaction.kind === 'COMMENT') Object.values(reactionEntities).filter((reaction) => reaction.kind === 'COMMENT')
) )
@ -49,9 +48,12 @@ export const CommentsTree = (props: Props) => {
} }
return newSortedComments return newSortedComments
}) })
const { seen } = useFeed()
const shoutLastSeen = createMemo(() => seen()[props.shoutSlug] ?? 0)
onMount(() => { onMount(() => {
const currentDate = new Date() const currentDate = new Date()
const setCookie = () => localStorage?.setItem(`${props.shout.slug}`, `${currentDate}`) const setCookie = () => localStorage?.setItem(`${props.shoutSlug}`, `${currentDate}`)
if (!shoutLastSeen()) { if (!shoutLastSeen()) {
setCookie() setCookie()
} else if (currentDate.getTime() > shoutLastSeen()) { } else if (currentDate.getTime() > shoutLastSeen()) {
@ -69,18 +71,6 @@ export const CommentsTree = (props: Props) => {
} }
}) })
const [posting, setPosting] = createSignal(false) const [posting, setPosting] = createSignal(false)
const [commentsLoading, setCommentsLoading] = createSignal(false)
const [pagination, setPagination] = createSignal(0)
const loadMoreComments = async () => {
setCommentsLoading(true)
const next = pagination() + 1
const offset = next * COMMENTS_PER_PAGE
const rrr = await loadShoutComments(props.shout.id, COMMENTS_PER_PAGE, offset)
rrr && setPagination(next)
setCommentsLoading(false)
return rrr as LoadMoreItems
}
const handleSubmitComment = async (value: string) => { const handleSubmitComment = async (value: string) => {
setPosting(true) setPosting(true)
try { try {
@ -88,17 +78,18 @@ export const CommentsTree = (props: Props) => {
reaction: { reaction: {
kind: ReactionKind.Comment, kind: ReactionKind.Comment,
body: value, body: value,
shout: props.shout.id shout: props.shoutId
} }
}) })
setClearEditor(true) setClearEditor(true)
await loadMoreComments() await loadReactionsBy({ by: { shout: props.shoutSlug } })
} catch (error) { } catch (error) {
console.error('[handleCreate reaction]:', error) console.error('[handleCreate reaction]:', error)
} }
setClearEditor(false) setClearEditor(false)
setPosting(false) setPosting(false)
} }
return ( return (
<> <>
<div class={styles.commentsHeaderWrapper}> <div class={styles.commentsHeaderWrapper}>
@ -136,24 +127,12 @@ export const CommentsTree = (props: Props) => {
</ul> </ul>
</Show> </Show>
</div> </div>
<Show when={commentsLoading()}>
<InlineLoader />
</Show>
<LoadMoreWrapper
loadFunction={loadMoreComments}
pageSize={COMMENTS_PER_PAGE}
hidden={
props.shout?.stat?.commented === 0 ||
commentsLoading() ||
comments().length >= (props.shout?.stat?.commented || 0)
}
>
<ul class={styles.comments}> <ul class={styles.comments}>
<For each={sortedComments().filter((r) => !r.reply_to)}> <For each={sortedComments().filter((r) => !r.reply_to)}>
{(reaction) => ( {(reaction) => (
<Comment <Comment
sortedComments={sortedComments()} sortedComments={sortedComments()}
isArticleAuthor={props.shout.authors?.some((a) => a && reaction.created_by.id === a.id)} isArticleAuthor={Boolean(props.articleAuthors.some((a) => a?.id === reaction.created_by.id))}
comment={reaction} comment={reaction}
clickedReply={(id) => setClickedReplyId(id)} clickedReply={(id) => setClickedReplyId(id)}
clickedReplyId={clickedReplyId()} clickedReplyId={clickedReplyId()}
@ -162,7 +141,6 @@ export const CommentsTree = (props: Props) => {
)} )}
</For> </For>
</ul> </ul>
</LoadMoreWrapper>
<ShowIfAuthenticated <ShowIfAuthenticated
fallback={ fallback={
<div class={styles.signInMessage}> <div class={styles.signInMessage}>

View File

@ -6,9 +6,10 @@ import { For, Show, createEffect, createMemo, createSignal, on, onCleanup, onMou
import { isServer } from 'solid-js/web' import { isServer } from 'solid-js/web'
import { useFeed } from '~/context/feed' import { useFeed } from '~/context/feed'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { useReactions } from '~/context/reactions'
import { useSession } from '~/context/session' import { useSession } from '~/context/session'
import { DEFAULT_HEADER_OFFSET, useUI } from '~/context/ui' import { DEFAULT_HEADER_OFFSET, useUI } from '~/context/ui'
import { type Author, type Maybe, type Shout, type Topic } from '~/graphql/schema/core.gen' import type { Author, Maybe, Shout, Topic } from '~/graphql/schema/core.gen'
import { processPrepositions } from '~/intl/prepositions' import { processPrepositions } from '~/intl/prepositions'
import { isCyrillic } from '~/intl/translate' import { isCyrillic } from '~/intl/translate'
import { getImageUrl } from '~/lib/getThumbUrl' import { getImageUrl } from '~/lib/getThumbUrl'
@ -32,8 +33,8 @@ import styles from './Article.module.scss'
import { AudioHeader } from './AudioHeader' import { AudioHeader } from './AudioHeader'
import { AudioPlayer } from './AudioPlayer' import { AudioPlayer } from './AudioPlayer'
import { CommentsTree } from './CommentsTree' import { CommentsTree } from './CommentsTree'
import { RatingControl as ShoutRatingControl } from './RatingControl'
import { SharePopup, getShareUrl } from './SharePopup' import { SharePopup, getShareUrl } from './SharePopup'
import { ShoutRatingControl } from './ShoutRatingControl'
type Props = { type Props = {
article: Shout article: Shout
@ -62,11 +63,15 @@ const scrollTo = (el: HTMLElement) => {
} }
const imgSrcRegExp = /<img[^>]+src\s*=\s*["']([^"']+)["']/gi const imgSrcRegExp = /<img[^>]+src\s*=\s*["']([^"']+)["']/gi
const COMMENTS_PER_PAGE = 30
const VOTES_PER_PAGE = 50
export const FullArticle = (props: Props) => { export const FullArticle = (props: Props) => {
const [searchParams, changeSearchParams] = useSearchParams<ArticlePageSearchParams>() const [searchParams, changeSearchParams] = useSearchParams<ArticlePageSearchParams>()
const { showModal } = useUI() const { showModal } = useUI()
const { loadReactionsBy } = useReactions()
const [selectedImage, setSelectedImage] = createSignal('') const [selectedImage, setSelectedImage] = createSignal('')
const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false)
const [isActionPopupActive, setIsActionPopupActive] = createSignal(false) const [isActionPopupActive, setIsActionPopupActive] = createSignal(false)
const { t, formatDate, lang } = useLocalize() const { t, formatDate, lang } = useLocalize()
const { session, requireAuthentication } = useSession() const { session, requireAuthentication } = useSession()
@ -74,6 +79,27 @@ export const FullArticle = (props: Props) => {
const { addSeen } = useFeed() const { addSeen } = useFeed()
const formattedDate = createMemo(() => formatDate(new Date((props.article.published_at || 0) * 1000))) const formattedDate = createMemo(() => formatDate(new Date((props.article.published_at || 0) * 1000)))
const [pages, setPages] = createSignal<Record<string, number>>({})
createEffect(
on(
pages,
async (p: Record<string, number>) => {
await loadReactionsBy({
by: { shout: props.article.slug, comment: true },
limit: COMMENTS_PER_PAGE,
offset: COMMENTS_PER_PAGE * p.comments || 0
})
await loadReactionsBy({
by: { shout: props.article.slug, rating: true },
limit: VOTES_PER_PAGE,
offset: VOTES_PER_PAGE * p.rating || 0
})
setIsReactionsLoaded(true)
},
{ defer: true }
)
)
const canEdit = createMemo( const canEdit = createMemo(
() => () =>
Boolean(author()?.id) && Boolean(author()?.id) &&
@ -141,7 +167,7 @@ export const FullArticle = (props: Props) => {
let commentsRef: HTMLDivElement | undefined let commentsRef: HTMLDivElement | undefined
createEffect(() => { createEffect(() => {
if (searchParams?.commentId) { if (searchParams?.commentId && isReactionsLoaded()) {
const commentElement = document.querySelector<HTMLElement>( const commentElement = document.querySelector<HTMLElement>(
`[id='comment_${searchParams?.commentId}']` `[id='comment_${searchParams?.commentId}']`
) )
@ -280,16 +306,9 @@ export const FullArticle = (props: Props) => {
}) })
} }
createEffect( onMount(() => {
on( console.debug(props.article)
() => props.article, setPages((_) => ({ comments: 0, rating: 0 }))
() => {
updateIframeSizes()
}
)
)
onMount(async () => {
addSeen(props.article.slug) addSeen(props.article.slug)
document.title = props.article.title document.title = props.article.title
updateIframeSizes() updateIframeSizes()
@ -560,7 +579,13 @@ export const FullArticle = (props: Props) => {
</For> </For>
</div> </div>
<div id="comments" ref={(el) => (commentsRef = el)}> <div id="comments" ref={(el) => (commentsRef = el)}>
<CommentsTree shout={props.article} /> <Show when={isReactionsLoaded()}>
<CommentsTree
shoutId={props.article.id}
shoutSlug={props.article.slug}
articleAuthors={props.article.authors as Author[]}
/>
</Show>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,254 +0,0 @@
import { useSearchParams } from '@solidjs/router'
import { clsx } from 'clsx'
import { Show, createEffect, createMemo, createSignal, on } from 'solid-js'
import { byCreated } from '~/lib/sort'
import { useLocalize } from '../../context/localize'
import { RATINGS_PER_PAGE, useReactions } from '../../context/reactions'
import { useSession } from '../../context/session'
import { useSnackbar } from '../../context/ui'
import { Reaction, ReactionKind, Shout } from '../../graphql/schema/core.gen'
import { Icon } from '../_shared/Icon'
import { InlineLoader } from '../_shared/InlineLoader'
import { LoadMoreItems, LoadMoreWrapper } from '../_shared/LoadMoreWrapper'
import { Popup } from '../_shared/Popup'
import { VotersList } from '../_shared/VotersList'
import stylesComment from './CommentRatingControl.module.scss'
import stylesShout from './ShoutRatingControl.module.scss'
interface RatingControlProps {
shout?: Shout
comment?: Reaction
class?: string
}
export const RatingControl = (props: RatingControlProps) => {
const { t, lang } = useLocalize()
const [_, changeSearchParams] = useSearchParams()
const snackbar = useSnackbar()
const { session } = useSession()
const {
reactionEntities,
reactionsByShout,
createReaction,
deleteReaction,
loadShoutRatings,
loadCommentRatings
} = useReactions()
const [myRate, setMyRate] = createSignal<Reaction | undefined>()
const [ratingReactions, setRatingReactions] = createSignal<Reaction[]>([])
const [isLoading, setIsLoading] = createSignal(false)
// reaction kind
const checkReaction = (reactionKind: ReactionKind) =>
Object.values(reactionEntities).some(
(r) =>
r.kind === reactionKind &&
r.created_by.slug === session()?.user?.app_data?.profile?.slug &&
r.shout.id === props.comment?.shout.id &&
r.reply_to === props.comment?.id
)
const isUpvoted = createMemo(() => checkReaction(ReactionKind.Like))
const isDownvoted = createMemo(() => checkReaction(ReactionKind.Dislike))
createEffect(() => {
const shout = props.comment?.shout.id || props.shout?.id
if (shout && !ratingReactions()) {
let result = Object.values(reactionEntities).filter(
(r) => [ReactionKind.Like, ReactionKind.Dislike].includes(r.kind) && r.shout.id === shout
)
if (props.comment?.id) result = result.filter((r) => r.reply_to === props.comment?.id)
setRatingReactions(result)
}
})
const deleteRating = async (reactionKind: ReactionKind) => {
const reactionToDelete = Object.values(reactionEntities).find(
(r) =>
r.kind === reactionKind &&
r.created_by.slug === session()?.user?.nickname &&
r.shout.id === props.comment?.shout.id &&
r.reply_to === props.comment?.id
)
return reactionToDelete && deleteReaction(reactionToDelete.id)
}
// rating change
const handleRatingChange = async (isUpvote: boolean) => {
setIsLoading(true)
let error = ''
try {
if (isUpvoted() && isUpvote) return
if (isDownvoted() && !isUpvote) return
if (isUpvoted() && !isUpvote) error = (await deleteRating(ReactionKind.Like))?.error || ''
if (isDownvoted() && isUpvote) error = (await deleteRating(ReactionKind.Dislike))?.error || ''
if (!(isUpvoted() || isDownvoted())) {
props.comment?.shout.id &&
(await createReaction({
reaction: {
kind: isUpvote ? ReactionKind.Like : ReactionKind.Dislike,
shout: props.comment.shout.id,
reply_to: props.comment?.id
}
}))
}
} catch (err) {
snackbar?.showSnackbar({ type: 'error', body: `${t('Error')}: ${error || err || ''}` })
}
setIsLoading(false)
}
const total = createMemo<number>(() =>
props.comment?.stat?.rating ? props.comment.stat.rating : props.shout?.stat?.rating || 0
)
createEffect(
on(
[ratingReactions, () => session()?.user?.app_data?.profile],
([reactions, me]) => {
console.debug('[RatingControl] on reactions update')
const ratingVotes = Object.values(reactions).filter((r) => !r.reply_to)
setRatingReactions((_) => ratingVotes.sort(byCreated))
const myReaction = reactions.find((r) => r.created_by.id === me?.id)
setMyRate((_) => myReaction)
},
{ defer: true }
)
)
const getTrigger = createMemo(() => {
return (
<div
class={clsx(stylesComment.commentRatingValue, {
[stylesComment.commentRatingPositive]: total() > 0 && Boolean(props.comment?.id),
[stylesComment.commentRatingNegative]: total() < 0 && Boolean(props.comment?.id),
[stylesShout.ratingValue]: !props.comment?.id
})}
>
{total()}
</div>
)
})
const VOTERS_PER_PAGE = 10
const [ratingPage, setRatingPage] = createSignal(0)
const [ratingLoading, setRatingLoading] = createSignal(false) // FIXME: use loading indication
const ratings = createMemo(() =>
props.shout
? reactionsByShout[props.shout?.slug]?.filter(
(r) => r.kind === ReactionKind.Like || r.kind === ReactionKind.Dislike
)
: []
)
const loadMoreReactions = async () => {
if (!(props.shout?.id || props.comment?.id)) return [] as LoadMoreItems
setRatingLoading(true)
const next = ratingPage() + 1
const offset = RATINGS_PER_PAGE * next
const loader = props.comment ? loadCommentRatings : loadShoutRatings
const rrr = await loader(props.shout?.id || 0, RATINGS_PER_PAGE, offset)
rrr && setRatingPage(next)
setRatingLoading(false)
return rrr as LoadMoreItems
}
return props.comment?.id ? (
<div class={stylesComment.commentRating}>
<button
role="button"
disabled={!session()?.user?.app_data?.profile}
onClick={() => handleRatingChange(true)}
class={clsx(stylesComment.commentRatingControl, stylesComment.commentRatingControlUp, {
[stylesComment.voted]: isUpvoted()
})}
/>
<Popup
trigger={
<div
class={clsx(stylesComment.commentRatingValue, {
[stylesComment.commentRatingPositive]: (props.comment?.stat?.rating || 0) > 0,
[stylesComment.commentRatingNegative]: (props.comment?.stat?.rating || 0) < 0
})}
>
{props.comment?.stat?.rating || 0}
</div>
}
variant="tiny"
>
<Show when={ratingLoading()}>
<InlineLoader />
</Show>
<LoadMoreWrapper
loadFunction={loadMoreReactions}
pageSize={VOTERS_PER_PAGE}
hidden={ratingLoading()}
>
<VotersList reactions={ratings()} fallbackMessage={t('This comment has not been rated yet')} />
</LoadMoreWrapper>
</Popup>
<button
role="button"
disabled={!session()?.user?.app_data?.profile}
onClick={() => handleRatingChange(false)}
class={clsx(stylesComment.commentRatingControl, stylesComment.commentRatingControlDown, {
[stylesComment.voted]: isDownvoted()
})}
/>
</div>
) : (
<div class={clsx(props.comment ? stylesComment.commentRating : stylesShout.rating, props.class)}>
<button
onClick={() => handleRatingChange(false)}
disabled={isLoading()}
class={
props.comment
? clsx(stylesComment.commentRatingControl, stylesComment.commentRatingControlUp, {
[stylesComment.voted]: myRate()?.kind === 'LIKE'
})
: ''
}
>
<Show when={!props.comment}>
<Icon
name={isDownvoted() ? 'rating-control-checked' : 'rating-control-less'}
class={isLoading() ? 'rotating' : ''}
/>
</Show>
</button>
<Popup trigger={getTrigger()} variant="tiny">
<Show
when={!!session()?.user?.app_data?.profile}
fallback={
<>
<span class="link" onClick={() => changeSearchParams({ mode: 'login', m: 'auth' })}>
{t('Enter')}
</span>
{lang() === 'ru' ? ', ' : ' '}
{t('to see the voters')}
</>
}
>
<VotersList
reactions={ratingReactions()}
fallbackMessage={isLoading() ? t('Loading') : t('No one rated yet')}
/>
</Show>
</Popup>
<button
onClick={() => handleRatingChange(true)}
disabled={isLoading()}
class={
props.comment
? clsx(stylesComment.commentRatingControl, stylesComment.commentRatingControlDown, {
[stylesComment.voted]: myRate()?.kind === 'DISLIKE'
})
: ''
}
>
<Show when={!props.comment}>
<Icon
name={isUpvoted() ? 'rating-control-checked' : 'rating-control-more'}
class={isLoading() ? 'rotating' : ''}
/>
</Show>
</button>
</div>
)
}

View File

@ -0,0 +1,107 @@
import { clsx } from 'clsx'
import { Show, createMemo, createSignal } from 'solid-js'
import { useFeed } from '~/context/feed'
import { useLocalize } from '~/context/localize'
import { useReactions } from '~/context/reactions'
import { useSession } from '~/context/session'
import type { Author } from '~/graphql/schema/core.gen'
import { ReactionKind, Shout } from '~/graphql/schema/core.gen'
import { Icon } from '../_shared/Icon'
import { Popup } from '../_shared/Popup'
import { VotersList } from '../_shared/VotersList'
import styles from './ShoutRatingControl.module.scss'
interface ShoutRatingControlProps {
shout: Shout
class?: string
}
export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
const { t } = useLocalize()
const { loadShout } = useFeed()
const { requireAuthentication, session } = useSession()
const author = createMemo<Author>(() => session()?.user?.app_data?.profile as Author)
const { reactionEntities, createReaction, deleteReaction, loadReactionsBy } = useReactions()
const [isLoading, setIsLoading] = createSignal(false)
const checkReaction = (reactionKind: ReactionKind) =>
Object.values(reactionEntities).some(
(r) =>
r.kind === reactionKind &&
r.created_by.id === author()?.id &&
r.shout.id === props.shout.id &&
!r.reply_to
)
const isUpvoted = createMemo(() => checkReaction(ReactionKind.Like))
const isDownvoted = createMemo(() => checkReaction(ReactionKind.Dislike))
const shoutRatingReactions = createMemo(() =>
Object.values(reactionEntities).filter(
(r) => ['LIKE', 'DISLIKE'].includes(r.kind) && r.shout.id === props.shout.id && !r.reply_to
)
)
const deleteShoutReaction = async (reactionKind: ReactionKind) => {
const reactionToDelete = Object.values(reactionEntities).find(
(r) =>
r.kind === reactionKind &&
r.created_by.id === author()?.id &&
r.shout.id === props.shout.id &&
!r.reply_to
)
if (reactionToDelete) return deleteReaction(reactionToDelete.id)
}
const handleRatingChange = (isUpvote: boolean) => {
requireAuthentication(async () => {
setIsLoading(true)
if (isUpvoted()) {
await deleteShoutReaction(ReactionKind.Like)
} else if (isDownvoted()) {
await deleteShoutReaction(ReactionKind.Dislike)
} else {
await createReaction({
reaction: {
kind: isUpvote ? ReactionKind.Like : ReactionKind.Dislike,
shout: props.shout.id
}
})
}
loadShout(props.shout.slug)
loadReactionsBy({
by: { shout: props.shout.slug }
})
setIsLoading(false)
}, 'vote')
}
return (
<div class={clsx(styles.rating, props.class)}>
<button onClick={() => handleRatingChange(false)} disabled={isLoading()}>
<Show when={!isDownvoted()} fallback={<Icon name="rating-control-checked" />}>
<Icon name="rating-control-less" />
</Show>
</button>
<Popup
trigger={<span class={styles.ratingValue}>{props.shout.stat?.rating || 0}</span>}
variant="tiny"
>
<VotersList
reactions={shoutRatingReactions()}
fallbackMessage={t('This post has not been rated yet')}
/>
</Popup>
<button onClick={() => handleRatingChange(true)} disabled={isLoading()}>
<Show when={!isUpvoted()} fallback={<Icon name="rating-control-checked" />}>
<Icon name="rating-control-more" />
</Show>
</button>
</div>
)
}

View File

@ -26,6 +26,7 @@ type Props = {
inviteView?: boolean inviteView?: boolean
onInvite?: (id: number) => void onInvite?: (id: number) => void
selected?: boolean selected?: boolean
subscriptionsMode?: boolean
} }
export const AuthorBadge = (props: Props) => { export const AuthorBadge = (props: Props) => {
const { session, requireAuthentication } = useSession() const { session, requireAuthentication } = useSession()
@ -116,7 +117,7 @@ export const AuthorBadge = (props: Props) => {
<div class={clsx('text-truncate', styles.bio)} innerHTML={props.author.bio || ''} /> <div class={clsx('text-truncate', styles.bio)} innerHTML={props.author.bio || ''} />
</Match> </Match>
</Switch> </Switch>
<Show when={props.author?.stat}> <Show when={props.author?.stat && !props.subscriptionsMode}>
<div class={styles.bio}> <div class={styles.bio}>
<Show when={(props.author?.stat?.shouts || 0) > 0}> <Show when={(props.author?.stat?.shouts || 0) > 0}>
<div>{t('some posts', { count: props.author.stat?.shouts ?? 0 })}</div> <div>{t('some posts', { count: props.author.stat?.shouts ?? 0 })}</div>

View File

@ -162,7 +162,7 @@ export const AuthorCard = (props: Props) => {
<For each={authorSubs()}> <For each={authorSubs()}>
{(subscription) => {(subscription) =>
'name' in subscription ? ( 'name' in subscription ? (
<AuthorBadge author={subscription as Author} nameOnly={true} /> <AuthorBadge author={subscription as Author} subscriptionsMode={true} />
) : ( ) : (
<TopicBadge topic={subscription as Topic} subscriptionsMode={true} /> <TopicBadge topic={subscription as Topic} subscriptionsMode={true} />
) )

View File

@ -1,4 +1,3 @@
import { Editor } from '@tiptap/core'
import { Blockquote } from '@tiptap/extension-blockquote' import { Blockquote } from '@tiptap/extension-blockquote'
import { Bold } from '@tiptap/extension-bold' import { Bold } from '@tiptap/extension-bold'
import { BubbleMenu } from '@tiptap/extension-bubble-menu' import { BubbleMenu } from '@tiptap/extension-bubble-menu'
@ -11,7 +10,7 @@ import { Paragraph } from '@tiptap/extension-paragraph'
import { Placeholder } from '@tiptap/extension-placeholder' import { Placeholder } from '@tiptap/extension-placeholder'
import { Text } from '@tiptap/extension-text' import { Text } from '@tiptap/extension-text'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Show, Suspense, createEffect, createMemo, createSignal, onCleanup, onMount } from 'solid-js' import { Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js'
import { Portal } from 'solid-js/web' import { Portal } from 'solid-js/web'
import { import {
createEditorTransaction, createEditorTransaction,
@ -20,23 +19,26 @@ import {
useEditorIsEmpty, useEditorIsEmpty,
useEditorIsFocused useEditorIsFocused
} from 'solid-tiptap' } from 'solid-tiptap'
import { Modal } from '~/components/_shared/Modal'
import { useUI } from '~/context/ui' import { useEditorContext } from '~/context/editor'
import { useLocalize } from '~/context/localize'
import { UploadedFile } from '~/types/upload' import { UploadedFile } from '~/types/upload'
import { useEditorContext } from '../../context/editor'
import { useLocalize } from '../../context/localize'
import { Button } from '../_shared/Button' import { Button } from '../_shared/Button'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { Loading } from '../_shared/Loading' import { Loading } from '../_shared/Loading'
import { Modal } from '../_shared/Modal'
import { Popover } from '../_shared/Popover' import { Popover } from '../_shared/Popover'
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient' import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
import { LinkBubbleMenuModule } from './LinkBubbleMenu' import { LinkBubbleMenuModule } from './LinkBubbleMenu'
import styles from './SimplifiedEditor.module.scss'
import { TextBubbleMenu } from './TextBubbleMenu' import { TextBubbleMenu } from './TextBubbleMenu'
import { UploadModalContent } from './UploadModalContent' import { UploadModalContent } from './UploadModalContent'
import { Figcaption } from './extensions/Figcaption' import { Figcaption } from './extensions/Figcaption'
import { Figure } from './extensions/Figure' import { Figure } from './extensions/Figure'
import { Editor } from '@tiptap/core'
import { useUI } from '~/context/ui'
import styles from './SimplifiedEditor.module.scss'
type Props = { type Props = {
placeholder: string placeholder: string
initialContent?: string initialContent?: string
@ -65,25 +67,31 @@ type Props = {
const DEFAULT_MAX_LENGTH = 400 const DEFAULT_MAX_LENGTH = 400
const SimplifiedEditor = (props: Props) => { const SimplifiedEditor = (props: Props) => {
const maxLength = props.maxLength ?? DEFAULT_MAX_LENGTH
let wrapperEditorElRef: HTMLElement | undefined
let editorElRef: HTMLElement | undefined
let textBubbleMenuRef: HTMLDivElement | undefined
let linkBubbleMenuRef: HTMLDivElement | undefined
const { showModal, hideModal } = useUI()
const { t } = useLocalize() const { t } = useLocalize()
const { showModal, hideModal } = useUI()
const [counter, setCounter] = createSignal<number>(0) const [counter, setCounter] = createSignal<number>(0)
const [shouldShowLinkBubbleMenu, setShouldShowLinkBubbleMenu] = createSignal(false) const [shouldShowLinkBubbleMenu, setShouldShowLinkBubbleMenu] = createSignal(false)
const isCancelButtonVisible = createMemo(() => props.isCancelButtonVisible !== false) const isCancelButtonVisible = createMemo(() => props.isCancelButtonVisible !== false)
const { setEditor, editor } = useEditorContext() const [editorElement, setEditorElement] = createSignal<HTMLDivElement>()
const { editor, setEditor } = useEditorContext()
const maxLength = props.maxLength ?? DEFAULT_MAX_LENGTH
let wrapperEditorElRef: HTMLElement | undefined
let textBubbleMenuRef: HTMLDivElement | undefined
let linkBubbleMenuRef: HTMLDivElement | undefined
const ImageFigure = Figure.extend({ const ImageFigure = Figure.extend({
name: 'capturedImage', name: 'capturedImage',
content: 'figcaption image' content: 'figcaption image'
}) })
createEffect(() => {
const e = createTiptapEditor(() => ({ createEffect(
element: editorElRef as HTMLElement, on(
() => editorElement(),
(ee: HTMLDivElement | undefined) => {
if (ee && textBubbleMenuRef && linkBubbleMenuRef) {
const freshEditor = createTiptapEditor<HTMLElement>(() => ({
element: ee,
editorProps: { editorProps: {
attributes: { attributes: {
class: styles.simplifiedEditorField class: styles.simplifiedEditorField
@ -140,12 +148,17 @@ const SimplifiedEditor = (props: Props) => {
}) })
], ],
autofocus: props.autoFocus, autofocus: props.autoFocus,
content: content ?? null content: props.initialContent || null
})) }))
const editorInstance = freshEditor()
if (!editorInstance) return
setEditor(editorInstance)
}
},
{ defer: true }
)
)
e() && setEditor(e() as Editor)
})
const content = props.initialContent
const isEmpty = useEditorIsEmpty(() => editor()) const isEmpty = useEditorIsEmpty(() => editor())
const isFocused = useEditorIsFocused(() => editor()) const isFocused = useEditorIsFocused(() => editor())
@ -198,7 +211,7 @@ const SimplifiedEditor = (props: Props) => {
} }
if (props.resetToInitial) { if (props.resetToInitial) {
editor()?.commands.clearContent(true) editor()?.commands.clearContent(true)
props.initialContent && editor()?.commands.setContent(props.initialContent) if (props.initialContent) editor()?.commands.setContent(props.initialContent)
} }
}) })
@ -261,7 +274,6 @@ const SimplifiedEditor = (props: Props) => {
return ( return (
<ShowOnlyOnClient> <ShowOnlyOnClient>
<Suspense>
<div <div
ref={(el) => (wrapperEditorElRef = el)} ref={(el) => (wrapperEditorElRef = el)}
class={clsx(styles.SimplifiedEditor, { class={clsx(styles.SimplifiedEditor, {
@ -278,11 +290,7 @@ const SimplifiedEditor = (props: Props) => {
<Show when={props.label && counter() > 0}> <Show when={props.label && counter() > 0}>
<div class={styles.label}>{props.label}</div> <div class={styles.label}>{props.label}</div>
</Show> </Show>
<div style={props.maxHeight ? maxHeightStyle : undefined} ref={setEditorElement} />
<Show when={props.maxHeight} fallback={<div ref={(el) => (editorElRef = el)} />}>
<div style={maxHeightStyle} ref={(el) => (editorElRef = el)} />
</Show>
<Show when={!props.onlyBubbleControls}> <Show when={!props.onlyBubbleControls}>
<div class={clsx(styles.controls, { [styles.alwaysVisible]: props.controlsAlwaysVisible })}> <div class={clsx(styles.controls, { [styles.alwaysVisible]: props.controlsAlwaysVisible })}>
<div class={styles.actions}> <div class={styles.actions}>
@ -371,11 +379,14 @@ const SimplifiedEditor = (props: Props) => {
<Show when={props.imageEnabled}> <Show when={props.imageEnabled}>
<Portal> <Portal>
<Modal variant="narrow" name="simplifiedEditorUploadImage"> <Modal variant="narrow" name="simplifiedEditorUploadImage">
<UploadModalContent onClose={(value) => value && renderImage(value)} /> <UploadModalContent
onClose={(value) => {
renderImage(value as UploadedFile)
}}
/>
</Modal> </Modal>
</Portal> </Portal>
</Show> </Show>
<Show when={!!editor()}>
<Show when={props.onlyBubbleControls}> <Show when={props.onlyBubbleControls}>
<TextBubbleMenu <TextBubbleMenu
shouldShow={true} shouldShow={true}
@ -389,9 +400,7 @@ const SimplifiedEditor = (props: Props) => {
ref={(el) => (linkBubbleMenuRef = el)} ref={(el) => (linkBubbleMenuRef = el)}
onClose={handleHideLinkBubble} onClose={handleHideLinkBubble}
/> />
</Show>
</div> </div>
</Suspense>
</ShowOnlyOnClient> </ShowOnlyOnClient>
) )
} }

View File

@ -10,8 +10,8 @@ import type { Author, Maybe, Shout, Topic } from '~/graphql/schema/core.gen'
import { capitalize } from '~/utils/capitalize' import { capitalize } from '~/utils/capitalize'
import { descFromBody } from '~/utils/meta' import { descFromBody } from '~/utils/meta'
import { CoverImage } from '../../Article/CoverImage' import { CoverImage } from '../../Article/CoverImage'
import { RatingControl as ShoutRatingControl } from '../../Article/RatingControl'
import { SharePopup, getShareUrl } from '../../Article/SharePopup' import { SharePopup, getShareUrl } from '../../Article/SharePopup'
import { ShoutRatingControl } from '../../Article/ShoutRatingControl'
import { AuthorLink } from '../../Author/AuthorLink' import { AuthorLink } from '../../Author/AuthorLink'
import stylesHeader from '../../HeaderNav/Header.module.scss' import stylesHeader from '../../HeaderNav/Header.module.scss'
import { CardTopic } from '../CardTopic' import { CardTopic } from '../CardTopic'

View File

@ -753,12 +753,7 @@
white-space: nowrap; white-space: nowrap;
} }
.rightItem {
margin-right: 0;
position: absolute;
right: 0;
top: 0;
}
} }
a:link, a:link,
@ -801,13 +796,6 @@
} }
} }
.rightItemIcon {
display: inline-block;
margin-left: 0.3em;
position: relative;
top: 0.15em;
}
.editorPopup { .editorPopup {
border: 1px solid rgb(0 0 0 / 15%) !important; border: 1px solid rgb(0 0 0 / 15%) !important;
border-radius: 1.6rem; border-radius: 1.6rem;

View File

@ -18,6 +18,8 @@ import stylesAuthorList from './AuthorsList.module.scss'
type Props = { type Props = {
authors: Author[] authors: Author[]
authorsByFollowers?: Author[]
authorsByShouts?: Author[]
isLoaded: boolean isLoaded: boolean
} }
@ -33,43 +35,48 @@ export const AllAuthors = (props: Props) => {
const [searchParams, changeSearchParams] = useSearchParams<{ by?: string }>() const [searchParams, changeSearchParams] = useSearchParams<{ by?: string }>()
const { authorsSorted, setAuthorsSort, loadAuthors } = useAuthors() const { authorsSorted, setAuthorsSort, loadAuthors } = useAuthors()
const [loading, setLoading] = createSignal<boolean>(false) const [loading, setLoading] = createSignal<boolean>(false)
const [searchQuery, setSearchQuery] = createSignal('') const [_currentAuthors, setCurrentAuthors] = createSignal<Author[]>([])
const [filteredAuthors, setFilteredAuthors] = createSignal<Author[]>([])
// UPDATE Fetch authors initially and when searchParams.by changes
createEffect(() => { createEffect(() => {
// Load all authors initially
fetchAuthors(searchParams.by || 'name', 0) fetchAuthors(searchParams.by || 'name', 0)
}) })
/* const authors = createMemo(() => { const authors = createMemo(() => {
let sortedAuthors = [...props.authors] let sortedAuthors = [...(props.authors || authorsSorted())] // Clone the array to avoid mutating the original
sortedAuthors = authorsSorted() console.log('Before Sorting:', sortedAuthors.slice(0, 5)) // Log the first 5 authors for comparison
if (!searchParams.by || searchParams.by === 'name') { if (searchParams.by === 'name') {
sortedAuthors = sortedAuthors.sort(byFirstChar) sortedAuthors = sortedAuthors.sort(byFirstChar)
console.log('Sorted by Name:', sortedAuthors.slice(0, 5))
} else if (searchParams.by === 'shouts') { } else if (searchParams.by === 'shouts') {
sortedAuthors = sortedAuthors.sort(byStat('shouts')) sortedAuthors = sortedAuthors.sort(byStat('shouts'))
console.log('Sorted by Shouts:', sortedAuthors.slice(0, 5))
} else if (searchParams.by === 'followers') { } else if (searchParams.by === 'followers') {
sortedAuthors = sortedAuthors.sort(byStat('followers')) sortedAuthors = sortedAuthors.sort(byStat('followers'))
console.log('Sorted by Followers:', sortedAuthors.slice(0, 5))
} }
return sortedAuthors console.log('After Sorting:', sortedAuthors.slice(0, 5))
}) */
const authors = createMemo(() => {
let sortedAuthors: Author[] = []
if (!searchParams.by || searchParams.by === 'name') {
sortedAuthors = [...props.authors].sort(byFirstChar)
} else {
sortedAuthors = authorsSorted().sort(byStat(searchParams.by || 'shouts'))
}
return sortedAuthors return sortedAuthors
}) })
// Log authors data and searchParams for debugging
createEffect(() => { createEffect(() => {
setFilteredAuthors(dummyFilter(authors(), searchQuery(), lang()) as Author[]) console.log('Authors:', props.authors.slice(0, 5)) // Log the first 5 authors
console.log('Sorted Authors:', authors().slice(0, 5)) // Log the first 5 sorted authors
console.log('Search Params "by":', searchParams.by)
}) })
// filter
const [searchQuery, setSearchQuery] = createSignal('')
const [filteredAuthors, setFilteredAuthors] = createSignal<Author[]>([])
createEffect(
() => authors() && setFilteredAuthors(dummyFilter(authors(), searchQuery(), lang()) as Author[])
)
// store by first char
const byLetterFiltered = createMemo<{ [letter: string]: Author[] }>(() => { const byLetterFiltered = createMemo<{ [letter: string]: Author[] }>(() => {
if (!(filteredAuthors()?.length > 0)) return {} if (!(filteredAuthors()?.length > 0)) return {}
console.debug('[components.AllAuthors] update byLetterFiltered', filteredAuthors()?.length)
return filteredAuthors().reduce( return filteredAuthors().reduce(
(acc, author: Author) => authorLetterReduce(acc, author, lang()), (acc, author: Author) => authorLetterReduce(acc, author, lang()),
{} as { [letter: string]: Author[] } {} as { [letter: string]: Author[] }
@ -86,6 +93,7 @@ export const AllAuthors = (props: Props) => {
const fetchAuthors = async (queryType: string, page: number) => { const fetchAuthors = async (queryType: string, page: number) => {
try { try {
console.debug('[components.AuthorsList] fetching authors...')
setLoading(true) setLoading(true)
setAuthorsSort?.(queryType) setAuthorsSort?.(queryType)
const offset = AUTHORS_PER_PAGE * page const offset = AUTHORS_PER_PAGE * page
@ -94,6 +102,8 @@ export const AllAuthors = (props: Props) => {
limit: AUTHORS_PER_PAGE, limit: AUTHORS_PER_PAGE,
offset offset
}) })
// UPDATE authors to currentAuthors state
setCurrentAuthors((prev) => [...prev, ...authorsSorted()])
} catch (error) { } catch (error) {
console.error('[components.AuthorsList] error fetching authors:', error) console.error('[components.AuthorsList] error fetching authors:', error)
} finally { } finally {
@ -121,7 +131,7 @@ export const AllAuthors = (props: Props) => {
<ul class={clsx(styles.viewSwitcher, 'view-switcher')}> <ul class={clsx(styles.viewSwitcher, 'view-switcher')}>
<li <li
class={clsx({ class={clsx({
['view-switcher__item--selected']: searchParams?.by === 'shouts' ['view-switcher__item--selected']: !searchParams?.by || searchParams?.by === 'shouts'
})} })}
> >
<a href="#" onClick={() => changeSearchParams({ by: 'shouts' })}> <a href="#" onClick={() => changeSearchParams({ by: 'shouts' })}>
@ -139,7 +149,7 @@ export const AllAuthors = (props: Props) => {
</li> </li>
<li <li
class={clsx({ class={clsx({
['view-switcher__item--selected']: !searchParams?.by || searchParams?.by === 'name' ['view-switcher__item--selected']: searchParams?.by === 'name'
})} })}
> >
<a href="#" onClick={() => changeSearchParams({ by: 'name' })}> <a href="#" onClick={() => changeSearchParams({ by: 'name' })}>
@ -237,13 +247,12 @@ export const AllAuthors = (props: Props) => {
</div> </div>
</div> </div>
) )
return ( return (
<> <>
<Show when={props.isLoaded} fallback={<Loading />}> <Show when={props.isLoaded} fallback={<Loading />}>
<div class="offset-md-5"> <div class="offset-md-5">
<TabNavigator /> <TabNavigator />
<Show when={!searchParams.by || searchParams.by === 'name'} fallback={<AuthorsSortedList />}> <Show when={searchParams?.by === 'name'} fallback={<AuthorsSortedList />}>
<AbcNavigator /> <AbcNavigator />
<AbcAuthorsList /> <AbcAuthorsList />
</Show> </Show>

View File

@ -61,7 +61,7 @@ export const AuthorView = (props: AuthorViewProps) => {
on( on(
[() => session()?.user?.app_data?.profile, () => props.authorSlug || ''], [() => session()?.user?.app_data?.profile, () => props.authorSlug || ''],
async ([me, slug]) => { async ([me, slug]) => {
console.debug('[AuthorView] checking if my profile') console.debug('check if my profile')
const my = slug && me?.slug === slug const my = slug && me?.slug === slug
if (my) { if (my) {
console.debug('[Author] my profile precached') console.debug('[Author] my profile precached')
@ -86,7 +86,7 @@ export const AuthorView = (props: AuthorViewProps) => {
() => authorsEntities()[props.author?.slug || props.authorSlug || ''], () => authorsEntities()[props.author?.slug || props.authorSlug || ''],
async (found) => { async (found) => {
if (!found) return if (!found) return
console.debug('[AuthorView] ') setAuthor(found)
console.info(`[Author] profile for @${found.slug} fetched`) console.info(`[Author] profile for @${found.slug} fetched`)
const followsResp = await query(getAuthorFollowsQuery, { slug: found.slug }).toPromise() const followsResp = await query(getAuthorFollowsQuery, { slug: found.slug }).toPromise()
const follows = followsResp?.data?.get_author_followers || {} const follows = followsResp?.data?.get_author_followers || {}
@ -96,7 +96,6 @@ export const AuthorView = (props: AuthorViewProps) => {
setFollowers(followersResp?.data?.get_author_followers || []) setFollowers(followersResp?.data?.get_author_followers || [])
console.info(`[Author] followers for @${found.slug} fetched`) console.info(`[Author] followers for @${found.slug} fetched`)
setIsFetching(false) setIsFetching(false)
setTimeout(() => setAuthor(found), 1)
}, },
{ defer: true } { defer: true }
) )
@ -124,37 +123,7 @@ export const AuthorView = (props: AuthorViewProps) => {
(tab) => tab && console.log('[views.Author] profile tab switched') (tab) => tab && console.log('[views.Author] profile tab switched')
) )
) )
const AuthorFeed = () => (
<Show when={Array.isArray(props.shouts) && props.shouts.length > 0 && props.shouts[0]}>
<Row1 article={props.shouts?.[0] as Shout} noauthor={true} nodate={true} />
<Show when={props.shouts?.length || 0}>
<Show when={props.shouts?.length === 1}>
<Row1 article={props.shouts?.[0] as Shout} noauthor={true} nodate={true} />
</Show>
<Show when={props.shouts?.length === 2}>
<Row2 articles={props.shouts as Shout[]} isEqual={true} noauthor={true} nodate={true} />
</Show>
<Show when={props.shouts?.length === 3}>
<Row3 articles={props.shouts as Shout[]} noauthor={true} nodate={true} />
</Show>
<Show when={props.shouts && props.shouts.length > 3}>
<For each={pages()}>
{(page) => (
<>
<Row1 article={page[0]} noauthor={true} nodate={true} />
<Row2 articles={page.slice(1, 3)} isEqual={true} noauthor={true} />
<Row1 article={page[3]} noauthor={true} nodate={true} />
<Row2 articles={page.slice(4, 6)} isEqual={true} noauthor={true} />
<Row1 article={page[6]} noauthor={true} nodate={true} />
<Row2 articles={page.slice(7, 9)} isEqual={true} noauthor={true} />
</>
)}
</For>
</Show>
</Show>
</Show>
)
return ( return (
<div class={styles.authorPage}> <div class={styles.authorPage}>
<div class="wide-container"> <div class="wide-container">
@ -260,7 +229,34 @@ export const AuthorView = (props: AuthorViewProps) => {
</div> </div>
</Show> </Show>
<AuthorFeed /> <Show when={Array.isArray(props.shouts) && props.shouts.length > 0 && props.shouts[0]}>
<Row1 article={props.shouts?.[0] as Shout} noauthor={true} nodate={true} />
<Show when={props.shouts && props.shouts.length > 1}>
<Switch>
<Match when={props.shouts && props.shouts.length === 2}>
<Row2 articles={props.shouts as Shout[]} isEqual={true} noauthor={true} nodate={true} />
</Match>
<Match when={props.shouts && props.shouts.length === 3}>
<Row3 articles={props.shouts as Shout[]} noauthor={true} nodate={true} />
</Match>
<Match when={props.shouts && props.shouts.length > 3}>
<For each={pages()}>
{(page) => (
<>
<Row1 article={page[0]} noauthor={true} nodate={true} />
<Row2 articles={page.slice(1, 3)} isEqual={true} noauthor={true} />
<Row1 article={page[3]} noauthor={true} nodate={true} />
<Row2 articles={page.slice(4, 6)} isEqual={true} noauthor={true} />
<Row1 article={page[6]} noauthor={true} nodate={true} />
<Row2 articles={page.slice(7, 9)} isEqual={true} noauthor={true} />
</>
)}
</For>
</Match>
</Switch>
</Show>
</Show>
</Match> </Match>
</Switch> </Switch>
</div> </div>

View File

@ -9,27 +9,6 @@
} }
} }
.invert {
filter: invert(100%);
}
.rotating {
/* Define the keyframes for the animation */
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Apply the animation to the element */
animation: rotate .7s ease-out infinite; /* Rotate infinitely over 2 seconds using a linear timing function */
}
.notificationsCounter { .notificationsCounter {
@include media-breakpoint-up(md) { @include media-breakpoint-up(md) {
left: 1.8rem; left: 1.8rem;

View File

@ -1,6 +1,8 @@
import { Link } from '@solidjs/meta'
import type { JSX } from 'solid-js' import type { JSX } from 'solid-js'
import { Link } from '@solidjs/meta'
import { splitProps } from 'solid-js' import { splitProps } from 'solid-js'
import { getImageUrl } from '~/lib/getThumbUrl' import { getImageUrl } from '~/lib/getThumbUrl'
type Props = JSX.ImgHTMLAttributes<HTMLImageElement> & { type Props = JSX.ImgHTMLAttributes<HTMLImageElement> & {

View File

@ -5,7 +5,6 @@ import { Author, Reaction, Shout } from '~/graphql/schema/core.gen'
import { byCreated } from '~/lib/sort' import { byCreated } from '~/lib/sort'
import { SortFunction } from '~/types/common' import { SortFunction } from '~/types/common'
import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll' import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll'
import { Loading } from './Loading'
export type LoadMoreItems = Shout[] | Author[] | Reaction[] export type LoadMoreItems = Shout[] | Author[] | Reaction[]
@ -53,12 +52,14 @@ export const LoadMoreWrapper = (props: LoadMoreProps) => {
return ( return (
<> <>
{props.children} {props.children}
<Show when={isLoading()}> <Show when={isLoadMoreButtonVisible() && !props.hidden}>
<Loading />
</Show>
<Show when={isLoadMoreButtonVisible() && !props.hidden && !isLoading()}>
<div class="load-more-container"> <div class="load-more-container">
<Button onClick={loadItems} value={t('Load more')} title={`${items().length} ${t('loaded')}`} /> <Button
onClick={loadItems}
disabled={isLoading()}
value={t('Load more')}
title={`${items().length} ${t('loaded')}`}
/>
</div> </div>
</Show> </Show>
</> </>

View File

@ -125,7 +125,7 @@ export const AuthorsProvider = (props: { children: JSX.Element }) => {
})) }))
// Определяем функцию сортировки по рейтингу // Определяем функцию сортировки по рейтингу
const sortByRating: SortFunction<{ slug: string; rating: number }> = (a, b) => a.rating - b.rating const sortByRating: SortFunction<{ slug: string; rating: number }> = (a, b) => b.rating - a.rating
// Фильтруем и сортируем авторов // Фильтруем и сортируем авторов
const sortedTopAuthors = filterAndSort(authors, sortByRating) const sortedTopAuthors = filterAndSort(authors, sortByRating)

View File

@ -2,12 +2,7 @@ import type { JSX } from 'solid-js'
import { createContext, onCleanup, useContext } from 'solid-js' import { createContext, onCleanup, useContext } from 'solid-js'
import { createStore, reconcile } from 'solid-js/store' import { createStore, reconcile } from 'solid-js/store'
import { import { loadReactions } from '~/graphql/api/public'
loadCommentRatings,
loadReactions,
loadShoutComments,
loadShoutRatings
} from '~/graphql/api/public'
import createReactionMutation from '~/graphql/mutation/core/reaction-create' import createReactionMutation from '~/graphql/mutation/core/reaction-create'
import destroyReactionMutation from '~/graphql/mutation/core/reaction-destroy' import destroyReactionMutation from '~/graphql/mutation/core/reaction-destroy'
import updateReactionMutation from '~/graphql/mutation/core/reaction-update' import updateReactionMutation from '~/graphql/mutation/core/reaction-update'
@ -22,16 +17,10 @@ import { useGraphQL } from './graphql'
import { useLocalize } from './localize' import { useLocalize } from './localize'
import { useSnackbar } from './ui' import { useSnackbar } from './ui'
export const COMMENTS_PER_PAGE = 50
export const RATINGS_PER_PAGE = 100
type ReactionsContextType = { type ReactionsContextType = {
reactionEntities: Record<number, Reaction> reactionEntities: Record<number, Reaction>
reactionsByShout: Record<string, Reaction[]> reactionsByShout: Record<string, Reaction[]>
loadReactionsBy: (args: QueryLoad_Reactions_ByArgs) => Promise<Reaction[]> loadReactionsBy: (args: QueryLoad_Reactions_ByArgs) => Promise<Reaction[]>
loadShoutComments: (shout: number, limit?: number, offset?: number) => Promise<Reaction[]>
loadShoutRatings: (shout: number, limit?: number, offset?: number) => Promise<Reaction[]>
loadCommentRatings: (comment: number, limit?: number, offset?: number) => Promise<Reaction[]>
createReaction: (reaction: MutationCreate_ReactionArgs) => Promise<void> createReaction: (reaction: MutationCreate_ReactionArgs) => Promise<void>
updateReaction: (reaction: MutationUpdate_ReactionArgs) => Promise<Reaction> updateReaction: (reaction: MutationUpdate_ReactionArgs) => Promise<Reaction>
deleteReaction: (id: number) => Promise<{ error: string } | null> deleteReaction: (id: number) => Promise<{ error: string } | null>
@ -74,42 +63,6 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => {
return result return result
} }
const loadShoutRatingsAdding = async (
shout: number,
limit = RATINGS_PER_PAGE,
offset = 0
): Promise<Reaction[]> => {
const fetcher = await loadShoutRatings({ shout, limit, offset })
const result = (await fetcher()) || []
console.debug('[context.reactions] shout ratings loaded', result)
result && addReactions(result)
return result
}
const loadCommentRatingsAdding = async (
comment: number,
limit = RATINGS_PER_PAGE,
offset = 0
): Promise<Reaction[]> => {
const fetcher = await loadCommentRatings({ comment, limit, offset })
const result = (await fetcher()) || []
console.debug('[context.reactions] shout ratings loaded', result)
result && addReactions(result)
return result
}
const loadShoutCommentsAdding = async (
shout: number,
limit = COMMENTS_PER_PAGE,
offset = 0
): Promise<Reaction[]> => {
const fetcher = await loadShoutComments({ shout, limit, offset })
const result = (await fetcher()) || []
console.debug('[context.reactions] shout comments loaded', result)
result && addReactions(result)
return result
}
const createReaction = async (input: MutationCreate_ReactionArgs): Promise<void> => { const createReaction = async (input: MutationCreate_ReactionArgs): Promise<void> => {
const resp = await mutation(createReactionMutation, input).toPromise() const resp = await mutation(createReactionMutation, input).toPromise()
const { error, reaction } = resp?.data?.create_reaction || {} const { error, reaction } = resp?.data?.create_reaction || {}
@ -169,9 +122,6 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => {
const actions = { const actions = {
loadReactionsBy, loadReactionsBy,
loadShoutComments: loadShoutCommentsAdding,
loadShoutRatings: loadShoutRatingsAdding,
loadCommentRatings: loadCommentRatingsAdding,
createReaction, createReaction,
updateReaction, updateReaction,
deleteReaction, deleteReaction,

View File

@ -1,14 +1,11 @@
import { cache } from '@solidjs/router' import { cache } from '@solidjs/router'
import { defaultClient } from '~/context/graphql' import { defaultClient } from '~/context/graphql'
import loadShoutCommentsQuery from '~/graphql/query/core/article-comments-load'
import getShoutQuery from '~/graphql/query/core/article-load' import getShoutQuery from '~/graphql/query/core/article-load'
import loadShoutRatingsQuery from '~/graphql/query/core/article-ratings-load'
import loadShoutsByQuery from '~/graphql/query/core/articles-load-by' import loadShoutsByQuery from '~/graphql/query/core/articles-load-by'
import loadShoutsSearchQuery from '~/graphql/query/core/articles-load-search' import loadShoutsSearchQuery from '~/graphql/query/core/articles-load-search'
import getAuthorQuery from '~/graphql/query/core/author-by' import getAuthorQuery from '~/graphql/query/core/author-by'
import loadAuthorsAllQuery from '~/graphql/query/core/authors-all' import loadAuthorsAllQuery from '~/graphql/query/core/authors-all'
import loadAuthorsByQuery from '~/graphql/query/core/authors-load-by' import loadAuthorsByQuery from '~/graphql/query/core/authors-load-by'
import loadCommentRatingsQuery from '~/graphql/query/core/comment-ratings-load'
import loadReactionsByQuery from '~/graphql/query/core/reactions-load-by' import loadReactionsByQuery from '~/graphql/query/core/reactions-load-by'
import loadFollowersByTopicQuery from '~/graphql/query/core/topic-followers' import loadFollowersByTopicQuery from '~/graphql/query/core/topic-followers'
import loadTopicsQuery from '~/graphql/query/core/topics-all' import loadTopicsQuery from '~/graphql/query/core/topics-all'
@ -19,7 +16,6 @@ import {
QueryGet_ShoutArgs, QueryGet_ShoutArgs,
QueryLoad_Authors_ByArgs, QueryLoad_Authors_ByArgs,
QueryLoad_Reactions_ByArgs, QueryLoad_Reactions_ByArgs,
QueryLoad_Shout_RatingsArgs,
QueryLoad_Shouts_SearchArgs, QueryLoad_Shouts_SearchArgs,
Reaction, Reaction,
Shout, Shout,
@ -61,39 +57,16 @@ export const loadShouts = (options: LoadShoutsOptions) => {
}, `shouts-${filter}-${page}`) }, `shouts-${filter}-${page}`)
} }
export const loadShoutComments = (options: QueryLoad_Shout_RatingsArgs) => {
const page = `${options.offset || 0}-${(options.limit || 1) + (options.offset || 0)}`
return cache(async () => {
const resp = await defaultClient.query(loadShoutCommentsQuery, options).toPromise()
const result = resp?.data?.load_reactions_by
if (result) return result as Reaction[]
}, `shout-${options.shout}-comments-${page}`)
}
export const loadShoutRatings = (options: QueryLoad_Shout_RatingsArgs) => {
const page = `${options.offset || 0}-${(options.limit || 1) + (options.offset || 0)}`
return cache(async () => {
const resp = await defaultClient.query(loadShoutRatingsQuery, options).toPromise()
const result = resp?.data?.load_reactions_by
if (result) return result as Reaction[]
}, `shout-${options.shout}-ratings-${page}`)
}
// biome-ignore lint/suspicious/noExplicitAny: FIXME: wait backend
export const loadCommentRatings = (options: any) => {
const page = `${options.offset || 0}-${(options.limit || 1) + (options.offset || 0)}`
return cache(async () => {
const resp = await defaultClient.query(loadCommentRatingsQuery, options).toPromise()
const result = resp?.data?.load_reactions_by
if (result) return result as Reaction[]
}, `comment-${options.comment}-ratings-${page}`)
}
export const loadReactions = (options: QueryLoad_Reactions_ByArgs) => { export const loadReactions = (options: QueryLoad_Reactions_ByArgs) => {
if (!options.by) {
console.debug(options)
throw new Error('[api] wrong loadReactions call')
}
const kind = options.by?.comment ? 'comments' : options.by?.rating ? 'votes' : 'reactions' const kind = options.by?.comment ? 'comments' : options.by?.rating ? 'votes' : 'reactions'
const allorone = options.by?.shout ? `shout-${options.by.shout}` : 'all' const allorone = options.by?.shout ? `shout-${options.by.shout}` : 'all'
const page = `${options.offset || 0}-${(options?.limit || 0) + (options.offset || 0)}` const page = `${options.offset || 0}-${(options?.limit || 0) + (options.offset || 0)}`
const filter = new URLSearchParams(options.by as Record<string, string>) const filter = new URLSearchParams(options.by as Record<string, string>)
// console.debug(options)
return cache(async () => { return cache(async () => {
const resp = await defaultClient.query(loadReactionsByQuery, options).toPromise() const resp = await defaultClient.query(loadReactionsByQuery, options).toPromise()
const result = resp?.data?.load_reactions_by const result = resp?.data?.load_reactions_by
@ -102,6 +75,7 @@ export const loadReactions = (options: QueryLoad_Reactions_ByArgs) => {
} }
export const getShout = (options: QueryGet_ShoutArgs) => { export const getShout = (options: QueryGet_ShoutArgs) => {
// console.debug('[lib.api] get shout options', options)
return cache( return cache(
async () => { async () => {
const resp = await defaultClient.query(getShoutQuery, { ...options }).toPromise() const resp = await defaultClient.query(getShoutQuery, { ...options }).toPromise()

View File

@ -18,7 +18,6 @@ export default gql`
slug slug
} }
created_by { created_by {
id
name name
slug slug
pic pic

View File

@ -1,8 +1,8 @@
import { gql } from '@urql/core' import { gql } from '@urql/core'
export default gql` export default gql`
mutation DeleteReactionMutation($reaction: Int!) { mutation DeleteReactionMutation($reaction_id: Int!) {
delete_reaction(reaction_id: $reaction) { delete_reaction(reaction_id: $reaction_id) {
error error
reaction { reaction {
id id

View File

@ -1,29 +0,0 @@
import { gql } from '@urql/core'
export default gql`
query LoadReactions($shout: Int!, $limit: Int, $offset: Int) {
load_shout_comments(shout: $shout, limit: $limit, offset: $offset) {
id
kind
body
reply_to
shout {
id
slug
title
}
created_by {
id
name
slug
pic
created_at
}
created_at
updated_at
stat {
rating
}
}
}
`

View File

@ -1,29 +0,0 @@
import { gql } from '@urql/core'
export default gql`
query LoadReactions($shout: Int!, $limit: Int, $offset: Int) {
load_shout_ratings(shout: $shout, limit: $limit, offset: $offset) {
id
kind
body
reply_to
shout {
id
slug
title
}
created_by {
id
name
slug
pic
created_at
}
created_at
updated_at
stat {
rating
}
}
}
`

View File

@ -1,29 +0,0 @@
import { gql } from '@urql/core'
export default gql`
query LoadReactions($comment: Int!, $limit: Int, $offset: Int) {
load_comment_ratings(comment: $comment, limit: $limit, offset: $offset) {
id
kind
body
reply_to
shout {
id
slug
title
}
created_by {
id
name
slug
pic
created_at
}
created_at
updated_at
stat {
rating
}
}
}
`

View File

@ -13,7 +13,6 @@ export default gql`
title title
} }
created_by { created_by {
id
name name
slug slug
pic pic

View File

@ -456,7 +456,7 @@
"Theory": "Теории", "Theory": "Теории",
"There are unsaved changes in your profile settings. Are you sure you want to leave the page without saving?": "В настройках вашего профиля есть несохраненные изменения. Уверены, что хотите покинуть страницу без сохранения?", "There are unsaved changes in your profile settings. Are you sure you want to leave the page without saving?": "В настройках вашего профиля есть несохраненные изменения. Уверены, что хотите покинуть страницу без сохранения?",
"There are unsaved changes in your publishing settings. Are you sure you want to leave the page without saving?": "В настройках публикации есть несохраненные изменения. Уверены, что хотите покинуть страницу без сохранения?", "There are unsaved changes in your publishing settings. Are you sure you want to leave the page without saving?": "В настройках публикации есть несохраненные изменения. Уверены, что хотите покинуть страницу без сохранения?",
"This comment has not been rated yet": "Этот комментарий еще пока никто не оценил", "This comment has not yet been rated": "Этот комментарий еще пока никто не оценил",
"This content is not published yet": "Содержимое ещё не опубликовано", "This content is not published yet": "Содержимое ещё не опубликовано",
"This email is": "Этот email", "This email is": "Этот email",
"This email is not verified": "Этот email не подтвержден", "This email is not verified": "Этот email не подтвержден",

View File

@ -34,7 +34,7 @@ export const byStat = (metric: string) => {
return (a: { stat?: SomeStat }, b: { stat?: SomeStat }) => { return (a: { stat?: SomeStat }, b: { stat?: SomeStat }) => {
const aStat = a.stat?.[metric] ?? 0 const aStat = a.stat?.[metric] ?? 0
const bStat = b.stat?.[metric] ?? 0 const bStat = b.stat?.[metric] ?? 0
return bStat - aStat return aStat - bStat
} }
} }

View File

@ -1,29 +1,38 @@
import { RouteDefinition, RoutePreloadFuncArgs, type RouteSectionProps, createAsync } from '@solidjs/router' import { RouteDefinition, RouteLoadFuncArgs, type RouteSectionProps, createAsync } from '@solidjs/router'
import { Suspense, createEffect, on } from 'solid-js' import { Suspense, createEffect, on } from 'solid-js'
import { AllAuthors } from '~/components/Views/AllAuthors' import { AllAuthors } from '~/components/Views/AllAuthors'
import { AUTHORS_PER_PAGE } from '~/components/Views/AllAuthors/AllAuthors'
import { Loading } from '~/components/_shared/Loading' import { Loading } from '~/components/_shared/Loading'
import { PageLayout } from '~/components/_shared/PageLayout' import { PageLayout } from '~/components/_shared/PageLayout'
import { useAuthors } from '~/context/authors' import { useAuthors } from '~/context/authors'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { loadAuthorsAll } from '~/graphql/api/public' import { loadAuthors, loadAuthorsAll } from '~/graphql/api/public'
import { Author } from '~/graphql/schema/core.gen' import { Author, AuthorsBy } from '~/graphql/schema/core.gen'
const fetchAuthorsWithStat = async (offset = 0, order?: string) => {
const by: AuthorsBy = { order }
const authorsFetcher = loadAuthors({ by, offset, limit: AUTHORS_PER_PAGE })
return await authorsFetcher()
}
// Fetch Function
const fetchAllAuthors = async () => { const fetchAllAuthors = async () => {
const authorsAllFetcher = loadAuthorsAll() const authorsAllFetcher = loadAuthorsAll()
return await authorsAllFetcher() return await authorsAllFetcher()
} }
//Route Defenition
export const route = { export const route = {
load: async ({ location: { query: _q } }: RoutePreloadFuncArgs) => { load: async ({ location: { query } }: RouteLoadFuncArgs) => {
const by = query.by
const isAll = !by || by === 'name'
return { return {
authors: await fetchAllAuthors() authors: isAll && (await fetchAllAuthors()),
} authorsByFollowers: await fetchAuthorsWithStat(10, 'followers'),
authorsByShouts: await fetchAuthorsWithStat(10, 'shouts')
} as AllAuthorsData
} }
} satisfies RouteDefinition } satisfies RouteDefinition
type AllAuthorsData = { authors: Author[] } type AllAuthorsData = { authors: Author[]; authorsByFollowers: Author[]; authorsByShouts: Author[] }
// addAuthors to context // addAuthors to context
@ -31,20 +40,25 @@ export default function AllAuthorsPage(props: RouteSectionProps<AllAuthorsData>)
const { t } = useLocalize() const { t } = useLocalize()
const { addAuthors } = useAuthors() const { addAuthors } = useAuthors()
// async load data: from ssr or fetch
const data = createAsync<AllAuthorsData>(async () => { const data = createAsync<AllAuthorsData>(async () => {
if (props.data) return props.data if (props.data) return props.data
const authors = await fetchAllAuthors()
return { return {
authors: authors || [] authors: await fetchAllAuthors(),
} authorsByFollowers: await fetchAuthorsWithStat(10, 'followers'),
authorsByShouts: await fetchAuthorsWithStat(10, 'shouts')
} as AllAuthorsData
}) })
// update context when data is loaded
createEffect( createEffect(
on( on(
[data, () => addAuthors], [data, () => addAuthors],
([data, aa]) => { ([data, aa]) => {
if (data && aa) { if (data && aa) {
aa(data.authors as Author[]) aa(data.authors as Author[])
aa(data.authorsByFollowers as Author[])
aa(data.authorsByShouts as Author[])
console.debug('[routes.author] added all authors:', data.authors) console.debug('[routes.author] added all authors:', data.authors)
} }
}, },
@ -59,7 +73,12 @@ export default function AllAuthorsPage(props: RouteSectionProps<AllAuthorsData>)
desc="List of authors of the open editorial community" desc="List of authors of the open editorial community"
> >
<Suspense fallback={<Loading />}> <Suspense fallback={<Loading />}>
<AllAuthors isLoaded={Boolean(data()?.authors)} authors={data()?.authors || []} /> <AllAuthors
isLoaded={Boolean(data()?.authors)}
authors={data()?.authors || []}
authorsByFollowers={data()?.authorsByFollowers}
authorsByShouts={data()?.authorsByShouts}
/>
</Suspense> </Suspense>
</PageLayout> </PageLayout>
) )

View File

@ -1,5 +1,5 @@
import { RouteSectionProps, createAsync } from '@solidjs/router' import { RouteSectionProps } from '@solidjs/router'
import { ErrorBoundary, createEffect, createMemo } from 'solid-js' import { ErrorBoundary, createEffect, createMemo, createSignal, on } from 'solid-js'
import { AuthorView } from '~/components/Views/Author' import { AuthorView } from '~/components/Views/Author'
import { FourOuFourView } from '~/components/Views/FourOuFour' import { FourOuFourView } from '~/components/Views/FourOuFour'
import { LoadMoreItems, LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper' import { LoadMoreItems, LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper'
@ -32,7 +32,6 @@ const fetchAllTopics = async () => {
const fetchAuthor = async (slug: string) => { const fetchAuthor = async (slug: string) => {
const authorFetcher = loadAuthors({ by: { slug }, limit: 1, offset: 0 } as QueryLoad_Authors_ByArgs) const authorFetcher = loadAuthors({ by: { slug }, limit: 1, offset: 0 } as QueryLoad_Authors_ByArgs)
const aaa = await authorFetcher() const aaa = await authorFetcher()
console.debug(aaa)
return aaa?.[0] return aaa?.[0]
} }
@ -50,20 +49,11 @@ export const route = {
export type AuthorPageProps = { articles?: Shout[]; author?: Author; topics?: Topic[] } export type AuthorPageProps = { articles?: Shout[]; author?: Author; topics?: Topic[] }
export default function AuthorPage(props: RouteSectionProps<AuthorPageProps>) { export default function AuthorPage(props: RouteSectionProps<AuthorPageProps>) {
const { authorsEntities } = useAuthors() const { addAuthor, authorsEntities } = useAuthors()
const { addFeed, feedByAuthor } = useFeed() const [author, setAuthor] = createSignal<Author | undefined>(undefined)
const { t } = useLocalize() const { t } = useLocalize()
const author = createAsync(
async () =>
props.data.author || authorsEntities()[props.params.slug] || (await fetchAuthor(props.params.slug))
)
const shoutsByAuthor = createMemo(() => feedByAuthor()[props.params.slug])
const title = createMemo(() => `${author()?.name || ''}`) const title = createMemo(() => `${author()?.name || ''}`)
const cover = createMemo(() =>
author()?.pic
? getImageUrl(author()?.pic || '', { width: 1200 })
: getImageUrl('production/image/logo_image.png')
)
createEffect(() => { createEffect(() => {
if (author()) { if (author()) {
@ -76,7 +66,32 @@ export default function AuthorPage(props: RouteSectionProps<AuthorPageProps>) {
} }
}) })
const cover = createMemo(() =>
author()?.pic
? getImageUrl(author()?.pic || '', { width: 1200 })
: getImageUrl('production/image/logo_image.png')
)
// author shouts // author shouts
const { addFeed, feedByAuthor } = useFeed()
const shoutsByAuthor = createMemo(() => feedByAuthor()[props.params.slug])
createEffect(
on(
[() => props.params.slug || '', author],
async ([slug, profile]) => {
if (!profile) {
const loadedAuthor = authorsEntities()[slug] || (await fetchAuthor(slug))
if (loadedAuthor) {
addAuthor(loadedAuthor)
setAuthor(loadedAuthor)
}
}
},
{ defer: true }
)
)
const loadAuthorShoutsMore = async (offset: number) => { const loadAuthorShoutsMore = async (offset: number) => {
const loadedShouts = await fetchAuthorShouts(props.params.slug, offset) const loadedShouts = await fetchAuthorShouts(props.params.slug, offset)
loadedShouts && addFeed(loadedShouts) loadedShouts && addFeed(loadedShouts)

View File

@ -125,7 +125,7 @@ export default (props: RouteSectionProps<{ shouts: Shout[]; topics: Topic[] }>)
key="feed" key="feed"
desc="Independent media project about culture, science, art and society with horizontal editing" desc="Independent media project about culture, science, art and society with horizontal editing"
> >
<LoadMoreWrapper loadFunction={loadMoreFeed} pageSize={AUTHORS_PER_PAGE} hidden={!feed()}> <LoadMoreWrapper loadFunction={loadMoreFeed} pageSize={AUTHORS_PER_PAGE}>
<ReactionsProvider> <ReactionsProvider>
<Feed shouts={feed() || (shouts() as Shout[])} order={order() as FeedProps['order']} /> <Feed shouts={feed() || (shouts() as Shout[])} order={order() as FeedProps['order']} />
</ReactionsProvider> </ReactionsProvider>