commit
77ba42dba9
|
@ -2,7 +2,10 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1, viewport-fit=cover"
|
||||||
|
/>
|
||||||
<title>Phanpy</title>
|
<title>Phanpy</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
|
|
11
package-lock.json
generated
11
package-lock.json
generated
|
@ -13,6 +13,7 @@
|
||||||
"fast-blurhash": "~1.1.2",
|
"fast-blurhash": "~1.1.2",
|
||||||
"history": "~5.3.0",
|
"history": "~5.3.0",
|
||||||
"iconify-icon": "~1.0.2",
|
"iconify-icon": "~1.0.2",
|
||||||
|
"just-debounce-it": "^3.2.0",
|
||||||
"masto": "~4.10.1",
|
"masto": "~4.10.1",
|
||||||
"mem": "~9.0.2",
|
"mem": "~9.0.2",
|
||||||
"preact": "~10.11.3",
|
"preact": "~10.11.3",
|
||||||
|
@ -4046,6 +4047,11 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/just-debounce-it": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/just-debounce-it/-/just-debounce-it-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-WXzwLL0745uNuedrCsCs3rpmfD6DBaf7uuVwaq98/8dafURfgQaBsSpjiPp5+CW6Vjltwy9cOGI6qE71b3T8iQ=="
|
||||||
|
},
|
||||||
"node_modules/kolorist": {
|
"node_modules/kolorist": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.6.0.tgz",
|
||||||
|
@ -8562,6 +8568,11 @@
|
||||||
"integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==",
|
"integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"just-debounce-it": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/just-debounce-it/-/just-debounce-it-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-WXzwLL0745uNuedrCsCs3rpmfD6DBaf7uuVwaq98/8dafURfgQaBsSpjiPp5+CW6Vjltwy9cOGI6qE71b3T8iQ=="
|
||||||
|
},
|
||||||
"kolorist": {
|
"kolorist": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.6.0.tgz",
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
"fast-blurhash": "~1.1.2",
|
"fast-blurhash": "~1.1.2",
|
||||||
"history": "~5.3.0",
|
"history": "~5.3.0",
|
||||||
"iconify-icon": "~1.0.2",
|
"iconify-icon": "~1.0.2",
|
||||||
|
"just-debounce-it": "^3.2.0",
|
||||||
"masto": "~4.10.1",
|
"masto": "~4.10.1",
|
||||||
"mem": "~9.0.2",
|
"mem": "~9.0.2",
|
||||||
"preact": "~10.11.3",
|
"preact": "~10.11.3",
|
||||||
|
|
BIN
public/logo-maskable-512.png
Normal file
BIN
public/logo-maskable-512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.3 KiB |
|
@ -5,12 +5,17 @@ import { CacheFirst, StaleWhileRevalidate } from 'workbox-strategies';
|
||||||
|
|
||||||
const imageRoute = new Route(
|
const imageRoute = new Route(
|
||||||
({ request, sameOrigin }) => {
|
({ request, sameOrigin }) => {
|
||||||
return !sameOrigin && request.destination === 'image';
|
const isRemote = !sameOrigin;
|
||||||
|
const isImage = request.destination === 'image';
|
||||||
|
const isAvatar = request.url.includes('/avatars/');
|
||||||
|
const isEmoji = request.url.includes('/emoji/');
|
||||||
|
return isRemote && isImage && (isAvatar || isEmoji);
|
||||||
},
|
},
|
||||||
new CacheFirst({
|
new CacheFirst({
|
||||||
cacheName: 'remote-images',
|
cacheName: 'remote-images',
|
||||||
plugins: [
|
plugins: [
|
||||||
new ExpirationPlugin({
|
new ExpirationPlugin({
|
||||||
|
maxEntries: 100,
|
||||||
maxAgeSeconds: 7 * 24 * 60 * 60, // 7 days
|
maxAgeSeconds: 7 * 24 * 60 * 60, // 7 days
|
||||||
purgeOnQuotaError: true,
|
purgeOnQuotaError: true,
|
||||||
}),
|
}),
|
||||||
|
|
55
src/app.css
55
src/app.css
|
@ -137,6 +137,7 @@ a.mention span {
|
||||||
transparent
|
transparent
|
||||||
);
|
);
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
|
transition: opacity 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
.timeline.contextual > li:first-child {
|
.timeline.contextual > li:first-child {
|
||||||
background-position: 0 16px;
|
background-position: 0 16px;
|
||||||
|
@ -273,6 +274,14 @@ a.mention span {
|
||||||
.timeline.contextual > li.thread .replies li:before {
|
.timeline.contextual > li.thread .replies li:before {
|
||||||
left: calc(50px + 16px + 16px);
|
left: calc(50px + 16px + 16px);
|
||||||
}
|
}
|
||||||
|
.timeline.contextual.loading > li:not(.hero) {
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
/* background-image: none !important; */
|
||||||
|
}
|
||||||
|
/* .timeline.contextual.loading > li:not(.hero):before {
|
||||||
|
content: none !important;
|
||||||
|
} */
|
||||||
|
|
||||||
.timeline-deck.compact .status {
|
.timeline-deck.compact .status {
|
||||||
max-height: max(25vh, 160px);
|
max-height: max(25vh, 160px);
|
||||||
|
@ -334,12 +343,12 @@ a.mention span {
|
||||||
}
|
}
|
||||||
@keyframes slide-in {
|
@keyframes slide-in {
|
||||||
0% {
|
0% {
|
||||||
opacity: 0;
|
opacity: 0.5;
|
||||||
transform: translateX(100%);
|
transform: translate3d(100%, 0, 0);
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateX(0);
|
transform: translate3d(0, 0, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.deck-backdrop .deck {
|
.deck-backdrop .deck {
|
||||||
|
@ -347,7 +356,7 @@ a.mention span {
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
border-left: 1px solid var(--divider-color);
|
border-left: 1px solid var(--divider-color);
|
||||||
background-color: var(--bg-color);
|
background-color: var(--bg-color);
|
||||||
animation: slide-in 0.2s ease-out;
|
animation: slide-in 0.5s var(--timing-function);
|
||||||
box-shadow: -1px 0 var(--bg-color);
|
box-shadow: -1px 0 var(--bg-color);
|
||||||
}
|
}
|
||||||
.deck-backdrop .deck .status {
|
.deck-backdrop .deck .status {
|
||||||
|
@ -356,7 +365,7 @@ a.mention span {
|
||||||
|
|
||||||
.decks {
|
.decks {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
transition: transform 0.2s ease-in-out;
|
transition: transform 0.5s var(--timing-function);
|
||||||
}
|
}
|
||||||
|
|
||||||
.deck-close {
|
.deck-close {
|
||||||
|
@ -405,31 +414,6 @@ a.mention span {
|
||||||
width: 40em;
|
width: 40em;
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background-color: var(--bg-color);
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid var(--divider-color);
|
|
||||||
overflow: auto;
|
|
||||||
max-height: 90vh;
|
|
||||||
max-height: 90dvh;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.box > :is(h1, h2, h3):first-of-type {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
.box .close-button {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
float: right;
|
|
||||||
margin: -16px -8px 0 0;
|
|
||||||
transform: translate(0, -8px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.box-shadow {
|
|
||||||
box-shadow: 0px 36px 89px rgb(0 0 0 / 4%),
|
|
||||||
0px 23.3333px 52.1227px rgb(0 0 0 / 3%),
|
|
||||||
0px 13.8667px 28.3481px rgb(0 0 0 / 2%), 0px 7.2px 14.4625px rgb(0 0 0 / 2%),
|
|
||||||
0px 2.93333px 7.25185px rgb(0 0 0 / 2%),
|
|
||||||
0px 0.666667px 3.50231px rgb(0 0 0 / 1%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* CAROUSEL */
|
/* CAROUSEL */
|
||||||
|
@ -463,6 +447,9 @@ a.mention span {
|
||||||
max-height: 100vh;
|
max-height: 100vh;
|
||||||
max-height: 100dvh;
|
max-height: 100dvh;
|
||||||
}
|
}
|
||||||
|
.carousel > * video {
|
||||||
|
min-height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
.carousel-top-controls {
|
.carousel-top-controls {
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -587,7 +574,7 @@ button.carousel-dot[disabled].active {
|
||||||
padding-right: max(16px, env(safe-area-inset-right));
|
padding-right: max(16px, env(safe-area-inset-right));
|
||||||
padding-bottom: max(16px, env(safe-area-inset-bottom));
|
padding-bottom: max(16px, env(safe-area-inset-bottom));
|
||||||
box-shadow: 0 -1px 32px var(--divider-color);
|
box-shadow: 0 -1px 32px var(--divider-color);
|
||||||
animation: slide-up 0.2s ease-out;
|
animation: slide-up 0.2s var(--timing-function);
|
||||||
border: 1px solid var(--outline-color);
|
border: 1px solid var(--outline-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -632,13 +619,15 @@ button.carousel-dot[disabled].active {
|
||||||
border: 1px solid var(--outline-color);
|
border: 1px solid var(--outline-color);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
transition: all 0.2s ease-in-out;
|
transition: all 0.2s ease-in-out;
|
||||||
|
box-shadow: 0 0 8px var(--bg-faded-color), 0 4px 8px var(--bg-faded-color),
|
||||||
|
0 2px 4px var(--bg-faded-color);
|
||||||
}
|
}
|
||||||
.menu-container menu li {
|
.menu-container menu li {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
.menu-container > button:is(:active, :focus) + menu,
|
.menu-container > button:is(:hover, :active, :focus) + menu,
|
||||||
.menu-container menu:is(:hover, :active) {
|
.menu-container menu:is(:hover, :active) {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
|
@ -663,7 +652,7 @@ button.carousel-dot[disabled].active {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
.decks:has(~ .deck-backdrop) {
|
.decks:has(~ .deck-backdrop) {
|
||||||
transform: translateX(-5vw);
|
transform: translate3d(-5vw, 0, 0);
|
||||||
}
|
}
|
||||||
.deck-backdrop .deck {
|
.deck-backdrop .deck {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
|
|
|
@ -104,7 +104,7 @@ async function startStream() {
|
||||||
export function App() {
|
export function App() {
|
||||||
const snapStates = useSnapshot(states);
|
const snapStates = useSnapshot(states);
|
||||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||||
const [uiState, setUIState] = useState('default');
|
const [uiState, setUIState] = useState('loading');
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const theme = store.local.get('theme');
|
const theme = store.local.get('theme');
|
||||||
|
@ -144,6 +144,7 @@ export function App() {
|
||||||
url: `https://${instanceURL}`,
|
url: `https://${instanceURL}`,
|
||||||
accessToken,
|
accessToken,
|
||||||
disableVersionCheck: true,
|
disableVersionCheck: true,
|
||||||
|
timeout: 30_000,
|
||||||
});
|
});
|
||||||
|
|
||||||
const mastoAccount = await masto.accounts.verifyCredentials();
|
const mastoAccount = await masto.accounts.verifyCredentials();
|
||||||
|
@ -185,6 +186,7 @@ export function App() {
|
||||||
url: `https://${instanceURL}`,
|
url: `https://${instanceURL}`,
|
||||||
accessToken,
|
accessToken,
|
||||||
disableVersionCheck: true,
|
disableVersionCheck: true,
|
||||||
|
timeout: 30_000,
|
||||||
});
|
});
|
||||||
setIsLoggedIn(true);
|
setIsLoggedIn(true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -192,6 +194,8 @@ export function App() {
|
||||||
}
|
}
|
||||||
setUIState('default');
|
setUIState('default');
|
||||||
})();
|
})();
|
||||||
|
} else {
|
||||||
|
setUIState('default');
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
|
@ -78,6 +78,7 @@
|
||||||
text-shadow: 0 1px 10px var(--bg-color), 0 1px 10px var(--bg-color),
|
text-shadow: 0 1px 10px var(--bg-color), 0 1px 10px var(--bg-color),
|
||||||
0 1px 10px var(--bg-color), 0 1px 10px var(--bg-color),
|
0 1px 10px var(--bg-color), 0 1px 10px var(--bg-color),
|
||||||
0 1px 10px var(--bg-color);
|
0 1px 10px var(--bg-color);
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
#_compose-container .status-preview-legend.reply-to {
|
#_compose-container .status-preview-legend.reply-to {
|
||||||
color: var(--reply-to-color);
|
color: var(--reply-to-color);
|
||||||
|
@ -359,3 +360,10 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
color: var(--red-color);
|
color: var(--red-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (display-mode: standalone) {
|
||||||
|
/* No popping in standalone mode */
|
||||||
|
#compose-container .pop-button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -256,6 +256,12 @@ function Compose({
|
||||||
const canClose = () => {
|
const canClose = () => {
|
||||||
const { value, dataset } = textareaRef.current;
|
const { value, dataset } = textareaRef.current;
|
||||||
|
|
||||||
|
// check if loading
|
||||||
|
if (uiState === 'loading') {
|
||||||
|
console.log('canClose', { uiState });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// check for status and media attachments
|
// check for status and media attachments
|
||||||
const hasMediaAttachments = mediaAttachments.length > 0;
|
const hasMediaAttachments = mediaAttachments.length > 0;
|
||||||
if (!value && !hasMediaAttachments) {
|
if (!value && !hasMediaAttachments) {
|
||||||
|
@ -297,6 +303,7 @@ function Compose({
|
||||||
isSelf,
|
isSelf,
|
||||||
hasOnlyAcct,
|
hasOnlyAcct,
|
||||||
sameWithSource,
|
sameWithSource,
|
||||||
|
uiState,
|
||||||
});
|
});
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
@ -341,7 +348,8 @@ function Compose({
|
||||||
<span>
|
<span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="light"
|
class="light pop-button"
|
||||||
|
disabled={uiState === 'loading'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// If there are non-ID media attachments (not yet uploaded), show confirmation dialog because they are not going to be passed to the new window
|
// If there are non-ID media attachments (not yet uploaded), show confirmation dialog because they are not going to be passed to the new window
|
||||||
const containNonIDMediaAttachments =
|
const containNonIDMediaAttachments =
|
||||||
|
@ -386,6 +394,7 @@ function Compose({
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="light close-button"
|
class="light close-button"
|
||||||
|
disabled={uiState === 'loading'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (confirmClose()) {
|
if (confirmClose()) {
|
||||||
onClose();
|
onClose();
|
||||||
|
@ -399,7 +408,8 @@ function Compose({
|
||||||
hasOpener && (
|
hasOpener && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="light"
|
class="light pop-button"
|
||||||
|
disabled={uiState === 'loading'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// If there are non-ID media attachments (not yet uploaded), show confirmation dialog because they are not going to be passed to the new window
|
// If there are non-ID media attachments (not yet uploaded), show confirmation dialog because they are not going to be passed to the new window
|
||||||
const containNonIDMediaAttachments =
|
const containNonIDMediaAttachments =
|
||||||
|
@ -532,7 +542,6 @@ function Compose({
|
||||||
const params = {
|
const params = {
|
||||||
file,
|
file,
|
||||||
description,
|
description,
|
||||||
skipPolling: true,
|
|
||||||
};
|
};
|
||||||
return masto.mediaAttachments.create(params).then((res) => {
|
return masto.mediaAttachments.create(params).then((res) => {
|
||||||
if (res.id) {
|
if (res.id) {
|
||||||
|
@ -554,6 +563,7 @@ function Compose({
|
||||||
// Alert all the reasons
|
// Alert all the reasons
|
||||||
results.forEach((result) => {
|
results.forEach((result) => {
|
||||||
if (result.status === 'rejected') {
|
if (result.status === 'rejected') {
|
||||||
|
console.error(result);
|
||||||
alert(result.reason || `Attachment #${i} failed`);
|
alert(result.reason || `Attachment #${i} failed`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,11 +6,18 @@ import states from '../utils/states';
|
||||||
import Avatar from './avatar';
|
import Avatar from './avatar';
|
||||||
|
|
||||||
function NameText({ account, showAvatar, showAcct, short, external }) {
|
function NameText({ account, showAvatar, showAcct, short, external }) {
|
||||||
const { acct, avatar, avatarStatic, id, url, displayName, username, emojis } =
|
const { acct, avatar, avatarStatic, id, url, displayName, emojis } = account;
|
||||||
account;
|
let { username } = account;
|
||||||
|
|
||||||
const displayNameWithEmoji = emojifyText(displayName, emojis);
|
const displayNameWithEmoji = emojifyText(displayName, emojis);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!short &&
|
||||||
|
username.toLowerCase().trim() === (displayName || '').toLowerCase().trim()
|
||||||
|
) {
|
||||||
|
username = null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
class={`name-text ${short ? 'short' : ''}`}
|
class={`name-text ${short ? 'short' : ''}`}
|
||||||
|
@ -35,7 +42,7 @@ function NameText({ account, showAvatar, showAcct, short, external }) {
|
||||||
__html: displayNameWithEmoji,
|
__html: displayNameWithEmoji,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{!showAcct && (
|
{!showAcct && username && (
|
||||||
<>
|
<>
|
||||||
{' '}
|
{' '}
|
||||||
<i>@{username}</i>
|
<i>@{username}</i>
|
||||||
|
|
|
@ -62,6 +62,7 @@
|
||||||
padding: 16px 16px 20px;
|
padding: 16px 16px 20px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
.status.large {
|
.status.large {
|
||||||
--fade-in-out-bg: linear-gradient(
|
--fade-in-out-bg: linear-gradient(
|
||||||
|
@ -81,7 +82,7 @@
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
}
|
}
|
||||||
.status.small {
|
.status.small {
|
||||||
font-size: 95%;
|
/* font-size: 95%; */
|
||||||
}
|
}
|
||||||
.status.skeleton {
|
.status.skeleton {
|
||||||
color: var(--outline-color);
|
color: var(--outline-color);
|
||||||
|
@ -283,6 +284,7 @@
|
||||||
}
|
}
|
||||||
.status .media-video {
|
.status .media-video {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
background-clip: padding-box;
|
||||||
}
|
}
|
||||||
.status .media-video:before {
|
.status .media-video:before {
|
||||||
/* draw a circle in the middle */
|
/* draw a circle in the middle */
|
||||||
|
@ -408,6 +410,16 @@ a.card:hover {
|
||||||
|
|
||||||
/* POLLS */
|
/* POLLS */
|
||||||
|
|
||||||
|
.poll {
|
||||||
|
transition: opacity 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
.poll.loading {
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.poll.read-only {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
.poll-option {
|
.poll-option {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
@ -607,6 +619,26 @@ a.card:hover {
|
||||||
color: var(--green-color);
|
color: var(--green-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* BADGE */
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
right: 4px;
|
||||||
|
line-height: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
.status-badge .favourite {
|
||||||
|
color: var(--favourite-color);
|
||||||
|
}
|
||||||
|
.status-badge .reblog {
|
||||||
|
color: var(--reblog-color);
|
||||||
|
}
|
||||||
|
.status-badge .bookmark {
|
||||||
|
color: var(--link-color);
|
||||||
|
}
|
||||||
|
|
||||||
/* MISC */
|
/* MISC */
|
||||||
|
|
||||||
.status-aside {
|
.status-aside {
|
||||||
|
|
|
@ -130,7 +130,7 @@ function Status({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [showSpoiler, setShowSpoiler] = useState(false);
|
const showSpoiler = snapStates.spoilers.has(id) || false;
|
||||||
|
|
||||||
const debugHover = (e) => {
|
const debugHover = (e) => {
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
|
@ -197,6 +197,13 @@ function Status({
|
||||||
}`}
|
}`}
|
||||||
onMouseEnter={debugHover}
|
onMouseEnter={debugHover}
|
||||||
>
|
>
|
||||||
|
{size !== 'l' && (
|
||||||
|
<div class="status-badge">
|
||||||
|
{reblogged && <Icon class="reblog" icon="rocket" size="s" />}
|
||||||
|
{favourited && <Icon class="favourite" icon="heart" size="s" />}
|
||||||
|
{bookmarked && <Icon class="bookmark" icon="bookmark" size="s" />}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{size !== 's' && (
|
{size !== 's' && (
|
||||||
<a
|
<a
|
||||||
href={url}
|
href={url}
|
||||||
|
@ -293,7 +300,11 @@ function Status({
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setShowSpoiler(!showSpoiler);
|
if (showSpoiler) {
|
||||||
|
states.spoilers.delete(id);
|
||||||
|
} else {
|
||||||
|
states.spoilers.set(id, true);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon={showSpoiler ? 'eye-open' : 'eye-close'} />{' '}
|
<Icon icon={showSpoiler ? 'eye-open' : 'eye-close'} />{' '}
|
||||||
|
@ -348,7 +359,15 @@ function Status({
|
||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{!!poll && <Poll poll={poll} readOnly={readOnly} />}
|
{!!poll && (
|
||||||
|
<Poll
|
||||||
|
poll={poll}
|
||||||
|
readOnly={readOnly}
|
||||||
|
onUpdate={(newPoll) => {
|
||||||
|
states.statuses.get(id).poll = newPoll;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{!spoilerText && sensitive && !!mediaAttachments.length && (
|
{!spoilerText && sensitive && !!mediaAttachments.length && (
|
||||||
<button
|
<button
|
||||||
class="plain spoiler"
|
class="plain spoiler"
|
||||||
|
@ -356,7 +375,11 @@ function Status({
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setShowSpoiler(!showSpoiler);
|
if (showSpoiler) {
|
||||||
|
states.spoilers.delete(id);
|
||||||
|
} else {
|
||||||
|
states.spoilers.add(id);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon={showSpoiler ? 'eye-open' : 'eye-close'} /> Sensitive
|
<Icon icon={showSpoiler ? 'eye-open' : 'eye-close'} /> Sensitive
|
||||||
|
@ -455,6 +478,14 @@ function Status({
|
||||||
count={reblogsCount}
|
count={reblogsCount}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
|
if (!reblogged) {
|
||||||
|
const yes = confirm(
|
||||||
|
'Are you sure that you want to boost this post?',
|
||||||
|
);
|
||||||
|
if (!yes) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
// Optimistic
|
// Optimistic
|
||||||
states.statuses.set(id, {
|
states.statuses.set(id, {
|
||||||
...status,
|
...status,
|
||||||
|
@ -607,7 +638,7 @@ video = Video clip
|
||||||
audio = Audio track
|
audio = Audio track
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function Media({ media, showOriginal, onClick }) {
|
function Media({ media, showOriginal, onClick = () => {} }) {
|
||||||
const { blurhash, description, meta, previewUrl, remoteUrl, url, type } =
|
const { blurhash, description, meta, previewUrl, remoteUrl, url, type } =
|
||||||
media;
|
media;
|
||||||
const { original, small, focus } = meta || {};
|
const { original, small, focus } = meta || {};
|
||||||
|
@ -850,14 +881,9 @@ function Card({ card }) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function Poll({ poll, readOnly }) {
|
function Poll({ poll, readOnly, onUpdate = () => {} }) {
|
||||||
const [pollSnapshot, setPollSnapshot] = useState(poll);
|
|
||||||
const [uiState, setUIState] = useState('default');
|
const [uiState, setUIState] = useState('default');
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setPollSnapshot(poll);
|
|
||||||
}, [poll]);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
expired,
|
expired,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
|
@ -868,12 +894,16 @@ function Poll({ poll, readOnly }) {
|
||||||
voted,
|
voted,
|
||||||
votersCount,
|
votersCount,
|
||||||
votesCount,
|
votesCount,
|
||||||
} = pollSnapshot;
|
} = poll;
|
||||||
|
|
||||||
const expiresAtDate = !!expiresAt && new Date(expiresAt);
|
const expiresAtDate = !!expiresAt && new Date(expiresAt);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="poll">
|
<div
|
||||||
|
class={`poll ${readOnly ? 'read-only' : ''} ${
|
||||||
|
uiState === 'loading' ? 'loading' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{voted || expired ? (
|
{voted || expired ? (
|
||||||
options.map((option, i) => {
|
options.map((option, i) => {
|
||||||
const { title, votesCount: optionVotesCount } = option;
|
const { title, votesCount: optionVotesCount } = option;
|
||||||
|
@ -930,13 +960,9 @@ function Poll({ poll, readOnly }) {
|
||||||
choices: votes,
|
choices: votes,
|
||||||
});
|
});
|
||||||
console.log(pollResponse);
|
console.log(pollResponse);
|
||||||
setPollSnapshot(pollResponse);
|
onUpdate(pollResponse);
|
||||||
setUIState('default');
|
setUIState('default');
|
||||||
}}
|
}}
|
||||||
style={{
|
|
||||||
pointerEvents: uiState === 'loading' || readOnly ? 'none' : 'auto',
|
|
||||||
opacity: uiState === 'loading' ? 0.5 : 1,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{options.map((option, i) => {
|
{options.map((option, i) => {
|
||||||
const { title } = option;
|
const { title } = option;
|
||||||
|
@ -968,6 +994,31 @@ function Poll({ poll, readOnly }) {
|
||||||
)}
|
)}
|
||||||
{!readOnly && (
|
{!readOnly && (
|
||||||
<p class="poll-meta">
|
<p class="poll-meta">
|
||||||
|
{!expired && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="textual"
|
||||||
|
disabled={uiState === 'loading'}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setUIState('loading');
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const pollResponse = await masto.poll.fetch(id);
|
||||||
|
onUpdate(pollResponse);
|
||||||
|
} catch (e) {
|
||||||
|
// Silent fail
|
||||||
|
}
|
||||||
|
setUIState('default');
|
||||||
|
})();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</button>{' '}
|
||||||
|
•{' '}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<span title={votersCount}>{shortenNumber(votersCount)}</span>{' '}
|
<span title={votersCount}>{shortenNumber(votersCount)}</span>{' '}
|
||||||
{votersCount === 1 ? 'voter' : 'voters'}
|
{votersCount === 1 ? 'voter' : 'voters'}
|
||||||
{votersCount !== votesCount && (
|
{votersCount !== votesCount && (
|
||||||
|
@ -1012,10 +1063,10 @@ function EditedAtModal({ statusID, onClose = () => {} }) {
|
||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="edit-history" class="box">
|
<div id="edit-history" class="sheet">
|
||||||
<button type="button" class="close-button plain large" onClick={onClose}>
|
{/* <button type="button" class="close-button plain large" onClick={onClose}>
|
||||||
<Icon icon="x" alt="Close" />
|
<Icon icon="x" alt="Close" />
|
||||||
</button>
|
</button> */}
|
||||||
<h2>Edit History</h2>
|
<h2>Edit History</h2>
|
||||||
{uiState === 'error' && <p>Failed to load history</p>}
|
{uiState === 'error' && <p>Failed to load history</p>}
|
||||||
{uiState === 'loading' && (
|
{uiState === 'loading' && (
|
||||||
|
|
|
@ -29,6 +29,7 @@ if (window.opener) {
|
||||||
url: `https://${instanceURL}`,
|
url: `https://${instanceURL}`,
|
||||||
accessToken,
|
accessToken,
|
||||||
disableVersionCheck: true,
|
disableVersionCheck: true,
|
||||||
|
timeout: 30_000,
|
||||||
});
|
});
|
||||||
console.info('Logged in successfully.');
|
console.info('Logged in successfully.');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -30,6 +30,8 @@
|
||||||
--img-bg-color: rgba(128, 128, 128, 0.2);
|
--img-bg-color: rgba(128, 128, 128, 0.2);
|
||||||
--loader-color: #1c1e2199;
|
--loader-color: #1c1e2199;
|
||||||
--comment-line-color: #e5e5e5;
|
--comment-line-color: #e5e5e5;
|
||||||
|
|
||||||
|
--timing-function: cubic-bezier(0.3, 0.5, 0, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
|
@ -118,8 +120,9 @@ button,
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
button > * {
|
:is(button, .button) > * {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
:is(button, .button):not(:disabled, .disabled):hover {
|
:is(button, .button):not(:disabled, .disabled):hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -154,6 +157,15 @@ button > * {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:is(button, .button).textual {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
vertical-align: baseline;
|
||||||
|
color: var(--link-color);
|
||||||
|
background-color: transparent;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
input[type='text'],
|
input[type='text'],
|
||||||
textarea,
|
textarea,
|
||||||
select {
|
select {
|
||||||
|
|
|
@ -82,6 +82,7 @@ function Home({ hidden }) {
|
||||||
const diffMins = Math.round(diff / 1000 / 60);
|
const diffMins = Math.round(diff / 1000 / 60);
|
||||||
if (diffMins > 1) {
|
if (diffMins > 1) {
|
||||||
console.log('visible', { lastHidden, diffMins });
|
console.log('visible', { lastHidden, diffMins });
|
||||||
|
setUIState('loading');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const newStatus = await masto.timelines.fetchHome({
|
const newStatus = await masto.timelines.fetchHome({
|
||||||
|
@ -91,6 +92,7 @@ function Home({ hidden }) {
|
||||||
if (newStatus.length && newStatus[0].id !== states.home[0].id) {
|
if (newStatus.length && newStatus[0].id !== states.home[0].id) {
|
||||||
states.homeNew = newStatus;
|
states.homeNew = newStatus;
|
||||||
}
|
}
|
||||||
|
setUIState('default');
|
||||||
})();
|
})();
|
||||||
// loadStatuses(true);
|
// loadStatuses(true);
|
||||||
// states.homeNew = [];
|
// states.homeNew = [];
|
||||||
|
@ -101,6 +103,7 @@ function Home({ hidden }) {
|
||||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
setUIState('default');
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
26
src/pages/login.css
Normal file
26
src/pages/login.css
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
#login {
|
||||||
|
padding: 16px;
|
||||||
|
background-image: radial-gradient(
|
||||||
|
closest-side at 50% 50%,
|
||||||
|
var(--bg-color),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#login .error {
|
||||||
|
color: var(--red-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#login label p {
|
||||||
|
margin: 0 0 0.25em 0;
|
||||||
|
padding: 0;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 90%;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--text-insignificant-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#login input {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
|
@ -1,3 +1,5 @@
|
||||||
|
import './login.css';
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
|
|
||||||
import Loader from '../components/loader';
|
import Loader from '../components/loader';
|
||||||
|
@ -53,7 +55,7 @@ function Login() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main class="box" style={{ textAlign: 'center' }}>
|
<main id="login" style={{ textAlign: 'center' }}>
|
||||||
<form onSubmit={onSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
<h1>Log in</h1>
|
<h1>Log in</h1>
|
||||||
<label>
|
<label>
|
||||||
|
@ -82,8 +84,8 @@ function Login() {
|
||||||
<button class="large" disabled={uiState === 'loading'}>
|
<button class="large" disabled={uiState === 'loading'}>
|
||||||
Log in
|
Log in
|
||||||
</button>{' '}
|
</button>{' '}
|
||||||
<Loader hidden={uiState !== 'loading'} />
|
|
||||||
</p>
|
</p>
|
||||||
|
<Loader hidden={uiState !== 'loading'} />
|
||||||
<hr />
|
<hr />
|
||||||
<p>
|
<p>
|
||||||
<a href="https://joinmastodon.org/servers" target="_blank">
|
<a href="https://joinmastodon.org/servers" target="_blank">
|
||||||
|
|
|
@ -17,20 +17,20 @@
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
color: var(--text-insignificant-color);
|
color: var(--text-insignificant-color);
|
||||||
}
|
}
|
||||||
.notification-type.favourite {
|
.notification-type.notification-favourite {
|
||||||
color: var(--favourite-color);
|
color: var(--favourite-color);
|
||||||
}
|
}
|
||||||
.notification-type.reblog {
|
.notification-type.notification-reblog {
|
||||||
color: var(--reblog-color);
|
color: var(--reblog-color);
|
||||||
}
|
}
|
||||||
.notification-type.poll,
|
.notification-type.notification-poll,
|
||||||
.notification-type.mention {
|
.notification-type.notification-mention {
|
||||||
color: var(--link-color);
|
color: var(--link-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification .status-link {
|
.notification .status-link {
|
||||||
border-radius: 8px 8px 0 0;
|
border-radius: 8px 8px 0 0;
|
||||||
border: 1px solid var(--divider-color);
|
border: 1px solid var(--outline-color);
|
||||||
max-height: 160px;
|
max-height: 160px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
/* fade out mask gradient bottom */
|
/* fade out mask gradient bottom */
|
||||||
|
@ -41,9 +41,16 @@
|
||||||
);
|
);
|
||||||
filter: saturate(0.25);
|
filter: saturate(0.25);
|
||||||
}
|
}
|
||||||
|
.notification .status-link.status-type-mention {
|
||||||
|
max-height: 320px;
|
||||||
|
filter: none;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
margin-top: calc(-16px - 1px);
|
||||||
|
}
|
||||||
.notification .status-link:hover {
|
.notification .status-link:hover {
|
||||||
background-color: var(--bg-blur-color);
|
background-color: var(--bg-blur-color);
|
||||||
filter: saturate(1);
|
filter: saturate(1);
|
||||||
|
border-color: var(--outline-hover-color);
|
||||||
}
|
}
|
||||||
.notification .status-link > * {
|
.notification .status-link > * {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
|
@ -41,7 +41,7 @@ const contentText = {
|
||||||
update: 'A status you interacted with has been edited.',
|
update: 'A status you interacted with has been edited.',
|
||||||
};
|
};
|
||||||
|
|
||||||
const LIMIT = 20;
|
const LIMIT = 30; // 30 is the maximum limit :(
|
||||||
|
|
||||||
function Notification({ notification }) {
|
function Notification({ notification }) {
|
||||||
const { id, type, status, account, _accounts } = notification;
|
const { id, type, status, account, _accounts } = notification;
|
||||||
|
@ -61,7 +61,7 @@ function Notification({ notification }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
class={`notification-type ${type}`}
|
class={`notification-type notification-${type}`}
|
||||||
title={new Date(notification.createdAt).toLocaleString()}
|
title={new Date(notification.createdAt).toLocaleString()}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
|
@ -82,6 +82,7 @@ function Notification({ notification }) {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="notification-content">
|
<div class="notification-content">
|
||||||
|
{type !== 'mention' && (
|
||||||
<p>
|
<p>
|
||||||
{!/poll|update/i.test(type) && (
|
{!/poll|update/i.test(type) && (
|
||||||
<>
|
<>
|
||||||
|
@ -110,6 +111,7 @@ function Notification({ notification }) {
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
)}
|
||||||
{_accounts?.length > 1 && (
|
{_accounts?.length > 1 && (
|
||||||
<p>
|
<p>
|
||||||
{_accounts.map((account, i) => (
|
{_accounts.map((account, i) => (
|
||||||
|
@ -142,7 +144,10 @@ function Notification({ notification }) {
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{status && (
|
{status && (
|
||||||
<Link class="status-link" href={`#/s/${actualStatusID}`}>
|
<Link
|
||||||
|
class={`status-link status-type-${type}`}
|
||||||
|
href={`#/s/${actualStatusID}`}
|
||||||
|
>
|
||||||
<Status status={status} size="s" />
|
<Status status={status} size="s" />
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,10 +1,5 @@
|
||||||
#settings-container {
|
|
||||||
padding-bottom: 3em;
|
|
||||||
animation: fade-in 0.2s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
#settings-container h2 {
|
#settings-container h2 {
|
||||||
font-size: .9em;
|
font-size: 0.9em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--text-insignificant-color);
|
color: var(--text-insignificant-color);
|
||||||
}
|
}
|
||||||
|
@ -63,7 +58,7 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 1px;
|
padding: 1px;
|
||||||
}
|
}
|
||||||
#settings-container .radio-group input[type="radio"] {
|
#settings-container .radio-group input[type='radio'] {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
|
@ -23,10 +23,10 @@ function Settings({ onClose }) {
|
||||||
const [currentDefault, setCurrentDefault] = useState(0);
|
const [currentDefault, setCurrentDefault] = useState(0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="settings-container" class="box">
|
<div id="settings-container" class="sheet">
|
||||||
<button type="button" class="close-button plain large" onClick={onClose}>
|
{/* <button type="button" class="close-button plain large" onClick={onClose}>
|
||||||
<Icon icon="x" alt="Close" />
|
<Icon icon="x" alt="Close" />
|
||||||
</button>
|
</button> */}
|
||||||
<h2>Accounts</h2>
|
<h2>Accounts</h2>
|
||||||
<ul class="accounts-list">
|
<ul class="accounts-list">
|
||||||
{accounts.map((account, i) => {
|
{accounts.map((account, i) => {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import debounce from 'just-debounce-it';
|
||||||
import { Link } from 'preact-router/match';
|
import { Link } from 'preact-router/match';
|
||||||
import {
|
import {
|
||||||
useEffect,
|
useEffect,
|
||||||
|
@ -13,34 +14,57 @@ import Loader from '../components/loader';
|
||||||
import Status from '../components/status';
|
import Status from '../components/status';
|
||||||
import shortenNumber from '../utils/shorten-number';
|
import shortenNumber from '../utils/shorten-number';
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
|
import store from '../utils/store';
|
||||||
import useTitle from '../utils/useTitle';
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
function StatusPage({ id }) {
|
function StatusPage({ id }) {
|
||||||
const snapStates = useSnapshot(states);
|
const snapStates = useSnapshot(states);
|
||||||
const [statuses, setStatuses] = useState([{ id }]);
|
const [statuses, setStatuses] = useState([]);
|
||||||
const [uiState, setUIState] = useState('default');
|
const [uiState, setUIState] = useState('default');
|
||||||
|
const userInitiated = useRef(true); // Initial open is user-initiated
|
||||||
const heroStatusRef = useRef();
|
const heroStatusRef = useRef();
|
||||||
|
|
||||||
useEffect(async () => {
|
const scrollableRef = useRef();
|
||||||
const containsStatus = statuses.find((s) => s.id === id);
|
useEffect(() => {
|
||||||
const statusesWithSameAccountID = statuses.filter(
|
const onScroll = debounce(() => {
|
||||||
(s) => s.accountID === containsStatus?.accountID,
|
// console.log('onScroll');
|
||||||
);
|
const { scrollTop } = scrollableRef.current;
|
||||||
if (statusesWithSameAccountID.length > 1) {
|
states.scrollPositions.set(id, scrollTop);
|
||||||
setStatuses(
|
}, 100);
|
||||||
statusesWithSameAccountID.map((s) => ({
|
scrollableRef.current.addEventListener('scroll', onScroll, {
|
||||||
...s,
|
passive: true,
|
||||||
thread: true,
|
});
|
||||||
descendant: undefined,
|
onScroll();
|
||||||
ancestor: undefined,
|
return () => {
|
||||||
})),
|
scrollableRef.current?.removeEventListener('scroll', onScroll);
|
||||||
);
|
};
|
||||||
} else {
|
}, [id]);
|
||||||
setStatuses([{ id }]);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
setUIState('loading');
|
setUIState('loading');
|
||||||
|
|
||||||
|
const containsStatus = statuses.find((s) => s.id === id);
|
||||||
|
if (!containsStatus) {
|
||||||
|
// Case 1: On first load, or when navigating to a status that's not cached at all
|
||||||
|
setStatuses([{ id }]);
|
||||||
|
} else {
|
||||||
|
const cachedStatuses = store.session.getJSON('statuses-' + id);
|
||||||
|
if (cachedStatuses) {
|
||||||
|
// Case 2: Looks like we've cached this status before, let's restore them to make it snappy
|
||||||
|
const reallyCachedStatuses = cachedStatuses.filter(
|
||||||
|
(s) => snapStates.statuses.has(s.id),
|
||||||
|
// Some are not cached in the global state, so we need to filter them out
|
||||||
|
);
|
||||||
|
setStatuses(reallyCachedStatuses);
|
||||||
|
} else {
|
||||||
|
// Case 3: Unknown state, could be a sub-comment. Let's slice off all descendant statuses after the hero status to be safe because they are custom-rendered with sub-comments etc
|
||||||
|
const heroIndex = statuses.findIndex((s) => s.id === id);
|
||||||
|
const slicedStatuses = statuses.slice(0, heroIndex + 1);
|
||||||
|
setStatuses(slicedStatuses);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
const hasStatus = snapStates.statuses.has(id);
|
const hasStatus = snapStates.statuses.has(id);
|
||||||
let heroStatus = snapStates.statuses.get(id);
|
let heroStatus = snapStates.statuses.get(id);
|
||||||
try {
|
try {
|
||||||
|
@ -103,34 +127,55 @@ function StatusPage({ id }) {
|
||||||
replies: s.__replies?.map((r) => r.id),
|
replies: s.__replies?.map((r) => r.id),
|
||||||
})),
|
})),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
setUIState('default');
|
||||||
console.log({ allStatuses });
|
console.log({ allStatuses });
|
||||||
setStatuses(allStatuses);
|
setStatuses(allStatuses);
|
||||||
|
store.session.setJSON('statuses-' + id, allStatuses);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
setUIState('error');
|
setUIState('error');
|
||||||
}
|
}
|
||||||
|
})();
|
||||||
setUIState('default');
|
|
||||||
}, [id, snapStates.reloadStatusPage]);
|
}, [id, snapStates.reloadStatusPage]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (heroStatusRef.current && statuses.length > 1) {
|
if (!statuses.length) return;
|
||||||
heroStatusRef.current.scrollIntoView({
|
const isLoading = uiState === 'loading';
|
||||||
|
if (userInitiated.current) {
|
||||||
|
const hasAncestors = statuses.findIndex((s) => s.id === id) > 0; // Cannot use `ancestor` key because the hero state is dynamic
|
||||||
|
if (!isLoading && hasAncestors) {
|
||||||
|
// Case 1: User initiated, has ancestors, after statuses are loaded, SNAP to hero status
|
||||||
|
console.log('Case 1');
|
||||||
|
heroStatusRef.current?.scrollIntoView();
|
||||||
|
} else if (isLoading && statuses.length > 1) {
|
||||||
|
// Case 2: User initiated, while statuses are loading, SMOOTH-SCROLL to hero status
|
||||||
|
console.log('Case 2');
|
||||||
|
heroStatusRef.current?.scrollIntoView({
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
block: 'start',
|
block: 'start',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [id]);
|
} else {
|
||||||
|
const scrollPosition = states.scrollPositions.get(id);
|
||||||
useLayoutEffect(() => {
|
if (scrollPosition && scrollableRef.current) {
|
||||||
const hasAncestor = statuses.some((s) => s.ancestor);
|
// Case 3: Not user initiated (e.g. back/forward button), restore to saved scroll position
|
||||||
if (hasAncestor) {
|
console.log('Case 3');
|
||||||
heroStatusRef.current?.scrollIntoView({
|
scrollableRef.current.scrollTop = scrollPosition;
|
||||||
// behavior: 'smooth',
|
|
||||||
block: 'start',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, [statuses]);
|
}
|
||||||
|
console.log('No case', {
|
||||||
|
isLoading,
|
||||||
|
userInitiated: userInitiated.current,
|
||||||
|
statusesLength: statuses.length,
|
||||||
|
// scrollPosition,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isLoading) {
|
||||||
|
// Reset user initiated flag after statuses are loaded
|
||||||
|
userInitiated.current = false;
|
||||||
|
}
|
||||||
|
}, [statuses, uiState]);
|
||||||
|
|
||||||
const heroStatus = snapStates.statuses.get(id);
|
const heroStatus = snapStates.statuses.get(id);
|
||||||
const heroDisplayName = useMemo(() => {
|
const heroDisplayName = useMemo(() => {
|
||||||
|
@ -177,11 +222,13 @@ function StatusPage({ id }) {
|
||||||
}, [statuses.length, limit]);
|
}, [statuses.length, limit]);
|
||||||
|
|
||||||
const hasManyStatuses = statuses.length > 40;
|
const hasManyStatuses = statuses.length > 40;
|
||||||
|
const hasDescendants = statuses.some((s) => s.descendant);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="deck-backdrop">
|
<div class="deck-backdrop">
|
||||||
<Link href={closeLink}></Link>
|
<Link href={closeLink}></Link>
|
||||||
<div
|
<div
|
||||||
|
ref={scrollableRef}
|
||||||
class={`status-deck deck contained ${
|
class={`status-deck deck contained ${
|
||||||
statuses.length > 1 ? 'padded-bottom' : ''
|
statuses.length > 1 ? 'padded-bottom' : ''
|
||||||
}`}
|
}`}
|
||||||
|
@ -200,7 +247,11 @@ function StatusPage({ id }) {
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<ul class="timeline flat contextual">
|
<ul
|
||||||
|
class={`timeline flat contextual ${
|
||||||
|
uiState === 'loading' ? 'loading' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{statuses.slice(0, limit).map((status) => {
|
{statuses.slice(0, limit).map((status) => {
|
||||||
const {
|
const {
|
||||||
id: statusID,
|
id: statusID,
|
||||||
|
@ -216,7 +267,7 @@ function StatusPage({ id }) {
|
||||||
ref={isHero ? heroStatusRef : null}
|
ref={isHero ? heroStatusRef : null}
|
||||||
class={`${ancestor ? 'ancestor' : ''} ${
|
class={`${ancestor ? 'ancestor' : ''} ${
|
||||||
descendant ? 'descendant' : ''
|
descendant ? 'descendant' : ''
|
||||||
} ${thread ? 'thread' : ''}`}
|
} ${thread ? 'thread' : ''} ${isHero ? 'hero' : ''}`}
|
||||||
>
|
>
|
||||||
{isHero ? (
|
{isHero ? (
|
||||||
<Status statusID={statusID} withinContext size="l" />
|
<Status statusID={statusID} withinContext size="l" />
|
||||||
|
@ -226,6 +277,9 @@ function StatusPage({ id }) {
|
||||||
status-link
|
status-link
|
||||||
"
|
"
|
||||||
href={`#/s/${statusID}`}
|
href={`#/s/${statusID}`}
|
||||||
|
onClick={() => {
|
||||||
|
userInitiated.current = true;
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Status
|
<Status
|
||||||
statusID={statusID}
|
statusID={statusID}
|
||||||
|
@ -245,7 +299,13 @@ function StatusPage({ id }) {
|
||||||
<ul>
|
<ul>
|
||||||
{replies.map((replyID) => (
|
{replies.map((replyID) => (
|
||||||
<li key={replyID}>
|
<li key={replyID}>
|
||||||
<Link class="status-link" href={`#/s/${replyID}`}>
|
<Link
|
||||||
|
class="status-link"
|
||||||
|
href={`#/s/${replyID}`}
|
||||||
|
onClick={() => {
|
||||||
|
userInitiated.current = true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Status statusID={replyID} withinContext size="s" />
|
<Status statusID={replyID} withinContext size="s" />
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
|
@ -256,7 +316,7 @@ function StatusPage({ id }) {
|
||||||
{uiState === 'loading' &&
|
{uiState === 'loading' &&
|
||||||
isHero &&
|
isHero &&
|
||||||
!!heroStatus?.repliesCount &&
|
!!heroStatus?.repliesCount &&
|
||||||
statuses.length === 1 && (
|
!hasDescendants && (
|
||||||
<div class="status-loading">
|
<div class="status-loading">
|
||||||
<Loader />
|
<Loader />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,39 +1,41 @@
|
||||||
#welcome {
|
#welcome {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
background-image: radial-gradient(
|
||||||
|
closest-side at 50% 50%,
|
||||||
|
var(--bg-faded-color),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#welcome h1 {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
#welcome img {
|
#welcome img {
|
||||||
margin-top: 16px;
|
vertical-align: top;
|
||||||
height: auto;
|
|
||||||
}
|
}
|
||||||
@keyframes dance {
|
|
||||||
|
#welcome h2 {
|
||||||
|
font-size: 3em;
|
||||||
|
letter-spacing: -0.05ex;
|
||||||
|
margin: 16px 0;
|
||||||
|
padding: 0;
|
||||||
|
/* gradiented text */
|
||||||
|
background: linear-gradient(45deg, var(--purple-color), var(--red-color));
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes psychedelic {
|
||||||
0% {
|
0% {
|
||||||
transform: rotate(0deg);
|
filter: hue-rotate(0deg);
|
||||||
}
|
|
||||||
20% {
|
|
||||||
transform: rotate(5deg);
|
|
||||||
}
|
|
||||||
40% {
|
|
||||||
transform: rotate(-5deg);
|
|
||||||
}
|
|
||||||
60% {
|
|
||||||
transform: rotate(5deg);
|
|
||||||
}
|
|
||||||
80% {
|
|
||||||
transform: rotate(-5deg);
|
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
transform: rotate(0deg);
|
filter: hue-rotate(360deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#welcome:hover img {
|
#welcome:hover h2 {
|
||||||
animation: dance 2s infinite 15s linear;
|
animation: psychedelic 60s infinite;
|
||||||
}
|
|
||||||
|
|
||||||
#welcome .warning {
|
|
||||||
font-weight: bold;
|
|
||||||
padding: 16px;
|
|
||||||
background: lemonchiffon;
|
|
||||||
color: chocolate;
|
|
||||||
border-radius: 16px;
|
|
||||||
}
|
}
|
|
@ -6,22 +6,25 @@ import useTitle from '../utils/useTitle';
|
||||||
function Welcome() {
|
function Welcome() {
|
||||||
useTitle();
|
useTitle();
|
||||||
return (
|
return (
|
||||||
<main id="welcome" class="box">
|
<main id="welcome">
|
||||||
|
<h1>
|
||||||
<img
|
<img
|
||||||
src={logo}
|
src={logo}
|
||||||
alt=""
|
alt=""
|
||||||
width="140"
|
width="24"
|
||||||
height="140"
|
height="24"
|
||||||
style={{
|
style={{
|
||||||
aspectRatio: '1/1',
|
aspectRatio: '1/1',
|
||||||
}}
|
}}
|
||||||
/>
|
/>{' '}
|
||||||
<h1>Welcome</h1>
|
Phanpy
|
||||||
<p>Phanpy is a minimalistic opinionated Mastodon web client.</p>
|
</h1>
|
||||||
<p class="warning">
|
<h2>
|
||||||
🚧 This is an early ALPHA project. Many features are missing, many bugs
|
Trunk-tastic
|
||||||
are present. Please report issues as detailed as possible. Thanks 🙏
|
<br />
|
||||||
</p>
|
Mastodon Experience
|
||||||
|
</h2>
|
||||||
|
<p>A minimalistic opinionated Mastodon web client.</p>
|
||||||
<p>
|
<p>
|
||||||
<big>
|
<big>
|
||||||
<b>
|
<b>
|
||||||
|
|
|
@ -10,8 +10,10 @@ export default proxy({
|
||||||
notifications: [],
|
notifications: [],
|
||||||
notificationsNew: [],
|
notificationsNew: [],
|
||||||
notificationsLastFetchTime: null,
|
notificationsLastFetchTime: null,
|
||||||
accounts: new WeakMap(),
|
accounts: new Map(),
|
||||||
reloadStatusPage: 0,
|
reloadStatusPage: 0,
|
||||||
|
spoilers: proxyMap([]),
|
||||||
|
scrollPositions: new Map(),
|
||||||
// Modals
|
// Modals
|
||||||
showCompose: false,
|
showCompose: false,
|
||||||
showSettings: false,
|
showSettings: false,
|
||||||
|
|
|
@ -35,6 +35,12 @@ export default defineConfig({
|
||||||
sizes: '512x512',
|
sizes: '512x512',
|
||||||
type: 'image/png',
|
type: 'image/png',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
src: 'logo-maskable-512.png',
|
||||||
|
sizes: '512x512',
|
||||||
|
type: 'image/png',
|
||||||
|
purpose: 'maskable',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
strategies: 'injectManifest',
|
strategies: 'injectManifest',
|
||||||
|
|
Loading…
Reference in a new issue