Compare commits

...

49 commits

Author SHA1 Message Date
Natsu Kagami 2b7969f9ec
Merge branch 'main' of github.com:cheeaun/phanpy 2023-10-21 23:26:48 +02:00
Lim Chee Aun 4c4e89ac9d Contain the overscroll behavior in notifications popover 2023-10-20 23:11:26 +08:00
Lim Chee Aun 4da968df2e Fix avatars not bunching properly 2023-10-20 22:10:55 +08:00
Lim Chee Aun c6f368ac0b Make sure the calendar picker works in dark mode 2023-10-20 22:04:56 +08:00
Lim Chee Aun 87e243ea58 Make scrolling work inside filter bar 2023-10-20 22:00:56 +08:00
Lim Chee Aun 66f9c3b918 Fix async/await 2023-10-20 20:54:24 +08:00
Lim Chee Aun 137ad7f4dd Cache search enabled check 2023-10-20 20:48:30 +08:00
Lim Chee Aun 8ddc44fba6 Mobile Safari need this
Else it'll be almost zero width
2023-10-20 19:46:47 +08:00
Lim Chee Aun 3721acf3d3 Attempt to make month picker better 2023-10-20 19:24:01 +08:00
Lim Chee Aun ab7df0f66c Experiment: month filter for account statuses 2023-10-20 18:11:13 +08:00
Lim Chee Aun d1aedcaef2 Fix unneeded id passed here 2023-10-20 17:11:10 +08:00
Lim Chee Aun 691aea3389 Update loading state of account info 2023-10-20 13:07:31 +08:00
Lim Chee Aun 72f204771f Minor adjustments for search page 2023-10-20 12:53:23 +08:00
Lim Chee Aun dba921a3fd Add key 2023-10-20 12:52:56 +08:00
Lim Chee Aun 4646859177 Fix text shadows applied to search popover 2023-10-20 00:11:14 +08:00
Lim Chee Aun 66fa6fbe52 Memoize getHTMLText 2023-10-19 22:57:56 +08:00
Lim Chee Aun 861619ce57 Fix max-width of nav menu 2023-10-19 22:10:20 +08:00
Lim Chee Aun 71bf8608e6 Relayout the menu items in nav menu again 2023-10-19 21:07:00 +08:00
Lim Chee Aun 2916d1146b Adjust the <p> out 2023-10-19 20:50:32 +08:00
Lim Chee Aun d62712d587 double-tap zoom out once reach max scale 2023-10-19 20:47:11 +08:00
Lim Chee Aun a37c3d6081 Sneak in a slight copy change 2023-10-19 20:19:55 +08:00
Lim Chee Aun 73e995f494 s/for/about 2023-10-19 20:04:07 +08:00
Lim Chee Aun 1dc0069cdc More descriptive toasts copy 2023-10-19 20:02:31 +08:00
Lim Chee Aun a5532488aa Bunch these avatars too 2023-10-19 17:45:37 +08:00
Lim Chee Aun c9545cdc34 Try focus first, then postMessage 2023-10-19 17:45:27 +08:00
Lim Chee Aun e9075906f8 Fix refresh key not unique enough
JS converted these to numbers, much fail
2023-10-19 17:25:17 +08:00
Lim Chee Aun 3339c5c1d6 Change div to span 2023-10-19 16:07:02 +08:00
Lim Chee Aun 965f948899 Recode some nested modal closing logic
Seems more robust
2023-10-19 16:06:55 +08:00
Lim Chee Aun c0c2bb45fe Auto-close account sheet when location path changes
Test this on account sheet first, probably useful for other sheets too
2023-10-19 10:15:54 +08:00
Lim Chee Aun 106cd16e41 Add loading state to filter bar 2023-10-19 10:13:53 +08:00
Lim Chee Aun 7145c20136 Fix wonky filter bar button transitions 2023-10-19 10:13:26 +08:00
Lim Chee Aun e4b6637680 Ok, hopefully fix messed up tag_name
Seems working but need better tag name
2023-10-19 07:43:54 +08:00
Lim Chee Aun cd57e97e2b Fix Preact wrongly rearrange the elements 2023-10-19 01:14:23 +08:00
Lim Chee Aun c1588322aa Bunch the avatars 2023-10-19 01:13:37 +08:00
Lim Chee Aun 3eda1e2267 Fix familiarFollowers call not working 2023-10-19 01:13:12 +08:00
Lim Chee Aun 3617bdc9cb Try tag_name
Why this action so complicated
2023-10-19 01:12:54 +08:00
Lim Chee Aun 26cf40dcea Break the words 2023-10-17 23:23:58 +08:00
Lim Chee Aun 8ae9131543 Private notes 2023-10-17 20:20:26 +08:00
Lim Chee Aun 1b0a77dfae Pluralization for post(s)
Srsly need a i18n lib soon
2023-10-17 14:56:57 +08:00
Lim Chee Aun e3f58442aa Move release creation to prodtag
There is a limitation of workflow: An action in a workflow run can’t trigger a new workflow run.
https://github.com/orgs/community/discussions/27028#discussioncomment-3254360
2023-10-16 23:23:39 +08:00
Lim Chee Aun 119dae29ca Try move this down 2023-10-16 21:38:14 +08:00
Lim Chee Aun c538cfeaaa Add AbortSignal.timeout polyfill 2023-10-16 21:35:56 +08:00
Lim Chee Aun e153f9f541 Prevent undefined class name lol 2023-10-16 20:21:09 +08:00
Lim Chee Aun 42db913b22 Need permissions 2023-10-16 20:14:15 +08:00
Lim Chee Aun 834b1fe1e1 Test fix push seems to not trigger after tag push but branch push instead
Also allow manual trigger
2023-10-16 20:02:27 +08:00
Lim Chee Aun 809b7cc2d2 Micro perf optimizations maybe 2023-10-16 17:01:16 +08:00
Lim Chee Aun 673001e4e0 Fix captions got squashed 2023-10-16 01:55:11 +08:00
Lim Chee Aun 54e69ed23b Perhaps need to be inside waitUntil block? 2023-10-15 23:50:37 +08:00
Lim Chee Aun 7e1bb08b1b Show contributors image in README 2023-10-15 22:55:41 +08:00
31 changed files with 1024 additions and 349 deletions

