diff --git a/src/app.css b/src/app.css
index 6aa65765..2dbdc43c 100644
--- a/src/app.css
+++ b/src/app.css
@@ -103,6 +103,10 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
max-width: 100%;
background-color: var(--bg-color);
overflow-anchor: auto;
+
+ &.wide {
+ width: 60em;
+ }
}
.deck.contained {
overflow: auto;
diff --git a/src/app.jsx b/src/app.jsx
index b9a55979..aa9d1ee5 100644
--- a/src/app.jsx
+++ b/src/app.jsx
@@ -24,6 +24,7 @@ import Shortcuts from './components/shortcuts';
import NotFound from './pages/404';
import AccountStatuses from './pages/account-statuses';
import Bookmarks from './pages/bookmarks';
+import Catchup from './pages/catchup';
import Favourites from './pages/favourites';
import FollowedHashtags from './pages/followed-hashtags';
import Following from './pages/following';
@@ -394,7 +395,7 @@ function PrimaryRoutes({ isLoggedIn, loading }) {
const location = useLocation();
const nonRootLocation = useMemo(() => {
const { pathname } = location;
- return !/^\/(login|welcome)/.test(pathname);
+ return !/^\/(login|welcome)/i.test(pathname);
}, [location]);
return (
@@ -457,6 +458,7 @@ function SecondaryRoutes({ isLoggedIn }) {
} />
} />
+ } />
>
)}
} />
diff --git a/src/cloak-mode.css b/src/cloak-mode.css
index c9e03051..cf5bf757 100644
--- a/src/cloak-mode.css
+++ b/src/cloak-mode.css
@@ -12,7 +12,8 @@ body.cloak,
.account-container :is(header, main > *:not(.actions)),
.account-container :is(header, main > *:not(.actions)) *,
.header-double-lines,
- .account-block {
+ .account-block,
+ .post-peek-html * {
text-decoration-thickness: 1.1em;
text-decoration-line: line-through;
/* text-rendering: optimizeSpeed; */
@@ -26,9 +27,10 @@ body.cloak,
.status :is(img, video, audio),
.media-post .media,
- .avatar,
+ .avatar *,
.emoji,
- .header-banner {
+ .header-banner,
+ .post-peek-media {
filter: contrast(0) !important;
background-color: #000 !important;
}
diff --git a/src/pages/trending.css b/src/components/links-bar.css
similarity index 78%
rename from src/pages/trending.css
rename to src/components/links-bar.css
index 31b0ed8c..f9668c89 100644
--- a/src/pages/trending.css
+++ b/src/components/links-bar.css
@@ -15,7 +15,7 @@
text-shadow: 0 1px var(--bg-blur-color);
transition: opacity 0.3s ease-out;
- &:not(#columns &) {
+ #trending-page &:not(#columns &) {
@media (min-width: 40em) {
width: 95vw;
max-width: calc(320px * 3.3);
@@ -96,6 +96,7 @@
}
article {
+ width: 100%;
display: flex;
flex-direction: column;
justify-content: flex-end;
@@ -113,34 +114,34 @@
margin: 0 0 -16px;
padding: 0;
position: relative;
- }
- img {
- position: absolute;
- inset: 0;
- width: 100%;
- height: 100%;
- object-fit: cover;
- vertical-align: top;
- mask-image: linear-gradient(
- to bottom,
- hsl(0, 0%, 0%) 0%,
- hsla(0, 0%, 0%, 0.987) 14%,
- hsla(0, 0%, 0%, 0.951) 26.2%,
- hsla(0, 0%, 0%, 0.896) 36.8%,
- hsla(0, 0%, 0%, 0.825) 45.9%,
- hsla(0, 0%, 0%, 0.741) 53.7%,
- hsla(0, 0%, 0%, 0.648) 60.4%,
- hsla(0, 0%, 0%, 0.55) 66.2%,
- hsla(0, 0%, 0%, 0.45) 71.2%,
- hsla(0, 0%, 0%, 0.352) 75.6%,
- hsla(0, 0%, 0%, 0.259) 79.6%,
- hsla(0, 0%, 0%, 0.175) 83.4%,
- hsla(0, 0%, 0%, 0.104) 87.2%,
- hsla(0, 0%, 0%, 0.049) 91.1%,
- hsla(0, 0%, 0%, 0.013) 95.3%,
- hsla(0, 0%, 0%, 0) 100%
- );
+ img {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ vertical-align: top;
+ mask-image: linear-gradient(
+ to bottom,
+ hsl(0, 0%, 0%) 0%,
+ hsla(0, 0%, 0%, 0.987) 14%,
+ hsla(0, 0%, 0%, 0.951) 26.2%,
+ hsla(0, 0%, 0%, 0.896) 36.8%,
+ hsla(0, 0%, 0%, 0.825) 45.9%,
+ hsla(0, 0%, 0%, 0.741) 53.7%,
+ hsla(0, 0%, 0%, 0.648) 60.4%,
+ hsla(0, 0%, 0%, 0.55) 66.2%,
+ hsla(0, 0%, 0%, 0.45) 71.2%,
+ hsla(0, 0%, 0%, 0.352) 75.6%,
+ hsla(0, 0%, 0%, 0.259) 79.6%,
+ hsla(0, 0%, 0%, 0.175) 83.4%,
+ hsla(0, 0%, 0%, 0.104) 87.2%,
+ hsla(0, 0%, 0%, 0.049) 91.1%,
+ hsla(0, 0%, 0%, 0.013) 95.3%,
+ hsla(0, 0%, 0%, 0) 100%
+ );
+ }
}
}
@@ -187,5 +188,9 @@
overflow: hidden;
font-size: 90%;
}
+
+ hr {
+ margin: 4px 0;
+ }
}
}
diff --git a/src/components/nav-menu.jsx b/src/components/nav-menu.jsx
index 053cb010..ff11a6bb 100644
--- a/src/components/nav-menu.jsx
+++ b/src/components/nav-menu.jsx
@@ -176,6 +176,10 @@ function NavMenu(props) {
Following
)}
+
+
+ Catch-up
+
Mentions
diff --git a/src/pages/catchup.css b/src/pages/catchup.css
new file mode 100644
index 00000000..30ce0096
--- /dev/null
+++ b/src/pages/catchup.css
@@ -0,0 +1,832 @@
+#catchup-page {
+ transform: none;
+ padding-bottom: 0 !important;
+
+ .deck {
+ background-color: var(--bg-faded-color);
+ }
+
+ /* Hide the shortcuts nav + adjustments */
+ ~ :is(#shortcuts, #compose-button) {
+ display: none;
+ }
+ .timeline-deck {
+ margin-top: 0 !important;
+ }
+ header {
+ /* --margin-top: 8px !important; */
+ position: static;
+ }
+
+ h1 sup {
+ font-size: 12px;
+ text-transform: uppercase;
+ font-weight: 500;
+ color: var(--text-insignificant-color);
+ }
+
+ .catchup-start {
+ padding: 16px;
+ padding-top: 15vh;
+ text-align: center;
+ max-width: 40em;
+ margin-inline: auto;
+
+ .catchup-info {
+ animation: appear 0.3s ease-out;
+ display: flex;
+ gap: 0.25em;
+ align-items: center;
+ justify-content: center;
+ }
+ }
+
+ .catchup-prev {
+ margin: 2em auto;
+ padding: 1em;
+ width: fit-content;
+ color: var(--text-insignificant-color);
+ border-top: 1px solid var(--bg-color);
+
+ ul,
+ ul li {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ }
+
+ ul li {
+ display: flex;
+ margin-bottom: 8px;
+ align-items: center;
+ gap: 8px;
+ text-align: left;
+ justify-content: space-between;
+
+ a {
+ display: flex;
+ gap: 0.25em;
+ align-items: center;
+ }
+ }
+ }
+}
+
+.catchup-form {
+ display: inline-flex;
+ align-items: center;
+ gap: 16px;
+ padding: 16px 16px;
+ background-color: var(--link-bg-color);
+ border-radius: 32px;
+ flex-wrap: wrap;
+
+ * {
+ flex-grow: 1;
+ }
+
+ input[type='range'] {
+ accent-color: var(--link-color);
+ direction: rtl;
+ }
+}
+
+.catchup-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 16px;
+ gap: 8px;
+
+ aside {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 90%;
+
+ button[hidden] {
+ display: inline;
+ opacity: 0;
+ pointer-events: none;
+ }
+ }
+}
+
+.catchup-posts-viz-bar {
+ margin: 0 16px;
+ border-radius: 3px;
+ border: 1px solid var(--bg-color);
+ display: flex;
+ gap: 1px;
+ pointer-events: none;
+ justify-content: stretch;
+ height: 3px;
+
+ &:has(.post-dot:nth-child(320)) {
+ gap: 0;
+ }
+
+ .post-dot {
+ display: block;
+ width: 100%;
+ height: 3px;
+ border-radius: 3px;
+ opacity: 0.5;
+ background-color: var(--link-color);
+ transition: 0.25s ease-in-out;
+ transition-property: opacity, transform;
+
+ &.post-dot-highlight {
+ opacity: 1;
+ }
+ }
+
+ &:has(.post-dot:not(.post-dot-highlight)) .post-dot-highlight {
+ /* transform: scaleY(2); */
+ transform: scale3d(1, 2, 1);
+ }
+}
+
+.catchup-filters {
+ padding: 8px 16px;
+ display: flex;
+ /* flex-wrap: wrap; */
+ gap: 8px;
+ align-items: center;
+ overflow-x: auto;
+ overflow-y: hidden;
+ max-width: 100%;
+ mask-image: linear-gradient(
+ to right,
+ transparent,
+ black 16px calc(100% - 16px),
+ transparent
+ );
+ padding-inline-end: 25%;
+
+ .filter-label {
+ text-transform: uppercase;
+ font-size: 12px;
+ font-weight: 500;
+ color: var(--text-insignificant-color);
+ }
+
+ label {
+ font-size: 80%;
+ white-space: nowrap;
+ cursor: pointer;
+ user-select: none;
+ color: var(--text-insignificant-color);
+ position: relative;
+
+ select {
+ /* appearance: none;
+ background-color: var(--bg-faded-color);
+ color: var(--link-color);
+ border: 0;
+ border-radius: 12px; */
+ padding: 4px;
+ margin: 0;
+ }
+
+ input[type] {
+ left: 0;
+ position: absolute;
+ opacity: 0;
+ pointer-events: none;
+ }
+
+ &.filter-cat {
+ padding: 8px 12px;
+ background-color: var(--bg-blur-color);
+ border-radius: 24px;
+ display: inline-block;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+
+ &:is(:hover, :focus) {
+ background-color: var(--link-bg-color);
+ }
+
+ &:has(:checked) {
+ color: var(--link-color);
+ background-color: var(--link-bg-color);
+ box-shadow: inset 0 0 0 2px var(--link-color);
+ }
+
+ .count {
+ font-size: 70%;
+ margin-left: 4px;
+ background-color: var(--bg-color);
+ padding: 4px 6px;
+ border-radius: 12px;
+ display: inline-block;
+ }
+ }
+
+ &.filter-author {
+ flex-direction: column;
+ padding: 0;
+ background-color: transparent;
+ position: relative;
+ width: 50px;
+ /* transition: filter 0.15s ease; */
+
+ .avatar {
+ margin-bottom: 2px;
+ /* transition: box-shadow 0.15s ease; */
+ }
+
+ &:is(:hover, :focus) {
+ filter: none;
+
+ .avatar {
+ box-shadow: 0 0 0 0.5px var(--bg-color),
+ 0 0 0 3px var(--link-faded-color) !important;
+
+ img {
+ filter: none;
+ }
+ }
+
+ .count {
+ color: var(--text-color);
+ }
+ }
+
+ .avatar {
+ &.has-alpha {
+ border-radius: 2px;
+ }
+
+ img {
+ transition: filter 0.15s ease;
+ }
+ }
+
+ &:has(:checked) {
+ box-shadow: none;
+ filter: none;
+
+ .avatar {
+ box-shadow: 0 0 0 1px var(--bg-color), 0 0 0 3px var(--link-color);
+ }
+
+ .username {
+ color: var(--link-color);
+ font-weight: 500;
+ }
+
+ .count {
+ color: var(--link-color);
+ border-color: var(--link-color);
+ box-shadow: 0 0 0 1px var(--link-color);
+ }
+ }
+
+ .count {
+ position: absolute;
+ right: -4px;
+ top: -4px;
+ font-size: 10px;
+ background-color: var(--bg-color);
+ border-radius: 12px;
+ display: inline-block;
+ border: 1px solid var(--link-faded-color);
+ padding: 0 2px;
+ min-width: 16px;
+ min-height: 16px;
+ text-align: center;
+ line-height: 14px;
+ }
+
+ .username {
+ display: block;
+ width: 100%;
+ overflow: hidden;
+ text-align: center;
+ mask-image: linear-gradient(
+ to right,
+ black calc(100% - 0.5em),
+ transparent 100%
+ );
+ }
+ }
+ }
+
+ &:has(.filter-author :checked)
+ .filter-author:not(:has(:checked)):not(:is(:hover, :focus)) {
+ .avatar img {
+ filter: grayscale(1) contrast(2) opacity(0.5);
+ }
+ }
+
+ .radio-field-group {
+ display: flex;
+ border: 0;
+ padding: 0;
+ margin: 0;
+ border-radius: 4px;
+ overflow: hidden;
+ gap: 1px;
+
+ label {
+ padding: 4px 8px;
+ line-height: 2em;
+ min-width: 32px;
+ text-align: center;
+ background-color: var(--bg-blur-color);
+ margin: 0;
+ cursor: pointer;
+
+ &:is(:hover, :focus) {
+ background-color: var(--link-bg-color);
+ }
+
+ &:has(:checked) {
+ font-weight: 500;
+ color: var(--text-color);
+ background-color: var(--link-bg-color);
+ box-shadow: inset 0 -2px 0 var(--link-color);
+ }
+
+ &:has(:disabled) {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+ }
+ }
+}
+
+.catchup-list {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ /* background-color: var(--bg-color); */
+ border-top: var(--hairline-width) solid var(--bg-faded-color);
+ --corner-radius: 8px;
+
+ @media (min-width: 40em) {
+ border-radius: var(--corner-radius);
+ /* border: var(--hairline-width) solid var(--outline-color); */
+
+ > li {
+ &:first-child > a {
+ border-top-left-radius: var(--corner-radius);
+ border-top-right-radius: var(--corner-radius);
+ }
+
+ &:last-child > a {
+ border-bottom-left-radius: var(--corner-radius);
+ border-bottom-right-radius: var(--corner-radius);
+ }
+ }
+ }
+
+ > li {
+ margin: 0 0 1px;
+ padding: 0;
+ list-style: none;
+ /* border-bottom: var(--hairline-width) solid var(--outline-color); */
+
+ &.separator {
+ height: 16px;
+ pointer-events: none;
+
+ @media (min-width: 40em) {
+ height: 32px;
+ }
+ }
+
+ @media (min-width: 40em) {
+ &.separator + li a {
+ border-top-left-radius: var(--corner-radius);
+ border-top-right-radius: var(--corner-radius);
+ }
+
+ &:has(+ .separator) a {
+ border-bottom-left-radius: var(--corner-radius);
+ border-bottom-right-radius: var(--corner-radius);
+ }
+ }
+
+ > a {
+ background-color: var(--bg-color);
+ text-decoration: none;
+ color: inherit;
+ display: block;
+ /* transition: background-color 0.3s ease; */
+
+ &:is(:hover, :focus) {
+ position: relative;
+ z-index: 1;
+ background-color: var(--bg-faded-color);
+ box-shadow: 0 8px 16px -8px var(--drop-shadow-color),
+ inset 0 1px var(--bg-color);
+ outline: 1px solid var(--outline-color);
+ text-shadow: 0 1px var(--bg-color);
+ }
+
+ &:active {
+ filter: brightness(0.95);
+ box-shadow: none;
+ text-shadow: none;
+ }
+
+ &:visited {
+ color: var(--outline-color);
+
+ *,
+ .post-peek-html * a[href] {
+ color: var(--outline-color) !important;
+
+ * {
+ color: var(--outline-color) !important;
+ }
+ }
+ }
+ }
+ }
+
+ .post-line {
+ border-radius: inherit;
+ animation: appear-smooth 0.3s ease-in-out both;
+ --pad: 16px;
+ min-height: 44px;
+ padding: var(--pad);
+ column-gap: calc(0.5 * var(--pad));
+ row-gap: 4px;
+ width: 100%;
+ /* display: flex;
+ flex-direction: column; */
+ display: grid;
+ grid-template-columns: 1fr auto;
+ grid-template-rows: auto auto;
+ grid-template-areas:
+ 'author meta'
+ 'content content';
+ /* align-items: center; */
+ background-image: linear-gradient(
+ 160deg,
+ var(--post-bg-color),
+ transparent min(80px, 50%)
+ );
+ /* background-image: linear-gradient(
+ 90deg,
+ var(--post-bg-color),
+ var(--post-bg-color) 8px,
+ transparent 8px
+ ); */
+
+ @media (min-width: 40em) {
+ /* flex-direction: row;
+ align-items: center; */
+ grid-template-columns: auto 1fr auto;
+ grid-template-rows: 1fr;
+ grid-template-areas: 'author content meta';
+ }
+
+ &.reblog {
+ --post-bg-color: var(--reblog-faded-color);
+ }
+ &.group {
+ --post-bg-color: var(--group-faded-color);
+ }
+ &.reply-to {
+ --post-bg-color: var(--reply-to-faded-color);
+ }
+ &.followed-tags {
+ --post-bg-color: var(--hashtag-faded-color);
+ }
+ &.filtered {
+ filter: grayscale(1);
+ background-image: none;
+
+ .post-author {
+ opacity: 0.5;
+ }
+ }
+ &.visibility-direct {
+ --yellow-stripes: repeating-linear-gradient(
+ -45deg,
+ var(--reply-to-faded-color),
+ var(--reply-to-faded-color) 10px,
+ var(--reply-to-faded-color) 10px,
+ transparent 10px,
+ transparent 20px
+ );
+ background-image: var(--yellow-stripes);
+ }
+
+ .post-reblog-avatar {
+ display: flex;
+ gap: 4px;
+ align-items: center;
+ flex-shrink: 0;
+
+ .icon {
+ color: var(--reblog-color);
+ }
+ }
+
+ .post-author {
+ grid-area: author;
+ flex-shrink: 0;
+ white-space: nowrap;
+ overflow: hidden;
+ mask-image: linear-gradient(
+ to right,
+ black calc(100% - 1em),
+ transparent 100%
+ );
+
+ @media (min-width: 40em) {
+ --width: 25vw;
+ width: var(--width);
+ min-width: 9em;
+ max-width: 13em;
+ }
+
+ b {
+ font-weight: normal;
+ /* font-weight: 500; */
+ opacity: 0.7;
+ }
+ i {
+ opacity: 0.5;
+ }
+ }
+ }
+
+ > li:nth-child(10) ~ li .post-line {
+ animation: none;
+ }
+
+ .post-peek {
+ grid-area: content;
+ display: flex;
+ flex: 1;
+ column-gap: 8px;
+ row-gap: 4px;
+ align-self: stretch;
+ /* align-items: center; */
+ /* margin-left: 24px; */
+ flex-direction: row-reverse;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+
+ /* CLOAK - uncomment when taking screenshots */
+ /* text-decoration-thickness: 1.1em;
+ text-decoration-line: line-through;
+ text-rendering: optimizeSpeed;
+ filter: opacity(0.5);
+ pointer-events: none;
+ img {
+ filter: contrast(0) !important;
+ background-color: #000 !important;
+ } */
+
+ .post-peek-content {
+ flex-shrink: 1;
+ flex-grow: 1;
+ flex-basis: 20em;
+ display: -webkit-box;
+ -webkit-line-clamp: 3;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ line-height: 1.3;
+ opacity: 0.9;
+ /* font-size: 0.9em; */
+ text-wrap: balance;
+
+ &:empty {
+ display: none;
+ }
+
+ .post-peek-html {
+ pointer-events: none;
+
+ * {
+ margin: 0;
+ padding: 0;
+ display: inline;
+ white-space: normal;
+ }
+
+ pre,
+ code {
+ font-size: 0.9em;
+ color: var(--green-color);
+ }
+
+ strong,
+ b {
+ font-weight: 500;
+ }
+
+ br {
+ content: ' ';
+ }
+
+ /* all block level elements */
+ p,
+ div,
+ blockquote,
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6,
+ li,
+ pre,
+ br {
+ &:after {
+ content: ' ';
+ }
+ }
+
+ br:after {
+ font-size: 0.75em;
+ content: ' ↵ ';
+ opacity: 0.35;
+ }
+
+ .ellipsis:after {
+ content: '...';
+ }
+
+ .invisible {
+ display: none;
+ }
+
+ /* Links are not clickable, so remove the underlines */
+ a {
+ text-decoration: none;
+ text-decoration-color: transparent;
+ color: var(--link-text-color);
+ }
+ }
+
+ .post-peek-spoiler {
+ line-height: 1.5;
+ border-radius: 1em;
+ padding-inline: 0.5em;
+ border: 1px dashed var(--button-bg-color);
+
+ .icon {
+ vertical-align: middle;
+ color: var(--button-bg-color);
+ }
+ }
+
+ .post-peek-filtered {
+ border-radius: 1em;
+ padding-inline: 0.5em;
+ border: 1px dashed var(--outline-hover-color);
+
+ .icon {
+ vertical-align: middle;
+ color: var(--outline-hover-color);
+ }
+ }
+ }
+
+ .post-peek-post-content {
+ flex-shrink: 0;
+ display: flex;
+ gap: 4px;
+ align-self: center;
+ transition: transform 0.05s ease-in-out;
+
+ &:empty {
+ display: none;
+ }
+
+ &:has(.post-peek-media):hover {
+ transform: scale(1.5);
+ }
+
+ img {
+ border-radius: 2px;
+ outline: var(--hairline-width) solid var(--outline-color);
+ vertical-align: middle;
+ object-fit: cover;
+ box-shadow: 0 0 0 1px var(--outline-color);
+ object-fit: cover;
+ transition: transform 0.05s ease-in-out;
+ background-color: var(--bg-color);
+
+ &:hover {
+ transform: scale(1.5);
+ position: relative;
+ z-index: 1;
+ animation: position-object 5s ease-in-out 5;
+
+ /* @media (min-width: 40em) and (min-height: 600px) {
+ transform: scale(3);
+ } */
+ }
+ }
+
+ @media (max-width: 40em) {
+ &:has(.post-peek-media),
+ .post-peek-media:first-child img {
+ transform-origin: left center;
+ }
+ }
+
+ .post-peek-faux-media {
+ width: 48px;
+ height: 48px;
+ border-radius: 4px;
+ background-color: var(--bg-faded-color);
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 0 0 1px var(--outline-color);
+ }
+
+ .post-peek-card img {
+ outline: 3px double var(--link-faded-color);
+ }
+ }
+
+ .post-peek-tag {
+ display: inline-block;
+ border-radius: 4px;
+ font-size: 12px;
+ color: var(--text-insignificant-color);
+ font-weight: 500;
+ text-transform: uppercase;
+ border: 1px solid var(--outline-color);
+ padding: 2px !important;
+ align-self: center;
+ background-color: var(--bg-faded-color);
+ line-height: 1;
+
+ &.post-peek-poll {
+ display: inline-flex;
+ align-items: center;
+ gap: 2px;
+ flex-direction: column;
+ color: var(--text-color);
+ }
+
+ &.post-peek-thread {
+ font-size: 10px;
+ /* padding: 2px 4px; */
+ font-weight: 700;
+ background-color: var(--bg-color);
+ color: var(--reply-to-text-color) !important;
+ border-color: var(--reply-to-color);
+ background-image: repeating-linear-gradient(
+ -70deg,
+ transparent,
+ transparent 3px,
+ var(--reply-to-faded-color) 3px,
+ var(--reply-to-faded-color) 4px
+ );
+ }
+ }
+ }
+ > li > a:is(:hover, :focus) .post-peek-content {
+ opacity: 1;
+ }
+
+ .post-meta {
+ grid-area: meta;
+ font-size: 90%;
+ color: var(--text-insignificant-color);
+ white-space: nowrap;
+ display: flex;
+ align-items: center;
+ align-self: flex-start;
+ column-gap: 8px;
+ }
+
+ .post-stats {
+ opacity: 0;
+ display: inline-flex;
+ gap: 2px;
+ align-items: center;
+ transform: translateX(4px);
+ /* transition: all 0.25s ease-out; */
+
+ &:empty {
+ display: none;
+ }
+ }
+ .post-line:hover .post-stats {
+ opacity: 1;
+ transform: translateX(0);
+ }
+
+ + footer {
+ min-height: 15vh;
+ color: var(--text-insignificant-color);
+ padding-block: 15vh;
+ text-align: center;
+ }
+}
diff --git a/src/pages/catchup.jsx b/src/pages/catchup.jsx
new file mode 100644
index 00000000..d05e9f31
--- /dev/null
+++ b/src/pages/catchup.jsx
@@ -0,0 +1,1371 @@
+import '../components/links-bar.css';
+import './catchup.css';
+
+import autoAnimate from '@formkit/auto-animate';
+import { getBlurHashAverageColor } from 'fast-blurhash';
+import { Fragment } from 'preact';
+import { memo } from 'preact/compat';
+import { useEffect, useMemo, useReducer, useRef, useState } from 'preact/hooks';
+import { useSearchParams } from 'react-router-dom';
+import { uid } from 'uid/single';
+
+import Avatar from '../components/avatar';
+import Icon from '../components/icon';
+import Link from '../components/link';
+import Loader from '../components/loader';
+import NameText from '../components/name-text';
+import NavMenu from '../components/nav-menu';
+import RelativeTime from '../components/relative-time';
+import { api } from '../utils/api';
+import { oklab2rgb, rgb2oklab } from '../utils/color-utils';
+import db from '../utils/db';
+import emojifyText from '../utils/emojify-text';
+import { isFiltered } from '../utils/filters';
+import getHTMLText from '../utils/getHTMLText';
+import niceDateTime from '../utils/nice-date-time';
+import shortenNumber from '../utils/shorten-number';
+import showToast from '../utils/show-toast';
+import states, { getStatus, saveStatus, statusKey } from '../utils/states';
+import store from '../utils/store';
+import {
+ getCurrentAccount,
+ getCurrentAccountNS,
+ getCurrentInstance,
+ getCurrentInstanceConfiguration,
+} from '../utils/store-utils';
+import {
+ assignFollowedTags,
+ clearFollowedTagsState,
+ dedupeBoosts,
+} from '../utils/timeline-utils';
+import useScrollFn from '../utils/useScrollFn';
+import useTitle from '../utils/useTitle';
+
+const FILTER_CONTEXT = 'home';
+
+function Catchup() {
+ useTitle('Catch-up', '/catchup');
+ const { masto, instance } = api();
+ const [searchParams, setSearchParams] = useSearchParams();
+ const id = searchParams.get('id');
+ const [uiState, setUIState] = useState('start');
+ const [showTopLinks, setShowTopLinks] = useState(false);
+
+ const currentAccount = useMemo(() => {
+ return store.session.get('currentAccount');
+ }, []);
+ const isSelf = (accountID) => accountID === currentAccount;
+
+ async function fetchHome({ maxCreatedAt }) {
+ const maxCreatedAtDate = maxCreatedAt ? new Date(maxCreatedAt) : null;
+ console.debug('fetchHome', maxCreatedAtDate);
+ const allResults = [];
+ const homeIterator = masto.v1.timelines.home.list({ limit: 40 });
+ mainloop: while (true) {
+ try {
+ const results = await homeIterator.next();
+ const { value } = results;
+ if (value?.length) {
+ // This ignores maxCreatedAt filter, but it's ok for now
+ await assignFollowedTags(value, instance);
+ let addedResults = false;
+ for (let i = 0; i < value.length; i++) {
+ const item = value[i];
+ const createdAtDate = new Date(item.createdAt);
+ if (!maxCreatedAtDate || createdAtDate >= maxCreatedAtDate) {
+ // Filtered
+ const selfPost = isSelf(
+ item.reblog?.account?.id || item.account.id,
+ );
+ const filterInfo =
+ !selfPost &&
+ isFiltered(
+ item.reblog?.filtered || item.filtered,
+ FILTER_CONTEXT,
+ );
+ if (filterInfo?.action === 'hide') continue;
+ item._filtered = filterInfo;
+
+ // Followed tags
+ const sKey = statusKey(item.id, instance);
+ item._followedTags = states.statusFollowedTags[sKey]
+ ? [...states.statusFollowedTags[sKey]]
+ : [];
+
+ allResults.push(item);
+ addedResults = true;
+ } else {
+ // Don't immediately stop, still add the other items that might still be within range
+ // break mainloop;
+ }
+ // Only stop when ALL items are outside of range
+ if (!addedResults) {
+ break mainloop;
+ }
+ }
+ } else {
+ break mainloop;
+ }
+ // Pause 1s
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ } catch (e) {
+ console.error(e);
+ break mainloop;
+ }
+ }
+
+ // Post-process all results
+ // 1. Threadify - tag 1st-post in a thread
+ allResults.forEach((status) => {
+ if (status?.inReplyToId) {
+ const replyToStatus = allResults.find(
+ (s) => s.id === status.inReplyToId,
+ );
+ if (replyToStatus && !replyToStatus.inReplyToId) {
+ replyToStatus._thread = true;
+ }
+ }
+ });
+
+ return allResults;
+ }
+
+ const [posts, setPosts] = useState([]);
+ const catchupRangeRef = useRef();
+ async function handleCatchupClick({ duration } = {}) {
+ const now = Date.now();
+ const maxCreatedAt = duration ? now - duration : null;
+ setUIState('loading');
+ const results = await fetchHome({ maxCreatedAt });
+ // Namespaced by account ID
+ // Possible conflict if ID matches between different accounts from different instances
+ const ns = getCurrentAccountNS();
+ const catchupID = `${ns}-${uid()}`;
+ try {
+ await db.catchup.set(catchupID, {
+ id: catchupID,
+ posts: results,
+ count: results.length,
+ startAt: maxCreatedAt,
+ endAt: now,
+ });
+ setSearchParams({ id: catchupID });
+ } catch (e) {
+ console.error(e, results);
+ // setUIState('error');
+ }
+ // setPosts(results);
+ // setUIState('results');
+ }
+
+ useEffect(() => {
+ if (id) {
+ (async () => {
+ const catchup = await db.catchup.get(id);
+ if (catchup) {
+ setPosts(catchup.posts);
+ setUIState('results');
+ }
+ })();
+ } else if (uiState === 'results') {
+ setPosts([]);
+ setUIState('start');
+ }
+ }, [id]);
+
+ const [reloadCatchupsCount, reloadCatchups] = useReducer((c) => c + 1, 0);
+ const [lastCatchupEndAt, setLastCatchupEndAt] = useState(null);
+ const [prevCatchups, setPrevCatchups] = useState([]);
+ useEffect(() => {
+ (async () => {
+ try {
+ const catchups = await db.catchup.keys();
+ if (catchups.length) {
+ const ns = getCurrentAccountNS();
+ const ownKeys = catchups.filter((key) => key.startsWith(`${ns}-`));
+ if (ownKeys.length) {
+ let ownCatchups = await db.catchup.getMany(ownKeys);
+ ownCatchups.sort((a, b) => b.endAt - a.endAt);
+
+ // Split to 1st 3 last catchups, and the rest
+ let lastCatchups = ownCatchups.slice(0, 3);
+ let restCatchups = ownCatchups.slice(3);
+
+ const trimmedCatchups = lastCatchups.map((c) => {
+ const { id, count, startAt, endAt } = c;
+ return {
+ id,
+ count,
+ startAt,
+ endAt,
+ };
+ });
+ setPrevCatchups(trimmedCatchups);
+ setLastCatchupEndAt(lastCatchups[0].endAt);
+
+ // GC time
+ ownCatchups = null;
+ lastCatchups = null;
+
+ queueMicrotask(() => {
+ if (restCatchups.length) {
+ // delete them
+ db.catchup
+ .delMany(restCatchups.map((c) => c.id))
+ .then(() => {
+ // GC time
+ restCatchups = null;
+ })
+ .catch((e) => {
+ console.error(e);
+ });
+ }
+ });
+
+ return;
+ }
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ setPrevCatchups([]);
+ })();
+ }, [reloadCatchupsCount]);
+ useEffect(() => {
+ if (uiState === 'start') {
+ reloadCatchups();
+ }
+ }, [uiState === 'start']);
+
+ const [filterCounts, links] = useMemo(() => {
+ let filtereds = 0,
+ groups = 0,
+ boosts = 0,
+ replies = 0,
+ followedTags = 0,
+ originals = 0;
+ const links = {};
+ for (const post of posts) {
+ if (post._filtered) {
+ filtereds++;
+ post.__FILTER = 'filtered';
+ } else if (post.group) {
+ groups++;
+ post.__FILTER = 'group';
+ } else if (post.reblog) {
+ boosts++;
+ post.__FILTER = 'boost';
+ } else if (post._followedTags?.length) {
+ followedTags++;
+ post.__FILTER = 'followedTags';
+ } else if (
+ post.inReplyToId &&
+ post.inReplyToAccountId !== post.account?.id
+ ) {
+ replies++;
+ post.__FILTER = 'reply';
+ } else {
+ originals++;
+ post.__FILTER = 'original';
+ }
+
+ const thePost = post.reblog || post;
+ if (
+ thePost.card?.url &&
+ thePost.card?.image &&
+ thePost.card?.type === 'link'
+ ) {
+ const { card, favouritesCount, reblogsCount } = thePost;
+ let { url } = card;
+ url = url.replace(/\/$/, '');
+ if (!links[url]) {
+ links[url] = {
+ postID: thePost.id,
+ card,
+ shared: 1,
+ sharers: [post.account],
+ likes: favouritesCount,
+ boosts: reblogsCount,
+ };
+ } else {
+ if (links[url].sharers.find((a) => a.id === post.account.id)) {
+ continue;
+ }
+ links[url].shared++;
+ links[url].sharers.push(post.account);
+ if (links[url].postID !== thePost.id) {
+ links[url].likes += favouritesCount;
+ links[url].boosts += reblogsCount;
+ }
+ }
+ }
+ }
+
+ let topLinks = [];
+ for (const link in links) {
+ topLinks.push({
+ url: link,
+ ...links[link],
+ });
+ }
+ topLinks.sort((a, b) => {
+ if (a.shared > b.shared) return -1;
+ if (a.shared < b.shared) return 1;
+ if (a.boosts > b.boosts) return -1;
+ if (a.boosts < b.boosts) return 1;
+ if (a.likes > b.likes) return -1;
+ if (a.likes < b.likes) return 1;
+ return 0;
+ });
+
+ // Slice links to shared > 1 but min 10 links
+ if (topLinks.length > 10) {
+ linksLoop: for (let i = 10; i < topLinks.length; i++) {
+ const { shared } = topLinks[i];
+ if (shared <= 1) {
+ topLinks = topLinks.slice(0, i);
+ break linksLoop;
+ }
+ }
+ }
+
+ return [
+ {
+ Filtered: filtereds,
+ Groups: groups,
+ Boosts: boosts,
+ Replies: replies,
+ 'Followed tags': followedTags,
+ Original: originals,
+ },
+ topLinks,
+ ];
+ }, [posts]);
+
+ const [selectedFilterCategory, setSelectedFilterCategory] = useState('All');
+ const [selectedAuthor, setSelectedAuthor] = useState(null);
+
+ const [range, setRange] = useState(1);
+ const ranges = [
+ { label: 'last 1 hour', value: 1 },
+ { label: 'last 2 hours', value: 2 },
+ { label: 'last 3 hours', value: 3 },
+ { label: 'last 4 hours', value: 4 },
+ { label: 'last 5 hours', value: 5 },
+ { label: 'last 6 hours', value: 6 },
+ { label: 'last 7 hours', value: 7 },
+ { label: 'last 8 hours', value: 8 },
+ { label: 'last 9 hours', value: 9 },
+ { label: 'last 10 hours', value: 10 },
+ { label: 'last 11 hours', value: 11 },
+ { label: 'last 12 hours', value: 12 },
+ { label: 'beyond 12 hours', value: 13 },
+ ];
+
+ const [sortBy, setSortBy] = useState('createdAt');
+ const [sortOrder, setSortOrder] = useState('asc');
+ const [groupBy, setGroupBy] = useState(null);
+
+ const [filteredPosts, authors, authorCounts] = useMemo(() => {
+ let authors = [];
+ const authorCounts = {};
+ let filteredPosts = posts.filter((post) => {
+ return (
+ selectedFilterCategory === 'All' ||
+ post.__FILTER ===
+ {
+ Filtered: 'filtered',
+ Groups: 'group',
+ Boosts: 'boost',
+ Replies: 'reply',
+ 'Followed tags': 'followedTags',
+ Original: 'original',
+ }[selectedFilterCategory]
+ );
+ });
+
+ filteredPosts.forEach((post) => {
+ if (!authors.find((a) => a.id === post.account.id)) {
+ authors.push(post.account);
+ }
+ authorCounts[post.account.id] = (authorCounts[post.account.id] || 0) + 1;
+ });
+
+ if (selectedAuthor && authorCounts[selectedAuthor]) {
+ filteredPosts = filteredPosts.filter(
+ (post) => post.account.id === selectedAuthor,
+ );
+ }
+
+ const authorsHash = {};
+ for (const author of authors) {
+ authorsHash[author.id] = author;
+ }
+
+ return [filteredPosts, authorsHash, authorCounts];
+ }, [selectedFilterCategory, selectedAuthor, posts]);
+
+ const authorCountsList = useMemo(
+ () =>
+ Object.keys(authorCounts).sort(
+ (a, b) => authorCounts[b] - authorCounts[a],
+ ),
+ [authorCounts],
+ );
+
+ const sortedFilteredPosts = useMemo(() => {
+ const authorIndices = {};
+ authorCountsList.forEach((authorID, index) => {
+ authorIndices[authorID] = index;
+ });
+ return filteredPosts.sort((a, b) => {
+ if (groupBy === 'account') {
+ const aAccountID = a.account.id;
+ const bAccountID = b.account.id;
+ const aIndex = authorIndices[aAccountID];
+ const bIndex = authorIndices[bAccountID];
+ const order = aIndex - bIndex;
+ if (order !== 0) {
+ return order;
+ }
+ }
+ if (sortBy !== 'createdAt') {
+ a = a.reblog || a;
+ b = b.reblog || b;
+ if (a[sortBy] === b[sortBy]) {
+ return a.createdAt > b.createdAt ? 1 : -1;
+ }
+ }
+ if (sortOrder === 'asc') {
+ return a[sortBy] > b[sortBy] ? 1 : -1;
+ } else {
+ return b[sortBy] > a[sortBy] ? 1 : -1;
+ }
+ });
+ }, [filteredPosts, sortBy, sortOrder, groupBy, authorCountsList]);
+
+ const prevGroup = useRef(null);
+
+ const authorsListParent = useRef(null);
+ useEffect(() => {
+ if (authorsListParent.current && authorCountsList.length < 30) {
+ autoAnimate(authorsListParent.current, {
+ duration: 200,
+ });
+ }
+ }, [selectedFilterCategory, authorCountsList, authorsListParent]);
+
+ const postsBar = useMemo(() => {
+ return posts.map((post) => {
+ // If part of filteredPosts
+ const isFiltered = filteredPosts.find((p) => p.id === post.id);
+ return (
+
+ );
+ });
+ }, [posts, filteredPosts]);
+
+ const scrollableRef = useRef(null);
+ const headerRef = useRef(null);
+
+ useScrollFn(
+ {
+ scrollableRef,
+ },
+ ({ scrollDirection, nearReachStart }) => {
+ if (headerRef.current) {
+ const hiddenUI = scrollDirection === 'end' && !nearReachStart;
+ headerRef.current.hidden = hiddenUI;
+ }
+ },
+ [],
+ );
+
+ // if range value exceeded lastCatchupEndAt, show error
+ const lastCatchupRange = useMemo(() => {
+ // return hour, not ms
+ if (!lastCatchupEndAt) return null;
+ return (Date.now() - lastCatchupEndAt) / 1000 / 60 / 60;
+ }, [lastCatchupEndAt, range]);
+
+ return (
+
+
+
{
+ if (!e.target.closest('a, button')) {
+ scrollableRef.current?.scrollTo({
+ top: 0,
+ behavior: 'smooth',
+ });
+ }
+ }}
+ >
+
+
+
+ {uiState === 'start' && (
+
+
+ Catch-up beta
+
+
Let's catch up on the posts from your followings.
+
+ Show me all posts from…
+
+
+ setRange(+e.target.value)}
+ />{' '}
+
+ {ranges[range - 1].label}
+
+
+ {range == ranges[ranges.length - 1].value
+ ? 'until the max'
+ : niceDateTime(
+ new Date(Date.now() - range * 60 * 60 * 1000),
+ )}
+
+
+ {' '}
+
+
+ {lastCatchupRange && range > lastCatchupRange && (
+
+ Overlaps with your last catch-up
+
+ )}
+
+
+ Note: your instance might only show a maximum of 800 posts in
+ the Home timeline regardless of the time range. Could be less
+ or more.
+
+
+ {!!prevCatchups?.length && (
+
+
Previously…
+
+ {prevCatchups.map((pc) => (
+ -
+
+ {' '}
+
+ {formatRange(
+ new Date(pc.startAt),
+ new Date(pc.endAt),
+ )}{' '}
+
+ {pc.count} posts
+
+
+ {' '}
+
+
+ ))}
+
+ {prevCatchups.length >= 3 && (
+
+
+ Note: Only max 3 will be stored. The rest will be
+ automatically removed.
+
+
+ )}
+
+ )}
+
+ )}
+ {uiState === 'loading' && (
+
+
+
Fetching posts…
+
This might take a while.
+
+ )}
+ {uiState === 'results' && (
+ <>
+
+
+
+
+ {links.map((link) => {
+ const { card, shared, sharers, likes, boosts } = link;
+ const {
+ blurhash,
+ title,
+ description,
+ url,
+ image,
+ imageDescription,
+ language,
+ width,
+ height,
+ publishedAt,
+ } = card;
+ const domain = new URL(url).hostname
+ .replace(/^www\./, '')
+ .replace(/\/$/, '');
+ let accentColor;
+ if (blurhash) {
+ const averageColor = getBlurHashAverageColor(blurhash);
+ const labAverageColor = rgb2oklab(averageColor);
+ accentColor = oklab2rgb([
+ 0.6,
+ labAverageColor[1],
+ labAverageColor[2],
+ ]);
+ }
+
+ return (
+
+
+
+
+
+ {!!description && (
+
+ {description}
+
+ )}
+
+
+ Shared by{' '}
+ {sharers.map((s) => {
+ const { avatarStatic, displayName } = s;
+ return (
+
+ );
+ })}
+
+
+
+
+ );
+ })}
+
+
+
+ {posts.length >= 5 && (
+ {postsBar}
+ )}
+ {posts.length >= 2 && (
+
+
+ {[
+ 'Original',
+ 'Replies',
+ 'Boosts',
+ 'Followed tags',
+ 'Groups',
+ 'Filtered',
+ ].map(
+ (label) =>
+ !!filterCounts[label] && (
+
+ ),
+ )}
+
+ )}
+ {posts.length >= 2 && !!authorCounts && (
+
+ {authorCountsList.map((author) => (
+
+ ))}
+ {authorCountsList.length > 5 && (
+
+ {authorCountsList.length} authors
+
+ )}
+
+ )}
+ {posts.length >= 2 && (
+
+ Sort{' '}
+
+
+ Group{' '}
+
+ {
+ selectedAuthor && authorCountsList.length > 1 ? (
+
+ ) : null
+ //
+ }
+
+ )}
+
+ {sortedFilteredPosts.map((post, i) => {
+ const id = post.reblog?.id || post.id;
+ let showSeparator = false;
+ if (groupBy === 'account') {
+ if (
+ prevGroup.current &&
+ post.account.id !== prevGroup.current &&
+ i > 0
+ ) {
+ showSeparator = true;
+ }
+ prevGroup.current = post.account.id;
+ }
+ return (
+
+ {showSeparator && }
+ -
+
+
+
+
+
+ );
+ })}
+
+
+ >
+ )}
+
+
+
+ );
+}
+
+const PostLine = memo(
+ function ({ post }) {
+ const {
+ id,
+ account,
+ group,
+ reblog,
+ inReplyToId,
+ inReplyToAccountId,
+ _followedTags: isFollowedTags,
+ _filtered: filterInfo,
+ visibility,
+ } = post;
+ const isReplyTo = inReplyToId && inReplyToAccountId !== account.id;
+ const isFiltered = !!filterInfo;
+
+ const debugHover = (e) => {
+ if (e.shiftKey) {
+ console.log({
+ ...post,
+ });
+ }
+ };
+
+ return (
+
+
+ {reblog ? (
+
+ {' '}
+ {' '}
+ {/* */}
+
+
+ ) : (
+
+ )}
+
+
+
+ {' '}
+
+
+
+ );
+ },
+ (oldProps, newProps) => {
+ return oldProps?.post?.id === newProps?.post?.id;
+ },
+);
+
+const IntersectionPostLine = ({ root, ...props }) => {
+ const ref = useRef();
+ const [show, setShow] = useState(false);
+ useEffect(() => {
+ const observer = new IntersectionObserver(
+ (entries) => {
+ const entry = entries[0];
+ if (entry.isIntersecting) {
+ queueMicrotask(() => setShow(true));
+ observer.unobserve(ref.current);
+ }
+ },
+ {
+ root,
+ rootMargin: `${Math.max(320, screen.height * 0.75)}px`,
+ },
+ );
+ if (ref.current) observer.observe(ref.current);
+ return () => {
+ if (ref.current) observer.unobserve(ref.current);
+ };
+ }, []);
+
+ return show ? (
+
+ ) : (
+
+ );
+};
+
+const MEDIA_SIZE = 48;
+
+function PostPeek({ post, filterInfo }) {
+ const {
+ spoilerText,
+ sensitive,
+ content,
+ emojis,
+ poll,
+ mediaAttachments,
+ card,
+ inReplyToId,
+ inReplyToAccountId,
+ account,
+ _thread,
+ } = post;
+ const isThread =
+ (inReplyToId && inReplyToAccountId === account.id) || !!_thread;
+ const showMedia = !spoilerText && !sensitive;
+ const postText = content ? getHTMLText(content) : '';
+
+ return (
+
+
+ {!!filterInfo ? (
+ <>
+ {isThread && (
+ <>
+ Thread{' '}
+ >
+ )}
+
+ Filtered{filterInfo?.titlesStr ? `: ${filterInfo.titlesStr}` : ''}
+
+ >
+ ) : !!spoilerText ? (
+ <>
+ {isThread && (
+ <>
+ Thread{' '}
+ >
+ )}
+
+ {spoilerText}
+
+ >
+ ) : (
+
+ {isThread && (
+ <>
+
Thread{' '}
+ >
+ )}
+ {content ? (
+
+ ) : mediaAttachments?.length === 1 &&
+ mediaAttachments[0].description ? (
+ <>
+
ALT{' '}
+
{mediaAttachments[0].description}
+ >
+ ) : null}
+
+ )}
+
+ {!filterInfo && (
+
+ {!!poll && (
+
+
+ Poll
+
+ )}
+ {!!mediaAttachments?.length
+ ? mediaAttachments.map((m) => (
+
+ {{
+ image:
+ (m.previewUrl || m.url) && showMedia ? (
+
+ ) : (
+ 🖼
+ ),
+ gifv:
+ m.previewUrl && showMedia ? (
+
+ ) : (
+ 🎞️
+ ),
+ video:
+ m.previewUrl && showMedia ? (
+
+ ) : (
+ 📹
+ ),
+ audio: 🎵,
+ }[m.type] || null}
+
+ ))
+ : !!card &&
+ card.image &&
+ showMedia && (
+
+ {card.image ? (
+
+ ) : (
+ 🔗
+ )}
+
+ )}
+
+ )}
+
+ );
+}
+
+function PostStats({ post }) {
+ const { reblogsCount, repliesCount, favouritesCount } = post;
+ return (
+
+ {repliesCount > 0 && (
+ <>
+ {shortenNumber(repliesCount)}
+ >
+ )}
+ {favouritesCount > 0 && (
+ <>
+ {shortenNumber(favouritesCount)}
+ >
+ )}
+ {reblogsCount > 0 && (
+ <>
+ {shortenNumber(reblogsCount)}
+ >
+ )}
+
+ );
+}
+
+const { locale } = new Intl.DateTimeFormat().resolvedOptions();
+const dtf = new Intl.DateTimeFormat(locale, {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ hour: 'numeric',
+ minute: 'numeric',
+});
+function formatRange(startDate, endDate) {
+ return dtf.formatRange(startDate, endDate);
+}
+
+export default Catchup;
diff --git a/src/pages/trending.jsx b/src/pages/trending.jsx
index c1126aa1..5d293150 100644
--- a/src/pages/trending.jsx
+++ b/src/pages/trending.jsx
@@ -1,4 +1,4 @@
-import './trending.css';
+import '../components/links-bar.css';
import { MenuItem } from '@szhsin/react-menu';
import { getBlurHashAverageColor } from 'fast-blurhash';
diff --git a/src/utils/db.js b/src/utils/db.js
index 5db67ffd..589fa016 100644
--- a/src/utils/db.js
+++ b/src/utils/db.js
@@ -9,20 +9,20 @@ import {
set,
} from 'idb-keyval';
-const draftsStore = createStore('drafts-db', 'drafts-store');
-
-// Add additonal `draftsStore` parameter to all methods
-
-const drafts = {
- set: (key, val) => set(key, val, draftsStore),
- get: (key) => get(key, draftsStore),
- getMany: (keys) => getMany(keys, draftsStore),
- del: (key) => del(key, draftsStore),
- delMany: (keys) => delMany(keys, draftsStore),
- clear: () => clear(draftsStore),
- keys: () => keys(draftsStore),
-};
+function initDB(dbName, storeName) {
+ const store = createStore(dbName, storeName);
+ return {
+ set: (key, val) => set(key, val, store),
+ get: (key) => get(key, store),
+ getMany: (keys) => getMany(keys, store),
+ del: (key) => del(key, store),
+ delMany: (keys) => delMany(keys, store),
+ clear: () => clear(store),
+ keys: () => keys(store),
+ };
+}
export default {
- drafts,
+ drafts: initDB('drafts-db', 'drafts-store'),
+ catchup: initDB('catchup-db', 'catchup-store'),
};