mirror of
https://github.com/cheeaun/phanpy.git
synced 2025-03-22 13:49:23 +01:00
commit
87f1d17ce3
44 changed files with 1614 additions and 552 deletions
124
package-lock.json
generated
124
package-lock.json
generated
|
@ -9,11 +9,12 @@
|
|||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@formatjs/intl-localematcher": "~0.4.2",
|
||||
"@formkit/auto-animate": "~0.8.0",
|
||||
"@github/text-expander-element": "~2.5.0",
|
||||
"@iconify-icons/mingcute": "~1.2.8",
|
||||
"@iconify-icons/mingcute": "~1.2.9",
|
||||
"@justinribeiro/lite-youtube": "~1.5.0",
|
||||
"@szhsin/react-menu": "~4.1.0",
|
||||
"@uidotdev/usehooks": "~2.4.0",
|
||||
"@uidotdev/usehooks": "~2.4.1",
|
||||
"dayjs": "~1.11.10",
|
||||
"dayjs-twitter": "~0.5.0",
|
||||
"fast-blurhash": "~1.1.2",
|
||||
|
@ -21,7 +22,7 @@
|
|||
"idb-keyval": "~6.2.1",
|
||||
"just-debounce-it": "~3.2.0",
|
||||
"lz-string": "~1.5.0",
|
||||
"masto": "~6.3.1",
|
||||
"masto": "~6.3.3",
|
||||
"moize": "~6.1.6",
|
||||
"p-retry": "~6.1.0",
|
||||
"p-throttle": "~5.1.0",
|
||||
|
@ -41,12 +42,12 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@preact/preset-vite": "~2.6.0",
|
||||
"@trivago/prettier-plugin-sort-imports": "~4.2.0",
|
||||
"@trivago/prettier-plugin-sort-imports": "~4.2.1",
|
||||
"postcss": "~8.4.31",
|
||||
"postcss-dark-theme-class": "~1.0.0",
|
||||
"postcss-preset-env": "~9.2.0",
|
||||
"twitter-text": "~3.1.0",
|
||||
"vite": "~4.4.11",
|
||||
"vite": "~4.5.0",
|
||||
"vite-plugin-generate-file": "~0.0.4",
|
||||
"vite-plugin-html-config": "~1.0.11",
|
||||
"vite-plugin-pwa": "~0.16.5",
|
||||
|
@ -3043,6 +3044,11 @@
|
|||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formkit/auto-animate": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@formkit/auto-animate/-/auto-animate-0.8.0.tgz",
|
||||
"integrity": "sha512-G8f7489ka0mWyi+1IEZT+xgIwcpWtRMmE2x+IrVoQ+KM1cP6VDj/TbujZjwxdb0P8w8b16/qBfViRmydbYHwMw=="
|
||||
},
|
||||
"node_modules/@github/combobox-nav": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@github/combobox-nav/-/combobox-nav-2.1.5.tgz",
|
||||
|
@ -3057,9 +3063,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@iconify-icons/mingcute": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@iconify-icons/mingcute/-/mingcute-1.2.8.tgz",
|
||||
"integrity": "sha512-9mH0dn/rtsKvaR/P57LgTB8IGoN3ePxCiap3EQfmNSu1x+w2ib478HHxUnXdg1WpyRFbX81aFtUDvq7yuSOyeg==",
|
||||
"version": "1.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@iconify-icons/mingcute/-/mingcute-1.2.9.tgz",
|
||||
"integrity": "sha512-u+hX7mh7amKlWFHOTi52tnJ52NWQVAFevjDcQkZvK4Zj2UyVVKZ45yKBsFHo4OTJDzBkIafJh4C4fkPJsvCtOA==",
|
||||
"dependencies": {
|
||||
"@iconify/types": "*"
|
||||
}
|
||||
|
@ -3298,14 +3304,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@trivago/prettier-plugin-sort-imports": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.2.0.tgz",
|
||||
"integrity": "sha512-YBepjbt+ZNBVmN3ev1amQH3lWCmHyt5qTbLCp/syXJRu/Kw2koXh44qayB1gMRxcL/gV8egmjN5xWSrYyfUtyw==",
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.2.1.tgz",
|
||||
"integrity": "sha512-iuy2MPVURGdxILTchHr15VAioItuYBejKfcTmQFlxIuqA7jeaT6ngr5aUIG6S6U096d6a6lJCgaOwlRrPLlOPg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/generator": "7.17.7",
|
||||
"@babel/parser": "^7.20.5",
|
||||
"@babel/traverse": "7.17.3",
|
||||
"@babel/traverse": "7.23.2",
|
||||
"@babel/types": "7.17.0",
|
||||
"javascript-natural-sort": "0.7.1",
|
||||
"lodash": "^4.17.21"
|
||||
|
@ -3334,27 +3340,6 @@
|
|||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@trivago/prettier-plugin-sort-imports/node_modules/@babel/traverse": {
|
||||
"version": "7.17.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.3.tgz",
|
||||
"integrity": "sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.16.7",
|
||||
"@babel/generator": "^7.17.3",
|
||||
"@babel/helper-environment-visitor": "^7.16.7",
|
||||
"@babel/helper-function-name": "^7.16.7",
|
||||
"@babel/helper-hoist-variables": "^7.16.7",
|
||||
"@babel/helper-split-export-declaration": "^7.16.7",
|
||||
"@babel/parser": "^7.17.3",
|
||||
"@babel/types": "^7.17.0",
|
||||
"debug": "^4.1.0",
|
||||
"globals": "^11.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@trivago/prettier-plugin-sort-imports/node_modules/@babel/types": {
|
||||
"version": "7.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz",
|
||||
|
@ -3410,9 +3395,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/@uidotdev/usehooks": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@uidotdev/usehooks/-/usehooks-2.4.0.tgz",
|
||||
"integrity": "sha512-NrpTsZUGsawYxFbEXrd8+FPpfziC4M01GSQgYWOnGa84UiavqVCzCL5bSRe6rfQc4QsHS2rGAA0h63ya/j+p6A==",
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@uidotdev/usehooks/-/usehooks-2.4.1.tgz",
|
||||
"integrity": "sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg==",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
|
@ -5272,9 +5257,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/masto": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/masto/-/masto-6.3.1.tgz",
|
||||
"integrity": "sha512-Os3MlbGFNL6KHxlKldYY+d/1exO6oBjtF4vx8d6cmXRmeeeW3mKQeunTZz+yY5qWksPg2eVdk+FOhaEnOeclVw==",
|
||||
"version": "6.3.3",
|
||||
"resolved": "https://registry.npmjs.org/masto/-/masto-6.3.3.tgz",
|
||||
"integrity": "sha512-hmDsiscImeZfpkS+5oEWk3w5mkbxERFKN/UpuaoKZpVWWoGWCNnO7iPfQHygs/phP7PQqS6pVHlE5ylqSylf6A==",
|
||||
"dependencies": {
|
||||
"change-case": "^4.1.2",
|
||||
"events-to-async": "^2.0.0",
|
||||
|
@ -7260,9 +7245,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "4.4.11",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.11.tgz",
|
||||
"integrity": "sha512-ksNZJlkcU9b0lBwAGZGGaZHCMqHsc8OpgtoYhsQ4/I2v5cnpmmmqe5pM4nv/4Hn6G/2GhTdj0DhZh2e+Er1q5A==",
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz",
|
||||
"integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.18.10",
|
||||
|
@ -9558,6 +9543,11 @@
|
|||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"@formkit/auto-animate": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@formkit/auto-animate/-/auto-animate-0.8.0.tgz",
|
||||
"integrity": "sha512-G8f7489ka0mWyi+1IEZT+xgIwcpWtRMmE2x+IrVoQ+KM1cP6VDj/TbujZjwxdb0P8w8b16/qBfViRmydbYHwMw=="
|
||||
},
|
||||
"@github/combobox-nav": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@github/combobox-nav/-/combobox-nav-2.1.5.tgz",
|
||||
|
@ -9572,9 +9562,9 @@
|
|||
}
|
||||
},
|
||||
"@iconify-icons/mingcute": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@iconify-icons/mingcute/-/mingcute-1.2.8.tgz",
|
||||
"integrity": "sha512-9mH0dn/rtsKvaR/P57LgTB8IGoN3ePxCiap3EQfmNSu1x+w2ib478HHxUnXdg1WpyRFbX81aFtUDvq7yuSOyeg==",
|
||||
"version": "1.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@iconify-icons/mingcute/-/mingcute-1.2.9.tgz",
|
||||
"integrity": "sha512-u+hX7mh7amKlWFHOTi52tnJ52NWQVAFevjDcQkZvK4Zj2UyVVKZ45yKBsFHo4OTJDzBkIafJh4C4fkPJsvCtOA==",
|
||||
"requires": {
|
||||
"@iconify/types": "*"
|
||||
}
|
||||
|
@ -9771,14 +9761,14 @@
|
|||
}
|
||||
},
|
||||
"@trivago/prettier-plugin-sort-imports": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.2.0.tgz",
|
||||
"integrity": "sha512-YBepjbt+ZNBVmN3ev1amQH3lWCmHyt5qTbLCp/syXJRu/Kw2koXh44qayB1gMRxcL/gV8egmjN5xWSrYyfUtyw==",
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.2.1.tgz",
|
||||
"integrity": "sha512-iuy2MPVURGdxILTchHr15VAioItuYBejKfcTmQFlxIuqA7jeaT6ngr5aUIG6S6U096d6a6lJCgaOwlRrPLlOPg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/generator": "7.17.7",
|
||||
"@babel/parser": "^7.20.5",
|
||||
"@babel/traverse": "7.17.3",
|
||||
"@babel/traverse": "7.23.2",
|
||||
"@babel/types": "7.17.0",
|
||||
"javascript-natural-sort": "0.7.1",
|
||||
"lodash": "^4.17.21"
|
||||
|
@ -9795,24 +9785,6 @@
|
|||
"source-map": "^0.5.0"
|
||||
}
|
||||
},
|
||||
"@babel/traverse": {
|
||||
"version": "7.17.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.3.tgz",
|
||||
"integrity": "sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.16.7",
|
||||
"@babel/generator": "^7.17.3",
|
||||
"@babel/helper-environment-visitor": "^7.16.7",
|
||||
"@babel/helper-function-name": "^7.16.7",
|
||||
"@babel/helper-hoist-variables": "^7.16.7",
|
||||
"@babel/helper-split-export-declaration": "^7.16.7",
|
||||
"@babel/parser": "^7.17.3",
|
||||
"@babel/types": "^7.17.0",
|
||||
"debug": "^4.1.0",
|
||||
"globals": "^11.1.0"
|
||||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz",
|
||||
|
@ -9864,9 +9836,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"@uidotdev/usehooks": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@uidotdev/usehooks/-/usehooks-2.4.0.tgz",
|
||||
"integrity": "sha512-NrpTsZUGsawYxFbEXrd8+FPpfziC4M01GSQgYWOnGa84UiavqVCzCL5bSRe6rfQc4QsHS2rGAA0h63ya/j+p6A==",
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@uidotdev/usehooks/-/usehooks-2.4.1.tgz",
|
||||
"integrity": "sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg==",
|
||||
"requires": {}
|
||||
},
|
||||
"@vue/compiler-core": {
|
||||
|
@ -11216,9 +11188,9 @@
|
|||
}
|
||||
},
|
||||
"masto": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/masto/-/masto-6.3.1.tgz",
|
||||
"integrity": "sha512-Os3MlbGFNL6KHxlKldYY+d/1exO6oBjtF4vx8d6cmXRmeeeW3mKQeunTZz+yY5qWksPg2eVdk+FOhaEnOeclVw==",
|
||||
"version": "6.3.3",
|
||||
"resolved": "https://registry.npmjs.org/masto/-/masto-6.3.3.tgz",
|
||||
"integrity": "sha512-hmDsiscImeZfpkS+5oEWk3w5mkbxERFKN/UpuaoKZpVWWoGWCNnO7iPfQHygs/phP7PQqS6pVHlE5ylqSylf6A==",
|
||||
"requires": {
|
||||
"change-case": "^4.1.2",
|
||||
"events-to-async": "^2.0.0",
|
||||
|
@ -12453,9 +12425,9 @@
|
|||
}
|
||||
},
|
||||
"vite": {
|
||||
"version": "4.4.11",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.11.tgz",
|
||||
"integrity": "sha512-ksNZJlkcU9b0lBwAGZGGaZHCMqHsc8OpgtoYhsQ4/I2v5cnpmmmqe5pM4nv/4Hn6G/2GhTdj0DhZh2e+Er1q5A==",
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz",
|
||||
"integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"esbuild": "^0.18.10",
|
||||
|
|
11
package.json
11
package.json
|
@ -11,11 +11,12 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@formatjs/intl-localematcher": "~0.4.2",
|
||||
"@formkit/auto-animate": "~0.8.0",
|
||||
"@github/text-expander-element": "~2.5.0",
|
||||
"@iconify-icons/mingcute": "~1.2.8",
|
||||
"@iconify-icons/mingcute": "~1.2.9",
|
||||
"@justinribeiro/lite-youtube": "~1.5.0",
|
||||
"@szhsin/react-menu": "~4.1.0",
|
||||
"@uidotdev/usehooks": "~2.4.0",
|
||||
"@uidotdev/usehooks": "~2.4.1",
|
||||
"dayjs": "~1.11.10",
|
||||
"dayjs-twitter": "~0.5.0",
|
||||
"fast-blurhash": "~1.1.2",
|
||||
|
@ -23,7 +24,7 @@
|
|||
"idb-keyval": "~6.2.1",
|
||||
"just-debounce-it": "~3.2.0",
|
||||
"lz-string": "~1.5.0",
|
||||
"masto": "~6.3.1",
|
||||
"masto": "~6.3.3",
|
||||
"moize": "~6.1.6",
|
||||
"p-retry": "~6.1.0",
|
||||
"p-throttle": "~5.1.0",
|
||||
|
@ -43,12 +44,12 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@preact/preset-vite": "~2.6.0",
|
||||
"@trivago/prettier-plugin-sort-imports": "~4.2.0",
|
||||
"@trivago/prettier-plugin-sort-imports": "~4.2.1",
|
||||
"postcss": "~8.4.31",
|
||||
"postcss-dark-theme-class": "~1.0.0",
|
||||
"postcss-preset-env": "~9.2.0",
|
||||
"twitter-text": "~3.1.0",
|
||||
"vite": "~4.4.11",
|
||||
"vite": "~4.5.0",
|
||||
"vite-plugin-generate-file": "~0.0.4",
|
||||
"vite-plugin-html-config": "~1.0.11",
|
||||
"vite-plugin-pwa": "~0.16.5",
|
||||
|
|
90
src/app.css
90
src/app.css
|
@ -86,6 +86,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
|||
width: var(--main-width);
|
||||
max-width: 100%;
|
||||
background-color: var(--bg-color);
|
||||
overflow-anchor: auto;
|
||||
}
|
||||
.deck.contained {
|
||||
overflow: auto;
|
||||
|
@ -273,6 +274,11 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
|||
background-color: var(--bg-color);
|
||||
box-shadow: inset 0 -3px var(--comment-line-color),
|
||||
inset 0 3px var(--comment-line-color);
|
||||
overscroll-behavior-x: contain;
|
||||
|
||||
.status-link {
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
.timeline.contextual .replies[data-comments-level='4'] {
|
||||
overflow-x: auto;
|
||||
|
@ -1051,13 +1057,13 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
|||
width: 100%;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
background-color: var(--average-color-alpha);
|
||||
background-image: radial-gradient(
|
||||
background-color: var(--accent-alpha-color);
|
||||
/* background-image: radial-gradient(
|
||||
closest-side,
|
||||
var(--average-color) 10%,
|
||||
var(--average-color-alpha) 40%,
|
||||
var(--accent-color) 10%,
|
||||
var(--accent-alpha-color) 40%,
|
||||
transparent 100%
|
||||
);
|
||||
); */
|
||||
}
|
||||
.carousel .carousel-item :is(img, video) {
|
||||
width: auto;
|
||||
|
@ -1372,7 +1378,8 @@ body:has(.media-modal-container + .status-deck) .media-post-link {
|
|||
margin-top: -44px;
|
||||
background-image: radial-gradient(
|
||||
circle,
|
||||
var(--bg-faded-color) 0px 14px,
|
||||
var(--bg-faded-color) 0px 13px,
|
||||
var(--outline-color) 13px 14px,
|
||||
transparent 14px
|
||||
);
|
||||
}
|
||||
|
@ -1792,6 +1799,7 @@ meter.donut[hidden] {
|
|||
background-image: none;
|
||||
box-shadow: 0 3px 8px -1px var(--drop-shadow-color),
|
||||
0 10px 36px -4px var(--button-bg-blur-color);
|
||||
text-align: center;
|
||||
}
|
||||
.toastify-bottom {
|
||||
margin-bottom: env(safe-area-inset-bottom);
|
||||
|
@ -2127,6 +2135,72 @@ ul.link-list li a .icon {
|
|||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.filter-field {
|
||||
flex-shrink: 0;
|
||||
padding: 8px 16px;
|
||||
border-radius: 999px;
|
||||
color: var(--text-color);
|
||||
background-color: var(--bg-color);
|
||||
background-image: none;
|
||||
border: 2px solid transparent;
|
||||
margin: 0;
|
||||
/* appearance: none; */
|
||||
line-height: 1;
|
||||
font-size: 90%;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
> .icon {
|
||||
color: var(--link-color);
|
||||
}
|
||||
|
||||
&:placeholder-shown {
|
||||
color: var(--text-insignificant-color);
|
||||
}
|
||||
|
||||
&:is(:hover, :focus-visible) {
|
||||
border-color: var(--link-light-color);
|
||||
}
|
||||
&:focus {
|
||||
outline-color: var(--link-light-color);
|
||||
}
|
||||
&.is-active {
|
||||
border-color: var(--link-color);
|
||||
box-shadow: inset 0 0 8px var(--link-faded-color);
|
||||
}
|
||||
|
||||
:is(input, select) {
|
||||
background-color: transparent;
|
||||
background-image: none;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
color: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
appearance: none;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input[type='month'] {
|
||||
min-width: 6em;
|
||||
|
||||
&::-webkit-calendar-picker-indicator {
|
||||
/* replace icon with triangle */
|
||||
opacity: 0.5;
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none"><path d="M4 6L8 10L12 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>');
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
&::-webkit-calendar-picker-indicator {
|
||||
filter: invert(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.filter-bar.centered {
|
||||
justify-content: center;
|
||||
|
@ -2229,6 +2303,10 @@ ul.link-list li a .icon {
|
|||
}
|
||||
.deck > header {
|
||||
text-shadow: 0 1px var(--bg-color);
|
||||
|
||||
form {
|
||||
text-shadow: none;
|
||||
}
|
||||
}
|
||||
.deck > header h1 {
|
||||
font-size: 1.5em;
|
||||
|
|
234
src/app.jsx
234
src/app.jsx
|
@ -1,5 +1,6 @@
|
|||
import './app.css';
|
||||
|
||||
import debounce from 'just-debounce-it';
|
||||
import {
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
|
@ -9,7 +10,7 @@ import {
|
|||
} from 'preact/hooks';
|
||||
import { matchPath, Route, Routes, useLocation } from 'react-router-dom';
|
||||
import 'swiped-events';
|
||||
import { useSnapshot } from 'valtio';
|
||||
import { subscribe, useSnapshot } from 'valtio';
|
||||
|
||||
import BackgroundService from './components/background-service';
|
||||
import ComposeButton from './components/compose-button';
|
||||
|
@ -68,8 +69,64 @@ setTimeout(() => {
|
|||
}
|
||||
}, 5000);
|
||||
|
||||
(() => {
|
||||
window.__IDLE__ = false;
|
||||
const nonIdleEvents = [
|
||||
'mousemove',
|
||||
'mousedown',
|
||||
'resize',
|
||||
'keydown',
|
||||
'touchstart',
|
||||
'pointerdown',
|
||||
'pointermove',
|
||||
'wheel',
|
||||
];
|
||||
const IDLE_TIME = 5_000; // 5 seconds
|
||||
const setIdle = debounce(() => {
|
||||
window.__IDLE__ = true;
|
||||
}, IDLE_TIME);
|
||||
const onNonIdle = () => {
|
||||
window.__IDLE__ = false;
|
||||
setIdle();
|
||||
};
|
||||
nonIdleEvents.forEach((event) => {
|
||||
window.addEventListener(event, onNonIdle, {
|
||||
passive: true,
|
||||
capture: true,
|
||||
});
|
||||
});
|
||||
// document.addEventListener(
|
||||
// 'visibilitychange',
|
||||
// () => {
|
||||
// if (document.visibilityState === 'visible') {
|
||||
// onNonIdle();
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// passive: true,
|
||||
// },
|
||||
// );
|
||||
})();
|
||||
|
||||
subscribe(states, (changes) => {
|
||||
for (const [action, path, value, prevValue] of changes) {
|
||||
// Change #app dataset based on settings.shortcutsViewMode
|
||||
if (path.join('.') === 'settings.shortcutsViewMode') {
|
||||
const $app = document.getElementById('app');
|
||||
if ($app) {
|
||||
$app.dataset.shortcutsViewMode = states.shortcuts?.length ? value : '';
|
||||
}
|
||||
}
|
||||
|
||||
// Add/Remove cloak class to body
|
||||
if (path.join('.') === 'settings.cloakMode') {
|
||||
const $body = document.body;
|
||||
$body.classList.toggle('cloak', value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function App() {
|
||||
const snapStates = useSnapshot(states);
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||
const [uiState, setUIState] = useState('loading');
|
||||
|
||||
|
@ -153,106 +210,27 @@ function App() {
|
|||
|
||||
let location = useLocation();
|
||||
states.currentLocation = location.pathname;
|
||||
// useLayoutEffect(() => {
|
||||
// states.currentLocation = location.pathname;
|
||||
// }, [location.pathname]);
|
||||
|
||||
useEffect(focusDeck, [location, isLoggedIn]);
|
||||
|
||||
const prevLocation = snapStates.prevLocation;
|
||||
const backgroundLocation = useRef(prevLocation || null);
|
||||
const isModalPage = useMemo(() => {
|
||||
return (
|
||||
matchPath('/:instance/s/:id', location.pathname) ||
|
||||
matchPath('/s/:id', location.pathname)
|
||||
);
|
||||
}, [location.pathname, matchPath]);
|
||||
if (isModalPage) {
|
||||
if (!backgroundLocation.current) backgroundLocation.current = prevLocation;
|
||||
} else {
|
||||
backgroundLocation.current = null;
|
||||
}
|
||||
console.debug({
|
||||
backgroundLocation: backgroundLocation.current,
|
||||
location,
|
||||
});
|
||||
|
||||
if (/\/https?:/.test(location.pathname)) {
|
||||
return <HttpRoute />;
|
||||
}
|
||||
|
||||
const nonRootLocation = useMemo(() => {
|
||||
const { pathname } = location;
|
||||
return !/^\/(login|welcome)/.test(pathname);
|
||||
}, [location]);
|
||||
|
||||
// Change #app dataset based on snapStates.settings.shortcutsViewMode
|
||||
useEffect(() => {
|
||||
const $app = document.getElementById('app');
|
||||
if ($app) {
|
||||
$app.dataset.shortcutsViewMode = snapStates.shortcuts?.length
|
||||
? snapStates.settings.shortcutsViewMode
|
||||
: '';
|
||||
}
|
||||
}, [snapStates.shortcuts, snapStates.settings.shortcutsViewMode]);
|
||||
|
||||
// Add/Remove cloak class to body
|
||||
useEffect(() => {
|
||||
const $body = document.body;
|
||||
$body.classList.toggle('cloak', snapStates.settings.cloakMode);
|
||||
}, [snapStates.settings.cloakMode]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Routes location={nonRootLocation || location}>
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
isLoggedIn ? (
|
||||
<Home />
|
||||
) : uiState === 'loading' ? (
|
||||
<Loader id="loader-root" />
|
||||
) : (
|
||||
<Welcome />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/welcome" element={<Welcome />} />
|
||||
</Routes>
|
||||
<Routes location={backgroundLocation.current || location}>
|
||||
{isLoggedIn && (
|
||||
<Route path="/notifications" element={<Notifications />} />
|
||||
)}
|
||||
{isLoggedIn && <Route path="/mentions" element={<Mentions />} />}
|
||||
{isLoggedIn && <Route path="/following" element={<Following />} />}
|
||||
{isLoggedIn && <Route path="/b" element={<Bookmarks />} />}
|
||||
{isLoggedIn && <Route path="/f" element={<Favourites />} />}
|
||||
{isLoggedIn && (
|
||||
<Route path="/l">
|
||||
<Route index element={<Lists />} />
|
||||
<Route path=":id" element={<List />} />
|
||||
</Route>
|
||||
)}
|
||||
{isLoggedIn && <Route path="/ft" element={<FollowedHashtags />} />}
|
||||
<Route path="/:instance?/t/:hashtag" element={<Hashtag />} />
|
||||
<Route path="/:instance?/a/:id" element={<AccountStatuses />} />
|
||||
<Route path="/:instance?/p">
|
||||
<Route index element={<Public />} />
|
||||
<Route path="l" element={<Public local />} />
|
||||
</Route>
|
||||
<Route path="/:instance?/trending" element={<Trending />} />
|
||||
<Route path="/:instance?/search" element={<Search />} />
|
||||
{/* <Route path="/:anything" element={<NotFound />} /> */}
|
||||
</Routes>
|
||||
<PrimaryRoutes isLoggedIn={isLoggedIn} loading={uiState === 'loading'} />
|
||||
<SecondaryRoutes isLoggedIn={isLoggedIn} />
|
||||
{uiState === 'default' && (
|
||||
<Routes>
|
||||
<Route path="/:instance?/s/:id" element={<StatusRoute />} />
|
||||
</Routes>
|
||||
)}
|
||||
{isLoggedIn && <ComposeButton />}
|
||||
{isLoggedIn &&
|
||||
!snapStates.settings.shortcutsColumnsMode &&
|
||||
snapStates.settings.shortcutsViewMode !== 'multi-column' && (
|
||||
<Shortcuts />
|
||||
)}
|
||||
{isLoggedIn && <Shortcuts />}
|
||||
<Modals />
|
||||
{isLoggedIn && <NotificationService />}
|
||||
<BackgroundService isLoggedIn={isLoggedIn} />
|
||||
|
@ -262,4 +240,86 @@ function App() {
|
|||
);
|
||||
}
|
||||
|
||||
function PrimaryRoutes({ isLoggedIn, loading }) {
|
||||
const location = useLocation();
|
||||
const nonRootLocation = useMemo(() => {
|
||||
const { pathname } = location;
|
||||
return !/^\/(login|welcome)/.test(pathname);
|
||||
}, [location]);
|
||||
|
||||
return (
|
||||
<Routes location={nonRootLocation || location}>
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
isLoggedIn ? (
|
||||
<Home />
|
||||
) : loading ? (
|
||||
<Loader id="loader-root" />
|
||||
) : (
|
||||
<Welcome />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/welcome" element={<Welcome />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
||||
function getPrevLocation() {
|
||||
return states.prevLocation || null;
|
||||
}
|
||||
function SecondaryRoutes({ isLoggedIn }) {
|
||||
// const snapStates = useSnapshot(states);
|
||||
const location = useLocation();
|
||||
// const prevLocation = snapStates.prevLocation;
|
||||
const backgroundLocation = useRef(getPrevLocation());
|
||||
|
||||
const isModalPage = useMemo(() => {
|
||||
return (
|
||||
matchPath('/:instance/s/:id', location.pathname) ||
|
||||
matchPath('/s/:id', location.pathname)
|
||||
);
|
||||
}, [location.pathname, matchPath]);
|
||||
if (isModalPage) {
|
||||
if (!backgroundLocation.current)
|
||||
backgroundLocation.current = getPrevLocation();
|
||||
} else {
|
||||
backgroundLocation.current = null;
|
||||
}
|
||||
console.debug({
|
||||
backgroundLocation: backgroundLocation.current,
|
||||
location,
|
||||
});
|
||||
|
||||
return (
|
||||
<Routes location={backgroundLocation.current || location}>
|
||||
{isLoggedIn && (
|
||||
<>
|
||||
<Route path="/notifications" element={<Notifications />} />
|
||||
<Route path="/mentions" element={<Mentions />} />
|
||||
<Route path="/following" element={<Following />} />
|
||||
<Route path="/b" element={<Bookmarks />} />
|
||||
<Route path="/f" element={<Favourites />} />
|
||||
<Route path="/l">
|
||||
<Route index element={<Lists />} />
|
||||
<Route path=":id" element={<List />} />
|
||||
</Route>
|
||||
<Route path="/ft" element={<FollowedHashtags />} />
|
||||
</>
|
||||
)}
|
||||
<Route path="/:instance?/t/:hashtag" element={<Hashtag />} />
|
||||
<Route path="/:instance?/a/:id" element={<AccountStatuses />} />
|
||||
<Route path="/:instance?/p">
|
||||
<Route index element={<Public />} />
|
||||
<Route path="l" element={<Public local />} />
|
||||
</Route>
|
||||
<Route path="/:instance?/trending" element={<Trending />} />
|
||||
<Route path="/:instance?/search" element={<Search />} />
|
||||
{/* <Route path="/:anything" element={<NotFound />} /> */}
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
||||
export { App };
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
.account-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
/* display: flex; */
|
||||
/* flex-direction: column; */
|
||||
/* overflow: hidden; */
|
||||
overflow-y: auto;
|
||||
max-width: 100%;
|
||||
--banner-overlap: 44px;
|
||||
}
|
||||
|
||||
.account-container.skeleton {
|
||||
|
@ -51,6 +53,7 @@
|
|||
|
||||
.account-container .header-banner {
|
||||
/* pointer-events: none; */
|
||||
vertical-align: top;
|
||||
aspect-ratio: 6 / 1;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
|
@ -75,7 +78,7 @@
|
|||
hsla(0, 0%, 0%, 0.013) 95.3%,
|
||||
hsla(0, 0%, 0%, 0) 100%
|
||||
);
|
||||
margin-bottom: -44px;
|
||||
margin-bottom: calc(-1 * var(--banner-overlap));
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
opacity: 0;
|
||||
|
@ -116,18 +119,26 @@
|
|||
}
|
||||
.account-container .header-banner:active {
|
||||
mask-image: none;
|
||||
}
|
||||
.account-container .header-banner:active + header .avatar + * {
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
opacity: 0 !important;
|
||||
}
|
||||
.account-container .header-banner:active + header .avatar {
|
||||
transition: filter 0.3s ease-in-out;
|
||||
filter: none !important;
|
||||
}
|
||||
.account-container .header-banner:active + header .avatar img {
|
||||
transition: border-radius 0.3s ease-in-out;
|
||||
border-radius: 8px;
|
||||
|
||||
& + header {
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
& + header .avatar + * {
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
||||
&,
|
||||
& + header .avatar {
|
||||
transition: filter 0.3s ease-in-out;
|
||||
filter: none !important;
|
||||
}
|
||||
|
||||
& + header .avatar img {
|
||||
transition: border-radius 0.3s ease-in-out;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-height: 480px) {
|
||||
|
@ -165,6 +176,10 @@
|
|||
animation: fade-in 0.3s both ease-in-out 0.2s;
|
||||
}
|
||||
|
||||
.account-container .account-block .account-block-acct {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.private-note-tag {
|
||||
z-index: 1;
|
||||
appearance: none;
|
||||
|
@ -290,8 +305,11 @@
|
|||
color: inherit;
|
||||
}
|
||||
|
||||
.account-container footer {
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
.account-container .actions {
|
||||
margin-block: 8px;
|
||||
/* margin-block: 8px; */
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
|
@ -396,13 +414,64 @@
|
|||
animation: none;
|
||||
}
|
||||
.timeline-start .account-container main {
|
||||
padding: 1px 16px 1px;
|
||||
padding: 1px 16px 16px;
|
||||
}
|
||||
.timeline-start .account-container main > * {
|
||||
animation: none;
|
||||
}
|
||||
.timeline-start .account-container .account-block .account-block-acct {
|
||||
opacity: 0.5;
|
||||
|
||||
.faux-header-bg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sheet .account-container {
|
||||
border-radius: 16px 16px 0 0;
|
||||
overflow-x: hidden;
|
||||
max-height: 75vh;
|
||||
overscroll-behavior: none;
|
||||
|
||||
header {
|
||||
padding-bottom: 16px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
/* --bg-color: red; */
|
||||
background-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent 30%,
|
||||
var(--bg-color) var(--banner-overlap),
|
||||
var(--bg-color) calc(100% - 8px),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.faux-header-bg {
|
||||
display: block;
|
||||
height: var(--banner-overlap);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
background-color: var(--bg-color);
|
||||
margin-top: calc(-1 * var(--banner-overlap));
|
||||
}
|
||||
|
||||
main {
|
||||
margin-top: -8px;
|
||||
padding-top: 1px;
|
||||
}
|
||||
|
||||
footer {
|
||||
min-height: calc(40px + 16px);
|
||||
animation: slide-up 0.3s ease-out 0.3s both;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
background-color: var(--bg-faded-blur-color);
|
||||
backdrop-filter: blur(16px) saturate(3);
|
||||
padding: 8px 16px;
|
||||
border-top: var(--hairline-width) solid var(--outline-color);
|
||||
padding-bottom: max(8px, env(safe-area-inset-bottom));
|
||||
box-shadow: 0 -8px 16px -8px var(--drop-shadow-color);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes swoosh-bg-image {
|
||||
|
@ -609,6 +678,7 @@
|
|||
|
||||
@media (min-width: 40em) {
|
||||
.timeline-start .account-container {
|
||||
--banner-overlap: 77px;
|
||||
--item-radius: 16px;
|
||||
border: 1px solid var(--divider-color);
|
||||
margin: 16px 0;
|
||||
|
@ -625,9 +695,9 @@
|
|||
var(--shadow-offset) var(--shadow-offset) var(--shadow-blur)
|
||||
var(--shadow-spread) var(--header-color-2, var(--drop-shadow-color));
|
||||
}
|
||||
.timeline-start .account-container .header-banner {
|
||||
/* .timeline-start .account-container .header-banner {
|
||||
margin-bottom: -77px;
|
||||
}
|
||||
} */
|
||||
.timeline-start .account-container header .account-block {
|
||||
font-size: 175%;
|
||||
/* margin-bottom: -8px; */
|
||||
|
|
|
@ -314,6 +314,7 @@ function AccountInfo({
|
|||
|
||||
return (
|
||||
<div
|
||||
tabIndex="-1"
|
||||
class={`account-container ${uiState === 'loading' ? 'skeleton' : ''}`}
|
||||
style={{
|
||||
'--header-color-1': headerCornerColors[0],
|
||||
|
@ -343,20 +344,39 @@ function AccountInfo({
|
|||
</header>
|
||||
<main>
|
||||
<div class="note">
|
||||
<p>████████ ███████</p>
|
||||
<p>███████████████ ███████████████</p>
|
||||
<p>███████ ████ ████</p>
|
||||
<p>████ ████████ ██████ █████████ ████ ██</p>
|
||||
</div>
|
||||
<div class="stats">
|
||||
<div>
|
||||
<span>██</span> Followers
|
||||
<div class="account-metadata-box">
|
||||
<div class="profile-metadata">
|
||||
<div class="profile-field">
|
||||
<b class="more-insignificant">███</b>
|
||||
<p>██████</p>
|
||||
</div>
|
||||
<div class="profile-field">
|
||||
<b class="more-insignificant">████</b>
|
||||
<p>███████████</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span>██</span> Following
|
||||
<div class="stats">
|
||||
<div>
|
||||
<span>██</span> Followers
|
||||
</div>
|
||||
<div>
|
||||
<span>██</span> Following
|
||||
</div>
|
||||
<div>
|
||||
<span>██</span> Posts
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span>██</span> Posts
|
||||
</div>
|
||||
<div>Joined ██</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<span />
|
||||
<span class="buttons">
|
||||
<button type="button" title="More" class="plain" disabled>
|
||||
<Icon icon="more" size="l" alt="More" />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
|
@ -379,7 +399,7 @@ function AccountInfo({
|
|||
/>
|
||||
</div>
|
||||
)}
|
||||
{header && !/missing\.png$/.test(header) && (
|
||||
{!!header && !/missing\.png$/.test(header) && (
|
||||
<img
|
||||
src={header}
|
||||
alt=""
|
||||
|
@ -486,7 +506,8 @@ function AccountInfo({
|
|||
internal={!standalone}
|
||||
/>
|
||||
</header>
|
||||
<main tabIndex="-1">
|
||||
<div class="faux-header-bg" aria-hidden="true" />
|
||||
<main>
|
||||
{!!memorial && <span class="tag">In Memoriam</span>}
|
||||
{!!bot && (
|
||||
<span class="tag">
|
||||
|
@ -729,13 +750,15 @@ function AccountInfo({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
<RelatedActions
|
||||
info={info}
|
||||
instance={instance}
|
||||
authenticated={authenticated}
|
||||
onRelationshipChange={onRelationshipChange}
|
||||
/>
|
||||
</main>
|
||||
</footer>
|
||||
</>
|
||||
)
|
||||
)}
|
||||
|
@ -1366,6 +1389,7 @@ function AddRemoveListsSheet({ accountID, onClose }) {
|
|||
(async () => {
|
||||
try {
|
||||
const lists = await masto.v1.lists.list();
|
||||
lists.sort((a, b) => a.title.localeCompare(b.title));
|
||||
const listsContainingAccount = await masto.v1.accounts
|
||||
.$select(accountID)
|
||||
.lists.list();
|
||||
|
|
|
@ -2,6 +2,8 @@ import './avatar.css';
|
|||
|
||||
import { useRef } from 'preact/hooks';
|
||||
|
||||
import mem from '../utils/mem';
|
||||
|
||||
const SIZES = {
|
||||
s: 16,
|
||||
m: 20,
|
||||
|
@ -90,4 +92,4 @@ function Avatar({ url, size, alt = '', squircle, ...props }) {
|
|||
);
|
||||
}
|
||||
|
||||
export default Avatar;
|
||||
export default mem(Avatar);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { memo } from 'preact/compat';
|
||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
import { api } from '../utils/api';
|
||||
import states, { saveStatus } from '../utils/states';
|
||||
|
@ -11,57 +12,62 @@ export default memo(function BackgroundService({ isLoggedIn }) {
|
|||
// - WebSocket to receive notifications when page is visible
|
||||
const [visible, setVisible] = useState(true);
|
||||
usePageVisibility(setVisible);
|
||||
useEffect(() => {
|
||||
let sub;
|
||||
if (isLoggedIn && visible) {
|
||||
const { masto, streaming, instance } = api();
|
||||
(async () => {
|
||||
// 1. Get the latest notification
|
||||
if (states.notificationsLast) {
|
||||
const notificationsIterator = masto.v1.notifications.list({
|
||||
limit: 1,
|
||||
since_id: states.notificationsLast.id,
|
||||
});
|
||||
const { value: notifications } = await notificationsIterator.next();
|
||||
if (notifications?.length) {
|
||||
let lastReadId;
|
||||
try {
|
||||
const markers = await masto.v1.markers.fetch({
|
||||
timeline: 'notifications',
|
||||
});
|
||||
lastReadId = markers?.notifications?.lastReadId;
|
||||
} catch (e) {}
|
||||
if (lastReadId) {
|
||||
if (notifications[0].id !== lastReadId) {
|
||||
states.notificationsShowNew = true;
|
||||
}
|
||||
} else {
|
||||
states.notificationsShowNew = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Start streaming
|
||||
if (streaming) {
|
||||
sub = streaming.user.notification.subscribe();
|
||||
console.log('🎏 Streaming notification', sub);
|
||||
for await (const entry of sub) {
|
||||
if (!sub) break;
|
||||
console.log('🔔🔔 Notification entry', entry);
|
||||
if (entry.event === 'notification') {
|
||||
console.log('🔔🔔 Notification', entry);
|
||||
saveStatus(entry.payload, instance, {
|
||||
skipThreading: true,
|
||||
});
|
||||
}
|
||||
const subRef = useRef();
|
||||
const debouncedStartNotifications = useDebouncedCallback(() => {
|
||||
const { masto, streaming, instance } = api();
|
||||
(async () => {
|
||||
// 1. Get the latest notification
|
||||
if (states.notificationsLast) {
|
||||
const notificationsIterator = masto.v1.notifications.list({
|
||||
limit: 1,
|
||||
since_id: states.notificationsLast.id,
|
||||
});
|
||||
const { value: notifications } = await notificationsIterator.next();
|
||||
if (notifications?.length) {
|
||||
let lastReadId;
|
||||
try {
|
||||
const markers = await masto.v1.markers.fetch({
|
||||
timeline: 'notifications',
|
||||
});
|
||||
lastReadId = markers?.notifications?.lastReadId;
|
||||
} catch (e) {}
|
||||
if (lastReadId) {
|
||||
states.notificationsShowNew = notifications[0].id !== lastReadId;
|
||||
} else {
|
||||
states.notificationsShowNew = true;
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
// 2. Start streaming
|
||||
if (streaming) {
|
||||
let sub = (subRef.current = streaming.user.notification.subscribe());
|
||||
console.log('🎏 Streaming notification', sub);
|
||||
for await (const entry of sub) {
|
||||
if (!sub) break;
|
||||
console.log('🔔🔔 Notification entry', entry);
|
||||
if (entry.event === 'notification') {
|
||||
console.log('🔔🔔 Notification', entry);
|
||||
saveStatus(entry.payload, instance, {
|
||||
skipThreading: true,
|
||||
});
|
||||
}
|
||||
states.notificationsShowNew = true;
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, 3000);
|
||||
useEffect(() => {
|
||||
// let sub;
|
||||
if (isLoggedIn && visible) {
|
||||
debouncedStartNotifications();
|
||||
}
|
||||
return () => {
|
||||
sub?.unsubscribe?.();
|
||||
sub = null;
|
||||
// sub?.unsubscribe?.();
|
||||
// sub = null;
|
||||
debouncedStartNotifications?.cancel?.();
|
||||
subRef.current?.unsubscribe?.();
|
||||
subRef.current = null;
|
||||
};
|
||||
}, [visible, isLoggedIn]);
|
||||
|
||||
|
|
|
@ -56,8 +56,8 @@
|
|||
|
||||
@media (min-width: 40em) {
|
||||
#compose-container textarea {
|
||||
font-size: 150%;
|
||||
font-size: calc(100% + 50% / var(--text-weight));
|
||||
/* font-size: 150%;
|
||||
font-size: calc(100% + 50% / var(--text-weight)); */
|
||||
max-height: 65vh;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import {
|
|||
getCurrentAccount,
|
||||
getCurrentAccountNS,
|
||||
getCurrentInstance,
|
||||
getCurrentInstanceConfiguration,
|
||||
} from '../utils/store-utils';
|
||||
import supports from '../utils/supports';
|
||||
import useInterval from '../utils/useInterval';
|
||||
|
@ -119,21 +120,30 @@ function Compose({
|
|||
const currentAccount = getCurrentAccount();
|
||||
const currentAccountInfo = currentAccount.info;
|
||||
|
||||
const { configuration } = getCurrentInstance();
|
||||
const configuration = getCurrentInstanceConfiguration();
|
||||
console.log('⚙️ Configuration', configuration);
|
||||
|
||||
const {
|
||||
statuses: { maxCharacters, maxMediaAttachments, charactersReservedPerUrl },
|
||||
statuses: {
|
||||
maxCharacters,
|
||||
maxMediaAttachments,
|
||||
charactersReservedPerUrl,
|
||||
} = {},
|
||||
mediaAttachments: {
|
||||
supportedMimeTypes,
|
||||
supportedMimeTypes = [],
|
||||
imageSizeLimit,
|
||||
imageMatrixLimit,
|
||||
videoSizeLimit,
|
||||
videoMatrixLimit,
|
||||
videoFrameRateLimit,
|
||||
},
|
||||
polls: { maxOptions, maxCharactersPerOption, maxExpiration, minExpiration },
|
||||
} = configuration;
|
||||
} = {},
|
||||
polls: {
|
||||
maxOptions,
|
||||
maxCharactersPerOption,
|
||||
maxExpiration,
|
||||
minExpiration,
|
||||
} = {},
|
||||
} = configuration || {};
|
||||
|
||||
const textareaRef = useRef();
|
||||
const spoilerTextRef = useRef();
|
||||
|
@ -377,6 +387,14 @@ function Compose({
|
|||
enableOnFormTags: true,
|
||||
// Use keyup because Esc keydown will close the confirm dialog on Safari
|
||||
keyup: true,
|
||||
ignoreEventWhen: (e) => {
|
||||
const modals = document.querySelectorAll('#modal-container > *');
|
||||
const hasModal = !!modals;
|
||||
const hasOnlyComposer =
|
||||
modals.length === 1 && modals[0].querySelector('#compose-container');
|
||||
console.log('hasModal', hasModal, 'hasOnlyComposer', hasOnlyComposer);
|
||||
return hasModal && !hasOnlyComposer;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -1200,7 +1218,7 @@ const Textarea = forwardRef((props, ref) => {
|
|||
const [text, setText] = useState(ref.current?.value || '');
|
||||
const { maxCharacters, performSearch = () => {}, ...textareaProps } = props;
|
||||
const snapStates = useSnapshot(states);
|
||||
const charCount = snapStates.composerCharacterCount;
|
||||
// const charCount = snapStates.composerCharacterCount;
|
||||
|
||||
const customEmojis = useRef();
|
||||
useEffect(() => {
|
||||
|
@ -1432,7 +1450,7 @@ const Textarea = forwardRef((props, ref) => {
|
|||
style={{
|
||||
width: '100%',
|
||||
height: '4em',
|
||||
'--text-weight': (1 + charCount / 140).toFixed(1) || 1,
|
||||
// '--text-weight': (1 + charCount / 140).toFixed(1) || 1,
|
||||
}}
|
||||
/>
|
||||
</text-expander>
|
||||
|
|
|
@ -101,6 +101,7 @@ export const ICONS = {
|
|||
'account-warning': () => import('@iconify-icons/mingcute/user-warning-line'),
|
||||
keyboard: () => import('@iconify-icons/mingcute/keyboard-line'),
|
||||
cloud: () => import('@iconify-icons/mingcute/cloud-line'),
|
||||
month: () => import('@iconify-icons/mingcute/calendar-month-line'),
|
||||
};
|
||||
|
||||
function Icon({
|
||||
|
|
|
@ -135,8 +135,12 @@ export default memo(function KeyboardShortcutsHelp() {
|
|||
keys: <kbd>r</kbd>,
|
||||
},
|
||||
{
|
||||
action: 'Favourite',
|
||||
keys: <kbd>f</kbd>,
|
||||
action: 'Like (favourite)',
|
||||
keys: (
|
||||
<>
|
||||
<kbd>l</kbd> or <kbd>f</kbd>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
action: 'Boost',
|
||||
|
|
|
@ -24,7 +24,7 @@ export default function MediaAltModal({ alt, lang, onClose }) {
|
|||
);
|
||||
|
||||
return (
|
||||
<div class="sheet">
|
||||
<div class="sheet" tabindex="-1">
|
||||
{!!onClose && (
|
||||
<button type="button" class="sheet-close outer" onClick={onClose}>
|
||||
<Icon icon="x" />
|
||||
|
|
|
@ -3,12 +3,13 @@ import { getBlurHashAverageColor } from 'fast-blurhash';
|
|||
import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
import { oklab2rgb, rgb2oklab } from '../utils/color-utils';
|
||||
import states from '../utils/states';
|
||||
|
||||
import Icon from './icon';
|
||||
import Link from './link';
|
||||
import Media from './media';
|
||||
import MediaAltModal from './media-alt-modal';
|
||||
import MenuLink from './menu-link';
|
||||
import Modal from './modal';
|
||||
|
||||
function MediaModal({
|
||||
mediaAttachments,
|
||||
|
@ -64,9 +65,17 @@ function MediaModal({
|
|||
};
|
||||
}, []);
|
||||
|
||||
useHotkeys('esc', onClose, [onClose]);
|
||||
|
||||
const [showMediaAlt, setShowMediaAlt] = useState(false);
|
||||
useHotkeys(
|
||||
'esc',
|
||||
onClose,
|
||||
{
|
||||
ignoreEventWhen: (e) => {
|
||||
const hasModal = !!document.querySelector('#modal-container > *');
|
||||
return hasModal;
|
||||
},
|
||||
},
|
||||
[onClose],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let handleScroll = () => {
|
||||
|
@ -112,17 +121,22 @@ function MediaModal({
|
|||
>
|
||||
{mediaAttachments?.map((media, i) => {
|
||||
const { blurhash } = media;
|
||||
const rgbAverageColor = blurhash
|
||||
? getBlurHashAverageColor(blurhash)
|
||||
: null;
|
||||
let accentColor;
|
||||
if (blurhash) {
|
||||
const averageColor = getBlurHashAverageColor(blurhash);
|
||||
const labAverageColor = rgb2oklab(averageColor);
|
||||
accentColor = oklab2rgb([
|
||||
0.6,
|
||||
labAverageColor[1],
|
||||
labAverageColor[2],
|
||||
]);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
class="carousel-item"
|
||||
style={{
|
||||
'--average-color': `rgb(${rgbAverageColor?.join(',')})`,
|
||||
'--average-color-alpha': `rgba(${rgbAverageColor?.join(
|
||||
',',
|
||||
)}, .5)`,
|
||||
'--accent-color': `rgb(${accentColor?.join(',')})`,
|
||||
'--accent-alpha-color': `rgba(${accentColor?.join(',')}, 0.4)`,
|
||||
}}
|
||||
tabindex="0"
|
||||
key={media.id}
|
||||
|
@ -139,10 +153,10 @@ function MediaModal({
|
|||
class="media-alt"
|
||||
hidden={!showControls}
|
||||
onClick={() => {
|
||||
setShowMediaAlt({
|
||||
states.showMediaAlt = {
|
||||
alt: media.description,
|
||||
lang,
|
||||
});
|
||||
};
|
||||
}}
|
||||
>
|
||||
<span class="alt-badge">ALT</span>
|
||||
|
@ -274,23 +288,6 @@ function MediaModal({
|
|||
</button>
|
||||
</div>
|
||||
)}
|
||||
{!!showMediaAlt && (
|
||||
<Modal
|
||||
class="light"
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
setShowMediaAlt(false);
|
||||
carouselRef.current.focus();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MediaAltModal
|
||||
alt={showMediaAlt.alt || showMediaAlt}
|
||||
lang={showMediaAlt?.lang}
|
||||
onClose={() => setShowMediaAlt(false)}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -170,15 +170,18 @@ function Media({
|
|||
const maxAspectHeight =
|
||||
window.innerHeight * (orientation === 'portrait' ? 0.45 : 0.33);
|
||||
const maxHeight = orientation === 'portrait' ? 0 : 160;
|
||||
const mediaStyles = {
|
||||
'--width': `${width}px`,
|
||||
'--height': `${height}px`,
|
||||
// Calculate '--aspectWidth' based on aspect ratio calculated from '--width' and '--height', max height has to be 160px
|
||||
'--aspectWidth': `${
|
||||
(width / height) * Math.max(maxHeight, maxAspectHeight)
|
||||
}px`,
|
||||
aspectRatio: `${width} / ${height}`,
|
||||
};
|
||||
const mediaStyles =
|
||||
width && height
|
||||
? {
|
||||
'--width': `${width}px`,
|
||||
'--height': `${height}px`,
|
||||
// Calculate '--aspectWidth' based on aspect ratio calculated from '--width' and '--height', max height has to be 160px
|
||||
'--aspectWidth': `${
|
||||
(width / height) * Math.max(maxHeight, maxAspectHeight)
|
||||
}px`,
|
||||
aspectRatio: `${width} / ${height}`,
|
||||
}
|
||||
: {};
|
||||
|
||||
const longDesc = isMediaCaptionLong(description);
|
||||
const showInlineDesc =
|
||||
|
|
|
@ -20,9 +20,22 @@ function Modal({ children, onClose, onClick, class: className }) {
|
|||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
const escRef = useHotkeys('esc', onClose, [onClose], {
|
||||
enabled: !!onClose,
|
||||
});
|
||||
const escRef = useHotkeys(
|
||||
'esc',
|
||||
() => {
|
||||
setTimeout(() => {
|
||||
onClose?.();
|
||||
}, 0);
|
||||
},
|
||||
{
|
||||
enabled: !!onClose,
|
||||
// Using keyup and setTimeout above
|
||||
// This will run "later" to prevent clash with esc handlers from other components
|
||||
keydown: false,
|
||||
keyup: true,
|
||||
},
|
||||
[onClose],
|
||||
);
|
||||
|
||||
const Modal = (
|
||||
<div
|
||||
|
|
|
@ -183,10 +183,8 @@ export default function Modals() {
|
|||
{!!snapStates.showMediaAlt && (
|
||||
<Modal
|
||||
class="light"
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
states.showMediaAlt = false;
|
||||
}
|
||||
onClose={(e) => {
|
||||
states.showMediaAlt = false;
|
||||
}}
|
||||
>
|
||||
<MediaAltModal
|
||||
|
|
|
@ -192,7 +192,7 @@ function NavMenu(props) {
|
|||
<Icon icon="bookmark" size="l" /> <span>Bookmarks</span>
|
||||
</MenuLink>
|
||||
<MenuLink to="/f">
|
||||
<Icon icon="heart" size="l" /> <span>Favourites</span>
|
||||
<Icon icon="heart" size="l" /> <span>Likes</span>
|
||||
</MenuLink>
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -77,11 +77,7 @@ export default memo(function NotificationService() {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
'🛎️ Notification not found',
|
||||
notificationID,
|
||||
notificationAccessToken,
|
||||
);
|
||||
console.warn('🛎️ Notification not found', id);
|
||||
}
|
||||
})();
|
||||
}, [id, accessToken]);
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { memo } from 'preact/compat';
|
||||
|
||||
import shortenNumber from '../utils/shorten-number';
|
||||
import states from '../utils/states';
|
||||
import store from '../utils/store';
|
||||
|
@ -47,24 +49,24 @@ const contentText = {
|
|||
reblog_reply: 'boosted your reply.',
|
||||
follow: 'followed you.',
|
||||
follow_request: 'requested to follow you.',
|
||||
favourite: 'favourited your post.',
|
||||
'favourite+account': (count) => `favourited ${count} of your posts.`,
|
||||
favourite_reply: 'favourited your reply.',
|
||||
favourite: 'liked your post.',
|
||||
'favourite+account': (count) => `liked ${count} of your posts.`,
|
||||
favourite_reply: 'liked your reply.',
|
||||
poll: 'A poll you have voted in or created has ended.',
|
||||
'poll-self': 'A poll you have created has ended.',
|
||||
'poll-voted': 'A poll you have voted in has ended.',
|
||||
update: 'A post you interacted with has been edited.',
|
||||
'favourite+reblog': 'boosted & favourited your post.',
|
||||
'favourite+reblog': 'boosted & liked your post.',
|
||||
'favourite+reblog+account': (count) =>
|
||||
`boosted & favourited ${count} of your posts.`,
|
||||
'favourite+reblog_reply': 'boosted & favourited your reply.',
|
||||
`boosted & liked ${count} of your posts.`,
|
||||
'favourite+reblog_reply': 'boosted & liked your reply.',
|
||||
'admin.sign_up': 'signed up.',
|
||||
'admin.report': (targetAccount) => <>reported {targetAccount}</>,
|
||||
};
|
||||
|
||||
const AVATARS_LIMIT = 50;
|
||||
|
||||
function Notification({ notification, instance, reload, isStatic }) {
|
||||
function Notification({ notification, instance, isStatic }) {
|
||||
const { id, status, account, report, _accounts, _statuses } = notification;
|
||||
let { type } = notification;
|
||||
|
||||
|
@ -140,8 +142,8 @@ function Notification({ notification, instance, reload, isStatic }) {
|
|||
|
||||
const genericAccountsHeading =
|
||||
{
|
||||
'favourite+reblog': 'Boosted/Favourited by…',
|
||||
favourite: 'Favourited by…',
|
||||
'favourite+reblog': 'Boosted/Liked by…',
|
||||
favourite: 'Liked by…',
|
||||
reblog: 'Boosted by…',
|
||||
follow: 'Followed by…',
|
||||
}[type] || 'Accounts';
|
||||
|
@ -153,8 +155,14 @@ function Notification({ notification, instance, reload, isStatic }) {
|
|||
};
|
||||
};
|
||||
|
||||
console.debug('RENDER Notification', notification.id);
|
||||
|
||||
return (
|
||||
<div class={`notification notification-${type}`} tabIndex="0">
|
||||
<div
|
||||
class={`notification notification-${type}`}
|
||||
data-notification-id={id}
|
||||
tabIndex="0"
|
||||
>
|
||||
<div
|
||||
class={`notification-type notification-${type}`}
|
||||
title={formattedCreatedAt}
|
||||
|
@ -207,12 +215,7 @@ function Notification({ notification, instance, reload, isStatic }) {
|
|||
)}
|
||||
</p>
|
||||
{type === 'follow_request' && (
|
||||
<FollowRequestButtons
|
||||
accountID={account.id}
|
||||
onChange={() => {
|
||||
// reload();
|
||||
}}
|
||||
/>
|
||||
<FollowRequestButtons accountID={account.id} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
@ -327,4 +330,4 @@ function TruncatedLink(props) {
|
|||
return <Link {...props} data-read-more="Read more →" ref={ref} />;
|
||||
}
|
||||
|
||||
export default Notification;
|
||||
export default memo(Notification);
|
||||
|
|
|
@ -85,7 +85,7 @@
|
|||
transform: scale(0.975);
|
||||
transition: all 0.2s ease-out;
|
||||
}
|
||||
#shortcuts-settings-container .shortcuts-view-mode label:has(input:checked) {
|
||||
#shortcuts-settings-container .shortcuts-view-mode label.checked {
|
||||
box-shadow: inset 0 0 0 3px var(--link-color);
|
||||
}
|
||||
#shortcuts-settings-container
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import './shortcuts-settings.css';
|
||||
|
||||
import { useAutoAnimate } from '@formkit/auto-animate/preact';
|
||||
import {
|
||||
compressToEncodedURIComponent,
|
||||
decompressFromEncodedURIComponent,
|
||||
|
@ -45,7 +46,7 @@ const TYPE_TEXT = {
|
|||
search: 'Search',
|
||||
'account-statuses': 'Account',
|
||||
bookmarks: 'Bookmarks',
|
||||
favourites: 'Favourites',
|
||||
favourites: 'Likes',
|
||||
hashtag: 'Hashtag',
|
||||
trending: 'Trending',
|
||||
mentions: 'Mentions',
|
||||
|
@ -177,7 +178,7 @@ export const SHORTCUTS_META = {
|
|||
},
|
||||
favourites: {
|
||||
id: 'favourites',
|
||||
title: 'Favourites',
|
||||
title: 'Likes',
|
||||
path: '/f',
|
||||
icon: 'heart',
|
||||
},
|
||||
|
@ -201,10 +202,13 @@ function ShortcutsSettings({ onClose }) {
|
|||
const [showForm, setShowForm] = useState(false);
|
||||
const [showImportExport, setShowImportExport] = useState(false);
|
||||
|
||||
const [shortcutsListParent] = useAutoAnimate();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const lists = await masto.v1.lists.list();
|
||||
lists.sort((a, b) => a.title.localeCompare(b.title));
|
||||
setLists(lists);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
@ -267,25 +271,27 @@ function ShortcutsSettings({ onClose }) {
|
|||
label: 'Multi-column',
|
||||
imgURL: multiColumnUrl,
|
||||
},
|
||||
].map(({ value, label, imgURL }) => (
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
name="shortcuts-view-mode"
|
||||
value={value}
|
||||
checked={
|
||||
snapStates.settings.shortcutsViewMode === value ||
|
||||
(value === 'float-button' &&
|
||||
!snapStates.settings.shortcutsViewMode)
|
||||
}
|
||||
onChange={(e) => {
|
||||
states.settings.shortcutsViewMode = e.target.value;
|
||||
}}
|
||||
/>{' '}
|
||||
<img src={imgURL} alt="" width="80" height="58" />{' '}
|
||||
<span>{label}</span>
|
||||
</label>
|
||||
))}
|
||||
].map(({ value, label, imgURL }) => {
|
||||
const checked =
|
||||
snapStates.settings.shortcutsViewMode === value ||
|
||||
(value === 'float-button' &&
|
||||
!snapStates.settings.shortcutsViewMode);
|
||||
return (
|
||||
<label key={value} class={checked ? 'checked' : ''}>
|
||||
<input
|
||||
type="radio"
|
||||
name="shortcuts-view-mode"
|
||||
value={value}
|
||||
checked={checked}
|
||||
onChange={(e) => {
|
||||
states.settings.shortcutsViewMode = e.target.value;
|
||||
}}
|
||||
/>{' '}
|
||||
<img src={imgURL} alt="" width="80" height="58" />{' '}
|
||||
<span>{label}</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{/* <select
|
||||
value={snapStates.settings.shortcutsViewMode || 'float-button'}
|
||||
|
@ -315,9 +321,10 @@ function ShortcutsSettings({ onClose }) {
|
|||
</details>
|
||||
</p> */}
|
||||
{shortcuts.length > 0 ? (
|
||||
<ol class="shortcuts-list">
|
||||
<ol class="shortcuts-list" ref={shortcutsListParent}>
|
||||
{shortcuts.filter(Boolean).map((shortcut, i) => {
|
||||
const key = i + Object.values(shortcut);
|
||||
// const key = i + Object.values(shortcut);
|
||||
const key = Object.values(shortcut).join('-');
|
||||
const { type } = shortcut;
|
||||
if (!SHORTCUTS_META[type]) return null;
|
||||
let { icon, title, subtitle } = SHORTCUTS_META[type];
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import './shortcuts.css';
|
||||
|
||||
import { Menu, MenuItem } from '@szhsin/react-menu';
|
||||
import { memo } from 'preact/compat';
|
||||
import { useMemo, useRef } from 'preact/hooks';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
@ -18,11 +19,17 @@ import MenuLink from './menu-link';
|
|||
function Shortcuts() {
|
||||
const { instance } = api();
|
||||
const snapStates = useSnapshot(states);
|
||||
const { shortcuts } = snapStates;
|
||||
const { shortcuts, settings } = snapStates;
|
||||
|
||||
if (!shortcuts.length) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
settings.shortcutsColumnsMode ||
|
||||
settings.shortcutsViewMode === 'multi-column'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const menuRef = useRef();
|
||||
|
||||
|
@ -180,4 +187,4 @@ function Shortcuts() {
|
|||
);
|
||||
}
|
||||
|
||||
export default Shortcuts;
|
||||
export default memo(Shortcuts);
|
||||
|
|
|
@ -512,7 +512,7 @@ function Status({
|
|||
)}{' '}
|
||||
{favouritesCount > 0 && (
|
||||
<span>
|
||||
<Icon icon="heart" alt="Favourites" size="s" />{' '}
|
||||
<Icon icon="heart" alt="Likes" size="s" />{' '}
|
||||
<span>{shortenNumber(favouritesCount)}</span>
|
||||
</span>
|
||||
)}
|
||||
|
@ -550,7 +550,7 @@ function Status({
|
|||
<MenuItem onClick={() => setShowReactions(true)}>
|
||||
<Icon icon="react" />
|
||||
<span>
|
||||
Boosted/Favourited by<span class="more-insignificant">…</span>
|
||||
Boosted/Liked by<span class="more-insignificant">…</span>
|
||||
</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
|
@ -603,8 +603,8 @@ function Status({
|
|||
if (!isSizeLarge) {
|
||||
showToast(
|
||||
favourited
|
||||
? `Unfavourited @${username || acct}'s post`
|
||||
: `Favourited @${username || acct}'s post`,
|
||||
? `Unliked @${username || acct}'s post`
|
||||
: `Liked @${username || acct}'s post`,
|
||||
);
|
||||
}
|
||||
} catch (e) {}
|
||||
|
@ -616,7 +616,7 @@ function Status({
|
|||
color: favourited && 'var(--favourite-color)',
|
||||
}}
|
||||
/>
|
||||
<span>{favourited ? 'Unfavourite' : 'Favourite'}</span>
|
||||
<span>{favourited ? 'Unlike' : 'Like'}</span>
|
||||
</MenuItem>
|
||||
</div>
|
||||
<div class="menu-horizontal">
|
||||
|
@ -794,10 +794,7 @@ function Status({
|
|||
|
||||
const contextMenuRef = useRef();
|
||||
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
|
||||
const [contextMenuAnchorPoint, setContextMenuAnchorPoint] = useState({
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
const [contextMenuProps, setContextMenuProps] = useState({});
|
||||
const isIOS =
|
||||
window.ontouchstart !== undefined &&
|
||||
/iPad|iPhone|iPod/.test(navigator.userAgent);
|
||||
|
@ -814,9 +811,12 @@ function Status({
|
|||
const link = e.target.closest('a');
|
||||
if (link && /^https?:\/\//.test(link.getAttribute('href'))) return;
|
||||
e.preventDefault();
|
||||
setContextMenuAnchorPoint({
|
||||
x: clientX,
|
||||
y: clientY,
|
||||
setContextMenuProps({
|
||||
anchorPoint: {
|
||||
x: clientX,
|
||||
y: clientY,
|
||||
},
|
||||
direction: 'right',
|
||||
});
|
||||
setIsContextMenuOpen(true);
|
||||
}
|
||||
|
@ -836,15 +836,15 @@ function Status({
|
|||
enabled: hotkeysEnabled,
|
||||
});
|
||||
const fRef = useHotkeys(
|
||||
'f',
|
||||
'f, l',
|
||||
() => {
|
||||
try {
|
||||
favouriteStatus();
|
||||
if (!isSizeLarge) {
|
||||
showToast(
|
||||
favourited
|
||||
? `Unfavourited @${username || acct}'s post`
|
||||
: `Favourited @${username || acct}'s post`,
|
||||
? `Unliked @${username || acct}'s post`
|
||||
: `Liked @${username || acct}'s post`,
|
||||
);
|
||||
}
|
||||
} catch (e) {}
|
||||
|
@ -996,9 +996,12 @@ function Status({
|
|||
const link = e.target.closest('a');
|
||||
if (link && /^https?:\/\//.test(link.getAttribute('href'))) return;
|
||||
e.preventDefault();
|
||||
setContextMenuAnchorPoint({
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
setContextMenuProps({
|
||||
anchorPoint: {
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
},
|
||||
direction: 'right',
|
||||
});
|
||||
setIsContextMenuOpen(true);
|
||||
}}
|
||||
|
@ -1008,8 +1011,7 @@ function Status({
|
|||
<ControlledMenu
|
||||
ref={contextMenuRef}
|
||||
state={isContextMenuOpen ? 'open' : undefined}
|
||||
anchorPoint={contextMenuAnchorPoint}
|
||||
direction="right"
|
||||
{...contextMenuProps}
|
||||
onClose={(e) => {
|
||||
setIsContextMenuOpen(false);
|
||||
// statusRef.current?.focus?.();
|
||||
|
@ -1086,49 +1088,78 @@ function Status({
|
|||
(_deleted ? (
|
||||
<span class="status-deleted-tag">Deleted</span>
|
||||
) : url && !previewMode && !quoted ? (
|
||||
<Menu
|
||||
instanceRef={menuInstanceRef}
|
||||
portal={{
|
||||
target: document.body,
|
||||
<Link
|
||||
to={instance ? `/${instance}/s/${id}` : `/s/${id}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onStatusLinkClick?.(e, status);
|
||||
setContextMenuProps({
|
||||
anchorRef: {
|
||||
current: e.currentTarget,
|
||||
},
|
||||
align: 'end',
|
||||
direction: 'bottom',
|
||||
gap: 4,
|
||||
});
|
||||
setIsContextMenuOpen(true);
|
||||
}}
|
||||
containerProps={{
|
||||
style: {
|
||||
// Higher than the backdrop
|
||||
zIndex: 1001,
|
||||
},
|
||||
onClick: (e) => {
|
||||
if (e.target === e.currentTarget)
|
||||
menuInstanceRef.current?.closeMenu?.();
|
||||
},
|
||||
}}
|
||||
align="end"
|
||||
gap={4}
|
||||
overflow="auto"
|
||||
viewScroll="close"
|
||||
boundingBoxPadding="8 8 8 8"
|
||||
unmountOnClose
|
||||
menuButton={({ open }) => (
|
||||
<Link
|
||||
to={instance ? `/${instance}/s/${id}` : `/s/${id}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onStatusLinkClick?.(e, status);
|
||||
}}
|
||||
class={`time ${open ? 'is-open' : ''}`}
|
||||
>
|
||||
<Icon
|
||||
icon={visibilityIconsMap[visibility]}
|
||||
alt={visibilityText[visibility]}
|
||||
size="s"
|
||||
/>{' '}
|
||||
<RelativeTime datetime={createdAtDate} format="micro" />
|
||||
</Link>
|
||||
)}
|
||||
class={`time ${
|
||||
isContextMenuOpen && contextMenuProps?.anchorRef
|
||||
? 'is-open'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
{StatusMenuItems}
|
||||
</Menu>
|
||||
<Icon
|
||||
icon={visibilityIconsMap[visibility]}
|
||||
alt={visibilityText[visibility]}
|
||||
size="s"
|
||||
/>{' '}
|
||||
<RelativeTime datetime={createdAtDate} format="micro" />
|
||||
</Link>
|
||||
) : (
|
||||
// <Menu
|
||||
// instanceRef={menuInstanceRef}
|
||||
// portal={{
|
||||
// target: document.body,
|
||||
// }}
|
||||
// containerProps={{
|
||||
// style: {
|
||||
// // Higher than the backdrop
|
||||
// zIndex: 1001,
|
||||
// },
|
||||
// onClick: (e) => {
|
||||
// if (e.target === e.currentTarget)
|
||||
// menuInstanceRef.current?.closeMenu?.();
|
||||
// },
|
||||
// }}
|
||||
// align="end"
|
||||
// gap={4}
|
||||
// overflow="auto"
|
||||
// viewScroll="close"
|
||||
// boundingBoxPadding="8 8 8 8"
|
||||
// unmountOnClose
|
||||
// menuButton={({ open }) => (
|
||||
// <Link
|
||||
// to={instance ? `/${instance}/s/${id}` : `/s/${id}`}
|
||||
// onClick={(e) => {
|
||||
// e.preventDefault();
|
||||
// e.stopPropagation();
|
||||
// onStatusLinkClick?.(e, status);
|
||||
// }}
|
||||
// class={`time ${open ? 'is-open' : ''}`}
|
||||
// >
|
||||
// <Icon
|
||||
// icon={visibilityIconsMap[visibility]}
|
||||
// alt={visibilityText[visibility]}
|
||||
// size="s"
|
||||
// />{' '}
|
||||
// <RelativeTime datetime={createdAtDate} format="micro" />
|
||||
// </Link>
|
||||
// )}
|
||||
// >
|
||||
// {StatusMenuItems}
|
||||
// </Menu>
|
||||
<span class="time">
|
||||
<Icon
|
||||
icon={visibilityIconsMap[visibility]}
|
||||
|
@ -1497,8 +1528,8 @@ function Status({
|
|||
<div class="action has-count">
|
||||
<StatusButton
|
||||
checked={favourited}
|
||||
title={['Favourite', 'Unfavourite']}
|
||||
alt={['Favourite', 'Favourited']}
|
||||
title={['Like', 'Unlike']}
|
||||
alt={['Like', 'Liked']}
|
||||
class="favourite-button"
|
||||
icon="heart"
|
||||
count={favouritesCount}
|
||||
|
@ -1931,7 +1962,7 @@ function ReactionsModal({ statusID, instance, onClose }) {
|
|||
</button>
|
||||
)}
|
||||
<header>
|
||||
<h2>Boosted/Favourited by…</h2>
|
||||
<h2>Boosted/Liked by…</h2>
|
||||
</header>
|
||||
<main>
|
||||
{accounts.length > 0 ? (
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { useIdle } from '@uidotdev/usehooks';
|
||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { InView } from 'react-intersection-observer';
|
||||
|
@ -211,21 +210,19 @@ function Timeline({
|
|||
}
|
||||
}, [nearReachEnd, showMore]);
|
||||
|
||||
const idle = useIdle(5000);
|
||||
console.debug('🧘♀️ IDLE', idle);
|
||||
const loadOrCheckUpdates = useCallback(
|
||||
async ({ disableHoverCheck = false } = {}) => {
|
||||
async ({ disableIdleCheck = false } = {}) => {
|
||||
console.log('✨ Load or check updates', {
|
||||
autoRefresh: snapStates.settings.autoRefresh,
|
||||
scrollTop: scrollableRef.current.scrollTop,
|
||||
disableHoverCheck,
|
||||
idle,
|
||||
disableIdleCheck,
|
||||
idle: window.__IDLE__,
|
||||
inBackground: inBackground(),
|
||||
});
|
||||
if (
|
||||
snapStates.settings.autoRefresh &&
|
||||
scrollableRef.current.scrollTop === 0 &&
|
||||
(disableHoverCheck || idle) &&
|
||||
(disableIdleCheck || window.__IDLE__) &&
|
||||
!inBackground()
|
||||
) {
|
||||
console.log('✨ Load updates', snapStates.settings.autoRefresh);
|
||||
|
@ -239,7 +236,11 @@ function Timeline({
|
|||
}
|
||||
}
|
||||
},
|
||||
[id, idle, loadItems, checkForUpdates, snapStates.settings.autoRefresh],
|
||||
[id, loadItems, checkForUpdates, snapStates.settings.autoRefresh],
|
||||
);
|
||||
const debouncedLoadOrCheckUpdates = useDebouncedCallback(
|
||||
loadOrCheckUpdates,
|
||||
3000,
|
||||
);
|
||||
|
||||
const lastHiddenTime = useRef();
|
||||
|
@ -248,12 +249,14 @@ function Timeline({
|
|||
if (visible) {
|
||||
const timeDiff = Date.now() - lastHiddenTime.current;
|
||||
if (!lastHiddenTime.current || timeDiff > 1000 * 60) {
|
||||
loadOrCheckUpdates({
|
||||
disableHoverCheck: true,
|
||||
// 1 minute
|
||||
debouncedLoadOrCheckUpdates({
|
||||
disableIdleCheck: true,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
lastHiddenTime.current = Date.now();
|
||||
debouncedLoadOrCheckUpdates.cancel();
|
||||
}
|
||||
setVisible(visible);
|
||||
},
|
||||
|
@ -609,11 +612,16 @@ function StatusCarousel({ title, class: className, children }) {
|
|||
|
||||
function TimelineStatusCompact({ status, instance }) {
|
||||
const snapStates = useSnapshot(states);
|
||||
const { id } = status;
|
||||
const { id, visibility } = status;
|
||||
const statusPeekText = statusPeek(status);
|
||||
const sKey = statusKey(id, instance);
|
||||
return (
|
||||
<article class="status compact-thread" tabindex="-1">
|
||||
<article
|
||||
class={`status compact-thread ${
|
||||
visibility === 'direct' ? 'visibility-direct' : ''
|
||||
}`}
|
||||
tabindex="-1"
|
||||
>
|
||||
{!!snapStates.statusThreadNumber[sKey] ? (
|
||||
<div class="status-thread-badge">
|
||||
<Icon icon="thread" size="s" />
|
||||
|
|
|
@ -20,8 +20,8 @@ const throttle = pThrottle({
|
|||
// Using other API instances instead of lingva.ml because of this bug (slashes don't work):
|
||||
// https://github.com/thedaviddelta/lingva-translate/issues/68
|
||||
const LINGVA_INSTANCES = [
|
||||
'lingva.garudalinux.org',
|
||||
'lingva.lunar.icu',
|
||||
'lingva.garudalinux.org',
|
||||
'translate.plausibility.cloud',
|
||||
];
|
||||
let currentLingvaInstance = 0;
|
||||
|
@ -35,7 +35,10 @@ function _lingvaTranslate(text, source, target) {
|
|||
text,
|
||||
)}`,
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error(res.statusText);
|
||||
return res.json();
|
||||
})
|
||||
.then((res) => {
|
||||
return {
|
||||
provider: 'lingva',
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
--divider-color: rgba(0, 0, 0, 0.1);
|
||||
--backdrop-color: rgba(0, 0, 0, 0.05);
|
||||
--backdrop-darker-color: rgba(0, 0, 0, 0.25);
|
||||
--backdrop-solid-color: #ccc;
|
||||
--backdrop-solid-color: #eee;
|
||||
--img-bg-color: rgba(128, 128, 128, 0.2);
|
||||
--loader-color: #1c1e2199;
|
||||
--comment-line-color: #e5e5e5;
|
||||
|
@ -106,7 +106,7 @@
|
|||
--divider-color: rgba(255, 255, 255, 0.1);
|
||||
--bg-blur-color: #24252699;
|
||||
--backdrop-color: rgba(0, 0, 0, 0.5);
|
||||
--backdrop-solid-color: #333;
|
||||
--backdrop-solid-color: #111;
|
||||
--loader-color: #f0f2f599;
|
||||
--comment-line-color: #565656;
|
||||
--drop-shadow-color: rgba(0, 0, 0, 0.5);
|
||||
|
@ -148,6 +148,16 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
p {
|
||||
/*
|
||||
white-space is shorthand for two values; white-space-collapse and text-wrap
|
||||
https://developer.mozilla.org/en-US/docs/Web/CSS/white-space
|
||||
!important is needed to override higher specificity when elements are styled
|
||||
with `white-space` and 1 value, which doesn't have "pretty"
|
||||
*/
|
||||
text-wrap: pretty !important;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--link-color);
|
||||
text-decoration-color: var(--link-faded-color);
|
||||
|
|
|
@ -10,24 +10,139 @@ import Link from '../components/link';
|
|||
import Menu2 from '../components/menu2';
|
||||
import Timeline from '../components/timeline';
|
||||
import { api } from '../utils/api';
|
||||
import pmem from '../utils/pmem';
|
||||
import showToast from '../utils/show-toast';
|
||||
import states from '../utils/states';
|
||||
import { saveStatus } from '../utils/states';
|
||||
import useTitle from '../utils/useTitle';
|
||||
|
||||
const LIMIT = 20;
|
||||
const MIN_YEAR = 1983;
|
||||
const MIN_YEAR_MONTH = `${MIN_YEAR}-01`; // Birth of the Internet
|
||||
|
||||
const supportsInputMonth = (() => {
|
||||
try {
|
||||
const input = document.createElement('input');
|
||||
input.setAttribute('type', 'month');
|
||||
return input.type === 'month';
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
|
||||
async function _isSearchEnabled(instance) {
|
||||
const { masto } = api({ instance });
|
||||
const results = await masto.v2.search.fetch({
|
||||
q: 'from:me',
|
||||
type: 'statuses',
|
||||
limit: 1,
|
||||
});
|
||||
return !!results?.statuses?.length;
|
||||
}
|
||||
const isSearchEnabled = pmem(_isSearchEnabled);
|
||||
|
||||
function AccountStatuses() {
|
||||
const snapStates = useSnapshot(states);
|
||||
const { id, ...params } = useParams();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const month = searchParams.get('month');
|
||||
const excludeReplies = !searchParams.get('replies');
|
||||
const excludeBoosts = !!searchParams.get('boosts');
|
||||
const tagged = searchParams.get('tagged');
|
||||
const media = !!searchParams.get('media');
|
||||
const { masto, instance, authenticated } = api({ instance: params.instance });
|
||||
const accountStatusesIterator = useRef();
|
||||
|
||||
const allSearchParams = [month, excludeReplies, excludeBoosts, tagged, media];
|
||||
const [account, setAccount] = useState();
|
||||
const searchOffsetRef = useRef(0);
|
||||
useEffect(() => {
|
||||
searchOffsetRef.current = 0;
|
||||
}, allSearchParams);
|
||||
|
||||
const sameCurrentInstance = useMemo(
|
||||
() => instance === api().instance,
|
||||
[instance],
|
||||
);
|
||||
const [searchEnabled, setSearchEnabled] = useState(false);
|
||||
useEffect(() => {
|
||||
// Only enable for current logged-in instance
|
||||
// Most remote instances don't allow unauthenticated searches
|
||||
if (!sameCurrentInstance) return;
|
||||
if (!account?.acct) return;
|
||||
(async () => {
|
||||
const enabled = await isSearchEnabled(instance);
|
||||
console.log({ enabled });
|
||||
setSearchEnabled(enabled);
|
||||
})();
|
||||
}, [instance, sameCurrentInstance, account?.acct]);
|
||||
|
||||
async function fetchAccountStatuses(firstLoad) {
|
||||
const isValidMonth = /^\d{4}-[01]\d$/.test(month);
|
||||
const isValidYear = month?.split?.('-')?.[0] >= MIN_YEAR;
|
||||
if (isValidMonth && isValidYear) {
|
||||
if (!account) {
|
||||
return {
|
||||
value: [],
|
||||
done: true,
|
||||
};
|
||||
}
|
||||
const [_year, _month] = month.split('-');
|
||||
const monthIndex = parseInt(_month, 10) - 1;
|
||||
// YYYY-MM (no day)
|
||||
// Search options:
|
||||
// - from:account
|
||||
// - after:YYYY-MM-DD (non-inclusive)
|
||||
// - before:YYYY-MM-DD (non-inclusive)
|
||||
|
||||
// Last day of previous month
|
||||
const after = new Date(_year, monthIndex, 0);
|
||||
const afterStr = `${after.getFullYear()}-${(after.getMonth() + 1)
|
||||
.toString()
|
||||
.padStart(2, '0')}-${after.getDate().toString().padStart(2, '0')}`;
|
||||
// First day of next month
|
||||
const before = new Date(_year, monthIndex + 1, 1);
|
||||
const beforeStr = `${before.getFullYear()}-${(before.getMonth() + 1)
|
||||
.toString()
|
||||
.padStart(2, '0')}-${before.getDate().toString().padStart(2, '0')}`;
|
||||
console.log({
|
||||
month,
|
||||
_year,
|
||||
_month,
|
||||
monthIndex,
|
||||
after,
|
||||
before,
|
||||
afterStr,
|
||||
beforeStr,
|
||||
});
|
||||
|
||||
let limit;
|
||||
if (firstLoad) {
|
||||
limit = LIMIT + 1;
|
||||
searchOffsetRef.current = 0;
|
||||
} else {
|
||||
limit = LIMIT + searchOffsetRef.current + 1;
|
||||
searchOffsetRef.current += LIMIT;
|
||||
}
|
||||
|
||||
const searchResults = await masto.v2.search.fetch({
|
||||
q: `from:${account.acct} after:${afterStr} before:${beforeStr}`,
|
||||
type: 'statuses',
|
||||
limit,
|
||||
offset: searchOffsetRef.current,
|
||||
});
|
||||
if (searchResults?.statuses?.length) {
|
||||
const value = searchResults.statuses.slice(0, LIMIT);
|
||||
value.forEach((item) => {
|
||||
saveStatus(item, instance);
|
||||
});
|
||||
const done = searchResults.statuses.length <= LIMIT;
|
||||
return { value, done };
|
||||
} else {
|
||||
return { value: [], done: true };
|
||||
}
|
||||
}
|
||||
|
||||
const results = [];
|
||||
if (firstLoad) {
|
||||
const { value: pinnedStatuses } = await masto.v1.accounts
|
||||
|
@ -78,7 +193,6 @@ function AccountStatuses() {
|
|||
};
|
||||
}
|
||||
|
||||
const [account, setAccount] = useState();
|
||||
const [featuredTags, setFeaturedTags] = useState([]);
|
||||
useTitle(
|
||||
`${account?.displayName ? account.displayName + ' ' : ''}@${
|
||||
|
@ -98,7 +212,7 @@ function AccountStatuses() {
|
|||
try {
|
||||
const featuredTags = await masto.v1.accounts
|
||||
.$select(id)
|
||||
.featuredTags.list(id);
|
||||
.featuredTags.list();
|
||||
console.log({ featuredTags });
|
||||
setFeaturedTags(featuredTags);
|
||||
} catch (e) {
|
||||
|
@ -112,7 +226,8 @@ function AccountStatuses() {
|
|||
const filterBarRef = useRef();
|
||||
const TimelineStart = useMemo(() => {
|
||||
const cachedAccount = snapStates.accounts[`${id}@${instance}`];
|
||||
const filtered = !excludeReplies || excludeBoosts || tagged || media;
|
||||
const filtered =
|
||||
!excludeReplies || excludeBoosts || tagged || media || !!month;
|
||||
return (
|
||||
<>
|
||||
<AccountInfo
|
||||
|
@ -170,6 +285,7 @@ function AccountStatuses() {
|
|||
</Link>
|
||||
{featuredTags.map((tag) => (
|
||||
<Link
|
||||
key={tag.id}
|
||||
to={`/${instance}/a/${id}${
|
||||
tagged === tag.name
|
||||
? ''
|
||||
|
@ -192,6 +308,50 @@ function AccountStatuses() {
|
|||
{/* <span class="filter-count">{tag.statusesCount}</span> */}
|
||||
</Link>
|
||||
))}
|
||||
{searchEnabled &&
|
||||
(supportsInputMonth ? (
|
||||
<label class={`filter-field ${month ? 'is-active' : ''}`}>
|
||||
<Icon icon="month" size="l" />
|
||||
<input
|
||||
type="month"
|
||||
disabled={!account?.acct}
|
||||
value={month || ''}
|
||||
min={MIN_YEAR_MONTH}
|
||||
max={new Date().toISOString().slice(0, 7)}
|
||||
onInput={(e) => {
|
||||
const { value, validity } = e.currentTarget;
|
||||
if (!validity.valid) return;
|
||||
setSearchParams(
|
||||
value
|
||||
? {
|
||||
month: value,
|
||||
}
|
||||
: {},
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
) : (
|
||||
// Fallback to <select> for month and <input type="number"> for year
|
||||
<MonthPicker
|
||||
class={`filter-field ${month ? 'is-active' : ''}`}
|
||||
disabled={!account?.acct}
|
||||
value={month || ''}
|
||||
min={MIN_YEAR_MONTH}
|
||||
max={new Date().toISOString().slice(0, 7)}
|
||||
onInput={(e) => {
|
||||
const { value, validity } = e;
|
||||
if (!validity.valid) return;
|
||||
setSearchParams(
|
||||
value
|
||||
? {
|
||||
month: value,
|
||||
}
|
||||
: {},
|
||||
);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -199,11 +359,9 @@ function AccountStatuses() {
|
|||
id,
|
||||
instance,
|
||||
authenticated,
|
||||
excludeReplies,
|
||||
excludeBoosts,
|
||||
featuredTags,
|
||||
tagged,
|
||||
media,
|
||||
searchEnabled,
|
||||
...allSearchParams,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -218,7 +376,7 @@ function AccountStatuses() {
|
|||
(filterBarRef.current.offsetWidth - active.offsetWidth) / 2,
|
||||
});
|
||||
}
|
||||
}, [featuredTags, tagged, media, excludeReplies, excludeBoosts]);
|
||||
}, [featuredTags, searchEnabled, ...allSearchParams]);
|
||||
|
||||
const accountInstance = useMemo(() => {
|
||||
if (!account?.url) return null;
|
||||
|
@ -258,7 +416,13 @@ function AccountStatuses() {
|
|||
useItemID
|
||||
boostsCarousel={snapStates.settings.boostsCarousel}
|
||||
timelineStart={TimelineStart}
|
||||
refresh={[excludeReplies, excludeBoosts, tagged, media].toString()}
|
||||
refresh={[
|
||||
excludeReplies,
|
||||
excludeBoosts,
|
||||
tagged,
|
||||
media,
|
||||
month + account?.acct,
|
||||
].toString()}
|
||||
headerEnd={
|
||||
<Menu2
|
||||
portal
|
||||
|
@ -303,4 +467,100 @@ function AccountStatuses() {
|
|||
);
|
||||
}
|
||||
|
||||
function MonthPicker(props) {
|
||||
const {
|
||||
class: className,
|
||||
disabled,
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
onInput = () => {},
|
||||
} = props;
|
||||
const [_year, _month] = value?.split('-') || [];
|
||||
const monthFieldRef = useRef();
|
||||
const yearFieldRef = useRef();
|
||||
|
||||
const checkValidity = (month, year) => {
|
||||
const [minYear, minMonth] = min?.split('-') || [];
|
||||
const [maxYear, maxMonth] = max?.split('-') || [];
|
||||
if (year < minYear) return false;
|
||||
if (year > maxYear) return false;
|
||||
if (year === minYear && month < minMonth) return false;
|
||||
if (year === maxYear && month > maxMonth) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
return (
|
||||
<div class={className}>
|
||||
<Icon icon="month" size="l" />
|
||||
<select
|
||||
ref={monthFieldRef}
|
||||
disabled={disabled}
|
||||
value={_month || ''}
|
||||
onInput={(e) => {
|
||||
const { value: month } = e.currentTarget;
|
||||
const year = yearFieldRef.current.value;
|
||||
if (!checkValidity(month, year))
|
||||
return {
|
||||
value: '',
|
||||
validity: {
|
||||
valid: false,
|
||||
},
|
||||
};
|
||||
onInput({
|
||||
value: month ? `${year}-${month}` : '',
|
||||
validity: {
|
||||
valid: true,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<option value="">Month</option>
|
||||
<option disabled>-----</option>
|
||||
{Array.from({ length: 12 }, (_, i) => (
|
||||
<option
|
||||
value={
|
||||
// Month is 1-indexed
|
||||
(i + 1).toString().padStart(2, '0')
|
||||
}
|
||||
key={i}
|
||||
>
|
||||
{new Date(0, i).toLocaleString('default', {
|
||||
month: 'long',
|
||||
})}
|
||||
</option>
|
||||
))}
|
||||
</select>{' '}
|
||||
<input
|
||||
ref={yearFieldRef}
|
||||
type="number"
|
||||
disabled={disabled}
|
||||
value={_year || new Date().getFullYear()}
|
||||
min={min?.slice(0, 4) || MIN_YEAR}
|
||||
max={max?.slice(0, 4) || new Date().getFullYear()}
|
||||
onInput={(e) => {
|
||||
const { value: year, validity } = e.currentTarget;
|
||||
const month = monthFieldRef.current.value;
|
||||
if (!validity.valid || !checkValidity(month, year))
|
||||
return {
|
||||
value: '',
|
||||
validity: {
|
||||
valid: false,
|
||||
},
|
||||
};
|
||||
onInput({
|
||||
value: year ? `${year}-${month}` : '',
|
||||
validity: {
|
||||
valid: true,
|
||||
},
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
width: '4.5em',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AccountStatuses;
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import './accounts.css';
|
||||
|
||||
import { useAutoAnimate } from '@formkit/auto-animate/preact';
|
||||
import { Menu, MenuDivider, MenuItem } from '@szhsin/react-menu';
|
||||
import { useReducer, useState } from 'preact/hooks';
|
||||
import { useReducer } from 'preact/hooks';
|
||||
|
||||
import Avatar from '../components/avatar';
|
||||
import Icon from '../components/icon';
|
||||
|
@ -18,9 +19,9 @@ function Accounts({ onClose }) {
|
|||
const accounts = store.local.getJSON('accounts');
|
||||
const currentAccount = store.session.get('currentAccount');
|
||||
const moreThanOneAccount = accounts.length > 1;
|
||||
const [currentDefault, setCurrentDefault] = useState(0);
|
||||
|
||||
const [_, reload] = useReducer((x) => x + 1, 0);
|
||||
const [accountsListParent] = useAutoAnimate();
|
||||
|
||||
return (
|
||||
<div id="accounts-container" class="sheet" tabIndex="-1">
|
||||
|
@ -34,12 +35,12 @@ function Accounts({ onClose }) {
|
|||
</header>
|
||||
<main>
|
||||
<section>
|
||||
<ul class="accounts-list">
|
||||
<ul class="accounts-list" ref={accountsListParent}>
|
||||
{accounts.map((account, i) => {
|
||||
const isCurrent = account.info.id === currentAccount;
|
||||
const isDefault = i === (currentDefault || 0);
|
||||
const isDefault = i === 0; // first account is always default
|
||||
return (
|
||||
<li key={i + account.id}>
|
||||
<li key={account.info.id}>
|
||||
<div>
|
||||
{moreThanOneAccount && (
|
||||
<span class={`current ${isCurrent ? 'is-current' : ''}`}>
|
||||
|
@ -120,7 +121,7 @@ function Accounts({ onClose }) {
|
|||
accounts.splice(i, 1);
|
||||
accounts.unshift(account);
|
||||
store.local.setJSON('accounts', accounts);
|
||||
setCurrentDefault(i);
|
||||
reload();
|
||||
}}
|
||||
>
|
||||
<Icon icon="check-circle" />
|
||||
|
|
|
@ -7,7 +7,7 @@ import useTitle from '../utils/useTitle';
|
|||
const LIMIT = 20;
|
||||
|
||||
function Favourites() {
|
||||
useTitle('Favourites', '/f');
|
||||
useTitle('Likes', '/f');
|
||||
const { masto, instance } = api();
|
||||
const favouritesIterator = useRef();
|
||||
async function fetchFavourites(firstLoad) {
|
||||
|
@ -19,10 +19,10 @@ function Favourites() {
|
|||
|
||||
return (
|
||||
<Timeline
|
||||
title="Favourites"
|
||||
title="Likes"
|
||||
id="favourites"
|
||||
emptyText="No favourites yet. Go favourite something!"
|
||||
errorText="Unable to load favourites"
|
||||
emptyText="No likes yet. Go like something!"
|
||||
errorText="Unable to load likes"
|
||||
instance={instance}
|
||||
fetchItems={fetchFavourites}
|
||||
/>
|
||||
|
|
|
@ -23,6 +23,7 @@ function Lists() {
|
|||
(async () => {
|
||||
try {
|
||||
const lists = await masto.v1.lists.list();
|
||||
lists.sort((a, b) => a.title.localeCompare(b.title));
|
||||
console.log(lists);
|
||||
setLists(lists);
|
||||
setUIState('default');
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
padding: 0;
|
||||
height: 40em;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
.notifications-menu .status {
|
||||
font-size: inherit;
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import './notifications.css';
|
||||
|
||||
import { useIdle } from '@uidotdev/usehooks';
|
||||
import { Fragment } from 'preact';
|
||||
import { memo } from 'preact/compat';
|
||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { InView } from 'react-intersection-observer';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
|
@ -166,14 +167,12 @@ function Notifications({ columnMode }) {
|
|||
}
|
||||
}, [reachStart]);
|
||||
|
||||
useEffect(() => {
|
||||
if (nearReachEnd && showMore) {
|
||||
loadNotifications();
|
||||
}
|
||||
}, [nearReachEnd, showMore]);
|
||||
// useEffect(() => {
|
||||
// if (nearReachEnd && showMore) {
|
||||
// loadNotifications();
|
||||
// }
|
||||
// }, [nearReachEnd, showMore]);
|
||||
|
||||
const idle = useIdle(5000);
|
||||
console.debug('🧘♀️ IDLE', idle);
|
||||
const loadUpdates = useCallback(() => {
|
||||
console.log('✨ Load updates', {
|
||||
autoRefresh: snapStates.settings.autoRefresh,
|
||||
|
@ -185,7 +184,7 @@ function Notifications({ columnMode }) {
|
|||
if (
|
||||
snapStates.settings.autoRefresh &&
|
||||
scrollableRef.current?.scrollTop === 0 &&
|
||||
idle &&
|
||||
window.__IDLE__ &&
|
||||
!inBackground() &&
|
||||
snapStates.notificationsShowNew &&
|
||||
uiState !== 'loading'
|
||||
|
@ -193,7 +192,6 @@ function Notifications({ columnMode }) {
|
|||
loadNotifications(true);
|
||||
}
|
||||
}, [
|
||||
idle,
|
||||
snapStates.notificationsShowNew,
|
||||
snapStates.settings.autoRefresh,
|
||||
uiState,
|
||||
|
@ -220,23 +218,23 @@ function Notifications({ columnMode }) {
|
|||
}
|
||||
}, [notificationID, notificationAccessToken]);
|
||||
|
||||
useEffect(() => {
|
||||
if (uiState === 'default') {
|
||||
(async () => {
|
||||
try {
|
||||
const registration = await getRegistration();
|
||||
if (registration?.getNotifications) {
|
||||
const notifications = await registration.getNotifications();
|
||||
console.log('🔔 Push notifications', notifications);
|
||||
// Close all notifications?
|
||||
// notifications.forEach((notification) => {
|
||||
// notification.close();
|
||||
// });
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
}
|
||||
}, [uiState]);
|
||||
// useEffect(() => {
|
||||
// if (uiState === 'default') {
|
||||
// (async () => {
|
||||
// try {
|
||||
// const registration = await getRegistration();
|
||||
// if (registration?.getNotifications) {
|
||||
// const notifications = await registration.getNotifications();
|
||||
// console.log('🔔 Push notifications', notifications);
|
||||
// // Close all notifications?
|
||||
// // notifications.forEach((notification) => {
|
||||
// // notification.close();
|
||||
// // });
|
||||
// }
|
||||
// } catch (e) {}
|
||||
// })();
|
||||
// }
|
||||
// }, [uiState]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -412,18 +410,14 @@ function Notifications({ columnMode }) {
|
|||
hideTime: true,
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<Fragment key={notification.id}>
|
||||
{differentDay && <h2 class="timeline-header">{heading}</h2>}
|
||||
<Notification
|
||||
instance={instance}
|
||||
notification={notification}
|
||||
key={notification.id}
|
||||
reload={() => {
|
||||
loadNotifications(true);
|
||||
loadFollowRequests();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
|
@ -458,15 +452,27 @@ function Notifications({ columnMode }) {
|
|||
</>
|
||||
)}
|
||||
{showMore && (
|
||||
<button
|
||||
type="button"
|
||||
class="plain block"
|
||||
disabled={uiState === 'loading'}
|
||||
onClick={() => loadNotifications()}
|
||||
style={{ marginBlockEnd: '6em' }}
|
||||
<InView
|
||||
onChange={(inView) => {
|
||||
if (inView) {
|
||||
loadNotifications();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{uiState === 'loading' ? <Loader abrupt /> : <>Show more…</>}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="plain block"
|
||||
disabled={uiState === 'loading'}
|
||||
onClick={() => loadNotifications()}
|
||||
style={{ marginBlockEnd: '6em' }}
|
||||
>
|
||||
{uiState === 'loading' ? (
|
||||
<Loader abrupt />
|
||||
) : (
|
||||
<>Show more…</>
|
||||
)}
|
||||
</button>
|
||||
</InView>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -40,8 +40,14 @@ ul.link-list.hashtag-list li a {
|
|||
}
|
||||
|
||||
@media (min-width: 40em) {
|
||||
#search-page header input {
|
||||
background-color: var(--bg-color);
|
||||
#search-page {
|
||||
header input {
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -71,6 +71,11 @@ function Search(props) {
|
|||
};
|
||||
|
||||
function loadResults(firstLoad) {
|
||||
if (!firstLoad && !authenticated) {
|
||||
// Search results pagination is only available to authenticated users
|
||||
return;
|
||||
}
|
||||
|
||||
setUIState('loading');
|
||||
if (firstLoad && !type) {
|
||||
setStatusResults(statusResults.slice(0, SHORT_LIMIT));
|
||||
|
@ -89,6 +94,7 @@ function Search(props) {
|
|||
params.type = type;
|
||||
if (authenticated) params.offset = offsetRef.current;
|
||||
}
|
||||
|
||||
try {
|
||||
const results = await masto.v2.search.fetch(params);
|
||||
console.log(results);
|
||||
|
@ -141,7 +147,7 @@ function Search(props) {
|
|||
return (
|
||||
<div id="search-page" class="deck-container" ref={scrollableRef}>
|
||||
<div class="timeline-deck deck">
|
||||
<header>
|
||||
<header class={uiState === 'loading' ? 'loading' : ''}>
|
||||
<div class="header-grid">
|
||||
<div class="header-side">
|
||||
<NavMenu />
|
||||
|
@ -181,7 +187,9 @@ function Search(props) {
|
|||
return 0;
|
||||
})
|
||||
.map((link) => (
|
||||
<Link to={link.to}>{link.label}</Link>
|
||||
<Link to={link.to} key={link.type}>
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -140,3 +140,10 @@
|
|||
color: var(--link-color);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#settings-container .version-string {
|
||||
padding: 4px;
|
||||
font-family: var(--monospace-font);
|
||||
font-size: 85%;
|
||||
text-align: center;
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
removeSubscription,
|
||||
updateSubscription,
|
||||
} from '../utils/push-notifications';
|
||||
import showToast from '../utils/show-toast';
|
||||
import states from '../utils/states';
|
||||
import store from '../utils/store';
|
||||
|
||||
|
@ -508,21 +509,38 @@ function Settings({ onClose }) {
|
|||
</p>
|
||||
{__BUILD_TIME__ && (
|
||||
<p>
|
||||
<span class="insignificant">Last build:</span>{' '}
|
||||
<RelativeTime datetime={new Date(__BUILD_TIME__)} />{' '}
|
||||
{__COMMIT_HASH__ && (
|
||||
<>
|
||||
(
|
||||
<a
|
||||
href={`https://github.com/cheeaun/phanpy/commit/${__COMMIT_HASH__}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<code>{__COMMIT_HASH__}</code>
|
||||
</a>
|
||||
)
|
||||
</>
|
||||
)}
|
||||
Version:{' '}
|
||||
<input
|
||||
type="text"
|
||||
class="version-string"
|
||||
readOnly
|
||||
size="18" // Manually calculated here
|
||||
value={`${__BUILD_TIME__.slice(0, 10).replace(/-/g, '.')}${
|
||||
__COMMIT_HASH__ ? `.${__COMMIT_HASH__}` : ''
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
e.target.select();
|
||||
// Copy to clipboard
|
||||
try {
|
||||
navigator.clipboard.writeText(e.target.value);
|
||||
showToast('Version string copied');
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
showToast('Unable to copy version string');
|
||||
}
|
||||
}}
|
||||
/>{' '}
|
||||
<span class="ib insignificant">
|
||||
(
|
||||
<a
|
||||
href={`https://github.com/cheeaun/phanpy/commit/${__COMMIT_HASH__}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<RelativeTime datetime={new Date(__BUILD_TIME__)} />
|
||||
</a>
|
||||
)
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
|
@ -709,7 +727,7 @@ function PushNotificationsSection({ onClose }) {
|
|||
},
|
||||
{
|
||||
value: 'favourite',
|
||||
label: 'Favourites',
|
||||
label: 'Likes',
|
||||
},
|
||||
{
|
||||
value: 'reblog',
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
.ancestors-indicator {
|
||||
font-size: 70% !important;
|
||||
|
||||
& > .avatar:not(:first-child) {
|
||||
& > .avatar ~ .avatar {
|
||||
margin-left: -4px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -168,7 +168,10 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
|||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const mediaParam = searchParams.get('media');
|
||||
const showMedia = parseInt(mediaParam, 10) > 0;
|
||||
const [viewMode, setViewMode] = useState(searchParams.get('view'));
|
||||
const firstLoad = useRef(!states.prevLocation && history.length === 1);
|
||||
const [viewMode, setViewMode] = useState(
|
||||
searchParams.get('view') || firstLoad.current ? 'full' : null,
|
||||
);
|
||||
const translate = !!parseInt(searchParams.get('translate'));
|
||||
const { masto, instance } = api({ instance: propInstance });
|
||||
const {
|
||||
|
@ -534,6 +537,10 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
|||
// If media is open, esc to close media first
|
||||
// Else close the status page
|
||||
enabled: !showMedia,
|
||||
ignoreEventWhen: (e) => {
|
||||
const hasModal = !!document.querySelector('#modal-container > *');
|
||||
return hasModal;
|
||||
},
|
||||
},
|
||||
);
|
||||
// For backspace, will always close both media and status page
|
||||
|
@ -839,9 +846,11 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
|||
ref={scrollableRef}
|
||||
class={`status-deck deck contained ${
|
||||
statuses.length > 1 ? 'padded-bottom' : ''
|
||||
} ${initialPageState.current === 'status' ? 'slide-in' : ''} ${
|
||||
viewMode ? `deck-view-${viewMode}` : ''
|
||||
}`}
|
||||
} ${
|
||||
initialPageState.current === 'status' && !firstLoad.current
|
||||
? 'slide-in'
|
||||
: ''
|
||||
} ${viewMode ? `deck-view-${viewMode}` : ''}`}
|
||||
onAnimationEnd={(e) => {
|
||||
// Fix the bounce effect when switching viewMode
|
||||
// `slide-in` animation kicks in when switching viewMode
|
||||
|
@ -959,6 +968,23 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
|||
)}
|
||||
</h1>
|
||||
<div class="header-side">
|
||||
<button
|
||||
type="button"
|
||||
class="plain4"
|
||||
style={{
|
||||
display: viewMode === 'full' ? '' : 'none',
|
||||
}}
|
||||
onClick={() => {
|
||||
setViewMode(null);
|
||||
searchParams.delete('media');
|
||||
searchParams.delete('media-only');
|
||||
searchParams.delete('view');
|
||||
setSearchParams(searchParams);
|
||||
}}
|
||||
title="Switch to Side Peek view"
|
||||
>
|
||||
<Icon icon="layout4" size="l" />
|
||||
</button>
|
||||
<Menu
|
||||
align="end"
|
||||
portal={{
|
||||
|
|
191
src/pages/trending.css
Normal file
191
src/pages/trending.css
Normal file
|
@ -0,0 +1,191 @@
|
|||
.links-bar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding: 16px 16px 20px 16px;
|
||||
gap: 16px;
|
||||
overflow-x: auto;
|
||||
background-color: var(--bg-faded-color);
|
||||
mask-image: linear-gradient(
|
||||
to right,
|
||||
transparent,
|
||||
black 16px,
|
||||
black calc(100% - 16px),
|
||||
transparent
|
||||
);
|
||||
text-shadow: 0 1px var(--bg-blur-color);
|
||||
transition: opacity 0.3s ease-out;
|
||||
|
||||
&:not(#columns &) {
|
||||
@media (min-width: 40em) {
|
||||
width: 95vw;
|
||||
max-width: calc(320px * 3.3);
|
||||
transform: translateX(calc(-50% + var(--main-width) / 2));
|
||||
}
|
||||
}
|
||||
|
||||
& > header {
|
||||
width: 1.2em;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
|
||||
h3 {
|
||||
font-size: 90%;
|
||||
font-style: italic;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-insignificant-color);
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 0;
|
||||
transform-origin: top left;
|
||||
transform: rotate(-90deg) translateX(-100%);
|
||||
user-select: none;
|
||||
background-image: linear-gradient(
|
||||
to left,
|
||||
var(--text-color),
|
||||
var(--link-color)
|
||||
);
|
||||
background-clip: text;
|
||||
text-fill-color: transparent;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
min-width: 240px;
|
||||
flex-grow: 1;
|
||||
max-width: 320px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
background-color: var(--accent-alpha-color);
|
||||
border: 4px solid transparent;
|
||||
box-shadow: 0 4px 8px -2px var(--drop-shadow-color);
|
||||
transition: all 0.15s ease-out;
|
||||
display: flex;
|
||||
background-image: linear-gradient(
|
||||
to bottom,
|
||||
var(--accent-color, var(--link-text-color)) -50%,
|
||||
transparent
|
||||
);
|
||||
background-clip: border-box;
|
||||
background-origin: border-box;
|
||||
min-height: 160px;
|
||||
height: 320px;
|
||||
max-height: 50vh;
|
||||
|
||||
&:not(:active):is(:hover, :focus-visible) {
|
||||
border-color: var(--accent-color, var(--link-light-color));
|
||||
box-shadow: 0 4px 8px var(--drop-shadow-color),
|
||||
0 8px 16px var(--drop-shadow-color);
|
||||
transform-origin: center bottom;
|
||||
transform: scale(1.02);
|
||||
|
||||
img {
|
||||
animation: position-object 5s ease-in-out 1s 5;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
transition: none;
|
||||
transform: scale(1.015);
|
||||
filter: brightness(0.8);
|
||||
}
|
||||
|
||||
article {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
background-color: var(--bg-color);
|
||||
background-repeat: no-repeat;
|
||||
background-image: linear-gradient(
|
||||
to bottom,
|
||||
var(--accent-alpha-color) 70%,
|
||||
var(--bg-color) 100%
|
||||
);
|
||||
transition: background-position-y 0.15s ease-out;
|
||||
|
||||
&:is(:hover, :focus-visible) {
|
||||
background-position-y: -40px;
|
||||
}
|
||||
|
||||
figure {
|
||||
flex-grow: 1;
|
||||
margin: 0 0 -16px;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
vertical-align: top;
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
hsl(0, 0%, 0%) 0%,
|
||||
hsla(0, 0%, 0%, 0.987) 14%,
|
||||
hsla(0, 0%, 0%, 0.951) 26.2%,
|
||||
hsla(0, 0%, 0%, 0.896) 36.8%,
|
||||
hsla(0, 0%, 0%, 0.825) 45.9%,
|
||||
hsla(0, 0%, 0%, 0.741) 53.7%,
|
||||
hsla(0, 0%, 0%, 0.648) 60.4%,
|
||||
hsla(0, 0%, 0%, 0.55) 66.2%,
|
||||
hsla(0, 0%, 0%, 0.45) 71.2%,
|
||||
hsla(0, 0%, 0%, 0.352) 75.6%,
|
||||
hsla(0, 0%, 0%, 0.259) 79.6%,
|
||||
hsla(0, 0%, 0%, 0.175) 83.4%,
|
||||
hsla(0, 0%, 0%, 0.104) 87.2%,
|
||||
hsla(0, 0%, 0%, 0.049) 91.1%,
|
||||
hsla(0, 0%, 0%, 0.013) 95.3%,
|
||||
hsla(0, 0%, 0%, 0) 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.article-body {
|
||||
padding: 0 8px 8px;
|
||||
line-height: 1.3;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.article-meta {
|
||||
color: var(--text-insignificant-color);
|
||||
font-size: 90%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&:hover .domain {
|
||||
color: var(--link-text-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: normal;
|
||||
font-size: inherit;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-wrap: balance;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--text-insignificant-color);
|
||||
margin: 0;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
font-size: 90%;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,7 @@
|
|||
import './trending.css';
|
||||
|
||||
import { MenuItem } from '@szhsin/react-menu';
|
||||
import { getBlurHashAverageColor } from 'fast-blurhash';
|
||||
import { useMemo, useRef, useState } from 'preact/hooks';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
@ -6,15 +9,28 @@ import { useSnapshot } from 'valtio';
|
|||
import Icon from '../components/icon';
|
||||
import Link from '../components/link';
|
||||
import Menu2 from '../components/menu2';
|
||||
import RelativeTime from '../components/relative-time';
|
||||
import Timeline from '../components/timeline';
|
||||
import { api } from '../utils/api';
|
||||
import { oklab2rgb, rgb2oklab } from '../utils/color-utils';
|
||||
import { filteredItems } from '../utils/filters';
|
||||
import pmem from '../utils/pmem';
|
||||
import states from '../utils/states';
|
||||
import { saveStatus } from '../utils/states';
|
||||
import useTitle from '../utils/useTitle';
|
||||
|
||||
const LIMIT = 20;
|
||||
|
||||
const fetchLinks = pmem(
|
||||
(masto) => {
|
||||
return masto.v1.trends.links.list().next();
|
||||
},
|
||||
{
|
||||
// News last much longer
|
||||
maxAge: 10 * 60 * 1000, // 10 minutes
|
||||
},
|
||||
);
|
||||
|
||||
function Trending({ columnMode, ...props }) {
|
||||
const snapStates = useSnapshot(states);
|
||||
const params = columnMode ? {} : useParams();
|
||||
|
@ -27,6 +43,7 @@ function Trending({ columnMode, ...props }) {
|
|||
const latestItem = useRef();
|
||||
|
||||
const [hashtags, setHashtags] = useState([]);
|
||||
const [links, setLinks] = useState([]);
|
||||
const trendIterator = useRef();
|
||||
async function fetchTrend(firstLoad) {
|
||||
if (firstLoad || !trendIterator.current) {
|
||||
|
@ -38,8 +55,24 @@ function Trending({ columnMode, ...props }) {
|
|||
try {
|
||||
const iterator = masto.v1.trends.tags.list();
|
||||
const { value: tags } = await iterator.next();
|
||||
console.log(tags);
|
||||
setHashtags(tags);
|
||||
console.log('tags', tags);
|
||||
if (tags?.length) {
|
||||
setHashtags(tags);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
// Get links
|
||||
try {
|
||||
const { value } = await fetchLinks(masto);
|
||||
// 4 types available: link, photo, video, rich
|
||||
// Only want links for now
|
||||
const links = value?.filter?.((link) => link.type === 'link');
|
||||
console.log('links', links);
|
||||
if (links?.length) {
|
||||
setLinks(links);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
@ -84,26 +117,124 @@ function Trending({ columnMode, ...props }) {
|
|||
}
|
||||
|
||||
const TimelineStart = useMemo(() => {
|
||||
if (!hashtags.length) return null;
|
||||
return (
|
||||
<div class="filter-bar">
|
||||
<Icon icon="chart" class="insignificant" size="l" />
|
||||
{hashtags.map((tag, i) => {
|
||||
const { name, history } = tag;
|
||||
const total = history.reduce((acc, cur) => acc + +cur.uses, 0);
|
||||
return (
|
||||
<Link to={`/${instance}/t/${name}`}>
|
||||
<span>
|
||||
<span class="more-insignificant">#</span>
|
||||
{name}
|
||||
</span>
|
||||
<span class="filter-count">{total.toLocaleString()}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<>
|
||||
{!!hashtags.length && (
|
||||
<div class="filter-bar">
|
||||
<Icon icon="chart" class="insignificant" size="l" />
|
||||
{hashtags.map((tag, i) => {
|
||||
const { name, history } = tag;
|
||||
const total = history.reduce((acc, cur) => acc + +cur.uses, 0);
|
||||
return (
|
||||
<Link to={`/${instance}/t/${name}`} key={name}>
|
||||
<span>
|
||||
<span class="more-insignificant">#</span>
|
||||
{name}
|
||||
</span>
|
||||
<span class="filter-count">{total.toLocaleString()}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{!!links.length && (
|
||||
<div class="links-bar">
|
||||
<header>
|
||||
<h3>Trending News</h3>
|
||||
</header>
|
||||
{links.map((link) => {
|
||||
const {
|
||||
authorName,
|
||||
authorUrl,
|
||||
blurhash,
|
||||
description,
|
||||
height,
|
||||
image,
|
||||
imageDescription,
|
||||
language,
|
||||
providerName,
|
||||
providerUrl,
|
||||
publishedAt,
|
||||
title,
|
||||
url,
|
||||
width,
|
||||
} = link;
|
||||
const domain = new URL(url).hostname
|
||||
.replace(/^www\./, '')
|
||||
.replace(/\/$/, '');
|
||||
let accentColor;
|
||||
if (blurhash) {
|
||||
const averageColor = getBlurHashAverageColor(blurhash);
|
||||
const labAverageColor = rgb2oklab(averageColor);
|
||||
accentColor = oklab2rgb([
|
||||
0.6,
|
||||
labAverageColor[1],
|
||||
labAverageColor[2],
|
||||
]);
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
key={url}
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={
|
||||
accentColor
|
||||
? {
|
||||
'--accent-color': `rgb(${accentColor.join(',')})`,
|
||||
'--accent-alpha-color': `rgba(${accentColor.join(
|
||||
',',
|
||||
)}, 0.4)`,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<article>
|
||||
<figure>
|
||||
<img
|
||||
src={image}
|
||||
alt={imageDescription}
|
||||
width={width}
|
||||
height={height}
|
||||
loading="lazy"
|
||||
/>
|
||||
</figure>
|
||||
<div class="article-body">
|
||||
<header>
|
||||
<div class="article-meta">
|
||||
<span class="domain">{domain}</span>{' '}
|
||||
{!!publishedAt && <>· </>}
|
||||
{!!publishedAt && (
|
||||
<>
|
||||
<RelativeTime
|
||||
datetime={publishedAt}
|
||||
format="micro"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{!!title && (
|
||||
<h1 class="title" lang={language} dir="auto">
|
||||
{title}
|
||||
</h1>
|
||||
)}
|
||||
</header>
|
||||
{!!description && (
|
||||
<p class="description" lang={language} dir="auto">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}, [hashtags]);
|
||||
}, [hashtags, links]);
|
||||
|
||||
return (
|
||||
<Timeline
|
||||
|
|
52
src/utils/color-utils.js
Normal file
52
src/utils/color-utils.js
Normal file
|
@ -0,0 +1,52 @@
|
|||
// https://gist.github.com/earthbound19/e7fe15fdf8ca3ef814750a61bc75b5ce
|
||||
function clamp(value, min, max) {
|
||||
return Math.max(Math.min(value, max), min);
|
||||
}
|
||||
|
||||
const gammaToLinear = (c) =>
|
||||
c >= 0.04045 ? Math.pow((c + 0.055) / 1.055, 2.4) : c / 12.92;
|
||||
const linearToGamma = (c) =>
|
||||
c >= 0.0031308 ? 1.055 * Math.pow(c, 1 / 2.4) - 0.055 : 12.92 * c;
|
||||
|
||||
export function rgb2oklab([r, g, b]) {
|
||||
r = gammaToLinear(r / 255);
|
||||
g = gammaToLinear(g / 255);
|
||||
b = gammaToLinear(b / 255);
|
||||
var l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
|
||||
var m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
|
||||
var s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
|
||||
l = Math.cbrt(l);
|
||||
m = Math.cbrt(m);
|
||||
s = Math.cbrt(s);
|
||||
return [
|
||||
l * +0.2104542553 + m * +0.793617785 + s * -0.0040720468,
|
||||
l * +1.9779984951 + m * -2.428592205 + s * +0.4505937099,
|
||||
l * +0.0259040371 + m * +0.7827717662 + s * -0.808675766,
|
||||
];
|
||||
}
|
||||
|
||||
export function oklab2rgb([L, a, b]) {
|
||||
var l = L + a * +0.3963377774 + b * +0.2158037573;
|
||||
var m = L + a * -0.1055613458 + b * -0.0638541728;
|
||||
var s = L + a * -0.0894841775 + b * -1.291485548;
|
||||
// The ** operator here cubes; same as l_*l_*l_ in the C++ example:
|
||||
l = l ** 3;
|
||||
m = m ** 3;
|
||||
s = s ** 3;
|
||||
var r = l * +4.0767416621 + m * -3.3077115913 + s * +0.2309699292;
|
||||
var g = l * -1.2684380046 + m * +2.6097574011 + s * -0.3413193965;
|
||||
var b = l * -0.0041960863 + m * -0.7034186147 + s * +1.707614701;
|
||||
// Convert linear RGB values returned from oklab math to sRGB for our use before returning them:
|
||||
r = 255 * linearToGamma(r);
|
||||
g = 255 * linearToGamma(g);
|
||||
b = 255 * linearToGamma(b);
|
||||
// OPTION: clamp r g and b values to the range 0-255; but if you use the values immediately to draw, JavaScript clamps them on use:
|
||||
r = clamp(r, 0, 255);
|
||||
g = clamp(g, 0, 255);
|
||||
b = clamp(b, 0, 255);
|
||||
// OPTION: round the values. May not be necessary if you use them immediately for rendering in JavaScript, as JavaScript (also) discards decimals on render:
|
||||
r = Math.round(r);
|
||||
g = Math.round(g);
|
||||
b = Math.round(b);
|
||||
return [r, g, b];
|
||||
}
|
|
@ -6,7 +6,7 @@ function groupNotifications(notifications) {
|
|||
const cleanNotifications = [];
|
||||
for (let i = 0, j = 0; i < notifications.length; i++) {
|
||||
const notification = notifications[i];
|
||||
const { status, account, type, createdAt } = notification;
|
||||
const { id, status, account, type, createdAt } = notification;
|
||||
const date = new Date(createdAt).toLocaleDateString();
|
||||
let virtualType = type;
|
||||
if (type === 'favourite' || type === 'reblog') {
|
||||
|
@ -23,9 +23,11 @@ function groupNotifications(notifications) {
|
|||
if (mappedAccount) {
|
||||
mappedAccount._types.push(type);
|
||||
mappedAccount._types.sort().reverse();
|
||||
mappedNotification.id += `-${id}`;
|
||||
} else {
|
||||
account._types = [type];
|
||||
mappedNotification._accounts.push(account);
|
||||
mappedNotification.id += `-${id}`;
|
||||
}
|
||||
} else {
|
||||
account._types = [type];
|
||||
|
@ -47,13 +49,14 @@ function groupNotifications(notifications) {
|
|||
const cleanNotifications2 = [];
|
||||
for (let i = 0, j = 0; i < cleanNotifications.length; i++) {
|
||||
const notification = cleanNotifications[i];
|
||||
const { account, _accounts, type, createdAt } = notification;
|
||||
const { id, account, _accounts, type, createdAt } = notification;
|
||||
const date = new Date(createdAt).toLocaleDateString();
|
||||
if (type === 'favourite+reblog' && account && _accounts.length === 1) {
|
||||
const key = `${account?.id}-${type}-${date}`;
|
||||
const mappedNotification = notificationsMap2[key];
|
||||
if (mappedNotification) {
|
||||
mappedNotification._statuses.push(notification.status);
|
||||
mappedNotification.id += `-${id}`;
|
||||
} else {
|
||||
let n = (notificationsMap2[key] = {
|
||||
...notification,
|
||||
|
|
|
@ -81,3 +81,42 @@ export function getCurrentInstance() {
|
|||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// Massage these instance configurations to match the Mastodon API
|
||||
// - Pleroma
|
||||
function getInstanceConfiguration(instance) {
|
||||
const {
|
||||
configuration,
|
||||
maxMediaAttachments,
|
||||
maxTootChars,
|
||||
pleroma,
|
||||
pollLimits,
|
||||
} = instance;
|
||||
|
||||
const statuses = configuration?.statuses || {};
|
||||
if (maxMediaAttachments) {
|
||||
statuses.maxMediaAttachments ??= maxMediaAttachments;
|
||||
}
|
||||
if (maxTootChars) {
|
||||
statuses.maxCharacters ??= maxTootChars;
|
||||
}
|
||||
|
||||
const polls = configuration?.polls || {};
|
||||
if (pollLimits) {
|
||||
polls.maxCharactersPerOption ??= pollLimits.maxOptionChars;
|
||||
polls.maxExpiration ??= pollLimits.maxExpiration;
|
||||
polls.maxOptions ??= pollLimits.maxOptions;
|
||||
polls.minExpiration ??= pollLimits.minExpiration;
|
||||
}
|
||||
|
||||
return {
|
||||
...configuration,
|
||||
statuses,
|
||||
polls,
|
||||
};
|
||||
}
|
||||
|
||||
export function getCurrentInstanceConfiguration() {
|
||||
const instance = getCurrentInstance();
|
||||
return getInstanceConfiguration(instance);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue