mirror of
https://github.com/cheeaun/phanpy.git
synced 2024-11-21 16:55:25 +03:00
New feature: Unsent Drafts
For now, this only works for unsent unsaved drafts e.g. the browser kill the page without giving the user the chance to discard
This commit is contained in:
parent
486695cbeb
commit
71b50382e9
12 changed files with 538 additions and 10 deletions
34
package-lock.json
generated
34
package-lock.json
generated
|
@ -13,7 +13,9 @@
|
|||
"dayjs": "~1.11.7",
|
||||
"dayjs-twitter": "~0.5.0",
|
||||
"fast-blurhash": "~1.1.2",
|
||||
"fast-deep-equal": "~3.1.3",
|
||||
"history": "~5.3.0",
|
||||
"idb-keyval": "~6.2.0",
|
||||
"just-debounce-it": "~3.2.0",
|
||||
"masto": "~5.2.0",
|
||||
"mem": "~9.0.2",
|
||||
|
@ -3259,8 +3261,7 @@
|
|||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.2.12",
|
||||
|
@ -3625,6 +3626,14 @@
|
|||
"integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/idb-keyval": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.0.tgz",
|
||||
"integrity": "sha512-uw+MIyQn2jl3+hroD7hF8J7PUviBU7BPKWw4f/ISf32D4LoGu98yHjrzWWJDASu9QNrX10tCJqk9YY0ClWm8Ng==",
|
||||
"dependencies": {
|
||||
"safari-14-idb-fix": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
|
@ -4822,6 +4831,11 @@
|
|||
"queue-microtask": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/safari-14-idb-fix": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/safari-14-idb-fix/-/safari-14-idb-fix-3.0.0.tgz",
|
||||
"integrity": "sha512-eBNFLob4PMq8JA1dGyFn6G97q3/WzNtFK4RnzT1fnLq+9RyrGknzYiM/9B12MnKAxuj1IXr7UKYtTNtjyKMBog=="
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
|
@ -8132,8 +8146,7 @@
|
|||
"fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
||||
},
|
||||
"fast-glob": {
|
||||
"version": "3.2.12",
|
||||
|
@ -8411,6 +8424,14 @@
|
|||
"integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==",
|
||||
"dev": true
|
||||
},
|
||||
"idb-keyval": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.0.tgz",
|
||||
"integrity": "sha512-uw+MIyQn2jl3+hroD7hF8J7PUviBU7BPKWw4f/ISf32D4LoGu98yHjrzWWJDASu9QNrX10tCJqk9YY0ClWm8Ng==",
|
||||
"requires": {
|
||||
"safari-14-idb-fix": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
|
@ -9267,6 +9288,11 @@
|
|||
"queue-microtask": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"safari-14-idb-fix": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/safari-14-idb-fix/-/safari-14-idb-fix-3.0.0.tgz",
|
||||
"integrity": "sha512-eBNFLob4PMq8JA1dGyFn6G97q3/WzNtFK4RnzT1fnLq+9RyrGknzYiM/9B12MnKAxuj1IXr7UKYtTNtjyKMBog=="
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
|
|
|
@ -15,7 +15,9 @@
|
|||
"dayjs": "~1.11.7",
|
||||
"dayjs-twitter": "~0.5.0",
|
||||
"fast-blurhash": "~1.1.2",
|
||||
"fast-deep-equal": "~3.1.3",
|
||||
"history": "~5.3.0",
|
||||
"idb-keyval": "~6.2.0",
|
||||
"just-debounce-it": "~3.2.0",
|
||||
"masto": "~5.2.0",
|
||||
"mem": "~9.0.2",
|
||||
|
|
12
src/app.jsx
12
src/app.jsx
|
@ -11,6 +11,7 @@ import { useSnapshot } from 'valtio';
|
|||
|
||||
import Account from './components/account';
|
||||
import Compose from './components/compose';
|
||||
import Drafts from './components/drafts';
|
||||
import Loader from './components/loader';
|
||||
import Modal from './components/modal';
|
||||
import Home from './pages/home';
|
||||
|
@ -280,6 +281,17 @@ function App() {
|
|||
<Account account={snapStates.showAccount} />
|
||||
</Modal>
|
||||
)}
|
||||
{!!snapStates.showDrafts && (
|
||||
<Modal
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
states.showDrafts = false;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Drafts />
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import './compose.css';
|
||||
|
||||
import '@github/text-expander-element';
|
||||
import equal from 'fast-deep-equal';
|
||||
import { forwardRef } from 'preact/compat';
|
||||
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
@ -10,12 +11,14 @@ import { useSnapshot } from 'valtio';
|
|||
|
||||
import supportedLanguages from '../data/status-supported-languages';
|
||||
import urlRegex from '../data/url-regex';
|
||||
import db from '../utils/db';
|
||||
import emojifyText from '../utils/emojify-text';
|
||||
import openCompose from '../utils/open-compose';
|
||||
import states from '../utils/states';
|
||||
import store from '../utils/store';
|
||||
import { getCurrentAccount } from '../utils/store-utils';
|
||||
import { getCurrentAccount, getCurrentAccountNS } from '../utils/store-utils';
|
||||
import useDebouncedCallback from '../utils/useDebouncedCallback';
|
||||
import useInterval from '../utils/useInterval';
|
||||
import visibilityIconsMap from '../utils/visibility-icons-map';
|
||||
|
||||
import Avatar from './avatar';
|
||||
|
@ -81,7 +84,7 @@ function Compose({
|
|||
}) {
|
||||
console.warn('RENDER COMPOSER');
|
||||
const [uiState, setUIState] = useState('default');
|
||||
const UID = useRef(uid());
|
||||
const UID = useRef(draftStatus?.uid || uid());
|
||||
console.log('Compose UID', UID.current);
|
||||
|
||||
const currentAccount = getCurrentAccount();
|
||||
|
@ -178,7 +181,6 @@ function Compose({
|
|||
}
|
||||
if (draftStatus) {
|
||||
const {
|
||||
uid,
|
||||
status,
|
||||
spoilerText,
|
||||
visibility,
|
||||
|
@ -187,7 +189,6 @@ function Compose({
|
|||
poll,
|
||||
mediaAttachments,
|
||||
} = draftStatus;
|
||||
UID.current = uid;
|
||||
const composablePoll = !!poll?.options && {
|
||||
...poll,
|
||||
options: poll.options.map((o) => o?.title || o),
|
||||
|
@ -348,6 +349,72 @@ function Compose({
|
|||
},
|
||||
);
|
||||
|
||||
const prevBackgroundDraft = useRef({});
|
||||
const draftKey = () => {
|
||||
const ns = getCurrentAccountNS();
|
||||
return `${ns}#${UID.current}`;
|
||||
};
|
||||
const saveUnsavedDraft = () => {
|
||||
// Not enabling this for editing status
|
||||
// I don't think this warrant a draft mode for a status that's already posted
|
||||
// Maybe it could be a big edit change but it should be rare
|
||||
if (editStatus) return;
|
||||
const key = draftKey();
|
||||
const backgroundDraft = {
|
||||
key,
|
||||
replyTo: replyToStatus
|
||||
? {
|
||||
/* Smaller payload of replyToStatus. Reasons:
|
||||
- No point storing whole thing
|
||||
- Could have media attachments
|
||||
- Could be deleted/edited later
|
||||
*/
|
||||
id: replyToStatus.id,
|
||||
account: {
|
||||
id: replyToStatus.account.id,
|
||||
username: replyToStatus.account.username,
|
||||
acct: replyToStatus.account.acct,
|
||||
},
|
||||
}
|
||||
: null,
|
||||
draftStatus: {
|
||||
uid: UID.current,
|
||||
status: textareaRef.current.value,
|
||||
spoilerText: spoilerTextRef.current.value,
|
||||
visibility,
|
||||
language,
|
||||
sensitive,
|
||||
poll,
|
||||
mediaAttachments,
|
||||
},
|
||||
};
|
||||
if (!equal(backgroundDraft, prevBackgroundDraft.current) && !canClose()) {
|
||||
console.debug('not equal', backgroundDraft, prevBackgroundDraft.current);
|
||||
db.drafts
|
||||
.set(key, {
|
||||
...backgroundDraft,
|
||||
state: 'unsaved',
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
.then(() => {
|
||||
console.debug('DRAFT saved', key, backgroundDraft);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error('DRAFT failed', key, e);
|
||||
});
|
||||
prevBackgroundDraft.current = structuredClone(backgroundDraft);
|
||||
}
|
||||
};
|
||||
useInterval(saveUnsavedDraft, 5000); // background save every 5s
|
||||
useEffect(() => {
|
||||
saveUnsavedDraft();
|
||||
// If unmounted, means user discarded the draft
|
||||
// Also means pop-out 🙈, but it's okay because the pop-out will persist the ID and re-create the draft
|
||||
return () => {
|
||||
db.drafts.del(draftKey());
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div id="compose-container" class={standalone ? 'standalone' : ''}>
|
||||
<div class="compose-top">
|
||||
|
@ -383,7 +450,6 @@ function Compose({
|
|||
// );
|
||||
|
||||
const newWin = openCompose({
|
||||
uid: UID.current,
|
||||
editStatus,
|
||||
replyToStatus,
|
||||
draftStatus: {
|
||||
|
@ -473,7 +539,7 @@ function Compose({
|
|||
mediaAttachments,
|
||||
},
|
||||
};
|
||||
window.opener.__COMPOSE__ = passData;
|
||||
window.opener.__COMPOSE__ = passData; // Pass it here instead of `showCompose` due to some weird proxy issue again
|
||||
window.opener.__STATES__.showCompose = true;
|
||||
},
|
||||
});
|
||||
|
|
94
src/components/drafts.css
Normal file
94
src/components/drafts.css
Normal file
|
@ -0,0 +1,94 @@
|
|||
.drafts-list {
|
||||
margin: 1em 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.drafts-list > li {
|
||||
margin: 8px 0 16px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.mini-draft-meta {
|
||||
font-size: 80%;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
padding: 8px 0;
|
||||
}
|
||||
.mini-draft-meta * {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
button.draft-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
border: 1px solid var(--link-faded-color);
|
||||
text-align: left;
|
||||
padding: 0;
|
||||
}
|
||||
button.draft-item:is(:hover, :focus) {
|
||||
border-color: var(--link-color);
|
||||
box-shadow: 0 0 0 3px var(--link-faded-color);
|
||||
filter: none !important;
|
||||
}
|
||||
|
||||
.mini-draft {
|
||||
display: flex;
|
||||
gap: 0 8px;
|
||||
font-size: 90%;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.mini-draft-aside {
|
||||
width: 64px;
|
||||
aspect-ratio: 1 / 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--bg-faded-color);
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid var(--outline-color);
|
||||
}
|
||||
.mini-draft-aside.has-image {
|
||||
background-image: var(--bg-image);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
.mini-draft-aside.has-image > span {
|
||||
background-color: var(--bg-faded-blur-color);
|
||||
backdrop-filter: blur(8px);
|
||||
padding: 4px 8px;
|
||||
border-radius: 32px;
|
||||
}
|
||||
.mini-draft-aside.has-image > span * {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.mini-draft-main {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.mini-draft-spoiler,
|
||||
.mini-draft-status {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
display: box;
|
||||
-webkit-box-orient: vertical;
|
||||
box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
line-height: 1.1;
|
||||
}
|
||||
.mini-draft-spoiler + .mini-draft-status {
|
||||
border-top: 1px dashed var(--text-insignificant-color);
|
||||
padding-top: 4px;
|
||||
margin-top: 4px;
|
||||
color: var(--text-insignificant-color);
|
||||
}
|
240
src/components/drafts.jsx
Normal file
240
src/components/drafts.jsx
Normal file
|
@ -0,0 +1,240 @@
|
|||
import './drafts.css';
|
||||
|
||||
import { useEffect, useMemo, useReducer, useState } from 'react';
|
||||
|
||||
import db from '../utils/db';
|
||||
import states from '../utils/states';
|
||||
import { getCurrentAccountNS } from '../utils/store-utils';
|
||||
|
||||
import Icon from './icon';
|
||||
import Loader from './loader';
|
||||
|
||||
function Drafts() {
|
||||
const [uiState, setUIState] = useState('default');
|
||||
const [drafts, setDrafts] = useState([]);
|
||||
const [reloadCount, reload] = useReducer((c) => c + 1, 0);
|
||||
|
||||
useEffect(() => {
|
||||
setUIState('loading');
|
||||
(async () => {
|
||||
try {
|
||||
const keys = await db.drafts.keys();
|
||||
if (keys.length) {
|
||||
const ns = getCurrentAccountNS();
|
||||
const ownKeys = keys.filter((key) => key.startsWith(ns));
|
||||
if (ownKeys.length) {
|
||||
const drafts = await db.drafts.getMany(ownKeys);
|
||||
drafts.sort(
|
||||
(a, b) =>
|
||||
new Date(b.updatedAt).getTime() -
|
||||
new Date(a.updatedAt).getTime(),
|
||||
);
|
||||
setDrafts(drafts);
|
||||
} else {
|
||||
setDrafts([]);
|
||||
}
|
||||
} else {
|
||||
setDrafts([]);
|
||||
}
|
||||
setUIState('default');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setUIState('error');
|
||||
}
|
||||
})();
|
||||
}, [reloadCount]);
|
||||
|
||||
const hasDrafts = drafts?.length > 0;
|
||||
|
||||
return (
|
||||
<div class="sheet">
|
||||
<header>
|
||||
<h2>
|
||||
Unsent drafts <Loader abrupt hidden={uiState !== 'loading'} />
|
||||
</h2>
|
||||
{hasDrafts && (
|
||||
<div class="insignificant">
|
||||
Looks like you have unsent drafts. Let's continue where you left
|
||||
off.
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
<main>
|
||||
{hasDrafts ? (
|
||||
<>
|
||||
<ul class="drafts-list">
|
||||
{drafts.map((draft) => {
|
||||
const { updatedAt, key, draftStatus, replyTo } = draft;
|
||||
const currentYear = new Date().getFullYear();
|
||||
const updatedAtDate = new Date(updatedAt);
|
||||
return (
|
||||
<li key={updatedAt}>
|
||||
<div class="mini-draft-meta">
|
||||
<b>
|
||||
<Icon icon={replyTo ? 'reply' : 'quill'} size="s" />{' '}
|
||||
<time>
|
||||
{!!replyTo && (
|
||||
<>
|
||||
@{replyTo.account.acct}
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
{Intl.DateTimeFormat('en', {
|
||||
// Show year if not current year
|
||||
year:
|
||||
updatedAtDate.getFullYear() === currentYear
|
||||
? undefined
|
||||
: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
weekday: 'short',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
}).format(updatedAtDate)}
|
||||
</time>
|
||||
</b>
|
||||
<button
|
||||
type="button"
|
||||
class="small light"
|
||||
disabled={uiState === 'loading'}
|
||||
onClick={() => {
|
||||
(async () => {
|
||||
try {
|
||||
const yes = confirm(
|
||||
'Are you sure you want to delete this draft?',
|
||||
);
|
||||
if (yes) {
|
||||
await db.drafts.del(key);
|
||||
reload();
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Error deleting draft! Please try again.');
|
||||
}
|
||||
})();
|
||||
}}
|
||||
>
|
||||
Delete…
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
disabled={uiState === 'loading'}
|
||||
class="draft-item"
|
||||
onClick={async () => {
|
||||
// console.log({ draftStatus });
|
||||
let replyToStatus;
|
||||
if (replyTo) {
|
||||
setUIState('loading');
|
||||
try {
|
||||
replyToStatus = await masto.v1.statuses.fetch(
|
||||
replyTo.id,
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert('Error fetching reply-to status!');
|
||||
setUIState('default');
|
||||
return;
|
||||
}
|
||||
setUIState('default');
|
||||
}
|
||||
window.__COMPOSE__ = {
|
||||
draftStatus,
|
||||
replyToStatus,
|
||||
};
|
||||
states.showCompose = true;
|
||||
states.showDrafts = false;
|
||||
}}
|
||||
>
|
||||
<MiniDraft draft={draft} />
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<p>
|
||||
<button
|
||||
type="button"
|
||||
class="light danger"
|
||||
disabled={uiState === 'loading'}
|
||||
onClick={() => {
|
||||
(async () => {
|
||||
const yes = confirm(
|
||||
'Are you sure you want to delete all drafts?',
|
||||
);
|
||||
if (yes) {
|
||||
setUIState('loading');
|
||||
try {
|
||||
await db.drafts.delMany(
|
||||
drafts.map((draft) => draft.key),
|
||||
);
|
||||
setUIState('default');
|
||||
reload();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert('Error deleting drafts! Please try again.');
|
||||
setUIState('error');
|
||||
}
|
||||
}
|
||||
})();
|
||||
}}
|
||||
>
|
||||
Delete all drafts…
|
||||
</button>
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p>No drafts found.</p>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MiniDraft({ draft }) {
|
||||
const { draftStatus, replyTo } = draft;
|
||||
const { status, spoilerText, poll, mediaAttachments } = draftStatus;
|
||||
const hasPoll = poll?.options?.length > 0;
|
||||
const hasMedia = mediaAttachments?.length > 0;
|
||||
const hasPollOrMedia = hasPoll || hasMedia;
|
||||
const firstImageMedia = useMemo(() => {
|
||||
if (!hasMedia) return;
|
||||
const image = mediaAttachments.find((media) => /image/.test(media.type));
|
||||
if (!image) return;
|
||||
const { file } = image;
|
||||
const objectURL = URL.createObjectURL(file);
|
||||
return objectURL;
|
||||
}, [hasMedia, mediaAttachments]);
|
||||
return (
|
||||
<>
|
||||
<div class="mini-draft">
|
||||
{hasPollOrMedia && (
|
||||
<div
|
||||
class={`mini-draft-aside ${firstImageMedia ? 'has-image' : ''}`}
|
||||
style={
|
||||
firstImageMedia
|
||||
? {
|
||||
'--bg-image': `url(${firstImageMedia})`,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
{hasPoll && <Icon icon="poll" />}
|
||||
{hasMedia && (
|
||||
<span>
|
||||
<Icon icon="attachment" />{' '}
|
||||
<small>{mediaAttachments?.length}</small>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div class="mini-draft-main">
|
||||
{!!spoilerText && <div class="mini-draft-spoiler">{spoilerText}</div>}
|
||||
{!!status && <div class="mini-draft-status">{status}</div>}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Drafts;
|
|
@ -7,7 +7,9 @@ import { useSnapshot } from 'valtio';
|
|||
import Icon from '../components/icon';
|
||||
import Loader from '../components/loader';
|
||||
import Status from '../components/status';
|
||||
import db from '../utils/db';
|
||||
import states, { saveStatus } from '../utils/states';
|
||||
import { getCurrentAccountNS } from '../utils/store-utils';
|
||||
import useDebouncedCallback from '../utils/useDebouncedCallback';
|
||||
import useScroll from '../utils/useScroll';
|
||||
|
||||
|
@ -181,6 +183,19 @@ function Home({ hidden }) {
|
|||
}
|
||||
}, [reachTop]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const keys = await db.drafts.keys();
|
||||
if (keys.length) {
|
||||
const ns = getCurrentAccountNS();
|
||||
const ownKeys = keys.filter((key) => key.startsWith(ns));
|
||||
if (ownKeys.length) {
|
||||
states.showDrafts = true;
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
id="home-page"
|
||||
|
|
|
@ -184,6 +184,19 @@ function Settings({ onClose }) {
|
|||
</label>
|
||||
</div>
|
||||
</form>
|
||||
<h2>Hidden features</h2>
|
||||
<p>
|
||||
<button
|
||||
type="button"
|
||||
class="light"
|
||||
onClick={() => {
|
||||
states.showDrafts = true;
|
||||
states.showSettings = false;
|
||||
}}
|
||||
>
|
||||
Unsent drafts
|
||||
</button>
|
||||
</p>
|
||||
<h2>About</h2>
|
||||
<p>
|
||||
<a href="https://github.com/cheeaun/phanpy" target="_blank">
|
||||
|
|
28
src/utils/db.js
Normal file
28
src/utils/db.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
import {
|
||||
clear,
|
||||
createStore,
|
||||
del,
|
||||
delMany,
|
||||
get,
|
||||
getMany,
|
||||
keys,
|
||||
set,
|
||||
} from 'idb-keyval';
|
||||
|
||||
const draftsStore = createStore('drafts-db', 'drafts-store');
|
||||
|
||||
// Add additonal `draftsStore` parameter to all methods
|
||||
|
||||
const drafts = {
|
||||
set: (key, val) => set(key, val, draftsStore),
|
||||
get: (key) => get(key, draftsStore),
|
||||
getMany: (keys) => getMany(keys, draftsStore),
|
||||
del: (key) => del(key, draftsStore),
|
||||
delMany: (keys) => delMany(keys, draftsStore),
|
||||
clear: () => clear(draftsStore),
|
||||
keys: () => keys(draftsStore),
|
||||
};
|
||||
|
||||
export default {
|
||||
drafts,
|
||||
};
|
|
@ -18,6 +18,7 @@ const states = proxy({
|
|||
showCompose: false,
|
||||
showSettings: false,
|
||||
showAccount: false,
|
||||
showDrafts: false,
|
||||
composeCharacterCount: 0,
|
||||
});
|
||||
export default states;
|
||||
|
|
|
@ -7,3 +7,12 @@ export function getCurrentAccount() {
|
|||
accounts.find((a) => a.info.id === currentAccount) || accounts[0];
|
||||
return account;
|
||||
}
|
||||
|
||||
export function getCurrentAccountNS() {
|
||||
const account = getCurrentAccount();
|
||||
const {
|
||||
instanceURL,
|
||||
info: { id },
|
||||
} = account;
|
||||
return `${id}@${instanceURL}`;
|
||||
}
|
||||
|
|
22
src/utils/useInterval.js
Normal file
22
src/utils/useInterval.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
// useInterval with Preact
|
||||
import { useEffect, useRef } from 'preact/hooks';
|
||||
|
||||
export default function useInterval(callback, delay) {
|
||||
const savedCallback = useRef();
|
||||
|
||||
// Remember the latest callback.
|
||||
useEffect(() => {
|
||||
savedCallback.current = callback;
|
||||
}, [callback]);
|
||||
|
||||
// Set up the interval.
|
||||
useEffect(() => {
|
||||
function tick() {
|
||||
savedCallback.current();
|
||||
}
|
||||
if (delay !== null) {
|
||||
let id = setInterval(tick, delay);
|
||||
return () => clearInterval(id);
|
||||
}
|
||||
}, [delay]);
|
||||
}
|
Loading…
Reference in a new issue