Add 'more' menu
- Refactor Toast - Fix locale for datetime strings in status - Nicer shadow for menus
This commit is contained in:
parent
f7b398e078
commit
8aaba24d1f
7 changed files with 423 additions and 212 deletions
24
src/app.css
24
src/app.css
|
@ -1042,7 +1042,7 @@ body:has(.status-deck) .media-post-link {
|
||||||
background-color: var(--bg-color);
|
background-color: var(--bg-color);
|
||||||
border: 1px solid var(--outline-color);
|
border: 1px solid var(--outline-color);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 3px 6px var(--drop-shadow-color);
|
box-shadow: 0 3px 16px -3px var(--drop-shadow-color);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
animation: appear-smooth 0.15s ease-in-out;
|
animation: appear-smooth 0.15s ease-in-out;
|
||||||
width: 16em;
|
width: 16em;
|
||||||
|
@ -1052,8 +1052,25 @@ body:has(.status-deck) .media-post-link {
|
||||||
.szh-menu__item--focusable {
|
.szh-menu__item--focusable {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
.szh-menu__header {
|
||||||
|
margin: -8px 0 8px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
color: var(--text-insignificant-color);
|
||||||
|
font-size: 90%;
|
||||||
|
background-color: var(--bg-faded-color);
|
||||||
|
/* background-image: linear-gradient(to top, var(--bg-faded-color), transparent); */
|
||||||
|
text-shadow: 0 1px 0 var(--bg-color);
|
||||||
|
line-height: 1.2;
|
||||||
|
/* border-bottom: 1px solid var(--outline-color); */
|
||||||
|
}
|
||||||
|
.szh-menu__header * {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
.szh-menu .szh-menu__item {
|
.szh-menu .szh-menu__item {
|
||||||
display: block;
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
line-height: 1;
|
||||||
padding: 8px 16px !important;
|
padding: 8px 16px !important;
|
||||||
transition: all 0.1s ease-in-out;
|
transition: all 0.1s ease-in-out;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
@ -1065,14 +1082,15 @@ body:has(.status-deck) .media-post-link {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
.szh-menu .szh-menu__item a {
|
.szh-menu .szh-menu__item a {
|
||||||
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
padding: 8px 16px !important;
|
padding: 8px 16px !important;
|
||||||
margin: -8px -16px !important;
|
margin: -8px -16px !important;
|
||||||
gap: 8px;
|
|
||||||
}
|
}
|
||||||
.szh-menu .szh-menu__item a.is-active {
|
.szh-menu .szh-menu__item a.is-active {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
36
src/app.jsx
36
src/app.jsx
|
@ -14,7 +14,6 @@ import {
|
||||||
useLocation,
|
useLocation,
|
||||||
useNavigate,
|
useNavigate,
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
import Toastify from 'toastify-js';
|
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
import Account from './components/account';
|
import Account from './components/account';
|
||||||
|
@ -53,6 +52,7 @@ import {
|
||||||
initPreferences,
|
initPreferences,
|
||||||
} from './utils/api';
|
} from './utils/api';
|
||||||
import { getAccessToken } from './utils/auth';
|
import { getAccessToken } from './utils/auth';
|
||||||
|
import showToast from './utils/show-toast';
|
||||||
import states, { getStatus, saveStatus } from './utils/states';
|
import states, { getStatus, saveStatus } from './utils/states';
|
||||||
import store from './utils/store';
|
import store from './utils/store';
|
||||||
import { getCurrentAccount } from './utils/store-utils';
|
import { getCurrentAccount } from './utils/store-utils';
|
||||||
|
@ -328,26 +328,20 @@ function App() {
|
||||||
window.__COMPOSE__ = null;
|
window.__COMPOSE__ = null;
|
||||||
if (newStatus) {
|
if (newStatus) {
|
||||||
states.reloadStatusPage++;
|
states.reloadStatusPage++;
|
||||||
setTimeout(() => {
|
showToast({
|
||||||
const toast = Toastify({
|
text: 'Status posted. Check it out.',
|
||||||
className: 'shiny-pill',
|
delay: 1000,
|
||||||
text: 'Status posted. Check it out.',
|
duration: 10_000, // 10 seconds
|
||||||
duration: 10_000, // 10 seconds
|
onClick: (toast) => {
|
||||||
gravity: 'bottom',
|
toast.hideToast();
|
||||||
position: 'center',
|
states.prevLocation = location;
|
||||||
// destination: `/#/s/${newStatus.id}`,
|
navigate(
|
||||||
onClick: () => {
|
instance
|
||||||
toast.hideToast();
|
? `/${instance}/s/${newStatus.id}`
|
||||||
states.prevLocation = location;
|
: `/s/${newStatus.id}`,
|
||||||
navigate(
|
);
|
||||||
instance
|
},
|
||||||
? `/${instance}/s/${newStatus.id}`
|
});
|
||||||
: `/s/${newStatus.id}`,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
toast.showToast();
|
|
||||||
}, 1000);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -58,6 +58,9 @@ const ICONS = {
|
||||||
following: 'mingcute:walk-line',
|
following: 'mingcute:walk-line',
|
||||||
pin: 'mingcute:pin-line',
|
pin: 'mingcute:pin-line',
|
||||||
bus: 'mingcute:bus-2-line',
|
bus: 'mingcute:bus-2-line',
|
||||||
|
link: 'mingcute:link-2-line',
|
||||||
|
history: 'mingcute:history-line',
|
||||||
|
share: 'mingcute:share-2-line',
|
||||||
};
|
};
|
||||||
|
|
||||||
const modules = import.meta.glob('/node_modules/@iconify-icons/mingcute/*.js');
|
const modules = import.meta.glob('/node_modules/@iconify-icons/mingcute/*.js');
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import './status.css';
|
import './status.css';
|
||||||
|
|
||||||
import { Menu, MenuItem } from '@szhsin/react-menu';
|
import { Menu, MenuDivider, MenuHeader, MenuItem } from '@szhsin/react-menu';
|
||||||
import mem from 'mem';
|
import mem from 'mem';
|
||||||
import pThrottle from 'p-throttle';
|
import pThrottle from 'p-throttle';
|
||||||
import { memo } from 'preact/compat';
|
import { memo } from 'preact/compat';
|
||||||
|
@ -17,6 +17,7 @@ import enhanceContent from '../utils/enhance-content';
|
||||||
import handleContentLinks from '../utils/handle-content-links';
|
import handleContentLinks from '../utils/handle-content-links';
|
||||||
import htmlContentLength from '../utils/html-content-length';
|
import htmlContentLength from '../utils/html-content-length';
|
||||||
import shortenNumber from '../utils/shorten-number';
|
import shortenNumber from '../utils/shorten-number';
|
||||||
|
import showToast from '../utils/show-toast';
|
||||||
import states, { saveStatus, statusKey } from '../utils/states';
|
import states, { saveStatus, statusKey } from '../utils/states';
|
||||||
import store from '../utils/store';
|
import store from '../utils/store';
|
||||||
import visibilityIconsMap from '../utils/visibility-icons-map';
|
import visibilityIconsMap from '../utils/visibility-icons-map';
|
||||||
|
@ -25,6 +26,7 @@ import Avatar from './avatar';
|
||||||
import Icon from './icon';
|
import Icon from './icon';
|
||||||
import Link from './link';
|
import Link from './link';
|
||||||
import Media from './media';
|
import Media from './media';
|
||||||
|
import MenuLink from './MenuLink';
|
||||||
import RelativeTime from './relative-time';
|
import RelativeTime from './relative-time';
|
||||||
|
|
||||||
const throttle = pThrottle({
|
const throttle = pThrottle({
|
||||||
|
@ -41,6 +43,13 @@ function fetchAccount(id, masto) {
|
||||||
}
|
}
|
||||||
const memFetchAccount = mem(fetchAccount);
|
const memFetchAccount = mem(fetchAccount);
|
||||||
|
|
||||||
|
const visibilityText = {
|
||||||
|
public: 'Public',
|
||||||
|
unlisted: 'Unlisted',
|
||||||
|
private: 'Followers only',
|
||||||
|
direct: 'Mentioned people only',
|
||||||
|
};
|
||||||
|
|
||||||
function Status({
|
function Status({
|
||||||
statusID,
|
statusID,
|
||||||
status,
|
status,
|
||||||
|
@ -217,6 +226,276 @@ function Status({
|
||||||
const textWeight = () =>
|
const textWeight = () =>
|
||||||
Math.round((spoilerText.length + htmlContentLength(content)) / 140) || 1;
|
Math.round((spoilerText.length + htmlContentLength(content)) / 140) || 1;
|
||||||
|
|
||||||
|
const locale = new Intl.DateTimeFormat().resolvedOptions().locale;
|
||||||
|
const createdDateText = Intl.DateTimeFormat(locale, {
|
||||||
|
// Show year if not current year
|
||||||
|
year: createdAtDate.getFullYear() === currentYear ? undefined : 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: '2-digit',
|
||||||
|
}).format(createdAtDate);
|
||||||
|
const editedDateText =
|
||||||
|
editedAt &&
|
||||||
|
Intl.DateTimeFormat(locale, {
|
||||||
|
// Show year if not this year
|
||||||
|
year: editedAtDate.getFullYear() === currentYear ? undefined : 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: '2-digit',
|
||||||
|
}).format(editedAtDate);
|
||||||
|
|
||||||
|
const isSizeLarge = size === 'l';
|
||||||
|
// TODO: if visibility = private, only can boost own statuses
|
||||||
|
const canBoost = authenticated && visibility !== 'direct';
|
||||||
|
|
||||||
|
const replyStatus = () => {
|
||||||
|
if (!sameInstance || !authenticated) {
|
||||||
|
return alert(unauthInteractionErrorMessage);
|
||||||
|
}
|
||||||
|
states.showCompose = {
|
||||||
|
replyToStatus: status,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const boostStatus = async () => {
|
||||||
|
if (!sameInstance || !authenticated) {
|
||||||
|
return alert(unauthInteractionErrorMessage);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (!reblogged) {
|
||||||
|
const yes = confirm('Boost this post?');
|
||||||
|
if (!yes) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Optimistic
|
||||||
|
states.statuses[sKey] = {
|
||||||
|
...status,
|
||||||
|
reblogged: !reblogged,
|
||||||
|
reblogsCount: reblogsCount + (reblogged ? -1 : 1),
|
||||||
|
};
|
||||||
|
if (reblogged) {
|
||||||
|
const newStatus = await masto.v1.statuses.unreblog(id);
|
||||||
|
saveStatus(newStatus, instance);
|
||||||
|
} else {
|
||||||
|
const newStatus = await masto.v1.statuses.reblog(id);
|
||||||
|
saveStatus(newStatus, instance);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
// Revert optimistism
|
||||||
|
states.statuses[sKey] = status;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const favouriteStatus = async () => {
|
||||||
|
if (!sameInstance || !authenticated) {
|
||||||
|
return alert(unauthInteractionErrorMessage);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// Optimistic
|
||||||
|
states.statuses[sKey] = {
|
||||||
|
...status,
|
||||||
|
favourited: !favourited,
|
||||||
|
favouritesCount: favouritesCount + (favourited ? -1 : 1),
|
||||||
|
};
|
||||||
|
if (favourited) {
|
||||||
|
const newStatus = await masto.v1.statuses.unfavourite(id);
|
||||||
|
saveStatus(newStatus, instance);
|
||||||
|
} else {
|
||||||
|
const newStatus = await masto.v1.statuses.favourite(id);
|
||||||
|
saveStatus(newStatus, instance);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
// Revert optimistism
|
||||||
|
states.statuses[sKey] = status;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const bookmarkStatus = async () => {
|
||||||
|
if (!sameInstance || !authenticated) {
|
||||||
|
return alert(unauthInteractionErrorMessage);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// Optimistic
|
||||||
|
states.statuses[sKey] = {
|
||||||
|
...status,
|
||||||
|
bookmarked: !bookmarked,
|
||||||
|
};
|
||||||
|
if (bookmarked) {
|
||||||
|
const newStatus = await masto.v1.statuses.unbookmark(id);
|
||||||
|
saveStatus(newStatus, instance);
|
||||||
|
} else {
|
||||||
|
const newStatus = await masto.v1.statuses.bookmark(id);
|
||||||
|
saveStatus(newStatus, instance);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
// Revert optimistism
|
||||||
|
states.statuses[sKey] = status;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const StatusMenuItems = (
|
||||||
|
<>
|
||||||
|
{!isSizeLarge && (
|
||||||
|
<>
|
||||||
|
<MenuHeader>
|
||||||
|
<span class="ib">
|
||||||
|
<Icon icon={visibilityIconsMap[visibility]} size="s" />{' '}
|
||||||
|
<span>{visibilityText[visibility]}</span>
|
||||||
|
</span>{' '}
|
||||||
|
<span class="ib">
|
||||||
|
{repliesCount > 0 && (
|
||||||
|
<span>
|
||||||
|
<Icon icon="reply" alt="Replies" size="s" />{' '}
|
||||||
|
<span>{shortenNumber(repliesCount)}</span>
|
||||||
|
</span>
|
||||||
|
)}{' '}
|
||||||
|
{reblogsCount > 0 && (
|
||||||
|
<span>
|
||||||
|
<Icon icon="rocket" alt="Boosts" size="s" />{' '}
|
||||||
|
<span>{shortenNumber(reblogsCount)}</span>
|
||||||
|
</span>
|
||||||
|
)}{' '}
|
||||||
|
{favouritesCount > 0 && (
|
||||||
|
<span>
|
||||||
|
<Icon icon="heart" alt="Favourites" size="s" />{' '}
|
||||||
|
<span>{shortenNumber(favouritesCount)}</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
{createdDateText}
|
||||||
|
</MenuHeader>
|
||||||
|
<MenuLink to={instance ? `/${instance}/s/${id}` : `/s/${id}`}>
|
||||||
|
<Icon icon="arrow-right" />
|
||||||
|
View post and replies
|
||||||
|
</MenuLink>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!!editedAt && (
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
setShowEdited(id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="history" />
|
||||||
|
<span>
|
||||||
|
Show Edit History
|
||||||
|
<br />
|
||||||
|
<small class="more-insignificant">Edited: {editedDateText}</small>
|
||||||
|
</span>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
{(!isSizeLarge || !!editedAt) && <MenuDivider />}
|
||||||
|
{!isSizeLarge && (
|
||||||
|
<>
|
||||||
|
<MenuItem onClick={replyStatus}>
|
||||||
|
<Icon icon="reply" />
|
||||||
|
<span>Reply</span>
|
||||||
|
</MenuItem>
|
||||||
|
{canBoost && (
|
||||||
|
<MenuItem
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
await boostStatus();
|
||||||
|
if (!isSizeLarge)
|
||||||
|
showToast(reblogged ? 'Unboosted' : 'Boosted');
|
||||||
|
} catch (e) {}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="rocket" />
|
||||||
|
<span>{reblogged ? 'Unboost' : 'Boost…'}</span>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
try {
|
||||||
|
favouriteStatus();
|
||||||
|
if (!isSizeLarge)
|
||||||
|
showToast(favourited ? 'Unfavourited' : 'Favourited');
|
||||||
|
} catch (e) {}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="heart" />
|
||||||
|
<span>{favourited ? 'Unfavourite' : 'Favourite'}</span>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
try {
|
||||||
|
bookmarkStatus();
|
||||||
|
if (!isSizeLarge)
|
||||||
|
showToast(bookmarked ? 'Unbookmarked' : 'Bookmarked');
|
||||||
|
} catch (e) {}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="bookmark" />
|
||||||
|
<span>{bookmarked ? 'Unbookmark' : 'Bookmark'}</span>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuDivider />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<MenuItem href={url} target="_blank">
|
||||||
|
<Icon icon="external" />
|
||||||
|
<span>Open link to post</span>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
// Copy url to clipboard
|
||||||
|
try {
|
||||||
|
navigator.clipboard.writeText(url);
|
||||||
|
showToast('Link copied');
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showToast('Unable to copy link');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="link" />
|
||||||
|
<span>Copy link to post</span>
|
||||||
|
</MenuItem>
|
||||||
|
{navigator?.share &&
|
||||||
|
navigator?.canShare?.({
|
||||||
|
url,
|
||||||
|
}) && (
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
try {
|
||||||
|
navigator.share({
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
alert("Sharing doesn't seem to work.");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="share" />
|
||||||
|
<span>Share…</span>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
{isSelf && (
|
||||||
|
<>
|
||||||
|
<MenuDivider />
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
states.showCompose = {
|
||||||
|
editStatus: status,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="pencil" />
|
||||||
|
<span>Edit</span>
|
||||||
|
</MenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article
|
<article
|
||||||
ref={statusRef}
|
ref={statusRef}
|
||||||
|
@ -265,7 +544,7 @@ function Status({
|
||||||
account={status.account}
|
account={status.account}
|
||||||
instance={instance}
|
instance={instance}
|
||||||
showAvatar={size === 's'}
|
showAvatar={size === 's'}
|
||||||
showAcct={size === 'l'}
|
showAcct={isSizeLarge}
|
||||||
/>
|
/>
|
||||||
{/* {inReplyToAccount && !withinContext && size !== 's' && (
|
{/* {inReplyToAccount && !withinContext && size !== 's' && (
|
||||||
<>
|
<>
|
||||||
|
@ -279,22 +558,42 @@ function Status({
|
||||||
{/* </span> */}{' '}
|
{/* </span> */}{' '}
|
||||||
{size !== 'l' &&
|
{size !== 'l' &&
|
||||||
(url ? (
|
(url ? (
|
||||||
<Link
|
<Menu
|
||||||
to={instance ? `/${instance}/s/${id}` : `/s/${id}`}
|
portal={{
|
||||||
class="time"
|
target:
|
||||||
|
document.querySelector('.status-deck') || document.body,
|
||||||
|
}}
|
||||||
|
align="end"
|
||||||
|
offsetY={4}
|
||||||
|
overflow="auto"
|
||||||
|
viewScroll="close"
|
||||||
|
boundingBoxPadding="8 8 8 8"
|
||||||
|
menuButton={
|
||||||
|
<Link
|
||||||
|
to={instance ? `/${instance}/s/${id}` : `/s/${id}`}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
console.log('click', e);
|
||||||
|
}}
|
||||||
|
class="time"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon={visibilityIconsMap[visibility]}
|
||||||
|
alt={visibilityText[visibility]}
|
||||||
|
size="s"
|
||||||
|
/>{' '}
|
||||||
|
<RelativeTime datetime={createdAtDate} format="micro" />
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Icon
|
{StatusMenuItems}
|
||||||
icon={visibilityIconsMap[visibility]}
|
</Menu>
|
||||||
alt={visibility}
|
|
||||||
size="s"
|
|
||||||
/>{' '}
|
|
||||||
<RelativeTime datetime={createdAtDate} format="micro" />
|
|
||||||
</Link>
|
|
||||||
) : (
|
) : (
|
||||||
<span class="time">
|
<span class="time">
|
||||||
<Icon
|
<Icon
|
||||||
icon={visibilityIconsMap[visibility]}
|
icon={visibilityIconsMap[visibility]}
|
||||||
alt={visibility}
|
alt={visibilityText[visibility]}
|
||||||
size="s"
|
size="s"
|
||||||
/>{' '}
|
/>{' '}
|
||||||
<RelativeTime datetime={createdAtDate} format="micro" />
|
<RelativeTime datetime={createdAtDate} format="micro" />
|
||||||
|
@ -337,7 +636,7 @@ function Status({
|
||||||
} ${showSpoiler ? 'show-spoiler' : ''}`}
|
} ${showSpoiler ? 'show-spoiler' : ''}`}
|
||||||
data-content-text-weight={contentTextWeight ? textWeight() : null}
|
data-content-text-weight={contentTextWeight ? textWeight() : null}
|
||||||
style={
|
style={
|
||||||
(size === 'l' || contentTextWeight) && {
|
(isSizeLarge || contentTextWeight) && {
|
||||||
'--content-text-weight': textWeight(),
|
'--content-text-weight': textWeight(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -457,12 +756,12 @@ function Status({
|
||||||
} ${mediaAttachments.length > 4 ? 'media-gt4' : ''}`}
|
} ${mediaAttachments.length > 4 ? 'media-gt4' : ''}`}
|
||||||
>
|
>
|
||||||
{mediaAttachments
|
{mediaAttachments
|
||||||
.slice(0, size === 'l' ? undefined : 4)
|
.slice(0, isSizeLarge ? undefined : 4)
|
||||||
.map((media, i) => (
|
.map((media, i) => (
|
||||||
<Media
|
<Media
|
||||||
key={media.id}
|
key={media.id}
|
||||||
media={media}
|
media={media}
|
||||||
autoAnimate={size === 'l'}
|
autoAnimate={isSizeLarge}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
@ -485,23 +784,13 @@ function Status({
|
||||||
<Card card={card} instance={currentInstance} />
|
<Card card={card} instance={currentInstance} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{size === 'l' && (
|
{isSizeLarge && (
|
||||||
<>
|
<>
|
||||||
<div class="extra-meta">
|
<div class="extra-meta">
|
||||||
<Icon icon={visibilityIconsMap[visibility]} alt={visibility} />{' '}
|
<Icon icon={visibilityIconsMap[visibility]} alt={visibility} />{' '}
|
||||||
<a href={url} target="_blank">
|
<a href={url} target="_blank">
|
||||||
<time class="created" datetime={createdAtDate.toISOString()}>
|
<time class="created" datetime={createdAtDate.toISOString()}>
|
||||||
{Intl.DateTimeFormat('en', {
|
{createdDateText}
|
||||||
// Show year if not current year
|
|
||||||
year:
|
|
||||||
createdAtDate.getFullYear() === currentYear
|
|
||||||
? undefined
|
|
||||||
: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: 'numeric',
|
|
||||||
minute: '2-digit',
|
|
||||||
}).format(createdAtDate)}
|
|
||||||
</time>
|
</time>
|
||||||
</a>
|
</a>
|
||||||
{editedAt && (
|
{editedAt && (
|
||||||
|
@ -515,17 +804,7 @@ function Status({
|
||||||
setShowEdited(id);
|
setShowEdited(id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Intl.DateTimeFormat('en', {
|
{editedDateText}
|
||||||
// Show year if not this year
|
|
||||||
year:
|
|
||||||
editedAtDate.getFullYear() === currentYear
|
|
||||||
? undefined
|
|
||||||
: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: 'numeric',
|
|
||||||
minute: '2-digit',
|
|
||||||
}).format(editedAtDate)}
|
|
||||||
</time>
|
</time>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -538,18 +817,10 @@ function Status({
|
||||||
class="reply-button"
|
class="reply-button"
|
||||||
icon="comment"
|
icon="comment"
|
||||||
count={repliesCount}
|
count={repliesCount}
|
||||||
onClick={() => {
|
onClick={replyStatus}
|
||||||
if (!sameInstance || !authenticated) {
|
|
||||||
return alert(unauthInteractionErrorMessage);
|
|
||||||
}
|
|
||||||
states.showCompose = {
|
|
||||||
replyToStatus: status,
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* TODO: if visibility = private, only can reblog own statuses */}
|
{canBoost && (
|
||||||
{visibility !== 'direct' && (
|
|
||||||
<div class="action has-count">
|
<div class="action has-count">
|
||||||
<StatusButton
|
<StatusButton
|
||||||
checked={reblogged}
|
checked={reblogged}
|
||||||
|
@ -558,38 +829,7 @@ function Status({
|
||||||
class="reblog-button"
|
class="reblog-button"
|
||||||
icon="rocket"
|
icon="rocket"
|
||||||
count={reblogsCount}
|
count={reblogsCount}
|
||||||
onClick={async () => {
|
onClick={boostStatus}
|
||||||
if (!sameInstance || !authenticated) {
|
|
||||||
return alert(unauthInteractionErrorMessage);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
if (!reblogged) {
|
|
||||||
const yes = confirm('Boost this post?');
|
|
||||||
if (!yes) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Optimistic
|
|
||||||
states.statuses[sKey] = {
|
|
||||||
...status,
|
|
||||||
reblogged: !reblogged,
|
|
||||||
reblogsCount: reblogsCount + (reblogged ? -1 : 1),
|
|
||||||
};
|
|
||||||
if (reblogged) {
|
|
||||||
const newStatus = await masto.v1.statuses.unreblog(
|
|
||||||
id,
|
|
||||||
);
|
|
||||||
saveStatus(newStatus, instance);
|
|
||||||
} else {
|
|
||||||
const newStatus = await masto.v1.statuses.reblog(id);
|
|
||||||
saveStatus(newStatus, instance);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
// Revert optimistism
|
|
||||||
states.statuses[sKey] = status;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -601,33 +841,7 @@ function Status({
|
||||||
class="favourite-button"
|
class="favourite-button"
|
||||||
icon="heart"
|
icon="heart"
|
||||||
count={favouritesCount}
|
count={favouritesCount}
|
||||||
onClick={async () => {
|
onClick={favouriteStatus}
|
||||||
if (!sameInstance || !authenticated) {
|
|
||||||
return alert(unauthInteractionErrorMessage);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// Optimistic
|
|
||||||
states.statuses[sKey] = {
|
|
||||||
...status,
|
|
||||||
favourited: !favourited,
|
|
||||||
favouritesCount:
|
|
||||||
favouritesCount + (favourited ? -1 : 1),
|
|
||||||
};
|
|
||||||
if (favourited) {
|
|
||||||
const newStatus = await masto.v1.statuses.unfavourite(
|
|
||||||
id,
|
|
||||||
);
|
|
||||||
saveStatus(newStatus, instance);
|
|
||||||
} else {
|
|
||||||
const newStatus = await masto.v1.statuses.favourite(id);
|
|
||||||
saveStatus(newStatus, instance);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
// Revert optimistism
|
|
||||||
states.statuses[sKey] = status;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="action">
|
<div class="action">
|
||||||
|
@ -637,61 +851,33 @@ function Status({
|
||||||
alt={['Bookmark', 'Bookmarked']}
|
alt={['Bookmark', 'Bookmarked']}
|
||||||
class="bookmark-button"
|
class="bookmark-button"
|
||||||
icon="bookmark"
|
icon="bookmark"
|
||||||
onClick={async () => {
|
onClick={bookmarkStatus}
|
||||||
if (!sameInstance || !authenticated) {
|
|
||||||
return alert(unauthInteractionErrorMessage);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// Optimistic
|
|
||||||
states.statuses[sKey] = {
|
|
||||||
...status,
|
|
||||||
bookmarked: !bookmarked,
|
|
||||||
};
|
|
||||||
if (bookmarked) {
|
|
||||||
const newStatus = await masto.v1.statuses.unbookmark(
|
|
||||||
id,
|
|
||||||
);
|
|
||||||
saveStatus(newStatus, instance);
|
|
||||||
} else {
|
|
||||||
const newStatus = await masto.v1.statuses.bookmark(id);
|
|
||||||
saveStatus(newStatus, instance);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
// Revert optimistism
|
|
||||||
states.statuses[sKey] = status;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{isSelf && (
|
<Menu
|
||||||
<Menu
|
portal={{
|
||||||
align="end"
|
target:
|
||||||
menuButton={
|
document.querySelector('.status-deck') || document.body,
|
||||||
<div class="action">
|
}}
|
||||||
<button
|
align="end"
|
||||||
type="button"
|
offsetY={4}
|
||||||
title="More"
|
overflow="auto"
|
||||||
class="plain more-button"
|
viewScroll="close"
|
||||||
>
|
boundingBoxPadding="8 8 8 8"
|
||||||
<Icon icon="more" size="l" alt="More" />
|
menuButton={
|
||||||
</button>
|
<div class="action">
|
||||||
</div>
|
<button
|
||||||
}
|
type="button"
|
||||||
>
|
title="More"
|
||||||
{isSelf && (
|
class="plain more-button"
|
||||||
<MenuItem
|
|
||||||
onClick={() => {
|
|
||||||
states.showCompose = {
|
|
||||||
editStatus: status,
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Icon icon="pencil" /> <span>Edit…</span>
|
<Icon icon="more" size="l" alt="More" />
|
||||||
</MenuItem>
|
</button>
|
||||||
)}
|
</div>
|
||||||
</Menu>
|
}
|
||||||
)}
|
>
|
||||||
|
{StatusMenuItems}
|
||||||
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -297,6 +297,9 @@ code {
|
||||||
.insignificant {
|
.insignificant {
|
||||||
color: var(--text-insignificant-color);
|
color: var(--text-insignificant-color);
|
||||||
}
|
}
|
||||||
|
.more-insignificant {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
.hide-until-focus-visible {
|
.hide-until-focus-visible {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
@ -7,11 +7,11 @@ import {
|
||||||
} from '@szhsin/react-menu';
|
} from '@szhsin/react-menu';
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import Toastify from 'toastify-js';
|
|
||||||
|
|
||||||
import Icon from '../components/icon';
|
import Icon from '../components/icon';
|
||||||
import Timeline from '../components/timeline';
|
import Timeline from '../components/timeline';
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
|
import showToast from '../utils/show-toast';
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
import useTitle from '../utils/useTitle';
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
|
@ -142,14 +142,7 @@ function Hashtags(props) {
|
||||||
.unfollow(hashtag)
|
.unfollow(hashtag)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setInfo({ ...info, following: false });
|
setInfo({ ...info, following: false });
|
||||||
const toast = Toastify({
|
showToast(`Unfollowed #${hashtag}`);
|
||||||
className: 'shiny-pill',
|
|
||||||
text: `Unfollowed #${hashtag}`,
|
|
||||||
duration: 3000,
|
|
||||||
gravity: 'bottom',
|
|
||||||
position: 'center',
|
|
||||||
});
|
|
||||||
toast.showToast();
|
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
alert(e);
|
alert(e);
|
||||||
|
@ -163,14 +156,7 @@ function Hashtags(props) {
|
||||||
.follow(hashtag)
|
.follow(hashtag)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setInfo({ ...info, following: true });
|
setInfo({ ...info, following: true });
|
||||||
const toast = Toastify({
|
showToast(`Followed #${hashtag}`);
|
||||||
className: 'shiny-pill',
|
|
||||||
text: `Followed #${hashtag}`,
|
|
||||||
duration: 3000,
|
|
||||||
gravity: 'bottom',
|
|
||||||
position: 'center',
|
|
||||||
});
|
|
||||||
toast.showToast();
|
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
alert(e);
|
alert(e);
|
||||||
|
@ -247,9 +233,11 @@ function Hashtags(props) {
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="x" alt="Remove hashtag" class="danger-icon" />{' '}
|
<Icon icon="x" alt="Remove hashtag" class="danger-icon" />
|
||||||
<Icon icon="hashtag" />
|
<span>
|
||||||
<span>{t}</span>
|
<span class="more-insignificant">#</span>
|
||||||
|
{t}
|
||||||
|
</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</MenuGroup>
|
</MenuGroup>
|
||||||
|
@ -278,14 +266,7 @@ function Hashtags(props) {
|
||||||
alert('This shortcut already exists');
|
alert('This shortcut already exists');
|
||||||
} else {
|
} else {
|
||||||
states.shortcuts.push(shortcut);
|
states.shortcuts.push(shortcut);
|
||||||
const toast = Toastify({
|
showToast(`Hashtag shortcut added`);
|
||||||
className: 'shiny-pill',
|
|
||||||
text: `Hashtag shortcut added`,
|
|
||||||
duration: 3000,
|
|
||||||
gravity: 'bottom',
|
|
||||||
position: 'center',
|
|
||||||
});
|
|
||||||
toast.showToast();
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
26
src/utils/show-toast.js
Normal file
26
src/utils/show-toast.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import Toastify from 'toastify-js';
|
||||||
|
|
||||||
|
function showToast(props) {
|
||||||
|
if (typeof props === 'string') {
|
||||||
|
props = { text: props };
|
||||||
|
}
|
||||||
|
const { onClick = () => {}, delay, ...rest } = props;
|
||||||
|
const toast = Toastify({
|
||||||
|
className: 'shiny-pill',
|
||||||
|
gravity: 'bottom',
|
||||||
|
position: 'center',
|
||||||
|
...rest,
|
||||||
|
onClick: () => {
|
||||||
|
onClick(toast); // Pass in the object itself!
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (delay) {
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.showToast();
|
||||||
|
}, delay);
|
||||||
|
} else {
|
||||||
|
toast.showToast();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default showToast;
|
Loading…
Add table
Reference in a new issue