Merge pull request #11 from cheeaun/main
|
@ -1,5 +1,5 @@
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="design/logo-2.svg" width="128" height="128" alt="">
|
<img src="design/logo-3.svg" width="128" height="128" alt="">
|
||||||
|
|
||||||
Phanpy
|
Phanpy
|
||||||
===
|
===
|
||||||
|
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 47 KiB |
|
@ -1 +1,10 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;"><rect id="Logo-simple" serif:id="Logo simple" x="0" y="0" width="63.994" height="63.994" style="fill:none;"/><g id="Logo-simple1" serif:id="Logo simple"><path d="M56.352,22.413c-1.293,-5.447 -5.633,-10.525 -10.622,-12.696c-5.656,-2.462 -17.315,-3.499 -23.317,-2.075c-5.293,1.256 -10.462,5.488 -12.696,10.621c-2.462,5.657 -3.499,17.316 -2.075,23.318c1.293,5.447 5.633,10.525 10.621,12.696c5.657,2.462 17.316,3.499 23.318,2.075c5.293,-1.256 10.462,-5.488 12.696,-10.622c2.462,-5.656 3.499,-17.315 2.075,-23.317Z" style="fill:#d8e7fe;stroke:#a4bff7;stroke-width:6px;"/><path d="M38.644,24.754c0.838,4.163 1.381,10.15 1.004,15.758" style="fill:none;stroke:#6892e2;stroke-width:6px;"/><path d="M27.013,23.719c-1.56,3.95 -3.152,9.747 -3.77,15.333" style="fill:none;stroke:#6892e2;stroke-width:6px;"/></g></svg>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 128 128" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
|
||||||
|
<rect id="Logo-simple" serif:id="Logo simple" x="0" y="0" width="127.988" height="127.988" style="fill:none;"/>
|
||||||
|
<g id="Logo-simple1" serif:id="Logo simple">
|
||||||
|
<path d="M107.564,46.848c-2.312,-9.745 -10.077,-18.829 -19.001,-22.713c-10.12,-4.404 -30.977,-6.26 -41.715,-3.712c-9.469,2.248 -18.716,9.818 -22.713,19.002c-4.404,10.119 -6.26,30.977 -3.712,41.714c2.313,9.746 10.078,18.83 19.002,22.714c10.119,4.404 30.977,6.26 41.714,3.711c9.47,-2.247 18.717,-9.817 22.714,-19.001c4.404,-10.12 6.26,-30.977 3.711,-41.715Z" style="fill:#d8e7fe;stroke:#a4bff7;stroke-width:12px;"/>
|
||||||
|
<path d="M75.885,51.037c1.5,7.447 2.472,18.158 1.796,28.19" style="fill:none;stroke:#6892e2;stroke-width:12px;"/>
|
||||||
|
<path d="M55.078,49.186c-2.791,7.065 -5.639,17.436 -6.745,27.429" style="fill:none;stroke:#6892e2;stroke-width:12px;"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.3 KiB |
14
index.html
|
@ -4,6 +4,10 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Phanpy</title>
|
<title>Phanpy</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Minimalistic opinionated Mastodon web client"
|
||||||
|
/>
|
||||||
<meta name="color-scheme" content="dark light" />
|
<meta name="color-scheme" content="dark light" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||||
|
@ -11,6 +15,16 @@
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
<link rel="canonical" href="https://phanpy.social" />
|
<link rel="canonical" href="https://phanpy.social" />
|
||||||
|
<meta
|
||||||
|
name="theme-color"
|
||||||
|
content="#fff"
|
||||||
|
media="(prefers-color-scheme: light)"
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
name="theme-color"
|
||||||
|
content="#242526"
|
||||||
|
media="(prefers-color-scheme: dark)"
|
||||||
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
5940
package-lock.json
generated
12
package.json
|
@ -6,7 +6,8 @@
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"fetch-instances": "env $(cat .env.dev | grep -v \"#\" | xargs) node scripts/fetch-instances-list.js"
|
"fetch-instances": "env $(cat .env.dev | grep -v \"#\" | xargs) node scripts/fetch-instances-list.js",
|
||||||
|
"source-map-explorer": "npx source-map-explorer dist/assets/*.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@github/text-expander-element": "~2.3.0",
|
"@github/text-expander-element": "~2.3.0",
|
||||||
|
@ -14,7 +15,7 @@
|
||||||
"fast-blurhash": "~1.1.2",
|
"fast-blurhash": "~1.1.2",
|
||||||
"history": "~5.3.0",
|
"history": "~5.3.0",
|
||||||
"iconify-icon": "~1.0.2",
|
"iconify-icon": "~1.0.2",
|
||||||
"masto": "~4.10.0",
|
"masto": "~4.10.1",
|
||||||
"mem": "~9.0.2",
|
"mem": "~9.0.2",
|
||||||
"preact": "~10.11.3",
|
"preact": "~10.11.3",
|
||||||
"preact-router": "~4.1.0",
|
"preact-router": "~4.1.0",
|
||||||
|
@ -29,7 +30,12 @@
|
||||||
"autoprefixer": "~10.4.13",
|
"autoprefixer": "~10.4.13",
|
||||||
"postcss": "~8.4.20",
|
"postcss": "~8.4.20",
|
||||||
"postcss-dark-theme-class": "~0.7.3",
|
"postcss-dark-theme-class": "~0.7.3",
|
||||||
"vite": "~4.0.1"
|
"vite": "~4.0.2",
|
||||||
|
"vite-plugin-pwa": "~0.14.0",
|
||||||
|
"workbox-cacheable-response": "~6.5.4",
|
||||||
|
"workbox-expiration": "~6.5.4",
|
||||||
|
"workbox-routing": "~6.5.4",
|
||||||
|
"workbox-strategies": "~6.5.4"
|
||||||
},
|
},
|
||||||
"postcss": {
|
"postcss": {
|
||||||
"plugins": {
|
"plugins": {
|
||||||
|
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 2.2 KiB |
BIN
public/logo-192.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
public/logo-512.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
2
public/robots.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
User-agent: *
|
||||||
|
Allow: /
|
58
public/sw.js
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import { CacheableResponsePlugin } from 'workbox-cacheable-response';
|
||||||
|
import { ExpirationPlugin } from 'workbox-expiration';
|
||||||
|
import { RegExpRoute, registerRoute, Route } from 'workbox-routing';
|
||||||
|
import { CacheFirst, StaleWhileRevalidate } from 'workbox-strategies';
|
||||||
|
|
||||||
|
const imageRoute = new Route(
|
||||||
|
({ request, sameOrigin }) => {
|
||||||
|
return !sameOrigin && request.destination === 'image';
|
||||||
|
},
|
||||||
|
new CacheFirst({
|
||||||
|
cacheName: 'remote-images',
|
||||||
|
plugins: [
|
||||||
|
new ExpirationPlugin({
|
||||||
|
maxAgeSeconds: 7 * 24 * 60 * 60, // 7 days
|
||||||
|
purgeOnQuotaError: true,
|
||||||
|
}),
|
||||||
|
new CacheableResponsePlugin({
|
||||||
|
statuses: [0, 200],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
registerRoute(imageRoute);
|
||||||
|
|
||||||
|
// Cache /instance because masto.js has to keep calling it while initializing
|
||||||
|
const apiExtendedRoute = new RegExpRoute(
|
||||||
|
/^https?:\/\/[^\/]+\/api\/v\d+\/instance/,
|
||||||
|
new StaleWhileRevalidate({
|
||||||
|
cacheName: 'api-extended',
|
||||||
|
plugins: [
|
||||||
|
new ExpirationPlugin({
|
||||||
|
maxAgeSeconds: 24 * 60 * 60, // 1 day
|
||||||
|
}),
|
||||||
|
new CacheableResponsePlugin({
|
||||||
|
statuses: [0, 200],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
registerRoute(apiExtendedRoute);
|
||||||
|
|
||||||
|
// Not caching API requests, doesn't seem to be necessary fo now
|
||||||
|
//
|
||||||
|
// const apiRoute = new RegExpRoute(
|
||||||
|
// /^https?:\/\/[^\/]+\/api\//,
|
||||||
|
// new StaleWhileRevalidate({
|
||||||
|
// cacheName: 'api',
|
||||||
|
// plugins: [
|
||||||
|
// new ExpirationPlugin({
|
||||||
|
// maxAgeSeconds: 60, // 1 minute
|
||||||
|
// }),
|
||||||
|
// new CacheableResponsePlugin({
|
||||||
|
// statuses: [0, 200],
|
||||||
|
// }),
|
||||||
|
// ],
|
||||||
|
// }),
|
||||||
|
// );
|
||||||
|
// registerRoute(apiRoute);
|
130
src/app.css
|
@ -72,7 +72,6 @@ a.mention span {
|
||||||
|
|
||||||
.deck header {
|
.deck header {
|
||||||
min-height: 3em;
|
min-height: 3em;
|
||||||
padding: 0 8px;
|
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
background-color: var(--bg-blur-color);
|
background-color: var(--bg-blur-color);
|
||||||
|
@ -92,7 +91,7 @@ a.mention span {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
.deck header h1 {
|
.deck header h1 {
|
||||||
margin: 0;
|
margin: 0 8px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -105,7 +104,8 @@ a.mention span {
|
||||||
font-size: 1.45em;
|
font-size: 1.45em;
|
||||||
}
|
}
|
||||||
.deck.padded-bottom .timeline > li:last-child {
|
.deck.padded-bottom .timeline > li:last-child {
|
||||||
padding-bottom: 80vh;
|
padding-bottom: 80vh !important;
|
||||||
|
padding-bottom: 80dvh !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline {
|
.timeline {
|
||||||
|
@ -146,7 +146,41 @@ a.mention span {
|
||||||
.timeline.contextual > li.descendant {
|
.timeline.contextual > li.descendant {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.timeline.contextual > li.descendant.indirect:before {
|
.timeline.contextual > li.descendant {
|
||||||
|
padding-bottom: 1em;
|
||||||
|
}
|
||||||
|
.timeline.contextual > li.descendant:not(.thread) > .status-link {
|
||||||
|
padding-left: 40px;
|
||||||
|
}
|
||||||
|
.timeline.contextual
|
||||||
|
> li.descendant.thread
|
||||||
|
> .status-link
|
||||||
|
+ .replies
|
||||||
|
> summary {
|
||||||
|
margin-left: calc(50px + 16px + 16px);
|
||||||
|
}
|
||||||
|
.timeline.contextual
|
||||||
|
> li.descendant.thread
|
||||||
|
> .status-link
|
||||||
|
+ .replies
|
||||||
|
.status-link {
|
||||||
|
padding-left: calc(50px + 16px + 16px);
|
||||||
|
}
|
||||||
|
.timeline.contextual
|
||||||
|
> li.descendant:not(.thread)
|
||||||
|
> .status-link
|
||||||
|
+ .replies
|
||||||
|
> summary {
|
||||||
|
margin-left: calc(40px + 16px);
|
||||||
|
}
|
||||||
|
.timeline.contextual
|
||||||
|
> li.descendant:not(.thread)
|
||||||
|
> .status-link
|
||||||
|
+ .replies
|
||||||
|
.status-link {
|
||||||
|
padding-left: calc(40px + 16px);
|
||||||
|
}
|
||||||
|
.timeline.contextual > li.descendant:not(.thread):before {
|
||||||
--radius: 10px;
|
--radius: 10px;
|
||||||
--diameter: calc(var(--radius) * 2);
|
--diameter: calc(var(--radius) * 2);
|
||||||
content: '';
|
content: '';
|
||||||
|
@ -161,9 +195,83 @@ a.mention span {
|
||||||
border-color: transparent transparent var(--comment-line-color) transparent;
|
border-color: transparent transparent var(--comment-line-color) transparent;
|
||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
}
|
}
|
||||||
.timeline.contextual > li.descendant.indirect .status-link {
|
.timeline.contextual > li .replies {
|
||||||
|
margin-top: -12px;
|
||||||
|
}
|
||||||
|
.timeline.contextual > li .replies :is(ul, li) {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
.timeline.contextual > li .replies summary {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background-color: var(--bg-faded-color);
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-insignificant-color);
|
||||||
|
user-select: none;
|
||||||
|
box-shadow: 0 0 0 2px var(--bg-color);
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
.timeline.contextual > li .replies summary:active,
|
||||||
|
.timeline.contextual > li .replies[open] summary {
|
||||||
|
color: var(--text-color);
|
||||||
|
background-color: var(--comment-line-color);
|
||||||
|
background-image: linear-gradient(
|
||||||
|
to top right,
|
||||||
|
var(--comment-line-color),
|
||||||
|
var(--bg-faded-color)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
.timeline.contextual > li .replies[open] summary {
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
}
|
||||||
|
.timeline.contextual > li .replies summary[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.timeline.contextual > li .replies li {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.timeline.contextual > li .replies li .status {
|
||||||
|
--width: 3px;
|
||||||
|
--left: 0px;
|
||||||
|
--right: calc(var(--left) + var(--width));
|
||||||
|
background-image: linear-gradient(
|
||||||
|
to right,
|
||||||
|
transparent,
|
||||||
|
transparent var(--left),
|
||||||
|
var(--comment-line-color) var(--left),
|
||||||
|
var(--comment-line-color) var(--right),
|
||||||
|
transparent var(--right),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
.timeline.contextual > li .replies li:last-child .status {
|
||||||
|
background-size: 100% 20px;
|
||||||
|
}
|
||||||
|
.timeline.contextual > li .replies li:before {
|
||||||
|
--radius: 10px;
|
||||||
|
--diameter: calc(var(--radius) * 2);
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: calc(40px + 16px);
|
||||||
|
width: var(--diameter);
|
||||||
|
height: var(--diameter);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
border-style: solid;
|
||||||
|
border-width: var(--width);
|
||||||
|
border-color: transparent transparent var(--comment-line-color) transparent;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
.timeline.contextual > li.thread .replies li:before {
|
||||||
|
left: calc(50px + 16px + 16px);
|
||||||
|
}
|
||||||
|
|
||||||
.timeline-deck.compact .status {
|
.timeline-deck.compact .status {
|
||||||
max-height: max(25vh, 160px);
|
max-height: max(25vh, 160px);
|
||||||
|
@ -353,13 +461,18 @@ a.mention span {
|
||||||
|
|
||||||
.carousel-top-controls {
|
.carousel-top-controls {
|
||||||
top: 0;
|
top: 0;
|
||||||
|
top: env(safe-area-inset-top, 0);
|
||||||
}
|
}
|
||||||
.carousel-controls {
|
.carousel-controls {
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
bottom: env(safe-area-inset-bottom, 0);
|
||||||
}
|
}
|
||||||
:is(.carousel-top-controls, .carousel-controls) {
|
:is(.carousel-top-controls, .carousel-controls) {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
width: 100%;
|
left: 0;
|
||||||
|
left: env(safe-area-inset-left, 0);
|
||||||
|
right: 0;
|
||||||
|
right: env(safe-area-inset-right, 0);
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
@ -432,7 +545,9 @@ button.carousel-dot[disabled].active {
|
||||||
#compose-button {
|
#compose-button {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 16px;
|
bottom: 16px;
|
||||||
|
bottom: max(16px, env(safe-area-inset-bottom));
|
||||||
right: 16px;
|
right: 16px;
|
||||||
|
right: max(16px, env(safe-area-inset-right));
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
box-shadow: 0 0 32px var(--bg-color);
|
box-shadow: 0 0 32px var(--bg-color);
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
@ -463,6 +578,9 @@ button.carousel-dot[disabled].active {
|
||||||
max-width: calc(40em - 50px - 16px);
|
max-width: calc(40em - 50px - 16px);
|
||||||
border-radius: 16px 16px 0 0;
|
border-radius: 16px 16px 0 0;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
padding-left: max(16px, env(safe-area-inset-left));
|
||||||
|
padding-right: max(16px, env(safe-area-inset-right));
|
||||||
|
padding-bottom: max(16px, env(safe-area-inset-bottom));
|
||||||
box-shadow: 0 -1px 32px var(--divider-color);
|
box-shadow: 0 -1px 32px var(--divider-color);
|
||||||
animation: slide-up 0.2s ease-out;
|
animation: slide-up 0.2s ease-out;
|
||||||
border: 1px solid var(--outline-color);
|
border: 1px solid var(--outline-color);
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="1.5" clip-rule="evenodd" viewBox="0 0 64 64">
|
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="1.5" clip-rule="evenodd" viewBox="0 0 128 128">
|
||||||
<path fill="none" d="M0 0h63.99v63.99H0z"/>
|
<g stroke-width="12">
|
||||||
<g stroke-width="6">
|
<path fill="#d8e7fe" stroke="#a4bff7" d="M107.56 46.85c-2.3-9.75-10.07-18.83-19-22.72-10.12-4.4-30.97-6.26-41.71-3.7-9.47 2.24-18.72 9.81-22.72 19-4.4 10.11-6.26 30.97-3.7 41.7 2.3 9.76 10.07 18.84 19 22.72 10.11 4.4 30.97 6.26 41.7 3.71 9.48-2.24 18.73-9.81 22.72-19 4.4-10.12 6.26-30.97 3.71-41.71Z"/>
|
||||||
<path fill="#d8e7fe" stroke="#a4bff7" d="M56.35 22.41a19.43 19.43 0 0 0-10.62-12.7c-5.66-2.46-17.32-3.5-23.32-2.07a19.43 19.43 0 0 0-12.7 10.62c-2.46 5.66-3.5 17.32-2.07 23.32a19.43 19.43 0 0 0 10.62 12.7c5.66 2.46 17.32 3.5 23.32 2.07a19.43 19.43 0 0 0 12.7-10.62c2.46-5.66 3.5-17.31 2.07-23.32Z"/>
|
<path fill="none" stroke="#6892e2" d="M75.89 51.04c1.5 7.44 2.47 18.16 1.8 28.19M55.08 49.19a113.84 113.84 0 0 0-6.75 27.43"/>
|
||||||
<path fill="none" stroke="#6892e2" d="M38.64 24.75a63.7 63.7 0 0 1 1 15.76M27.01 23.72a63.64 63.64 0 0 0-3.77 15.33"/>
|
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 703 B After Width: | Height: | Size: 672 B |
|
@ -12,6 +12,10 @@
|
||||||
font-size: 95%;
|
font-size: 95%;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
#account-container .note:not(:has(p)) {
|
||||||
|
/* Some notes don't have <p> tags, so we need to add some padding */
|
||||||
|
padding: 1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
#account-container .stats {
|
#account-container .stats {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -38,6 +42,7 @@
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
#account-container .profile-field {
|
#account-container .profile-field {
|
||||||
|
min-width: 0;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
background-color: var(--bg-faded-color);
|
background-color: var(--bg-faded-color);
|
||||||
|
@ -52,6 +57,9 @@
|
||||||
color: var(--text-insignificant-color);
|
color: var(--text-insignificant-color);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
#account-container .profile-field b .icon {
|
||||||
|
color: var(--green-color);
|
||||||
|
}
|
||||||
#account-container .profile-field p {
|
#account-container .profile-field p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import shortenNumber from '../utils/shorten-number';
|
||||||
import store from '../utils/store';
|
import store from '../utils/store';
|
||||||
|
|
||||||
import Avatar from './avatar';
|
import Avatar from './avatar';
|
||||||
|
import Icon from './icon';
|
||||||
import NameText from './name-text';
|
import NameText from './name-text';
|
||||||
|
|
||||||
function Account({ account }) {
|
function Account({ account }) {
|
||||||
|
@ -129,10 +130,19 @@ function Account({ account }) {
|
||||||
__html: enhanceContent(note, { emojis }),
|
__html: enhanceContent(note, { emojis }),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{fields?.length > 0 && (
|
||||||
<div class="profile-metadata">
|
<div class="profile-metadata">
|
||||||
{fields.map(({ name, value }) => (
|
{fields.map(({ name, value, verifiedAt }) => (
|
||||||
<div class="profile-field" key={name}>
|
<div
|
||||||
<b>{name}</b>
|
class={`profile-field ${
|
||||||
|
verifiedAt ? 'profile-verified' : ''
|
||||||
|
}`}
|
||||||
|
key={name}
|
||||||
|
>
|
||||||
|
<b>
|
||||||
|
{name}{' '}
|
||||||
|
{!!verifiedAt && <Icon icon="check-circle" size="s" />}
|
||||||
|
</b>
|
||||||
<p
|
<p
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: value,
|
__html: value,
|
||||||
|
@ -141,6 +151,7 @@ function Account({ account }) {
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<p class="stats">
|
<p class="stats">
|
||||||
<span>
|
<span>
|
||||||
<b title={statusesCount}>{shortenNumber(statusesCount)}</b> Posts
|
<b title={statusesCount}>{shortenNumber(statusesCount)}</b> Posts
|
||||||
|
|
|
@ -41,6 +41,7 @@ const ICONS = {
|
||||||
popout: 'mingcute:external-link-line',
|
popout: 'mingcute:external-link-line',
|
||||||
popin: ['mingcute:external-link-line', '180deg'],
|
popin: ['mingcute:external-link-line', '180deg'],
|
||||||
plus: 'mingcute:add-circle-line',
|
plus: 'mingcute:add-circle-line',
|
||||||
|
'chevron-left': 'mingcute:left-line',
|
||||||
};
|
};
|
||||||
|
|
||||||
function Icon({ icon, size = 'm', alt, title, class: className = '' }) {
|
function Icon({ icon, size = 'm', alt, title, class: className = '' }) {
|
||||||
|
|
|
@ -3,8 +3,9 @@
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
animation: slow-appear .3s ease-in-out 1s both;
|
animation: slow-appear 0.3s ease-in-out 1s both;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
margin: 8px;
|
||||||
}
|
}
|
||||||
@keyframes slow-appear {
|
@keyframes slow-appear {
|
||||||
0% {
|
0% {
|
||||||
|
|
|
@ -90,14 +90,6 @@
|
||||||
.status.skeleton > .avatar {
|
.status.skeleton > .avatar {
|
||||||
background-color: var(--outline-color);
|
background-color: var(--outline-color);
|
||||||
}
|
}
|
||||||
.indirect .status {
|
|
||||||
padding-left: 57px;
|
|
||||||
}
|
|
||||||
.indirect .status .avatar {
|
|
||||||
width: 25px !important;
|
|
||||||
height: 25px !important;
|
|
||||||
transform: translateX(5px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status .container {
|
.status .container {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
@ -242,7 +234,7 @@
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: 2px;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
.status .media {
|
.status .media {
|
||||||
|
@ -454,12 +446,16 @@ a.card:hover {
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
}
|
}
|
||||||
|
.poll-option-title .icon {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
/* EXTRA META */
|
/* EXTRA META */
|
||||||
|
|
||||||
.status .extra-meta {
|
.status .extra-meta {
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
color: var(--text-insignificant-color);
|
color: var(--text-insignificant-color);
|
||||||
|
font-size: 90%;
|
||||||
}
|
}
|
||||||
.status .extra-meta * {
|
.status .extra-meta * {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
@ -477,7 +473,7 @@ a.card:hover {
|
||||||
}
|
}
|
||||||
.status.large .extra-meta {
|
.status.large .extra-meta {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
margin-left: calc(-50px - 8px);
|
margin-left: calc(-50px - 16px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ACTIONS */
|
/* ACTIONS */
|
||||||
|
@ -485,32 +481,36 @@ a.card:hover {
|
||||||
.status .actions {
|
.status .actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
justify-content: space-between;
|
|
||||||
}
|
}
|
||||||
.status.large .actions {
|
.status.large .actions {
|
||||||
padding-top: 8px;
|
padding-top: 4px;
|
||||||
padding-bottom: 16px;
|
padding-bottom: 16px;
|
||||||
margin-left: calc(-50px - 16px);
|
margin-left: calc(-50px - 16px);
|
||||||
color: var(--text-insignificant-color);
|
color: var(--text-insignificant-color);
|
||||||
|
border-top: 1px solid var(--outline-color);
|
||||||
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
.status .actions > button {
|
.status .action.has-count {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.status .action > button {
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
min-width: 40px;
|
min-width: 40px;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
.status .actions > button.plain {
|
.status .action > button.plain {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
border: 1.5px solid transparent;
|
border: 1.5px solid transparent;
|
||||||
}
|
}
|
||||||
.status .actions > button.plain:hover {
|
.status .action > button.plain:hover {
|
||||||
color: var(--link-color);
|
color: var(--link-color);
|
||||||
background-color: var(--button-plain-bg-hover-color);
|
background-color: var(--button-plain-bg-hover-color);
|
||||||
}
|
}
|
||||||
.status .actions > button.plain.reblog-button:hover {
|
.status .action > button.plain.reblog-button:hover {
|
||||||
color: var(--reblog-color);
|
color: var(--reblog-color);
|
||||||
}
|
}
|
||||||
.status .actions > button.plain.reblog-button.checked {
|
.status .action > button.plain.reblog-button.checked {
|
||||||
color: var(--reblog-color);
|
color: var(--reblog-color);
|
||||||
border-color: var(--reblog-color);
|
border-color: var(--reblog-color);
|
||||||
}
|
}
|
||||||
|
@ -529,13 +529,13 @@ a.card:hover {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.status .actions > button.plain.reblog-button.checked .icon {
|
.status .action > button.plain.reblog-button.checked .icon {
|
||||||
animation: reblogged 1s ease-in-out;
|
animation: reblogged 1s ease-in-out;
|
||||||
}
|
}
|
||||||
.status .actions > button.plain.favourite-button:hover {
|
.status .action > button.plain.favourite-button:hover {
|
||||||
color: var(--favourite-color);
|
color: var(--favourite-color);
|
||||||
}
|
}
|
||||||
.status .actions > button.plain.favourite-button.checked {
|
.status .action > button.plain.favourite-button.checked {
|
||||||
color: var(--favourite-color);
|
color: var(--favourite-color);
|
||||||
border-color: var(--favourite-color);
|
border-color: var(--favourite-color);
|
||||||
}
|
}
|
||||||
|
@ -557,11 +557,11 @@ a.card:hover {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.status .actions > button.plain.favourite-button.checked .icon {
|
.status .action > button.plain.favourite-button.checked .icon {
|
||||||
transform-origin: bottom center;
|
transform-origin: bottom center;
|
||||||
animation: hearted 1s ease-in-out;
|
animation: hearted 1s ease-in-out;
|
||||||
}
|
}
|
||||||
.status .actions > button.plain.bookmark-button.checked {
|
.status .action > button.plain.bookmark-button.checked {
|
||||||
color: var(--link-color);
|
color: var(--link-color);
|
||||||
border-color: var(--link-color);
|
border-color: var(--link-color);
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
--text-insignificant-color: #1c1e2199;
|
--text-insignificant-color: #1c1e2199;
|
||||||
--link-color: var(--blue-color);
|
--link-color: var(--blue-color);
|
||||||
--link-light-color: #4169e199;
|
--link-light-color: #4169e199;
|
||||||
--link-faded-color: #4169e122;
|
--link-faded-color: #4169e133;
|
||||||
--link-bg-hover-color: #f0f2f599;
|
--link-bg-hover-color: #f0f2f599;
|
||||||
--button-bg-color: var(--blue-color);
|
--button-bg-color: var(--blue-color);
|
||||||
--button-bg-blur-color: #4169e1aa;
|
--button-bg-blur-color: #4169e1aa;
|
||||||
|
@ -44,7 +44,7 @@
|
||||||
--text-color: #f0f2f5;
|
--text-color: #f0f2f5;
|
||||||
--text-insignificant-color: #f0f2f599;
|
--text-insignificant-color: #f0f2f599;
|
||||||
--link-light-color: #6494ed99;
|
--link-light-color: #6494ed99;
|
||||||
--link-faded-color: #6494ed44;
|
--link-faded-color: #6494ed55;
|
||||||
--link-bg-hover-color: #34353799;
|
--link-bg-hover-color: #34353799;
|
||||||
--divider-color: rgba(255, 255, 255, 0.1);
|
--divider-color: rgba(255, 255, 255, 0.1);
|
||||||
--bg-blur-color: #24252699;
|
--bg-blur-color: #24252699;
|
||||||
|
|
|
@ -85,7 +85,8 @@ function Home({ hidden }) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const newStatus = await masto.timelines.fetchHome({
|
const newStatus = await masto.timelines.fetchHome({
|
||||||
limit: 1,
|
limit: 2,
|
||||||
|
// Need 2 because "new posts" only appear when there are 2 or more
|
||||||
});
|
});
|
||||||
if (newStatus.length && newStatus[0].id !== states.home[0].id) {
|
if (newStatus.length && newStatus[0].id !== states.home[0].id) {
|
||||||
states.homeNew = newStatus;
|
states.homeNew = newStatus;
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { useSnapshot } from 'valtio';
|
||||||
import Icon from '../components/icon';
|
import Icon from '../components/icon';
|
||||||
import Loader from '../components/loader';
|
import Loader from '../components/loader';
|
||||||
import Status from '../components/status';
|
import Status from '../components/status';
|
||||||
|
import shortenNumber from '../utils/shorten-number';
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
import useTitle from '../utils/useTitle';
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
|
@ -21,22 +22,37 @@ function StatusPage({ id }) {
|
||||||
const heroStatusRef = useRef();
|
const heroStatusRef = useRef();
|
||||||
|
|
||||||
useEffect(async () => {
|
useEffect(async () => {
|
||||||
// If id is completely new, reset the whole list
|
const containsStatus = statuses.find((s) => s.id === id);
|
||||||
if (!statuses.find((s) => s.id === id)) {
|
const statusesWithSameAccountID = statuses.filter(
|
||||||
|
(s) => s.accountID === containsStatus?.accountID,
|
||||||
|
);
|
||||||
|
if (statusesWithSameAccountID.length > 1) {
|
||||||
|
setStatuses(
|
||||||
|
statusesWithSameAccountID.map((s) => ({
|
||||||
|
...s,
|
||||||
|
thread: true,
|
||||||
|
descendant: undefined,
|
||||||
|
ancestor: undefined,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
setStatuses([{ id }]);
|
setStatuses([{ id }]);
|
||||||
}
|
}
|
||||||
|
|
||||||
setUIState('loading');
|
setUIState('loading');
|
||||||
|
|
||||||
if (!states.statuses.has(id)) {
|
const hasStatus = snapStates.statuses.has(id);
|
||||||
|
let heroStatus = snapStates.statuses.get(id);
|
||||||
try {
|
try {
|
||||||
const status = await masto.statuses.fetch(id);
|
heroStatus = await masto.statuses.fetch(id);
|
||||||
states.statuses.set(id, status);
|
states.statuses.set(id, heroStatus);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
// Silent fail if status is cached
|
||||||
|
if (!hasStatus) {
|
||||||
setUIState('error');
|
setUIState('error');
|
||||||
alert('Error fetching status');
|
alert('Error fetching status');
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -46,38 +62,51 @@ function StatusPage({ id }) {
|
||||||
ancestors.forEach((status) => {
|
ancestors.forEach((status) => {
|
||||||
states.statuses.set(status.id, status);
|
states.statuses.set(status.id, status);
|
||||||
});
|
});
|
||||||
const directReplies = [];
|
const nestedDescendants = [];
|
||||||
descendants.forEach((status) => {
|
descendants.forEach((status) => {
|
||||||
states.statuses.set(status.id, status);
|
states.statuses.set(status.id, status);
|
||||||
if (status.inReplyToId === id) {
|
if (status.inReplyToAccountId === status.account.id) {
|
||||||
directReplies.push(status);
|
// If replying to self, it's part of the thread, level 1
|
||||||
|
nestedDescendants.push(status);
|
||||||
|
} else if (status.inReplyToId === heroStatus.id) {
|
||||||
|
// If replying to the hero status, it's a reply, level 1
|
||||||
|
nestedDescendants.push(status);
|
||||||
|
} else {
|
||||||
|
// If replying to someone else, it's a reply to a reply, level 2
|
||||||
|
const parent = descendants.find((s) => s.id === status.inReplyToId);
|
||||||
|
if (parent) {
|
||||||
|
if (!parent.__replies) {
|
||||||
|
parent.__replies = [];
|
||||||
|
}
|
||||||
|
parent.__replies.push(status);
|
||||||
|
} else {
|
||||||
|
// If no parent, it's probably a reply to a reply to a reply, level 3
|
||||||
|
console.warn('[LEVEL 3] No parent found for', status);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
console.log({ ancestors, descendants, directReplies });
|
|
||||||
|
|
||||||
if (directReplies.length) {
|
console.log({ ancestors, descendants, nestedDescendants });
|
||||||
const heroStatus = states.statuses.get(id);
|
|
||||||
const heroStatusRepliesCount = heroStatus.repliesCount;
|
|
||||||
if (heroStatusRepliesCount != directReplies.length) {
|
|
||||||
// If replies count doesn't match, refetch the status
|
|
||||||
const status = await masto.statuses.fetch(id);
|
|
||||||
states.statuses.set(id, status);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const allStatuses = [
|
const allStatuses = [
|
||||||
...ancestors.map((s) => ({ id: s.id, ancestor: true })),
|
...ancestors.map((s) => ({
|
||||||
{ id },
|
|
||||||
...descendants.map((s) => ({
|
|
||||||
id: s.id,
|
id: s.id,
|
||||||
|
ancestor: true,
|
||||||
|
accountID: s.account.id,
|
||||||
|
})),
|
||||||
|
{ id, accountID: heroStatus.account.id },
|
||||||
|
...nestedDescendants.map((s) => ({
|
||||||
|
id: s.id,
|
||||||
|
accountID: s.account.id,
|
||||||
descendant: true,
|
descendant: true,
|
||||||
directReply:
|
thread: s.account.id === heroStatus.account.id,
|
||||||
s.inReplyToId === id || s.inReplyToAccountId === s.account.id,
|
replies: s.__replies?.map((r) => r.id),
|
||||||
// I can assume if the reply is to the same account, it's a direct reply. In other words, it's a thread?!?
|
|
||||||
})),
|
})),
|
||||||
];
|
];
|
||||||
|
console.log({ allStatuses });
|
||||||
setStatuses(allStatuses);
|
setStatuses(allStatuses);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
setUIState('error');
|
setUIState('error');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,13 +123,16 @@ function StatusPage({ id }) {
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
|
const hasAncestor = statuses.some((s) => s.ancestor);
|
||||||
|
if (hasAncestor) {
|
||||||
heroStatusRef.current?.scrollIntoView({
|
heroStatusRef.current?.scrollIntoView({
|
||||||
// behavior: 'smooth',
|
// behavior: 'smooth',
|
||||||
block: 'start',
|
block: 'start',
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}, [statuses]);
|
}, [statuses]);
|
||||||
|
|
||||||
const heroStatus = states.statuses.get(id);
|
const heroStatus = snapStates.statuses.get(id);
|
||||||
const heroDisplayName = useMemo(() => {
|
const heroDisplayName = useMemo(() => {
|
||||||
// Remove shortcodes from display name
|
// Remove shortcodes from display name
|
||||||
if (!heroStatus) return '';
|
if (!heroStatus) return '';
|
||||||
|
@ -133,14 +165,19 @@ function StatusPage({ id }) {
|
||||||
: 'Status',
|
: 'Status',
|
||||||
);
|
);
|
||||||
|
|
||||||
const comments = statuses.filter((s) => s.descendant);
|
|
||||||
const replies = comments.filter((s) => s.directReply);
|
|
||||||
|
|
||||||
const prevRoute = states.history.findLast((h) => {
|
const prevRoute = states.history.findLast((h) => {
|
||||||
return h === '/' || /notifications/i.test(h);
|
return h === '/' || /notifications/i.test(h);
|
||||||
});
|
});
|
||||||
const closeLink = `#${prevRoute || '/'}`;
|
const closeLink = `#${prevRoute || '/'}`;
|
||||||
|
|
||||||
|
const [limit, setLimit] = useState(40);
|
||||||
|
const showMore = useMemo(() => {
|
||||||
|
// return number of statuses to show
|
||||||
|
return statuses.length - limit;
|
||||||
|
}, [statuses.length, limit]);
|
||||||
|
|
||||||
|
const hasManyStatuses = statuses.length > 40;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="deck-backdrop">
|
<div class="deck-backdrop">
|
||||||
<Link href={closeLink}></Link>
|
<Link href={closeLink}></Link>
|
||||||
|
@ -150,6 +187,11 @@ function StatusPage({ id }) {
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<header>
|
<header>
|
||||||
|
{/* <div>
|
||||||
|
<Link class="button plain deck-close" href={closeLink}>
|
||||||
|
<Icon icon="chevron-left" size="xl" />
|
||||||
|
</Link>
|
||||||
|
</div> */}
|
||||||
<h1>Status</h1>
|
<h1>Status</h1>
|
||||||
<div class="header-side">
|
<div class="header-side">
|
||||||
<Loader hidden={uiState !== 'loading'} />
|
<Loader hidden={uiState !== 'loading'} />
|
||||||
|
@ -159,8 +201,14 @@ function StatusPage({ id }) {
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<ul class="timeline flat contextual">
|
<ul class="timeline flat contextual">
|
||||||
{statuses.map((status) => {
|
{statuses.slice(0, limit).map((status) => {
|
||||||
const { id: statusID, ancestor, descendant, directReply } = status;
|
const {
|
||||||
|
id: statusID,
|
||||||
|
ancestor,
|
||||||
|
descendant,
|
||||||
|
thread,
|
||||||
|
replies,
|
||||||
|
} = status;
|
||||||
const isHero = statusID === id;
|
const isHero = statusID === id;
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
|
@ -168,7 +216,7 @@ function StatusPage({ id }) {
|
||||||
ref={isHero ? heroStatusRef : null}
|
ref={isHero ? heroStatusRef : null}
|
||||||
class={`${ancestor ? 'ancestor' : ''} ${
|
class={`${ancestor ? 'ancestor' : ''} ${
|
||||||
descendant ? 'descendant' : ''
|
descendant ? 'descendant' : ''
|
||||||
} ${descendant && !directReply ? 'indirect' : ''}`}
|
} ${thread ? 'thread' : ''}`}
|
||||||
>
|
>
|
||||||
{isHero ? (
|
{isHero ? (
|
||||||
<Status statusID={statusID} withinContext size="l" />
|
<Status statusID={statusID} withinContext size="l" />
|
||||||
|
@ -179,36 +227,57 @@ function StatusPage({ id }) {
|
||||||
"
|
"
|
||||||
href={`#/s/${statusID}`}
|
href={`#/s/${statusID}`}
|
||||||
>
|
>
|
||||||
<Status statusID={statusID} withinContext />
|
<Status
|
||||||
|
statusID={statusID}
|
||||||
|
withinContext
|
||||||
|
size={thread || ancestor ? 'm' : 's'}
|
||||||
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
{descendant && replies?.length > 0 && (
|
||||||
|
<details class="replies" open={!hasManyStatuses}>
|
||||||
|
<summary hidden={!hasManyStatuses}>
|
||||||
|
<span title={replies.length}>
|
||||||
|
{shortenNumber(replies.length)}
|
||||||
|
</span>{' '}
|
||||||
|
repl{replies.length === 1 ? 'y' : 'ies'}
|
||||||
|
</summary>
|
||||||
|
<ul>
|
||||||
|
{replies.map((replyID) => (
|
||||||
|
<li key={replyID}>
|
||||||
|
<Link class="status-link" href={`#/s/${replyID}`}>
|
||||||
|
<Status statusID={replyID} withinContext size="s" />
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
{uiState === 'loading' &&
|
{uiState === 'loading' &&
|
||||||
isHero &&
|
isHero &&
|
||||||
!!heroStatus?.repliesCount &&
|
!!heroStatus?.repliesCount &&
|
||||||
statuses.length === 1 && (
|
statuses.length === 1 && (
|
||||||
<div class="status-loading">
|
<div class="status-loading">
|
||||||
<Loader />
|
<Loader />
|
||||||
{/* {' '}<span>
|
|
||||||
{!!replies.length &&
|
|
||||||
replies.length !== comments.length && (
|
|
||||||
<>
|
|
||||||
{replies.length} repl
|
|
||||||
{replies.length > 1 ? 'ies' : 'y'}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!!comments.length && (
|
|
||||||
<>
|
|
||||||
{' '}
|
|
||||||
• {comments.length} comment
|
|
||||||
{comments.length > 1 ? 's' : ''}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</span> */}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{showMore > 0 && (
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="plain block"
|
||||||
|
disabled={uiState === 'loading'}
|
||||||
|
onClick={() => setLimit((l) => l + 40)}
|
||||||
|
style={{ marginBlockEnd: '6em' }}
|
||||||
|
>
|
||||||
|
Show more…{' '}
|
||||||
|
<span class="tag">{showMore > 40 ? '40+' : showMore}</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
export default function htmlContentLength(html) {
|
export default function htmlContentLength(html) {
|
||||||
|
if (!html) return 0;
|
||||||
div.innerHTML = html;
|
div.innerHTML = html;
|
||||||
return div.innerText.length;
|
return div.innerText.length;
|
||||||
}
|
}
|
||||||
|
|
23
src/utils/useDebouncedCallback.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { useCallback, useRef } from 'preact/hooks';
|
||||||
|
|
||||||
|
export default function useDebouncedCallback(
|
||||||
|
callback,
|
||||||
|
delay,
|
||||||
|
dependencies = [],
|
||||||
|
) {
|
||||||
|
const timeout = useRef();
|
||||||
|
|
||||||
|
const comboDeps = dependencies
|
||||||
|
? [callback, delay, ...dependencies]
|
||||||
|
: [callback, delay];
|
||||||
|
|
||||||
|
return useCallback((...args) => {
|
||||||
|
if (timeout.current != null) {
|
||||||
|
clearTimeout(timeout.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout.current = setTimeout(() => {
|
||||||
|
callback(...args);
|
||||||
|
}, delay);
|
||||||
|
}, comboDeps);
|
||||||
|
}
|
|
@ -2,16 +2,53 @@ import preact from '@preact/preset-vite';
|
||||||
import { execSync } from 'child_process';
|
import { execSync } from 'child_process';
|
||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
import { defineConfig, splitVendorChunkPlugin } from 'vite';
|
import { defineConfig, splitVendorChunkPlugin } from 'vite';
|
||||||
|
import { VitePWA } from 'vite-plugin-pwa';
|
||||||
|
|
||||||
|
const { VITE_CLIENT_NAME: CLIENT_NAME, NODE_ENV } = process.env;
|
||||||
|
|
||||||
const commitHash = execSync('git rev-parse --short HEAD').toString().trim();
|
const commitHash = execSync('git rev-parse --short HEAD').toString().trim();
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
mode: NODE_ENV,
|
||||||
define: {
|
define: {
|
||||||
__BUILD_TIME__: JSON.stringify(Date.now()),
|
__BUILD_TIME__: JSON.stringify(Date.now()),
|
||||||
__COMMIT_HASH__: JSON.stringify(commitHash),
|
__COMMIT_HASH__: JSON.stringify(commitHash),
|
||||||
},
|
},
|
||||||
plugins: [preact(), splitVendorChunkPlugin()],
|
plugins: [
|
||||||
|
preact(),
|
||||||
|
splitVendorChunkPlugin(),
|
||||||
|
VitePWA({
|
||||||
|
manifest: {
|
||||||
|
name: CLIENT_NAME,
|
||||||
|
short_name: CLIENT_NAME,
|
||||||
|
description: 'Minimalistic opinionated Mastodon web client',
|
||||||
|
theme_color: '#ffffff',
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: 'logo-192.png',
|
||||||
|
sizes: '192x192',
|
||||||
|
type: 'image/png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'logo-512.png',
|
||||||
|
sizes: '512x512',
|
||||||
|
type: 'image/png',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
strategies: 'injectManifest',
|
||||||
|
injectRegister: 'inline',
|
||||||
|
injectManifest: {
|
||||||
|
// Prevent "Unable to find a place to inject the manifest" error
|
||||||
|
injectionPoint: undefined,
|
||||||
|
},
|
||||||
|
devOptions: {
|
||||||
|
enabled: NODE_ENV === 'development',
|
||||||
|
type: 'module',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
build: {
|
build: {
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
|
|