New feature: poll

- More fixes
This commit is contained in:
Lim Chee Aun 2022-12-14 21:48:17 +08:00
parent 121e9176f3
commit 72751709df
9 changed files with 456 additions and 147 deletions

View file

@ -8,7 +8,7 @@
<meta name="color-scheme" content="dark light" /> <meta name="color-scheme" content="dark light" />
</head> </head>
<body> <body>
<div id="app"></div> <div id="app-standalone"></div>
<script type="module" src="/src/compose.jsx"></script> <script type="module" src="/src/compose.jsx"></script>
</body> </body>
</html> </html>

View file

@ -4,7 +4,7 @@ body {
padding: 0; padding: 0;
background-color: var(--bg-color); background-color: var(--bg-color);
color: var(--text-color); color: var(--text-color);
overflow: hidden; /* overflow: hidden; */
} }
#app { #app {

View file

@ -277,8 +277,8 @@ export function App() {
? snapStates.showCompose.replyToStatus ? snapStates.showCompose.replyToStatus
: null : null
} }
editStatus={snapStates.showCompose?.editStatus || null} editStatus={states.showCompose?.editStatus || null}
draftStatus={snapStates.showCompose?.draftStatus || null} draftStatus={states.showCompose?.draftStatus || null}
onClose={(results) => { onClose={(results) => {
const { newStatus } = results || {}; const { newStatus } = results || {};
states.showCompose = false; states.showCompose = false;

View file

@ -6,6 +6,10 @@
max-height: 100vh; max-height: 100vh;
overflow: auto; overflow: auto;
} }
#compose-container.standalone {
max-height: none;
margin: auto;
}
#compose-container .compose-top { #compose-container .compose-top {
text-align: right; text-align: right;
@ -110,8 +114,8 @@
min-width: 0; min-width: 0;
} }
#compose-container .toolbar-button { #compose-container .toolbar-button {
cursor: pointer;
display: inline-block; display: inline-block;
color: var(--text-color);
background-color: var(--bg-faded-color); background-color: var(--bg-faded-color);
padding: 0 8px; padding: 0 8px;
border-radius: 8px; border-radius: 8px;
@ -123,6 +127,7 @@
position: relative; position: relative;
white-space: nowrap; white-space: nowrap;
border: 2px solid transparent; border: 2px solid transparent;
vertical-align: middle;
} }
#compose-container .toolbar-button > * { #compose-container .toolbar-button > * {
vertical-align: middle; vertical-align: middle;
@ -131,9 +136,11 @@
} }
#compose-container .toolbar-button:has([disabled]) { #compose-container .toolbar-button:has([disabled]) {
pointer-events: none; pointer-events: none;
background-color: var(--bg-faded-color);
opacity: 0.5;
} }
#compose-container .toolbar-button:has([disabled]) > * { #compose-container .toolbar-button:has([disabled]) > * {
filter: opacity(0.5); /* filter: opacity(0.5); */
} }
#compose-container #compose-container
.toolbar-button:not(.show-field) .toolbar-button:not(.show-field)
@ -157,10 +164,12 @@
right: 0; right: 0;
left: auto !important; left: auto !important;
} }
#compose-container .toolbar-button:hover { #compose-container .toolbar-button:not(:disabled):hover {
cursor: pointer;
filter: none;
border-color: var(--divider-color); border-color: var(--divider-color);
} }
#compose-container .toolbar-button:active { #compose-container .toolbar-button:not(:disabled):active {
filter: brightness(0.8); filter: brightness(0.8);
} }
@ -272,3 +281,80 @@
color: var(--green-color); color: var(--green-color);
margin-bottom: 4px; margin-bottom: 4px;
} }
#compose-container .poll {
background-color: var(--bg-faded-color);
border-radius: 8px;
margin: 8px 0 0;
}
#compose-container .poll-choices {
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px;
}
#compose-container .poll-choice {
display: flex;
gap: 8px;
align-items: center;
justify-content: stretch;
flex-direction: row-reverse;
}
#compose-container .poll-choice input {
flex-grow: 1;
min-width: 0;
}
#compose-container .poll-button {
border: 2px solid var(--outline-color);
width: 28px;
height: 28px;
padding: 0;
flex-shrink: 0;
line-height: 0;
overflow: hidden;
transition: border-radius 1s ease-out;
font-size: 14px;
}
#compose-container .multiple .poll-button {
border-radius: 4px;
}
#compose-container .poll-toolbar {
display: flex;
gap: 8px;
align-items: stretch;
justify-content: space-between;
font-size: 90%;
border-top: 1px solid var(--outline-color);
padding: 8px;
}
#compose-container .poll-toolbar select {
padding: 4px;
}
#compose-container .multiple-choices {
flex-grow: 1;
display: flex;
gap: 4px;
align-items: center;
border-left: 1px solid var(--outline-color);
padding-left: 8px;
}
#compose-container .expires-in {
flex-grow: 1;
border-left: 1px solid var(--outline-color);
padding-left: 8px;
display: flex;
gap: 4px;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
}
#compose-container .remove-poll-button {
width: 100%;
color: var(--red-color);
}

