New experiment: multi-column mode

This commit is contained in:
Lim Chee Aun 2023-02-18 20:48:24 +08:00
parent 45a1fc057e
commit 522d55ebb8
9 changed files with 176 additions and 53 deletions

View file

@ -1197,12 +1197,12 @@ meter.donut:is(.danger, .explode):after {
width: 100%; width: 100%;
flex-grow: 1; flex-grow: 1;
} }
:is(#home-page, #welcome) ~ .deck-container { :is(#home-page, #welcome, #columns) ~ .deck-container {
z-index: 10; z-index: 10;
position: fixed; position: fixed;
inset: 0; inset: 0;
} }
:is(#home-page, #welcome):has(~ .deck-container) { :is(#home-page, #welcome, #columns):has(~ .deck-container) {
display: block; display: block;
position: absolute; position: absolute;
user-select: none; user-select: none;
@ -1338,28 +1338,82 @@ ul.link-list li a .icon {
display: flex; display: flex;
width: 100vw; width: 100vw;
overflow-y: hidden; overflow-y: hidden;
overflow-x: auto; overflow-x: scroll;
scroll-snap-type: x mandatory; scroll-snap-type: x mandatory;
scroll-behavior: smooth; scroll-behavior: smooth;
scrollbar-width: none; /* scrollbar-width: none; */
overscroll-behavior: contain; overscroll-behavior: contain;
overscroll-behavior-x: contain;
} }
#columns::-webkit-scrollbar { /* #columns::-webkit-scrollbar {
display: none; display: none;
} } */
#columns > * { #columns > * {
overscroll-behavior: auto; overscroll-behavior: auto;
scroll-snap-align: left; scroll-snap-align: left;
scroll-snap-stop: always; scroll-snap-stop: always;
position: static !important; overscroll-behavior: auto;
opacity: 1 !important; flex-basis: min(100vw, 360px);
content-visibility: auto !important;
pointer-events: auto !important;
user-select: auto !important;
/* background-color: var(--bg-color); */
flex-basis: min(100vw, 480px);
flex-shrink: 0; flex-shrink: 0;
} }
#columns .header-grid input {
pointer-events: none;
}
#columns
.header-grid
.header-side:first-of-type
:is(button, .button)
~ :is(button, .button),
#columns .deck-container:not(:first-of-type) .header-grid .header-side > * {
display: none;
}
@media (min-width: 40em) {
#columns {
gap: 16px;
padding: 16px;
background-color: var(--bg-blur-color);
height: 100vh;
height: 100dvh;
justify-content: stretch;
align-items: stretch;
}
#columns > * {
padding: 0 16px;
border: var(--hairline-width) solid var(--outline-color);
border-radius: 16px;
box-shadow: 0 4px 16px var(--drop-shadow-color);
height: unset;
background-image: linear-gradient(
160deg,
transparent 20%,
var(--bg-color),
transparent 75%
);
}
#columns > *:focus-visible,
#columns > *:has(:focus-visible) {
box-shadow: 0 4px 16px var(--drop-shadow-color),
0 4px 16px var(--drop-shadow-color);
border-color: var(--outline-hover-color);
}
#columns .timeline:not(.flat) > li:has(.status-link.is-active),
#columns
.timeline:not(.flat)
> li:not(:has(.status-carousel)):has(+ li .status-link.is-active),
#columns
.timeline:not(.flat)
> li:not(:has(.status-carousel)):has(.status-link.is-active)
+ li {
transform: none;
}
#columns .timeline-deck > header {
margin: 0;
}
#columns li:has(.status-carousel) {
width: auto;
transform: none;
}
}
/* OTHERS */ /* OTHERS */

View file

