From 9d78e67381ded5a5cc770f76c6cc0a69f0954f66 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Tue, 13 Dec 2022 20:42:09 +0800 Subject: [PATCH 01/33] New feature: pop-out compose window - More consistent design for both reply-to status and source status preview - Fixed bugs too - Make sure index.css is always above --- .prettierrc | 8 +- compose/index.html | 14 +++ src/app.jsx | 3 +- src/components/compose.css | 54 +++++++--- src/components/compose.jsx | 211 +++++++++++++++++++++++++++++++------ src/components/icon.jsx | 18 +++- src/components/status.css | 6 +- src/compose.jsx | 55 ++++++++++ src/main.jsx | 1 - vite.config.js | 9 ++ 10 files changed, 329 insertions(+), 50 deletions(-) create mode 100644 compose/index.html create mode 100644 src/compose.jsx diff --git a/.prettierrc b/.prettierrc index 6e1e7a60..07e43051 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 00000000..23ba6668 --- /dev/null +++ b/compose/index.html @@ -0,0 +1,14 @@ + + + + + + + Compose / Phanpy + + + +
+ + + diff --git a/src/app.jsx b/src/app.jsx index 29ab55fd..4767c4d6 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 2914e3d5..8c150437 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 654f0806..1d6380ca 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 2955ae4b..a55314c7 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 8cec7e3f..741f8910 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 00000000..46de5233 --- /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 09eba20f..c285d6dd 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 019d2194..53b43051 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'), + }, + }, + }, }); From 6195f458001f19d20ddf82789c95c28cf3fb19af Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Tue, 13 Dec 2022 20:42:19 +0800 Subject: [PATCH 02/33] Remove unused code --- src/components/status.css | 3 --- src/pages/home.jsx | 1 - src/pages/notifications.jsx | 1 - 3 files changed, 5 deletions(-) diff --git a/src/components/status.css b/src/components/status.css index 741f8910..40034bed 100644 --- a/src/components/status.css +++ b/src/components/status.css @@ -177,9 +177,6 @@ .status .content p:last-child { margin-block-end: 0; } -.status .content p code { - color: var(--green-color); -} .status .content .invisible { display: none; } diff --git a/src/pages/home.jsx b/src/pages/home.jsx index db2423c0..56b382cb 100644 --- a/src/pages/home.jsx +++ b/src/pages/home.jsx @@ -49,7 +49,6 @@ export default ({ hidden }) => { states.home.push(...homeValues); } states.homeLastFetchTime = Date.now(); - console.log(allStatuses); return allStatuses; } diff --git a/src/pages/notifications.jsx b/src/pages/notifications.jsx index e7628f57..355e8c6c 100644 --- a/src/pages/notifications.jsx +++ b/src/pages/notifications.jsx @@ -151,7 +151,6 @@ export default () => { states.notifications.push(...notificationsValues); } states.notificationsLastFetchTime = Date.now(); - console.log(allNotifications); return allNotifications; } From e2346bc32ae1e40c6eaaefcabbbb3114f499a76c Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Tue, 13 Dec 2022 21:54:16 +0800 Subject: [PATCH 03/33] Shift+click to open compose window - Fixes for closing - Easier code for checking if can close window --- src/app.jsx | 18 +++++-- src/components/compose.jsx | 100 +++++++++++++++++-------------------- src/compose.jsx | 10 +++- src/utils/open-compose.js | 23 +++++++++ 4 files changed, 94 insertions(+), 57 deletions(-) create mode 100644 src/utils/open-compose.js diff --git a/src/app.jsx b/src/app.jsx index 4767c4d6..93f5e27a 100644 --- a/src/app.jsx +++ b/src/app.jsx @@ -18,6 +18,7 @@ import Settings from './pages/settings'; import Status from './pages/status'; import Welcome from './pages/welcome'; import { getAccessToken } from './utils/auth'; +import openCompose from './utils/open-compose'; import states from './utils/states'; import store from './utils/store'; @@ -224,7 +225,17 @@ export function App() { @@ -268,9 +279,10 @@ export function App() { } editStatus={snapStates.showCompose?.editStatus || null} draftStatus={snapStates.showCompose?.draftStatus || null} - onClose={(result) => { + onClose={(results) => { + const { newStatus } = results || {}; states.showCompose = false; - if (result) { + if (newStatus) { states.reloadStatusPage++; } }} diff --git a/src/components/compose.jsx b/src/components/compose.jsx index 1d6380ca..bffca125 100644 --- a/src/components/compose.jsx +++ b/src/components/compose.jsx @@ -5,6 +5,7 @@ import { useEffect, useMemo, useRef, useState } from 'preact/hooks'; import stringLength from 'string-length'; import emojifyText from '../utils/emojify-text'; +import openCompose from '../utils/open-compose'; import store from '../utils/store'; import visibilityIconsMap from '../utils/visibility-icons-map'; @@ -207,30 +208,36 @@ export default ({ const canClose = () => { const { value, dataset } = textareaRef.current; - // check for non-ID media attachments - const hasNonIDMediaAttachments = + // check for status and media attachments with IDs + const hasIDMediaAttachments = mediaAttachments.length > 0 && - mediaAttachments.some((media) => !media.id); + mediaAttachments.every((media) => media.id); + if (!value && hasIDMediaAttachments) { + console.log('canClose', { value, mediaAttachments }); + return true; + } // check if status contains only "@acct", if replying - const hasAcct = + const isSelf = replyToStatus?.account.id === currentAccount; + const hasOnlyAcct = replyToStatus && value.trim() === `@${replyToStatus.account.acct}`; + if (!isSelf && hasOnlyAcct) { + console.log('canClose', { isSelf, hasOnlyAcct }); + return true; + } - // check if status is different than source - const differentThanSource = dataset?.source && value !== dataset.source; + // check if status is same with source + const sameWithSource = value === dataset?.source; + if (sameWithSource) { + console.log('canClose', { sameWithSource }); + return true; + } - console.log({ - value, - hasAcct, - differentThanSource, - hasNonIDMediaAttachments, - }); + return false; + }; - if ( - (value && !hasAcct) || - differentThanSource || - hasNonIDMediaAttachments - ) { + const confirmClose = () => { + if (canClose()) { const yes = confirm(beforeUnloadCopy); return yes; } @@ -283,30 +290,11 @@ export default ({ } } - const url = new URL('/compose/', window.location); - const screenWidth = window.screen.width; - const screenHeight = window.screen.height; - const left = Math.max(0, (screenWidth - 600) / 2); - const top = Math.max(0, (screenHeight - 450) / 2); - const width = Math.min(screenWidth, 600); - const height = Math.min(screenHeight, 450); - const newWin = window.open( - url, - 'compose' + Math.random(), - `width=${width},height=${height},left=${left},top=${top}`, - ); - - if (!newWin) { - alert('Looks like your browser is blocking popups.'); - return; - } - const mediaAttachmentsWithIDs = mediaAttachments.filter( (media) => media.id, ); - newWin.masto = masto; - newWin.__COMPOSE__ = { + const newWin = openCompose({ editStatus, replyToStatus, draftStatus: { @@ -316,10 +304,14 @@ export default ({ sensitive, mediaAttachments: mediaAttachmentsWithIDs, }, - }; - onClose(() => { - window.opener.__STATES__.reloadStatusPage++; }); + + if (!newWin) { + alert('Looks like your browser is blocking popups.'); + return; + } + + onClose(); }} > @@ -328,7 +320,7 @@ export default ({ type="button" class="light close-button" onClick={() => { - if (canClose()) { + if (confirmClose()) { onClose(); } }} @@ -363,18 +355,20 @@ export default ({ (media) => media.id, ); - onClose(() => { - window.opener.__STATES__.showCompose = { - editStatus, - replyToStatus, - draftStatus: { - status: textareaRef.current.value, - spoilerText: spoilerTextRef.current.value, - visibility, - sensitive, - mediaAttachments: mediaAttachmentsWithIDs, - }, - }; + onClose({ + fn: () => { + window.opener.__STATES__.showCompose = { + editStatus, + replyToStatus, + draftStatus: { + status: textareaRef.current.value, + spoilerText: spoilerTextRef.current.value, + visibility, + sensitive, + mediaAttachments: mediaAttachmentsWithIDs, + }, + }; + }, }); }} > diff --git a/src/compose.jsx b/src/compose.jsx index 46de5233..e3310d2b 100644 --- a/src/compose.jsx +++ b/src/compose.jsx @@ -8,6 +8,10 @@ import { useEffect, useState } from 'preact/hooks'; import Compose from './components/compose'; +if (window.opener) { + console = window.opener.console; +} + function App() { const [uiState, setUIState] = useState('default'); @@ -42,8 +46,12 @@ function App() { replyToStatus={replyToStatus} draftStatus={draftStatus} standalone - onClose={(fn = () => {}) => { + onClose={(results) => { + const { newStatus, fn = () => {} } = results || {}; try { + if (newStatus) { + window.opener.__STATES__.reloadStatusPage++; + } fn(); setUIState('closed'); } catch (e) {} diff --git a/src/utils/open-compose.js b/src/utils/open-compose.js new file mode 100644 index 00000000..daa026af --- /dev/null +++ b/src/utils/open-compose.js @@ -0,0 +1,23 @@ +export default (opts) => { + const url = new URL('/compose/', window.location); + const { width: screenWidth, height: screenHeight } = window.screen; + const left = Math.max(0, (screenWidth - 600) / 2); + const top = Math.max(0, (screenHeight - 450) / 2); + const width = Math.min(screenWidth, 600); + const height = Math.min(screenHeight, 450); + const newWin = window.open( + url, + 'compose' + Math.random(), + `width=${width},height=${height},left=${left},top=${top}`, + ); + + if (newWin) { + if (masto) { + newWin.masto = masto; + } + + newWin.__COMPOSE__ = opts; + } + + return newWin; +}; From fb6bca9016cc5ff91842b7678113a38999f69027 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Tue, 13 Dec 2022 22:26:29 +0800 Subject: [PATCH 04/33] Fix wrong logic again --- src/components/compose.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/compose.jsx b/src/components/compose.jsx index bffca125..4ce618b5 100644 --- a/src/components/compose.jsx +++ b/src/components/compose.jsx @@ -237,7 +237,7 @@ export default ({ }; const confirmClose = () => { - if (canClose()) { + if (!canClose()) { const yes = confirm(beforeUnloadCopy); return yes; } From 04084d33398db0685e6ae02dcc7905bc68a476dd Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Tue, 13 Dec 2022 22:34:23 +0800 Subject: [PATCH 05/33] 'not-allowed' cursor looks very annoying --- src/components/compose.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/compose.css b/src/components/compose.css index 8c150437..4e4e071e 100644 --- a/src/components/compose.css +++ b/src/components/compose.css @@ -139,10 +139,10 @@ outline: 0; } #compose-container .toolbar-button:has([disabled]) { - cursor: not-allowed; + pointer-events: none; } #compose-container .toolbar-button:has([disabled]) > * { - filter: opacity(0.3); + filter: opacity(0.5); } #compose-container .toolbar-button:not(.show-field) From 2b21c421d1432539c9d55d65dc0358a0b3adf437 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Wed, 14 Dec 2022 00:19:49 +0800 Subject: [PATCH 06/33] Comment out this console.log --- src/components/compose.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/compose.jsx b/src/components/compose.jsx index 4ce618b5..eda2d3fa 100644 --- a/src/components/compose.jsx +++ b/src/components/compose.jsx @@ -114,7 +114,7 @@ export default ({ useEffect(() => { if (textExpanderRef.current) { const handleChange = (e) => { - console.log('text-expander-change', e); + // console.log('text-expander-change', e); const { key, provide, text } = e.detail; textExpanderTextRef.current = text; if (text === '') { From 0e1f4c527ef82e7877020bdd16ab10176bcd27ed Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Wed, 14 Dec 2022 00:20:24 +0800 Subject: [PATCH 07/33] This close window check is getting on my nerves --- src/components/compose.jsx | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/components/compose.jsx b/src/components/compose.jsx index eda2d3fa..aef824c6 100644 --- a/src/components/compose.jsx +++ b/src/components/compose.jsx @@ -208,15 +208,20 @@ export default ({ const canClose = () => { const { value, dataset } = textareaRef.current; - // check for status and media attachments with IDs - const hasIDMediaAttachments = - mediaAttachments.length > 0 && - mediaAttachments.every((media) => media.id); - if (!value && hasIDMediaAttachments) { + // check for status and media attachments + const hasMediaAttachments = mediaAttachments.length > 0; + if (!value && !hasMediaAttachments) { console.log('canClose', { value, mediaAttachments }); return true; } + // check if all media attachments have IDs + const hasIDMediaAttachments = mediaAttachments.every((media) => media.id); + if (hasIDMediaAttachments) { + console.log('canClose', { hasIDMediaAttachments }); + return true; + } + // check if status contains only "@acct", if replying const isSelf = replyToStatus?.account.id === currentAccount; const hasOnlyAcct = @@ -233,6 +238,15 @@ export default ({ return true; } + console.log('canClose', { + value, + hasMediaAttachments, + hasIDMediaAttachments, + isSelf, + hasOnlyAcct, + sameWithSource, + }); + return false; }; From 38aaf89529651c15af54926f67d048f896f51480 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Wed, 14 Dec 2022 00:20:43 +0800 Subject: [PATCH 08/33] Don't need this animation, too many happening at the same time --- src/components/compose.css | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/components/compose.css b/src/components/compose.css index 4e4e071e..27fa7edc 100644 --- a/src/components/compose.css +++ b/src/components/compose.css @@ -53,14 +53,6 @@ */ pointer-events: none; } -@keyframes appear-down { - 0% { - transform: translateY(-2em); - } - 100% { - transform: translateY(0); - } -} #compose-container .status-preview-legend { pointer-events: none; @@ -101,7 +93,6 @@ z-index: 1; } #compose-container .status-preview ~ form { - animation: appear-down 1s ease-in-out; box-shadow: 0 -12px 12px -12px var(--divider-color); } From 975f3dd33f16fde07f1aa27f37144a60fb1698a8 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Wed, 14 Dec 2022 00:39:35 +0800 Subject: [PATCH 09/33] Finally fix HMR not working for this file --- src/components/compose.jsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/compose.jsx b/src/components/compose.jsx index aef824c6..dfce4c67 100644 --- a/src/components/compose.jsx +++ b/src/components/compose.jsx @@ -18,13 +18,13 @@ import Status from './status'; - Max character limit includes BOTH status text and Content Warning text */ -export default ({ +function Compose({ onClose, replyToStatus, editStatus, draftStatus, standalone, -}) => { +}) { const [uiState, setUIState] = useState('default'); const accounts = store.local.getJSON('accounts'); @@ -696,7 +696,7 @@ export default ({
); -}; +} function MediaAttachment({ attachment, @@ -759,3 +759,5 @@ function MediaAttachment({
); } + +export default Compose; From d4cdf2435fe62550274d60caaa17517cec14f169 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Wed, 14 Dec 2022 01:48:55 +0800 Subject: [PATCH 10/33] Fix poll percentage showing NaN This happens when votes are still 0, so 0 divide by 0 --- src/components/status.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/status.jsx b/src/components/status.jsx index 16a738c7..fddcd472 100644 --- a/src/components/status.jsx +++ b/src/components/status.jsx @@ -287,7 +287,8 @@ function Poll({ poll }) { {voted || expired ? ( options.map((option, i) => { const { title, votesCount: optionVotesCount } = option; - const percentage = Math.round((optionVotesCount / votesCount) * 100); + const percentage = + Math.round((optionVotesCount / votesCount) * 100) || 0; return (
Date: Wed, 14 Dec 2022 15:39:20 +0800 Subject: [PATCH 11/33] Better username lookup --- src/components/status.jsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/status.jsx b/src/components/status.jsx index fddcd472..a8980da7 100644 --- a/src/components/status.jsx +++ b/src/components/status.jsx @@ -695,9 +695,14 @@ function Status({ ) { e.preventDefault(); e.stopPropagation(); - const username = target.querySelector('span'); + const username = ( + target.querySelector('span') || target + ).innerText + .trim() + .replace(/^@/, ''); const mention = mentions.find( - (mention) => mention.username === username?.innerText.trim(), + (mention) => + mention.username === username || mention.acct === username, ); if (mention) { states.showAccount = mention.acct; From 8b5ac5154c1f0f800405fe92cc946d44dac0af43 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Wed, 14 Dec 2022 16:16:08 +0800 Subject: [PATCH 12/33] Fix pre meta text taking up space, due to some *very* long names/usernames --- src/components/status.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/status.css b/src/components/status.css index 40034bed..1c048439 100644 --- a/src/components/status.css +++ b/src/components/status.css @@ -42,6 +42,12 @@ opacity: 0.75; font-size: smaller; vertical-align: middle; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} +.status-pre-meta .name-text { + display: inline; } .status-pre-meta .icon { color: var(--reblog-color); From 410548603fddaea66d8613be0ee5496ef3b8d76c Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Wed, 14 Dec 2022 17:11:16 +0800 Subject: [PATCH 13/33] Fix check icon not appearing for own-voted poll choices --- src/components/status.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/status.jsx b/src/components/status.jsx index a8980da7..0d2db256 100644 --- a/src/components/status.jsx +++ b/src/components/status.jsx @@ -301,7 +301,7 @@ function Poll({ poll }) { {voted && ownVotes.includes(i) && ( <> {' '} - + )}
From b883836f8a5a38007971a3cafef8b4f6d8ff1041 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Wed, 14 Dec 2022 17:56:08 +0800 Subject: [PATCH 14/33] Fix percentage count for multiple-choice polls --- src/components/status.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/status.jsx b/src/components/status.jsx index 0d2db256..213d1994 100644 --- a/src/components/status.jsx +++ b/src/components/status.jsx @@ -287,8 +287,9 @@ function Poll({ poll }) { {voted || expired ? ( options.map((option, i) => { const { title, votesCount: optionVotesCount } = option; + const pollVotesCount = votersCount || votesCount; const percentage = - Math.round((optionVotesCount / votesCount) * 100) || 0; + Math.round((optionVotesCount / pollVotesCount) * 100) || 0; return (
Date: Wed, 14 Dec 2022 18:45:34 +0800 Subject: [PATCH 15/33] Slightly smaller font size for meta text in poll --- src/components/status.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/status.css b/src/components/status.css index 1c048439..29839f47 100644 --- a/src/components/status.css +++ b/src/components/status.css @@ -401,6 +401,7 @@ a.card:hover { } .poll-meta { margin: 8px 0; + font-size: 90%; } /* EXTRA META */ From 121e9176f37ff66ea54cb475d1928d8e2758f57f Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Wed, 14 Dec 2022 19:00:04 +0800 Subject: [PATCH 16/33] Add style for leading choices Also make sure the votes percentage doesn't shrink --- src/components/status.css | 7 +++++++ src/components/status.jsx | 6 +++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/status.css b/src/components/status.css index 29839f47..e0d2e7c1 100644 --- a/src/components/status.css +++ b/src/components/status.css @@ -396,6 +396,13 @@ a.card:hover { gap: 8px; cursor: pointer; } +.poll-option-votes { + flex-shrink: 0; + font-size: 90%; +} +.poll-option-leading .poll-option-votes { + font-weight: bold; +} .poll-vote-button { margin-top: 8px; } diff --git a/src/components/status.jsx b/src/components/status.jsx index 213d1994..ef304de7 100644 --- a/src/components/status.jsx +++ b/src/components/status.jsx @@ -290,9 +290,13 @@ function Poll({ poll }) { const pollVotesCount = votersCount || votesCount; const percentage = Math.round((optionVotesCount / pollVotesCount) * 100) || 0; + // check if current poll choice is the leading one + const isLeading = + optionVotesCount > 0 && + optionVotesCount === Math.max(...options.map((o) => o.votesCount)); return (
Date: Wed, 14 Dec 2022 21:48:17 +0800 Subject: [PATCH 17/33] New feature: poll - More fixes --- compose/index.html | 2 +- src/app.css | 2 +- src/app.jsx | 4 +- src/components/compose.css | 94 ++++++++- src/components/compose.jsx | 392 +++++++++++++++++++++++++++---------- src/components/icon.jsx | 3 + src/components/status.jsx | 88 ++++++--- src/compose.jsx | 5 +- src/index.css | 13 +- 9 files changed, 456 insertions(+), 147 deletions(-) diff --git a/compose/index.html b/compose/index.html index 23ba6668..34b7cca1 100644 --- a/compose/index.html +++ b/compose/index.html @@ -8,7 +8,7 @@ -
+
diff --git a/src/app.css b/src/app.css index de389690..4831ad3a 100644 --- a/src/app.css +++ b/src/app.css @@ -4,7 +4,7 @@ body { padding: 0; background-color: var(--bg-color); color: var(--text-color); - overflow: hidden; + /* overflow: hidden; */ } #app { diff --git a/src/app.jsx b/src/app.jsx index 93f5e27a..37b69227 100644 --- a/src/app.jsx +++ b/src/app.jsx @@ -277,8 +277,8 @@ export function App() { ? snapStates.showCompose.replyToStatus : null } - editStatus={snapStates.showCompose?.editStatus || null} - draftStatus={snapStates.showCompose?.draftStatus || null} + editStatus={states.showCompose?.editStatus || null} + draftStatus={states.showCompose?.draftStatus || null} onClose={(results) => { const { newStatus } = results || {}; states.showCompose = false; diff --git a/src/components/compose.css b/src/components/compose.css index 27fa7edc..5d315957 100644 --- a/src/components/compose.css +++ b/src/components/compose.css @@ -6,6 +6,10 @@ max-height: 100vh; overflow: auto; } +#compose-container.standalone { + max-height: none; + margin: auto; +} #compose-container .compose-top { text-align: right; @@ -110,8 +114,8 @@ min-width: 0; } #compose-container .toolbar-button { - cursor: pointer; display: inline-block; + color: var(--text-color); background-color: var(--bg-faded-color); padding: 0 8px; border-radius: 8px; @@ -123,6 +127,7 @@ position: relative; white-space: nowrap; border: 2px solid transparent; + vertical-align: middle; } #compose-container .toolbar-button > * { vertical-align: middle; @@ -131,9 +136,11 @@ } #compose-container .toolbar-button:has([disabled]) { pointer-events: none; + background-color: var(--bg-faded-color); + opacity: 0.5; } #compose-container .toolbar-button:has([disabled]) > * { - filter: opacity(0.5); + /* filter: opacity(0.5); */ } #compose-container .toolbar-button:not(.show-field) @@ -157,10 +164,12 @@ right: 0; left: auto !important; } -#compose-container .toolbar-button:hover { +#compose-container .toolbar-button:not(:disabled):hover { + cursor: pointer; + filter: none; border-color: var(--divider-color); } -#compose-container .toolbar-button:active { +#compose-container .toolbar-button:not(:disabled):active { filter: brightness(0.8); } @@ -272,3 +281,80 @@ color: var(--green-color); 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); +} diff --git a/src/components/compose.jsx b/src/components/compose.jsx index dfce4c67..1b1ba916 100644 --- a/src/components/compose.jsx +++ b/src/components/compose.jsx @@ -18,12 +18,31 @@ import Status from './status'; - 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({ onClose, replyToStatus, editStatus, draftStatus, standalone, + hasOpener, }) { const [uiState, setUIState] = useState('default'); @@ -57,10 +76,11 @@ function Compose({ } = configuration; const textareaRef = useRef(); - + const spoilerTextRef = useRef(); const [visibility, setVisibility] = useState('public'); const [sensitive, setSensitive] = useState(false); - const spoilerTextRef = useRef(); + const [mediaAttachments, setMediaAttachments] = useState([]); + const [poll, setPoll] = useState(null); useEffect(() => { if (replyToStatus) { @@ -78,15 +98,32 @@ function Compose({ setSensitive(sensitive); } if (draftStatus) { - const { status, spoilerText, visibility, sensitive, mediaAttachments } = - draftStatus; + const { + 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; spoilerTextRef.current.value = spoilerText; setVisibility(visibility); setSensitive(sensitive); + setPoll(composablePoll); setMediaAttachments(mediaAttachments); } 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'); (async () => { try { @@ -98,6 +135,7 @@ function Compose({ spoilerTextRef.current.value = spoilerText; setVisibility(visibility); setSensitive(sensitive); + setPoll(composablePoll); setMediaAttachments(mediaAttachments); setUIState('default'); } catch (e) { @@ -199,8 +237,6 @@ function Compose({ } }, []); - const [mediaAttachments, setMediaAttachments] = useState([]); - const formRef = useRef(); const beforeUnloadCopy = @@ -216,7 +252,9 @@ function Compose({ } // 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) { console.log('canClose', { hasIDMediaAttachments }); return true; @@ -242,6 +280,7 @@ function Compose({ value, hasMediaAttachments, hasIDMediaAttachments, + poll, isSelf, hasOnlyAcct, sameWithSource, @@ -316,6 +355,7 @@ function Compose({ spoilerText: spoilerTextRef.current.value, visibility, sensitive, + poll, mediaAttachments: mediaAttachmentsWithIDs, }, }); @@ -343,51 +383,54 @@ function Compose({ ) : ( - + onClose({ + fn: () => { + window.opener.__STATES__.showCompose = { + editStatus, + replyToStatus, + draftStatus: { + status: textareaRef.current.value, + spoilerText: spoilerTextRef.current.value, + visibility, + sensitive, + poll, + mediaAttachments: mediaAttachmentsWithIDs, + }, + }; + }, + }); + }} + > + + + ) )}
{!!replyToStatus && ( @@ -436,6 +479,16 @@ function Compose({ ); 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 // Post-cleanup @@ -449,8 +502,7 @@ function Compose({ if (mediaAttachments.length > 0) { // Upload media attachments first const mediaPromises = mediaAttachments.map((attachment) => { - const { file, description, sourceDescription, id } = - attachment; + const { file, description, id } = attachment; console.log('UPLOADING', attachment); if (id) { // If already uploaded @@ -493,6 +545,7 @@ function Compose({ status, spoilerText, sensitive, + poll, mediaIds: mediaAttachments.map((attachment) => attachment.id), }; if (!editStatus) { @@ -639,59 +692,87 @@ function Compose({ })}
)} + {!!poll && ( + { + if (poll) { + const newPoll = { ...poll }; + setPoll(newPoll); + } else { + setPoll(null); + } + }} + /> + )}
-
-
-
- {uiState === 'loading' && }{' '} - -
+ }} + /> + + {' '} + {' '} +
+ {uiState === 'loading' && }{' '} +
@@ -760,4 +841,111 @@ function MediaAttachment({ ); } +function Poll({ + poll, + disabled, + onInput = () => {}, + maxOptions, + maxExpiration, + minExpiration, + maxCharactersPerOption, +}) { + const { options, expiresIn, multiple } = poll; + + return ( +
+
+ {options.map((option, i) => ( +
+ { + const { value } = e.target; + options[i] = value; + onInput(poll); + }} + /> + +
+ ))} +
+
+ {' '} + + +
+
+ +
+
+ ); +} + export default Compose; diff --git a/src/components/icon.jsx b/src/components/icon.jsx index a55314c7..2fec0714 100644 --- a/src/components/icon.jsx +++ b/src/components/icon.jsx @@ -40,9 +40,12 @@ const ICONS = { external: 'mingcute:external-link-line', popout: 'mingcute:external-link-line', popin: ['mingcute:external-link-line', '180deg'], + plus: 'mingcute:add-circle-line', }; export default ({ icon, size = 'm', alt, title, class: className = '' }) => { + if (!icon) return null; + const iconSize = SIZES[size]; let iconName = ICONS[icon]; let rotate; diff --git a/src/components/status.jsx b/src/components/status.jsx index ef304de7..986df14d 100644 --- a/src/components/status.jsx +++ b/src/components/status.jsx @@ -264,10 +264,14 @@ function Card({ card }) { } } -function Poll({ poll }) { +function Poll({ poll, readOnly }) { const [pollSnapshot, setPollSnapshot] = useState(poll); const [uiState, setUIState] = useState('default'); + useEffect(() => { + setPollSnapshot(poll); + }, [poll]); + const { expired, expiresAt, @@ -280,7 +284,7 @@ function Poll({ poll }) { votesCount, } = pollSnapshot; - const expiresAtDate = new Date(expiresAt); + const expiresAtDate = !!expiresAt && new Date(expiresAt); return (
@@ -296,6 +300,7 @@ function Poll({ poll }) { optionVotesCount === Math.max(...options.map((o) => o.votesCount)); return (
@@ -357,37 +362,44 @@ function Poll({ poll }) { name="poll" value={i} disabled={uiState === 'loading'} + readOnly={readOnly} /> {title}
); })} - + {!readOnly && ( + + )} )} -

- {shortenNumber(votersCount)}{' '} - {votersCount === 1 ? 'voter' : 'voters'} - {votersCount !== votesCount && ( - <> - {' '} - • - {shortenNumber(votesCount)} - {' '} - vote - {votesCount === 1 ? '' : 's'} - - )}{' '} - • {expired ? 'Ended' : 'Ending'}{' '} - -

+ {!readOnly && ( +

+ {shortenNumber(votersCount)}{' '} + {votersCount === 1 ? 'voter' : 'voters'} + {votersCount !== votesCount && ( + <> + {' '} + • + {shortenNumber(votesCount)} + {' '} + vote + {votesCount === 1 ? '' : 's'} + + )}{' '} + • {expired ? 'Ended' : 'Ending'}{' '} + {!!expiresAtDate && ( + + )} +

+ )}
); } @@ -449,7 +461,7 @@ function EditedAtModal({ statusID, onClose = () => {} }) { }).format(createdAtDate)} - + ); })} @@ -470,7 +482,7 @@ function Status({ withinContext, size = 'm', skeleton, - editStatus, + readOnly, }) { if (skeleton) { return ( @@ -645,7 +657,7 @@ function Status({ )} {' '} - {size !== 'l' && !editStatus && ( + {size !== 'l' && uri ? ( + ) : ( + + {' '} + + {createdAtDate.toLocaleString()} + + )}
- {!!poll && } + {!!poll && } {!spoilerText && sensitive && !!mediaAttachments.length && (