Experiment: allow minimize composer

This commit is contained in:
Lim Chee Aun 2024-05-24 12:30:20 +08:00
parent 8aab997900
commit cd17ca0b42
13 changed files with 216 additions and 30 deletions

View file

@ -1609,6 +1609,47 @@ body:has(.media-modal-container + .status-deck) .media-post-link {
bottom: calc(16px + env(safe-area-inset-bottom) + 52px); bottom: calc(16px + env(safe-area-inset-bottom) + 52px);
} }
} }
#compose-button {
&.min {
outline: 2px solid var(--button-text-color);
&:after {
content: '';
display: block;
position: absolute;
top: 0;
right: 0;
width: 14px;
height: 14px;
border-radius: 50%;
background-color: var(--button-bg-color);
border: 2px solid var(--button-text-color);
box-shadow: 0 2px 8px var(--drop-shadow-color);
opacity: 0;
transition: opacity 0.2s ease-out 0.5s;
opacity: 1;
}
}
&.loading {
outline-color: var(--button-bg-blur-color);
&:before {
position: absolute;
inset: 0;
content: '';
border-radius: 50%;
animation: spin 5s linear infinite;
border: 2px dashed var(--button-text-color);
}
}
&.error {
&:after {
background-color: var(--red-color);
}
}
}
/* SHEET */ /* SHEET */

View file

@ -108,4 +108,5 @@ export const ICONS = {
settings: () => import('@iconify-icons/mingcute/settings-6-line'), settings: () => import('@iconify-icons/mingcute/settings-6-line'),
'heart-break': () => import('@iconify-icons/mingcute/heart-crack-line'), 'heart-break': () => import('@iconify-icons/mingcute/heart-crack-line'),
'user-x': () => import('@iconify-icons/mingcute/user-x-line'), 'user-x': () => import('@iconify-icons/mingcute/user-x-line'),
minimize: () => import('@iconify-icons/mingcute/arrows-down-line'),
}; };

View file