@ -33,7 +33,7 @@ import Bookmarks from './pages/bookmarks';
import Favourites from './pages/favourites'; import Favourites from './pages/favourites';
import FollowedHashtags from './pages/followed-hashtags'; import FollowedHashtags from './pages/followed-hashtags';
import Following from './pages/following'; import Following from './pages/following';
import Hashtags from './pages/hashtags'; import Hashtag from './pages/hashtag';
import Home from './pages/home'; import Home from './pages/home';
import HomeV1 from './pages/home-v1'; import HomeV1 from './pages/home-v1';
import List from './pages/list'; import List from './pages/list';
@ -269,7 +269,7 @@ function App() {
</Route> </Route>
)} )}
{isLoggedIn && <Route path="/ft" element={<FollowedHashtags />} />} {isLoggedIn && <Route path="/ft" element={<FollowedHashtags />} />}
<Route path="/:instance?/t/:hashtag" element={<Hashtags />} /> <Route path="/:instance?/t/:hashtag" element={<Hashtag />} />
<Route path="/:instance?/a/:id" element={<AccountStatuses />} /> <Route path="/:instance?/a/:id" element={<AccountStatuses />} />
<Route path="/:instance?/p"> <Route path="/:instance?/p">
<Route index element={<Public />} /> <Route index element={<Public />} />
@ -298,7 +298,7 @@ function App() {
</Link> </Link>
</li> </li>
</nav> </nav>
<Shortcuts /> {!snapStates.settings.shortcutsColumnsMode && <Shortcuts />}
{!!snapStates.showCompose && ( {!!snapStates.showCompose && (
<Modal> <Modal>
<Compose <Compose

View file

@ -16,15 +16,15 @@ const TYPES = [
'notifications', 'notifications',
'list', 'list',
'public', 'public',
'search', // NOTE: Hide for now
// NOTE: Hide for now, can't think of a good way to handle this // 'search', // Search on Mastodon ain't great
// 'account-statuses', // 'account-statuses', // Need @acct search first
'bookmarks', 'bookmarks',
'favourites', 'favourites',
'hashtag', 'hashtag',
]; ];
const TYPE_TEXT = { const TYPE_TEXT = {
following: 'Home', following: 'Home / Following',
notifications: 'Notifications', notifications: 'Notifications',
list: 'List', list: 'List',
public: 'Public', public: 'Public',
@ -80,7 +80,7 @@ const TYPE_PARAMS = {
}; };
export const SHORTCUTS_META = { export const SHORTCUTS_META = {
following: { following: {
title: 'Home', title: 'Home / Following',
path: (_, index) => (index === 0 ? '/' : '/l/f'), path: (_, index) => (index === 0 ? '/' : '/l/f'),
icon: 'home', icon: 'home',
}, },
@ -188,7 +188,24 @@ function ShortcutsSettings() {
Specify a list of shortcuts that'll appear in the floating Shortcuts Specify a list of shortcuts that'll appear in the floating Shortcuts
button. button.
</p> </p>
{snapStates.shortcuts.length > 0 ? ( <p>
<details>
<summary class="insignificant">
Experimental Multi-column mode
</summary>
<label>
<input
type="checkbox"
checked={snapStates.settings.shortcutsColumnsMode}
onChange={(e) => {
states.settings.shortcutsColumnsMode = e.target.checked;
}}
/>{' '}
Show shortcuts in multiple columns instead of the floating button.
</label>
</details>
</p>
{shortcuts.length > 0 ? (
<ol class="shortcuts-list"> <ol class="shortcuts-list">
{shortcuts.map((shortcut, i) => { {shortcuts.map((shortcut, i) => {
const key = i + Object.values(shortcut); const key = i + Object.values(shortcut);

View file

@ -7,8 +7,9 @@ import useTitle from '../utils/useTitle';
const LIMIT = 20; const LIMIT = 20;
function Hashtags() { function Hashtags(props) {
let { hashtag, ...params } = useParams(); let { hashtag, ...params } = useParams();
if (props.hashtag) hashtag = props.hashtag;
const { masto, instance } = api({ instance: params.instance }); const { masto, instance } = api({ instance: params.instance });
const title = instance ? `#${hashtag} on ${instance}` : `#${hashtag}`; const title = instance ? `#${hashtag} on ${instance}` : `#${hashtag}`;
useTitle(title, `/:instance?/t/:hashtag`); useTitle(title, `/:instance?/t/:hashtag`);

View file

@ -1,4 +1,5 @@
import { useEffect } from 'preact/hooks'; import { useEffect, useState } from 'preact/hooks';
import { useHotkeys } from 'react-hotkeys-hook';
import { useSnapshot } from 'valtio'; import { useSnapshot } from 'valtio';
import Icon from '../components/icon'; import Icon from '../components/icon';
@ -25,27 +26,69 @@ function Home() {
})(); })();
}, []); }, []);
const { shortcuts } = snapStates;
const { shortcutsColumnsMode } = snapStates.settings || {};
const [shortcutsComponents, setShortcutsComponents] = useState([]);
useEffect(() => {
if (shortcutsColumnsMode) {
const componentsPromises = shortcuts.map((shortcut) => {
const { type, ...params } = shortcut;
// Uppercase type
return import(`./${type}`).then((module) => {
const { default: Component } = module;
return <Component {...params} />;
});
});
Promise.all(componentsPromises)
.then((components) => {
setShortcutsComponents(components);
})
.catch((err) => {
console.error(err);
});
}
}, [shortcutsColumnsMode, shortcuts]);
useHotkeys(
['1', '2', '3', '4', '5', '6', '7', '8', '9'],
(e, handler) => {
try {
const index = parseInt(handler.keys[0], 10) - 1;
document.querySelectorAll('#columns > *')[index].focus();
} catch (e) {
console.error(e);
}
},
{
enabled: shortcutsColumnsMode,
},
);
return ( return (
<> <>
<Following {shortcutsColumnsMode ? (
title="Home" <div id="columns">{shortcutsComponents}</div>
path="/" ) : (
id="home" <Following
headerStart={false} title="Home"
headerEnd={ path="/"
<Link id="home"
to="/notifications" headerStart={false}
class={`button plain ${ headerEnd={
snapStates.notificationsShowNew ? 'has-badge' : '' <Link
}`} to="/notifications"
onClick={(e) => { class={`button plain ${
e.stopPropagation(); snapStates.notificationsShowNew ? 'has-badge' : ''
}} }`}
> onClick={(e) => {
<Icon icon="notification" size="l" alt="Notifications" /> e.stopPropagation();
</Link> }}
} >
/> <Icon icon="notification" size="l" alt="Notifications" />
</Link>
}
/>
)}
<button <button
// hidden={scrollDirection === 'end' && !nearReachStart} // hidden={scrollDirection === 'end' && !nearReachStart}
type="button" type="button"

View file

@ -9,9 +9,9 @@ import useTitle from '../utils/useTitle';
const LIMIT = 20; const LIMIT = 20;
function List() { function List(props) {
const { masto } = api(); const { masto } = api();
const { id } = useParams(); const id = props?.id || useParams()?.id;
const latestItem = useRef(); const latestItem = useRef();
const listIterator = useRef(); const listIterator = useRef();

View file

@ -9,10 +9,12 @@ import useTitle from '../utils/useTitle';
const LIMIT = 20; const LIMIT = 20;
function Public({ local }) { function Public({ local, ...props }) {
const isLocal = !!local; const isLocal = !!local;
const params = useParams(); const params = useParams();
const { masto, instance } = api({ instance: params.instance }); const { masto, instance } = api({
instance: props?.instance || params.instance,
});
const title = `${isLocal ? 'Local' : 'Federated'} timeline (${instance})`; const title = `${isLocal ? 'Local' : 'Federated'} timeline (${instance})`;
useTitle(title, isLocal ? `/:instance?/p/l` : `/:instance?/p`); useTitle(title, isLocal ? `/:instance?/p/l` : `/:instance?/p`);
const navigate = useNavigate(); const navigate = useNavigate();

View file

@ -12,7 +12,7 @@ import Status from '../components/status';
import { api } from '../utils/api'; import { api } from '../utils/api';
import useTitle from '../utils/useTitle'; import useTitle from '../utils/useTitle';
function Search() { function Search(props) {
const params = useParams(); const params = useParams();
const { masto, instance, authenticated } = api({ const { masto, instance, authenticated } = api({
instance: params.instance, instance: params.instance,
@ -20,7 +20,7 @@ function Search() {
const [uiState, setUiState] = useState('default'); const [uiState, setUiState] = useState('default');
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const searchFieldRef = useRef(); const searchFieldRef = useRef();
const q = searchParams.get('q'); const q = props?.query || searchParams.get('q');
useTitle(q ? `Search: ${q}` : 'Search', `/search`); useTitle(q ? `Search: ${q}` : 'Search', `/search`);
const [statusResults, setStatusResults] = useState([]); const [statusResults, setStatusResults] = useState([]);

View file

@ -35,6 +35,8 @@ const states = proxy({
shortcuts: store.account.get('shortcuts') ?? [], shortcuts: store.account.get('shortcuts') ?? [],
// Settings // Settings
settings: { settings: {
shortcutsColumnsMode:
store.account.get('settings-shortcutsColumnsMode') ?? false,
boostsCarousel: store.account.get('settings-boostsCarousel') ?? true, boostsCarousel: store.account.get('settings-boostsCarousel') ?? true,
}, },
}); });
@ -45,11 +47,15 @@ subscribeKey(states, 'notificationsLast', (v) => {
console.log('CHANGE', v); console.log('CHANGE', v);
store.account.set('notificationsLast', states.notificationsLast); store.account.set('notificationsLast', states.notificationsLast);
}); });
subscribeKey(states, 'settings-boostsCarousel', (v) => {
store.account.set('settings-boostsCarousel', !!v);
});
subscribe(states, (v) => { subscribe(states, (v) => {
const [action, path, value] = v[0]; console.debug('STATES change', v);
const [action, path, value, prevValue] = v[0];
if (path.join('.') === 'settings.boostsCarousel') {
store.account.set('settings-boostsCarousel', !!value);
}
if (path.join('.') === 'settings.shortcutsColumnsMode') {
store.account.set('settings-shortcutsColumnsMode', !!value);
}
if (path?.[0] === 'shortcuts') { if (path?.[0] === 'shortcuts') {
store.account.set('shortcuts', states.shortcuts); store.account.set('shortcuts', states.shortcuts);
} }