1
0
Fork 0

Compare commits

...

11 commits

Author SHA1 Message Date
Alexander Yakovlev 8cc8e03d66 Merge remote-tracking branch 'upstream/main' 2024-02-26 16:48:16 +06:00
Lim Chee Aun 707b51a1a0 Don't trigger auto list if meta/ctrl+enter 2024-02-26 14:57:09 +08:00
Lim Chee Aun 201ca6ce4a Catch-up (beta) 2024-02-26 14:02:58 +08:00
Lim Chee Aun a419bb9b61 Fix small typo 2024-02-26 14:02:12 +08:00
Lim Chee Aun a8b5c8cd64 Experimental "cloud" shortcuts settings import/export 2024-02-26 14:00:53 +08:00
Lim Chee Aun a3236ea0f0 Report post/profile 2024-02-26 13:59:26 +08:00
Lim Chee Aun c595b0ee31 Fix toasts showing for unauthenticated interactions 2024-02-26 11:58:22 +08:00
Lim Chee Aun 89f34d7942 Use em, and hide if there's nothing in account "note" 2024-02-26 11:56:18 +08:00
Lim Chee Aun f23e4b0dd9 Just in case, probably not needed 2024-02-25 13:37:50 +08:00
Lim Chee Aun e7d2d088ba Super weird fix for clicks "leaked" to the container 2024-02-25 13:37:29 +08:00
Lim Chee Aun bf609b979e Upgrade masto 2024-02-24 14:00:28 +08:00
24 changed files with 3078 additions and 139 deletions

8
package-lock.json generated
View file

