Merge pull request #11 from cheeaun/main

This commit is contained in:
Chee Aun 2022-12-19 18:44:01 +08:00 committed by GitHub
commit 82770e8035
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 7056 additions and 838 deletions

View file

@ -1,5 +1,5 @@
<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
===

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View file

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

Binary file not shown.

View file

@ -4,6 +4,10 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Phanpy</title>
<meta
name="description"
content="Minimalistic opinionated Mastodon web client"
/>
<meta name="color-scheme" content="dark light" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<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="mobile-web-app-capable" content="yes" />
<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>
<body>
<div id="app"></div>

5940
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -6,7 +6,8 @@
"dev": "vite",
"build": "vite build",
"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": {
"@github/text-expander-element": "~2.3.0",
@ -14,7 +15,7 @@
"fast-blurhash": "~1.1.2",
"history": "~5.3.0",
"iconify-icon": "~1.0.2",
"masto": "~4.10.0",
"masto": "~4.10.1",
"mem": "~9.0.2",
"preact": "~10.11.3",
"preact-router": "~4.1.0",
@ -29,7 +30,12 @@
"autoprefixer": "~10.4.13",
"postcss": "~8.4.20",
"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": {
"plugins": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
public/logo-192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
public/logo-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

2
public/robots.txt Normal file
View file

@ -0,0 +1,2 @@
User-agent: *
Allow: /

58
public/sw.js Normal file
View 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);

View file

@ -72,7 +72,6 @@ a.mention span {
.deck header {
min-height: 3em;
padding: 0 8px;
position: sticky;
top: 0;
background-color: var(--bg-blur-color);
@ -92,7 +91,7 @@ a.mention span {
text-align: right;
}
.deck header h1 {
margin: 0;
margin: 0 8px;
padding: 0;
font-size: 1.2em;
text-align: center;
@ -105,7 +104,8 @@ a.mention span {
font-size: 1.45em;
}
.deck.padded-bottom .timeline > li:last-child {
padding-bottom: 80vh;
padding-bottom: 80vh !important;
padding-bottom: 80dvh !important;
}
.timeline {
@ -146,7 +146,41 @@ a.mention span {
.timeline.contextual > li.descendant {
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;
--diameter: calc(var(--radius) * 2);
content: '';
@ -161,9 +195,83 @@ a.mention span {
border-color: transparent transparent var(--comment-line-color) transparent;
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;
}
.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 {
max-height: max(25vh, 160px);
@ -353,13 +461,18 @@ a.mention span {
.carousel-top-controls {
top: 0;
top: env(safe-area-inset-top, 0);
}
.carousel-controls {
bottom: 0;
bottom: env(safe-area-inset-bottom, 0);
}
:is(.carousel-top-controls, .carousel-controls) {
position: fixed;
width: 100%;
left: 0;
left: env(safe-area-inset-left, 0);
right: 0;
right: env(safe-area-inset-right, 0);
padding: 16px;
display: flex;
gap: 8px;
@ -432,7 +545,9 @@ button.carousel-dot[disabled].active {
#compose-button {
position: fixed;
bottom: 16px;
bottom: max(16px, env(safe-area-inset-bottom));
right: 16px;
right: max(16px, env(safe-area-inset-right));
padding: 16px;
box-shadow: 0 0 32px var(--bg-color);
z-index: 1;
@ -463,6 +578,9 @@ button.carousel-dot[disabled].active {
max-width: calc(40em - 50px - 16px);
border-radius: 16px 16px 0 0;
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);
animation: slide-up 0.2s ease-out;
border: 1px solid var(--outline-color);

View file

@ -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">
<path fill="none" d="M0 0h63.99v63.99H0z"/>
<g stroke-width="6">
<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="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"/>
<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">
<g stroke-width="12">
<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="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"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 703 B

After

Width:  |  Height:  |  Size: 672 B

View file

@ -12,6 +12,10 @@
font-size: 95%;
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 {
display: flex;
@ -38,6 +42,7 @@
gap: 12px;
}
#account-container .profile-field {
min-width: 0;
flex-grow: 1;
font-size: 90%;
background-color: var(--bg-faded-color);
@ -52,6 +57,9 @@
color: var(--text-insignificant-color);
text-transform: uppercase;
}
#account-container .profile-field b .icon {
color: var(--green-color);
}
#account-container .profile-field p {
margin: 0;
}

View file

@ -7,6 +7,7 @@ import shortenNumber from '../utils/shorten-number';
import store from '../utils/store';
import Avatar from './avatar';
import Icon from './icon';
import NameText from './name-text';
function Account({ account }) {
@ -129,18 +130,28 @@ function Account({ account }) {
__html: enhanceContent(note, { emojis }),
}}
/>
<div class="profile-metadata">
{fields.map(({ name, value }) => (
<div class="profile-field" key={name}>
<b>{name}</b>
<p
dangerouslySetInnerHTML={{
__html: value,
}}
/>
</div>
))}
</div>
{fields?.length > 0 && (
<div class="profile-metadata">
{fields.map(({ name, value, verifiedAt }) => (
<div
class={`profile-field ${
verifiedAt ? 'profile-verified' : ''
}`}
key={name}
>
<b>
{name}{' '}
{!!verifiedAt && <Icon icon="check-circle" size="s" />}
</b>
<p
dangerouslySetInnerHTML={{
__html: value,
}}
/>
</div>
))}
</div>
)}
<p class="stats">
<span>
<b title={statusesCount}>{shortenNumber(statusesCount)}</b> Posts

View file

@ -41,6 +41,7 @@ const ICONS = {
popout: 'mingcute:external-link-line',
popin: ['mingcute:external-link-line', '180deg'],
plus: 'mingcute:add-circle-line',
'chevron-left': 'mingcute:left-line',
};
function Icon({ icon, size = 'm', alt, title, class: className = '' }) {

View file

@ -3,8 +3,9 @@
width: 16px;
height: 16px;
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;
margin: 8px;
}
@keyframes slow-appear {
0% {

View file

@ -90,14 +90,6 @@
.status.skeleton > .avatar {
background-color: var(--outline-color);
}
.indirect .status {
padding-left: 57px;
}
.indirect .status .avatar {
width: 25px !important;
height: 25px !important;
transform: translateX(5px);
}
.status .container {
flex-grow: 1;
@ -242,7 +234,7 @@
margin-top: 8px;
display: flex;
flex-wrap: wrap;
gap: 8px;
gap: 2px;
flex-direction: row;
}
.status .media {
@ -454,12 +446,16 @@ a.card:hover {
margin: 8px 0;
font-size: 90%;
}
.poll-option-title .icon {
vertical-align: middle;
}
/* EXTRA META */
.status .extra-meta {
padding-top: 8px;
color: var(--text-insignificant-color);
font-size: 90%;
}
.status .extra-meta * {
vertical-align: middle;
@ -477,7 +473,7 @@ a.card:hover {
}
.status.large .extra-meta {
padding-top: 0;
margin-left: calc(-50px - 8px);
margin-left: calc(-50px - 16px);
}
/* ACTIONS */
@ -485,32 +481,36 @@ a.card:hover {
.status .actions {
display: flex;
gap: 8px;
justify-content: space-between;
}
.status.large .actions {
padding-top: 8px;
padding-top: 4px;
padding-bottom: 16px;
margin-left: calc(-50px - 16px);
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-width: 40px;
padding: 0 8px;
font-variant-numeric: tabular-nums;
}
.status .actions > button.plain {
.status .action > button.plain {
color: inherit;
border: 1.5px solid transparent;
}
.status .actions > button.plain:hover {
.status .action > button.plain:hover {
color: var(--link-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);
}
.status .actions > button.plain.reblog-button.checked {
.status .action > button.plain.reblog-button.checked {
color: var(--reblog-color);
border-color: var(--reblog-color);
}
@ -529,13 +529,13 @@ a.card:hover {
opacity: 0;
}
}
.status .actions > button.plain.reblog-button.checked .icon {
.status .action > button.plain.reblog-button.checked .icon {
animation: reblogged 1s ease-in-out;
}
.status .actions > button.plain.favourite-button:hover {
.status .action > button.plain.favourite-button:hover {
color: var(--favourite-color);
}
.status .actions > button.plain.favourite-button.checked {
.status .action > button.plain.favourite-button.checked {
color: var(--favourite-color);
border-color: var(--favourite-color);
}
@ -557,11 +557,11 @@ a.card:hover {
opacity: 0;
}
}
.status .actions > button.plain.favourite-button.checked .icon {
.status .action > button.plain.favourite-button.checked .icon {
transform-origin: bottom center;
animation: hearted 1s ease-in-out;
}
.status .actions > button.plain.bookmark-button.checked {
.status .action > button.plain.bookmark-button.checked {
color: var(--link-color);
border-color: var(--link-color);
}

File diff suppressed because it is too large Load diff

View file

@ -12,7 +12,7 @@
--text-insignificant-color: #1c1e2199;
--link-color: var(--blue-color);
--link-light-color: #4169e199;
--link-faded-color: #4169e122;
--link-faded-color: #4169e133;
--link-bg-hover-color: #f0f2f599;
--button-bg-color: var(--blue-color);
--button-bg-blur-color: #4169e1aa;
@ -44,7 +44,7 @@
--text-color: #f0f2f5;
--text-insignificant-color: #f0f2f599;
--link-light-color: #6494ed99;
--link-faded-color: #6494ed44;
--link-faded-color: #6494ed55;
--link-bg-hover-color: #34353799;
--divider-color: rgba(255, 255, 255, 0.1);
--bg-blur-color: #24252699;

View file

@ -85,7 +85,8 @@ function Home({ hidden }) {
setTimeout(() => {
(async () => {
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) {
states.homeNew = newStatus;

View file

@ -11,6 +11,7 @@ import { useSnapshot } from 'valtio';
import Icon from '../components/icon';
import Loader from '../components/loader';
import Status from '../components/status';
import shortenNumber from '../utils/shorten-number';
import states from '../utils/states';
import useTitle from '../utils/useTitle';
@ -21,22 +22,37 @@ function StatusPage({ id }) {
const heroStatusRef = useRef();
useEffect(async () => {
// If id is completely new, reset the whole list
if (!statuses.find((s) => s.id === id)) {
const containsStatus = 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 }]);
}
setUIState('loading');
if (!states.statuses.has(id)) {
try {
const status = await masto.statuses.fetch(id);
states.statuses.set(id, status);
} catch (e) {
const hasStatus = snapStates.statuses.has(id);
let heroStatus = snapStates.statuses.get(id);
try {
heroStatus = await masto.statuses.fetch(id);
states.statuses.set(id, heroStatus);
} catch (e) {
// Silent fail if status is cached
if (!hasStatus) {
setUIState('error');
alert('Error fetching status');
return;
}
return;
}
try {
@ -46,38 +62,51 @@ function StatusPage({ id }) {
ancestors.forEach((status) => {
states.statuses.set(status.id, status);
});
const directReplies = [];
const nestedDescendants = [];
descendants.forEach((status) => {
states.statuses.set(status.id, status);
if (status.inReplyToId === id) {
directReplies.push(status);
if (status.inReplyToAccountId === status.account.id) {
// 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) {
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);
}
}
console.log({ ancestors, descendants, nestedDescendants });
const allStatuses = [
...ancestors.map((s) => ({ id: s.id, ancestor: true })),
{ id },
...descendants.map((s) => ({
...ancestors.map((s) => ({
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,
directReply:
s.inReplyToId === id || s.inReplyToAccountId === s.account.id,
// I can assume if the reply is to the same account, it's a direct reply. In other words, it's a thread?!?
thread: s.account.id === heroStatus.account.id,
replies: s.__replies?.map((r) => r.id),
})),
];
console.log({ allStatuses });
setStatuses(allStatuses);
} catch (e) {
console.error(e);
setUIState('error');
}
@ -94,13 +123,16 @@ function StatusPage({ id }) {
}, [id]);
useLayoutEffect(() => {
heroStatusRef.current?.scrollIntoView({
// behavior: 'smooth',
block: 'start',
});
const hasAncestor = statuses.some((s) => s.ancestor);
if (hasAncestor) {
heroStatusRef.current?.scrollIntoView({
// behavior: 'smooth',
block: 'start',
});
}
}, [statuses]);
const heroStatus = states.statuses.get(id);
const heroStatus = snapStates.statuses.get(id);
const heroDisplayName = useMemo(() => {
// Remove shortcodes from display name
if (!heroStatus) return '';
@ -133,14 +165,19 @@ function StatusPage({ id }) {
: 'Status',
);
const comments = statuses.filter((s) => s.descendant);
const replies = comments.filter((s) => s.directReply);
const prevRoute = states.history.findLast((h) => {
return h === '/' || /notifications/i.test(h);
});
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 (
<div class="deck-backdrop">
<Link href={closeLink}></Link>
@ -150,6 +187,11 @@ function StatusPage({ id }) {
}`}
>
<header>
{/* <div>
<Link class="button plain deck-close" href={closeLink}>
<Icon icon="chevron-left" size="xl" />
</Link>
</div> */}
<h1>Status</h1>
<div class="header-side">
<Loader hidden={uiState !== 'loading'} />
@ -159,8 +201,14 @@ function StatusPage({ id }) {
</div>
</header>
<ul class="timeline flat contextual">
{statuses.map((status) => {
const { id: statusID, ancestor, descendant, directReply } = status;
{statuses.slice(0, limit).map((status) => {
const {
id: statusID,
ancestor,
descendant,
thread,
replies,
} = status;
const isHero = statusID === id;
return (
<li
@ -168,7 +216,7 @@ function StatusPage({ id }) {
ref={isHero ? heroStatusRef : null}
class={`${ancestor ? 'ancestor' : ''} ${
descendant ? 'descendant' : ''
} ${descendant && !directReply ? 'indirect' : ''}`}
} ${thread ? 'thread' : ''}`}
>
{isHero ? (
<Status statusID={statusID} withinContext size="l" />
@ -179,36 +227,57 @@ function StatusPage({ id }) {
"
href={`#/s/${statusID}`}
>
<Status statusID={statusID} withinContext />
<Status
statusID={statusID}
withinContext
size={thread || ancestor ? 'm' : 's'}
/>
</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' &&
isHero &&
!!heroStatus?.repliesCount &&
statuses.length === 1 && (
<div class="status-loading">
<Loader />
{/* {' '}<span>
{!!replies.length &&
replies.length !== comments.length && (
<>
{replies.length} repl
{replies.length > 1 ? 'ies' : 'y'}
</>
)}
{!!comments.length && (
<>
{' '}
&bull; {comments.length} comment
{comments.length > 1 ? 's' : ''}
</>
)}
</span> */}
</div>
)}
</li>
);
})}
{showMore > 0 && (
<li>
<button
type="button"
class="plain block"
disabled={uiState === 'loading'}
onClick={() => setLimit((l) => l + 40)}
style={{ marginBlockEnd: '6em' }}
>
Show more&hellip;{' '}
<span class="tag">{showMore > 40 ? '40+' : showMore}</span>
</button>
</li>
)}
</ul>
</div>
</div>

View file

@ -1,5 +1,6 @@
const div = document.createElement('div');
export default function htmlContentLength(html) {
if (!html) return 0;
div.innerHTML = html;
return div.innerText.length;
}

View 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);
}

View file

@ -2,16 +2,53 @@ import preact from '@preact/preset-vite';
import { execSync } from 'child_process';
import { resolve } from 'path';
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();
// https://vitejs.dev/config/
export default defineConfig({
mode: NODE_ENV,
define: {
__BUILD_TIME__: JSON.stringify(Date.now()),
__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: {
sourcemap: true,
rollupOptions: {