diff --git a/internal/api/model/status.go b/internal/api/model/status.go index ec09f702d..bcd0c0f93 100644 --- a/internal/api/model/status.go +++ b/internal/api/model/status.go @@ -166,6 +166,12 @@ type WebStatus struct { // after the "main" thread, so it and everything // below it can be considered "replies". ThreadFirstReply bool + + // Sorted slice of StatusEdit times for + // this status, from latest to oldest. + // Only set if status has been edited. + // Last entry is always creation time. + EditTimeline []string `json:"-"` } /* diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index 91c9fea8a..2c662c3bd 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -1217,6 +1217,45 @@ func (c *Converter) StatusToWebStatus( // Mark local. webStatus.Local = *s.Local + // Get edit history for this + // status, if it's been edited. + if webStatus.EditedAt != nil { + // Make sure edits are populated. + if len(s.Edits) != len(s.EditIDs) { + s.Edits, err = c.state.DB.GetStatusEditsByIDs(ctx, s.EditIDs) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + err := gtserror.Newf("db error getting status edits: %w", err) + return nil, err + } + } + + // Include each historical entry + // (this includes the created date). + for _, edit := range s.Edits { + webStatus.EditTimeline = append( + webStatus.EditTimeline, + util.FormatISO8601(edit.CreatedAt), + ) + } + + // Make sure to include latest revision. + webStatus.EditTimeline = append( + webStatus.EditTimeline, + *webStatus.EditedAt, + ) + + // Sort the slice so it goes from + // newest -> oldest, like a timeline. + // + // It'll look something like: + // + // - edit3 date (ie., latest version) + // - edit2 date (if we have it) + // - edit1 date (if we have it) + // - created date + slices.Reverse(webStatus.EditTimeline) + } + // Set additional templating // variables on media attachments. diff --git a/web/assets/themes/ecks-pee.css b/web/assets/themes/ecks-pee.css index a85e5da0b..bd479c67b 100644 --- a/web/assets/themes/ecks-pee.css +++ b/web/assets/themes/ecks-pee.css @@ -238,6 +238,31 @@ blockquote { border-right: 1px solid #001ea0; } +/* Status info dropdown button */ +.status .status-info .status-stats details.stats-more-info > summary { + color: var(--button-fg); + background: var(--ecks-pee-start-button); + border-left: 1px solid var(--ecks-pee-darkest-green); + border-right: 1px solid var(--ecks-pee-darkest-green); +} +.status .status-info .status-stats details.stats-more-info > summary:hover { + outline: 0; + background: var(--ecks-pee-light-green); +} + +/* Status info dropdown content */ +.status .status-info .status-stats .stats-more-info-content, +.status.expanded .status-info .status-stats .stats-more-info-content { + color: black; + text-shadow: none; + background: var(--ecks-pee-beige); + border: 0.2rem outset var(--ecks-pee-darker-beige); + border-radius: 0; +} +.status .status-info .status-stats .stats-item.edit-timeline { + border-top: var(--ecks-pee-dotted-trim); +} + /* Button stuff */ button, .button { border-left: 1px solid var(--ecks-pee-darkest-green); diff --git a/web/assets/themes/midnight-trip.css b/web/assets/themes/midnight-trip.css index 059e4ac8e..3f8619098 100644 --- a/web/assets/themes/midnight-trip.css +++ b/web/assets/themes/midnight-trip.css @@ -129,6 +129,16 @@ html, body { background: black; } +/* Status info dropdown content */ +.status.expanded .status-info .status-stats .stats-more-info-content, +.status .status-info .status-stats .stats-more-info-content { + background-color: black; + border: 0.25rem solid var(--magenta); +} +.status .status-info .status-stats .stats-item.edit-timeline { + border-top: 0.15rem dotted var(--acid-green); +} + /* Back + next links */ .backnextlinks { background: var(--gray1); diff --git a/web/assets/themes/moonlight-hunt.css b/web/assets/themes/moonlight-hunt.css index 630c7cd21..8dcfb0bb6 100644 --- a/web/assets/themes/moonlight-hunt.css +++ b/web/assets/themes/moonlight-hunt.css @@ -143,6 +143,12 @@ blockquote { background: var(--outer-space); } +/* Status info dropdown content */ +.status.expanded .status-info .status-stats .stats-more-info-content, +.status .status-info .status-stats .stats-more-info-content { + background: var(--outer-space); +} + /* Make show more/less buttons more legible */ .status .button { border: 1px solid var(--feral-orange); diff --git a/web/assets/themes/soft.css b/web/assets/themes/soft.css index 691558bee..01a8729f7 100644 --- a/web/assets/themes/soft.css +++ b/web/assets/themes/soft.css @@ -142,3 +142,9 @@ code, code[class*="language-"] { blockquote { background-color: var(--soft-lilac-translucent); } + +/* Status info dropdown content */ +.status.expanded .status-info .status-stats .stats-more-info-content, +.status .status-info .status-stats .stats-more-info-content { + background: var(--soft-pink); +} 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 . */ -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 }) { - - Open this status (opens in new tab) - ); } @@ -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 ( ); } diff --git a/web/template/status.tmpl b/web/template/status.tmpl index 4263e6020..9d2f80a5e 100644 --- a/web/template/status.tmpl +++ b/web/template/status.tmpl @@ -90,27 +90,7 @@ media photoswipe-gallery {{ (len .) | oddOrEven }} {{ if eq (len .) 1 }}single{{ {{- end }} -