View file

@ -1,4 +1,4 @@
name: Auto-create tag on every push to `production`
name: Auto-create tag/release on every push to `production`
on:
push:
@ -8,9 +8,23 @@ on:
jobs:
tag:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
ref: production
- run: git tag "`date +%Y.%m.%d`.`git rev-parse --short HEAD`" $(git rev-parse HEAD)
- run: git push --tags
# - run: git tag "`date +%Y.%m.%d`.`git rev-parse --short HEAD`" $(git rev-parse HEAD)
# - run: git push --tags
- uses: actions/setup-node@v3
with:
node-version: 18
- run: npm ci && npm run build
- run: cd dist && zip -r ../phanpy-dist.zip . && cd ..
- id: tag_name
run: echo ::set-output name=tag_name::$(date +%Y.%m.%d).$(git rev-parse --short HEAD)
- uses: softprops/action-gh-release@v1
with:
tag_name: ${{ steps.tag_name.outputs.tag_name }}
generate_release_notes: true
files: phanpy-dist.zip

View file

@ -1,25 +0,0 @@
name: Create Release on every tag push in `production`
on:
push:
branches:
- production
tags:
- '*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: production
- uses: actions/setup-node@v3
with:
node-version: 18
- run: npm ci && npm run build
- run: cd dist && zip -r ../phanpy-dist.zip . && cd ..
- uses: softprops/action-gh-release@v1
with:
generate_release_notes: true
files: phanpy-dist.zip

View file

