summaryrefslogtreecommitdiff
path: root/web/source
diff options
context:
space:
mode:
Diffstat (limited to 'web/source')
-rw-r--r--web/source/css/_media-wrapper.css3
-rw-r--r--web/source/css/status.css166
-rw-r--r--web/source/css/thread.css13
-rw-r--r--web/source/frontend/index.js22
-rw-r--r--web/source/settings/components/status.tsx113
5 files changed, 250 insertions, 67 deletions
diff --git a/web/source/css/_media-wrapper.css b/web/source/css/_media-wrapper.css
index 561ae1ed3..b8541df4b 100644
--- a/web/source/css/_media-wrapper.css
+++ b/web/source/css/_media-wrapper.css
@@ -29,7 +29,6 @@
border-radius: $br;
position: relative;
overflow: hidden;
- z-index: 2;
img {
width: 100%;
@@ -59,8 +58,8 @@
position: absolute;
height: 100%;
width: 100%;
- z-index: 3;
overflow: hidden;
+ z-index: 1;
display: grid;
padding: 1rem;
diff --git a/web/source/css/status.css b/web/source/css/status.css
index 91665cd45..81ee7601a 100644
--- a/web/source/css/status.css
+++ b/web/source/css/status.css
@@ -28,8 +28,6 @@
padding-top: 0.75rem;
a {
- position: relative;
- z-index: 1;
color: inherit;
text-decoration: none;
}
@@ -109,11 +107,6 @@
gap: 0.5rem;
}
- .text-spoiler > summary, .text {
- position: relative;
- z-index: 2;
- }
-
.text-spoiler > summary {
list-style: none;
display: flex;
@@ -193,7 +186,6 @@
.poll {
background-color: $gray2;
- z-index: 2;
display: flex;
flex-direction: column;
@@ -260,59 +252,150 @@
display: flex;
gap: 1rem;
- .stats-grouping {
+ .stats-grouping,
+ .stats-more-info-content {
display: flex;
flex-wrap: wrap;
- column-gap: 1rem;
+ }
- .edited-at {
- font-size: smaller;
- }
+ .stats-grouping {
+ column-gap: 1rem;
+ row-gap: 0.25rem;
}
.stats-item {
display: flex;
gap: 0.4rem;
+ width: fit-content;
}
- .stats-item.published-at {
- text-decoration: underline;
+ details.stats-more-info {
+ margin-left: auto;
+
+ & > summary {
+ display: flex;
+
+ /*
+ Make it easy to touch.
+ */
+ width: 3rem;
+ height: 2rem;
+ margin: -0.25rem -0.5rem;
+
+ /*
+ Remove details/summary
+ arrow and use our own.
+ */
+ list-style: none;
+ &::-webkit-details-marker {
+ display: none; /* Safari */
+ }
+
+ /*
+ Don't display the
+ "hide" button initially.
+ */
+ i.hide {
+ display: none;
+ }
+
+ /*
+ Normalize fa
+ icon alignment.
+ */
+ align-items: center;
+ i.fa {
+ text-align: center;
+ }
+
+ cursor: pointer;
+ border-radius: $br-inner;
+ &:focus-visible {
+ outline: $button-focus-outline;
+ }
+
+ &:hover {
+ outline: 0.1rem solid $fg-reduced;
+ }
+ }
+
+ @keyframes fade-in {
+ 0% {opacity: 0}
+ 100% {opacity: 1}
+ }
+
+ &[open] {
+ .stats-more-info-content {
+ animation: fade-in .1s;
+ }
+
+ & > summary i.show {
+ display: none;
+ }
+
+ & > summary i.hide {
+ display: block;
+ }
+ }
}
- .stats-item:not(.published-at):not(.edited-at) {
- z-index: 1;
+ .stats-more-info-content {
+ position: absolute;
+ right: 0;
+ z-index: 2;
+
+ flex-direction: column;
+ max-width: 100%;
+ row-gap: 0.5rem;
+
+ background: $status-info-bg;
+ padding: 0.5rem 0.75rem;
+ border: $boxshadow-border;
+ box-shadow: $boxshadow;
+
+ opacity: 1;
+
+ .stats-grouping {
+ width: 100%;
+ justify-content: space-between;
+ }
+ }
+
+ .stats-item.published-at dd a {
+ time.dt-published {
+ text-decoration: underline;
+ }
+
+ &:focus-visible {
+ outline: 0;
+ time.dt-published {
+ outline: $link-focus-outline;
+ outline-offset: -0.25rem;
+ }
+ }
+ }
+
+ .stats-item:not(.published-at):not(.edit-timeline) {
user-select: none;
}
- .language {
- margin-left: auto;
+ .stats-item.edit-timeline {
+ flex-direction: column;
+ width: 100%;
+ border-top: $boxshadow-border;
+ padding-top: 0.4rem;
+
+ dd {
+ display: flex;
+ align-items: center;
+ gap: 0.4rem;
+ }
}
}
grid-column: span 3;
}
- .status-link {
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- overflow: hidden;
- text-indent: 100%;
- white-space: nowrap;
-
- position: absolute;
- z-index: 0;
-
- &:focus-visible {
- /*
- Inset focus to compensate for themes where
- statuses have a really thick border.
- */
- outline-offset: -0.25rem;
- }
- }
-
&:first-child {
/* top left, top right */
border-top-left-radius: $br;
@@ -327,7 +410,8 @@
&.expanded {
background: $status-focus-bg;
- .status-info {
+ .status-info,
+ .status-info .status-stats .stats-more-info-content {
background: $status-focus-info-bg;
}
}
diff --git a/web/source/css/thread.css b/web/source/css/thread.css
index c67c95d4e..75dda550b 100644
--- a/web/source/css/thread.css
+++ b/web/source/css/thread.css
@@ -79,9 +79,18 @@
&.indent-3,
&.indent-4,
&.indent-5 {
- .status-link {
- margin-left: -0.5rem;
+ /*
+ Show a stripey line to the left of
+ indented statuses for better legibility.
+ */
+ &::before {
+ content: "";
+ position: absolute;
+ left: 0;
+ top: 0;
+ height: 100%;
border-left: 0.15rem dashed $border-accent;
+ margin-left: -0.5rem;
}
}
diff --git a/web/source/frontend/index.js b/web/source/frontend/index.js
index 860d6d10a..da158ed77 100644
--- a/web/source/frontend/index.js
+++ b/web/source/frontend/index.js
@@ -338,3 +338,25 @@ Array.from(document.getElementsByTagName('time')).forEach(timeTag => {
timeTag.textContent = dateTimeFormat.format(date);
}
});
+
+// When clicking anywhere that's not an open
+// stats-info-more-content details dropdown,
+// close that open dropdown.
+document.body.addEventListener("click", (e) => {
+ const openStats = document.querySelector("details.stats-more-info[open]");
+ if (!openStats) {
+ // No open stats
+ // details element.
+ return;
+ }
+
+ if (openStats.contains(e.target)) {
+ // Click is within stats
+ // element, leave it alone.
+ return;
+ }
+
+ // Click was outside of
+ // stats elements, close it.
+ openStats.removeAttribute("open");
+});
diff --git a/web/source/settings/components/status.tsx b/web/source/settings/components/status.tsx
index a5b85f214..9d0dfa2b4 100644
--- a/web/source/settings/components/status.tsx
+++ b/web/source/settings/components/status.tsx
@@ -17,7 +17,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-import React, { useState } from "react";
+import React, { useMemo, useState } from "react";
import { useVerifyCredentialsQuery } from "../lib/query/login";
import { MediaAttachment, Status as StatusType } from "../lib/types/status";
import sanitize from "sanitize-html";
@@ -68,15 +68,6 @@ export function Status({ status }: { status: StatusType }) {
<StatusHeader status={status} />
<StatusBody status={status} />
<StatusFooter status={status} />
- <a
- href={status.url}
- target="_blank"
- className="status-link"
- data-nosnippet
- title="Open this status (opens in new tab)"
- >
- Open this status (opens in new tab)
- </a>
</article>
);
}
@@ -266,25 +257,103 @@ function StatusMediaEntry({ media }: { media: MediaAttachment }) {
);
}
+function useVisibilityIcon(visibility: string): string {
+ return useMemo(() => {
+ switch (true) {
+ case visibility === "direct":
+ return "fa-envelope";
+ case visibility === "followers_only":
+ return "fa-lock";
+ case visibility === "unlisted":
+ return "fa-unlock";
+ case visibility === "public":
+ return "fa-globe";
+ default:
+ return "fa-question";
+ }
+ }, [visibility]);
+}
+
function StatusFooter({ status }: { status: StatusType }) {
+ const visibilityIcon = useVisibilityIcon(status.visibility);
return (
<aside className="status-info">
- <dl className="status-stats">
- <div className="stats-grouping">
+ <div className="status-stats">
+ <dl className="stats-grouping text-cutoff">
<div className="stats-item published-at text-cutoff">
<dt className="sr-only">Published</dt>
- <dd>
- <time dateTime={status.created_at}>
- { new Date(status.created_at).toLocaleString() }
- </time>
+ <dd className="text-cutoff">
+ <a
+ href={status.url}
+ className="u-url text-cutoff"
+ >
+ <time
+ className="dt-published text-cutoff"
+ dateTime={status.created_at}
+ >
+ {new Date(status.created_at).toLocaleString(undefined, {
+ year: 'numeric',
+ month: 'short',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ hour12: false
+ })}
+ </time>
+ </a>
</dd>
</div>
- </div>
- <div className="stats-item language">
- <dt className="sr-only">Language</dt>
- <dd>{status.language}</dd>
- </div>
- </dl>
+ <div className="stats-grouping">
+ <div className="stats-item visibility-level" title={status.visibility}>
+ <dt className="sr-only">Visibility</dt>
+ <dd>
+ <i className={`fa ${visibilityIcon}`} aria-hidden="true"></i>
+ <span className="sr-only">{status.visibility}</span>
+ </dd>
+ </div>
+ </div>
+ </dl>
+ <details className="stats-more-info">
+ <summary title="More info">
+ <i className="fa fa-fw fa-info" aria-hidden="true"></i>
+ <span className="sr-only">More info</span>
+ <i className="fa fa-fw fa-chevron-right show" aria-hidden="true"></i>
+ <i className="fa fa-fw fa-chevron-down hide" aria-hidden="true"></i>
+ </summary>
+ <dl className="stats-more-info-content">
+ <div className="stats-grouping">
+ <div className="stats-item" title="Language">
+ <dt>
+ <span className="sr-only">Language</span>
+ <i className="fa fa-language" aria-hidden="true"></i>
+ </dt>
+ <dd>{status.language}</dd>
+ </div>
+ <div className="stats-item" title="Replies">
+ <dt>
+ <span className="sr-only">Replies</span>
+ <i className="fa fa-reply-all" aria-hidden="true"></i>
+ </dt>
+ <dd>{status.replies_count}</dd>
+ </div>
+ <div className="stats-item" title="Faves">
+ <dt>
+ <span className="sr-only">Favourites</span>
+ <i className="fa fa-star" aria-hidden="true"></i>
+ </dt>
+ <dd>{status.favourites_count}</dd>
+ </div>
+ <div className="stats-item" title="Boosts">
+ <dt>
+ <span className="sr-only">Reblogs</span>
+ <i className="fa fa-retweet" aria-hidden="true"></i>
+ </dt>
+ <dd>{status.reblogs_count}</dd>
+ </div>
+ </div>
+ </dl>
+ </details>
+ </div>
</aside>
);
}