Add "Edited at" meta with Edit History modal

Much refactor, kinda ugly code still.

Edit History design is still very basic.
This commit is contained in:
Lim Chee Aun 2022-12-11 21:22:22 +08:00
parent 6f3eae15b6
commit cb64f5ffda
6 changed files with 329 additions and 140 deletions

View file

@ -291,11 +291,23 @@ a.hashtag {
.box { .box {
width: 40em; width: 40em;
max-width: 100vw; max-width: 100vw;
text-align: center;
padding: 16px; padding: 16px;
background-color: var(--bg-color); background-color: var(--bg-color);
border-radius: 8px; border-radius: 8px;
border: 1px solid var(--divider-color); border: 1px solid var(--divider-color);
overflow: auto;
max-height: 90vh;
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 {

View file

@ -91,7 +91,7 @@
color: var(--reply-to-color); color: var(--reply-to-color);
vertical-align: middle; vertical-align: middle;
} }
.status > .container > .meta .time { .status > .container > .meta :is(.time, .edited) {
color: inherit; color: inherit;
text-align: end; text-align: end;
opacity: 0.5; opacity: 0.5;
@ -369,6 +369,32 @@ a.card:hover {
margin: 8px 0; margin: 8px 0;
} }
/* EXTRA META */
.status .extra-meta {
padding-top: 8px;
color: var(--text-insignificant-color);
}
.status .extra-meta * {
vertical-align: middle;
}
.status .extra-meta a {
color: inherit;
text-decoration: none;
}
.status .extra-meta a:hover {
text-decoration: underline;
}
.status .extra-meta .edited:hover {
cursor: pointer;
color: var(--text-color);
}
.status.large .extra-meta {
padding-top: 0;
margin-left: calc(-50px - 16px);
background-color: var(--bg-color);
}
/* ACTIONS */ /* ACTIONS */
.status .actions { .status .actions {
@ -451,3 +477,21 @@ a.card:hover {
vertical-align: middle; vertical-align: middle;
object-fit: contain; object-fit: contain;
} }
/* EDIT HISTORY */
#edit-history {
min-height: 50vh;
min-height: 50dvh;
}
#edit-history :is(ol, ol li){
list-style: none;
margin: 0;
padding: 0;
}
#edit-history .history-item .status {
border: 1px solid var(--outline-color);
border-radius: 8px;
}

View file

@ -6,6 +6,7 @@ import { useEffect, useRef, useState } from 'preact/hooks';
import { InView } from 'react-intersection-observer'; import { InView } from 'react-intersection-observer';
import { useSnapshot } from 'valtio'; import { useSnapshot } from 'valtio';
import Loader from '../components/loader';
import Modal from '../components/modal'; import Modal from '../components/modal';
import NameText from '../components/name-text'; import NameText from '../components/name-text';
import enhanceContent from '../utils/enhance-content'; import enhanceContent from '../utils/enhance-content';
@ -375,12 +376,86 @@ function Poll({ poll }) {
); );
} }
function EditedAtModal({ statusID, onClose = () => {} }) {
const [uiState, setUIState] = useState('default');
const [editHistory, setEditHistory] = useState([]);
useEffect(() => {
setUIState('loading');
(async () => {
try {
const editHistory = await masto.statuses.fetchHistory(statusID);
console.log(editHistory);
setEditHistory(editHistory);
setUIState('default');
} catch (e) {
console.error(e);
setUIState('error');
}
})();
}, []);
const currentYear = new Date().getFullYear();
return (
<div id="edit-history" class="box">
<button type="button" class="close-button plain large" onClick={onClose}>
<Icon icon="x" alt="Close" />
</button>
<h2>Edit History</h2>
{uiState === 'error' && <p>Failed to load history</p>}
{uiState === 'loading' && (
<p>
<Loader abrupt /> Loading&hellip;
</p>
)}
{editHistory.length > 0 && (
<ol>
{editHistory.map((status) => {
const { createdAt } = status;
const createdAtDate = new Date(createdAt);
return (
<li key={createdAt} class="history-item">
<h3>
<time>
{Intl.DateTimeFormat('en', {
// Show year if not current year
year:
createdAtDate.getFullYear() === currentYear
? undefined
: 'numeric',
month: 'short',
day: 'numeric',
weekday: 'short',
hour: 'numeric',
minute: '2-digit',
second: '2-digit',
}).format(createdAtDate)}
</time>
</h3>
<Status status={status} size="s" withinContext editStatus />
</li>
);
})}
</ol>
)}
</div>
);
}
function fetchAccount(id) { function fetchAccount(id) {
return masto.accounts.fetch(id); return masto.accounts.fetch(id);
} }
const memFetchAccount = mem(fetchAccount); const memFetchAccount = mem(fetchAccount);
function Status({ statusID, status, withinContext, size = 'm', skeleton }) { function Status({
statusID,
status,
withinContext,
size = 'm',
skeleton,
editStatus,
}) {
if (skeleton) { if (skeleton) {
return ( return (
<div class="status skeleton"> <div class="status skeleton">
@ -443,8 +518,9 @@ function Status({ statusID, status, withinContext, size = 'm', skeleton }) {
} = status; } = status;
const createdAtDate = new Date(createdAt); const createdAtDate = new Date(createdAt);
const editedAtDate = new Date(editedAt);
let inReplyToAccountRef = mentions.find( let inReplyToAccountRef = mentions?.find(
(mention) => mention.id === inReplyToAccountId, (mention) => mention.id === inReplyToAccountId,
); );
if (!inReplyToAccountRef && inReplyToAccountId === id) { if (!inReplyToAccountRef && inReplyToAccountId === id) {
@ -498,8 +574,10 @@ function Status({ statusID, status, withinContext, size = 'm', skeleton }) {
} }
const [actionsUIState, setActionsUIState] = useState(null); // boost-loading, favourite-loading, bookmark-loading const [actionsUIState, setActionsUIState] = useState(null); // boost-loading, favourite-loading, bookmark-loading
const [showEdited, setShowEdited] = useState(false);
const carouselRef = useRef(null); const carouselRef = useRef(null);
const currentYear = new Date().getFullYear();
return ( return (
<div <div
@ -546,21 +624,23 @@ function Status({ statusID, status, withinContext, size = 'm', skeleton }) {
</> </>
)} )}
</span>{' '} </span>{' '}
<a href={uri} target="_blank" class="time"> {size !== 'l' && !editStatus && (
<Icon <a href={uri} target="_blank" class="time">
icon={visibilityIconsMap[visibility]} <Icon
alt={visibility} icon={visibilityIconsMap[visibility]}
size="s" alt={visibility}
/>{' '} size="s"
<relative-time />{' '}
datetime={createdAtDate.toISOString()} <relative-time
format="micro" datetime={createdAtDate.toISOString()}
threshold="P1D" format="micro"
prefix="" threshold="P1D"
> prefix=""
{createdAtDate.toLocaleString()} >
</relative-time> {createdAtDate.toLocaleString()}
</a> </relative-time>
</a>
)}
</div> </div>
<div <div
class={`content-container ${ class={`content-container ${
@ -593,7 +673,6 @@ function Status({ statusID, status, withinContext, size = 'm', skeleton }) {
if (target.parentNode.tagName.toLowerCase() === 'a') { if (target.parentNode.tagName.toLowerCase() === 'a') {
target = target.parentNode; target = target.parentNode;
} }
console.log('click', target, e);
if ( if (
target.tagName.toLowerCase() === 'a' && target.tagName.toLowerCase() === 'a' &&
target.classList.contains('u-url') target.classList.contains('u-url')
@ -662,53 +741,137 @@ function Status({ statusID, status, withinContext, size = 'm', skeleton }) {
)} )}
</div> </div>
{size === 'l' && ( {size === 'l' && (
<div class="actions"> <>
<button <div class="extra-meta">
type="button" <Icon icon={visibilityIconsMap[visibility]} alt={visibility} />{' '}
title="Comment" <a href={uri} target="_blank">
class="plain reply-button" <time class="created" datetime={createdAtDate.toISOString()}>
onClick={(e) => { {Intl.DateTimeFormat('en', {
e.preventDefault(); // Show year if not current year
e.stopPropagation(); year:
states.showCompose = { createdAtDate.getFullYear() === currentYear
replyToStatus: status, ? undefined
}; : 'numeric',
}} month: 'short',
> day: 'numeric',
<Icon icon="comment" size="l" alt="Reply" /> hour: 'numeric',
{!!repliesCount && ( minute: '2-digit',
}).format(createdAtDate)}
</time>
</a>
{editedAt && (
<> <>
{' '} {' '}
<small>{shortenNumber(repliesCount)}</small> &bull; <Icon icon="pencil" alt="Edited" />{' '}
<time
class="edited"
datetime={editedAtDate.toISOString()}
onClick={() => {
setShowEdited(id);
}}
>
{Intl.DateTimeFormat('en', {
// Show year if not this year
year:
editedAtDate.getFullYear() === currentYear
? undefined
: 'numeric',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
}).format(editedAtDate)}
</time>
</> </>
)} )}
</button> </div>
{/* TODO: if visibility = private, only can reblog own statuses */} <div class="actions">
{visibility !== 'direct' && (
<button <button
type="button" type="button"
title={reblogged ? 'Unboost' : 'Boost'} title="Comment"
class={`plain reblog-button ${reblogged ? 'reblogged' : ''}`} class="plain reply-button"
disabled={actionsUIState === 'boost-loading'} onClick={(e) => {
e.preventDefault();
e.stopPropagation();
states.showCompose = {
replyToStatus: status,
};
}}
>
<Icon icon="comment" size="l" alt="Reply" />
{!!repliesCount && (
<>
{' '}
<small>{shortenNumber(repliesCount)}</small>
</>
)}
</button>
{/* TODO: if visibility = private, only can reblog own statuses */}
{visibility !== 'direct' && (
<button
type="button"
title={reblogged ? 'Unboost' : 'Boost'}
class={`plain reblog-button ${reblogged ? 'reblogged' : ''}`}
disabled={actionsUIState === 'boost-loading'}
onClick={async (e) => {
e.preventDefault();
e.stopPropagation();
const yes = confirm(
reblogged ? 'Unboost this status?' : 'Boost this status?',
);
if (!yes) return;
setActionsUIState('boost-loading');
try {
if (reblogged) {
const newStatus = await masto.statuses.unreblog(id);
states.statuses.set(newStatus.id, newStatus);
} else {
const newStatus = await masto.statuses.reblog(id);
states.statuses.set(newStatus.id, newStatus);
states.statuses.set(
newStatus.reblog.id,
newStatus.reblog,
);
}
} catch (e) {
alert(e);
console.error(e);
} finally {
setActionsUIState(null);
}
}}
>
<Icon
icon="rocket"
size="l"
alt={reblogged ? 'Boosted' : 'Boost'}
/>
{!!reblogsCount && (
<>
{' '}
<small>{shortenNumber(reblogsCount)}</small>
</>
)}
</button>
)}
<button
type="button"
title={favourited ? 'Unfavourite' : 'Favourite'}
class={`plain favourite-button ${
favourited ? 'favourited' : ''
}`}
disabled={actionsUIState === 'favourite-loading'}
onClick={async (e) => { onClick={async (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const yes = confirm( setActionsUIState('favourite-loading');
reblogged ? 'Unboost this status?' : 'Boost this status?',
);
if (!yes) return;
setActionsUIState('boost-loading');
try { try {
if (reblogged) { if (favourited) {
const newStatus = await masto.statuses.unreblog(id); const newStatus = await masto.statuses.unfavourite(id);
states.statuses.set(newStatus.id, newStatus); states.statuses.set(newStatus.id, newStatus);
} else { } else {
const newStatus = await masto.statuses.reblog(id); const newStatus = await masto.statuses.favourite(id);
states.statuses.set(newStatus.id, newStatus); states.statuses.set(newStatus.id, newStatus);
states.statuses.set(
newStatus.reblog.id,
newStatus.reblog,
);
} }
} catch (e) { } catch (e) {
alert(e); alert(e);
@ -719,87 +882,52 @@ function Status({ statusID, status, withinContext, size = 'm', skeleton }) {
}} }}
> >
<Icon <Icon
icon="rocket" icon="heart"
size="l" size="l"
alt={reblogged ? 'Boosted' : 'Boost'} alt={favourited ? 'Favourited' : 'Favourite'}
/> />
{!!reblogsCount && ( {!!favouritesCount && (
<> <>
{' '} {' '}
<small>{shortenNumber(reblogsCount)}</small> <small>{shortenNumber(favouritesCount)}</small>
</> </>
)} )}
</button> </button>
)} <button
<button type="button"
type="button" title={bookmarked ? 'Unbookmark' : 'Bookmark'}
title={favourited ? 'Unfavourite' : 'Favourite'} class={`plain bookmark-button ${
class={`plain favourite-button ${favourited ? 'favourited' : ''}`} bookmarked ? 'bookmarked' : ''
disabled={actionsUIState === 'favourite-loading'} }`}
onClick={async (e) => { disabled={actionsUIState === 'bookmark-loading'}
e.preventDefault(); onClick={async (e) => {
e.stopPropagation(); e.preventDefault();
setActionsUIState('favourite-loading'); e.stopPropagation();
try { setActionsUIState('bookmark-loading');
if (favourited) { try {
const newStatus = await masto.statuses.unfavourite(id); if (bookmarked) {
states.statuses.set(newStatus.id, newStatus); const newStatus = await masto.statuses.unbookmark(id);
} else { states.statuses.set(newStatus.id, newStatus);
const newStatus = await masto.statuses.favourite(id); } else {
states.statuses.set(newStatus.id, newStatus); const newStatus = await masto.statuses.bookmark(id);
states.statuses.set(newStatus.id, newStatus);
}
} catch (e) {
alert(e);
console.error(e);
} finally {
setActionsUIState(null);
} }
} catch (e) { }}
alert(e); >
console.error(e); <Icon
} finally { icon="bookmark"
setActionsUIState(null); size="l"
} alt={bookmarked ? 'Bookmarked' : 'Bookmark'}
}} />
> </button>
<Icon </div>
icon="heart" </>
size="l"
alt={favourited ? 'Favourited' : 'Favourite'}
/>
{!!favouritesCount && (
<>
{' '}
<small>{shortenNumber(favouritesCount)}</small>
</>
)}
</button>
<button
type="button"
title={bookmarked ? 'Unbookmark' : 'Bookmark'}
class={`plain bookmark-button ${bookmarked ? 'bookmarked' : ''}`}
disabled={actionsUIState === 'bookmark-loading'}
onClick={async (e) => {
e.preventDefault();
e.stopPropagation();
setActionsUIState('bookmark-loading');
try {
if (bookmarked) {
const newStatus = await masto.statuses.unbookmark(id);
states.statuses.set(newStatus.id, newStatus);
} else {
const newStatus = await masto.statuses.bookmark(id);
states.statuses.set(newStatus.id, newStatus);
}
} catch (e) {
alert(e);
console.error(e);
} finally {
setActionsUIState(null);
}
}}
>
<Icon
icon="bookmark"
size="l"
alt={bookmarked ? 'Bookmarked' : 'Bookmark'}
/>
</button>
</div>
)} )}
</div> </div>
{showMediaModal !== false && ( {showMediaModal !== false && (
@ -908,6 +1036,22 @@ function Status({ statusID, status, withinContext, size = 'm', skeleton }) {
)} )}
</Modal> </Modal>
)} )}
{!!showEdited && (
<Modal
onClick={(e) => {
if (e.target === e.currentTarget) {
setShowEdited(false);
}
}}
>
<EditedAtModal
statusID={showEdited}
onClose={() => {
setShowEdited(false);
}}
/>
</Modal>
)}
</div> </div>
); );
} }

View file

@ -53,7 +53,7 @@ export default () => {
}; };
return ( return (
<main class="box"> <main class="box" style={{ textAlign: 'center' }}>
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<h1>Log in</h1> <h1>Log in</h1>
<label> <label>

View file

@ -1,16 +1,6 @@
#settings-container { #settings-container {
text-align: left;
padding-bottom: 3em; padding-bottom: 3em;
animation: fade-in 0.2s ease-out; animation: fade-in 0.2s ease-out;
max-height: 100vh;
overflow: auto;
position: relative;
}
#settings-container .close-button {
position: absolute;
top: 0;
right: 0;
} }
#settings-container h2 { #settings-container h2 {

View file

@ -1,6 +1,5 @@
#welcome { #welcome {
overflow: auto; text-align: center;
max-height: 90vh;
} }
#welcome img { #welcome img {