From 45950da71426bbd11f0ebf94d4b3e888df0e7794 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Fri, 24 Feb 2023 23:38:59 +0800 Subject: [PATCH 01/66] Don't POST if no choices set in poll --- src/components/status.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/status.jsx b/src/components/status.jsx index 5c21528a..9976af4c 100644 --- a/src/components/status.jsx +++ b/src/components/status.jsx @@ -951,6 +951,7 @@ function Poll({ choices.push(value); } }); + if (!choices.length) return; setUIState('loading'); await votePoll(choices); setUIState('default'); From be83ca735820776e51a2833b6e206b98bd4f2cdb Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Sat, 25 Feb 2023 01:49:40 +0800 Subject: [PATCH 02/66] Neat trick to expand click area --- src/app.css | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/app.css b/src/app.css index 7a53f6c9..e36174c7 100644 --- a/src/app.css +++ b/src/app.css @@ -749,6 +749,13 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) { .updates-button .icon { vertical-align: top; } +@media (pointer: coarse) { + .updates-button:after { + content: ''; + position: absolute; + inset: -16px; + } +} /* BOX */ From 9e867c7af5a2650970bea87859980779cea730c4 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Sat, 25 Feb 2023 10:03:34 +0800 Subject: [PATCH 03/66] Fix title not working when not logged in Welcome page's useTitle overridden other page's useTitle --- src/pages/welcome.jsx | 2 +- src/utils/useTitle.js | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/pages/welcome.jsx b/src/pages/welcome.jsx index 78c74990..99ceed6f 100644 --- a/src/pages/welcome.jsx +++ b/src/pages/welcome.jsx @@ -6,7 +6,7 @@ import states from '../utils/states'; import useTitle from '../utils/useTitle'; function Welcome() { - useTitle(); + useTitle(null, ['/', '/welcome']); return (

