diff --git a/.prettierrc b/.prettierrc index 6e1e7a6..07e4305 100644 --- a/.prettierrc +++ b/.prettierrc @@ -3,7 +3,13 @@ "useTabs": false, "singleQuote": true, "trailingComma": "all", - "importOrder": [".css$", "", "^../", "^[./]"], + "importOrder": [ + "index.css$", + ".css$", + "", + "^../", + "^[./]" + ], "importOrderSeparation": true, "importOrderSortSpecifiers": true, "importOrderGroupNamespaceSpecifiers": true, diff --git a/compose/index.html b/compose/index.html new file mode 100644 index 0000000..23ba666 --- /dev/null +++ b/compose/index.html @@ -0,0 +1,14 @@ + + + + + + + Compose / Phanpy + + + +
+ + + diff --git a/src/app.jsx b/src/app.jsx index 29ab55f..4767c4d 100644 --- a/src/app.jsx +++ b/src/app.jsx @@ -23,7 +23,7 @@ import store from './utils/store'; const { VITE_CLIENT_NAME: CLIENT_NAME } = import.meta.env; -window._STATES = states; +window.__STATES__ = states; async function startStream() { const stream = await masto.stream.streamUser(); @@ -267,6 +267,7 @@ export function App() { : null } editStatus={snapStates.showCompose?.editStatus || null} + draftStatus={snapStates.showCompose?.draftStatus || null} onClose={(result) => { states.showCompose = false; if (result) { diff --git a/src/components/compose.css b/src/components/compose.css index 2914e3d..8c15043 100644 --- a/src/components/compose.css +++ b/src/components/compose.css @@ -19,11 +19,6 @@ z-index: 100; } -#compose-container .close-button { - padding: 6px; - color: var(--text-insignificant-color); -} - #compose-container textarea { width: 100%; max-width: 100%; @@ -42,18 +37,21 @@ transform: translateY(0); } } -#compose-container .reply-to { +#compose-container .status-preview { border-radius: 8px 8px 0 0; max-height: 160px; - pointer-events: none; - filter: saturate(0.25) opacity(0.75); - background-color: var(--bg-blur-color); + background-color: var(--bg-color); margin: 0 12px; border: 1px solid var(--outline-color); border-bottom: 0; - /* box-shadow: 0 0 12px var(--divider-color); */ - /* mask-image: linear-gradient(rgba(0, 0, 0, 1), rgba(0, 0, 0, 1) 90%, transparent); */ animation: appear-up 1s ease-in-out; + overflow: auto; +} +#compose-container.standalone .status-preview * { + /* + For standalone mode (new window), prevent interacting with the status preview for now + */ + pointer-events: none; } @keyframes appear-down { 0% { @@ -63,6 +61,38 @@ transform: translateY(0); } } + +#compose-container .status-preview-legend { + pointer-events: none; + position: sticky; + bottom: 0; + padding: 8px; + font-size: 80%; + font-weight: bold; + text-align: center; + color: var(--text-insignificant-color); + background-color: var(--bg-blur-color); + /* background-image: linear-gradient( + to bottom, + transparent, + var(--bg-faded-color) + ); */ + border-top: 1px 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), + 0 1px 10px var(--bg-color); +} +#_compose-container .status-preview-legend.reply-to { + color: var(--reply-to-color); + background-color: var(--reply-to-faded-color); + /* background-image: linear-gradient( + to bottom, + transparent, + var(--reply-to-faded-color) + ); */ +} + #compose-container form { border-radius: 8px; padding: 4px 12px; @@ -70,7 +100,7 @@ position: relative; z-index: 1; } -#compose-container .reply-to ~ form { +#compose-container .status-preview ~ form { animation: appear-down 1s ease-in-out; box-shadow: 0 -12px 12px -12px var(--divider-color); } diff --git a/src/components/compose.jsx b/src/components/compose.jsx index 654f080..1d6380c 100644 --- a/src/components/compose.jsx +++ b/src/components/compose.jsx @@ -17,7 +17,13 @@ import Status from './status'; - Max character limit includes BOTH status text and Content Warning text */ -export default ({ onClose, replyToStatus, editStatus }) => { +export default ({ + onClose, + replyToStatus, + editStatus, + draftStatus, + standalone, +}) => { const [uiState, setUIState] = useState('default'); const accounts = store.local.getJSON('accounts'); @@ -51,27 +57,34 @@ export default ({ onClose, replyToStatus, editStatus }) => { const textareaRef = useRef(); - const [visibility, setVisibility] = useState( - replyToStatus?.visibility || 'public', - ); - const [sensitive, setSensitive] = useState(replyToStatus?.sensitive || false); + const [visibility, setVisibility] = useState('public'); + const [sensitive, setSensitive] = useState(false); const spoilerTextRef = useRef(); useEffect(() => { - let timer = setTimeout(() => { - const spoilerText = replyToStatus?.spoilerText; + if (replyToStatus) { + const { spoilerText, visibility, sensitive } = replyToStatus; if (spoilerText && spoilerTextRef.current) { spoilerTextRef.current.value = spoilerText; spoilerTextRef.current.focus(); } else { - textareaRef.current?.focus(); + textareaRef.current.focus(); + if (replyToStatus.account.id !== currentAccount) { + textareaRef.current.value = `@${replyToStatus.account.acct} `; + } } - }, 0); - return () => clearTimeout(timer); - }, []); - - useEffect(() => { - if (editStatus) { + setVisibility(visibility); + setSensitive(sensitive); + } + if (draftStatus) { + const { status, spoilerText, visibility, sensitive, mediaAttachments } = + draftStatus; + textareaRef.current.value = status; + spoilerTextRef.current.value = spoilerText; + setVisibility(visibility); + setSensitive(sensitive); + setMediaAttachments(mediaAttachments); + } else if (editStatus) { const { visibility, sensitive, mediaAttachments } = editStatus; setUIState('loading'); (async () => { @@ -93,7 +106,7 @@ export default ({ onClose, replyToStatus, editStatus }) => { } })(); } - }, [editStatus]); + }, [draftStatus, editStatus, replyToStatus]); const textExpanderRef = useRef(); const textExpanderTextRef = useRef(''); @@ -192,13 +205,32 @@ export default ({ onClose, replyToStatus, editStatus }) => { const beforeUnloadCopy = 'You have unsaved changes. Are you sure you want to discard this post?'; const canClose = () => { - // check for status or mediaAttachments const { value, dataset } = textareaRef.current; - const containNonIDMediaAttachments = + + // check for non-ID media attachments + const hasNonIDMediaAttachments = mediaAttachments.length > 0 && mediaAttachments.some((media) => !media.id); - if ((value && value !== dataset?.source) || containNonIDMediaAttachments) { + // check if status contains only "@acct", if replying + const hasAcct = + replyToStatus && value.trim() === `@${replyToStatus.account.acct}`; + + // check if status is different than source + const differentThanSource = dataset?.source && value !== dataset.source; + + console.log({ + value, + hasAcct, + differentThanSource, + hasNonIDMediaAttachments, + }); + + if ( + (value && !hasAcct) || + differentThanSource || + hasNonIDMediaAttachments + ) { const yes = confirm(beforeUnloadCopy); return yes; } @@ -223,7 +255,7 @@ export default ({ onClose, replyToStatus, editStatus }) => { }, []); return ( -
+
{currentAccountInfo?.avatarStatic && ( { alt={currentAccountInfo.username} /> )} - + {!standalone ? ( + + {' '} + + + ) : ( + + )}
{!!replyToStatus && ( -
+
+
+ Replying to @ + {replyToStatus.account.acct || replyToStatus.account.username} +
+
+ )} + {!!editStatus && ( +
+ +
Editing source status
)}
{ { const sensitive = e.target.checked; diff --git a/src/components/icon.jsx b/src/components/icon.jsx index 2955ae4..a55314c 100644 --- a/src/components/icon.jsx +++ b/src/components/icon.jsx @@ -1,3 +1,5 @@ +import 'iconify-icon'; + const SIZES = { s: 12, m: 16, @@ -35,11 +37,18 @@ const ICONS = { upload: 'mingcute:upload-3-line', gear: 'mingcute:settings-3-line', more: 'mingcute:more-1-line', + external: 'mingcute:external-link-line', + popout: 'mingcute:external-link-line', + popin: ['mingcute:external-link-line', '180deg'], }; export default ({ icon, size = 'm', alt, title, class: className = '' }) => { const iconSize = SIZES[size]; - const iconName = ICONS[icon]; + let iconName = ICONS[icon]; + let rotate; + if (Array.isArray(iconName)) { + [iconName, rotate] = iconName; + } return (
{ lineHeight: 0, }} > - + {alt}
diff --git a/src/components/status.css b/src/components/status.css index 8cec7e3..741f891 100644 --- a/src/components/status.css +++ b/src/components/status.css @@ -93,11 +93,13 @@ transform: translateX(5px); } -.status:not(.small) .container { - padding-left: 16px; +.status .container { flex-grow: 1; min-width: 0; } +.status:not(.small) .container { + padding-left: 16px; +} .status > .container > .meta { display: flex; diff --git a/src/compose.jsx b/src/compose.jsx new file mode 100644 index 0000000..46de523 --- /dev/null +++ b/src/compose.jsx @@ -0,0 +1,55 @@ +import './index.css'; + +import './app.css'; + +import '@github/time-elements'; +import { render } from 'preact'; +import { useEffect, useState } from 'preact/hooks'; + +import Compose from './components/compose'; + +function App() { + const [uiState, setUIState] = useState('default'); + + const { editStatus, replyToStatus, draftStatus } = window.__COMPOSE__ || {}; + + useEffect(() => { + if (uiState === 'closed') { + window.close(); + } + }, [uiState]); + + if (uiState === 'closed') { + return ( +
+

You may close this page now.

+

+ +

+
+ ); + } + + return ( + {}) => { + try { + fn(); + setUIState('closed'); + } catch (e) {} + }} + /> + ); +} + +render(, document.getElementById('app')); diff --git a/src/main.jsx b/src/main.jsx index 09eba20..c285d6d 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,7 +1,6 @@ import './index.css'; import '@github/time-elements'; -import 'iconify-icon'; import { render } from 'preact'; import { App } from './app'; diff --git a/vite.config.js b/vite.config.js index 019d219..53b4305 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,7 +1,16 @@ import preact from '@preact/preset-vite'; +import { resolve } from 'path'; import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [preact()], + build: { + rollupOptions: { + input: { + main: resolve(__dirname, 'index.html'), + compose: resolve(__dirname, 'compose/index.html'), + }, + }, + }, });