View file

@ -18,12 +18,31 @@ import Status from './status';
- Max character limit includes BOTH status text and Content Warning text - Max character limit includes BOTH status text and Content Warning text
*/ */
const expiryOptions = {
'5 minutes': 5 * 60,
'30 minutes': 30 * 60,
'1 hour': 60 * 60,
'6 hours': 6 * 60 * 60,
'1 day': 24 * 60 * 60,
'3 days': 3 * 24 * 60 * 60,
'7 days': 7 * 24 * 60 * 60,
};
const expirySeconds = Object.values(expiryOptions);
const oneDay = 24 * 60 * 60;
const expiresInFromExpiresAt = (expiresAt) => {
if (!expiresAt) return oneDay;
const delta = (new Date(expiresAt).getTime() - Date.now()) / 1000;
return expirySeconds.find((s) => s >= delta) || oneDay;
};
function Compose({ function Compose({
onClose, onClose,
replyToStatus, replyToStatus,
editStatus, editStatus,
draftStatus, draftStatus,
standalone, standalone,
hasOpener,
}) { }) {
const [uiState, setUIState] = useState('default'); const [uiState, setUIState] = useState('default');
@ -57,10 +76,11 @@ function Compose({
} = configuration; } = configuration;
const textareaRef = useRef(); const textareaRef = useRef();
const spoilerTextRef = useRef();
const [visibility, setVisibility] = useState('public'); const [visibility, setVisibility] = useState('public');
const [sensitive, setSensitive] = useState(false); const [sensitive, setSensitive] = useState(false);
const spoilerTextRef = useRef(); const [mediaAttachments, setMediaAttachments] = useState([]);
const [poll, setPoll] = useState(null);
useEffect(() => { useEffect(() => {
if (replyToStatus) { if (replyToStatus) {
@ -78,15 +98,32 @@ function Compose({
setSensitive(sensitive); setSensitive(sensitive);
} }
if (draftStatus) { if (draftStatus) {
const { status, spoilerText, visibility, sensitive, mediaAttachments } = const {
draftStatus; status,
spoilerText,
visibility,
sensitive,
poll,
mediaAttachments,
} = draftStatus;
const composablePoll = !!poll?.options && {
...poll,
options: poll.options.map((o) => o?.title || o),
expiresIn: poll?.expiresIn || expiresInFromExpiresAt(poll.expiresAt),
};
textareaRef.current.value = status; textareaRef.current.value = status;
spoilerTextRef.current.value = spoilerText; spoilerTextRef.current.value = spoilerText;
setVisibility(visibility); setVisibility(visibility);
setSensitive(sensitive); setSensitive(sensitive);
setPoll(composablePoll);
setMediaAttachments(mediaAttachments); setMediaAttachments(mediaAttachments);
} else if (editStatus) { } else if (editStatus) {
const { visibility, sensitive, mediaAttachments } = editStatus; const { visibility, sensitive, poll, mediaAttachments } = editStatus;
const composablePoll = !!poll?.options && {
...poll,
options: poll.options.map((o) => o?.title || o),
expiresIn: poll?.expiresIn || expiresInFromExpiresAt(poll.expiresAt),
};
setUIState('loading'); setUIState('loading');
(async () => { (async () => {
try { try {
@ -98,6 +135,7 @@ function Compose({
spoilerTextRef.current.value = spoilerText; spoilerTextRef.current.value = spoilerText;
setVisibility(visibility); setVisibility(visibility);
setSensitive(sensitive); setSensitive(sensitive);
setPoll(composablePoll);
setMediaAttachments(mediaAttachments); setMediaAttachments(mediaAttachments);
setUIState('default'); setUIState('default');
} catch (e) { } catch (e) {
@ -199,8 +237,6 @@ function Compose({
} }
}, []); }, []);
const [mediaAttachments, setMediaAttachments] = useState([]);
const formRef = useRef(); const formRef = useRef();
const beforeUnloadCopy = const beforeUnloadCopy =
@ -216,7 +252,9 @@ function Compose({
} }
// check if all media attachments have IDs // check if all media attachments have IDs
const hasIDMediaAttachments = mediaAttachments.every((media) => media.id); const hasIDMediaAttachments =
mediaAttachments.length > 0 &&
mediaAttachments.every((media) => media.id);
if (hasIDMediaAttachments) { if (hasIDMediaAttachments) {
console.log('canClose', { hasIDMediaAttachments }); console.log('canClose', { hasIDMediaAttachments });
return true; return true;
@ -242,6 +280,7 @@ function Compose({
value, value,
hasMediaAttachments, hasMediaAttachments,
hasIDMediaAttachments, hasIDMediaAttachments,
poll,
isSelf, isSelf,
hasOnlyAcct, hasOnlyAcct,
sameWithSource, sameWithSource,
@ -316,6 +355,7 @@ function Compose({
spoilerText: spoilerTextRef.current.value, spoilerText: spoilerTextRef.current.value,
visibility, visibility,
sensitive, sensitive,
poll,
mediaAttachments: mediaAttachmentsWithIDs, mediaAttachments: mediaAttachmentsWithIDs,
}, },
}); });
@ -343,51 +383,54 @@ function Compose({
</button> </button>
</span> </span>
) : ( ) : (
<button hasOpener && (
type="button" <button
class="light" type="button"
onClick={() => { class="light"
// If there are non-ID media attachments (not yet uploaded), show confirmation dialog because they are not going to be passed to the new window onClick={() => {
const containNonIDMediaAttachments = // If there are non-ID media attachments (not yet uploaded), show confirmation dialog because they are not going to be passed to the new window
mediaAttachments.length > 0 && const containNonIDMediaAttachments =
mediaAttachments.some((media) => !media.id); mediaAttachments.length > 0 &&
if (containNonIDMediaAttachments) { mediaAttachments.some((media) => !media.id);
const yes = confirm( if (containNonIDMediaAttachments) {
'You have media attachments that are not yet uploaded. Opening a new window will discard them and you will need to re-attach them. Are you sure you want to continue?', const yes = confirm(
); 'You have media attachments that are not yet uploaded. Opening a new window will discard them and you will need to re-attach them. Are you sure you want to continue?',
if (!yes) { );
if (!yes) {
return;
}
}
if (!window.opener) {
alert('Looks like you closed the parent window.');
return; return;
} }
}
if (!window.opener) { const mediaAttachmentsWithIDs = mediaAttachments.filter(
alert('Looks like you closed the parent window.'); (media) => media.id,
return; );
}
const mediaAttachmentsWithIDs = mediaAttachments.filter( onClose({
(media) => media.id, fn: () => {
); window.opener.__STATES__.showCompose = {
editStatus,
onClose({ replyToStatus,
fn: () => { draftStatus: {
window.opener.__STATES__.showCompose = { status: textareaRef.current.value,
editStatus, spoilerText: spoilerTextRef.current.value,
replyToStatus, visibility,
draftStatus: { sensitive,
status: textareaRef.current.value, poll,
spoilerText: spoilerTextRef.current.value, mediaAttachments: mediaAttachmentsWithIDs,
visibility, },
sensitive, };
mediaAttachments: mediaAttachmentsWithIDs, },
}, });
}; }}
}, >
}); <Icon icon="popin" alt="Pop in" />
}} </button>
> )
<Icon icon="popin" alt="Pop in" />
</button>
)} )}
</div> </div>
{!!replyToStatus && ( {!!replyToStatus && (
@ -436,6 +479,16 @@ function Compose({
); );
return; return;
} }
if (poll) {
if (poll.options.length < 2) {
alert('Poll must have at least 2 options');
return;
}
if (poll.options.some((option) => option === '')) {
alert('Some poll choices are empty');
return;
}
}
// TODO: check for URLs and use `charactersReservedPerUrl` to calculate max characters // TODO: check for URLs and use `charactersReservedPerUrl` to calculate max characters
// Post-cleanup // Post-cleanup
@ -449,8 +502,7 @@ function Compose({
if (mediaAttachments.length > 0) { if (mediaAttachments.length > 0) {
// Upload media attachments first // Upload media attachments first
const mediaPromises = mediaAttachments.map((attachment) => { const mediaPromises = mediaAttachments.map((attachment) => {
const { file, description, sourceDescription, id } = const { file, description, id } = attachment;
attachment;
console.log('UPLOADING', attachment); console.log('UPLOADING', attachment);
if (id) { if (id) {
// If already uploaded // If already uploaded
@ -493,6 +545,7 @@ function Compose({
status, status,
spoilerText, spoilerText,
sensitive, sensitive,
poll,
mediaIds: mediaAttachments.map((attachment) => attachment.id), mediaIds: mediaAttachments.map((attachment) => attachment.id),
}; };
if (!editStatus) { if (!editStatus) {
@ -639,59 +692,87 @@ function Compose({
})} })}
</div> </div>
)} )}
{!!poll && (
<Poll
maxOptions={maxOptions}
maxExpiration={maxExpiration}
minExpiration={minExpiration}
maxCharactersPerOption={maxCharactersPerOption}
poll={poll}
disabled={uiState === 'loading'}
onInput={(poll) => {
if (poll) {
const newPoll = { ...poll };
setPoll(newPoll);
} else {
setPoll(null);
}
}}
/>
)}
<div class="toolbar"> <div class="toolbar">
<div> <label class="toolbar-button">
<label class="toolbar-button"> <input
<input type="file"
type="file" accept={supportedMimeTypes.join(',')}
accept={supportedMimeTypes.join(',')} multiple={mediaAttachments.length < maxMediaAttachments - 1}
multiple={mediaAttachments.length < maxMediaAttachments - 1} disabled={
disabled={ uiState === 'loading' ||
uiState === 'loading' || mediaAttachments.length >= maxMediaAttachments ||
mediaAttachments.length >= maxMediaAttachments !!poll
}
onChange={(e) => {
const files = e.target.files;
if (!files) return;
const mediaFiles = Array.from(files).map((file) => ({
file,
type: file.type,
size: file.size,
url: URL.createObjectURL(file),
id: null, // indicate uploaded state
description: null,
}));
console.log('MEDIA ATTACHMENTS', files, mediaFiles);
// Validate max media attachments
if (
mediaAttachments.length + mediaFiles.length >
maxMediaAttachments
) {
alert(
`You can only attach up to ${maxMediaAttachments} files.`,
);
} else {
setMediaAttachments((attachments) => {
return attachments.concat(mediaFiles);
});
} }
onChange={(e) => { }}
const files = e.target.files; />
if (!files) return; <Icon icon="attachment" />
</label>{' '}
const mediaFiles = Array.from(files).map((file) => ({ <button
file, type="button"
type: file.type, class="toolbar-button"
size: file.size, disabled={
url: URL.createObjectURL(file), uiState === 'loading' || !!poll || !!mediaAttachments.length
id: null, // indicate uploaded state }
description: null, onClick={() => {
})); setPoll({
console.log('MEDIA ATTACHMENTS', files, mediaFiles); options: ['', ''],
expiresIn: 24 * 60 * 60, // 1 day
// Validate max media attachments multiple: false,
if ( });
mediaAttachments.length + mediaFiles.length > }}
maxMediaAttachments >
) { <Icon icon="poll" alt="Add poll" />
alert( </button>{' '}
`You can only attach up to ${maxMediaAttachments} files.`, <div class="spacer" />
); {uiState === 'loading' && <Loader abrupt />}{' '}
} else { <button type="submit" class="large" disabled={uiState === 'loading'}>
setMediaAttachments((attachments) => { {replyToStatus ? 'Reply' : editStatus ? 'Update' : 'Post'}
return attachments.concat(mediaFiles); </button>
});
}
}}
/>
<Icon icon="attachment" />
</label>
</div>
<div>
{uiState === 'loading' && <Loader abrupt />}{' '}
<button
type="submit"
class="large"
disabled={uiState === 'loading'}
>
{replyToStatus ? 'Reply' : editStatus ? 'Update' : 'Post'}
</button>
</div>
</div> </div>
</form> </form>
</div> </div>
@ -760,4 +841,111 @@ function MediaAttachment({
); );
} }
function Poll({
poll,
disabled,
onInput = () => {},
maxOptions,
maxExpiration,
minExpiration,
maxCharactersPerOption,
}) {
const { options, expiresIn, multiple } = poll;
return (
<div class={`poll ${multiple ? 'multiple' : ''}`}>
<div class="poll-choices">
{options.map((option, i) => (
<div class="poll-choice" key={i}>
<input
required
type="text"
value={option}
disabled={disabled}
maxlength={maxCharactersPerOption}
placeholder={`Choice ${i + 1}`}
onInput={(e) => {
const { value } = e.target;
options[i] = value;
onInput(poll);
}}
/>
<button
type="button"
class="plain2 poll-button"
disabled={disabled || options.length <= 1}
onClick={() => {
options.splice(i, 1);
onInput(poll);
}}
>
<Icon icon="x" size="s" />
</button>
</div>
))}
</div>
<div class="poll-toolbar">
<button
type="button"
class="plain2 poll-button"
disabled={disabled || options.length >= maxOptions}
onClick={() => {
options.push('');
onInput(poll);
}}
>
+
</button>{' '}
<label class="multiple-choices">
<input
type="checkbox"
checked={multiple}
disabled={disabled}
onChange={(e) => {
const { checked } = e.target;
poll.multiple = checked;
onInput(poll);
}}
/>{' '}
Multiple choices
</label>
<label class="expires-in">
Duration{' '}
<select
value={expiresIn}
disabled={disabled}
onChange={(e) => {
const { value } = e.target;
poll.expiresIn = value;
onInput(poll);
}}
>
{Object.entries(expiryOptions)
.filter(([label, value]) => {
return value >= minExpiration && value <= maxExpiration;
})
.map(([label, value]) => (
<option value={value} key={value}>
{label}
</option>
))}
</select>
</label>
</div>
<div class="poll-toolbar">
<button
type="button"
class="plain remove-poll-button"
disabled={disabled}
onClick={() => {
onInput(null);
}}
>
Remove poll
</button>
</div>
</div>
);
}
export default Compose; export default Compose;

View file

@ -40,9 +40,12 @@ const ICONS = {
external: 'mingcute:external-link-line', external: 'mingcute:external-link-line',
popout: 'mingcute:external-link-line', popout: 'mingcute:external-link-line',
popin: ['mingcute:external-link-line', '180deg'], popin: ['mingcute:external-link-line', '180deg'],
plus: 'mingcute:add-circle-line',
}; };
export default ({ icon, size = 'm', alt, title, class: className = '' }) => { export default ({ icon, size = 'm', alt, title, class: className = '' }) => {
if (!icon) return null;
const iconSize = SIZES[size]; const iconSize = SIZES[size];
let iconName = ICONS[icon]; let iconName = ICONS[icon];
let rotate; let rotate;

View file

@ -264,10 +264,14 @@ function Card({ card }) {
} }
} }
function Poll({ poll }) { function Poll({ poll, readOnly }) {
const [pollSnapshot, setPollSnapshot] = useState(poll); const [pollSnapshot, setPollSnapshot] = useState(poll);
const [uiState, setUIState] = useState('default'); const [uiState, setUIState] = useState('default');
useEffect(() => {
setPollSnapshot(poll);
}, [poll]);
const { const {
expired, expired,
expiresAt, expiresAt,
@ -280,7 +284,7 @@ function Poll({ poll }) {
votesCount, votesCount,
} = pollSnapshot; } = pollSnapshot;
const expiresAtDate = new Date(expiresAt); const expiresAtDate = !!expiresAt && new Date(expiresAt);
return ( return (
<div class="poll"> <div class="poll">
@ -296,6 +300,7 @@ function Poll({ poll }) {
optionVotesCount === Math.max(...options.map((o) => o.votesCount)); optionVotesCount === Math.max(...options.map((o) => o.votesCount));
return ( return (
<div <div
key={`${i}-${title}-${optionVotesCount}`}
class={`poll-option ${isLeading ? 'poll-option-leading' : ''}`} class={`poll-option ${isLeading ? 'poll-option-leading' : ''}`}
style={{ style={{
'--percentage': `${percentage}%`, '--percentage': `${percentage}%`,
@ -343,7 +348,7 @@ function Poll({ poll }) {
setUIState('default'); setUIState('default');
}} }}
style={{ style={{
pointerEvents: uiState === 'loading' ? 'none' : 'auto', pointerEvents: uiState === 'loading' || readOnly ? 'none' : 'auto',
opacity: uiState === 'loading' ? 0.5 : 1, opacity: uiState === 'loading' ? 0.5 : 1,
}} }}
> >
@ -357,37 +362,44 @@ function Poll({ poll }) {
name="poll" name="poll"
value={i} value={i}
disabled={uiState === 'loading'} disabled={uiState === 'loading'}
readOnly={readOnly}
/> />
<span class="poll-option-title">{title}</span> <span class="poll-option-title">{title}</span>
</label> </label>
</div> </div>
); );
})} })}
<button {!readOnly && (
class="poll-vote-button" <button
type="submit" class="poll-vote-button"
disabled={uiState === 'loading'} type="submit"
> disabled={uiState === 'loading'}
Vote >
</button> Vote
</button>
)}
</form> </form>
)} )}
<p class="poll-meta"> {!readOnly && (
<span title={votersCount}>{shortenNumber(votersCount)}</span>{' '} <p class="poll-meta">
{votersCount === 1 ? 'voter' : 'voters'} <span title={votersCount}>{shortenNumber(votersCount)}</span>{' '}
{votersCount !== votesCount && ( {votersCount === 1 ? 'voter' : 'voters'}
<> {votersCount !== votesCount && (
{' '} <>
&bull; <span title={votesCount}> {' '}
{shortenNumber(votesCount)} &bull; <span title={votesCount}>
</span>{' '} {shortenNumber(votesCount)}
vote </span>{' '}
{votesCount === 1 ? '' : 's'} vote
</> {votesCount === 1 ? '' : 's'}
)}{' '} </>
&bull; {expired ? 'Ended' : 'Ending'}{' '} )}{' '}
<relative-time datetime={expiresAtDate.toISOString()} /> &bull; {expired ? 'Ended' : 'Ending'}{' '}
</p> {!!expiresAtDate && (
<relative-time datetime={expiresAtDate.toISOString()} />
)}
</p>
)}
</div> </div>
); );
} }
@ -449,7 +461,7 @@ function EditedAtModal({ statusID, onClose = () => {} }) {
}).format(createdAtDate)} }).format(createdAtDate)}
</time> </time>
</h3> </h3>
<Status status={status} size="s" withinContext editStatus /> <Status status={status} size="s" withinContext readOnly />
</li> </li>
); );
})} })}
@ -470,7 +482,7 @@ function Status({
withinContext, withinContext,
size = 'm', size = 'm',
skeleton, skeleton,
editStatus, readOnly,
}) { }) {
if (skeleton) { if (skeleton) {
return ( return (
@ -645,7 +657,7 @@ function Status({
</> </>
)} )}
</span>{' '} </span>{' '}
{size !== 'l' && !editStatus && ( {size !== 'l' && uri ? (
<a href={uri} target="_blank" class="time"> <a href={uri} target="_blank" class="time">
<Icon <Icon
icon={visibilityIconsMap[visibility]} icon={visibilityIconsMap[visibility]}
@ -661,6 +673,22 @@ function Status({
{createdAtDate.toLocaleString()} {createdAtDate.toLocaleString()}
</relative-time> </relative-time>
</a> </a>
) : (
<span class="time">
<Icon
icon={visibilityIconsMap[visibility]}
alt={visibility}
size="s"
/>{' '}
<relative-time
datetime={createdAtDate.toISOString()}
format="micro"
threshold="P1D"
prefix=""
>
{createdAtDate.toLocaleString()}
</relative-time>
</span>
)} )}
</div> </div>
<div <div
@ -731,7 +759,7 @@ function Status({
}), }),
}} }}
/> />
{!!poll && <Poll poll={poll} />} {!!poll && <Poll poll={poll} readOnly={readOnly} />}
{!spoilerText && sensitive && !!mediaAttachments.length && ( {!spoilerText && sensitive && !!mediaAttachments.length && (
<button <button
class="plain spoiler" class="plain spoiler"

View file

@ -25,7 +25,7 @@ function App() {
if (uiState === 'closed') { if (uiState === 'closed') {
return ( return (
<div> <div class="box">
<p>You may close this page now.</p> <p>You may close this page now.</p>
<p> <p>
<button <button
@ -46,6 +46,7 @@ function App() {
replyToStatus={replyToStatus} replyToStatus={replyToStatus}
draftStatus={draftStatus} draftStatus={draftStatus}
standalone standalone
hasOpener={window.opener}
onClose={(results) => { onClose={(results) => {
const { newStatus, fn = () => {} } = results || {}; const { newStatus, fn = () => {} } = results || {};
try { try {
@ -60,4 +61,4 @@ function App() {
); );
} }
render(<App />, document.getElementById('app')); render(<App />, document.getElementById('app-standalone'));

View file

@ -114,7 +114,6 @@ button,
border: 0; border: 0;
background-color: var(--button-bg-color); background-color: var(--button-bg-color);
color: var(--button-text-color); color: var(--button-text-color);
cursor: pointer;
line-height: 1; line-height: 1;
vertical-align: middle; vertical-align: middle;
text-decoration: none; text-decoration: none;
@ -122,14 +121,14 @@ button,
button > * { button > * {
vertical-align: middle; vertical-align: middle;
} }
:is(button, .button):not([disabled]):hover { :is(button, .button):not(:disabled, .disabled):hover {
cursor: pointer;
filter: brightness(1.2); filter: brightness(1.2);
} }
:is(button, .button):active { :is(button, .button):not(:disabled, .disabled):active {
filter: brightness(0.8); filter: brightness(0.8);
} }
:is(button, .button)[disabled] { :is(button:disabled, .button.disabled) {
cursor: auto;
opacity: 0.5; opacity: 0.5;
} }
@ -215,6 +214,10 @@ code {
display: inline-block; display: inline-block;
} }
.spacer {
flex-grow: 1;
}
/* KEYFRAMES */ /* KEYFRAMES */
@keyframes fade-in { @keyframes fade-in {