diff --git a/src/utils/useTitle.js b/src/utils/useTitle.js index e4a53825..6257b004 100644 --- a/src/utils/useTitle.js +++ b/src/utils/useTitle.js @@ -9,10 +9,11 @@ const { VITE_CLIENT_NAME: CLIENT_NAME } = import.meta.env; export default function useTitle(title, path) { const snapStates = useSnapshot(states); const { currentLocation } = snapStates; - let paths = []; + const hasPaths = Array.isArray(path); + let paths = hasPaths ? path : []; // Workaround for matchPath not working for optional path segments // https://github.com/remix-run/react-router/discussions/9862 - if (/:?\w+\?/.test(path)) { + if (!hasPaths && /:?\w+\?/.test(path)) { paths.push(path.replace(/(:\w+)\?/g, '$1')); paths.push(path.replace(/\/?:\w+\?/g, '')); } @@ -24,7 +25,7 @@ export default function useTitle(title, path) { } console.debug({ paths, matched, currentLocation }); useEffect(() => { - if (path && !matched) return; + if (!matched) return; document.title = title ? `${title} / ${CLIENT_NAME}` : CLIENT_NAME; - }, [title, snapStates.currentLocation]); + }, [title, matched]); } From 49ef7e9ee46e5954f777a52e1fb1fac2e97e0a92 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Sat, 25 Feb 2023 10:03:52 +0800 Subject: [PATCH 04/66] Fix more icon (3 dots) too subtle --- src/components/icon.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/icon.jsx b/src/components/icon.jsx index 82221ee1..52bb42d8 100644 --- a/src/components/icon.jsx +++ b/src/components/icon.jsx @@ -37,7 +37,7 @@ const ICONS = { attachment: 'mingcute:attachment-line', upload: 'mingcute:upload-3-line', gear: 'mingcute:settings-3-line', - more: 'mingcute:more-1-line', + more: 'mingcute:more-3-line', external: 'mingcute:external-link-line', popout: 'mingcute:external-link-line', popin: ['mingcute:external-link-line', '180deg'], From 1f2dbb8e06a9482e49602be6f6f6ab2f790a1eec Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Sat, 25 Feb 2023 10:04:30 +0800 Subject: [PATCH 05/66] Experimental multi-hashtag timeline --- src/app.css | 21 ++- src/components/shortcuts-settings.jsx | 8 +- src/pages/hashtag.jsx | 236 +++++++++++++++++++++++++- 3 files changed, 253 insertions(+), 12 deletions(-) diff --git a/src/app.css b/src/app.css index e36174c7..d6664e0b 100644 --- a/src/app.css +++ b/src/app.css @@ -109,9 +109,8 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) { border-bottom: var(--hairline-width) solid var(--divider-color); min-height: 3em; display: grid; - grid-template-columns: 1fr max-content 1fr; + grid-template-columns: 1fr minmax(0, max-content) 1fr; align-items: center; - text-overflow: ellipsis; white-space: nowrap; } .deck > header .header-grid > .header-side:last-of-type { @@ -126,6 +125,9 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) { padding: 0; font-size: 1.2em; text-align: center; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .deck > header .header-grid.header-grid-2 { grid-template-columns: 1fr max-content; @@ -1080,7 +1082,7 @@ body:has(.status-deck) .media-post-link { .szh-menu__item:not(.szh-menu__item--disabled, .szh-menu__item--hover) { color: var(--text-color); } -.szh-menu .szh-menu__item--hover { +.szh-menu .szh-menu__item--hover:not(.menu-field) { color: var(--button-text-color); background-color: var(--button-bg-color); } @@ -1094,6 +1096,19 @@ body:has(.status-deck) .media-post-link { opacity: 0.5; font-weight: normal; } +.szh-menu .szh-menu__item form { + display: flex; + flex: 1; + gap: 8px; + align-items: center; +} +.szh-menu .szh-menu__item form > input[type='text'] { + flex-grow: 1; +} +.szh-menu .szh-menu__item--hover .danger-icon { + color: var(--red-color); + opacity: 1; +} /* GLASS MENU */ diff --git a/src/components/shortcuts-settings.jsx b/src/components/shortcuts-settings.jsx index d933ce27..d54e0a4c 100644 --- a/src/components/shortcuts-settings.jsx +++ b/src/components/shortcuts-settings.jsx @@ -75,7 +75,8 @@ const TYPE_PARAMS = { text: '#', name: 'hashtag', type: 'text', - placeholder: 'e.g PixelArt', + placeholder: 'e.g. PixelArt (Max 5, space-separated)', + pattern: '[^#]+', }, ], }; @@ -314,7 +315,7 @@ function ShortcutForm({ type, lists, followedHashtags, onSubmit, disabled }) { const data = new FormData(e.target); const result = {}; data.forEach((value, key) => { - result[key] = value; + result[key] = value?.trim(); }); if (!result.type) return; onSubmit(result); @@ -348,7 +349,7 @@ function ShortcutForm({ type, lists, followedHashtags, onSubmit, disabled }) {

{TYPE_PARAMS[currentType]?.map?.( - ({ text, name, type, placeholder }) => { + ({ text, name, type, placeholder, pattern }) => { if (currentType === 'list') { return (

@@ -382,6 +383,7 @@ function ShortcutForm({ type, lists, followedHashtags, onSubmit, disabled }) { autocorrect="off" autocapitalize="off" spellcheck={false} + pattern={pattern} /> {currentType === 'hashtag' && followedHashtags.length > 0 && ( diff --git a/src/pages/hashtag.jsx b/src/pages/hashtag.jsx index c51ca326..5b58eca6 100644 --- a/src/pages/hashtag.jsx +++ b/src/pages/hashtag.jsx @@ -1,17 +1,39 @@ -import { useRef } from 'preact/hooks'; -import { useParams } from 'react-router-dom'; +import { + FocusableItem, + Menu, + MenuDivider, + MenuGroup, + MenuItem, +} from '@szhsin/react-menu'; +import { useEffect, useRef, useState } from 'preact/hooks'; +import { useNavigate, useParams } from 'react-router-dom'; +import Toastify from 'toastify-js'; +import Icon from '../components/icon'; import Timeline from '../components/timeline'; import { api } from '../utils/api'; +import states from '../utils/states'; import useTitle from '../utils/useTitle'; const LIMIT = 20; +// Limit is 4 per "mode" +// https://github.com/mastodon/mastodon/issues/15194 +// Hard-coded https://github.com/mastodon/mastodon/blob/19614ba2477f3d12468f5ec251ce1cc5f8c6210c/app/models/tag_feed.rb#L4 +const TAGS_LIMIT_PER_MODE = 4; +const TOTAL_TAGS_LIMIT = TAGS_LIMIT_PER_MODE + 1; + function Hashtags(props) { + const navigate = useNavigate(); let { hashtag, ...params } = useParams(); if (props.hashtag) hashtag = props.hashtag; - const { masto, instance } = api({ instance: params.instance }); - const title = instance ? `#${hashtag} on ${instance}` : `#${hashtag}`; + let hashtags = hashtag.trim().split(/[\s+]+/); + hashtags.sort(); + hashtag = hashtags[0]; + + const { masto, instance, authenticated } = api({ instance: params.instance }); + const hashtagTitle = hashtags.map((t) => `#${t}`).join(' '); + const title = instance ? `${hashtagTitle} on ${instance}` : hashtagTitle; useTitle(title, `/:instance?/t/:hashtag`); const latestItem = useRef(); @@ -20,6 +42,7 @@ function Hashtags(props) { if (firstLoad || !hashtagsIterator.current) { hashtagsIterator.current = masto.v1.timelines.listHashtag(hashtag, { limit: LIMIT, + any: hashtags.slice(1), }); } const results = await hashtagsIterator.current.next(); @@ -37,6 +60,7 @@ function Hashtags(props) { const results = await masto.v1.timelines .listHashtag(hashtag, { limit: 1, + any: hashtags.slice(1), since_id: latestItem.current, }) .next(); @@ -50,14 +74,31 @@ function Hashtags(props) { } } + const [followUIState, setFollowUIState] = useState('default'); + const [info, setInfo] = useState(); + // Get hashtag info + useEffect(() => { + (async () => { + try { + const info = await masto.v1.tags.fetch(hashtag); + console.log(info); + setInfo(info); + } catch (e) { + console.error(e); + } + })(); + }, [hashtag]); + + const reachLimit = hashtags.length >= TOTAL_TAGS_LIMIT; + return (

{instance}

) @@ -68,6 +109,189 @@ function Hashtags(props) { errorText="Unable to load posts with this tag" fetchItems={fetchHashtags} checkForUpdates={checkForUpdates} + headerEnd={ + + + + } + > + {!!info && hashtags.length === 1 && ( + <> + { + setFollowUIState('loading'); + if (info.following) { + const yes = confirm(`Unfollow #${hashtag}?`); + if (!yes) { + setFollowUIState('default'); + return; + } + masto.v1.tags + .unfollow(hashtag) + .then(() => { + setInfo({ ...info, following: false }); + const toast = Toastify({ + className: 'shiny-pill', + text: `Unfollowed #${hashtag}`, + duration: 3000, + gravity: 'bottom', + position: 'center', + }); + toast.showToast(); + }) + .catch((e) => { + alert(e); + console.error(e); + }) + .finally(() => { + setFollowUIState('default'); + }); + } else { + masto.v1.tags + .follow(hashtag) + .then(() => { + setInfo({ ...info, following: true }); + const toast = Toastify({ + className: 'shiny-pill', + text: `Followed #${hashtag}`, + duration: 3000, + gravity: 'bottom', + position: 'center', + }); + toast.showToast(); + }) + .catch((e) => { + alert(e); + console.error(e); + }) + .finally(() => { + setFollowUIState('default'); + }); + } + }} + > + {info.following ? ( + <> + Following… + + ) : ( + <> + Follow + + )} + + + + )} + + {({ ref }) => ( +
{ + e.preventDefault(); + const newHashtag = e.target[0].value; + // Use includes but need to be case insensitive + if ( + newHashtag && + !hashtags.some( + (t) => t.toLowerCase() === newHashtag.toLowerCase(), + ) + ) { + hashtags.push(newHashtag); + hashtags.sort(); + navigate( + instance + ? `/${instance}/t/${hashtags.join('+')}` + : `/t/${hashtags.join('+')}`, + ); + } + }} + > + + + + )} +
+ + {hashtags.map((t, i) => ( + { + hashtags.splice(i, 1); + hashtags.sort(); + navigate( + instance + ? `/${instance}/t/${hashtags.join('+')}` + : `/t/${hashtags.join('+')}`, + ); + }} + > + {' '} + + {t} + + ))} + + + { + const shortcut = { + type: 'hashtag', + hashtag: hashtags.join(' '), + }; + // Check if already exists + const exists = states.shortcuts.some( + (s) => + s.type === shortcut.type && + s.hashtag + .split(/[\s+]+/) + .sort() + .join(' ') === + shortcut.hashtag + .split(/[\s+]+/) + .sort() + .join(' '), + ); + if (exists) { + alert('This shortcut already exists'); + } else { + states.shortcuts.push(shortcut); + const toast = Toastify({ + className: 'shiny-pill', + text: `Hashtag shortcut added`, + duration: 3000, + gravity: 'bottom', + position: 'center', + }); + toast.showToast(); + } + }} + > + Add to Shorcuts + +
+ } /> ); } From 8fdc430dd59f7b2ddf1d5193f7fd672541916e2f Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Sat, 25 Feb 2023 10:31:50 +0800 Subject: [PATCH 06/66] Reduce storage usage --- public/sw.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/sw.js b/public/sw.js index ddf9f42d..14514039 100644 --- a/public/sw.js +++ b/public/sw.js @@ -21,8 +21,8 @@ const imageRoute = new Route( cacheName: 'remote-images', plugins: [ new ExpirationPlugin({ - maxEntries: 100, - maxAgeSeconds: 7 * 24 * 60 * 60, // 7 days + maxEntries: 50, + maxAgeSeconds: 3 * 24 * 60 * 60, // 3 days purgeOnQuotaError: true, }), new CacheableResponsePlugin({ From aca3cb677b861728a3ab1c3a82289dd018a3a0ca Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Sat, 25 Feb 2023 10:50:02 +0800 Subject: [PATCH 07/66] Sometimes need exact distance instead ratio --- src/pages/status.jsx | 2 +- src/utils/useScroll.js | 26 ++++++++++++++++---------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/pages/status.jsx b/src/pages/status.jsx index 8d3a488e..80439378 100644 --- a/src/pages/status.jsx +++ b/src/pages/status.jsx @@ -457,7 +457,7 @@ function StatusPage() { const { nearReachStart } = useScroll({ scrollableElement: scrollableRef.current, - distanceFromStart: 0.2, + distanceFromStartPx: 16, }); return ( diff --git a/src/utils/useScroll.js b/src/utils/useScroll.js index af45ece9..dbbe7c76 100644 --- a/src/utils/useScroll.js +++ b/src/utils/useScroll.js @@ -7,6 +7,8 @@ export default function useScroll({ scrollThresholdStart = 10, scrollThresholdEnd = 10, direction = 'vertical', + distanceFromStartPx: _distanceFromStartPx, + distanceFromEndPx: _distanceFromEndPx, } = {}) { const [scrollDirection, setScrollDirection] = useState(null); const [reachStart, setReachStart] = useState(false); @@ -38,16 +40,20 @@ export default function useScroll({ const scrollDimension = isVertical ? scrollHeight : scrollWidth; const clientDimension = isVertical ? clientHeight : clientWidth; const scrollDistance = Math.abs(scrollStart - previousScrollStart); - const distanceFromStartPx = Math.min( - clientDimension * distanceFromStart, - scrollDimension, - scrollStart, - ); - const distanceFromEndPx = Math.min( - clientDimension * distanceFromEnd, - scrollDimension, - scrollDimension - scrollStart - clientDimension, - ); + const distanceFromStartPx = + _distanceFromStartPx || + Math.min( + clientDimension * distanceFromStart, + scrollDimension, + scrollStart, + ); + const distanceFromEndPx = + _distanceFromEndPx || + Math.min( + clientDimension * distanceFromEnd, + scrollDimension, + scrollDimension - scrollStart - clientDimension, + ); if ( scrollDistance >= From 1c8bba456d378751c07e6b76dc9f5507369ca14c Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Sat, 25 Feb 2023 11:25:50 +0800 Subject: [PATCH 08/66] Add menu for Public timeline Add menu item to switch between Local and Federated --- src/app.css | 2 ++ src/components/icon.jsx | 1 + src/pages/public.jsx | 62 +++++++++++++++++++++++++++++------------ 3 files changed, 47 insertions(+), 18 deletions(-) diff --git a/src/app.css b/src/app.css index d6664e0b..5dad9006 100644 --- a/src/app.css +++ b/src/app.css @@ -1053,11 +1053,13 @@ body:has(.status-deck) .media-post-link { background-color: transparent; } .szh-menu .szh-menu__item { + display: block; padding: 8px 16px !important; transition: all 0.1s ease-in-out; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + text-decoration: none; } .szh-menu .szh-menu__item * { vertical-align: middle; diff --git a/src/components/icon.jsx b/src/components/icon.jsx index 52bb42d8..19a1f794 100644 --- a/src/components/icon.jsx +++ b/src/components/icon.jsx @@ -57,6 +57,7 @@ const ICONS = { user: 'mingcute:user-4-line', following: 'mingcute:walk-line', pin: 'mingcute:pin-line', + bus: 'mingcute:bus-2-line', }; const modules = import.meta.glob('/node_modules/@iconify-icons/mingcute/*.js'); diff --git a/src/pages/public.jsx b/src/pages/public.jsx index 62bc15a1..ecd8f08d 100644 --- a/src/pages/public.jsx +++ b/src/pages/public.jsx @@ -1,4 +1,4 @@ -// EXPERIMENTAL: This is a work in progress and may not work as expected. +import { Menu, MenuDivider, MenuItem } from '@szhsin/react-menu'; import { useRef } from 'preact/hooks'; import { useNavigate, useParams } from 'react-router-dom'; @@ -75,25 +75,51 @@ function Public({ local, ...props }) { checkForUpdates={checkForUpdates} headerStart={<>} headerEnd={ - + } > - - + + {isLocal ? ( + <> + Switch to Federated + + ) : ( + <> + Switch to Local + + )} + + + { + let newInstance = prompt( + 'Enter a new instance e.g. "mastodon.social"', + ); + if (!/\./.test(newInstance)) { + if (newInstance) alert('Invalid instance'); + return; + } + if (newInstance) { + newInstance = newInstance.toLowerCase().trim(); + navigate(isLocal ? `/${newInstance}/p/l` : `/${newInstance}/p`); + } + }} + > + Go to another instance… + + } /> ); From 15a75e1ed0a5cf2d68211d205de5d712d797ee37 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Sat, 25 Feb 2023 11:36:07 +0800 Subject: [PATCH 09/66] Ellipsis-ify menu text --- src/app.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app.css b/src/app.css index 5dad9006..624c4711 100644 --- a/src/app.css +++ b/src/app.css @@ -1093,6 +1093,8 @@ body:has(.status-deck) .media-post-link { } .szh-menu .szh-menu__item .menu-grow { flex-grow: 1; + text-overflow: ellipsis; + overflow: hidden; } .szh-menu .szh-menu__item .menu-shortcut { opacity: 0.5; From b4a8e60144b9cdd7d1b8f34a60c982110e728401 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Sat, 25 Feb 2023 12:40:59 +0800 Subject: [PATCH 10/66] Fix wrong authenticated --- src/pages/hashtag.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/hashtag.jsx b/src/pages/hashtag.jsx index 5b58eca6..67de91aa 100644 --- a/src/pages/hashtag.jsx +++ b/src/pages/hashtag.jsx @@ -31,7 +31,8 @@ function Hashtags(props) { hashtags.sort(); hashtag = hashtags[0]; - const { masto, instance, authenticated } = api({ instance: params.instance }); + const { masto, instance } = api({ instance: params.instance }); + const { authenticated } = api(); const hashtagTitle = hashtags.map((t) => `#${t}`).join(' '); const title = instance ? `${hashtagTitle} on ${instance}` : hashtagTitle; useTitle(title, `/:instance?/t/:hashtag`); From 2afa84c2fd4c78ad2768bbf7f8356544c393e617 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Sat, 25 Feb 2023 14:19:57 +0800 Subject: [PATCH 11/66] Fix input width bug on Safari --- src/app.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app.css b/src/app.css index 624c4711..ca7f4644 100644 --- a/src/app.css +++ b/src/app.css @@ -1108,6 +1108,7 @@ body:has(.status-deck) .media-post-link { } .szh-menu .szh-menu__item form > input[type='text'] { flex-grow: 1; + min-width: 0; } .szh-menu .szh-menu__item--hover .danger-icon { color: var(--red-color); From 7b66b832d56164af72958db7ca4aa0dc24c77944 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Sat, 25 Feb 2023 14:20:26 +0800 Subject: [PATCH 12/66] Add more "instance" + saveStatus for new notifications --- src/app.jsx | 7 ++++++- src/components/compose.jsx | 2 +- src/pages/notifications.jsx | 3 +-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/app.jsx b/src/app.jsx index 113b74ae..95dc9eef 100644 --- a/src/app.jsx +++ b/src/app.jsx @@ -175,7 +175,7 @@ function App() { const notificationStream = useRef(); useEffect(() => { if (isLoggedIn && visible) { - const { masto } = api(); + const { masto, instance } = api(); (async () => { // 1. Get the latest notification if (states.notificationsLast) { @@ -200,6 +200,11 @@ function App() { notificationStream.current.on('notification', (notification) => { console.log('🔔🔔 Notification', notification); + if (notification.status) { + saveStatus(notification.status, instance, { + skipThreading: true, + }); + } states.notificationsShowNew = true; }); diff --git a/src/components/compose.jsx b/src/components/compose.jsx index 848e8c8f..9fdd23bd 100644 --- a/src/components/compose.jsx +++ b/src/components/compose.jsx @@ -772,7 +772,7 @@ function Compose({ editStatus.id, params, ); - saveStatus(newStatus, { + saveStatus(newStatus, instance, { skipThreading: true, }); } else { diff --git a/src/pages/notifications.jsx b/src/pages/notifications.jsx index 059fa421..0d0e0473 100644 --- a/src/pages/notifications.jsx +++ b/src/pages/notifications.jsx @@ -77,9 +77,8 @@ function Notifications() { if (notifications?.length) { notifications.forEach((notification) => { - saveStatus(notification.status, { + saveStatus(notification.status, instance, { skipThreading: true, - override: false, }); }); From 2b25fc67cf7426a7d86e2e0677350e5515d9a028 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Sat, 25 Feb 2023 14:34:08 +0800 Subject: [PATCH 13/66] Prevent action buttons from wrapping in Safari --- src/components/shortcuts-settings.css | 3 +++ src/components/shortcuts-settings.jsx | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/shortcuts-settings.css b/src/components/shortcuts-settings.css index bec8246a..756fa406 100644 --- a/src/components/shortcuts-settings.css +++ b/src/components/shortcuts-settings.css @@ -26,6 +26,9 @@ #shortcuts-settings-container .shortcuts-list li .shortcut-text { flex-grow: 1; } +#shortcuts-settings-container .shortcuts-list li .shortcut-actions { + flex-shrink: 0; +} #shortcuts-settings-container summary { cursor: pointer; diff --git a/src/components/shortcuts-settings.jsx b/src/components/shortcuts-settings.jsx index d54e0a4c..2c904355 100644 --- a/src/components/shortcuts-settings.jsx +++ b/src/components/shortcuts-settings.jsx @@ -236,7 +236,7 @@ function ShortcutsSettings() { {title} - + - - } - > - {isSelf && ( - { - states.showCompose = { - editStatus: status, - }; - }} + + - )} + + + + } + > + {StatusMenuItems} + )} diff --git a/src/index.css b/src/index.css index 479d565e..4910509a 100644 --- a/src/index.css +++ b/src/index.css @@ -297,6 +297,9 @@ code { .insignificant { color: var(--text-insignificant-color); } +.more-insignificant { + opacity: 0.5; +} .hide-until-focus-visible { display: none; diff --git a/src/pages/hashtag.jsx b/src/pages/hashtag.jsx index 67de91aa..6219c0c3 100644 --- a/src/pages/hashtag.jsx +++ b/src/pages/hashtag.jsx @@ -7,11 +7,11 @@ import { } from '@szhsin/react-menu'; import { useEffect, useRef, useState } from 'preact/hooks'; import { useNavigate, useParams } from 'react-router-dom'; -import Toastify from 'toastify-js'; import Icon from '../components/icon'; import Timeline from '../components/timeline'; import { api } from '../utils/api'; +import showToast from '../utils/show-toast'; import states from '../utils/states'; import useTitle from '../utils/useTitle'; @@ -142,14 +142,7 @@ function Hashtags(props) { .unfollow(hashtag) .then(() => { setInfo({ ...info, following: false }); - const toast = Toastify({ - className: 'shiny-pill', - text: `Unfollowed #${hashtag}`, - duration: 3000, - gravity: 'bottom', - position: 'center', - }); - toast.showToast(); + showToast(`Unfollowed #${hashtag}`); }) .catch((e) => { alert(e); @@ -163,14 +156,7 @@ function Hashtags(props) { .follow(hashtag) .then(() => { setInfo({ ...info, following: true }); - const toast = Toastify({ - className: 'shiny-pill', - text: `Followed #${hashtag}`, - duration: 3000, - gravity: 'bottom', - position: 'center', - }); - toast.showToast(); + showToast(`Followed #${hashtag}`); }) .catch((e) => { alert(e); @@ -247,9 +233,11 @@ function Hashtags(props) { ); }} > - {' '} - - {t} + + + # + {t} + ))} @@ -278,14 +266,7 @@ function Hashtags(props) { alert('This shortcut already exists'); } else { states.shortcuts.push(shortcut); - const toast = Toastify({ - className: 'shiny-pill', - text: `Hashtag shortcut added`, - duration: 3000, - gravity: 'bottom', - position: 'center', - }); - toast.showToast(); + showToast(`Hashtag shortcut added`); } }} > diff --git a/src/utils/show-toast.js b/src/utils/show-toast.js new file mode 100644 index 00000000..8cb02521 --- /dev/null +++ b/src/utils/show-toast.js @@ -0,0 +1,26 @@ +import Toastify from 'toastify-js'; + +function showToast(props) { + if (typeof props === 'string') { + props = { text: props }; + } + const { onClick = () => {}, delay, ...rest } = props; + const toast = Toastify({ + className: 'shiny-pill', + gravity: 'bottom', + position: 'center', + ...rest, + onClick: () => { + onClick(toast); // Pass in the object itself! + }, + }); + if (delay) { + setTimeout(() => { + toast.showToast(); + }, delay); + } else { + toast.showToast(); + } +} + +export default showToast; From 017b138d4ba3964f6cccd67ed0ac6b7c773114ac Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Mon, 27 Feb 2023 11:20:50 +0800 Subject: [PATCH 16/66] Add experimental highlighting to composer textarea --- package-lock.json | 67 ++++++++++++++++++++++++++++++++ package.json | 1 + src/components/compose.css | 6 +++ src/components/compose.jsx | 79 ++++++++++++++++++++++++++------------ 4 files changed, 128 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index b3adf415..51efccfe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "react-hotkeys-hook": "~4.3.7", "react-intersection-observer": "~9.4.2", "react-router-dom": "6.6.2", + "rich-textarea": "~0.19.5", "string-length": "~5.0.1", "swiped-events": "~1.1.7", "toastify-js": "~1.12.0", @@ -3200,6 +3201,20 @@ "@babel/core": "^7.12.10" } }, + "node_modules/babel-runtime": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-4.7.4.tgz", + "integrity": "sha512-0gnK56hiHkbUCwqtaiK15MAsnNxI8T7aOBYUyoAYyNxWs86ExL0NNTDhn0eDO/AIhJf0oXMgV5+1wfSLQ/FMyw==", + "dependencies": { + "core-js": "^0.6.1" + } + }, + "node_modules/babel-runtime/node_modules/core-js": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-0.6.1.tgz", + "integrity": "sha512-ANdRS9QdyvvVCqMD7gvDhgI5T+/t5FELQB1ZLN94oCDXTJLwt4Q1o6Nbc1wnVrhl6QPyJ5mv0k8hMCdAFLNbLg==", + "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js." + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -5733,6 +5748,14 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/range-at-index": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/range-at-index/-/range-at-index-1.0.4.tgz", + "integrity": "sha512-Aob2FK5jL0cCKvIzA0tuRrdsSXTvxLY5p9dr7GrLY31NwKtUk5EhgHwcKi0kbUacJukGVRglLi6MEqBHB4NHMA==", + "dependencies": { + "babel-runtime": "4.7.4" + } + }, "node_modules/react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -5957,6 +5980,18 @@ "node": ">=0.10.0" } }, + "node_modules/rich-textarea": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/rich-textarea/-/rich-textarea-0.19.5.tgz", + "integrity": "sha512-jGzE84BUs0VKLTEIdJaQcToQjRxYO1aNRnPNtnGupkwKWl59dCpp5EWWi5qxU1JMjPtKOBFSSynYG0srr71TJw==", + "dependencies": { + "range-at-index": "^1.0.4", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "react": ">=16.14.0" + } + }, "node_modules/rollup": { "version": "3.12.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.12.1.tgz", @@ -9146,6 +9181,21 @@ "dev": true, "requires": {} }, + "babel-runtime": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-4.7.4.tgz", + "integrity": "sha512-0gnK56hiHkbUCwqtaiK15MAsnNxI8T7aOBYUyoAYyNxWs86ExL0NNTDhn0eDO/AIhJf0oXMgV5+1wfSLQ/FMyw==", + "requires": { + "core-js": "^0.6.1" + }, + "dependencies": { + "core-js": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-0.6.1.tgz", + "integrity": "sha512-ANdRS9QdyvvVCqMD7gvDhgI5T+/t5FELQB1ZLN94oCDXTJLwt4Q1o6Nbc1wnVrhl6QPyJ5mv0k8hMCdAFLNbLg==" + } + } + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -10892,6 +10942,14 @@ "safe-buffer": "^5.1.0" } }, + "range-at-index": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/range-at-index/-/range-at-index-1.0.4.tgz", + "integrity": "sha512-Aob2FK5jL0cCKvIzA0tuRrdsSXTvxLY5p9dr7GrLY31NwKtUk5EhgHwcKi0kbUacJukGVRglLi6MEqBHB4NHMA==", + "requires": { + "babel-runtime": "4.7.4" + } + }, "react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -11057,6 +11115,15 @@ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true }, + "rich-textarea": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/rich-textarea/-/rich-textarea-0.19.5.tgz", + "integrity": "sha512-jGzE84BUs0VKLTEIdJaQcToQjRxYO1aNRnPNtnGupkwKWl59dCpp5EWWi5qxU1JMjPtKOBFSSynYG0srr71TJw==", + "requires": { + "range-at-index": "^1.0.4", + "use-sync-external-store": "^1.2.0" + } + }, "rollup": { "version": "3.12.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.12.1.tgz", diff --git a/package.json b/package.json index e457306e..fe84c7bc 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "react-hotkeys-hook": "~4.3.7", "react-intersection-observer": "~9.4.2", "react-router-dom": "6.6.2", + "rich-textarea": "~0.19.5", "string-length": "~5.0.1", "swiped-events": "~1.1.7", "toastify-js": "~1.12.0", diff --git a/src/components/compose.css b/src/components/compose.css index 8c0d0956..fff6aa54 100644 --- a/src/components/compose.css +++ b/src/components/compose.css @@ -466,6 +466,12 @@ vertical-align: middle; } +.compose-highlight, +.compose-link { + color: var(--link-color); + background-color: transparent; +} + @media (min-width: 50em) { #media-sheet main { flex-direction: row; diff --git a/src/components/compose.jsx b/src/components/compose.jsx index 9fdd23bd..191fd364 100644 --- a/src/components/compose.jsx +++ b/src/components/compose.jsx @@ -5,6 +5,7 @@ import equal from 'fast-deep-equal'; import { forwardRef } from 'preact/compat'; import { useEffect, useRef, useState } from 'preact/hooks'; import { useHotkeys } from 'react-hotkeys-hook'; +import { createRegexRenderer, RichTextarea } from 'rich-textarea'; import stringLength from 'string-length'; import { uid } from 'uid/single'; import { useDebouncedCallback } from 'use-debounce'; @@ -1038,6 +1039,28 @@ function Compose({ ); } +const HIGHLIGHT_REG = /(#|@|:)[\p{L}\p{M}\p{N}\p{Pc}_@\.]+:?/gu; +const highlightRenderer = createRegexRenderer([ + [ + HIGHLIGHT_REG, + ({ value }) => { + const first = value[0]; + const last = value[value.length - 1] === ':' ? ':' : ''; + const rest = value.slice(1, last ? -1 : undefined); + return ( + <> + {first} + {rest} + {last && {last}} + + ); + }, + ], + [urlRegexObj, (props) => ], +]); + +const RICH_TEXTAREA = true; + const Textarea = forwardRef((props, ref) => { const { masto } = api(); const [text, setText] = useState(ref.current?.value || ''); @@ -1216,33 +1239,39 @@ const Textarea = forwardRef((props, ref) => { }; }, []); + const fieldProps = { + autoCapitalize: 'sentences', + autoComplete: 'on', + autoCorrect: 'on', + spellCheck: 'true', + dir: 'auto', + rows: '6', + cols: '50', + ...textareaProps, + ref, + name: 'status', + value: text, + onInput: (e) => { + const { scrollHeight, offsetHeight, clientHeight, value } = e.target; + setText(value); + const offset = offsetHeight - clientHeight; + e.target.style.height = value ? scrollHeight + offset + 'px' : null; + props.onInput?.(e); + }, + style: { + width: '100%', + height: '4em', + '--text-weight': (1 + charCount / 140).toFixed(1) || 1, + }, + }; + return ( -