Compare commits
49 commits
96f1a9d9f2
...
2b7969f9ec
Author | SHA1 | Date | |
---|---|---|---|
Natsu Kagami | 2b7969f9ec | ||
4c4e89ac9d | |||
4da968df2e | |||
c6f368ac0b | |||
87e243ea58 | |||
66f9c3b918 | |||
137ad7f4dd | |||
8ddc44fba6 | |||
3721acf3d3 | |||
ab7df0f66c | |||
d1aedcaef2 | |||
691aea3389 | |||
72f204771f | |||
dba921a3fd | |||
4646859177 | |||
66fa6fbe52 | |||
861619ce57 | |||
71bf8608e6 | |||
2916d1146b | |||
d62712d587 | |||
a37c3d6081 | |||
73e995f494 | |||
1dc0069cdc | |||
a5532488aa | |||
c9545cdc34 | |||
e9075906f8 | |||
3339c5c1d6 | |||
965f948899 | |||
c0c2bb45fe | |||
106cd16e41 | |||
7145c20136 | |||
e4b6637680 | |||
cd57e97e2b | |||
c1588322aa | |||
3eda1e2267 | |||
3617bdc9cb | |||
26cf40dcea | |||
8ae9131543 | |||
1b0a77dfae | |||
e3f58442aa | |||
119dae29ca | |||
c538cfeaaa | |||
e153f9f541 | |||
42db913b22 | |||
834b1fe1e1 | |||
809b7cc2d2 | |||
673001e4e0 | |||
54e69ed23b | |||
7e1bb08b1b |
20
.github/workflows/prodtag.yml
vendored
20
.github/workflows/prodtag.yml
vendored
|
@ -1,4 +1,4 @@
|
||||||
name: Auto-create tag on every push to `production`
|
name: Auto-create tag/release on every push to `production`
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
@ -8,9 +8,23 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
tag:
|
tag:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: production
|
ref: production
|
||||||
- run: git tag "`date +%Y.%m.%d`.`git rev-parse --short HEAD`" $(git rev-parse HEAD)
|
# - run: git tag "`date +%Y.%m.%d`.`git rev-parse --short HEAD`" $(git rev-parse HEAD)
|
||||||
- run: git push --tags
|
# - 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
|
||||||
|
|
25
.github/workflows/tagrelease.yml
vendored
25
.github/workflows/tagrelease.yml
vendored
|
@ -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
|
|
|
@ -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.
|
[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))
|
- [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
|
## 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.
|
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.
|
||||||
|
|
|
@ -163,7 +163,6 @@ self.addEventListener('notificationclick', (event) => {
|
||||||
const { access_token, notification_type } = data;
|
const { access_token, notification_type } = data;
|
||||||
const url = `/#/notifications?id=${tag}&access_token=${btoa(access_token)}`;
|
const url = `/#/notifications?id=${tag}&access_token=${btoa(access_token)}`;
|
||||||
|
|
||||||
event.notification.close();
|
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
(async () => {
|
(async () => {
|
||||||
const clients = await self.clients.matchAll({
|
const clients = await self.clients.matchAll({
|
||||||
|
@ -180,12 +179,12 @@ self.addEventListener('notificationclick', (event) => {
|
||||||
console.log('NOTIFICATION CLICK navigate', url);
|
console.log('NOTIFICATION CLICK navigate', url);
|
||||||
if (bestClient) {
|
if (bestClient) {
|
||||||
console.log('NOTIFICATION CLICK postMessage', bestClient);
|
console.log('NOTIFICATION CLICK postMessage', bestClient);
|
||||||
|
bestClient.focus();
|
||||||
bestClient.postMessage?.({
|
bestClient.postMessage?.({
|
||||||
type: 'notification',
|
type: 'notification',
|
||||||
id: tag,
|
id: tag,
|
||||||
accessToken: access_token,
|
accessToken: access_token,
|
||||||
});
|
});
|
||||||
bestClient.focus();
|
|
||||||
} else {
|
} else {
|
||||||
console.log('NOTIFICATION CLICK openWindow', url);
|
console.log('NOTIFICATION CLICK openWindow', url);
|
||||||
await self.clients.openWindow(url);
|
await self.clients.openWindow(url);
|
||||||
|
@ -195,6 +194,7 @@ self.addEventListener('notificationclick', (event) => {
|
||||||
console.log('NOTIFICATION CLICK openWindow', url);
|
console.log('NOTIFICATION CLICK openWindow', url);
|
||||||
await self.clients.openWindow(url);
|
await self.clients.openWindow(url);
|
||||||
}
|
}
|
||||||
|
await event.notification.close();
|
||||||
})(),
|
})(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
98
src/app.css
98
src/app.css
|
@ -676,6 +676,10 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
padding-block: 16px !important;
|
padding-block: 16px !important;
|
||||||
|
|
||||||
|
.avatars-bunch > .avatar:not(:first-child) {
|
||||||
|
margin-left: -4px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.timeline .show-more:hover {
|
.timeline .show-more:hover {
|
||||||
filter: none !important;
|
filter: none !important;
|
||||||
|
@ -1418,6 +1422,13 @@ body:has(.media-modal-container + .status-deck) .media-post-link {
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
display: inline-block;
|
||||||
|
overflow: hidden;
|
||||||
|
line-height: 0;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
contain: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TAG */
|
/* TAG */
|
||||||
|
@ -1431,6 +1442,7 @@ body:has(.media-modal-container + .status-deck) .media-post-link {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
&.clickable {
|
&.clickable {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -2108,6 +2120,79 @@ ul.link-list li a .icon {
|
||||||
transparent
|
transparent
|
||||||
);
|
);
|
||||||
align-items: center;
|
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 {
|
.filter-bar.centered {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -2125,14 +2210,19 @@ ul.link-list li a .icon {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
border: 2px solid transparent;
|
border: 2px solid transparent;
|
||||||
transition: all 0.3s ease-out;
|
transition: border-color 0.3s ease-out;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
.filter-bar > a:is(:hover, :focus) {
|
.filter-bar > a:focus-visible {
|
||||||
border-color: var(--link-light-color);
|
border-color: var(--link-light-color);
|
||||||
}
|
}
|
||||||
|
@media (hover: hover) {
|
||||||
|
.filter-bar > a:hover {
|
||||||
|
border-color: var(--link-light-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
.filter-bar > a > * {
|
.filter-bar > a > * {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
@ -2205,6 +2295,10 @@ ul.link-list li a .icon {
|
||||||
}
|
}
|
||||||
.deck > header {
|
.deck > header {
|
||||||
text-shadow: 0 1px var(--bg-color);
|
text-shadow: 0 1px var(--bg-color);
|
||||||
|
|
||||||
|
form {
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.deck > header h1 {
|
.deck > header h1 {
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
|
|
|
@ -165,6 +165,77 @@
|
||||||
animation: fade-in 0.3s both ease-in-out 0.2s;
|
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 {
|
.account-container .note {
|
||||||
font-size: 95%;
|
font-size: 95%;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
|
@ -228,10 +299,11 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.account-container .actions button {
|
.account-container .actions button {
|
||||||
align-self: flex-end;
|
/* align-self: flex-end; */
|
||||||
}
|
}
|
||||||
.account-container .actions .buttons {
|
.account-container .actions .buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-container .account-metadata-box {
|
.account-container .account-metadata-box {
|
||||||
|
@ -571,3 +643,30 @@
|
||||||
drop-shadow(8px 0 8px var(--header-color-4, --bg-color));
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -223,9 +223,11 @@ function AccountInfo({
|
||||||
// On first load, fetch familiar followers, merge to top of results' `value`
|
// On first load, fetch familiar followers, merge to top of results' `value`
|
||||||
// Remove dups on every fetch
|
// Remove dups on every fetch
|
||||||
if (firstLoad) {
|
if (firstLoad) {
|
||||||
const familiarFollowers = await masto.v1.accounts
|
const familiarFollowers = await masto.v1.accounts.familiarFollowers.fetch(
|
||||||
.familiarFollowers(id)
|
{
|
||||||
.fetch();
|
id: [id],
|
||||||
|
},
|
||||||
|
);
|
||||||
familiarFollowersCache.current = familiarFollowers[0].accounts;
|
familiarFollowersCache.current = familiarFollowers[0].accounts;
|
||||||
newValue = [
|
newValue = [
|
||||||
...familiarFollowersCache.current,
|
...familiarFollowersCache.current,
|
||||||
|
@ -342,7 +344,18 @@ function AccountInfo({
|
||||||
<main>
|
<main>
|
||||||
<div class="note">
|
<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>
|
||||||
<div class="stats">
|
<div class="stats">
|
||||||
<div>
|
<div>
|
||||||
|
@ -354,7 +367,15 @@ function AccountInfo({
|
||||||
<div>
|
<div>
|
||||||
<span>██</span> Posts
|
<span>██</span> Posts
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
|
@ -548,11 +569,13 @@ function AccountInfo({
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
to={accountLink}
|
to={accountLink}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
states.showAccount = false;
|
// states.showAccount = false;
|
||||||
|
setTimeout(() => {
|
||||||
states.showGenericAccounts = {
|
states.showGenericAccounts = {
|
||||||
heading: 'Followers',
|
heading: 'Followers',
|
||||||
fetchAccounts: fetchFollowers,
|
fetchAccounts: fetchFollowers,
|
||||||
};
|
};
|
||||||
|
}, 0);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!!familiarFollowers.length && (
|
{!!familiarFollowers.length && (
|
||||||
|
@ -579,11 +602,13 @@ function AccountInfo({
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
to={accountLink}
|
to={accountLink}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
states.showAccount = false;
|
// states.showAccount = false;
|
||||||
|
setTimeout(() => {
|
||||||
states.showGenericAccounts = {
|
states.showGenericAccounts = {
|
||||||
heading: 'Following',
|
heading: 'Following',
|
||||||
fetchAccounts: fetchFollowing,
|
fetchAccounts: fetchFollowing,
|
||||||
};
|
};
|
||||||
|
}, 0);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span title={followingCount}>
|
<span title={followingCount}>
|
||||||
|
@ -595,13 +620,13 @@ function AccountInfo({
|
||||||
<LinkOrDiv
|
<LinkOrDiv
|
||||||
class="insignificant"
|
class="insignificant"
|
||||||
to={accountLink}
|
to={accountLink}
|
||||||
onClick={
|
// onClick={
|
||||||
standalone
|
// standalone
|
||||||
? undefined
|
// ? undefined
|
||||||
: () => {
|
// : () => {
|
||||||
hideAllModals();
|
// hideAllModals();
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
>
|
>
|
||||||
<span title={statusesCount}>
|
<span title={statusesCount}>
|
||||||
{shortenNumber(statusesCount)}
|
{shortenNumber(statusesCount)}
|
||||||
|
@ -624,9 +649,9 @@ function AccountInfo({
|
||||||
<LinkOrDiv
|
<LinkOrDiv
|
||||||
to={accountLink}
|
to={accountLink}
|
||||||
class="account-metadata-box"
|
class="account-metadata-box"
|
||||||
onClick={() => {
|
// onClick={() => {
|
||||||
states.showAccount = false;
|
// states.showAccount = false;
|
||||||
}}
|
// }}
|
||||||
>
|
>
|
||||||
<div class="shazam-container">
|
<div class="shazam-container">
|
||||||
<div class="shazam-container-inner">
|
<div class="shazam-container-inner">
|
||||||
|
@ -643,7 +668,9 @@ function AccountInfo({
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{postingStats.daysSinceLastPost < 365
|
{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} day${
|
||||||
postingStats.daysSinceLastPost > 1 ? 's' : ''
|
postingStats.daysSinceLastPost > 1 ? 's' : ''
|
||||||
}`
|
}`
|
||||||
|
@ -770,6 +797,7 @@ function RelatedActions({
|
||||||
requested,
|
requested,
|
||||||
domainBlocking,
|
domainBlocking,
|
||||||
endorsed,
|
endorsed,
|
||||||
|
note: privateNote,
|
||||||
} = relationship || {};
|
} = relationship || {};
|
||||||
|
|
||||||
const [currentInfo, setCurrentInfo] = useState(null);
|
const [currentInfo, setCurrentInfo] = useState(null);
|
||||||
|
@ -851,6 +879,7 @@ function RelatedActions({
|
||||||
|
|
||||||
const [showTranslatedBio, setShowTranslatedBio] = useState(false);
|
const [showTranslatedBio, setShowTranslatedBio] = useState(false);
|
||||||
const [showAddRemoveLists, setShowAddRemoveLists] = useState(false);
|
const [showAddRemoveLists, setShowAddRemoveLists] = useState(false);
|
||||||
|
const [showPrivateNoteModal, setShowPrivateNoteModal] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -861,9 +890,11 @@ function RelatedActions({
|
||||||
) : !!lastStatusAt ? (
|
) : !!lastStatusAt ? (
|
||||||
<small class="insignificant">
|
<small class="insignificant">
|
||||||
Last post:{' '}
|
Last post:{' '}
|
||||||
|
<span class="ib">
|
||||||
{niceDateTime(lastStatusAt, {
|
{niceDateTime(lastStatusAt, {
|
||||||
hideTime: true,
|
hideTime: true,
|
||||||
})}
|
})}
|
||||||
|
</span>
|
||||||
</small>
|
</small>
|
||||||
) : (
|
) : (
|
||||||
<span />
|
<span />
|
||||||
|
@ -872,6 +903,19 @@ function RelatedActions({
|
||||||
{blocking && <span class="tag danger">Blocked</span>}
|
{blocking && <span class="tag danger">Blocked</span>}
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
<span class="buttons">
|
<span class="buttons">
|
||||||
|
{!!privateNote && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="private-note-tag"
|
||||||
|
title="Private note"
|
||||||
|
onClick={() => {
|
||||||
|
setShowPrivateNoteModal(true);
|
||||||
|
}}
|
||||||
|
dir="auto"
|
||||||
|
>
|
||||||
|
<span>{privateNote}</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<Menu
|
<Menu
|
||||||
instanceRef={menuInstanceRef}
|
instanceRef={menuInstanceRef}
|
||||||
portal={{
|
portal={{
|
||||||
|
@ -925,6 +969,16 @@ function RelatedActions({
|
||||||
<Icon icon="translate" />
|
<Icon icon="translate" />
|
||||||
<span>Translate bio</span>
|
<span>Translate bio</span>
|
||||||
</MenuItem>
|
</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 */}
|
{/* Add/remove from lists is only possible if following the account */}
|
||||||
{following && (
|
{following && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
@ -1235,6 +1289,24 @@ function RelatedActions({
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</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 & close
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default AccountInfo;
|
export default AccountInfo;
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { useEffect } from 'preact/hooks';
|
||||||
|
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
|
import useLocationChange from '../utils/useLocationChange';
|
||||||
|
|
||||||
import AccountInfo from './account-info';
|
import AccountInfo from './account-info';
|
||||||
import Icon from './icon';
|
import Icon from './icon';
|
||||||
|
@ -16,17 +17,19 @@ function AccountSheet({ account, instance: propInstance, onClose }) {
|
||||||
}
|
}
|
||||||
}, [account]);
|
}, [account]);
|
||||||
|
|
||||||
|
useLocationChange(onClose);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class="sheet"
|
class="sheet"
|
||||||
onClick={(e) => {
|
// onClick={(e) => {
|
||||||
const accountBlock = e.target.closest('.account-block');
|
// const accountBlock = e.target.closest('.account-block');
|
||||||
if (accountBlock) {
|
// if (accountBlock) {
|
||||||
onClose({
|
// onClose({
|
||||||
destination: 'account-statuses',
|
// destination: 'account-statuses',
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
}}
|
// }}
|
||||||
>
|
>
|
||||||
{!!onClose && (
|
{!!onClose && (
|
||||||
<button type="button" class="sheet-close outer" onClick={onClose}>
|
<button type="button" class="sheet-close outer" onClick={onClose}>
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
background-color: var(--img-bg-color);
|
background-color: var(--img-bg-color);
|
||||||
|
contain: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar[data-loaded],
|
.avatar[data-loaded],
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { InView } from 'react-intersection-observer';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
|
import useLocationChange from '../utils/useLocationChange';
|
||||||
|
|
||||||
import AccountBlock from './account-block';
|
import AccountBlock from './account-block';
|
||||||
import Icon from './icon';
|
import Icon from './icon';
|
||||||
|
@ -16,6 +17,8 @@ export default function GenericAccounts({ onClose = () => {} }) {
|
||||||
const [accounts, setAccounts] = useState([]);
|
const [accounts, setAccounts] = useState([]);
|
||||||
const [showMore, setShowMore] = useState(false);
|
const [showMore, setShowMore] = useState(false);
|
||||||
|
|
||||||
|
useLocationChange(onClose);
|
||||||
|
|
||||||
if (!snapStates.showGenericAccounts) {
|
if (!snapStates.showGenericAccounts) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { memo } from 'preact/compat';
|
||||||
import { useEffect, useState } from 'preact/hooks';
|
import { useEffect, useState } from 'preact/hooks';
|
||||||
|
|
||||||
const SIZES = {
|
const SIZES = {
|
||||||
|
@ -101,6 +102,7 @@ export const ICONS = {
|
||||||
'account-warning': () => import('@iconify-icons/mingcute/user-warning-line'),
|
'account-warning': () => import('@iconify-icons/mingcute/user-warning-line'),
|
||||||
keyboard: () => import('@iconify-icons/mingcute/keyboard-line'),
|
keyboard: () => import('@iconify-icons/mingcute/keyboard-line'),
|
||||||
cloud: () => import('@iconify-icons/mingcute/cloud-line'),
|
cloud: () => import('@iconify-icons/mingcute/cloud-line'),
|
||||||
|
month: () => import('@iconify-icons/mingcute/calendar-month-line'),
|
||||||
};
|
};
|
||||||
|
|
||||||
function Icon({
|
function Icon({
|
||||||
|
@ -133,9 +135,6 @@ function Icon({
|
||||||
style={{
|
style={{
|
||||||
width: `${iconSize}px`,
|
width: `${iconSize}px`,
|
||||||
height: `${iconSize}px`,
|
height: `${iconSize}px`,
|
||||||
display: 'inline-block',
|
|
||||||
overflow: 'hidden',
|
|
||||||
lineHeight: 0,
|
|
||||||
...style,
|
...style,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -156,4 +155,4 @@ function Icon({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Icon;
|
export default memo(Icon);
|
||||||
|
|
|
@ -2,14 +2,14 @@ import './loader.css';
|
||||||
|
|
||||||
function Loader({ abrupt, hidden, ...props }) {
|
function Loader({ abrupt, hidden, ...props }) {
|
||||||
return (
|
return (
|
||||||
<div
|
<span
|
||||||
{...props}
|
{...props}
|
||||||
class={`loader-container ${abrupt ? 'abrupt' : ''} ${
|
class={`loader-container ${abrupt ? 'abrupt' : ''} ${
|
||||||
hidden ? 'hidden' : ''
|
hidden ? 'hidden' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div class="loader" />
|
<span class="loader" />
|
||||||
</div>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -130,6 +130,7 @@ function Media({
|
||||||
enabled: pinchZoomEnabled,
|
enabled: pinchZoomEnabled,
|
||||||
draggableUnZoomed: false,
|
draggableUnZoomed: false,
|
||||||
inertiaFriction: 0.9,
|
inertiaFriction: 0.9,
|
||||||
|
doubleTapZoomOutOnMaxScale: true,
|
||||||
containerProps: {
|
containerProps: {
|
||||||
className: 'media-zoom',
|
className: 'media-zoom',
|
||||||
style: {
|
style: {
|
||||||
|
|
|
@ -117,9 +117,10 @@ export default function Modals() {
|
||||||
instance={snapStates.showAccount?.instance}
|
instance={snapStates.showAccount?.instance}
|
||||||
onClose={({ destination } = {}) => {
|
onClose={({ destination } = {}) => {
|
||||||
states.showAccount = false;
|
states.showAccount = false;
|
||||||
if (destination) {
|
// states.showGenericAccounts = false;
|
||||||
states.showAccounts = false;
|
// if (destination) {
|
||||||
}
|
// states.showAccounts = false;
|
||||||
|
// }
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -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) {
|
@media (min-width: 23em) {
|
||||||
.nav-menu {
|
.nav-menu {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
@ -8,6 +14,7 @@
|
||||||
'left right';
|
'left right';
|
||||||
padding: 0;
|
padding: 0;
|
||||||
width: 22em;
|
width: 22em;
|
||||||
|
max-width: calc(100vw - 16px);
|
||||||
}
|
}
|
||||||
.nav-menu .top-menu {
|
.nav-menu .top-menu {
|
||||||
grid-area: top;
|
grid-area: top;
|
||||||
|
@ -27,7 +34,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.nav-menu section:last-child {
|
.nav-menu section:last-child {
|
||||||
background-color: var(--bg-faded-color);
|
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(
|
||||||
to right,
|
to right,
|
||||||
var(--divider-color) 1px,
|
var(--divider-color) 1px,
|
||||||
|
@ -45,6 +51,15 @@
|
||||||
animation: phanpying 0.2s ease-in-out both;
|
animation: phanpying 0.2s ease-in-out both;
|
||||||
border-top-right-radius: inherit;
|
border-top-right-radius: inherit;
|
||||||
border-bottom-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 {
|
.nav-menu section:last-child > .szh-menu__divider:first-child {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
@ -254,6 +254,7 @@ function NavMenu(props) {
|
||||||
<Icon icon="block" size="l" />
|
<Icon icon="block" size="l" />
|
||||||
Blocked users…
|
Blocked users…
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<MenuDivider className="divider-grow" />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
states.showKeyboardShortcutsHelp = true;
|
states.showKeyboardShortcutsHelp = true;
|
||||||
|
@ -268,7 +269,7 @@ function NavMenu(props) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="shortcut" size="l" />{' '}
|
<Icon icon="shortcut" size="l" />{' '}
|
||||||
<span>Shortcuts Settings…</span>
|
<span>Shortcuts / Columns…</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
|
@ -249,8 +249,7 @@ function ShortcutsSettings({ onClose }) {
|
||||||
</h2>
|
</h2>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<p>
|
<p>Specify a list of shortcuts that'll appear as:</p>
|
||||||
Specify a list of shortcuts that'll appear as:
|
|
||||||
<div class="shortcuts-view-mode">
|
<div class="shortcuts-view-mode">
|
||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
|
@ -298,7 +297,6 @@ function ShortcutsSettings({ onClose }) {
|
||||||
<option value="multi-column">Multi-column</option>
|
<option value="multi-column">Multi-column</option>
|
||||||
<option value="tab-menu-bar">Tab/Menu bar </option>
|
<option value="tab-menu-bar">Tab/Menu bar </option>
|
||||||
</select> */}
|
</select> */}
|
||||||
</p>
|
|
||||||
{/* <p>
|
{/* <p>
|
||||||
<details>
|
<details>
|
||||||
<summary class="insignificant">
|
<summary class="insignificant">
|
||||||
|
|
|
@ -1015,8 +1015,6 @@ body:has(#modal-container .carousel) .status .media img:hover {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
display: inline-flex;
|
|
||||||
gap: 4px;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
|
@ -1027,6 +1025,8 @@ body:has(#modal-container .carousel) .status .media img:hover {
|
||||||
white-space: pre-line;
|
white-space: pre-line;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
text-overflow: unset;
|
text-overflow: unset;
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -579,7 +579,11 @@ function Status({
|
||||||
try {
|
try {
|
||||||
const done = await confirmBoostStatus();
|
const done = await confirmBoostStatus();
|
||||||
if (!isSizeLarge && done) {
|
if (!isSizeLarge && done) {
|
||||||
showToast(reblogged ? 'Unboosted' : 'Boosted');
|
showToast(
|
||||||
|
reblogged
|
||||||
|
? `Unboosted @${username || acct}'s post`
|
||||||
|
: `Boosted @${username || acct}'s post`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}}
|
}}
|
||||||
|
@ -597,7 +601,11 @@ function Status({
|
||||||
try {
|
try {
|
||||||
favouriteStatus();
|
favouriteStatus();
|
||||||
if (!isSizeLarge) {
|
if (!isSizeLarge) {
|
||||||
showToast(favourited ? 'Unfavourited' : 'Favourited');
|
showToast(
|
||||||
|
favourited
|
||||||
|
? `Unfavourited @${username || acct}'s post`
|
||||||
|
: `Favourited @${username || acct}'s post`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}}
|
}}
|
||||||
|
@ -621,7 +629,11 @@ function Status({
|
||||||
try {
|
try {
|
||||||
bookmarkStatus();
|
bookmarkStatus();
|
||||||
if (!isSizeLarge) {
|
if (!isSizeLarge) {
|
||||||
showToast(bookmarked ? 'Unbookmarked' : 'Bookmarked');
|
showToast(
|
||||||
|
bookmarked
|
||||||
|
? `Unbookmarked @${username || acct}'s post`
|
||||||
|
: `Bookmarked @${username || acct}'s post`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}}
|
}}
|
||||||
|
@ -829,7 +841,11 @@ function Status({
|
||||||
try {
|
try {
|
||||||
favouriteStatus();
|
favouriteStatus();
|
||||||
if (!isSizeLarge) {
|
if (!isSizeLarge) {
|
||||||
showToast(favourited ? 'Unfavourited' : 'Favourited');
|
showToast(
|
||||||
|
favourited
|
||||||
|
? `Unfavourited @${username || acct}'s post`
|
||||||
|
: `Favourited @${username || acct}'s post`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
},
|
},
|
||||||
|
@ -843,7 +859,11 @@ function Status({
|
||||||
try {
|
try {
|
||||||
bookmarkStatus();
|
bookmarkStatus();
|
||||||
if (!isSizeLarge) {
|
if (!isSizeLarge) {
|
||||||
showToast(bookmarked ? 'Unbookmarked' : 'Bookmarked');
|
showToast(
|
||||||
|
bookmarked
|
||||||
|
? `Unbookmarked @${username || acct}'s post`
|
||||||
|
: `Bookmarked @${username || acct}'s post`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
},
|
},
|
||||||
|
@ -858,7 +878,11 @@ function Status({
|
||||||
try {
|
try {
|
||||||
const done = await confirmBoostStatus();
|
const done = await confirmBoostStatus();
|
||||||
if (!isSizeLarge && done) {
|
if (!isSizeLarge && done) {
|
||||||
showToast(reblogged ? 'Unboosted' : 'Boosted');
|
showToast(
|
||||||
|
reblogged
|
||||||
|
? `Unboosted @${username || acct}'s post`
|
||||||
|
: `Boosted @${username || acct}'s post`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -334,7 +334,13 @@ function Timeline({
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
{!!timelineStart && <div class="timeline-start">{timelineStart}</div>}
|
{!!timelineStart && (
|
||||||
|
<div
|
||||||
|
class={`timeline-start ${uiState === 'loading' ? 'loading' : ''}`}
|
||||||
|
>
|
||||||
|
{timelineStart}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{!!items.length ? (
|
{!!items.length ? (
|
||||||
<>
|
<>
|
||||||
<ul class="timeline">
|
<ul class="timeline">
|
||||||
|
|
|
@ -62,6 +62,9 @@
|
||||||
--close-button-bg-active-color: rgba(0, 0, 0, 0.2);
|
--close-button-bg-active-color: rgba(0, 0, 0, 0.2);
|
||||||
--close-button-color: rgba(0, 0, 0, 0.5);
|
--close-button-color: rgba(0, 0, 0, 0.5);
|
||||||
--close-button-hover-color: rgba(0, 0, 0, 1);
|
--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 */
|
/* Video colors won't change based on color scheme */
|
||||||
--media-fg-color: #f0f2f5;
|
--media-fg-color: #f0f2f5;
|
||||||
|
@ -111,6 +114,7 @@
|
||||||
--close-button-bg-active-color: rgba(255, 255, 255, 0.15);
|
--close-button-bg-active-color: rgba(255, 255, 255, 0.15);
|
||||||
--close-button-color: rgba(255, 255, 255, 0.5);
|
--close-button-color: rgba(255, 255, 255, 0.5);
|
||||||
--close-button-hover-color: rgba(255, 255, 255, 1);
|
--close-button-hover-color: rgba(255, 255, 255, 1);
|
||||||
|
--private-note-border-color: rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
13
src/main.jsx
13
src/main.jsx
|
@ -11,6 +11,19 @@ if (import.meta.env.DEV) {
|
||||||
import('preact/debug');
|
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(
|
render(
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
<App />
|
<App />
|
||||||
|
|
|
@ -10,24 +10,139 @@ import Link from '../components/link';
|
||||||
import Menu2 from '../components/menu2';
|
import Menu2 from '../components/menu2';
|
||||||
import Timeline from '../components/timeline';
|
import Timeline from '../components/timeline';
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
|
import pmem from '../utils/pmem';
|
||||||
import showToast from '../utils/show-toast';
|
import showToast from '../utils/show-toast';
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
import { saveStatus } from '../utils/states';
|
import { saveStatus } from '../utils/states';
|
||||||
import useTitle from '../utils/useTitle';
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
const LIMIT = 20;
|
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() {
|
function AccountStatuses() {
|
||||||
const snapStates = useSnapshot(states);
|
const snapStates = useSnapshot(states);
|
||||||
const { id, ...params } = useParams();
|
const { id, ...params } = useParams();
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const month = searchParams.get('month');
|
||||||
const excludeReplies = !searchParams.get('replies');
|
const excludeReplies = !searchParams.get('replies');
|
||||||
const excludeBoosts = !!searchParams.get('boosts');
|
const excludeBoosts = !!searchParams.get('boosts');
|
||||||
const tagged = searchParams.get('tagged');
|
const tagged = searchParams.get('tagged');
|
||||||
const media = !!searchParams.get('media');
|
const media = !!searchParams.get('media');
|
||||||
const { masto, instance, authenticated } = api({ instance: params.instance });
|
const { masto, instance, authenticated } = api({ instance: params.instance });
|
||||||
const accountStatusesIterator = useRef();
|
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) {
|
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 = [];
|
const results = [];
|
||||||
if (firstLoad) {
|
if (firstLoad) {
|
||||||
const { value: pinnedStatuses } = await masto.v1.accounts
|
const { value: pinnedStatuses } = await masto.v1.accounts
|
||||||
|
@ -78,7 +193,6 @@ function AccountStatuses() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const [account, setAccount] = useState();
|
|
||||||
const [featuredTags, setFeaturedTags] = useState([]);
|
const [featuredTags, setFeaturedTags] = useState([]);
|
||||||
useTitle(
|
useTitle(
|
||||||
`${account?.displayName ? account.displayName + ' ' : ''}@${
|
`${account?.displayName ? account.displayName + ' ' : ''}@${
|
||||||
|
@ -98,7 +212,7 @@ function AccountStatuses() {
|
||||||
try {
|
try {
|
||||||
const featuredTags = await masto.v1.accounts
|
const featuredTags = await masto.v1.accounts
|
||||||
.$select(id)
|
.$select(id)
|
||||||
.featuredTags.list(id);
|
.featuredTags.list();
|
||||||
console.log({ featuredTags });
|
console.log({ featuredTags });
|
||||||
setFeaturedTags(featuredTags);
|
setFeaturedTags(featuredTags);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -112,7 +226,8 @@ function AccountStatuses() {
|
||||||
const filterBarRef = useRef();
|
const filterBarRef = useRef();
|
||||||
const TimelineStart = useMemo(() => {
|
const TimelineStart = useMemo(() => {
|
||||||
const cachedAccount = snapStates.accounts[`${id}@${instance}`];
|
const cachedAccount = snapStates.accounts[`${id}@${instance}`];
|
||||||
const filtered = !excludeReplies || excludeBoosts || tagged || media;
|
const filtered =
|
||||||
|
!excludeReplies || excludeBoosts || tagged || media || !!month;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AccountInfo
|
<AccountInfo
|
||||||
|
@ -128,6 +243,7 @@ function AccountStatuses() {
|
||||||
to={`/${instance}/a/${id}`}
|
to={`/${instance}/a/${id}`}
|
||||||
class="insignificant filter-clear"
|
class="insignificant filter-clear"
|
||||||
title="Clear filters"
|
title="Clear filters"
|
||||||
|
key="clear-filters"
|
||||||
>
|
>
|
||||||
<Icon icon="x" size="l" />
|
<Icon icon="x" size="l" />
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -169,6 +285,7 @@ function AccountStatuses() {
|
||||||
</Link>
|
</Link>
|
||||||
{featuredTags.map((tag) => (
|
{featuredTags.map((tag) => (
|
||||||
<Link
|
<Link
|
||||||
|
key={tag.id}
|
||||||
to={`/${instance}/a/${id}${
|
to={`/${instance}/a/${id}${
|
||||||
tagged === tag.name
|
tagged === tag.name
|
||||||
? ''
|
? ''
|
||||||
|
@ -191,6 +308,48 @@ function AccountStatuses() {
|
||||||
{/* <span class="filter-count">{tag.statusesCount}</span> */}
|
{/* <span class="filter-count">{tag.statusesCount}</span> */}
|
||||||
</Link>
|
</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>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -198,11 +357,9 @@ function AccountStatuses() {
|
||||||
id,
|
id,
|
||||||
instance,
|
instance,
|
||||||
authenticated,
|
authenticated,
|
||||||
excludeReplies,
|
|
||||||
excludeBoosts,
|
|
||||||
featuredTags,
|
featuredTags,
|
||||||
tagged,
|
searchEnabled,
|
||||||
media,
|
...allSearchParams,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -217,7 +374,7 @@ function AccountStatuses() {
|
||||||
(filterBarRef.current.offsetWidth - active.offsetWidth) / 2,
|
(filterBarRef.current.offsetWidth - active.offsetWidth) / 2,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [featuredTags, tagged, media, excludeReplies, excludeBoosts]);
|
}, [featuredTags, searchEnabled, ...allSearchParams]);
|
||||||
|
|
||||||
const accountInstance = useMemo(() => {
|
const accountInstance = useMemo(() => {
|
||||||
if (!account?.url) return null;
|
if (!account?.url) return null;
|
||||||
|
@ -257,7 +414,13 @@ function AccountStatuses() {
|
||||||
useItemID
|
useItemID
|
||||||
boostsCarousel={snapStates.settings.boostsCarousel}
|
boostsCarousel={snapStates.settings.boostsCarousel}
|
||||||
timelineStart={TimelineStart}
|
timelineStart={TimelineStart}
|
||||||
refresh={excludeReplies + excludeBoosts + tagged + media}
|
refresh={[
|
||||||
|
excludeReplies,
|
||||||
|
excludeBoosts,
|
||||||
|
tagged,
|
||||||
|
media,
|
||||||
|
month + account?.acct,
|
||||||
|
].toString()}
|
||||||
headerEnd={
|
headerEnd={
|
||||||
<Menu2
|
<Menu2
|
||||||
portal
|
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;
|
export default AccountStatuses;
|
||||||
|
|
|
@ -63,7 +63,7 @@ function NotificationsLink() {
|
||||||
to="/notifications"
|
to="/notifications"
|
||||||
class={`button plain notifications-button ${
|
class={`button plain notifications-button ${
|
||||||
snapStates.notificationsShowNew ? 'has-badge' : ''
|
snapStates.notificationsShowNew ? 'has-badge' : ''
|
||||||
} ${menuState}`}
|
} ${menuState || ''}`}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (window.matchMedia('(min-width: calc(40em))').matches) {
|
if (window.matchMedia('(min-width: calc(40em))').matches) {
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
height: 40em;
|
height: 40em;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
}
|
}
|
||||||
.notifications-menu .status {
|
.notifications-menu .status {
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
|
|
|
@ -40,9 +40,15 @@ ul.link-list.hashtag-list li a {
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 40em) {
|
@media (min-width: 40em) {
|
||||||
#search-page header input {
|
#search-page {
|
||||||
|
header input {
|
||||||
background-color: var(--bg-color);
|
background-color: var(--bg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filter-bar {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-popover-container {
|
.search-popover-container {
|
||||||
|
|
|
@ -141,7 +141,7 @@ function Search(props) {
|
||||||
return (
|
return (
|
||||||
<div id="search-page" class="deck-container" ref={scrollableRef}>
|
<div id="search-page" class="deck-container" ref={scrollableRef}>
|
||||||
<div class="timeline-deck deck">
|
<div class="timeline-deck deck">
|
||||||
<header>
|
<header class={uiState === 'loading' ? 'loading' : ''}>
|
||||||
<div class="header-grid">
|
<div class="header-grid">
|
||||||
<div class="header-side">
|
<div class="header-side">
|
||||||
<NavMenu />
|
<NavMenu />
|
||||||
|
@ -152,7 +152,7 @@ function Search(props) {
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
{!!q && (
|
{!!q && (
|
||||||
<div class="filter-bar">
|
<div class={`filter-bar ${uiState === 'loading' ? 'loading' : ''}`}>
|
||||||
{!!type && (
|
{!!type && (
|
||||||
<Link to={`/search${q ? `?q=${encodeURIComponent(q)}` : ''}`}>
|
<Link to={`/search${q ? `?q=${encodeURIComponent(q)}` : ''}`}>
|
||||||
‹ All
|
‹ All
|
||||||
|
@ -181,7 +181,9 @@ function Search(props) {
|
||||||
return 0;
|
return 0;
|
||||||
})
|
})
|
||||||
.map((link) => (
|
.map((link) => (
|
||||||
<Link to={link.to}>{link.label}</Link>
|
<Link to={link.to} key={link.type}>
|
||||||
|
{link.label}
|
||||||
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -30,6 +30,10 @@
|
||||||
|
|
||||||
.ancestors-indicator {
|
.ancestors-indicator {
|
||||||
font-size: 70% !important;
|
font-size: 70% !important;
|
||||||
|
|
||||||
|
& > .avatar ~ .avatar {
|
||||||
|
margin-left: -4px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.ancestors-indicator:not([hidden]) {
|
.ancestors-indicator:not([hidden]) {
|
||||||
animation: slide-up 0.3s both ease-out 0.3s;
|
animation: slide-up 0.3s both ease-out 0.3s;
|
||||||
|
|
|
@ -650,6 +650,189 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
resetScrollPosition(status.id);
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
tabIndex="-1"
|
tabIndex="-1"
|
||||||
|
@ -869,196 +1052,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
uiState === 'loading' ? 'loading' : ''
|
uiState === 'loading' ? 'loading' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{statuses.slice(0, limit).map((status) => {
|
{statuses.slice(0, limit).map(renderStatus)}
|
||||||
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>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{showMore > 0 && (
|
{showMore > 0 && (
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
|
@ -1068,7 +1062,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
onClick={() => setLimit((l) => l + LIMIT)}
|
onClick={() => setLimit((l) => l + LIMIT)}
|
||||||
style={{ marginBlockEnd: '6em' }}
|
style={{ marginBlockEnd: '6em' }}
|
||||||
>
|
>
|
||||||
<div class="ib">
|
<div class="ib avatars-bunch">
|
||||||
{/* show avatars for first 5 statuses */}
|
{/* show avatars for first 5 statuses */}
|
||||||
{statuses.slice(limit, limit + 5).map((status) => (
|
{statuses.slice(limit, limit + 5).map((status) => (
|
||||||
<Avatar
|
<Avatar
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import mem from './mem';
|
||||||
|
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
function getHTMLText(html) {
|
function getHTMLText(html) {
|
||||||
if (!html) return '';
|
if (!html) return '';
|
||||||
|
@ -10,4 +12,4 @@ function getHTMLText(html) {
|
||||||
return div.innerText.replace(/[\r\n]{3,}/g, '\n\n').trim();
|
return div.innerText.replace(/[\r\n]{3,}/g, '\n\n').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
export default getHTMLText;
|
export default mem(getHTMLText);
|
||||||
|
|
23
src/utils/useLocationChange.js
Normal file
23
src/utils/useLocationChange.js
Normal 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]);
|
||||||
|
}
|
Loading…
Reference in a new issue