Add Generic Accounts modal
Also refactored whole bunch of stuff
This commit is contained in:
parent
dd2ca7bf35
commit
b57d8adf18
10 changed files with 506 additions and 194 deletions
196
src/app.jsx
196
src/app.jsx
|
@ -7,33 +7,21 @@ import {
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'preact/hooks';
|
} from 'preact/hooks';
|
||||||
import {
|
import { matchPath, Route, Routes, useLocation } from 'react-router-dom';
|
||||||
matchPath,
|
|
||||||
Route,
|
|
||||||
Routes,
|
|
||||||
useLocation,
|
|
||||||
useNavigate,
|
|
||||||
} from 'react-router-dom';
|
|
||||||
import 'swiped-events';
|
import 'swiped-events';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
import AccountSheet from './components/account-sheet';
|
|
||||||
import BackgroundService from './components/background-service';
|
import BackgroundService from './components/background-service';
|
||||||
import Compose from './components/compose';
|
|
||||||
import ComposeButton from './components/compose-button';
|
import ComposeButton from './components/compose-button';
|
||||||
import Drafts from './components/drafts';
|
|
||||||
import { ICONS } from './components/icon';
|
import { ICONS } from './components/icon';
|
||||||
import KeyboardShortcutsHelp from './components/keyboard-shortcuts-help';
|
import KeyboardShortcutsHelp from './components/keyboard-shortcuts-help';
|
||||||
import Loader from './components/loader';
|
import Loader from './components/loader';
|
||||||
import MediaModal from './components/media-modal';
|
import Modals from './components/modals';
|
||||||
import Modal from './components/modal';
|
|
||||||
import NotificationService from './components/notification-service';
|
import NotificationService from './components/notification-service';
|
||||||
import SearchCommand from './components/search-command';
|
import SearchCommand from './components/search-command';
|
||||||
import Shortcuts from './components/shortcuts';
|
import Shortcuts from './components/shortcuts';
|
||||||
import ShortcutsSettings from './components/shortcuts-settings';
|
|
||||||
import NotFound from './pages/404';
|
import NotFound from './pages/404';
|
||||||
import AccountStatuses from './pages/account-statuses';
|
import AccountStatuses from './pages/account-statuses';
|
||||||
import Accounts from './pages/accounts';
|
|
||||||
import Bookmarks from './pages/bookmarks';
|
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';
|
||||||
|
@ -48,7 +36,6 @@ import Mentions from './pages/mentions';
|
||||||
import Notifications from './pages/notifications';
|
import Notifications from './pages/notifications';
|
||||||
import Public from './pages/public';
|
import Public from './pages/public';
|
||||||
import Search from './pages/search';
|
import Search from './pages/search';
|
||||||
import Settings from './pages/settings';
|
|
||||||
import StatusRoute from './pages/status-route';
|
import StatusRoute from './pages/status-route';
|
||||||
import Trending from './pages/trending';
|
import Trending from './pages/trending';
|
||||||
import Welcome from './pages/welcome';
|
import Welcome from './pages/welcome';
|
||||||
|
@ -60,7 +47,7 @@ import {
|
||||||
initPreferences,
|
initPreferences,
|
||||||
} from './utils/api';
|
} from './utils/api';
|
||||||
import { getAccessToken } from './utils/auth';
|
import { getAccessToken } from './utils/auth';
|
||||||
import showToast from './utils/show-toast';
|
import focusDeck from './utils/focus-deck';
|
||||||
import states, { initStates } from './utils/states';
|
import states, { initStates } from './utils/states';
|
||||||
import store from './utils/store';
|
import store from './utils/store';
|
||||||
import { getCurrentAccount } from './utils/store-utils';
|
import { getCurrentAccount } from './utils/store-utils';
|
||||||
|
@ -85,7 +72,6 @@ function App() {
|
||||||
const snapStates = useSnapshot(states);
|
const snapStates = useSnapshot(states);
|
||||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||||
const [uiState, setUIState] = useState('loading');
|
const [uiState, setUIState] = useState('loading');
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const theme = store.local.get('theme');
|
const theme = store.local.get('theme');
|
||||||
|
@ -165,41 +151,9 @@ function App() {
|
||||||
let location = useLocation();
|
let location = useLocation();
|
||||||
states.currentLocation = location.pathname;
|
states.currentLocation = location.pathname;
|
||||||
|
|
||||||
const focusDeck = () => {
|
|
||||||
let timer = setTimeout(() => {
|
|
||||||
const columns = document.getElementById('columns');
|
|
||||||
if (columns) {
|
|
||||||
// Focus first column
|
|
||||||
// columns.querySelector('.deck-container')?.focus?.();
|
|
||||||
} else {
|
|
||||||
const backDrop = document.querySelector('.deck-backdrop');
|
|
||||||
if (backDrop) return;
|
|
||||||
// Focus last deck
|
|
||||||
const pages = document.querySelectorAll('.deck-container');
|
|
||||||
const page = pages[pages.length - 1]; // last one
|
|
||||||
if (page && page.tabIndex === -1) {
|
|
||||||
console.log('FOCUS', page);
|
|
||||||
page.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
};
|
|
||||||
useEffect(focusDeck, [location, isLoggedIn]);
|
useEffect(focusDeck, [location, isLoggedIn]);
|
||||||
const showModal =
|
|
||||||
snapStates.showCompose ||
|
|
||||||
snapStates.showSettings ||
|
|
||||||
snapStates.showAccounts ||
|
|
||||||
snapStates.showAccount ||
|
|
||||||
snapStates.showDrafts ||
|
|
||||||
snapStates.showMediaModal ||
|
|
||||||
snapStates.showShortcutsSettings ||
|
|
||||||
snapStates.showKeyboardShortcutsHelp;
|
|
||||||
useEffect(() => {
|
|
||||||
if (!showModal) focusDeck();
|
|
||||||
}, [showModal]);
|
|
||||||
|
|
||||||
const { prevLocation } = snapStates;
|
const prevLocation = snapStates.prevLocation;
|
||||||
const backgroundLocation = useRef(prevLocation || null);
|
const backgroundLocation = useRef(prevLocation || null);
|
||||||
const isModalPage = useMemo(() => {
|
const isModalPage = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
|
@ -294,147 +248,7 @@ function App() {
|
||||||
snapStates.settings.shortcutsViewMode !== 'multi-column' && (
|
snapStates.settings.shortcutsViewMode !== 'multi-column' && (
|
||||||
<Shortcuts />
|
<Shortcuts />
|
||||||
)}
|
)}
|
||||||
{!!snapStates.showCompose && (
|
<Modals />
|
||||||
<Modal>
|
|
||||||
<Compose
|
|
||||||
replyToStatus={
|
|
||||||
typeof snapStates.showCompose !== 'boolean'
|
|
||||||
? snapStates.showCompose.replyToStatus
|
|
||||||
: window.__COMPOSE__?.replyToStatus || null
|
|
||||||
}
|
|
||||||
editStatus={
|
|
||||||
states.showCompose?.editStatus ||
|
|
||||||
window.__COMPOSE__?.editStatus ||
|
|
||||||
null
|
|
||||||
}
|
|
||||||
draftStatus={
|
|
||||||
states.showCompose?.draftStatus ||
|
|
||||||
window.__COMPOSE__?.draftStatus ||
|
|
||||||
null
|
|
||||||
}
|
|
||||||
onClose={(results) => {
|
|
||||||
const { newStatus, instance } = results || {};
|
|
||||||
states.showCompose = false;
|
|
||||||
window.__COMPOSE__ = null;
|
|
||||||
if (newStatus) {
|
|
||||||
states.reloadStatusPage++;
|
|
||||||
showToast({
|
|
||||||
text: 'Post published. Check it out.',
|
|
||||||
delay: 1000,
|
|
||||||
duration: 10_000, // 10 seconds
|
|
||||||
onClick: (toast) => {
|
|
||||||
toast.hideToast();
|
|
||||||
states.prevLocation = location;
|
|
||||||
navigate(
|
|
||||||
instance
|
|
||||||
? `/${instance}/s/${newStatus.id}`
|
|
||||||
: `/s/${newStatus.id}`,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
{!!snapStates.showSettings && (
|
|
||||||
<Modal
|
|
||||||
onClick={(e) => {
|
|
||||||
if (e.target === e.currentTarget) {
|
|
||||||
states.showSettings = false;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Settings
|
|
||||||
onClose={() => {
|
|
||||||
states.showSettings = false;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
{!!snapStates.showAccounts && (
|
|
||||||
<Modal
|
|
||||||
onClick={(e) => {
|
|
||||||
if (e.target === e.currentTarget) {
|
|
||||||
states.showAccounts = false;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Accounts
|
|
||||||
onClose={() => {
|
|
||||||
states.showAccounts = false;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
{!!snapStates.showAccount && (
|
|
||||||
<Modal
|
|
||||||
class="light"
|
|
||||||
onClick={(e) => {
|
|
||||||
if (e.target === e.currentTarget) {
|
|
||||||
states.showAccount = false;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AccountSheet
|
|
||||||
account={snapStates.showAccount?.account || snapStates.showAccount}
|
|
||||||
instance={snapStates.showAccount?.instance}
|
|
||||||
onClose={({ destination } = {}) => {
|
|
||||||
states.showAccount = false;
|
|
||||||
if (destination) {
|
|
||||||
states.showAccounts = false;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
{!!snapStates.showDrafts && (
|
|
||||||
<Modal
|
|
||||||
onClick={(e) => {
|
|
||||||
if (e.target === e.currentTarget) {
|
|
||||||
states.showDrafts = false;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Drafts onClose={() => (states.showDrafts = false)} />
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
{!!snapStates.showMediaModal && (
|
|
||||||
<Modal
|
|
||||||
onClick={(e) => {
|
|
||||||
if (
|
|
||||||
e.target === e.currentTarget ||
|
|
||||||
e.target.classList.contains('media')
|
|
||||||
) {
|
|
||||||
states.showMediaModal = false;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MediaModal
|
|
||||||
mediaAttachments={snapStates.showMediaModal.mediaAttachments}
|
|
||||||
instance={snapStates.showMediaModal.instance}
|
|
||||||
index={snapStates.showMediaModal.index}
|
|
||||||
statusID={snapStates.showMediaModal.statusID}
|
|
||||||
onClose={() => {
|
|
||||||
states.showMediaModal = false;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
{!!snapStates.showShortcutsSettings && (
|
|
||||||
<Modal
|
|
||||||
class="light"
|
|
||||||
onClick={(e) => {
|
|
||||||
if (e.target === e.currentTarget) {
|
|
||||||
states.showShortcutsSettings = false;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ShortcutsSettings
|
|
||||||
onClose={() => (states.showShortcutsSettings = false)}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
<NotificationService />
|
<NotificationService />
|
||||||
<BackgroundService isLoggedIn={isLoggedIn} />
|
<BackgroundService isLoggedIn={isLoggedIn} />
|
||||||
<SearchCommand onClose={focusDeck} />
|
<SearchCommand onClose={focusDeck} />
|
||||||
|
|
|
@ -148,6 +148,12 @@
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
[tabindex='0']:is(:hover, :focus) {
|
||||||
|
color: var(--text-color);
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.timeline-start .account-container .stats {
|
.timeline-start .account-container .stats {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
|
@ -46,6 +46,8 @@ const MUTE_DURATIONS_LABELS = {
|
||||||
604_800_000: '1 week',
|
604_800_000: '1 week',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const LIMIT = 80;
|
||||||
|
|
||||||
function AccountInfo({
|
function AccountInfo({
|
||||||
account,
|
account,
|
||||||
fetchAccount = () => {},
|
fetchAccount = () => {},
|
||||||
|
@ -53,6 +55,7 @@ function AccountInfo({
|
||||||
instance,
|
instance,
|
||||||
authenticated,
|
authenticated,
|
||||||
}) {
|
}) {
|
||||||
|
const { masto } = api();
|
||||||
const [uiState, setUIState] = useState('default');
|
const [uiState, setUIState] = useState('default');
|
||||||
const isString = typeof account === 'string';
|
const isString = typeof account === 'string';
|
||||||
const [info, setInfo] = useState(isString ? null : account);
|
const [info, setInfo] = useState(isString ? null : account);
|
||||||
|
@ -114,6 +117,59 @@ function AccountInfo({
|
||||||
|
|
||||||
const [headerCornerColors, setHeaderCornerColors] = useState([]);
|
const [headerCornerColors, setHeaderCornerColors] = useState([]);
|
||||||
|
|
||||||
|
const followersIterator = useRef();
|
||||||
|
const familiarFollowersCache = useRef([]);
|
||||||
|
async function fetchFollowers(firstLoad) {
|
||||||
|
if (firstLoad || !followersIterator.current) {
|
||||||
|
followersIterator.current = masto.v1.accounts.listFollowers(id, {
|
||||||
|
limit: LIMIT,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const results = await followersIterator.current.next();
|
||||||
|
const { value } = results;
|
||||||
|
let newValue = [];
|
||||||
|
// On first load, fetch familiar followers, merge to top of results' `value`
|
||||||
|
// Remove dups on every fetch
|
||||||
|
if (firstLoad) {
|
||||||
|
const familiarFollowers = await masto.v1.accounts.fetchFamiliarFollowers(
|
||||||
|
id,
|
||||||
|
);
|
||||||
|
familiarFollowersCache.current = familiarFollowers[0].accounts;
|
||||||
|
newValue = [
|
||||||
|
...familiarFollowersCache.current,
|
||||||
|
...value.filter(
|
||||||
|
(account) =>
|
||||||
|
!familiarFollowersCache.current.some(
|
||||||
|
(familiar) => familiar.id === account.id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
newValue = value.filter(
|
||||||
|
(account) =>
|
||||||
|
!familiarFollowersCache.current.some(
|
||||||
|
(familiar) => familiar.id === account.id,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...results,
|
||||||
|
value: newValue,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const followingIterator = useRef();
|
||||||
|
async function fetchFollowing(firstLoad) {
|
||||||
|
if (firstLoad || !followingIterator.current) {
|
||||||
|
followingIterator.current = masto.v1.accounts.listFollowing(id, {
|
||||||
|
limit: LIMIT,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const results = await followingIterator.current.next();
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={`account-container ${uiState === 'loading' ? 'skeleton' : ''}`}
|
class={`account-container ${uiState === 'loading' ? 'skeleton' : ''}`}
|
||||||
|
@ -312,13 +368,30 @@ function AccountInfo({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<p class="stats">
|
<p class="stats">
|
||||||
<div>
|
<div
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => {
|
||||||
|
states.showGenericAccounts = {
|
||||||
|
heading: 'Followers',
|
||||||
|
fetchAccounts: fetchFollowers,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
>
|
||||||
<span title={followersCount}>
|
<span title={followersCount}>
|
||||||
{shortenNumber(followersCount)}
|
{shortenNumber(followersCount)}
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
Followers
|
Followers
|
||||||
</div>
|
</div>
|
||||||
<div class="insignificant">
|
<div
|
||||||
|
class="insignificant"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => {
|
||||||
|
states.showGenericAccounts = {
|
||||||
|
heading: 'Following',
|
||||||
|
fetchAccounts: fetchFollowing,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
>
|
||||||
<span title={followingCount}>
|
<span title={followingCount}>
|
||||||
{shortenNumber(followingCount)}
|
{shortenNumber(followingCount)}
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
|
|
42
src/components/generic-accounts.css
Normal file
42
src/components/generic-accounts.css
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
#generic-accounts-container {
|
||||||
|
.accounts-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 8px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
flex-direction: row;
|
||||||
|
column-gap: 1.5em;
|
||||||
|
row-gap: 16px;
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: 16em;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-block-acct {
|
||||||
|
font-size: 80%;
|
||||||
|
color: var(--text-insignificant-color);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.reactions-block {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-self: center;
|
||||||
|
|
||||||
|
.favourite-icon {
|
||||||
|
color: var(--favourite-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reblog-icon {
|
||||||
|
color: var(--reblog-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
135
src/components/generic-accounts.jsx
Normal file
135
src/components/generic-accounts.jsx
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
import './generic-accounts.css';
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'preact/hooks';
|
||||||
|
import { InView } from 'react-intersection-observer';
|
||||||
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
|
import states from '../utils/states';
|
||||||
|
|
||||||
|
import AccountBlock from './account-block';
|
||||||
|
import Icon from './icon';
|
||||||
|
import Loader from './loader';
|
||||||
|
|
||||||
|
export default function GenericAccounts({ onClose = () => {} }) {
|
||||||
|
const snapStates = useSnapshot(states);
|
||||||
|
const [uiState, setUIState] = useState('default');
|
||||||
|
const [accounts, setAccounts] = useState([]);
|
||||||
|
const [showMore, setShowMore] = useState(false);
|
||||||
|
|
||||||
|
if (!snapStates.showGenericAccounts) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
heading,
|
||||||
|
fetchAccounts,
|
||||||
|
accounts: staticAccounts,
|
||||||
|
showReactions,
|
||||||
|
} = snapStates.showGenericAccounts;
|
||||||
|
|
||||||
|
const loadAccounts = (firstLoad) => {
|
||||||
|
if (!fetchAccounts) return;
|
||||||
|
setUIState('loading');
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const { done, value } = await fetchAccounts(firstLoad);
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
if (firstLoad) {
|
||||||
|
setAccounts(value);
|
||||||
|
} else {
|
||||||
|
setAccounts((prev) => [...prev, ...value]);
|
||||||
|
}
|
||||||
|
setShowMore(!done);
|
||||||
|
} else {
|
||||||
|
setShowMore(false);
|
||||||
|
}
|
||||||
|
setUIState('default');
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
setUIState('error');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (staticAccounts?.length > 0) {
|
||||||
|
setAccounts(staticAccounts);
|
||||||
|
} else {
|
||||||
|
loadAccounts(true);
|
||||||
|
}
|
||||||
|
}, [staticAccounts]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="generic-accounts-container" class="sheet" tabindex="-1">
|
||||||
|
<button type="button" class="sheet-close" onClick={onClose}>
|
||||||
|
<Icon icon="x" />
|
||||||
|
</button>
|
||||||
|
<header>
|
||||||
|
<h2>{heading || 'Accounts'}</h2>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
{accounts.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<ul class="accounts-list">
|
||||||
|
{accounts.map((account) => (
|
||||||
|
<li key={account.id}>
|
||||||
|
{showReactions && account._types?.length > 0 && (
|
||||||
|
<div class="reactions-block">
|
||||||
|
{account._types.map((type) => (
|
||||||
|
<Icon
|
||||||
|
icon={
|
||||||
|
{
|
||||||
|
reblog: 'rocket',
|
||||||
|
favourite: 'heart',
|
||||||
|
}[type]
|
||||||
|
}
|
||||||
|
class={`${type}-icon`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<AccountBlock account={account} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
{uiState === 'default' ? (
|
||||||
|
showMore ? (
|
||||||
|
<InView
|
||||||
|
onChange={(inView) => {
|
||||||
|
if (inView) {
|
||||||
|
loadAccounts();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="plain block"
|
||||||
|
onClick={() => loadAccounts()}
|
||||||
|
>
|
||||||
|
Show more…
|
||||||
|
</button>
|
||||||
|
</InView>
|
||||||
|
) : (
|
||||||
|
<p class="ui-state insignificant">The end.</p>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
uiState === 'loading' && (
|
||||||
|
<p class="ui-state">
|
||||||
|
<Loader abrupt />
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : uiState === 'loading' ? (
|
||||||
|
<p class="ui-state">
|
||||||
|
<Loader abrupt />
|
||||||
|
</p>
|
||||||
|
) : uiState === 'error' ? (
|
||||||
|
<p class="ui-state">Error loading accounts</p>
|
||||||
|
) : (
|
||||||
|
<p class="ui-state insignificant">Nothing to show</p>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -45,6 +45,7 @@ export const ICONS = {
|
||||||
plus: () => import('@iconify-icons/mingcute/add-circle-line'),
|
plus: () => import('@iconify-icons/mingcute/add-circle-line'),
|
||||||
'chevron-left': () => import('@iconify-icons/mingcute/left-line'),
|
'chevron-left': () => import('@iconify-icons/mingcute/left-line'),
|
||||||
'chevron-right': () => import('@iconify-icons/mingcute/right-line'),
|
'chevron-right': () => import('@iconify-icons/mingcute/right-line'),
|
||||||
|
'chevron-down': () => import('@iconify-icons/mingcute/down-line'),
|
||||||
reply: [
|
reply: [
|
||||||
() => import('@iconify-icons/mingcute/share-forward-line'),
|
() => import('@iconify-icons/mingcute/share-forward-line'),
|
||||||
'180deg',
|
'180deg',
|
||||||
|
|
190
src/components/modals.jsx
Normal file
190
src/components/modals.jsx
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
import { subscribe, useSnapshot } from 'valtio';
|
||||||
|
|
||||||
|
import Accounts from '../pages/accounts';
|
||||||
|
import Settings from '../pages/settings';
|
||||||
|
import focusDeck from '../utils/focus-deck';
|
||||||
|
import showToast from '../utils/show-toast';
|
||||||
|
import states from '../utils/states';
|
||||||
|
|
||||||
|
import AccountSheet from './account-sheet';
|
||||||
|
import Compose from './compose';
|
||||||
|
import Drafts from './drafts';
|
||||||
|
import GenericAccounts from './generic-accounts';
|
||||||
|
import MediaModal from './media-modal';
|
||||||
|
import Modal from './modal';
|
||||||
|
import ShortcutsSettings from './shortcuts-settings';
|
||||||
|
|
||||||
|
subscribe(states, (changes) => {
|
||||||
|
for (const [action, path, value, prevValue] of changes) {
|
||||||
|
// When closing modal, focus on deck
|
||||||
|
if (/^show/i.test(path) && !value) {
|
||||||
|
focusDeck();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function Modals() {
|
||||||
|
const snapStates = useSnapshot(states);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!!snapStates.showCompose && (
|
||||||
|
<Modal>
|
||||||
|
<Compose
|
||||||
|
replyToStatus={
|
||||||
|
typeof snapStates.showCompose !== 'boolean'
|
||||||
|
? snapStates.showCompose.replyToStatus
|
||||||
|
: window.__COMPOSE__?.replyToStatus || null
|
||||||
|
}
|
||||||
|
editStatus={
|
||||||
|
states.showCompose?.editStatus ||
|
||||||
|
window.__COMPOSE__?.editStatus ||
|
||||||
|
null
|
||||||
|
}
|
||||||
|
draftStatus={
|
||||||
|
states.showCompose?.draftStatus ||
|
||||||
|
window.__COMPOSE__?.draftStatus ||
|
||||||
|
null
|
||||||
|
}
|
||||||
|
onClose={(results) => {
|
||||||
|
const { newStatus, instance } = results || {};
|
||||||
|
states.showCompose = false;
|
||||||
|
window.__COMPOSE__ = null;
|
||||||
|
if (newStatus) {
|
||||||
|
states.reloadStatusPage++;
|
||||||
|
showToast({
|
||||||
|
text: 'Post published. Check it out.',
|
||||||
|
delay: 1000,
|
||||||
|
duration: 10_000, // 10 seconds
|
||||||
|
onClick: (toast) => {
|
||||||
|
toast.hideToast();
|
||||||
|
states.prevLocation = location;
|
||||||
|
// navigate(
|
||||||
|
// instance
|
||||||
|
// ? `/${instance}/s/${newStatus.id}`
|
||||||
|
// : `/s/${newStatus.id}`,
|
||||||
|
// );
|
||||||
|
location.hash = instance
|
||||||
|
? `/${instance}/s/${newStatus.id}`
|
||||||
|
: `/s/${newStatus.id}`;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
{!!snapStates.showSettings && (
|
||||||
|
<Modal
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
states.showSettings = false;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Settings
|
||||||
|
onClose={() => {
|
||||||
|
states.showSettings = false;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
{!!snapStates.showAccounts && (
|
||||||
|
<Modal
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
states.showAccounts = false;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Accounts
|
||||||
|
onClose={() => {
|
||||||
|
states.showAccounts = false;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
{!!snapStates.showAccount && (
|
||||||
|
<Modal
|
||||||
|
class="light"
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
states.showAccount = false;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AccountSheet
|
||||||
|
account={snapStates.showAccount?.account || snapStates.showAccount}
|
||||||
|
instance={snapStates.showAccount?.instance}
|
||||||
|
onClose={({ destination } = {}) => {
|
||||||
|
states.showAccount = false;
|
||||||
|
if (destination) {
|
||||||
|
states.showAccounts = false;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
{!!snapStates.showDrafts && (
|
||||||
|
<Modal
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
states.showDrafts = false;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Drafts onClose={() => (states.showDrafts = false)} />
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
{!!snapStates.showMediaModal && (
|
||||||
|
<Modal
|
||||||
|
onClick={(e) => {
|
||||||
|
if (
|
||||||
|
e.target === e.currentTarget ||
|
||||||
|
e.target.classList.contains('media')
|
||||||
|
) {
|
||||||
|
states.showMediaModal = false;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MediaModal
|
||||||
|
mediaAttachments={snapStates.showMediaModal.mediaAttachments}
|
||||||
|
instance={snapStates.showMediaModal.instance}
|
||||||
|
index={snapStates.showMediaModal.index}
|
||||||
|
statusID={snapStates.showMediaModal.statusID}
|
||||||
|
onClose={() => {
|
||||||
|
states.showMediaModal = false;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
{!!snapStates.showShortcutsSettings && (
|
||||||
|
<Modal
|
||||||
|
class="light"
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
states.showShortcutsSettings = false;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ShortcutsSettings
|
||||||
|
onClose={() => (states.showShortcutsSettings = false)}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
{!!snapStates.showGenericAccounts && (
|
||||||
|
<Modal
|
||||||
|
class="light"
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
states.showGenericAccounts = false;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<GenericAccounts
|
||||||
|
onClose={() => (states.showGenericAccounts = false)}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -126,6 +126,21 @@ function Notification({ notification, instance, reload, isStatic }) {
|
||||||
const formattedCreatedAt =
|
const formattedCreatedAt =
|
||||||
notification.createdAt && new Date(notification.createdAt).toLocaleString();
|
notification.createdAt && new Date(notification.createdAt).toLocaleString();
|
||||||
|
|
||||||
|
const genericAccountsHeading =
|
||||||
|
{
|
||||||
|
'favourite+reblog': 'Boosted/Favourited by…',
|
||||||
|
favourite: 'Favourited by…',
|
||||||
|
reblog: 'Boosted by…',
|
||||||
|
follow: 'Followed by…',
|
||||||
|
}[type] || 'Accounts';
|
||||||
|
const handleOpenGenericAccounts = () => {
|
||||||
|
states.showGenericAccounts = {
|
||||||
|
heading: genericAccountsHeading,
|
||||||
|
accounts: _accounts,
|
||||||
|
showReactions: type === 'favourite+reblog',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={`notification notification-${type}`} tabIndex="0">
|
<div class={`notification notification-${type}`} tabIndex="0">
|
||||||
<div
|
<div
|
||||||
|
@ -153,7 +168,9 @@ function Notification({ notification, instance, reload, isStatic }) {
|
||||||
<>
|
<>
|
||||||
{_accounts?.length > 1 ? (
|
{_accounts?.length > 1 ? (
|
||||||
<>
|
<>
|
||||||
<b>{_accounts.length} people</b>{' '}
|
<b tabIndex="0" onClick={handleOpenGenericAccounts}>
|
||||||
|
{_accounts.length} people
|
||||||
|
</b>{' '}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
@ -228,6 +245,13 @@ function Notification({ notification, instance, reload, isStatic }) {
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
</>
|
</>
|
||||||
))}
|
))}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="small plain"
|
||||||
|
onClick={handleOpenGenericAccounts}
|
||||||
|
>
|
||||||
|
<Icon icon="chevron-down" />
|
||||||
|
</button>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{_statuses?.length > 1 && (
|
{_statuses?.length > 1 && (
|
||||||
|
|
|
@ -4,6 +4,11 @@
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
animation: appear 0.2s ease-out;
|
animation: appear 0.2s ease-out;
|
||||||
clear: both;
|
clear: both;
|
||||||
|
|
||||||
|
b[tabindex='0']:is(:hover, :focus) {
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.notification.notification-mention {
|
.notification.notification-mention {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
|
|
22
src/utils/focus-deck.jsx
Normal file
22
src/utils/focus-deck.jsx
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
const focusDeck = () => {
|
||||||
|
let timer = setTimeout(() => {
|
||||||
|
const columns = document.getElementById('columns');
|
||||||
|
if (columns) {
|
||||||
|
// Focus first column
|
||||||
|
// columns.querySelector('.deck-container')?.focus?.();
|
||||||
|
} else {
|
||||||
|
const backDrop = document.querySelector('.deck-backdrop');
|
||||||
|
if (backDrop) return;
|
||||||
|
// Focus last deck
|
||||||
|
const pages = document.querySelectorAll('.deck-container');
|
||||||
|
const page = pages[pages.length - 1]; // last one
|
||||||
|
if (page && page.tabIndex === -1) {
|
||||||
|
console.log('FOCUS', page);
|
||||||
|
page.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default focusDeck;
|
Loading…
Add table
Reference in a new issue