MVP Scheduled Posts implementation
Somehow the CSS got formatted differently
This commit is contained in:
parent
66ab13e63f
commit
7b1d6741dd
16 changed files with 1156 additions and 367 deletions
|
@ -43,6 +43,7 @@ import Login from './pages/login';
|
|||
import Mentions from './pages/mentions';
|
||||
import Notifications from './pages/notifications';
|
||||
import Public from './pages/public';
|
||||
import ScheduledPosts from './pages/scheduled-posts';
|
||||
import Search from './pages/search';
|
||||
import StatusRoute from './pages/status-route';
|
||||
import Trending from './pages/trending';
|
||||
|
@ -548,6 +549,7 @@ function SecondaryRoutes({ isLoggedIn }) {
|
|||
<Route path=":id" element={<List />} />
|
||||
</Route>
|
||||
<Route path="/fh" element={<FollowedHashtags />} />
|
||||
<Route path="/sp" element={<ScheduledPosts />} />
|
||||
<Route path="/ft" element={<Filters />} />
|
||||
<Route path="/catchup" element={<Catchup />} />
|
||||
<Route path="/annual_report/:year" element={<AnnualReport />} />
|
||||
|
|
|
@ -175,4 +175,7 @@ export const ICONS = {
|
|||
'user-x': () => import('@iconify-icons/mingcute/user-x-line'),
|
||||
minimize: () => import('@iconify-icons/mingcute/arrows-down-line'),
|
||||
celebrate: () => import('@iconify-icons/mingcute/celebrate-line'),
|
||||
schedule: () => import('@iconify-icons/mingcute/calendar-time-add-line'),
|
||||
month: () => import('@iconify-icons/mingcute/calendar-month-line'),
|
||||
day: () => import('@iconify-icons/mingcute/calendar-day-line'),
|
||||
};
|
||||
|
|
75
src/components/ScheduledAtField.jsx
Normal file
75
src/components/ScheduledAtField.jsx
Normal file
|
@ -0,0 +1,75 @@
|
|||
import { useEffect, useState } from 'preact/hooks';
|
||||
|
||||
export const MIN_SCHEDULED_AT = 6 * 60 * 1000; // 6 mins
|
||||
const MAX_SCHEDULED_AT = 90 * 24 * 60 * 60 * 1000; // 90 days
|
||||
|
||||
export default function ScheduledAtField({ scheduledAt, setScheduledAt }) {
|
||||
if (!scheduledAt || !(scheduledAt instanceof Date)) {
|
||||
console.warn('scheduledAt is not a Date:', scheduledAt);
|
||||
return;
|
||||
}
|
||||
const [minStr, setMinStr] = useState();
|
||||
const [maxStr, setMaxStr] = useState();
|
||||
const timezoneOffset = scheduledAt.getTimezoneOffset();
|
||||
|
||||
useEffect(() => {
|
||||
function updateMinStr() {
|
||||
const min = new Date(Date.now() + MIN_SCHEDULED_AT);
|
||||
const str = new Date(min.getTime() - timezoneOffset * 60000)
|
||||
.toISOString()
|
||||
.slice(0, 16);
|
||||
setMinStr(str);
|
||||
console.log('MIN', min);
|
||||
}
|
||||
updateMinStr();
|
||||
|
||||
function updateMaxStr() {
|
||||
const max = new Date(Date.now() + MAX_SCHEDULED_AT);
|
||||
const str = new Date(max.getTime() - timezoneOffset * 60000)
|
||||
.toISOString()
|
||||
.slice(0, 16);
|
||||
setMaxStr(str);
|
||||
console.log('MAX', max);
|
||||
}
|
||||
updateMaxStr();
|
||||
|
||||
// Update every 10s
|
||||
const intervalId = setInterval(() => {
|
||||
updateMinStr();
|
||||
updateMaxStr();
|
||||
}, 1000 * 10);
|
||||
return () => clearInterval(intervalId);
|
||||
}, []);
|
||||
|
||||
const defaultValue = scheduledAt
|
||||
? new Date(scheduledAt.getTime() - scheduledAt.getTimezoneOffset() * 60000)
|
||||
.toISOString()
|
||||
.slice(0, 16)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<input
|
||||
type="datetime-local"
|
||||
name="scheduledAt"
|
||||
defaultValue={defaultValue}
|
||||
min={minStr}
|
||||
max={maxStr}
|
||||
required
|
||||
onChange={(e) => {
|
||||
setScheduledAt(new Date(e.target.value));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function getLocalTimezoneName() {
|
||||
const date = new Date();
|
||||
const formatter = new Intl.DateTimeFormat(undefined, {
|
||||
timeZoneName: 'long',
|
||||
});
|
||||
const parts = formatter.formatToParts(date);
|
||||
const timezoneName = parts.find(
|
||||
(part) => part.type === 'timeZoneName',
|
||||
)?.value;
|
||||
return timezoneName;
|
||||
}
|
|
@ -94,8 +94,11 @@
|
|||
); */
|
||||
border-top: var(--hairline-width) solid var(--outline-color);
|
||||
backdrop-filter: blur(8px);
|
||||
text-shadow: 0 1px 10px var(--bg-color), 0 1px 10px var(--bg-color),
|
||||
0 1px 10px var(--bg-color), 0 1px 10px var(--bg-color),
|
||||
text-shadow:
|
||||
0 1px 10px var(--bg-color),
|
||||
0 1px 10px var(--bg-color),
|
||||
0 1px 10px var(--bg-color),
|
||||
0 1px 10px var(--bg-color),
|
||||
0 1px 10px var(--bg-color);
|
||||
z-index: 2;
|
||||
|
||||
|
@ -134,7 +137,9 @@
|
|||
}
|
||||
}
|
||||
#compose-container .status-preview ~ form {
|
||||
box-shadow: var(--drop-shadow), 0 -3px 6px -3px var(--drop-shadow-color);
|
||||
box-shadow:
|
||||
var(--drop-shadow),
|
||||
0 -3px 6px -3px var(--drop-shadow-color);
|
||||
}
|
||||
|
||||
#compose-container textarea {
|
||||
|
@ -412,16 +417,17 @@
|
|||
width: 80px;
|
||||
height: 80px;
|
||||
/* checkerboard background */
|
||||
background-image: linear-gradient(
|
||||
45deg,
|
||||
var(--img-bg-color) 25%,
|
||||
transparent 25%
|
||||
),
|
||||
background-image:
|
||||
linear-gradient(45deg, var(--img-bg-color) 25%, transparent 25%),
|
||||
linear-gradient(-45deg, var(--img-bg-color) 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, var(--img-bg-color) 75%),
|
||||
linear-gradient(-45deg, transparent 75%, var(--img-bg-color) 75%);
|
||||
background-size: 10px 10px;
|
||||
background-position: 0 0, 0 5px, 5px -5px, -5px 0px;
|
||||
background-position:
|
||||
0 0,
|
||||
0 5px,
|
||||
5px -5px,
|
||||
-5px 0px;
|
||||
}
|
||||
#compose-container .media-preview > * {
|
||||
width: 80px;
|
||||
|
@ -562,6 +568,25 @@
|
|||
color: var(--red-color);
|
||||
}
|
||||
|
||||
#compose-container {
|
||||
.scheduled-at {
|
||||
background-color: var(--bg-faded-color);
|
||||
border-radius: 8px;
|
||||
margin: 8px 0 0;
|
||||
justify-content: flex-end;
|
||||
text-align: end;
|
||||
|
||||
input[type='datetime-local'] {
|
||||
max-width: 80vw;
|
||||
padding: 4px;
|
||||
|
||||
&:invalid {
|
||||
border-color: var(--red-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.compose-menu-add-media {
|
||||
position: relative;
|
||||
|
||||
|
@ -660,16 +685,17 @@
|
|||
overflow: hidden;
|
||||
box-shadow: 0 2px 16px var(--img-bg-color);
|
||||
/* checkerboard background */
|
||||
background-image: linear-gradient(
|
||||
45deg,
|
||||
var(--img-bg-color) 25%,
|
||||
transparent 25%
|
||||
),
|
||||
background-image:
|
||||
linear-gradient(45deg, var(--img-bg-color) 25%, transparent 25%),
|
||||
linear-gradient(-45deg, var(--img-bg-color) 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, var(--img-bg-color) 75%),
|
||||
linear-gradient(-45deg, transparent 75%, var(--img-bg-color) 75%);
|
||||
background-size: 20px 20px;
|
||||
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
|
||||
background-position:
|
||||
0 0,
|
||||
0 10px,
|
||||
10px -10px,
|
||||
-10px 0px;
|
||||
flex: 0.8;
|
||||
}
|
||||
#media-sheet .media-preview > * {
|
||||
|
|
|
@ -59,6 +59,10 @@ import AccountBlock from './account-block';
|
|||
import Icon from './icon';
|
||||
import Loader from './loader';
|
||||
import Modal from './modal';
|
||||
import ScheduledAtField, {
|
||||
getLocalTimezoneName,
|
||||
MIN_SCHEDULED_AT,
|
||||
} from './ScheduledAtField';
|
||||
import Status from './status';
|
||||
|
||||
const {
|
||||
|
@ -207,6 +211,7 @@ const ADD_LABELS = {
|
|||
customEmoji: msg`Add custom emoji`,
|
||||
gif: msg`Add GIF`,
|
||||
poll: msg`Add poll`,
|
||||
scheduledPost: msg`Schedule post`,
|
||||
};
|
||||
|
||||
function Compose({
|
||||
|
@ -265,6 +270,7 @@ function Compose({
|
|||
const prevLanguage = useRef(language);
|
||||
const [mediaAttachments, setMediaAttachments] = useState([]);
|
||||
const [poll, setPoll] = useState(null);
|
||||
const [scheduledAt, setScheduledAt] = useState(null);
|
||||
|
||||
const prefs = store.account.get('preferences') || {};
|
||||
|
||||
|
@ -375,6 +381,7 @@ function Compose({
|
|||
sensitive,
|
||||
poll,
|
||||
mediaAttachments,
|
||||
scheduledAt,
|
||||
} = draftStatus;
|
||||
const composablePoll = !!poll?.options && {
|
||||
...poll,
|
||||
|
@ -394,6 +401,7 @@ function Compose({
|
|||
if (sensitive !== null) setSensitive(sensitive);
|
||||
if (composablePoll) setPoll(composablePoll);
|
||||
if (mediaAttachments) setMediaAttachments(mediaAttachments);
|
||||
if (scheduledAt) setScheduledAt(scheduledAt);
|
||||
}
|
||||
}, [draftStatus, editStatus, replyToStatus]);
|
||||
|
||||
|
@ -574,6 +582,7 @@ function Compose({
|
|||
sensitive,
|
||||
poll,
|
||||
mediaAttachments,
|
||||
scheduledAt,
|
||||
},
|
||||
};
|
||||
if (
|
||||
|
@ -773,6 +782,13 @@ function Compose({
|
|||
},
|
||||
});
|
||||
|
||||
const showScheduledAt = !editStatus;
|
||||
const scheduledAtButtonDisabled = uiState === 'loading' || !!scheduledAt;
|
||||
const onScheduledAtClick = () => {
|
||||
const date = new Date(Date.now() + MIN_SCHEDULED_AT);
|
||||
setScheduledAt(date);
|
||||
};
|
||||
|
||||
return (
|
||||
<div id="compose-container-outer">
|
||||
<div id="compose-container" class={standalone ? 'standalone' : ''}>
|
||||
|
@ -827,6 +843,7 @@ function Compose({
|
|||
sensitive,
|
||||
poll,
|
||||
mediaAttachments,
|
||||
scheduledAt,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -915,6 +932,7 @@ function Compose({
|
|||
sensitive,
|
||||
poll,
|
||||
mediaAttachments,
|
||||
scheduledAt,
|
||||
},
|
||||
};
|
||||
window.opener.__COMPOSE__ = passData; // Pass it here instead of `showCompose` due to some weird proxy issue again
|
||||
|
@ -991,11 +1009,17 @@ function Compose({
|
|||
const formData = new FormData(e.target);
|
||||
const entries = Object.fromEntries(formData.entries());
|
||||
console.log('ENTRIES', entries);
|
||||
let { status, visibility, sensitive, spoilerText } = entries;
|
||||
let { status, visibility, sensitive, spoilerText, scheduledAt } =
|
||||
entries;
|
||||
|
||||
// Pre-cleanup
|
||||
sensitive = sensitive === 'on'; // checkboxes return "on" if checked
|
||||
|
||||
// Convert datetime-local input value to RFC3339 Date string value
|
||||
scheduledAt = scheduledAt
|
||||
? new Date(scheduledAt).toISOString()
|
||||
: undefined;
|
||||
|
||||
// Validation
|
||||
/* Let the backend validate this
|
||||
if (stringLength(status) > maxCharacters) {
|
||||
|
@ -1125,6 +1149,7 @@ function Compose({
|
|||
params.visibility = visibility;
|
||||
// params.inReplyToId = replyToStatus?.id || undefined;
|
||||
params.in_reply_to_id = replyToStatus?.id || undefined;
|
||||
params.scheduled_at = scheduledAt;
|
||||
}
|
||||
params = removeNullUndefined(params);
|
||||
console.log('POST', params);
|
||||
|
@ -1161,6 +1186,7 @@ function Compose({
|
|||
type: editStatus ? 'edit' : replyToStatus ? 'reply' : 'post',
|
||||
newStatus,
|
||||
instance,
|
||||
scheduledAt,
|
||||
});
|
||||
} catch (e) {
|
||||
states.composerState.publishing = false;
|
||||
|
@ -1359,6 +1385,30 @@ function Compose({
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
{scheduledAt && (
|
||||
<div class="toolbar scheduled-at">
|
||||
<button
|
||||
type="button"
|
||||
class="plain4 small"
|
||||
onClick={() => {
|
||||
setScheduledAt(null);
|
||||
}}
|
||||
>
|
||||
<Icon icon="x" />
|
||||
</button>
|
||||
<label>
|
||||
<Trans>
|
||||
Posting on{' '}
|
||||
<ScheduledAtField
|
||||
scheduledAt={scheduledAt}
|
||||
setScheduledAt={setScheduledAt}
|
||||
/>
|
||||
</Trans>
|
||||
<br />
|
||||
<small>{getLocalTimezoneName()}</small>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
<div class="toolbar compose-footer">
|
||||
<span class="add-toolbar-button-group spacer">
|
||||
{showAddButton && (
|
||||
|
@ -1418,12 +1468,23 @@ function Compose({
|
|||
<span>{_(ADD_LABELS.gif)}</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem
|
||||
disabled={pollButtonDisabled}
|
||||
onClick={onPollButtonClick}
|
||||
>
|
||||
<Icon icon="poll" /> <span>{_(ADD_LABELS.poll)}</span>
|
||||
</MenuItem>
|
||||
{showPollButton && (
|
||||
<MenuItem
|
||||
disabled={pollButtonDisabled}
|
||||
onClick={onPollButtonClick}
|
||||
>
|
||||
<Icon icon="poll" /> <span>{_(ADD_LABELS.poll)}</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
{showScheduledAt && (
|
||||
<MenuItem
|
||||
disabled={scheduledAtButtonDisabled}
|
||||
onClick={onScheduledAtClick}
|
||||
>
|
||||
<Icon icon="schedule" />{' '}
|
||||
<span>{_(ADD_LABELS.scheduledPost)}</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu2>
|
||||
)}
|
||||
<span class="add-sub-toolbar-button-group" ref={addSubToolbarRef}>
|
||||
|
@ -1476,7 +1537,6 @@ function Compose({
|
|||
/>
|
||||
</button>
|
||||
)}
|
||||
{}
|
||||
{showPollButton && (
|
||||
<>
|
||||
<button
|
||||
|
@ -1489,6 +1549,16 @@ function Compose({
|
|||
</button>
|
||||
</>
|
||||
)}
|
||||
{showScheduledAt && (
|
||||
<button
|
||||
type="button"
|
||||
class={`toolbar-button ${scheduledAt ? 'highlight' : ''}`}
|
||||
disabled={scheduledAtButtonDisabled}
|
||||
onClick={onScheduledAtClick}
|
||||
>
|
||||
<Icon icon="schedule" alt={_(ADD_LABELS.scheduledPost)} />
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
{/* <div class="spacer" /> */}
|
||||
|
@ -1551,14 +1621,16 @@ function Compose({
|
|||
</select>
|
||||
</label>{' '}
|
||||
<button type="submit" disabled={uiState === 'loading'}>
|
||||
{replyToStatus
|
||||
? t`Reply`
|
||||
: editStatus
|
||||
? t`Update`
|
||||
: t({
|
||||
message: 'Post',
|
||||
context: 'Submit button in composer',
|
||||
})}
|
||||
{scheduledAt
|
||||
? t`Schedule`
|
||||
: replyToStatus
|
||||
? t`Reply`
|
||||
: editStatus
|
||||
? t`Update`
|
||||
: t({
|
||||
message: 'Post',
|
||||
context: 'Submit button in composer',
|
||||
})}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -63,15 +63,20 @@ export default function Modals() {
|
|||
null
|
||||
}
|
||||
onClose={(results) => {
|
||||
const { newStatus, instance, type } = results || {};
|
||||
const { newStatus, instance, type, scheduledAt } = results || {};
|
||||
states.showCompose = false;
|
||||
window.__COMPOSE__ = null;
|
||||
if (newStatus) {
|
||||
states.reloadStatusPage++;
|
||||
if (scheduledAt) states.reloadScheduledPosts++;
|
||||
showToast({
|
||||
text: {
|
||||
post: t`Post published. Check it out.`,
|
||||
reply: t`Reply posted. Check it out.`,
|
||||
post: scheduledAt
|
||||
? t`Post scheduled`
|
||||
: t`Post published. Check it out.`,
|
||||
reply: scheduledAt
|
||||
? t`Reply scheduled`
|
||||
: t`Reply posted. Check it out.`,
|
||||
edit: t`Post updated. Check it out.`,
|
||||
}[type || 'post'],
|
||||
delay: 1000,
|
||||
|
@ -79,11 +84,15 @@ export default function Modals() {
|
|||
onClick: (toast) => {
|
||||
toast.hideToast();
|
||||
states.prevLocation = location;
|
||||
navigate(
|
||||
instance
|
||||
? `/${instance}/s/${newStatus.id}`
|
||||
: `/s/${newStatus.id}`,
|
||||
);
|
||||
if (scheduledAt) {
|
||||
navigate('/sp');
|
||||
} else {
|
||||
navigate(
|
||||
instance
|
||||
? `/${instance}/s/${newStatus.id}`
|
||||
: `/s/${newStatus.id}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ function NameText({
|
|||
onClick,
|
||||
}) {
|
||||
const { i18n } = useLingui();
|
||||
if (!account) return null;
|
||||
const {
|
||||
acct,
|
||||
avatar,
|
||||
|
|
|
@ -254,6 +254,12 @@ function NavMenu(props) {
|
|||
<Trans>Followed Hashtags</Trans>
|
||||
</span>
|
||||
</MenuLink>
|
||||
<MenuLink to="/sp">
|
||||
<Icon icon="schedule" size="l" />{' '}
|
||||
<span>
|
||||
<Trans>Scheduled Posts</Trans>
|
||||
</span>
|
||||
</MenuLink>
|
||||
<MenuDivider />
|
||||
{supports('@mastodon/filters') && (
|
||||
<MenuLink to="/ft">
|
||||
|
|
|
@ -27,7 +27,7 @@ export default function Poll({
|
|||
ownVotes,
|
||||
voted,
|
||||
votersCount,
|
||||
votesCount,
|
||||
votesCount = 0,
|
||||
emojis,
|
||||
} = poll;
|
||||
const expiresAtDate = !!expiresAt && new Date(expiresAt); // Update poll at point of expiry
|
||||
|
|
|
@ -40,13 +40,15 @@ const rtfFromNow = (date) => {
|
|||
const seconds = (date.getTime() - Date.now()) / 1000;
|
||||
const absSeconds = Math.abs(seconds);
|
||||
if (absSeconds < minute) {
|
||||
return rtf.format(seconds, 'second');
|
||||
return rtf.format(Math.floor(seconds), 'second');
|
||||
} else if (absSeconds < hour) {
|
||||
return rtf.format(Math.floor(seconds / minute), 'minute');
|
||||
} else if (absSeconds < day) {
|
||||
return rtf.format(Math.floor(seconds / hour), 'hour');
|
||||
} else {
|
||||
} else if (absSeconds < 30 * day) {
|
||||
return rtf.format(Math.floor(seconds / day), 'day');
|
||||
} else {
|
||||
return rtf.format(Math.floor(seconds / day / 30), 'month');
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -76,7 +78,8 @@ export default function RelativeTime({ datetime, format }) {
|
|||
const [renderCount, rerender] = useReducer((x) => x + 1, 0);
|
||||
const date = useMemo(() => new Date(datetime), [datetime]);
|
||||
const [dateStr, dt, title] = useMemo(() => {
|
||||
if (!isValidDate(date)) return ['' + datetime, '', ''];
|
||||
if (!isValidDate(date))
|
||||
return ['' + (typeof datetime === 'string' ? datetime : ''), '', ''];
|
||||
let str;
|
||||
if (format === 'micro') {
|
||||
// If date <= 1 day ago or day is within this year
|
||||
|
|
|
@ -201,7 +201,7 @@ const PostContent =
|
|||
}
|
||||
}
|
||||
divRef.current.replaceChildren(dom.cloneNode(true));
|
||||
}, [content, emojis.length]);
|
||||
}, [content, emojis?.length]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -358,7 +358,7 @@ function Status({
|
|||
emojis: accountEmojis,
|
||||
bot,
|
||||
group,
|
||||
},
|
||||
} = {},
|
||||
id,
|
||||
repliesCount,
|
||||
reblogged,
|
||||
|
@ -428,7 +428,7 @@ function Status({
|
|||
return null;
|
||||
}
|
||||
|
||||
console.debug('RENDER Status', id, status?.account.displayName, quoted);
|
||||
console.debug('RENDER Status', id, status?.account?.displayName, quoted);
|
||||
|
||||
const debugHover = (e) => {
|
||||
if (e.shiftKey) {
|
||||
|
@ -646,7 +646,7 @@ function Status({
|
|||
[spoilerText, content],
|
||||
);
|
||||
|
||||
const createdDateText = niceDateTime(createdAtDate);
|
||||
const createdDateText = createdAt && niceDateTime(createdAtDate);
|
||||
const editedDateText = editedAt && niceDateTime(editedAtDate);
|
||||
|
||||
// Can boost if:
|
||||
|
@ -1792,146 +1792,148 @@ function Status({
|
|||
</a>
|
||||
)}
|
||||
<div class="container">
|
||||
<div class="meta">
|
||||
<span class="meta-name">
|
||||
<NameText
|
||||
account={status.account}
|
||||
instance={instance}
|
||||
showAvatar={size === 's'}
|
||||
showAcct={isSizeLarge}
|
||||
/>
|
||||
</span>
|
||||
{/* {inReplyToAccount && !withinContext && size !== 's' && (
|
||||
<>
|
||||
{' '}
|
||||
<span class="ib">
|
||||
<Icon icon="arrow-right" class="arrow" />{' '}
|
||||
<NameText account={inReplyToAccount} instance={instance} short />
|
||||
</span>
|
||||
</>
|
||||
)} */}
|
||||
{/* </span> */}{' '}
|
||||
{size !== 'l' &&
|
||||
(_deleted ? (
|
||||
<span class="status-deleted-tag">
|
||||
<Trans>Deleted</Trans>
|
||||
</span>
|
||||
) : url && !previewMode && !readOnly && !quoted ? (
|
||||
<Link
|
||||
to={instance ? `/${instance}/s/${id}` : `/s/${id}`}
|
||||
onClick={(e) => {
|
||||
if (
|
||||
e.metaKey ||
|
||||
e.ctrlKey ||
|
||||
e.shiftKey ||
|
||||
e.altKey ||
|
||||
e.which === 2
|
||||
) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onStatusLinkClick?.(e, status);
|
||||
setContextMenuProps({
|
||||
anchorRef: {
|
||||
current: e.currentTarget,
|
||||
},
|
||||
align: 'end',
|
||||
direction: 'bottom',
|
||||
gap: 4,
|
||||
});
|
||||
setIsContextMenuOpen(true);
|
||||
}}
|
||||
class={`time ${
|
||||
isContextMenuOpen && contextMenuProps?.anchorRef
|
||||
? 'is-open'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
{showCommentHint && !showCommentCount ? (
|
||||
<Icon
|
||||
icon="comment2"
|
||||
size="s"
|
||||
// alt={`${repliesCount} ${
|
||||
// repliesCount === 1 ? 'reply' : 'replies'
|
||||
// }`}
|
||||
alt={plural(repliesCount, {
|
||||
one: '# reply',
|
||||
other: '# replies',
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
visibility !== 'public' &&
|
||||
visibility !== 'direct' && (
|
||||
{!!(status.account || createdAt) && (
|
||||
<div class="meta">
|
||||
<span class="meta-name">
|
||||
<NameText
|
||||
account={status.account}
|
||||
instance={instance}
|
||||
showAvatar={size === 's'}
|
||||
showAcct={isSizeLarge}
|
||||
/>
|
||||
</span>
|
||||
{/* {inReplyToAccount && !withinContext && size !== 's' && (
|
||||
<>
|
||||
{' '}
|
||||
<span class="ib">
|
||||
<Icon icon="arrow-right" class="arrow" />{' '}
|
||||
<NameText account={inReplyToAccount} instance={instance} short />
|
||||
</span>
|
||||
</>
|
||||
)} */}
|
||||
{/* </span> */}{' '}
|
||||
{size !== 'l' &&
|
||||
(_deleted ? (
|
||||
<span class="status-deleted-tag">
|
||||
<Trans>Deleted</Trans>
|
||||
</span>
|
||||
) : url && !previewMode && !readOnly && !quoted ? (
|
||||
<Link
|
||||
to={instance ? `/${instance}/s/${id}` : `/s/${id}`}
|
||||
onClick={(e) => {
|
||||
if (
|
||||
e.metaKey ||
|
||||
e.ctrlKey ||
|
||||
e.shiftKey ||
|
||||
e.altKey ||
|
||||
e.which === 2
|
||||
) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onStatusLinkClick?.(e, status);
|
||||
setContextMenuProps({
|
||||
anchorRef: {
|
||||
current: e.currentTarget,
|
||||
},
|
||||
align: 'end',
|
||||
direction: 'bottom',
|
||||
gap: 4,
|
||||
});
|
||||
setIsContextMenuOpen(true);
|
||||
}}
|
||||
class={`time ${
|
||||
isContextMenuOpen && contextMenuProps?.anchorRef
|
||||
? 'is-open'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
{showCommentHint && !showCommentCount ? (
|
||||
<Icon
|
||||
icon={visibilityIconsMap[visibility]}
|
||||
alt={_(visibilityText[visibility])}
|
||||
icon="comment2"
|
||||
size="s"
|
||||
// alt={`${repliesCount} ${
|
||||
// repliesCount === 1 ? 'reply' : 'replies'
|
||||
// }`}
|
||||
alt={plural(repliesCount, {
|
||||
one: '# reply',
|
||||
other: '# replies',
|
||||
})}
|
||||
/>
|
||||
)
|
||||
)}{' '}
|
||||
<RelativeTime datetime={createdAtDate} format="micro" />
|
||||
{!previewMode && !readOnly && (
|
||||
<Icon icon="more2" class="more" alt={t`More`} />
|
||||
)}
|
||||
</Link>
|
||||
) : (
|
||||
// <Menu
|
||||
// instanceRef={menuInstanceRef}
|
||||
// portal={{
|
||||
// target: document.body,
|
||||
// }}
|
||||
// containerProps={{
|
||||
// style: {
|
||||
// // Higher than the backdrop
|
||||
// zIndex: 1001,
|
||||
// },
|
||||
// onClick: (e) => {
|
||||
// if (e.target === e.currentTarget)
|
||||
// menuInstanceRef.current?.closeMenu?.();
|
||||
// },
|
||||
// }}
|
||||
// align="end"
|
||||
// gap={4}
|
||||
// overflow="auto"
|
||||
// viewScroll="close"
|
||||
// boundingBoxPadding="8 8 8 8"
|
||||
// unmountOnClose
|
||||
// menuButton={({ open }) => (
|
||||
// <Link
|
||||
// to={instance ? `/${instance}/s/${id}` : `/s/${id}`}
|
||||
// onClick={(e) => {
|
||||
// e.preventDefault();
|
||||
// e.stopPropagation();
|
||||
// onStatusLinkClick?.(e, status);
|
||||
// }}
|
||||
// class={`time ${open ? 'is-open' : ''}`}
|
||||
// >
|
||||
// <Icon
|
||||
// icon={visibilityIconsMap[visibility]}
|
||||
// alt={visibilityText[visibility]}
|
||||
// size="s"
|
||||
// />{' '}
|
||||
// <RelativeTime datetime={createdAtDate} format="micro" />
|
||||
// </Link>
|
||||
// )}
|
||||
// >
|
||||
// {StatusMenuItems}
|
||||
// </Menu>
|
||||
<span class="time">
|
||||
{visibility !== 'public' && visibility !== 'direct' && (
|
||||
<>
|
||||
<Icon
|
||||
icon={visibilityIconsMap[visibility]}
|
||||
alt={_(visibilityText[visibility])}
|
||||
size="s"
|
||||
/>{' '}
|
||||
</>
|
||||
)}
|
||||
<RelativeTime datetime={createdAtDate} format="micro" />
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
visibility !== 'public' &&
|
||||
visibility !== 'direct' && (
|
||||
<Icon
|
||||
icon={visibilityIconsMap[visibility]}
|
||||
alt={_(visibilityText[visibility])}
|
||||
size="s"
|
||||
/>
|
||||
)
|
||||
)}{' '}
|
||||
<RelativeTime datetime={createdAtDate} format="micro" />
|
||||
{!previewMode && !readOnly && (
|
||||
<Icon icon="more2" class="more" alt={t`More`} />
|
||||
)}
|
||||
</Link>
|
||||
) : (
|
||||
// <Menu
|
||||
// instanceRef={menuInstanceRef}
|
||||
// portal={{
|
||||
// target: document.body,
|
||||
// }}
|
||||
// containerProps={{
|
||||
// style: {
|
||||
// // Higher than the backdrop
|
||||
// zIndex: 1001,
|
||||
// },
|
||||
// onClick: (e) => {
|
||||
// if (e.target === e.currentTarget)
|
||||
// menuInstanceRef.current?.closeMenu?.();
|
||||
// },
|
||||
// }}
|
||||
// align="end"
|
||||
// gap={4}
|
||||
// overflow="auto"
|
||||
// viewScroll="close"
|
||||
// boundingBoxPadding="8 8 8 8"
|
||||
// unmountOnClose
|
||||
// menuButton={({ open }) => (
|
||||
// <Link
|
||||
// to={instance ? `/${instance}/s/${id}` : `/s/${id}`}
|
||||
// onClick={(e) => {
|
||||
// e.preventDefault();
|
||||
// e.stopPropagation();
|
||||
// onStatusLinkClick?.(e, status);
|
||||
// }}
|
||||
// class={`time ${open ? 'is-open' : ''}`}
|
||||
// >
|
||||
// <Icon
|
||||
// icon={visibilityIconsMap[visibility]}
|
||||
// alt={visibilityText[visibility]}
|
||||
// size="s"
|
||||
// />{' '}
|
||||
// <RelativeTime datetime={createdAtDate} format="micro" />
|
||||
// </Link>
|
||||
// )}
|
||||
// >
|
||||
// {StatusMenuItems}
|
||||
// </Menu>
|
||||
<span class="time">
|
||||
{visibility !== 'public' && visibility !== 'direct' && (
|
||||
<>
|
||||
<Icon
|
||||
icon={visibilityIconsMap[visibility]}
|
||||
alt={_(visibilityText[visibility])}
|
||||
size="s"
|
||||
/>{' '}
|
||||
</>
|
||||
)}
|
||||
<RelativeTime datetime={createdAtDate} format="micro" />
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{visibility === 'direct' && (
|
||||
<>
|
||||
<div class="status-direct-badge">
|
||||
|
|
|
@ -11,8 +11,9 @@
|
|||
--main-width: 40em;
|
||||
text-size-adjust: none;
|
||||
--hairline-width: 1px;
|
||||
--monospace-font: ui-monospace, 'SFMono-Regular', Consolas, 'Liberation Mono',
|
||||
Menlo, Courier, monospace;
|
||||
--monospace-font:
|
||||
ui-monospace, 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier,
|
||||
monospace;
|
||||
|
||||
--blue-color: royalblue;
|
||||
--purple-color: blueviolet;
|
||||
|
@ -190,8 +191,16 @@ html {
|
|||
}
|
||||
|
||||
body {
|
||||
font-family: ui-rounded, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
|
||||
Ubuntu, Cantarell, Noto Sans, sans-serif;
|
||||
font-family:
|
||||
ui-rounded,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
Segoe UI,
|
||||
Roboto,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
Noto Sans,
|
||||
sans-serif;
|
||||
font-size: var(--text-size);
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
|
@ -367,6 +376,7 @@ button[hidden] {
|
|||
|
||||
input[type='text'],
|
||||
input[type='search'],
|
||||
input[type='datetime-local'],
|
||||
textarea,
|
||||
select {
|
||||
color: var(--text-color);
|
||||
|
@ -377,6 +387,7 @@ select {
|
|||
}
|
||||
input[type='text']:focus,
|
||||
input[type='search']:focus,
|
||||
input[type='datetime-local']:focus,
|
||||
textarea:focus,
|
||||
select:focus {
|
||||
border-color: var(--outline-color);
|
||||
|
|
428
src/locales/en.po
generated
428
src/locales/en.po
generated
File diff suppressed because it is too large
Load diff
132
src/pages/scheduled-posts.css
Normal file
132
src/pages/scheduled-posts.css
Normal file
|
@ -0,0 +1,132 @@
|
|||
#scheduled-posts-page {
|
||||
.posts-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
li {
|
||||
> button {
|
||||
text-align: start;
|
||||
color: inherit;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-bottom: 1px solid var(--outline-color);
|
||||
gap: 8px;
|
||||
|
||||
> .status {
|
||||
padding: 8px;
|
||||
pointer-events: none;
|
||||
background-color: var(--bg-blur-color);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--outline-color);
|
||||
font-size: 80%;
|
||||
max-height: 160px;
|
||||
overflow: hidden;
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
black calc(160px - 16px),
|
||||
transparent
|
||||
);
|
||||
|
||||
.media-container .media {
|
||||
width: 80px !important;
|
||||
height: 80px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.post-schedule-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
&.post-schedule-time {
|
||||
.icon,
|
||||
b {
|
||||
color: var(--red-text-color);
|
||||
}
|
||||
}
|
||||
&.post-schedule-month b {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#scheduled-post-sheet {
|
||||
header h2 {
|
||||
font-weight: normal;
|
||||
|
||||
small {
|
||||
font-size: var(--text-size);
|
||||
}
|
||||
}
|
||||
main > .status {
|
||||
background-color: var(--bg-blur-color);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--outline-color);
|
||||
overflow: auto;
|
||||
max-height: 50svh;
|
||||
|
||||
.media-container .media {
|
||||
width: 80px !important;
|
||||
height: 80px !important;
|
||||
}
|
||||
}
|
||||
.status-reply {
|
||||
border-radius: 16px 16px 0 0;
|
||||
max-height: 160px;
|
||||
background-color: var(--bg-color);
|
||||
margin: 0 12px;
|
||||
border: 1px solid var(--outline-color);
|
||||
border-bottom: 0;
|
||||
-webkit-animation: appear-up 1sease-in-out;
|
||||
animation: appear-up 1sease-in-out;
|
||||
overflow: auto;
|
||||
|
||||
> .status {
|
||||
font-size: 90%;
|
||||
}
|
||||
}
|
||||
footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 8px 0;
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
input[type='datetime-local'] {
|
||||
max-width: calc(100vw - 32px);
|
||||
min-width: 0; /* Adding a min-width to prevent overflow */
|
||||
|
||||
&:user-invalid {
|
||||
border-color: var(--red-color);
|
||||
}
|
||||
|
||||
@supports not selector(:user-invalid) {
|
||||
&:invalid {
|
||||
border-color: var(--red-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
376
src/pages/scheduled-posts.jsx
Normal file
376
src/pages/scheduled-posts.jsx
Normal file
|
@ -0,0 +1,376 @@
|
|||
import './scheduled-posts.css';
|
||||
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { MenuItem } from '@szhsin/react-menu';
|
||||
import { useEffect, useMemo, useReducer, useState } from 'preact/hooks';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
import Icon from '../components/icon';
|
||||
import Link from '../components/link';
|
||||
import Loader from '../components/loader';
|
||||
import MenuConfirm from '../components/menu-confirm';
|
||||
import Menu2 from '../components/menu2';
|
||||
import Modal from '../components/modal';
|
||||
import NavMenu from '../components/nav-menu';
|
||||
import RelativeTime from '../components/relative-time';
|
||||
import ScheduledAtField, {
|
||||
getLocalTimezoneName,
|
||||
} from '../components/ScheduledAtField';
|
||||
import Status from '../components/status';
|
||||
import { api } from '../utils/api';
|
||||
import niceDateTime from '../utils/nice-date-time';
|
||||
import showToast from '../utils/show-toast';
|
||||
import states from '../utils/states';
|
||||
import useTitle from '../utils/useTitle';
|
||||
|
||||
const LIMIT = 40;
|
||||
|
||||
export default function ScheduledPosts() {
|
||||
const { t } = useLingui();
|
||||
const snapStates = useSnapshot(states);
|
||||
useTitle(t`Scheduled Posts`, '/sp');
|
||||
const { masto } = api();
|
||||
const [scheduledPosts, setScheduledPosts] = useState([]);
|
||||
const [uiState, setUIState] = useState('default');
|
||||
const [reloadCount, reload] = useReducer((c) => c + 1, 0);
|
||||
const [showScheduledPostModal, setShowScheduledPostModal] = useState(false);
|
||||
|
||||
useEffect(reload, [snapStates.reloadScheduledPosts]);
|
||||
|
||||
useEffect(() => {
|
||||
setUIState('loading');
|
||||
(async () => {
|
||||
try {
|
||||
const postsIterator = masto.v1.scheduledStatuses.list({ limit: LIMIT });
|
||||
const allPosts = [];
|
||||
let posts;
|
||||
do {
|
||||
const result = await postsIterator.next();
|
||||
posts = result.value;
|
||||
if (posts?.length) {
|
||||
allPosts.push(...posts);
|
||||
}
|
||||
} while (posts?.length);
|
||||
setScheduledPosts(allPosts);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setUIState('error');
|
||||
} finally {
|
||||
setUIState('default');
|
||||
}
|
||||
})();
|
||||
}, [reloadCount]);
|
||||
|
||||
return (
|
||||
<div id="scheduled-posts-page" class="deck-container" tabIndex="-1">
|
||||
<div class="timeline-deck deck">
|
||||
<header>
|
||||
<div class="header-grid">
|
||||
<div class="header-side">
|
||||
<NavMenu />
|
||||
<Link to="/" class="button plain">
|
||||
<Icon icon="home" size="l" alt={t`Home`} />
|
||||
</Link>
|
||||
</div>
|
||||
<h1>
|
||||
<Trans>Scheduled Posts</Trans>
|
||||
</h1>
|
||||
<div class="header-side">
|
||||
<Menu2
|
||||
portal
|
||||
setDownOverflow
|
||||
overflow="auto"
|
||||
viewScroll="close"
|
||||
position="anchor"
|
||||
menuButton={
|
||||
<button type="button" class="plain">
|
||||
<Icon icon="more" size="l" alt={t`More`} />
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
reload();
|
||||
}}
|
||||
>
|
||||
<Icon icon="refresh" size="l" />
|
||||
<span>
|
||||
<Trans>Refresh</Trans>
|
||||
</span>
|
||||
</MenuItem>
|
||||
</Menu2>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
{!scheduledPosts.length ? (
|
||||
<p class="ui-state">
|
||||
{uiState === 'loading' ? <Loader /> : t`No scheduled posts.`}
|
||||
</p>
|
||||
) : (
|
||||
<ul class="posts-list">
|
||||
{scheduledPosts.map((post) => {
|
||||
const { id, params, scheduledAt, mediaAttachments } = post;
|
||||
const {
|
||||
inReplyToId,
|
||||
language,
|
||||
poll,
|
||||
sensitive,
|
||||
spoilerText,
|
||||
text,
|
||||
visibility,
|
||||
} = params;
|
||||
const status = {
|
||||
// account: account.info,
|
||||
id,
|
||||
inReplyToId,
|
||||
language,
|
||||
mediaAttachments,
|
||||
poll: poll
|
||||
? {
|
||||
...poll,
|
||||
expiresAt: new Date(Date.now() + poll.expiresIn * 1000),
|
||||
options: poll.options.map((option) => ({
|
||||
title: option,
|
||||
votesCount: 0,
|
||||
})),
|
||||
}
|
||||
: undefined,
|
||||
sensitive,
|
||||
spoilerText,
|
||||
text,
|
||||
visibility,
|
||||
content: `<p>${text}</p>`,
|
||||
// createdAt: scheduledAt,
|
||||
};
|
||||
|
||||
return (
|
||||
<li key={id}>
|
||||
<ScheduledPostPreview
|
||||
status={status}
|
||||
scheduledAt={scheduledAt}
|
||||
onClick={() => {
|
||||
setShowScheduledPostModal({
|
||||
post: status,
|
||||
scheduledAt: new Date(scheduledAt),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
{showScheduledPostModal && (
|
||||
<Modal
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
setShowScheduledPostModal(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ScheduledPostEdit
|
||||
post={showScheduledPostModal.post}
|
||||
scheduledAt={showScheduledPostModal.scheduledAt}
|
||||
onClose={() => setShowScheduledPostModal(false)}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ScheduledPostPreview({ status, scheduledAt, onClick }) {
|
||||
// Look at scheduledAt, if it's months away, ICON = 'month'. If it's days away, ICON = 'day', else ICON = 'time'
|
||||
const icon = useMemo(() => {
|
||||
const hours =
|
||||
(new Date(scheduledAt).getTime() - Date.now()) / (1000 * 60 * 60);
|
||||
if (hours < 24) {
|
||||
return 'time';
|
||||
} else if (hours < 720) {
|
||||
// 30 days
|
||||
return 'day';
|
||||
} else {
|
||||
return 'month';
|
||||
}
|
||||
}, [scheduledAt]);
|
||||
|
||||
return (
|
||||
<button type="button" class="textual block" onClick={onClick}>
|
||||
<div class={`post-schedule-meta post-schedule-${icon}`}>
|
||||
<Icon icon={icon} class="insignificant" />{' '}
|
||||
<span>
|
||||
<Trans comment="Scheduled [in 1 day] ([Thu, Feb 27, 6:30:00 PM])">
|
||||
Scheduled{' '}
|
||||
<b>
|
||||
<RelativeTime datetime={scheduledAt} />
|
||||
</b>{' '}
|
||||
<small>
|
||||
(
|
||||
{niceDateTime(scheduledAt, {
|
||||
formatOpts: {
|
||||
weekday: 'short',
|
||||
second: 'numeric',
|
||||
},
|
||||
})}
|
||||
)
|
||||
</small>
|
||||
</Trans>
|
||||
</span>
|
||||
</div>
|
||||
<Status status={status} size="s" previewMode readOnly />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function ScheduledPostEdit({ post, scheduledAt, onClose }) {
|
||||
const { masto } = api();
|
||||
const { t } = useLingui();
|
||||
const [uiState, setUIState] = useState('default');
|
||||
const [newScheduledAt, setNewScheduledAt] = useState();
|
||||
const differentScheduledAt =
|
||||
newScheduledAt && newScheduledAt.getTime() !== scheduledAt.getTime();
|
||||
const localTZ = getLocalTimezoneName();
|
||||
const pastSchedule = scheduledAt && scheduledAt <= Date.now();
|
||||
|
||||
const { inReplyToId } = post;
|
||||
const [replyToStatus, setReplyToStatus] = useState(null);
|
||||
// TODO: Uncomment this once https://github.com/mastodon/mastodon/issues/34000 is fixed
|
||||
// useEffect(() => {
|
||||
// if (inReplyToId) {
|
||||
// (async () => {
|
||||
// try {
|
||||
// const status = await masto.v1.statuses.$select(inReplyToId).fetch();
|
||||
// setReplyToStatus(status);
|
||||
// } catch (e) {
|
||||
// console.error(e);
|
||||
// }
|
||||
// })();
|
||||
// }
|
||||
// }, [inReplyToId]);
|
||||
|
||||
return (
|
||||
<div id="scheduled-post-sheet" class="sheet">
|
||||
<button type="button" class="sheet-close" onClick={onClose}>
|
||||
<Icon icon="x" size="l" alt={t`Close`} />
|
||||
</button>
|
||||
<header>
|
||||
<h2>
|
||||
<Trans comment="Scheduled [in 1 day]">
|
||||
Scheduled{' '}
|
||||
<b>
|
||||
<RelativeTime datetime={scheduledAt} />
|
||||
</b>
|
||||
</Trans>
|
||||
<br />
|
||||
<small>
|
||||
{niceDateTime(scheduledAt, {
|
||||
formatOpts: {
|
||||
weekday: 'short',
|
||||
second: 'numeric',
|
||||
},
|
||||
})}
|
||||
</small>
|
||||
</h2>
|
||||
</header>
|
||||
<main tabIndex="-1">
|
||||
{!!replyToStatus && (
|
||||
<div class="status-reply">
|
||||
<Status status={replyToStatus} size="s" previewMode readOnly />
|
||||
</div>
|
||||
)}
|
||||
<Status
|
||||
status={post}
|
||||
size="s"
|
||||
previewMode
|
||||
readOnly
|
||||
onMediaClick={(e, i, media, status) => {
|
||||
e.preventDefault();
|
||||
states.showMediaModal = {
|
||||
mediaAttachments: post.mediaAttachments,
|
||||
mediaIndex: i,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
setUIState('loading');
|
||||
(async () => {
|
||||
try {
|
||||
await masto.v1.scheduledStatuses.$select(post.id).update({
|
||||
scheduledAt: newScheduledAt.toISOString(),
|
||||
});
|
||||
showToast(t`Scheduled post rescheduled`);
|
||||
onClose();
|
||||
setUIState('default');
|
||||
states.reloadScheduledPosts++;
|
||||
} catch (e) {
|
||||
setUIState('error');
|
||||
console.error(e);
|
||||
showToast(t`Failed to reschedule post`);
|
||||
}
|
||||
})();
|
||||
}}
|
||||
>
|
||||
<footer>
|
||||
<div class="row">
|
||||
<span>
|
||||
<ScheduledAtField
|
||||
scheduledAt={scheduledAt}
|
||||
setScheduledAt={(date) => {
|
||||
setNewScheduledAt(date);
|
||||
}}
|
||||
/>{' '}
|
||||
<small class="ib">{localTZ}</small>
|
||||
</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<button
|
||||
disabled={
|
||||
!differentScheduledAt || uiState === 'loading' || pastSchedule
|
||||
}
|
||||
>
|
||||
<Trans>Reschedule</Trans>
|
||||
</button>
|
||||
<span class="grow" />
|
||||
<MenuConfirm
|
||||
align="end"
|
||||
menuItemClassName="danger"
|
||||
confirmLabel={t`Delete scheduled post?`}
|
||||
onClick={() => {
|
||||
setUIState('loading');
|
||||
(async () => {
|
||||
try {
|
||||
await api()
|
||||
.masto.v1.scheduledStatuses.$select(post.id)
|
||||
.remove();
|
||||
showToast(t`Scheduled post deleted`);
|
||||
onClose();
|
||||
setUIState('default');
|
||||
states.reloadScheduledPosts++;
|
||||
} catch (e) {
|
||||
setUIState('error');
|
||||
console.error(e);
|
||||
showToast(t`Failed to delete scheduled post`);
|
||||
}
|
||||
})();
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="light danger"
|
||||
disabled={uiState === 'loading' || pastSchedule}
|
||||
>
|
||||
<Trans>Delete…</Trans>
|
||||
</button>
|
||||
</MenuConfirm>
|
||||
</div>
|
||||
</footer>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -31,6 +31,7 @@ const states = proxy({
|
|||
id: null,
|
||||
counter: 0,
|
||||
},
|
||||
reloadScheduledPosts: 0,
|
||||
spoilers: {},
|
||||
spoilersMedia: {},
|
||||
scrollPositions: {},
|
||||
|
|
Loading…
Add table
Reference in a new issue