@ -23,7 +23,7 @@
"idb-keyval": "~6.2.1",
"just-debounce-it": "~3.2.0",
"lz-string": "~1.5.0",
"masto": "~6.5.4",
"masto": "~6.6.0",
"moize": "~6.1.6",
"p-retry": "~6.2.0",
"p-throttle": "~6.1.0",
@ -5622,9 +5622,9 @@
}
},
"node_modules/masto": {
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/masto/-/masto-6.5.4.tgz",
"integrity": "sha512-hJbgQ6RlWpeA5FJ+l0CPha2zq+OlWr7bbUn09xnzNNOvBVgusTwwB898n996D3qhhr3jEpR36QUpZVxnxH+P7g==",
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/masto/-/masto-6.6.0.tgz",
"integrity": "sha512-spXDwI22M7cWaFPBmpEY+05F+9kAE0nLwzUx5YPECKsLEEQ+c+a92yo/WOnocNs/ILr9/OCKjYPyCykL6TVw+w==",
"dependencies": {
"change-case": "^4.1.2",
"events-to-async": "^2.0.1",

View file

@ -25,7 +25,7 @@
"idb-keyval": "~6.2.1",
"just-debounce-it": "~3.2.0",
"lz-string": "~1.5.0",
"masto": "~6.5.4",
"masto": "~6.6.0",
"moize": "~6.1.6",
"p-retry": "~6.2.0",
"p-throttle": "~6.1.0",

View file

@ -103,6 +103,10 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
max-width: 100%;
background-color: var(--bg-color);
overflow-anchor: auto;
&.wide {
width: 60em;
}
}
.deck.contained {
overflow: auto;
@ -2109,7 +2113,7 @@ meter.donut[hidden] {
:root .toastify {
user-select: none;
padding: 8px 16px;
border-radius: 999px;
border-radius: 44px;
pointer-events: none;
color: var(--button-text-color);
text-shadow: 0 calc(var(--hairline-width) * -1) var(--drop-shadow-color);

View file

@ -24,6 +24,7 @@ import Shortcuts from './components/shortcuts';
import NotFound from './pages/404';
import AccountStatuses from './pages/account-statuses';
import Bookmarks from './pages/bookmarks';
import Catchup from './pages/catchup';
import Favourites from './pages/favourites';
import FollowedHashtags from './pages/followed-hashtags';
import Following from './pages/following';
@ -394,7 +395,7 @@ function PrimaryRoutes({ isLoggedIn, loading }) {
const location = useLocation();
const nonRootLocation = useMemo(() => {
const { pathname } = location;
return !/^\/(login|welcome)/.test(pathname);
return !/^\/(login|welcome)/i.test(pathname);
}, [location]);
return (
@ -457,6 +458,7 @@ function SecondaryRoutes({ isLoggedIn }) {
<Route path=":id" element={<List />} />
</Route>
<Route path="/ft" element={<FollowedHashtags />} />
<Route path="/catchup" element={<Catchup />} />
</>
)}
<Route path="/:instance?/t/:hashtag" element={<Hashtag />} />

View file

@ -12,7 +12,8 @@ body.cloak,
.account-container :is(header, main > *:not(.actions)),
.account-container :is(header, main > *:not(.actions)) *,
.header-double-lines,
.account-block {
.account-block,
.post-peek-html * {
text-decoration-thickness: 1.1em;
text-decoration-line: line-through;
/* text-rendering: optimizeSpeed; */
@ -26,9 +27,10 @@ body.cloak,
.status :is(img, video, audio),
.media-post .media,
.avatar,
.avatar *,
.emoji,
.header-banner {
.header-banner,
.post-peek-media {
filter: contrast(0) !important;
background-color: #000 !important;
}

View file

@ -73,7 +73,7 @@ export const ICONS = {
() => import('@iconify-icons/mingcute/forbid-circle-line'),
'180deg',
],
flag: () => import('@iconify-icons/mingcute/flag-4-line'),
flag: () => import('@iconify-icons/mingcute/flag-1-line'),
time: () => import('@iconify-icons/mingcute/time-line'),
refresh: () => import('@iconify-icons/mingcute/refresh-2-line'),
emoji2: () => import('@iconify-icons/mingcute/emoji-2-line'),

View file

@ -9,11 +9,15 @@
--original-color: var(--link-color);
.note {
font-size: 95%;
font-size: 0.95em;
line-height: 1.4;
text-wrap: pretty;
margin-bottom: 16px;
&:empty {
display: none;
}
> *:first-child {
margin-top: 0;
padding-top: 0;

View file

@ -1238,10 +1238,38 @@ function RelatedActions({
</>
)}
</MenuConfirm>
{/* <MenuItem>
<Icon icon="flag" />
<span>Report @{username}</span>
</MenuItem> */}
<MenuItem
className="danger"
onClick={() => {
states.showReportModal = {
account: currentInfo || info,
};
}}
>
<Icon icon="flag" />
<span>Report @{username}</span>
</MenuItem>
</>
)}
{import.meta.env.DEV && currentAuthenticated && isSelf && (
<>
<MenuDivider />
<MenuItem
onClick={async () => {
const relationships =
await currentMasto.v1.accounts.relationships.fetch({
id: [accountID.current],
});
const { note } = relationships[0] || {};
if (note) {
alert(note);
console.log(note);
}
}}
>
<Icon icon="pencil" />
<span>See note</span>
</MenuItem>
</>
)}
</Menu2>

View file

@ -1558,7 +1558,7 @@ const Textarea = forwardRef((props, ref) => {
onKeyDown={(e) => {
// Get line before cursor position after pressing 'Enter'
const { key, target } = e;
if (key === 'Enter') {
if (key === 'Enter' && !(e.ctrlKey || e.metaKey)) {
try {
const { value, selectionStart } = target;
const textBeforeCursor = value.slice(0, selectionStart);

View file

@ -15,7 +15,7 @@
text-shadow: 0 1px var(--bg-blur-color);
transition: opacity 0.3s ease-out;
&:not(#columns &) {
#trending-page &:not(#columns &) {
@media (min-width: 40em) {
width: 95vw;
max-width: calc(320px * 3.3);
@ -96,6 +96,7 @@
}
article {
width: 100%;
display: flex;
flex-direction: column;
justify-content: flex-end;
@ -113,34 +114,34 @@
margin: 0 0 -16px;
padding: 0;
position: relative;
}
img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
vertical-align: top;
mask-image: linear-gradient(
to bottom,
hsl(0, 0%, 0%) 0%,
hsla(0, 0%, 0%, 0.987) 14%,
hsla(0, 0%, 0%, 0.951) 26.2%,
hsla(0, 0%, 0%, 0.896) 36.8%,
hsla(0, 0%, 0%, 0.825) 45.9%,
hsla(0, 0%, 0%, 0.741) 53.7%,
hsla(0, 0%, 0%, 0.648) 60.4%,
hsla(0, 0%, 0%, 0.55) 66.2%,
hsla(0, 0%, 0%, 0.45) 71.2%,
hsla(0, 0%, 0%, 0.352) 75.6%,
hsla(0, 0%, 0%, 0.259) 79.6%,
hsla(0, 0%, 0%, 0.175) 83.4%,
hsla(0, 0%, 0%, 0.104) 87.2%,
hsla(0, 0%, 0%, 0.049) 91.1%,
hsla(0, 0%, 0%, 0.013) 95.3%,
hsla(0, 0%, 0%, 0) 100%
);
img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
vertical-align: top;
mask-image: linear-gradient(
to bottom,
hsl(0, 0%, 0%) 0%,
hsla(0, 0%, 0%, 0.987) 14%,
hsla(0, 0%, 0%, 0.951) 26.2%,
hsla(0, 0%, 0%, 0.896) 36.8%,
hsla(0, 0%, 0%, 0.825) 45.9%,
hsla(0, 0%, 0%, 0.741) 53.7%,
hsla(0, 0%, 0%, 0.648) 60.4%,
hsla(0, 0%, 0%, 0.55) 66.2%,
hsla(0, 0%, 0%, 0.45) 71.2%,
hsla(0, 0%, 0%, 0.352) 75.6%,
hsla(0, 0%, 0%, 0.259) 79.6%,
hsla(0, 0%, 0%, 0.175) 83.4%,
hsla(0, 0%, 0%, 0.104) 87.2%,
hsla(0, 0%, 0%, 0.049) 91.1%,
hsla(0, 0%, 0%, 0.013) 95.3%,
hsla(0, 0%, 0%, 0) 100%
);
}
}
}
@ -187,5 +188,9 @@
overflow: hidden;
font-size: 90%;
}
hr {
margin: 4px 0;
}
}
}

View file

@ -56,8 +56,18 @@ function Modal({ children, onClose, onClick, class: className }) {
}}
tabIndex="-1"
onFocus={(e) => {
if (e.target === e.currentTarget) {
modalRef.current?.querySelector?.('[tabindex="-1"]')?.focus?.();
try {
if (e.target === e.currentTarget) {
const focusElement =
modalRef.current?.querySelector('[tabindex="-1"]');
const isFocusable =
getComputedStyle(focusElement)?.pointerEvents !== 'none';
if (focusElement && isFocusable) {
focusElement.focus();
}
}
} catch (err) {
console.error(err);
}
}}
>

View file

@ -15,6 +15,7 @@ import GenericAccounts from './generic-accounts';
import MediaAltModal from './media-alt-modal';
import MediaModal from './media-modal';
import Modal from './modal';
import ReportModal from './report-modal';
import ShortcutsSettings from './shortcuts-settings';
subscribe(states, (changes) => {
@ -218,6 +219,21 @@ export default function Modals() {
/>
</Modal>
)}
{!!snapStates.showReportModal && (
<Modal
onClose={() => {
states.showReportModal = false;
}}
>
<ReportModal
account={snapStates.showReportModal.account}
post={snapStates.showReportModal.post}
onClose={() => {
states.showReportModal = false;
}}
/>
</Modal>
)}
</>
);
}

View file

@ -1,6 +1,7 @@
import './nav-menu.css';
import { ControlledMenu, MenuDivider, MenuItem } from '@szhsin/react-menu';
import { memo } from 'preact/compat';
import { useEffect, useRef, useState } from 'preact/hooks';
import { useLongPress } from 'use-long-press';
import { useSnapshot } from 'valtio';
@ -83,6 +84,7 @@ function NavMenu(props) {
return results;
}
const buttonClickTS = useRef();
return (
<>
<button
@ -93,6 +95,7 @@ function NavMenu(props) {
} ${open ? 'active' : ''}`}
style={{ position: 'relative' }}
onClick={() => {
buttonClickTS.current = Date.now();
setMenuState((state) => (!state ? 'open' : undefined));
}}
onContextMenu={(e) => {
@ -124,6 +127,9 @@ function NavMenu(props) {
zIndex: 10,
},
onClick: () => {
if (Date.now() - buttonClickTS.current < 300) {
return;
}
setMenuState(undefined);
},
}}
@ -170,6 +176,10 @@ function NavMenu(props) {
<Icon icon="following" size="l" /> <span>Following</span>
</MenuLink>
)}
<MenuLink to="/catchup">
<Icon icon="history" />
<span>Catch-up</span>
</MenuLink>
<MenuLink to="/mentions">
<Icon icon="at" size="l" /> <span>Mentions</span>
</MenuLink>
@ -298,4 +308,4 @@ function NavMenu(props) {
);
}
export default NavMenu;
export default memo(NavMenu);

View file

@ -0,0 +1,196 @@
.report-modal-container {
width: 100%;
max-height: 100%;
display: flex;
flex-direction: column;
max-width: 40em;
background-color: var(--bg-color);
box-shadow: 0 16px 32px -8px var(--drop-shadow-color);
overflow-y: auto;
animation: slide-up-smooth 0.3s ease-in-out;
position: relative;
@media (min-width: 40em) {
max-height: calc(100% - 32px);
}
h1 {
margin: 0;
padding: 0;
}
.top-controls {
position: sticky;
top: var(--sai-top, 0);
z-index: 1;
background-color: var(--bg-blur-color);
backdrop-filter: blur(16px);
padding: 16px;
display: flex;
gap: 8px;
justify-content: space-between;
pointer-events: auto;
align-items: center;
h1 {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
main {
padding: 0 16px 16px;
/* display: flex;
flex-direction: column;
gap: 16px; */
}
form {
/* display: flex; */
/* flex-direction: column; */
/* gap: 16px; */
text-wrap: pretty;
input {
margin-inline: 0;
}
}
.report-preview {
background-color: var(--bg-color);
border-radius: 8px;
border: 2px dashed var(--red-color);
box-shadow: inset 0 0 16px -4px var(--red-bg-color);
overflow: auto;
max-height: 33vh;
.status {
font-size: 90%;
user-select: none;
pointer-events: none;
-webkit-touch-callout: none;
-webkit-user-drag: none;
filter: grayscale(0.5);
}
.account-block {
margin: 16px;
user-select: none;
pointer-events: none;
-webkit-touch-callout: none;
-webkit-user-drag: none;
filter: grayscale(0.5);
}
}
.rubber-stamp {
pointer-events: none;
user-select: none;
position: absolute;
right: 32px;
margin-top: -48px;
animation: rubber-stamp 0.3s ease-in both;
position: absolute;
font-weight: bold;
color: var(--red-color);
text-transform: uppercase;
letter-spacing: -0.5px;
font-size: 2em;
line-height: 1;
padding: 0.1em;
border: 0.15em solid var(--red-color);
border-radius: 0.3em;
background-color: var(--bg-blur-color);
text-align: center;
/* Noise pattern - https://css-tricks.com/making-static-noise-from-a-weird-css-gradient-bug/ */
mask-image: repeating-conic-gradient(
#000 0 0.01%,
rgba(0, 0, 0, 0.45) 0 0.02%
);
small {
display: block;
font-size: 11px;
}
}
p {
margin-block: 0.5em;
}
section {
label {
display: flex;
gap: 8px;
align-items: center;
cursor: pointer;
margin-bottom: 8px;
&:has(:checked) {
.insignificant {
color: var(--text-color);
}
}
}
> label:last-child {
margin-bottom: 0;
}
}
.report-categories {
label {
align-items: flex-start;
}
.report-rules {
margin-left: 1.75em;
}
}
.report-comment {
display: flex;
gap: 8px;
align-items: flex-start;
margin-top: 2em;
flex-wrap: wrap;
p {
margin: 0;
padding: 8px 0 0;
flex-shrink: 0;
label {
margin-bottom: 0;
}
}
textarea {
flex-grow: 1;
resize: vertical;
}
}
footer {
margin-top: 2em;
display: flex;
gap: 8px;
align-items: center;
button {
border-radius: 8px !important;
align-self: stretch;
}
}
}
@keyframes rubber-stamp {
0% {
transform: rotate(-20deg) scale(5);
opacity: 0;
}
100% {
transform: rotate(-20deg) scale(1);
opacity: 1;
}
}

View file

@ -0,0 +1,298 @@
import './report-modal.css';
import { Fragment } from 'preact';
import { useMemo, useRef, useState } from 'preact/hooks';
import { api } from '../utils/api';
import showToast from '../utils/show-toast';
import { getCurrentInstance } from '../utils/store-utils';
import AccountBlock from './account-block';
import Icon from './icon';
import Loader from './loader';
import Status from './status';
// NOTE: `dislike` hidden for now, it's actually not used for reporting
// Mastodon shows another screen for unfollowing, muting or blocking instead of reporting
const CATEGORIES = [, /*'dislike'*/ 'spam', 'legal', 'violation', 'other'];
// `violation` will be set if there are `rule_ids[]`
const CATEGORIES_INFO = {
// dislike: {
// label: 'Dislike',
// description: 'Not something you want to see',
// },
spam: {
label: 'Spam',
description: 'Malicious links, fake engagement, or repetitive replies',
},
legal: {
label: 'Illegal',
description: "Violates the law of your or the server's country",
},
violation: {
label: 'Server rule violation',
description: 'Breaks specific server rules',
stampLabel: 'Violation',
},
other: {
label: 'Other',
description: "Issue doesn't fit other categories",
excludeStamp: true,
},
};
function ReportModal({ account, post, onClose }) {
const { masto } = api();
const [uiState, setUIState] = useState('default');
const [username, domain] = account.acct.split('@');
const [rules, currentDomain] = useMemo(() => {
const { rules, domain } = getCurrentInstance();
return [rules || [], domain];
});
const [selectedCategory, setSelectedCategory] = useState(null);
const [showRules, setShowRules] = useState(false);
const rulesRef = useRef(null);
const [hasRules, setHasRules] = useState(false);
return (
<div class="report-modal-container">
<div class="top-controls">
<h1>{post ? 'Report Post' : `Report @${username}`}</h1>
<button
type="button"
class="plain4 small"
disabled={uiState === 'loading'}
onClick={() => onClose()}
>
<Icon icon="x" size="xl" />
</button>
</div>
<main>
<div class="report-preview">
{post ? (
<Status status={post} size="s" previewMode />
) : (
<AccountBlock
account={account}
avatarSize="xxl"
useAvatarStatic
showStats
showActivity
/>
)}
</div>
{!!selectedCategory &&
!CATEGORIES_INFO[selectedCategory].excludeStamp && (
<span
class="rubber-stamp"
key={selectedCategory}
aria-hidden="true"
>
{CATEGORIES_INFO[selectedCategory].stampLabel ||
CATEGORIES_INFO[selectedCategory].label}
<small>Pending review</small>
</span>
)}
<form
onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.target);
const entries = Object.fromEntries(formData.entries());
console.log('ENTRIES', entries);
let { category, comment, forward } = entries;
if (!comment) comment = undefined;
if (forward === 'on') forward = true;
const ruleIds =
category === 'violation'
? Object.entries(entries)
.filter(([key]) => key.startsWith('rule_ids'))
.map(([key, value]) => value)
: undefined;
const params = {
category,
comment,
forward,
ruleIds,
};
console.log('PARAMS', params);
setUIState('loading');
(async () => {
try {
await masto.v1.reports.create({
accountId: account.id,
statusIds: post?.id ? [post.id] : undefined,
category,
comment,
ruleIds,
forward,
});
setUIState('success');
showToast(post ? 'Post reported' : 'Profile reported');
onClose();
} catch (error) {
console.error(error);
setUIState('error');
showToast(
error?.message ||
(post
? 'Unable to report post'
: 'Unable to report profile'),
);
}
})();
}}
>
<p>
{post
? `What's the issue with this post?`
: `What's the issue with this profile?`}
</p>
<section class="report-categories">
{CATEGORIES.map((category) =>
category === 'violation' && !rules?.length ? null : (
<Fragment key={category}>
<label class="report-category">
<input
type="radio"
name="category"
value={category}
required
disabled={uiState === 'loading'}
onChange={(e) => {
setSelectedCategory(e.target.value);
setShowRules(e.target.value === 'violation');
}}
/>
<span>
{CATEGORIES_INFO[category].label} &nbsp;
<small class="ib insignificant">
{CATEGORIES_INFO[category].description}
</small>
</span>
</label>
{category === 'violation' && !!rules?.length && (
<div
class="shazam-container no-animation"
hidden={!showRules}
>
<div class="shazam-container-inner">
<div class="report-rules" ref={rulesRef}>
{rules.map((rule, i) => (
<label class="report-rule" key={rule.id}>
<input
type="checkbox"
name={`rule_ids[${i}]`}
value={rule.id}
required={showRules && !hasRules}
disabled={uiState === 'loading'}
onChange={(e) => {
const { checked } = e.target;
if (checked) {
setHasRules(true);
} else {
const checkedInputs =
rulesRef.current.querySelectorAll(
'input:checked',
);
if (!checkedInputs.length) {
setHasRules(false);
}
}
}}
/>
<span>{rule.text}</span>
</label>
))}
</div>
</div>
</div>
)}
</Fragment>
),
)}
</section>
<section class="report-comment">
<p>
<label for="report-comment">Additional info</label>
</p>
<textarea
maxlength="1000"
rows="1"
name="comment"
id="report-comment"
disabled={uiState === 'loading'}
/>
</section>
<section>
{domain !== currentDomain && (
<p>
<label>
<input
type="checkbox"
switch
name="forward"
disabled={uiState === 'loading'}
/>{' '}
<span>
Forward to <i>{domain}</i>
</span>
</label>
</p>
)}
</section>
<footer>
<button type="submit" disabled={uiState === 'loading'}>
Send Report
</button>{' '}
<button
type="submit"
class="plain2"
disabled={uiState === 'loading'}
onClick={async () => {
try {
await masto.v1.accounts.$select(account.id).mute(); // Infinite duration
showToast(`Muted ${username}`);
} catch (e) {
console.error(e);
showToast(`Unable to mute ${username}`);
}
// onSubmit will still run
}}
>
Send Report <small class="ib">+ Mute profile</small>
</button>{' '}
<button
type="submit"
class="plain2"
disabled={uiState === 'loading'}
onClick={async () => {
try {
await masto.v1.accounts.$select(account.id).block();
showToast(`Blocked ${username}`);
} catch (e) {
console.error(e);
showToast(`Unable to block ${username}`);
}
// onSubmit will still run
}}
>
Send Report <small class="ib">+ Block profile</small>
</button>
<Loader hidden={uiState !== 'loading'} />
</footer>
</form>
</main>
</div>
);
}
export default ReportModal;

View file

@ -153,6 +153,15 @@
}
#import-export-container section p {
margin: 8px 0;
&.field-button {
display: flex;
gap: 8px;
button {
flex-shrink: 0;
}
}
}
#import-export-container section details > summary {
cursor: pointer;
@ -182,3 +191,14 @@
font-size: 90%;
flex-shrink: 0;
}
#import-export-container {
footer {
font-size: 90%;
color: var(--text-insignificant-color);
.icon {
vertical-align: text-bottom;
}
}
}

View file

@ -17,6 +17,7 @@ import { fetchFollowedTags } from '../utils/followed-tags';
import pmem from '../utils/pmem';
import showToast from '../utils/show-toast';
import states from '../utils/states';
import store from '../utils/store';
import AsyncText from './AsyncText';
import Icon from './icon';
@ -716,6 +717,7 @@ function ShortcutForm({
}
function ImportExport({ shortcuts, onClose }) {
const { masto } = api();
const shortcutsStr = useMemo(() => {
if (!shortcuts) return '';
if (!shortcuts.filter(Boolean).length) return '';
@ -754,6 +756,8 @@ function ImportExport({ shortcuts, onClose }) {
}, [importShortcutStr]);
const hasCurrentSettings = states.shortcuts.length > 0;
const shortcutsImportFieldRef = useRef();
return (
<div id="import-export-container" class="sheet">
{!!onClose && (
@ -772,8 +776,9 @@ function ImportExport({ shortcuts, onClose }) {
<Icon icon="arrow-down-circle" size="l" class="insignificant" />{' '}
<span>Import</span>
</h3>
<p>
<p class="field-button">
<input
ref={shortcutsImportFieldRef}
type="text"
name="import"
placeholder="Paste shortcuts here"
@ -782,6 +787,53 @@ function ImportExport({ shortcuts, onClose }) {
setImportShortcutStr(e.target.value);
}}
/>
{states.settings.shortcutSettingsCloudImportExport && (
<button
type="button"
class="plain2 small"
disabled={importUIState === 'cloud-downloading'}
onClick={async () => {
setImportUIState('cloud-downloading');
const currentAccount = store.session.get('currentAccount');
showToast(
'Downloading saved shortcuts from instance server…',
);
try {
const relationships =
await masto.v1.accounts.relationships.fetch({
id: [currentAccount],
});
const relationship = relationships[0];
if (relationship) {
const { note = '' } = relationship;
if (
/<phanpy-shortcuts-settings>(.*)<\/phanpy-shortcuts-settings>/.test(
note,
)
) {
const settings = note.match(
/<phanpy-shortcuts-settings>(.*)<\/phanpy-shortcuts-settings>/,
)[1];
const { v, dt, data } = JSON.parse(settings);
shortcutsImportFieldRef.current.value = data;
shortcutsImportFieldRef.current.dispatchEvent(
new Event('input'),
);
}
}
setImportUIState('default');
} catch (e) {
console.error(e);
setImportUIState('error');
showToast('Unable to download shortcuts');
}
}}
title="Download shortcuts from instance server"
>
<Icon icon="cloud" />
<Icon icon="arrow-down" />
</button>
)}
</p>
{!!parsedImportShortcutStr &&
Array.isArray(parsedImportShortcutStr) && (
@ -991,8 +1043,64 @@ function ImportExport({ shortcuts, onClose }) {
<Icon icon="share" /> <span>Share</span>
</button>
)}{' '}
{states.settings.shortcutSettingsCloudImportExport && (
<button
type="button"
class="plain2"
disabled={importUIState === 'cloud-uploading'}
onClick={async () => {
setImportUIState('cloud-uploading');
const currentAccount = store.session.get('currentAccount');
try {
const relationships =
await masto.v1.accounts.relationships.fetch({
id: [currentAccount],
});
const relationship = relationships[0];
if (relationship) {
const { note = '' } = relationship;
// const newNote = `${note}\n\n\n$<phanpy-shortcuts-settings>{shortcutsStr}</phanpy-shortcuts-settings>`;
let newNote = '';
if (
/<phanpy-shortcuts-settings>(.*)<\/phanpy-shortcuts-settings>/.test(
note,
)
) {
const settingsJSON = JSON.stringify({
v: '1', // version
dt: Date.now(), // datetime stamp
data: shortcutsStr, // shortcuts settings string
});
newNote = note.replace(
/<phanpy-shortcuts-settings>(.*)<\/phanpy-shortcuts-settings>/,
`<phanpy-shortcuts-settings>${settingsJSON}</phanpy-shortcuts-settings>`,
);
} else {
newNote = `${note}\n\n\n<phanpy-shortcuts-settings>${settingsJSON}</phanpy-shortcuts-settings>`;
}
showToast('Saving shortcuts to instance server…');
await masto.v1.accounts
.$select(currentAccount)
.note.create({
comment: newNote,
});
setImportUIState('default');
showToast('Shortcuts saved');
}
} catch (e) {
console.error(e);
setImportUIState('error');
showToast('Unable to save shortcuts');
}
}}
title="Sync to instance server"
>
<Icon icon="cloud" />
<Icon icon="arrow-up" />
</button>
)}{' '}
{shortcutsStr.length > 0 && (
<small class="insignificant">
<small class="insignificant ib">
{shortcutsStr.length} characters
</small>
)}
@ -1008,6 +1116,14 @@ function ImportExport({ shortcuts, onClose }) {
</details>
)}
</section>
{states.settings.shortcutSettingsCloudImportExport && (
<footer>
<p>
<Icon icon="cloud" /> Import/export settings from/to instance
server (Very experimental)
</p>
</footer>
)}
</main>
</div>
);

View file

@ -559,12 +559,11 @@ function Status({
if (reblogged) {
const newStatus = await masto.v1.statuses.$select(id).unreblog();
saveStatus(newStatus, instance);
return true;
} else {
const newStatus = await masto.v1.statuses.$select(id).reblog();
saveStatus(newStatus, instance);
return true;
}
return true;
} catch (e) {
console.error(e);
// Revert optimistism
@ -575,7 +574,8 @@ function Status({
const favouriteStatus = async () => {
if (!sameInstance || !authenticated) {
return alert(unauthInteractionErrorMessage);
alert(unauthInteractionErrorMessage);
return false;
}
try {
// Optimistic
@ -591,16 +591,31 @@ function Status({
const newStatus = await masto.v1.statuses.$select(id).favourite();
saveStatus(newStatus, instance);
}
return true;
} catch (e) {
console.error(e);
// Revert optimistism
states.statuses[sKey] = status;
return false;
}
};
const favouriteStatusNotify = async () => {
try {
const done = await favouriteStatus();
if (!isSizeLarge && done) {
showToast(
favourited
? `Unliked @${username || acct}'s post`
: `Liked @${username || acct}'s post`,
);
}
} catch (e) {}
};
const bookmarkStatus = async () => {
if (!sameInstance || !authenticated) {
return alert(unauthInteractionErrorMessage);
alert(unauthInteractionErrorMessage);
return false;
}
try {
// Optimistic
@ -615,12 +630,26 @@ function Status({
const newStatus = await masto.v1.statuses.$select(id).bookmark();
saveStatus(newStatus, instance);
}
return true;
} catch (e) {
console.error(e);
// Revert optimistism
states.statuses[sKey] = status;
return false;
}
};
const bookmarkStatusNotify = async () => {
try {
const done = await bookmarkStatus();
if (!isSizeLarge && done) {
showToast(
bookmarked
? `Unbookmarked @${username || acct}'s post`
: `Bookmarked @${username || acct}'s post`,
);
}
} catch (e) {}
};
const differentLanguage =
!!language &&
@ -752,18 +781,7 @@ function Status({
</span>
</MenuConfirm>
<MenuItem
onClick={() => {
try {
favouriteStatus();
if (!isSizeLarge) {
showToast(
favourited
? `Unliked @${username || acct}'s post`
: `Liked @${username || acct}'s post`,
);
}
} catch (e) {}
}}
onClick={favouriteStatusNotify}
className={`menu-favourite ${favourited ? 'checked' : ''}`}
>
<Icon icon="heart" />
@ -776,18 +794,7 @@ function Status({
</span>
</MenuItem>
<MenuItem
onClick={() => {
try {
bookmarkStatus();
if (!isSizeLarge) {
showToast(
bookmarked
? `Unbookmarked @${username || acct}'s post`
: `Bookmarked @${username || acct}'s post`,
);
}
} catch (e) {}
}}
onClick={bookmarkStatusNotify}
className={`menu-bookmark ${bookmarked ? 'checked' : ''}`}
>
<Icon icon="bookmark" />
@ -1040,6 +1047,23 @@ function Status({
)}
</div>
)}
{!isSelf && isSizeLarge && (
<>
<MenuDivider />
<MenuItem
className="danger"
onClick={() => {
states.showReportModal = {
account: status.account,
post: status,
};
}}
>
<Icon icon="flag" />
<span>Report post</span>
</MenuItem>
</>
)}
</>
);
@ -1085,42 +1109,12 @@ function Status({
const rRef = useHotkeys('r, shift+r', replyStatus, {
enabled: hotkeysEnabled,
});
const fRef = useHotkeys(
'f, l',
() => {
try {
favouriteStatus();
if (!isSizeLarge) {
showToast(
favourited
? `Unliked @${username || acct}'s post`
: `Liked @${username || acct}'s post`,
);
}
} catch (e) {}
},
{
enabled: hotkeysEnabled,
},
);
const dRef = useHotkeys(
'd',
() => {
try {
bookmarkStatus();
if (!isSizeLarge) {
showToast(
bookmarked
? `Unbookmarked @${username || acct}'s post`
: `Bookmarked @${username || acct}'s post`,
);
}
} catch (e) {}
},
{
enabled: hotkeysEnabled,
},
);
const fRef = useHotkeys('f, l', favouriteStatusNotify, {
enabled: hotkeysEnabled,
});
const dRef = useHotkeys('d', bookmarkStatusNotify, {
enabled: hotkeysEnabled,
});
const bRef = useHotkeys(
'shift+b',
() => {
@ -1420,16 +1414,7 @@ function Status({
icon="heart"
iconSize="m"
count={favouritesCount}
onClick={() => {
try {
favouriteStatus();
showToast(
favourited
? `Unliked @${username || acct}'s post`
: `Liked @${username || acct}'s post`,
);
} catch (e) {}
}}
onClick={favouriteStatusNotify}
/>
<button
type="button"

832
src/pages/catchup.css Normal file
View file

@ -0,0 +1,832 @@
#catchup-page {
transform: none;
padding-bottom: 0 !important;
.deck {
background-color: var(--bg-faded-color);
}
/* Hide the shortcuts nav + adjustments */
~ :is(#shortcuts, #compose-button) {
display: none;
}
.timeline-deck {
margin-top: 0 !important;
}
header {
/* --margin-top: 8px !important; */
position: static;
}
h1 sup {
font-size: 12px;
text-transform: uppercase;
font-weight: 500;
color: var(--text-insignificant-color);
}
.catchup-start {
padding: 16px;
padding-top: 15vh;
text-align: center;
max-width: 40em;
margin-inline: auto;
.catchup-info {
animation: appear 0.3s ease-out;
display: flex;
gap: 0.25em;
align-items: center;
justify-content: center;
}
}
.catchup-prev {
margin: 2em auto;
padding: 1em;
width: fit-content;
color: var(--text-insignificant-color);
border-top: 1px solid var(--bg-color);
ul,
ul li {
margin: 0;
padding: 0;
list-style: none;
}
ul li {
display: flex;
margin-bottom: 8px;
align-items: center;
gap: 8px;
text-align: left;
justify-content: space-between;
a {
display: flex;
gap: 0.25em;
align-items: center;
}
}
}
}
.catchup-form {
display: inline-flex;
align-items: center;
gap: 16px;
padding: 16px 16px;
background-color: var(--link-bg-color);
border-radius: 32px;
flex-wrap: wrap;
* {
flex-grow: 1;
}
input[type='range'] {
accent-color: var(--link-color);
direction: rtl;
}
}
.catchup-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
gap: 8px;
aside {
display: flex;
align-items: center;
gap: 8px;
font-size: 90%;
button[hidden] {
display: inline;
opacity: 0;
pointer-events: none;
}
}
}
.catchup-posts-viz-bar {
margin: 0 16px;
border-radius: 3px;
border: 1px solid var(--bg-color);
display: flex;
gap: 1px;
pointer-events: none;
justify-content: stretch;
height: 3px;
&:has(.post-dot:nth-child(320)) {
gap: 0;
}
.post-dot {
display: block;
width: 100%;
height: 3px;
border-radius: 3px;
opacity: 0.5;
background-color: var(--link-color);
transition: 0.25s ease-in-out;
transition-property: opacity, transform;
&.post-dot-highlight {
opacity: 1;
}
}
&:has(.post-dot:not(.post-dot-highlight)) .post-dot-highlight {
/* transform: scaleY(2); */
transform: scale3d(1, 2, 1);
}
}
.catchup-filters {
padding: 8px 16px;
display: flex;
/* flex-wrap: wrap; */
gap: 8px;
align-items: center;
overflow-x: auto;
overflow-y: hidden;
max-width: 100%;
mask-image: linear-gradient(
to right,
transparent,
black 16px calc(100% - 16px),
transparent
);
padding-inline-end: 25%;
.filter-label {
text-transform: uppercase;
font-size: 12px;
font-weight: 500;
color: var(--text-insignificant-color);
}
label {
font-size: 80%;
white-space: nowrap;
cursor: pointer;
user-select: none;
color: var(--text-insignificant-color);
position: relative;
select {
/* appearance: none;
background-color: var(--bg-faded-color);
color: var(--link-color);
border: 0;
border-radius: 12px; */
padding: 4px;
margin: 0;
}
input[type] {
left: 0;
position: absolute;
opacity: 0;
pointer-events: none;
}
&.filter-cat {
padding: 8px 12px;
background-color: var(--bg-blur-color);
border-radius: 24px;
display: inline-block;
display: flex;
align-items: center;
gap: 4px;
&:is(:hover, :focus) {
background-color: var(--link-bg-color);
}
&:has(:checked) {
color: var(--link-color);
background-color: var(--link-bg-color);
box-shadow: inset 0 0 0 2px var(--link-color);
}
.count {
font-size: 70%;
margin-left: 4px;
background-color: var(--bg-color);
padding: 4px 6px;
border-radius: 12px;
display: inline-block;
}
}
&.filter-author {
flex-direction: column;
padding: 0;
background-color: transparent;
position: relative;
width: 50px;
/* transition: filter 0.15s ease; */
.avatar {
margin-bottom: 2px;
/* transition: box-shadow 0.15s ease; */
}
&:is(:hover, :focus) {
filter: none;
.avatar {
box-shadow: 0 0 0 0.5px var(--bg-color),
0 0 0 3px var(--link-faded-color) !important;
img {
filter: none;
}
}
.count {
color: var(--text-color);
}
}
.avatar {
&.has-alpha {
border-radius: 2px;
}
img {
transition: filter 0.15s ease;
}
}
&:has(:checked) {
box-shadow: none;
filter: none;
.avatar {
box-shadow: 0 0 0 1px var(--bg-color), 0 0 0 3px var(--link-color);
}
.username {
color: var(--link-color);
font-weight: 500;
}
.count {
color: var(--link-color);
border-color: var(--link-color);
box-shadow: 0 0 0 1px var(--link-color);
}
}
.count {
position: absolute;
right: -4px;
top: -4px;
font-size: 10px;
background-color: var(--bg-color);
border-radius: 12px;
display: inline-block;
border: 1px solid var(--link-faded-color);
padding: 0 2px;
min-width: 16px;
min-height: 16px;
text-align: center;
line-height: 14px;
}
.username {
display: block;
width: 100%;
overflow: hidden;
text-align: center;
mask-image: linear-gradient(
to right,
black calc(100% - 0.5em),
transparent 100%
);
}
}
}
&:has(.filter-author :checked)
.filter-author:not(:has(:checked)):not(:is(:hover, :focus)) {
.avatar img {
filter: grayscale(1) contrast(2) opacity(0.5);
}
}
.radio-field-group {
display: flex;
border: 0;
padding: 0;
margin: 0;
border-radius: 4px;
overflow: hidden;
gap: 1px;
label {
padding: 4px 8px;
line-height: 2em;
min-width: 32px;
text-align: center;
background-color: var(--bg-blur-color);
margin: 0;
cursor: pointer;
&:is(:hover, :focus) {
background-color: var(--link-bg-color);
}
&:has(:checked) {
font-weight: 500;
color: var(--text-color);
background-color: var(--link-bg-color);
box-shadow: inset 0 -2px 0 var(--link-color);
}
&:has(:disabled) {
opacity: 0.5;
cursor: not-allowed;
}
}
}
}
.catchup-list {
margin: 0;
padding: 0;
list-style: none;
/* background-color: var(--bg-color); */
border-top: var(--hairline-width) solid var(--bg-faded-color);
--corner-radius: 8px;
@media (min-width: 40em) {
border-radius: var(--corner-radius);
/* border: var(--hairline-width) solid var(--outline-color); */
> li {
&:first-child > a {
border-top-left-radius: var(--corner-radius);
border-top-right-radius: var(--corner-radius);
}
&:last-child > a {
border-bottom-left-radius: var(--corner-radius);
border-bottom-right-radius: var(--corner-radius);
}
}
}
> li {
margin: 0 0 1px;
padding: 0;
list-style: none;
/* border-bottom: var(--hairline-width) solid var(--outline-color); */
&.separator {
height: 16px;
pointer-events: none;
@media (min-width: 40em) {
height: 32px;
}
}
@media (min-width: 40em) {
&.separator + li a {
border-top-left-radius: var(--corner-radius);
border-top-right-radius: var(--corner-radius);
}
&:has(+ .separator) a {
border-bottom-left-radius: var(--corner-radius);
border-bottom-right-radius: var(--corner-radius);
}
}
> a {
background-color: var(--bg-color);
text-decoration: none;
color: inherit;
display: block;
/* transition: background-color 0.3s ease; */
&:is(:hover, :focus) {
position: relative;
z-index: 1;
background-color: var(--bg-faded-color);
box-shadow: 0 8px 16px -8px var(--drop-shadow-color),
inset 0 1px var(--bg-color);
outline: 1px solid var(--outline-color);
text-shadow: 0 1px var(--bg-color);
}
&:active {
filter: brightness(0.95);
box-shadow: none;
text-shadow: none;
}
&:visited {
color: var(--outline-color);
*,
.post-peek-html * a[href] {
color: var(--outline-color) !important;
* {
color: var(--outline-color) !important;
}
}
}
}
}
.post-line {
border-radius: inherit;
animation: appear-smooth 0.3s ease-in-out both;
--pad: 16px;
min-height: 44px;
padding: var(--pad);
column-gap: calc(0.5 * var(--pad));
row-gap: 4px;
width: 100%;
/* display: flex;
flex-direction: column; */
display: grid;
grid-template-columns: 1fr auto;
grid-template-rows: auto auto;
grid-template-areas:
'author meta'
'content content';
/* align-items: center; */
background-image: linear-gradient(
160deg,
var(--post-bg-color),
transparent min(80px, 50%)
);
/* background-image: linear-gradient(
90deg,
var(--post-bg-color),
var(--post-bg-color) 8px,
transparent 8px
); */
@media (min-width: 40em) {
/* flex-direction: row;
align-items: center; */
grid-template-columns: auto 1fr auto;
grid-template-rows: 1fr;
grid-template-areas: 'author content meta';
}
&.reblog {
--post-bg-color: var(--reblog-faded-color);
}
&.group {
--post-bg-color: var(--group-faded-color);
}
&.reply-to {
--post-bg-color: var(--reply-to-faded-color);
}
&.followed-tags {
--post-bg-color: var(--hashtag-faded-color);
}
&.filtered {
filter: grayscale(1);
background-image: none;
.post-author {
opacity: 0.5;
}
}
&.visibility-direct {
--yellow-stripes: repeating-linear-gradient(
-45deg,
var(--reply-to-faded-color),
var(--reply-to-faded-color) 10px,
var(--reply-to-faded-color) 10px,
transparent 10px,
transparent 20px
);
background-image: var(--yellow-stripes);
}
.post-reblog-avatar {
display: flex;
gap: 4px;
align-items: center;
flex-shrink: 0;
.icon {
color: var(--reblog-color);
}
}
.post-author {
grid-area: author;
flex-shrink: 0;
white-space: nowrap;
overflow: hidden;
mask-image: linear-gradient(
to right,
black calc(100% - 1em),
transparent 100%
);
@media (min-width: 40em) {
--width: 25vw;
width: var(--width);
min-width: 9em;
max-width: 13em;
}
b {
font-weight: normal;
/* font-weight: 500; */
opacity: 0.7;
}
i {
opacity: 0.5;
}
}
}
> li:nth-child(10) ~ li .post-line {
animation: none;
}
.post-peek {
grid-area: content;
display: flex;
flex: 1;
column-gap: 8px;
row-gap: 4px;
align-self: stretch;
/* align-items: center; */
/* margin-left: 24px; */
flex-direction: row-reverse;
flex-wrap: wrap;
justify-content: flex-end;
/* CLOAK - uncomment when taking screenshots */
/* text-decoration-thickness: 1.1em;
text-decoration-line: line-through;
text-rendering: optimizeSpeed;
filter: opacity(0.5);
pointer-events: none;
img {
filter: contrast(0) !important;
background-color: #000 !important;
} */
.post-peek-content {
flex-shrink: 1;
flex-grow: 1;
flex-basis: 20em;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
line-height: 1.3;
opacity: 0.9;
/* font-size: 0.9em; */
text-wrap: balance;
&:empty {
display: none;
}
.post-peek-html {
pointer-events: none;
* {
margin: 0;
padding: 0;
display: inline;
white-space: normal;
}
pre,
code {
font-size: 0.9em;
color: var(--green-color);
}
strong,
b {
font-weight: 500;
}
br {
content: ' ';
}
/* all block level elements */
p,
div,
blockquote,
h1,
h2,
h3,
h4,
h5,
h6,
li,
pre,
br {
&:after {
content: ' ';
}
}
br:after {
font-size: 0.75em;
content: ' ↵ ';
opacity: 0.35;
}
.ellipsis:after {
content: '...';
}
.invisible {
display: none;
}
/* Links are not clickable, so remove the underlines */
a {
text-decoration: none;
text-decoration-color: transparent;
color: var(--link-text-color);
}
}
.post-peek-spoiler {
line-height: 1.5;
border-radius: 1em;
padding-inline: 0.5em;
border: 1px dashed var(--button-bg-color);
.icon {
vertical-align: middle;
color: var(--button-bg-color);
}
}
.post-peek-filtered {
border-radius: 1em;
padding-inline: 0.5em;
border: 1px dashed var(--outline-hover-color);
.icon {
vertical-align: middle;
color: var(--outline-hover-color);
}
}
}
.post-peek-post-content {
flex-shrink: 0;
display: flex;
gap: 4px;
align-self: center;
transition: transform 0.05s ease-in-out;
&:empty {
display: none;
}
&:has(.post-peek-media):hover {
transform: scale(1.5);
}
img {
border-radius: 2px;
outline: var(--hairline-width) solid var(--outline-color);
vertical-align: middle;
object-fit: cover;
box-shadow: 0 0 0 1px var(--outline-color);
object-fit: cover;
transition: transform 0.05s ease-in-out;
background-color: var(--bg-color);
&:hover {
transform: scale(1.5);
position: relative;
z-index: 1;
animation: position-object 5s ease-in-out 5;
/* @media (min-width: 40em) and (min-height: 600px) {
transform: scale(3);
} */
}
}
@media (max-width: 40em) {
&:has(.post-peek-media),
.post-peek-media:first-child img {
transform-origin: left center;
}
}
.post-peek-faux-media {
width: 48px;
height: 48px;
border-radius: 4px;
background-color: var(--bg-faded-color);
display: inline-flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 0 1px var(--outline-color);
}
.post-peek-card img {
outline: 3px double var(--link-faded-color);
}
}
.post-peek-tag {
display: inline-block;
border-radius: 4px;
font-size: 12px;
color: var(--text-insignificant-color);
font-weight: 500;
text-transform: uppercase;
border: 1px solid var(--outline-color);
padding: 2px !important;
align-self: center;
background-color: var(--bg-faded-color);
line-height: 1;
&.post-peek-poll {
display: inline-flex;
align-items: center;
gap: 2px;
flex-direction: column;
color: var(--text-color);
}
&.post-peek-thread {
font-size: 10px;
/* padding: 2px 4px; */
font-weight: 700;
background-color: var(--bg-color);
color: var(--reply-to-text-color) !important;
border-color: var(--reply-to-color);
background-image: repeating-linear-gradient(
-70deg,
transparent,
transparent 3px,
var(--reply-to-faded-color) 3px,
var(--reply-to-faded-color) 4px
);
}
}
}
> li > a:is(:hover, :focus) .post-peek-content {
opacity: 1;
}
.post-meta {
grid-area: meta;
font-size: 90%;
color: var(--text-insignificant-color);
white-space: nowrap;
display: flex;
align-items: center;
align-self: flex-start;
column-gap: 8px;
}
.post-stats {
opacity: 0;
display: inline-flex;
gap: 2px;
align-items: center;
transform: translateX(4px);
/* transition: all 0.25s ease-out; */
&:empty {
display: none;
}
}
.post-line:hover .post-stats {
opacity: 1;
transform: translateX(0);
}
+ footer {
min-height: 15vh;
color: var(--text-insignificant-color);
padding-block: 15vh;
text-align: center;
}
}

1371
src/pages/catchup.jsx Normal file

File diff suppressed because it is too large Load diff

View file

@ -464,6 +464,39 @@ function Settings({ onClose }) {
</div>
</li>
)}
{authenticated && (
<li>
<label>
<input
type="checkbox"
checked={
snapStates.settings.shortcutSettingsCloudImportExport
}
onChange={(e) => {
states.settings.shortcutSettingsCloudImportExport =
e.target.checked;
}}
/>{' '}
"Cloud" import/export for shortcuts settings{' '}
<Icon icon="cloud" class="more-insignificant" />
</label>
<div class="sub-section insignificant">
<small>
Very experimental.
<br />
Stored in your own profiles notes. Profile (private) notes
are mainly used for other profiles, and hidden for own
profile.
</small>
</div>
<div class="sub-section insignificant">
<small>
Note: This feature uses currently-logged-in instance server
API.
</small>
</div>
</li>
)}
<li>
<label>
<input

View file

@ -1,4 +1,4 @@
import './trending.css';
import '../components/links-bar.css';
import { MenuItem } from '@szhsin/react-menu';
import { getBlurHashAverageColor } from 'fast-blurhash';

View file

@ -9,20 +9,20 @@ import {
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),
};
function initDB(dbName, storeName) {
const store = createStore(dbName, storeName);
return {
set: (key, val) => set(key, val, store),
get: (key) => get(key, store),
getMany: (keys) => getMany(keys, store),
del: (key) => del(key, store),
delMany: (keys) => delMany(keys, store),
clear: () => clear(store),
keys: () => keys(store),
};
}
export default {
drafts,
drafts: initDB('drafts-db', 'drafts-store'),
catchup: initDB('catchup-db', 'catchup-store'),
};

View file

@ -52,6 +52,7 @@ const states = proxy({
showGenericAccounts: false,
showMediaAlt: false,
showEmbedModal: false,
showReportModal: false,
// Shortcuts
shortcuts: [],
// Settings
@ -64,6 +65,7 @@ const states = proxy({
contentTranslationTargetLanguage: null,
contentTranslationHideLanguages: [],
contentTranslationAutoInline: false,
shortcutSettingsCloudImportExport: false,
mediaAltGenerator: false,
cloakMode: false,
},
@ -93,6 +95,8 @@ export function initStates() {
store.account.get('settings-contentTranslationHideLanguages') || [];
states.settings.contentTranslationAutoInline =
store.account.get('settings-contentTranslationAutoInline') ?? false;
states.settings.shortcutSettingsCloudImportExport =
store.account.get('settings-shortcutSettingsCloudImportExport') ?? false;
states.settings.mediaAltGenerator =
store.account.get('settings-mediaAltGenerator') ?? false;
states.settings.cloakMode = store.account.get('settings-cloakMode') ?? false;
@ -120,6 +124,9 @@ subscribe(states, (changes) => {
if (path.join('.') === 'settings.contentTranslationAutoInline') {
store.account.set('settings-contentTranslationAutoInline', !!value);
}
if (path.join('.') === 'settings.shortcutSettingsCloudImportExport') {
store.account.set('settings-shortcutSettingsCloudImportExport', !!value);
}
if (path.join('.') === 'settings.contentTranslationTargetLanguage') {
console.log('SET', value);
store.account.set('settings-contentTranslationTargetLanguage', value);