mirror of
https://github.com/cheeaun/phanpy.git
synced 2025-03-14 12:18:30 +03:00
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);
|
||||
border: 1px solid var(--outline-color);
|
||||
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;
|
||||
animation: appear-smooth 0.15s ease-in-out;
|
||||
width: 16em;
|
||||
|
@ -1052,8 +1052,25 @@ body:has(.status-deck) .media-post-link {
|
|||
.szh-menu__item--focusable {
|
||||
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 {
|
||||
display: block;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
line-height: 1;
|
||||
padding: 8px 16px !important;
|
||||
transition: all 0.1s ease-in-out;
|
||||
white-space: nowrap;
|
||||
|
@ -1065,14 +1082,15 @@ body:has(.status-deck) .media-post-link {
|
|||
vertical-align: middle;
|
||||
}
|
||||
.szh-menu .szh-menu__item a {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
padding: 8px 16px !important;
|
||||
margin: -8px -16px !important;
|
||||
gap: 8px;
|
||||
}
|
||||
.szh-menu .szh-menu__item a.is-active {
|
||||
font-weight: bold;
|
||||
|
|
36
src/app.jsx
36
src/app.jsx
|
@ -14,7 +14,6 @@ import {
|
|||
useLocation,
|
||||
useNavigate,
|
||||
} from 'react-router-dom';
|
||||
import Toastify from 'toastify-js';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
import Account from './components/account';
|
||||
|
@ -53,6 +52,7 @@ import {
|
|||
initPreferences,
|
||||
} from './utils/api';
|
||||
import { getAccessToken } from './utils/auth';
|
||||
import showToast from './utils/show-toast';
|
||||
import states, { getStatus, saveStatus } from './utils/states';
|
||||
import store from './utils/store';
|
||||
import { getCurrentAccount } from './utils/store-utils';
|
||||
|
@ -328,26 +328,20 @@ function App() {
|
|||
window.__COMPOSE__ = null;
|
||||
if (newStatus) {
|
||||
states.reloadStatusPage++;
|
||||
setTimeout(() => {
|
||||
const toast = Toastify({
|
||||
className: 'shiny-pill',
|
||||
text: 'Status posted. Check it out.',
|
||||
duration: 10_000, // 10 seconds
|
||||
gravity: 'bottom',
|
||||
position: 'center',
|
||||
// destination: `/#/s/${newStatus.id}`,
|
||||
onClick: () => {
|
||||
toast.hideToast();
|
||||
states.prevLocation = location;
|
||||
navigate(
|
||||
instance
|
||||
? `/${instance}/s/${newStatus.id}`
|
||||
: `/s/${newStatus.id}`,
|
||||
);
|
||||
},
|
||||
});
|
||||
toast.showToast();
|
||||
}, 1000);
|
||||
showToast({
|
||||
text: 'Status posted. Check it out.',
|
||||
delay: 1000,
|
||||
duration: 10_000, // 10 seconds
|
||||
onClick: (toast) => {
|
||||
toast.hideToast();
|
||||
states.prevLocation = location;
|
||||
navigate(
|
||||
instance
|
||||
? `/${instance}/s/${newStatus.id}`
|
||||
: `/s/${newStatus.id}`,
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -58,6 +58,9 @@ const ICONS = {
|
|||
following: 'mingcute:walk-line',
|
||||
pin: 'mingcute:pin-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');
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import './status.css';
|
||||
|
||||
import { Menu, MenuItem } from '@szhsin/react-menu';
|
||||
import { Menu, MenuDivider, MenuHeader, MenuItem } from '@szhsin/react-menu';
|
||||
import mem from 'mem';
|
||||
import pThrottle from 'p-throttle';
|
||||
import { memo } from 'preact/compat';
|
||||
|
@ -17,6 +17,7 @@ import enhanceContent from '../utils/enhance-content';
|
|||
import handleContentLinks from '../utils/handle-content-links';
|
||||
import htmlContentLength from '../utils/html-content-length';
|
||||
import shortenNumber from '../utils/shorten-number';
|
||||
import showToast from '../utils/show-toast';
|
||||
import states, { saveStatus, statusKey } from '../utils/states';
|
||||
import store from '../utils/store';
|
||||
import visibilityIconsMap from '../utils/visibility-icons-map';
|
||||
|
@ -25,6 +26,7 @@ import Avatar from './avatar';
|
|||
import Icon from './icon';
|
||||
import Link from './link';
|
||||
import Media from './media';
|
||||
import MenuLink from './MenuLink';
|
||||
import RelativeTime from './relative-time';
|
||||
|
||||
const throttle = pThrottle({
|
||||
|
@ -41,6 +43,13 @@ function fetchAccount(id, masto) {
|
|||
}
|
||||
const memFetchAccount = mem(fetchAccount);
|
||||
|
||||
const visibilityText = {
|
||||
public: 'Public',
|
||||
unlisted: 'Unlisted',
|
||||
private: 'Followers only',
|
||||
direct: 'Mentioned people only',
|
||||
};
|
||||
|
||||
function Status({
|
||||
statusID,
|
||||
status,
|
||||
|
@ -217,6 +226,276 @@ function Status({
|
|||
const textWeight = () =>
|
||||
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 (
|
||||
<article
|
||||
ref={statusRef}
|
||||
|
@ -265,7 +544,7 @@ function Status({
|
|||
account={status.account}
|
||||
instance={instance}
|
||||
showAvatar={size === 's'}
|
||||
showAcct={size === 'l'}
|
||||
showAcct={isSizeLarge}
|
||||
/>
|
||||
{/* {inReplyToAccount && !withinContext && size !== 's' && (
|
||||
<>
|
||||
|
@ -279,22 +558,42 @@ function Status({
|
|||
{/* </span> */}{' '}
|
||||
{size !== 'l' &&
|
||||
(url ? (
|
||||
<Link
|
||||
to={instance ? `/${instance}/s/${id}` : `/s/${id}`}
|
||||
class="time"
|
||||
<Menu
|
||||
portal={{
|
||||
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
|
||||
icon={visibilityIconsMap[visibility]}
|
||||
alt={visibility}
|
||||
size="s"
|
||||
/>{' '}
|
||||
<RelativeTime datetime={createdAtDate} format="micro" />
|
||||
</Link>
|
||||
{StatusMenuItems}
|
||||
</Menu>
|
||||
) : (
|
||||
<span class="time">
|
||||
<Icon
|
||||
icon={visibilityIconsMap[visibility]}
|
||||
alt={visibility}
|
||||
alt={visibilityText[visibility]}
|
||||
size="s"
|
||||
/>{' '}
|
||||
<RelativeTime datetime={createdAtDate} format="micro" />
|
||||
|
@ -337,7 +636,7 @@ function Status({
|
|||
} ${showSpoiler ? 'show-spoiler' : ''}`}
|
||||
data-content-text-weight={contentTextWeight ? textWeight() : null}
|
||||
style={
|
||||
(size === 'l' || contentTextWeight) && {
|
||||
(isSizeLarge || contentTextWeight) && {
|
||||
'--content-text-weight': textWeight(),
|
||||
}
|
||||
}
|
||||
|
@ -457,12 +756,12 @@ function Status({
|
|||
} ${mediaAttachments.length > 4 ? 'media-gt4' : ''}`}
|
||||
>
|
||||
{mediaAttachments
|
||||
.slice(0, size === 'l' ? undefined : 4)
|
||||
.slice(0, isSizeLarge ? undefined : 4)
|
||||
.map((media, i) => (
|
||||
<Media
|
||||
key={media.id}
|
||||
media={media}
|
||||
autoAnimate={size === 'l'}
|
||||
autoAnimate={isSizeLarge}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
@ -485,23 +784,13 @@ function Status({
|
|||
<Card card={card} instance={currentInstance} />
|
||||
)}
|
||||
</div>
|
||||
{size === 'l' && (
|
||||
{isSizeLarge && (
|
||||
<>
|
||||
<div class="extra-meta">
|
||||
<Icon icon={visibilityIconsMap[visibility]} alt={visibility} />{' '}
|
||||
<a href={url} target="_blank">
|
||||
<time class="created" datetime={createdAtDate.toISOString()}>
|
||||
{Intl.DateTimeFormat('en', {
|
||||
// Show year if not current year
|
||||
year:
|
||||
createdAtDate.getFullYear() === currentYear
|
||||
? undefined
|
||||
: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
}).format(createdAtDate)}
|
||||
{createdDateText}
|
||||
</time>
|
||||
</a>
|
||||
{editedAt && (
|
||||
|
@ -515,17 +804,7 @@ function Status({
|
|||
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)}
|
||||
{editedDateText}
|
||||
</time>
|
||||
</>
|
||||
)}
|
||||
|
@ -538,18 +817,10 @@ function Status({
|
|||
class="reply-button"
|
||||
icon="comment"
|
||||
count={repliesCount}
|
||||
onClick={() => {
|
||||
if (!sameInstance || !authenticated) {
|
||||
return alert(unauthInteractionErrorMessage);
|
||||
}
|
||||
states.showCompose = {
|
||||
replyToStatus: status,
|
||||
};
|
||||
}}
|
||||
onClick={replyStatus}
|
||||
/>
|
||||
</div>
|
||||
{/* TODO: if visibility = private, only can reblog own statuses */}
|
||||
{visibility !== 'direct' && (
|
||||
{canBoost && (
|
||||
<div class="action has-count">
|
||||
<StatusButton
|
||||
checked={reblogged}
|
||||
|
@ -558,38 +829,7 @@ function Status({
|
|||
class="reblog-button"
|
||||
icon="rocket"
|
||||
count={reblogsCount}
|
||||
onClick={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;
|
||||
}
|
||||
}}
|
||||
onClick={boostStatus}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
@ -601,33 +841,7 @@ function Status({
|
|||
class="favourite-button"
|
||||
icon="heart"
|
||||
count={favouritesCount}
|
||||
onClick={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;
|
||||
}
|
||||
}}
|
||||
onClick={favouriteStatus}
|
||||
/>
|
||||
</div>
|
||||
<div class="action">
|
||||
|
@ -637,61 +851,33 @@ function Status({
|
|||
alt={['Bookmark', 'Bookmarked']}
|
||||
class="bookmark-button"
|
||||
icon="bookmark"
|
||||
onClick={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;
|
||||
}
|
||||
}}
|
||||
onClick={bookmarkStatus}
|
||||
/>
|
||||
</div>
|
||||
{isSelf && (
|
||||
<Menu
|
||||
align="end"
|
||||
menuButton={
|
||||
<div class="action">
|
||||
<button
|
||||
type="button"
|
||||
title="More"
|
||||
class="plain more-button"
|
||||
>
|
||||
<Icon icon="more" size="l" alt="More" />
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{isSelf && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
states.showCompose = {
|
||||
editStatus: status,
|
||||
};
|
||||
}}
|
||||
<Menu
|
||||
portal={{
|
||||
target:
|
||||
document.querySelector('.status-deck') || document.body,
|
||||
}}
|
||||
align="end"
|
||||
offsetY={4}
|
||||
overflow="auto"
|
||||
viewScroll="close"
|
||||
boundingBoxPadding="8 8 8 8"
|
||||
menuButton={
|
||||
<div class="action">
|
||||
<button
|
||||
type="button"
|
||||
title="More"
|
||||
class="plain more-button"
|
||||
>
|
||||
<Icon icon="pencil" /> <span>Edit…</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
)}
|
||||
<Icon icon="more" size="l" alt="More" />
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{StatusMenuItems}
|
||||
</Menu>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -297,6 +297,9 @@ code {
|
|||
.insignificant {
|
||||
color: var(--text-insignificant-color);
|
||||
}
|
||||
.more-insignificant {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.hide-until-focus-visible {
|
||||
display: none;
|
||||
|
|
|
@ -7,11 +7,11 @@ import {
|
|||
} from '@szhsin/react-menu';
|
||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import Toastify from 'toastify-js';
|
||||
|
||||
import Icon from '../components/icon';
|
||||
import Timeline from '../components/timeline';
|
||||
import { api } from '../utils/api';
|
||||
import showToast from '../utils/show-toast';
|
||||
import states from '../utils/states';
|
||||
import useTitle from '../utils/useTitle';
|
||||
|
||||
|
@ -142,14 +142,7 @@ function Hashtags(props) {
|
|||
.unfollow(hashtag)
|
||||
.then(() => {
|
||||
setInfo({ ...info, following: false });
|
||||
const toast = Toastify({
|
||||
className: 'shiny-pill',
|
||||
text: `Unfollowed #${hashtag}`,
|
||||
duration: 3000,
|
||||
gravity: 'bottom',
|
||||
position: 'center',
|
||||
});
|
||||
toast.showToast();
|
||||
showToast(`Unfollowed #${hashtag}`);
|
||||
})
|
||||
.catch((e) => {
|
||||
alert(e);
|
||||
|
@ -163,14 +156,7 @@ function Hashtags(props) {
|
|||
.follow(hashtag)
|
||||
.then(() => {
|
||||
setInfo({ ...info, following: true });
|
||||
const toast = Toastify({
|
||||
className: 'shiny-pill',
|
||||
text: `Followed #${hashtag}`,
|
||||
duration: 3000,
|
||||
gravity: 'bottom',
|
||||
position: 'center',
|
||||
});
|
||||
toast.showToast();
|
||||
showToast(`Followed #${hashtag}`);
|
||||
})
|
||||
.catch((e) => {
|
||||
alert(e);
|
||||
|
@ -247,9 +233,11 @@ function Hashtags(props) {
|
|||
);
|
||||
}}
|
||||
>
|
||||
<Icon icon="x" alt="Remove hashtag" class="danger-icon" />{' '}
|
||||
<Icon icon="hashtag" />
|
||||
<span>{t}</span>
|
||||
<Icon icon="x" alt="Remove hashtag" class="danger-icon" />
|
||||
<span>
|
||||
<span class="more-insignificant">#</span>
|
||||
{t}
|
||||
</span>
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuGroup>
|
||||
|
@ -278,14 +266,7 @@ function Hashtags(props) {
|
|||
alert('This shortcut already exists');
|
||||
} else {
|
||||
states.shortcuts.push(shortcut);
|
||||
const toast = Toastify({
|
||||
className: 'shiny-pill',
|
||||
text: `Hashtag shortcut added`,
|
||||
duration: 3000,
|
||||
gravity: 'bottom',
|
||||
position: 'center',
|
||||
});
|
||||
toast.showToast();
|
||||
showToast(`Hashtag shortcut added`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
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