@ -138,10 +138,12 @@ Costs involved in running and developing this web app:
[Phanpy](https://bulbapedia.bulbagarden.net/wiki/Phanpy_(Pok%C3%A9mon)) is a Ground-type Pokémon.
## Maintainers
## Maintainers + contributors
- [Chee Aun](https://github.com/cheeaun) ([Mastodon](https://mastodon.social/@cheeaun)) ([Twitter](https://twitter.com/cheeaun))
[![Contributors](https://contrib.rocks/image?repo=cheeaun/phanpy)](https://github.com/cheeaun/phanpy/graphs/contributors)
## Backstory
I am one of the earliest users of Twitter. Twitter was launched on [15 July 2006](https://en.wikipedia.org/wiki/Twitter). I joined on December 2006 and my [first tweet](https://twitter.com/cheeaun/status/1298723) was posted on 18 December 2006.

View file

@ -163,7 +163,6 @@ self.addEventListener('notificationclick', (event) => {
const { access_token, notification_type } = data;
const url = `/#/notifications?id=${tag}&access_token=${btoa(access_token)}`;
event.notification.close();
event.waitUntil(
(async () => {
const clients = await self.clients.matchAll({
@ -180,12 +179,12 @@ self.addEventListener('notificationclick', (event) => {
console.log('NOTIFICATION CLICK navigate', url);
if (bestClient) {
console.log('NOTIFICATION CLICK postMessage', bestClient);
bestClient.focus();
bestClient.postMessage?.({
type: 'notification',
id: tag,
accessToken: access_token,
});
bestClient.focus();
} else {
console.log('NOTIFICATION CLICK openWindow', url);
await self.clients.openWindow(url);
@ -195,6 +194,7 @@ self.addEventListener('notificationclick', (event) => {
console.log('NOTIFICATION CLICK openWindow', url);
await self.clients.openWindow(url);
}
await event.notification.close();
})(),
);
});

View file

@ -676,6 +676,10 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
position: relative;
border-radius: 0;
padding-block: 16px !important;
.avatars-bunch > .avatar:not(:first-child) {
margin-left: -4px;
}
}
.timeline .show-more:hover {
filter: none !important;
@ -1418,6 +1422,13 @@ body:has(.media-modal-container + .status-deck) .media-post-link {
.icon {
flex-shrink: 0;
display: inline-block;
overflow: hidden;
line-height: 0;
svg {
contain: none;
}
}
/* TAG */
@ -1431,6 +1442,7 @@ body:has(.media-modal-container + .status-deck) .media-post-link {
display: inline-block;
margin: 4px;
align-self: center;
text-align: center;
&.clickable {
cursor: pointer;
@ -2108,6 +2120,79 @@ ul.link-list li a .icon {
transparent
);
align-items: center;
transition: opacity 0.3s ease-out;
&.loading,
.loading > & {
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;
@ -2125,14 +2210,19 @@ ul.link-list li a .icon {
text-decoration: none;
white-space: nowrap;
border: 2px solid transparent;
transition: all 0.3s ease-out;
transition: border-color 0.3s ease-out;
display: inline-flex;
align-items: center;
gap: 8px;
}
.filter-bar > a:is(:hover, :focus) {
.filter-bar > a:focus-visible {
border-color: var(--link-light-color);
}
@media (hover: hover) {
.filter-bar > a:hover {
border-color: var(--link-light-color);
}
}
.filter-bar > a > * {
vertical-align: middle;
}
@ -2205,6 +2295,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;

View file

@ -165,6 +165,77 @@
animation: fade-in 0.3s both ease-in-out 0.2s;
}
.private-note-tag {
z-index: 1;
appearance: none;
display: inline-block;
color: var(--private-note-text-color);
background-color: var(--private-note-bg-color);
border: 1px solid var(--private-note-border-color);
padding: 4px;
line-height: normal;
font-size: smaller;
border-radius: 0;
align-self: center !important;
/* clip a dog ear on top right */
clip-path: polygon(0 0, calc(100% - 4px) 0, 100% 4px, 100% 100%, 0 100%);
/* 4x4px square on top right */
background-size: 4px 4px;
background-repeat: no-repeat;
background-position: top right;
background-image: linear-gradient(
to bottom,
var(--private-note-border-color),
var(--private-note-border-color)
);
transition: transform 0.15s ease-in-out;
overflow-wrap: anywhere;
span {
color: inherit;
opacity: 0.75;
text-overflow: ellipsis;
overflow: hidden;
display: -webkit-box;
display: box;
-webkit-box-orient: vertical;
box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
text-align: left;
}
&:hover:not(:active) {
filter: none !important;
transform: rotate(-0.5deg) scale(1.05);
span {
opacity: 1;
}
}
}
.account-container .private-note {
font-size: 90%;
color: var(--text-insignificant-color);
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
padding: 12px;
background-color: var(--bg-faded-color);
display: flex;
gap: 0.5em;
align-items: center;
b {
font-size: 90%;
text-transform: uppercase;
}
p {
margin: 0;
padding: 0;
}
}
.account-container .note {
font-size: 95%;
line-height: 1.4;
@ -228,10 +299,11 @@
align-items: center;
}
.account-container .actions button {
align-self: flex-end;
/* align-self: flex-end; */
}
.account-container .actions .buttons {
display: flex;
align-items: center;
}
.account-container .account-metadata-box {
@ -571,3 +643,30 @@
drop-shadow(8px 0 8px var(--header-color-4, --bg-color));
}
}
#private-note-container {
textarea {
margin-top: 8px;
width: 100%;
resize: vertical;
height: 33vh;
min-height: 25vh;
max-height: 50vh;
color: var(--private-note-text-color);
background-color: var(--private-note-bg-color);
border: 1px solid var(--private-note-border-color);
box-shadow: 0 2px 8px var(--drop-shadow-color);
border-radius: 0;
padding: 16px;
}
footer {
display: flex;
justify-content: space-between;
padding: 8px 0;
* {
vertical-align: middle;
}
}
}

View file

@ -223,9 +223,11 @@ function AccountInfo({
// On first load, fetch familiar followers, merge to top of results' `value`
// Remove dups on every fetch
if (firstLoad) {
const familiarFollowers = await masto.v1.accounts
.familiarFollowers(id)
.fetch();
const familiarFollowers = await masto.v1.accounts.familiarFollowers.fetch(
{
id: [id],
},
);
familiarFollowersCache.current = familiarFollowers[0].accounts;
newValue = [
...familiarFollowersCache.current,
@ -341,8 +343,19 @@ function AccountInfo({
</header>
<main>
<div class="note">
<p> </p>
<p> </p>
<p> </p>
<p> </p>
</div>
<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 class="stats">
<div>
@ -354,7 +367,15 @@ function AccountInfo({
<div>
<span></span> Posts
</div>
<div>Joined </div>
</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>
</>
@ -548,11 +569,13 @@ function AccountInfo({
tabIndex={0}
to={accountLink}
onClick={() => {
states.showAccount = false;
// states.showAccount = false;
setTimeout(() => {
states.showGenericAccounts = {
heading: 'Followers',
fetchAccounts: fetchFollowers,
};
}, 0);
}}
>
{!!familiarFollowers.length && (
@ -579,11 +602,13 @@ function AccountInfo({
tabIndex={0}
to={accountLink}
onClick={() => {
states.showAccount = false;
// states.showAccount = false;
setTimeout(() => {
states.showGenericAccounts = {
heading: 'Following',
fetchAccounts: fetchFollowing,
};
}, 0);
}}
>
<span title={followingCount}>
@ -595,13 +620,13 @@ function AccountInfo({
<LinkOrDiv
class="insignificant"
to={accountLink}
onClick={
standalone
? undefined
: () => {
hideAllModals();
}
}
// onClick={
// standalone
// ? undefined
// : () => {
// hideAllModals();
// }
// }
>
<span title={statusesCount}>
{shortenNumber(statusesCount)}
@ -624,9 +649,9 @@ function AccountInfo({
<LinkOrDiv
to={accountLink}
class="account-metadata-box"
onClick={() => {
states.showAccount = false;
}}
// onClick={() => {
// states.showAccount = false;
// }}
>
<div class="shazam-container">
<div class="shazam-container-inner">
@ -643,7 +668,9 @@ function AccountInfo({
>
<div>
{postingStats.daysSinceLastPost < 365
? `Last ${postingStats.total} posts in the past
? `Last ${postingStats.total} post${
postingStats.total > 1 ? 's' : ''
} in the past
${postingStats.daysSinceLastPost} day${
postingStats.daysSinceLastPost > 1 ? 's' : ''
}`
@ -770,6 +797,7 @@ function RelatedActions({
requested,
domainBlocking,
endorsed,
note: privateNote,
} = relationship || {};
const [currentInfo, setCurrentInfo] = useState(null);
@ -851,6 +879,7 @@ function RelatedActions({
const [showTranslatedBio, setShowTranslatedBio] = useState(false);
const [showAddRemoveLists, setShowAddRemoveLists] = useState(false);
const [showPrivateNoteModal, setShowPrivateNoteModal] = useState(false);
return (
<>
@ -861,9 +890,11 @@ function RelatedActions({
) : !!lastStatusAt ? (
<small class="insignificant">
Last post:{' '}
<span class="ib">
{niceDateTime(lastStatusAt, {
hideTime: true,
})}
</span>
</small>
) : (
<span />
@ -872,6 +903,19 @@ function RelatedActions({
{blocking && <span class="tag danger">Blocked</span>}
</span>{' '}
<span class="buttons">
{!!privateNote && (
<button
type="button"
class="private-note-tag"
title="Private note"
onClick={() => {
setShowPrivateNoteModal(true);
}}
dir="auto"
>
<span>{privateNote}</span>
</button>
)}
<Menu
instanceRef={menuInstanceRef}
portal={{
@ -925,6 +969,16 @@ function RelatedActions({
<Icon icon="translate" />
<span>Translate bio</span>
</MenuItem>
<MenuItem
onClick={() => {
setShowPrivateNoteModal(true);
}}
>
<Icon icon="pencil" />
<span>
{privateNote ? 'Edit private note' : 'Add private note'}
</span>
</MenuItem>
{/* Add/remove from lists is only possible if following the account */}
{following && (
<MenuItem
@ -1235,6 +1289,24 @@ function RelatedActions({
/>
</Modal>
)}
{!!showPrivateNoteModal && (
<Modal
class="light"
onClose={() => {
setShowPrivateNoteModal(false);
}}
>
<PrivateNoteSheet
account={info}
note={privateNote}
onRelationshipChange={(relationship) => {
setRelationship(relationship);
// onRelationshipChange({ relationship, currentID: accountID.current });
}}
onClose={() => setShowPrivateNoteModal(false)}
/>
</Modal>
)}
</>
);
}
@ -1432,4 +1504,95 @@ function AddRemoveListsSheet({ accountID, onClose }) {
);
}
function PrivateNoteSheet({
account,
note: initialNote,
onRelationshipChange = () => {},
onClose = () => {},
}) {
const { masto } = api();
const [uiState, setUIState] = useState('default');
const textareaRef = useRef(null);
useEffect(() => {
let timer;
if (textareaRef.current && !initialNote) {
timer = setTimeout(() => {
textareaRef.current.focus?.();
}, 100);
}
return () => {
clearTimeout(timer);
};
}, []);
return (
<div class="sheet" id="private-note-container">
{!!onClose && (
<button type="button" class="sheet-close" onClick={onClose}>
<Icon icon="x" />
</button>
)}
<header>
<b>Private note about @{account?.username || account?.acct}</b>
</header>
<main>
<form
onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.target);
const note = formData.get('note');
if (note?.trim() !== initialNote?.trim()) {
setUIState('loading');
(async () => {
try {
const newRelationship = await masto.v1.accounts
.$select(account?.id)
.note.create({
comment: note,
});
console.log('updated relationship', newRelationship);
setUIState('default');
onRelationshipChange(newRelationship);
onClose();
} catch (e) {
console.error(e);
setUIState('error');
alert(e?.message || 'Unable to update private note.');
}
})();
}
}}
>
<textarea
ref={textareaRef}
name="note"
disabled={uiState === 'loading'}
>
{initialNote}
</textarea>
<footer>
<button
type="button"
class="light"
disabled={uiState === 'loading'}
onClick={() => {
onClose?.();
}}
>
Cancel
</button>
<span>
<Loader abrupt hidden={uiState !== 'loading'} />
<button disabled={uiState === 'loading'} type="submit">
Save &amp; close
</button>
</span>
</footer>
</form>
</main>
</div>
);
}
export default AccountInfo;

View file

@ -2,6 +2,7 @@ import { useEffect } from 'preact/hooks';
import { api } from '../utils/api';
import states from '../utils/states';
import useLocationChange from '../utils/useLocationChange';
import AccountInfo from './account-info';
import Icon from './icon';
@ -16,17 +17,19 @@ function AccountSheet({ account, instance: propInstance, onClose }) {
}
}, [account]);
useLocationChange(onClose);
return (
<div
class="sheet"
onClick={(e) => {
const accountBlock = e.target.closest('.account-block');
if (accountBlock) {
onClose({
destination: 'account-statuses',
});
}
}}
// onClick={(e) => {
// const accountBlock = e.target.closest('.account-block');
// if (accountBlock) {
// onClose({
// destination: 'account-statuses',
// });
// }
// }}
>
{!!onClose && (
<button type="button" class="sheet-close outer" onClick={onClose}>

View file

@ -21,6 +21,7 @@
height: 100%;
object-fit: cover;
background-color: var(--img-bg-color);
contain: none;
}
.avatar[data-loaded],

View file

@ -5,6 +5,7 @@ import { InView } from 'react-intersection-observer';
import { useSnapshot } from 'valtio';
import states from '../utils/states';
import useLocationChange from '../utils/useLocationChange';
import AccountBlock from './account-block';
import Icon from './icon';
@ -16,6 +17,8 @@ export default function GenericAccounts({ onClose = () => {} }) {
const [accounts, setAccounts] = useState([]);
const [showMore, setShowMore] = useState(false);
useLocationChange(onClose);
if (!snapStates.showGenericAccounts) {
return null;
}

View file

@ -1,3 +1,4 @@
import { memo } from 'preact/compat';
import { useEffect, useState } from 'preact/hooks';
const SIZES = {
@ -101,6 +102,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({
@ -133,9 +135,6 @@ function Icon({
style={{
width: `${iconSize}px`,
height: `${iconSize}px`,
display: 'inline-block',
overflow: 'hidden',
lineHeight: 0,
...style,
}}
>
@ -156,4 +155,4 @@ function Icon({
);
}
export default Icon;
export default memo(Icon);

View file

@ -2,14 +2,14 @@ import './loader.css';
function Loader({ abrupt, hidden, ...props }) {
return (
<div
<span
{...props}
class={`loader-container ${abrupt ? 'abrupt' : ''} ${
hidden ? 'hidden' : ''
}`}
>
<div class="loader" />
</div>
<span class="loader" />
</span>
);
}

View file

@ -130,6 +130,7 @@ function Media({
enabled: pinchZoomEnabled,
draggableUnZoomed: false,
inertiaFriction: 0.9,
doubleTapZoomOutOnMaxScale: true,
containerProps: {
className: 'media-zoom',
style: {

View file

@ -117,9 +117,10 @@ export default function Modals() {
instance={snapStates.showAccount?.instance}
onClose={({ destination } = {}) => {
states.showAccount = false;
if (destination) {
states.showAccounts = false;
}
// states.showGenericAccounts = false;
// if (destination) {
// states.showAccounts = false;
// }
}}
/>
</Modal>

View file

@ -1,3 +1,9 @@
.nav-menu section:last-child {
background-color: var(--bg-faded-color);
margin-bottom: -8px;
padding-bottom: 8px;
}
@media (min-width: 23em) {
.nav-menu {
display: grid;
@ -8,6 +14,7 @@
'left right';
padding: 0;
width: 22em;
max-width: calc(100vw - 16px);
}
.nav-menu .top-menu {
grid-area: top;
@ -27,7 +34,6 @@
}
}
.nav-menu section:last-child {
background-color: var(--bg-faded-color);
background-image: linear-gradient(
to right,
var(--divider-color) 1px,
@ -45,6 +51,15 @@
animation: phanpying 0.2s ease-in-out both;
border-top-right-radius: inherit;
border-bottom-right-radius: inherit;
margin-bottom: 0;
display: flex;
flex-direction: column;
.divider-grow {
flex-grow: 1;
height: auto;
background-color: transparent;
}
}
.nav-menu section:last-child > .szh-menu__divider:first-child {
display: none;

View file

@ -254,6 +254,7 @@ function NavMenu(props) {
<Icon icon="block" size="l" />
Blocked users&hellip;
</MenuItem>
<MenuDivider className="divider-grow" />
<MenuItem
onClick={() => {
states.showKeyboardShortcutsHelp = true;
@ -268,7 +269,7 @@ function NavMenu(props) {
}}
>
<Icon icon="shortcut" size="l" />{' '}
<span>Shortcuts Settings&hellip;</span>
<span>Shortcuts / Columns&hellip;</span>
</MenuItem>
<MenuItem
onClick={() => {

View file

@ -249,8 +249,7 @@ function ShortcutsSettings({ onClose }) {
</h2>
</header>
<main>
<p>
Specify a list of shortcuts that'll appear&nbsp;as:
<p>Specify a list of shortcuts that'll appear&nbsp;as:</p>
<div class="shortcuts-view-mode">
{[
{
@ -298,7 +297,6 @@ function ShortcutsSettings({ onClose }) {
<option value="multi-column">Multi-column</option>
<option value="tab-menu-bar">Tab/Menu bar </option>
</select> */}
</p>
{/* <p>
<details>
<summary class="insignificant">

View file

@ -1015,8 +1015,6 @@ body:has(#modal-container .carousel) .status .media img:hover {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: inline-flex;
gap: 4px;
&:hover {
color: var(--text-color);
@ -1027,6 +1025,8 @@ body:has(#modal-container .carousel) .status .media img:hover {
white-space: pre-line;
overflow: auto;
text-overflow: unset;
display: flex;
gap: 4px;
}
}

View file

@ -579,7 +579,11 @@ function Status({
try {
const done = await confirmBoostStatus();
if (!isSizeLarge && done) {
showToast(reblogged ? 'Unboosted' : 'Boosted');
showToast(
reblogged
? `Unboosted @${username || acct}'s post`
: `Boosted @${username || acct}'s post`,
);
}
} catch (e) {}
}}
@ -597,7 +601,11 @@ function Status({
try {
favouriteStatus();
if (!isSizeLarge) {
showToast(favourited ? 'Unfavourited' : 'Favourited');
showToast(
favourited
? `Unfavourited @${username || acct}'s post`
: `Favourited @${username || acct}'s post`,
);
}
} catch (e) {}
}}
@ -621,7 +629,11 @@ function Status({
try {
bookmarkStatus();
if (!isSizeLarge) {
showToast(bookmarked ? 'Unbookmarked' : 'Bookmarked');
showToast(
bookmarked
? `Unbookmarked @${username || acct}'s post`
: `Bookmarked @${username || acct}'s post`,
);
}
} catch (e) {}
}}
@ -829,7 +841,11 @@ function Status({
try {
favouriteStatus();
if (!isSizeLarge) {
showToast(favourited ? 'Unfavourited' : 'Favourited');
showToast(
favourited
? `Unfavourited @${username || acct}'s post`
: `Favourited @${username || acct}'s post`,
);
}
} catch (e) {}
},
@ -843,7 +859,11 @@ function Status({
try {
bookmarkStatus();
if (!isSizeLarge) {
showToast(bookmarked ? 'Unbookmarked' : 'Bookmarked');
showToast(
bookmarked
? `Unbookmarked @${username || acct}'s post`
: `Bookmarked @${username || acct}'s post`,
);
}
} catch (e) {}
},
@ -858,7 +878,11 @@ function Status({
try {
const done = await confirmBoostStatus();
if (!isSizeLarge && done) {
showToast(reblogged ? 'Unboosted' : 'Boosted');
showToast(
reblogged
? `Unboosted @${username || acct}'s post`
: `Boosted @${username || acct}'s post`,
);
}
} catch (e) {}
})();

View file

@ -334,7 +334,13 @@ function Timeline({
</button>
)}
</header>
{!!timelineStart && <div class="timeline-start">{timelineStart}</div>}
{!!timelineStart && (
<div
class={`timeline-start ${uiState === 'loading' ? 'loading' : ''}`}
>
{timelineStart}
</div>
)}
{!!items.length ? (
<>
<ul class="timeline">

View file

@ -62,6 +62,9 @@
--close-button-bg-active-color: rgba(0, 0, 0, 0.2);
--close-button-color: rgba(0, 0, 0, 0.5);
--close-button-hover-color: rgba(0, 0, 0, 1);
--private-note-text-color: var(--text-color);
--private-note-bg-color: color-mix(in srgb, yellow 20%, var(--bg-color));
--private-note-border-color: rgba(0, 0, 0, 0.2);
/* Video colors won't change based on color scheme */
--media-fg-color: #f0f2f5;
@ -111,6 +114,7 @@
--close-button-bg-active-color: rgba(255, 255, 255, 0.15);
--close-button-color: rgba(255, 255, 255, 0.5);
--close-button-hover-color: rgba(255, 255, 255, 1);
--private-note-border-color: rgba(255, 255, 255, 0.2);
}
}

View file

@ -11,6 +11,19 @@ if (import.meta.env.DEV) {
import('preact/debug');
}
// AbortSignal.timeout polyfill
// Temporary fix from https://github.com/mo/abortcontroller-polyfill/issues/73#issuecomment-1541180943
// Incorrect implementation, but should be good enough for now
if ('AbortSignal' in window) {
AbortSignal.timeout =
AbortSignal.timeout ||
((duration) => {
const controller = new AbortController();
setTimeout(() => controller.abort(), duration);
return controller.signal;
});
}
render(
<HashRouter>
<App />

View file

@ -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
@ -128,6 +243,7 @@ function AccountStatuses() {
to={`/${instance}/a/${id}`}
class="insignificant filter-clear"
title="Clear filters"
key="clear-filters"
>
<Icon icon="x" size="l" />
</Link>
@ -169,6 +285,7 @@ function AccountStatuses() {
</Link>
{featuredTags.map((tag) => (
<Link
key={tag.id}
to={`/${instance}/a/${id}${
tagged === tag.name
? ''
@ -191,6 +308,48 @@ 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 } = e.currentTarget;
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 } = e;
setSearchParams(
value
? {
month: value,
}
: {},
);
}}
/>
))}
</div>
</>
);
@ -198,11 +357,9 @@ function AccountStatuses() {
id,
instance,
authenticated,
excludeReplies,
excludeBoosts,
featuredTags,
tagged,
media,
searchEnabled,
...allSearchParams,
]);
useEffect(() => {
@ -217,7 +374,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;
@ -257,7 +414,13 @@ function AccountStatuses() {
useItemID
boostsCarousel={snapStates.settings.boostsCarousel}
timelineStart={TimelineStart}
refresh={excludeReplies + excludeBoosts + tagged + media}
refresh={[
excludeReplies,
excludeBoosts,
tagged,
media,
month + account?.acct,
].toString()}
headerEnd={
<Menu2
portal
@ -302,4 +465,68 @@ 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();
return (
<div class={className}>
<Icon icon="month" size="l" />
<select
ref={monthFieldRef}
disabled={disabled}
value={_month || ''}
onInput={(e) => {
const { value } = e.currentTarget;
onInput({
value: value ? `${yearFieldRef.current.value}-${value}` : '',
});
}}
>
<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 } = e.currentTarget;
onInput({
value: value ? `${value}-${monthFieldRef.current.value}` : '',
});
}}
style={{
width: '4.5em',
}}
/>
</div>
);
}
export default AccountStatuses;

View file

@ -63,7 +63,7 @@ function NotificationsLink() {
to="/notifications"
class={`button plain notifications-button ${
snapStates.notificationsShowNew ? 'has-badge' : ''
} ${menuState}`}
} ${menuState || ''}`}
onClick={(e) => {
e.stopPropagation();
if (window.matchMedia('(min-width: calc(40em))').matches) {

View file

@ -23,6 +23,7 @@
padding: 0;
height: 40em;
overflow: auto;
overscroll-behavior: contain;
}
.notifications-menu .status {
font-size: inherit;

View file

@ -40,9 +40,15 @@ ul.link-list.hashtag-list li a {
}
@media (min-width: 40em) {
#search-page header input {
#search-page {
header input {
background-color: var(--bg-color);
}
.filter-bar {
margin-top: 8px;
}
}
}
.search-popover-container {

View file

@ -141,7 +141,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 />
@ -152,7 +152,7 @@ function Search(props) {
</header>
<main>
{!!q && (
<div class="filter-bar">
<div class={`filter-bar ${uiState === 'loading' ? 'loading' : ''}`}>
{!!type && (
<Link to={`/search${q ? `?q=${encodeURIComponent(q)}` : ''}`}>
All
@ -181,7 +181,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>
)}

View file

@ -30,6 +30,10 @@
.ancestors-indicator {
font-size: 70% !important;
& > .avatar ~ .avatar {
margin-left: -4px;
}
}
.ancestors-indicator:not([hidden]) {
animation: slide-up 0.3s both ease-out 0.3s;

View file

@ -650,6 +650,189 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
resetScrollPosition(status.id);
}, []);
const renderStatus = (status) => {
const {
id: statusID,
ancestor,
isThread,
descendant,
thread,
replies,
repliesCount,
weight,
} = status;
const isHero = statusID === id;
// const StatusParent = useCallback(
// (props) =>
// isThread || thread || ancestor ? (
// <Link
// class="status-link"
// to={
// instance ? `/${instance}/s/${statusID}` : `/s/${statusID}`
// }
// onClick={() => {
// resetScrollPosition(statusID);
// }}
// {...props}
// />
// ) : (
// <div class="status-focus" tabIndex={0} {...props} />
// ),
// [isThread, thread],
// );
return (
<li
key={statusID}
ref={isHero ? heroStatusRef : null}
class={`${ancestor ? 'ancestor' : ''} ${
descendant ? 'descendant' : ''
} ${thread ? 'thread' : ''} ${isHero ? 'hero' : ''}`}
>
{isHero ? (
<>
<InView
threshold={0.1}
onChange={onView}
class="status-focus"
tabIndex={0}
>
<Status
statusID={statusID}
instance={instance}
withinContext
size="l"
enableTranslate
forceTranslate={translate}
/>
</InView>
{uiState !== 'loading' && !authenticated ? (
<div class="post-status-banner">
<p>
You're not logged in. Interactions (reply, boost, etc) are not
possible.
</p>
<Link to="/login" class="button">
Log in
</Link>
</div>
) : (
!sameInstance && (
<div class="post-status-banner">
<p>
This post is from another instance (<b>{instance}</b>).
Interactions (reply, boost, etc) are not possible.
</p>
<button
type="button"
disabled={uiState === 'loading'}
onClick={() => {
setUIState('loading');
(async () => {
try {
const results = await currentMasto.v2.search.fetch({
q: heroStatus.url,
type: 'statuses',
resolve: true,
limit: 1,
});
if (results.statuses.length) {
const status = results.statuses[0];
location.hash = currentInstance
? `/${currentInstance}/s/${status.id}`
: `/s/${status.id}`;
} else {
throw new Error('No results');
}
} catch (e) {
setUIState('default');
alert('Error: ' + e);
console.error(e);
}
})();
}}
>
<Icon icon="transfer" /> Switch to my instance to enable
interactions
</button>
</div>
)
)}
</>
) : (
// <StatusParent>
<Link
class="status-link"
to={instance ? `/${instance}/s/${statusID}` : `/s/${statusID}`}
onClick={() => {
resetScrollPosition(statusID);
}}
>
<Status
statusID={statusID}
instance={instance}
withinContext
size={thread || ancestor ? 'm' : 's'}
enableTranslate
onMediaClick={handleMediaClick}
onStatusLinkClick={handleStatusLinkClick}
/>
{ancestor && isThread && repliesCount > 1 && (
<div class="replies-link">
<Icon icon="comment" />{' '}
<span title={repliesCount}>{shortenNumber(repliesCount)}</span>
</div>
)}{' '}
{/* {replies?.length > LIMIT && (
<div class="replies-link">
<Icon icon="comment" />{' '}
<span title={replies.length}>
{shortenNumber(replies.length)}
</span>
</div>
)} */}
{/* </StatusParent> */}
</Link>
)}
{descendant && replies?.length > 0 && (
<SubComments
instance={instance}
replies={replies}
hasParentThread={thread}
level={1}
accWeight={weight}
openAll={totalDescendants.current < SUBCOMMENTS_OPEN_ALL_LIMIT}
/>
)}
{uiState === 'loading' &&
isHero &&
!!heroStatus?.repliesCount &&
!hasDescendants && (
<div class="status-loading">
<Loader />
</div>
)}
{uiState === 'error' &&
isHero &&
!!heroStatus?.repliesCount &&
!hasDescendants && (
<div class="status-error">
Unable to load replies.
<br />
<button
type="button"
class="plain"
onClick={() => {
states.reloadStatusPage++;
}}
>
Try again
</button>
</div>
)}
</li>
);
};
return (
<div
tabIndex="-1"
@ -869,196 +1052,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
uiState === 'loading' ? 'loading' : ''
}`}
>
{statuses.slice(0, limit).map((status) => {
const {
id: statusID,
ancestor,
isThread,
descendant,
thread,
replies,
repliesCount,
weight,
} = status;
const isHero = statusID === id;
// const StatusParent = useCallback(
// (props) =>
// isThread || thread || ancestor ? (
// <Link
// class="status-link"
// to={
// instance ? `/${instance}/s/${statusID}` : `/s/${statusID}`
// }
// onClick={() => {
// resetScrollPosition(statusID);
// }}
// {...props}
// />
// ) : (
// <div class="status-focus" tabIndex={0} {...props} />
// ),
// [isThread, thread],
// );
return (
<li
key={statusID}
ref={isHero ? heroStatusRef : null}
class={`${ancestor ? 'ancestor' : ''} ${
descendant ? 'descendant' : ''
} ${thread ? 'thread' : ''} ${isHero ? 'hero' : ''}`}
>
{isHero ? (
<>
<InView
threshold={0.1}
onChange={onView}
class="status-focus"
tabIndex={0}
>
<Status
statusID={statusID}
instance={instance}
withinContext
size="l"
enableTranslate
forceTranslate={translate}
/>
</InView>
{uiState !== 'loading' && !authenticated ? (
<div class="post-status-banner">
<p>
You're not logged in. Interactions (reply, boost, etc)
are not possible.
</p>
<Link to="/login" class="button">
Log in
</Link>
</div>
) : (
!sameInstance && (
<div class="post-status-banner">
<p>
This post is from another instance (
<b>{instance}</b>). Interactions (reply, boost, etc)
are not possible.
</p>
<button
type="button"
disabled={uiState === 'loading'}
onClick={() => {
setUIState('loading');
(async () => {
try {
const results =
await currentMasto.v2.search.fetch({
q: heroStatus.url,
type: 'statuses',
resolve: true,
limit: 1,
});
if (results.statuses.length) {
const status = results.statuses[0];
location.hash = currentInstance
? `/${currentInstance}/s/${status.id}`
: `/s/${status.id}`;
} else {
throw new Error('No results');
}
} catch (e) {
setUIState('default');
alert('Error: ' + e);
console.error(e);
}
})();
}}
>
<Icon icon="transfer" /> Switch to my instance to
enable interactions
</button>
</div>
)
)}
</>
) : (
// <StatusParent>
<Link
class="status-link"
to={
instance ? `/${instance}/s/${statusID}` : `/s/${statusID}`
}
onClick={() => {
resetScrollPosition(statusID);
}}
>
<Status
statusID={statusID}
instance={instance}
withinContext
size={thread || ancestor ? 'm' : 's'}
enableTranslate
onMediaClick={handleMediaClick}
onStatusLinkClick={handleStatusLinkClick}
/>
{ancestor && isThread && repliesCount > 1 && (
<div class="replies-link">
<Icon icon="comment" />{' '}
<span title={repliesCount}>
{shortenNumber(repliesCount)}
</span>
</div>
)}{' '}
{/* {replies?.length > LIMIT && (
<div class="replies-link">
<Icon icon="comment" />{' '}
<span title={replies.length}>
{shortenNumber(replies.length)}
</span>
</div>
)} */}
{/* </StatusParent> */}
</Link>
)}
{descendant && replies?.length > 0 && (
<SubComments
instance={instance}
replies={replies}
hasParentThread={thread}
level={1}
accWeight={weight}
openAll={
totalDescendants.current < SUBCOMMENTS_OPEN_ALL_LIMIT
}
/>
)}
{uiState === 'loading' &&
isHero &&
!!heroStatus?.repliesCount &&
!hasDescendants && (
<div class="status-loading">
<Loader />
</div>
)}
{uiState === 'error' &&
isHero &&
!!heroStatus?.repliesCount &&
!hasDescendants && (
<div class="status-error">
Unable to load replies.
<br />
<button
type="button"
class="plain"
onClick={() => {
states.reloadStatusPage++;
}}
>
Try again
</button>
</div>
)}
</li>
);
})}
{statuses.slice(0, limit).map(renderStatus)}
{showMore > 0 && (
<li>
<button
@ -1068,7 +1062,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
onClick={() => setLimit((l) => l + LIMIT)}
style={{ marginBlockEnd: '6em' }}
>
<div class="ib">
<div class="ib avatars-bunch">
{/* show avatars for first 5 statuses */}
{statuses.slice(limit, limit + 5).map((status) => (
<Avatar

View file

@ -1,3 +1,5 @@
import mem from './mem';
const div = document.createElement('div');
function getHTMLText(html) {
if (!html) return '';
@ -10,4 +12,4 @@ function getHTMLText(html) {
return div.innerText.replace(/[\r\n]{3,}/g, '\n\n').trim();
}
export default getHTMLText;
export default mem(getHTMLText);

View file

@ -0,0 +1,23 @@
import { useEffect, useRef } from 'preact/hooks';
import { useLocation } from 'react-router-dom';
// Hook that runs a callback when the location changes
// Won't run on the first render
export default function useLocationChange(fn) {
if (!fn) return;
const location = useLocation();
const currentLocationRef = useRef(location.pathname);
useEffect(() => {
// console.log('location', {
// current: currentLocationRef.current,
// next: location.pathname,
// });
if (
currentLocationRef.current &&
location.pathname !== currentLocationRef.current
) {
fn?.();
}
}, [location.pathname, fn]);
}