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%;
flex-grow: 1;
}
:is(#home-page, #welcome) ~ .deck-container {
:is(#home-page, #welcome, #columns) ~ .deck-container {
z-index: 10;
position: fixed;
inset: 0;
}
:is(#home-page, #welcome):has(~ .deck-container) {
:is(#home-page, #welcome, #columns):has(~ .deck-container) {
display: block;
position: absolute;
user-select: none;
@ -1338,28 +1338,82 @@ ul.link-list li a .icon {
display: flex;
width: 100vw;
overflow-y: hidden;
overflow-x: auto;
overflow-x: scroll;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
scrollbar-width: none;
/* scrollbar-width: none; */
overscroll-behavior: contain;
overscroll-behavior-x: contain;
}
#columns::-webkit-scrollbar {
/* #columns::-webkit-scrollbar {
display: none;
}
} */
#columns > * {
overscroll-behavior: auto;
scroll-snap-align: left;
scroll-snap-stop: always;
position: static !important;
opacity: 1 !important;
content-visibility: auto !important;
pointer-events: auto !important;
user-select: auto !important;
/* background-color: var(--bg-color); */
flex-basis: min(100vw, 480px);
overscroll-behavior: auto;
flex-basis: min(100vw, 360px);
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 */

View file

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

View file

@ -16,15 +16,15 @@ const TYPES = [
'notifications',
'list',
'public',
'search',
// NOTE: Hide for now, can't think of a good way to handle this
// 'account-statuses',
// NOTE: Hide for now
// 'search', // Search on Mastodon ain't great
// 'account-statuses', // Need @acct search first
'bookmarks',
'favourites',
'hashtag',
];
const TYPE_TEXT = {
following: 'Home',
following: 'Home / Following',
notifications: 'Notifications',
list: 'List',
public: 'Public',
@ -80,7 +80,7 @@ const TYPE_PARAMS = {
};
export const SHORTCUTS_META = {
following: {
title: 'Home',
title: 'Home / Following',
path: (_, index) => (index === 0 ? '/' : '/l/f'),
icon: 'home',
},
@ -188,7 +188,24 @@ function ShortcutsSettings() {
Specify a list of shortcuts that'll appear in the floating Shortcuts
button.
</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">
{shortcuts.map((shortcut, i) => {
const key = i + Object.values(shortcut);

View file

@ -7,8 +7,9 @@ import useTitle from '../utils/useTitle';
const LIMIT = 20;
function Hashtags() {
function Hashtags(props) {
let { hashtag, ...params } = useParams();
if (props.hashtag) hashtag = props.hashtag;
const { masto, instance } = api({ instance: params.instance });
const title = instance ? `#${hashtag} on ${instance}` : `#${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 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 (
<>
<Following
title="Home"
path="/"
id="home"
headerStart={false}
headerEnd={
<Link
to="/notifications"
class={`button plain ${
snapStates.notificationsShowNew ? 'has-badge' : ''
}`}
onClick={(e) => {
e.stopPropagation();
}}
>
<Icon icon="notification" size="l" alt="Notifications" />
</Link>
}
/>
{shortcutsColumnsMode ? (
<div id="columns">{shortcutsComponents}</div>
) : (
<Following
title="Home"
path="/"
id="home"
headerStart={false}
headerEnd={
<Link
to="/notifications"
class={`button plain ${
snapStates.notificationsShowNew ? 'has-badge' : ''
}`}
onClick={(e) => {
e.stopPropagation();
}}
>
<Icon icon="notification" size="l" alt="Notifications" />
</Link>
}
/>
)}
<button
// hidden={scrollDirection === 'end' && !nearReachStart}
type="button"

View file

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

View file

@ -9,10 +9,12 @@ import useTitle from '../utils/useTitle';
const LIMIT = 20;
function Public({ local }) {
function Public({ local, ...props }) {
const isLocal = !!local;
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})`;
useTitle(title, isLocal ? `/:instance?/p/l` : `/:instance?/p`);
const navigate = useNavigate();

View file

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

View file

@ -35,6 +35,8 @@ const states = proxy({
shortcuts: store.account.get('shortcuts') ?? [],
// Settings
settings: {
shortcutsColumnsMode:
store.account.get('settings-shortcutsColumnsMode') ?? false,
boostsCarousel: store.account.get('settings-boostsCarousel') ?? true,
},
});
@ -45,11 +47,15 @@ subscribeKey(states, 'notificationsLast', (v) => {
console.log('CHANGE', v);
store.account.set('notificationsLast', states.notificationsLast);
});
subscribeKey(states, 'settings-boostsCarousel', (v) => {
store.account.set('settings-boostsCarousel', !!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') {
store.account.set('shortcuts', states.shortcuts);
}