@ -19,6 +19,7 @@ import { getLists } from '../utils/lists';
import niceDateTime from '../utils/nice-date-time'; import niceDateTime from '../utils/nice-date-time';
import pmem from '../utils/pmem'; import pmem from '../utils/pmem';
import shortenNumber from '../utils/shorten-number'; import shortenNumber from '../utils/shorten-number';
import showCompose from '../utils/show-compose';
import showToast from '../utils/show-toast'; import showToast from '../utils/show-toast';
import states, { hideAllModals } from '../utils/states'; import states, { hideAllModals } from '../utils/states';
import store from '../utils/store'; import store from '../utils/store';
@ -1081,11 +1082,11 @@ function RelatedActions({
<> <>
<MenuItem <MenuItem
onClick={() => { onClick={() => {
states.showCompose = { showCompose({
draftStatus: { draftStatus: {
status: `@${currentInfo?.acct || acct} `, status: `@${currentInfo?.acct || acct} `,
}, },
}; });
}} }}
> >
<Icon icon="at" /> <Icon icon="at" />

View file

@ -1,4 +1,5 @@
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { useSnapshot } from 'valtio';
import openCompose from '../utils/open-compose'; import openCompose from '../utils/open-compose';
import openOSK from '../utils/open-osk'; import openOSK from '../utils/open-osk';
@ -7,7 +8,15 @@ import states from '../utils/states';
import Icon from './icon'; import Icon from './icon';
export default function ComposeButton() { export default function ComposeButton() {
const snapStates = useSnapshot(states);
function handleButton(e) { function handleButton(e) {
if (snapStates.composerState.minimized) {
states.composerState.minimized = false;
openOSK();
return;
}
if (e.shiftKey) { if (e.shiftKey) {
const newWin = openCompose(); const newWin = openCompose();
@ -28,7 +37,14 @@ export default function ComposeButton() {
}); });
return ( return (
<button type="button" id="compose-button" onClick={handleButton}> <button
type="button"
id="compose-button"
onClick={handleButton}
class={`${snapStates.composerState.minimized ? 'min' : ''} ${
snapStates.composerState.publishing ? 'loading' : ''
} ${snapStates.composerState.publishingError ? 'error' : ''}`}
>
<Icon icon="quill" size="xl" alt="Compose" /> <Icon icon="quill" size="xl" alt="Compose" />
</button> </button>
); );

View file

@ -514,6 +514,7 @@ function Compose({
// I don't think this warrant a draft mode for a status that's already posted // I don't think this warrant a draft mode for a status that's already posted
// Maybe it could be a big edit change but it should be rare // Maybe it could be a big edit change but it should be rare
if (editStatus) return; if (editStatus) return;
if (states.composerState.minimized) return;
const key = draftKey(); const key = draftKey();
const backgroundDraft = { const backgroundDraft = {
key, key,
@ -670,6 +671,11 @@ function Compose({
[replyToStatus], [replyToStatus],
); );
const onMinimize = () => {
saveUnsavedDraft();
states.composerState.minimized = true;
};
return ( return (
<div id="compose-container-outer"> <div id="compose-container-outer">
<div id="compose-container" class={standalone ? 'standalone' : ''}> <div id="compose-container" class={standalone ? 'standalone' : ''}>
@ -689,7 +695,7 @@ function Compose({
/> />
)} )}
{!standalone ? ( {!standalone ? (
<span> <span class="button-group">
<button <button
type="button" type="button"
class="light pop-button" class="light pop-button"
@ -736,6 +742,13 @@ function Compose({
> >
<Icon icon="popout" alt="Pop out" /> <Icon icon="popout" alt="Pop out" />
</button>{' '} </button>{' '}
<button
type="button"
class="light min-button"
onClick={onMinimize}
>
<Icon icon="minimize" alt="Minimize" />
</button>{' '}
<button <button
type="button" type="button"
class="light close-button" class="light close-button"
@ -810,6 +823,10 @@ function Compose({
} else { } else {
window.opener.__STATES__.showCompose = true; window.opener.__STATES__.showCompose = true;
} }
if (window.opener.__STATES__.composerState.minimized) {
// Maximize it
window.opener.__STATES__.composerState.minimized = false;
}
}, },
}); });
}} }}
@ -915,6 +932,8 @@ function Compose({
spoilerText = (sensitive && spoilerText) || undefined; spoilerText = (sensitive && spoilerText) || undefined;
status = status === '' ? undefined : status; status = status === '' ? undefined : status;
// states.composerState.minimized = true;
states.composerState.publishing = true;
setUIState('loading'); setUIState('loading');
(async () => { (async () => {
try { try {
@ -948,6 +967,8 @@ function Compose({
return result.status === 'rejected' || !result.value?.id; return result.status === 'rejected' || !result.value?.id;
}) })
) { ) {
states.composerState.publishing = false;
states.composerState.publishingError = true;
setUIState('error'); setUIState('error');
// Alert all the reasons // Alert all the reasons
results.forEach((result) => { results.forEach((result) => {
@ -1021,6 +1042,8 @@ function Compose({
newStatus = await masto.v1.statuses.create(params); newStatus = await masto.v1.statuses.create(params);
} }
} }
states.composerState.minimized = false;
states.composerState.publishing = false;
setUIState('default'); setUIState('default');
// Close // Close
@ -1031,6 +1054,8 @@ function Compose({
instance, instance,
}); });
} catch (e) { } catch (e) {
states.composerState.publishing = false;
states.composerState.publishingError = true;
console.error(e); console.error(e);
alert(e?.reason || e); alert(e?.reason || e);
setUIState('error'); setUIState('error');

View file

@ -10,17 +10,56 @@
align-items: center; align-items: center;
background-color: var(--backdrop-color); background-color: var(--backdrop-color);
animation: appear 0.5s var(--timing-function) both; animation: appear 0.5s var(--timing-function) both;
transition: all 0.5s var(--timing-function);
&.solid { &.solid {
background-color: var(--backdrop-solid-color); background-color: var(--backdrop-solid-color);
} }
--compose-button-dimension: 56px;
--compose-button-dimension-half: calc(var(--compose-button-dimension) / 2);
--compose-button-dimension-margin: 16px;
&.min {
/* Minimized */
pointer-events: none;
user-select: none;
overflow: hidden;
transform: scale(0);
--right: max(
var(--compose-button-dimension-margin),
env(safe-area-inset-right)
);
--bottom: max(
var(--compose-button-dimension-margin),
env(safe-area-inset-bottom)
);
--origin-right: calc(
100% - var(--compose-button-dimension-half) - var(--right)
);
--origin-bottom: calc(
100% - var(--compose-button-dimension-half) - var(--bottom)
);
transform-origin: var(--origin-right) var(--origin-bottom);
}
.sheet { .sheet {
transition: transform 0.3s var(--timing-function); transition: transform 0.3s var(--timing-function);
transform-origin: center bottom; transform-origin: 80% 80%;
} }
&:has(~ div) .sheet { &:has(~ div) .sheet {
transform: scale(0.975); transform: scale(0.975);
} }
} }
@media (max-width: calc(40em - 1px)) {
#app[data-shortcuts-view-mode='tab-menu-bar'] ~ #modal-container > div.min {
border: 2px solid red;
--bottom: calc(
var(--compose-button-dimension-margin) + env(safe-area-inset-bottom) +
52px
);
}
}

View file

@ -8,7 +8,7 @@ import useCloseWatcher from '../utils/useCloseWatcher';
const $modalContainer = document.getElementById('modal-container'); const $modalContainer = document.getElementById('modal-container');
function Modal({ children, onClose, onClick, class: className }) { function Modal({ children, onClose, onClick, class: className, minimized }) {
if (!children) return null; if (!children) return null;
const modalRef = useRef(); const modalRef = useRef();
@ -43,21 +43,30 @@ function Modal({ children, onClose, onClick, class: className }) {
useEffect(() => { useEffect(() => {
const $deckContainers = document.querySelectorAll('.deck-container'); const $deckContainers = document.querySelectorAll('.deck-container');
if (children) { if (minimized) {
$deckContainers.forEach(($deckContainer) => { // Similar to focusDeck in focus-deck.jsx
$deckContainer.setAttribute('inert', ''); // Focus last deck
}); const page = $deckContainers[$deckContainers.length - 1]; // last one
if (page && page.tabIndex === -1) {
page.focus();
}
} else { } else {
$deckContainers.forEach(($deckContainer) => { if (children) {
$deckContainer.removeAttribute('inert'); $deckContainers.forEach(($deckContainer) => {
}); $deckContainer.setAttribute('inert', '');
});
} else {
$deckContainers.forEach(($deckContainer) => {
$deckContainer.removeAttribute('inert');
});
}
} }
return () => { return () => {
$deckContainers.forEach(($deckContainer) => { $deckContainers.forEach(($deckContainer) => {
$deckContainer.removeAttribute('inert'); $deckContainer.removeAttribute('inert');
}); });
}; };
}, [children]); }, [children, minimized]);
const Modal = ( const Modal = (
<div <div
@ -72,7 +81,8 @@ function Modal({ children, onClose, onClick, class: className }) {
onClose?.(e); onClose?.(e);
} }
}} }}
tabIndex="-1" tabIndex={minimized ? 0 : '-1'}
inert={minimized}
onFocus={(e) => { onFocus={(e) => {
try { try {
if (e.target === e.currentTarget) { if (e.target === e.currentTarget) {

View file

@ -39,7 +39,10 @@ export default function Modals() {
return ( return (
<> <>
{!!snapStates.showCompose && ( {!!snapStates.showCompose && (
<Modal class="solid"> <Modal
class={`solid ${snapStates.composerState.minimized ? 'min' : ''}`}
minimized={!!snapStates.composerState.minimized}
>
<IntlSegmenterSuspense> <IntlSegmenterSuspense>
<Compose <Compose
replyToStatus={ replyToStatus={

View file

@ -51,6 +51,7 @@ import openCompose from '../utils/open-compose';
import pmem from '../utils/pmem'; import pmem from '../utils/pmem';
import safeBoundingBoxPadding from '../utils/safe-bounding-box-padding'; import safeBoundingBoxPadding from '../utils/safe-bounding-box-padding';
import shortenNumber from '../utils/shorten-number'; import shortenNumber from '../utils/shorten-number';
import showCompose from '../utils/show-compose';
import showToast from '../utils/show-toast'; import showToast from '../utils/show-toast';
import { speak, supportsTTS } from '../utils/speech'; import { speak, supportsTTS } from '../utils/speech';
import states, { getStatus, saveStatus, statusKey } from '../utils/states'; import states, { getStatus, saveStatus, statusKey } from '../utils/states';
@ -524,9 +525,9 @@ function Status({
}); });
if (newWin) return; if (newWin) return;
} }
states.showCompose = { showCompose({
replyToStatus: status, replyToStatus: status,
}; });
}; };
// Check if media has no descriptions // Check if media has no descriptions
@ -771,11 +772,11 @@ function Status({
menuExtras={ menuExtras={
<MenuItem <MenuItem
onClick={() => { onClick={() => {
states.showCompose = { showCompose({
draftStatus: { draftStatus: {
status: `\n${url}`, status: `\n${url}`,
}, },
}; });
}} }}
> >
<Icon icon="quote" /> <Icon icon="quote" />
@ -1092,9 +1093,9 @@ function Status({
{supports('@mastodon/post-edit') && ( {supports('@mastodon/post-edit') && (
<MenuItem <MenuItem
onClick={() => { onClick={() => {
states.showCompose = { showCompose({
editStatus: status, editStatus: status,
}; });
}} }}
> >
<Icon icon="pencil" /> <Icon icon="pencil" />
@ -2125,11 +2126,11 @@ function Status({
menuExtras={ menuExtras={
<MenuItem <MenuItem
onClick={() => { onClick={() => {
states.showCompose = { showCompose({
draftStatus: { draftStatus: {
status: `\n${url}`, status: `\n${url}`,
}, },
}; });
}} }}
> >
<Icon icon="quote" /> <Icon icon="quote" />

View file

@ -388,6 +388,27 @@ select.plain {
background-color: transparent; background-color: transparent;
} }
.button-group {
display: flex;
button,
.button {
margin-inline: calc(-1 * var(--hairline-width));
&:first-child:not(:only-child) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
&:not(:first-child, :last-child, :only-child) {
border-radius: 0;
}
&:last-child:not(:only-child) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
}
pre { pre {
tab-size: 2; tab-size: 2;
} }
@ -547,3 +568,9 @@ kbd {
.shazam-container-horizontal[hidden] { .shazam-container-horizontal[hidden] {
grid-template-columns: 0fr; grid-template-columns: 0fr;
} }
@keyframes spin {
to {
transform: rotate(360deg);
}
}

View file

@ -23,12 +23,6 @@
} }
} }
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.hero-heading { .hero-heading {
font-size: var(--text-size); font-size: var(--text-size);
display: inline-block; display: inline-block;

27
src/utils/show-compose.js Normal file
View file

@ -0,0 +1,27 @@
import openOSK from './open-osk';
import showToast from './show-toast';
import states from './states';
const TOAST_DURATION = 5_000; // 5 seconds
export default function showCompose(opts) {
if (!opts) opts = true;
if (states.showCompose) {
if (states.composerState.minimized) {
showToast({
duration: TOAST_DURATION,
text: `A draft post is currently minimized. Post or discard it before creating a new one.`,
});
} else {
showToast({
duration: TOAST_DURATION,
text: `A post is currently open. Post or discard it before creating a new one.`,
});
}
return;
}
openOSK();
states.showCompose = opts;
}

View file

@ -40,6 +40,7 @@ const states = proxy({
statusReply: {}, statusReply: {},
accounts: {}, accounts: {},
routeNotification: null, routeNotification: null,
composerState: {},
// Modals // Modals
showCompose: false, showCompose: false,
showSettings: false, showSettings: false,