Merge branch 'develop' into feat/enable-websocket-connection
This commit is contained in:
3
app/assets/icons/ic-account-circle.svg
Normal file
3
app/assets/icons/ic-account-circle.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.0001 16.0001C7.91675 16.0001 6.07508 14.9334 5.00008 13.3334C5.02508 11.6667 8.33342 10.7501 10.0001 10.7501C11.6667 10.7501 14.9751 11.6667 15.0001 13.3334C13.9251 14.9334 12.0834 16.0001 10.0001 16.0001ZM10.0001 4.16675C10.6631 4.16675 11.299 4.43014 11.7679 4.89898C12.2367 5.36782 12.5001 6.00371 12.5001 6.66675C12.5001 7.32979 12.2367 7.96568 11.7679 8.43452C11.299 8.90336 10.6631 9.16675 10.0001 9.16675C9.33704 9.16675 8.70116 8.90336 8.23232 8.43452C7.76347 7.96568 7.50008 7.32979 7.50008 6.66675C7.50008 6.00371 7.76347 5.36782 8.23232 4.89898C8.70116 4.43014 9.33704 4.16675 10.0001 4.16675ZM10.0001 1.66675C8.90573 1.66675 7.8221 1.8823 6.81105 2.30109C5.80001 2.71987 4.88135 3.3337 4.10753 4.10753C2.54472 5.67033 1.66675 7.78995 1.66675 10.0001C1.66675 12.2102 2.54472 14.3298 4.10753 15.8926C4.88135 16.6665 5.80001 17.2803 6.81105 17.6991C7.8221 18.1179 8.90573 18.3334 10.0001 18.3334C12.2102 18.3334 14.3298 17.4554 15.8926 15.8926C17.4554 14.3298 18.3334 12.2102 18.3334 10.0001C18.3334 5.39175 14.5834 1.66675 10.0001 1.66675Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -1,3 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.49995 17.0167L2.32495 11.8417L4.68328 9.48332L7.49995 12.3083L15.7333 4.06665L18.0916 6.42498L7.49995 17.0167Z" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 230 B After Width: | Height: | Size: 207 B |
@@ -1,3 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.5001 5.83345L7.50008 15.8334L2.91675 11.2501L4.09175 10.0751L7.50008 13.4751L16.3251 4.65845L17.5001 5.83345Z"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 229 B After Width: | Height: | Size: 206 B |
@@ -1,6 +1,5 @@
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { PasswordWizardType } from '@/types';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { User } from '@standardnotes/snjs/dist/@types/services/api/responses';
|
||||
|
||||
@@ -10,22 +9,12 @@ type Props = {
|
||||
}
|
||||
|
||||
const User = observer(({
|
||||
appState,
|
||||
application,
|
||||
}: Props) => {
|
||||
const { server, closeAccountMenu } = appState.accountMenu;
|
||||
appState,
|
||||
application,
|
||||
}: Props) => {
|
||||
const { server } = appState.accountMenu;
|
||||
const user = application.getUser();
|
||||
|
||||
const openPasswordWizard = () => {
|
||||
closeAccountMenu();
|
||||
application.presentPasswordWizard(PasswordWizardType.ChangePassword);
|
||||
};
|
||||
|
||||
const openSessionsModal = () => {
|
||||
closeAccountMenu();
|
||||
appState.openSessionsModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="sk-panel-section">
|
||||
{appState.sync.errorMessage && (
|
||||
@@ -56,12 +45,6 @@ const User = observer(({
|
||||
</div>
|
||||
</div>
|
||||
<div className="sk-panel-row" />
|
||||
<a className="sk-a info sk-panel-row condensed" onClick={openPasswordWizard}>
|
||||
Change Password
|
||||
</a>
|
||||
<a className="sk-a info sk-panel-row condensed" onClick={openSessionsModal}>
|
||||
Manage Sessions
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -6,11 +6,6 @@ import { ConfirmSignoutContainer } from '@/components/ConfirmSignoutModal';
|
||||
import Authentication from '@/components/AccountMenu/Authentication';
|
||||
import Footer from '@/components/AccountMenu/Footer';
|
||||
import User from '@/components/AccountMenu/User';
|
||||
import Encryption from '@/components/AccountMenu/Encryption';
|
||||
import Protections from '@/components/AccountMenu/Protections';
|
||||
import PasscodeLock from '@/components/AccountMenu/PasscodeLock';
|
||||
import DataBackup from '@/components/AccountMenu/DataBackup';
|
||||
import ErrorReporting from '@/components/AccountMenu/ErrorReporting';
|
||||
import { useEffect } from 'preact/hooks';
|
||||
|
||||
type Props = {
|
||||
@@ -51,25 +46,12 @@ const AccountMenu = observer(({ application, appState }: Props) => {
|
||||
application={application}
|
||||
appState={appState}
|
||||
/>
|
||||
{!showLogin && !showRegister && (
|
||||
{!showLogin && !showRegister && user && (
|
||||
<div>
|
||||
{user && (
|
||||
<User
|
||||
application={application}
|
||||
appState={appState}
|
||||
/>
|
||||
)}
|
||||
<Encryption appState={appState} />
|
||||
<Protections application={application} />
|
||||
<PasscodeLock
|
||||
<User
|
||||
application={application}
|
||||
appState={appState}
|
||||
/>
|
||||
<DataBackup
|
||||
application={application}
|
||||
appState={appState}
|
||||
/>
|
||||
<ErrorReporting appState={appState} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -9,9 +9,9 @@ const baseClass = `rounded px-4 py-1.75 font-bold text-sm fit-content`;
|
||||
type ButtonType = 'normal' | 'primary' | 'danger';
|
||||
|
||||
const buttonClasses: { [type in ButtonType]: string } = {
|
||||
normal: `${baseClass} bg-default color-text border-solid border-gray-300 border-1 focus:bg-contrast hover:bg-contrast`,
|
||||
normal: `${baseClass} bg-default color-text border-neutral border-solid border-gray-300 border-1 focus:bg-contrast hover:bg-contrast`,
|
||||
primary: `${baseClass} no-border bg-info color-info-contrast hover:brightness-130 focus:brightness-130`,
|
||||
danger: `${baseClass} bg-default color-danger border-solid border-gray-300 border-1 focus:bg-contrast hover:bg-contrast`,
|
||||
danger: `${baseClass} bg-default color-danger border-neutral border-solid border-gray-300 border-1 focus:bg-contrast hover:bg-contrast`,
|
||||
};
|
||||
|
||||
export const Button: FunctionComponent<{
|
||||
|
||||
@@ -28,13 +28,13 @@ export const DecoratedInput: FunctionalComponent<Props> = ({
|
||||
autocomplete = false,
|
||||
}) => {
|
||||
const baseClasses =
|
||||
'rounded py-1.5 px-3 text-input my-1 h-8 flex flex-row items-center';
|
||||
'rounded py-1.5 px-3 text-input my-1 h-8 flex flex-row items-center bg-contrast';
|
||||
const stateClasses = disabled
|
||||
? 'no-border bg-grey-5'
|
||||
? 'no-border'
|
||||
: 'border-solid border-1 border-gray-300';
|
||||
const classes = `${baseClasses} ${stateClasses} ${className}`;
|
||||
|
||||
const inputBaseClasses = 'w-full no-border color-black focus:shadow-none';
|
||||
const inputBaseClasses = 'w-full no-border color-text focus:shadow-none bg-contrast';
|
||||
const inputStateClasses = disabled ? 'overflow-ellipsis' : '';
|
||||
return (
|
||||
<div className={`${classes} focus-within:ring-info`}>
|
||||
|
||||
@@ -28,6 +28,7 @@ import DownloadIcon from '../../icons/ic-download.svg';
|
||||
import InfoIcon from '../../icons/ic-info.svg';
|
||||
import CheckIcon from '../../icons/ic-check.svg';
|
||||
import CheckBoldIcon from '../../icons/ic-check-bold.svg';
|
||||
import AccountCircleIcon from '../../icons/ic-account-circle.svg';
|
||||
|
||||
import { toDirective } from './utils';
|
||||
import { FunctionalComponent } from 'preact';
|
||||
@@ -61,7 +62,8 @@ const ICONS = {
|
||||
download: DownloadIcon,
|
||||
info: InfoIcon,
|
||||
check: CheckIcon,
|
||||
"check-bold": CheckBoldIcon,
|
||||
'check-bold': CheckBoldIcon,
|
||||
'account-circle': AccountCircleIcon,
|
||||
};
|
||||
|
||||
export type IconType = keyof typeof ICONS;
|
||||
|
||||
@@ -11,9 +11,9 @@ export const Input: FunctionalComponent<Props> = ({
|
||||
disabled = false,
|
||||
text,
|
||||
}) => {
|
||||
const base = `rounded py-1.5 px-3 text-input my-1 h-8`;
|
||||
const base = `rounded py-1.5 px-3 text-input my-1 h-8 bg-contrast`;
|
||||
const stateClasses = disabled
|
||||
? 'no-border bg-grey-5'
|
||||
? 'no-border'
|
||||
: 'border-solid border-1 border-gray-300';
|
||||
const classes = `${base} ${stateClasses} ${className}`;
|
||||
return (
|
||||
|
||||
@@ -33,11 +33,11 @@ export const ModalDialogLabel: FunctionComponent<{
|
||||
}> = ({ children, closeDialog }) => (
|
||||
<AlertDialogLabel className="">
|
||||
<div className="px-4 py-4 flex flex-row items-center">
|
||||
<div className="flex-grow color-black text-lg font-bold">{children}</div>
|
||||
<div className="flex-grow color-text text-lg font-bold">{children}</div>
|
||||
<IconButton
|
||||
focusable={true}
|
||||
title="Close"
|
||||
className="color-grey-1 h-5 w-5"
|
||||
className="color-neutral h-5 w-5"
|
||||
icon="close"
|
||||
onClick={() => closeDialog()}
|
||||
/>
|
||||
@@ -61,11 +61,11 @@ export const ModalDialogButtons: FunctionComponent = ({ children }) => (
|
||||
<div className="px-4 py-4 flex flex-row justify-end items-center">
|
||||
{children != undefined && Array.isArray(children)
|
||||
? children.map((child, idx, arr) => (
|
||||
<>
|
||||
{child}
|
||||
{idx < arr.length - 1 ? <div className="min-w-3" /> : undefined}
|
||||
</>
|
||||
))
|
||||
<>
|
||||
{child}
|
||||
{idx < arr.length - 1 ? <div className="min-w-3" /> : undefined}
|
||||
</>
|
||||
))
|
||||
: children}
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { RoundIconButton } from '@/components/RoundIconButton';
|
||||
import { TitleBar, Title } from '@/components/TitleBar';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { AccountPreferences, General, HelpAndFeedback, Security } from './panes';
|
||||
import { AccountPreferences, HelpAndFeedback, Listed, General, Security } from './panes';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { PreferencesMenu } from './PreferencesMenu';
|
||||
import { PreferencesMenuView } from './PreferencesMenuView';
|
||||
@@ -41,7 +41,7 @@ const PaneSelector: FunctionComponent<
|
||||
/>
|
||||
);
|
||||
case 'listed':
|
||||
return null;
|
||||
return <Listed application={props.application} />;
|
||||
case 'shortcuts':
|
||||
return null;
|
||||
case 'accessibility':
|
||||
|
||||
@@ -14,14 +14,15 @@ export const Text: FunctionComponent<{ className?: string }> = ({
|
||||
}) => <p className={`${className} text-xs`}>{children}</p>;
|
||||
|
||||
const buttonClasses = `block bg-default color-text rounded border-solid \
|
||||
border-1 border-gray-300 px-4 py-1.75 font-bold text-sm fit-content mt-3 \
|
||||
focus:bg-contrast hover:bg-contrast `;
|
||||
border-1 border-gray-300 px-4 py-1.75 font-bold text-sm fit-content \
|
||||
focus:bg-contrast hover:bg-contrast border-neutral`;
|
||||
|
||||
export const LinkButton: FunctionComponent<{ label: string; link: string }> = ({
|
||||
label,
|
||||
link,
|
||||
}) => (
|
||||
<a target="_blank" className={buttonClasses} href={link}>
|
||||
export const LinkButton: FunctionComponent<{
|
||||
label: string;
|
||||
link: string;
|
||||
className?: string;
|
||||
}> = ({ label, link, className }) => (
|
||||
<a target="_blank" className={`${className} ${buttonClasses}`} href={link}>
|
||||
{label}
|
||||
</a>
|
||||
);
|
||||
|
||||
@@ -15,7 +15,7 @@ export const MenuItem: FunctionComponent<Props> = ({
|
||||
onClick,
|
||||
}) => (
|
||||
<div
|
||||
className={`preferences-menu-item ${selected ? 'selected' : ''}`}
|
||||
className={`preferences-menu-item select-none ${selected ? 'selected' : ''}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onClick();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FunctionComponent } from 'preact';
|
||||
|
||||
export const PreferencesPane: FunctionComponent = ({ children }) => (
|
||||
<div className="color-black flex-grow flex flex-row overflow-y-auto min-h-0">
|
||||
<div className="color-foreground flex-grow flex flex-row overflow-y-auto min-h-0">
|
||||
<div className="flex-grow flex flex-col py-6 items-center">
|
||||
<div className="w-125 max-w-125 flex flex-col">
|
||||
{children != undefined && Array.isArray(children)
|
||||
|
||||
@@ -3,6 +3,7 @@ import { AppState } from '@/ui_models/app_state';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { PreferencesPane } from '../components';
|
||||
import { ErrorReporting } from './general-segments';
|
||||
import { Tools } from './general-segments/Tools';
|
||||
|
||||
interface GeneralProps {
|
||||
appState: AppState;
|
||||
@@ -11,6 +12,7 @@ interface GeneralProps {
|
||||
|
||||
export const General: FunctionComponent<GeneralProps> = (props) => (
|
||||
<PreferencesPane>
|
||||
<Tools application={props.application} />
|
||||
<ErrorReporting appState={props.appState} />
|
||||
</PreferencesPane>
|
||||
);
|
||||
|
||||
@@ -52,7 +52,11 @@ export const HelpAndFeedback: FunctionComponent = () => (
|
||||
</PreferencesSegment>
|
||||
<PreferencesSegment>
|
||||
<Subtitle>Can’t find your question here?</Subtitle>
|
||||
<LinkButton label="Open FAQ" link="https://standardnotes.com/help" />
|
||||
<LinkButton
|
||||
className="mt-3"
|
||||
label="Open FAQ"
|
||||
link="https://standardnotes.com/help"
|
||||
/>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
<PreferencesGroup>
|
||||
@@ -68,6 +72,7 @@ export const HelpAndFeedback: FunctionComponent = () => (
|
||||
before advocating for a feature request.
|
||||
</Text>
|
||||
<LinkButton
|
||||
className="mt-3"
|
||||
label="Go to the forum"
|
||||
link="https://forum.standardnotes.org/"
|
||||
/>
|
||||
@@ -82,6 +87,7 @@ export const HelpAndFeedback: FunctionComponent = () => (
|
||||
group for discussions on security, themes, editors and more.
|
||||
</Text>
|
||||
<LinkButton
|
||||
className="mt-3"
|
||||
link="https://standardnotes.com/slack"
|
||||
label="Join our Slack group"
|
||||
/>
|
||||
@@ -93,7 +99,7 @@ export const HelpAndFeedback: FunctionComponent = () => (
|
||||
<Text>
|
||||
Send an email to help@standardnotes.com and we’ll sort it out.
|
||||
</Text>
|
||||
<LinkButton link="mailto: help@standardnotes.com" label="Email us" />
|
||||
<LinkButton className="mt-3" link="mailto: help@standardnotes.com" label="Email us" />
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
</PreferencesPane>
|
||||
|
||||
116
app/assets/javascripts/preferences/panes/Listed.tsx
Normal file
116
app/assets/javascripts/preferences/panes/Listed.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import {
|
||||
PreferencesGroup,
|
||||
PreferencesPane,
|
||||
PreferencesSegment,
|
||||
Title,
|
||||
Subtitle,
|
||||
Text,
|
||||
LinkButton,
|
||||
} from '../components';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { ContentType, SNComponent } from '@standardnotes/snjs';
|
||||
import { SNItem } from '@standardnotes/snjs/dist/@types/models/core/item';
|
||||
import { useCallback, useEffect, useState } from 'preact/hooks';
|
||||
import { BlogItem } from './listed/BlogItem';
|
||||
|
||||
type Props = {
|
||||
application: WebApplication;
|
||||
};
|
||||
|
||||
export const Listed = observer(({ application }: Props) => {
|
||||
const [items, setItems] = useState<SNComponent[]>([]);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
const reloadItems = useCallback(() => {
|
||||
const components = application
|
||||
.getItems(ContentType.ActionsExtension)
|
||||
.filter(
|
||||
(item) => (item as SNComponent).package_info?.name === 'Listed'
|
||||
) as SNComponent[];
|
||||
setItems(components);
|
||||
}, [application]);
|
||||
|
||||
useEffect(() => {
|
||||
reloadItems();
|
||||
}, [reloadItems]);
|
||||
|
||||
const disconnectListedBlog = (item: SNItem) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
setIsDeleting(true);
|
||||
application
|
||||
.deleteItem(item)
|
||||
.then(() => {
|
||||
reloadItems();
|
||||
setIsDeleting(false);
|
||||
resolve(true);
|
||||
})
|
||||
.catch((err) => {
|
||||
application.alertService.alert(err);
|
||||
setIsDeleting(false);
|
||||
console.error(err);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<PreferencesPane>
|
||||
{items.length > 0 && (
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>
|
||||
Your {items.length === 1 ? 'Blog' : 'Blogs'} on Listed
|
||||
</Title>
|
||||
<div className="h-2 w-full" />
|
||||
{items.map((item, index, array) => {
|
||||
return (
|
||||
<BlogItem
|
||||
item={item}
|
||||
showSeparator={index !== array.length - 1}
|
||||
disabled={isDeleting}
|
||||
disconnect={disconnectListedBlog}
|
||||
key={item.uuid}
|
||||
application={application}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
)}
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>About Listed</Title>
|
||||
<div className="h-2 w-full" />
|
||||
<Subtitle>What is Listed?</Subtitle>
|
||||
<Text>
|
||||
Listed is a free blogging platform that allows you to create a
|
||||
public journal published directly from your notes.{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://listed.to"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</Text>
|
||||
</PreferencesSegment>
|
||||
{items.length === 0 ? (
|
||||
<PreferencesSegment>
|
||||
<Subtitle>How to get started?</Subtitle>
|
||||
<Text>
|
||||
First, you’ll need to sign up for Listed. Once you have your
|
||||
Listed account, follow the instructions to connect it with your
|
||||
Standard Notes account.
|
||||
</Text>
|
||||
<LinkButton
|
||||
className="min-w-20 mt-3"
|
||||
link="https://listed.to"
|
||||
label="Get started"
|
||||
/>
|
||||
</PreferencesSegment>
|
||||
) : null}
|
||||
</PreferencesGroup>
|
||||
</PreferencesPane>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import { HorizontalSeparator } from '@/components/shared/HorizontalSeparator';
|
||||
import { Switch } from '@/components/Switch';
|
||||
import {
|
||||
PreferencesGroup,
|
||||
PreferencesSegment,
|
||||
Subtitle,
|
||||
Text,
|
||||
Title,
|
||||
} from '@/preferences/components';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { PrefKey } from '@standardnotes/snjs';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionalComponent } from 'preact';
|
||||
import { useState } from 'preact/hooks';
|
||||
|
||||
type Props = {
|
||||
application: WebApplication;
|
||||
};
|
||||
|
||||
export const Tools: FunctionalComponent<Props> = observer(
|
||||
({ application }: Props) => {
|
||||
const [monospaceFont, setMonospaceFont] = useState(() =>
|
||||
application.getPreference(PrefKey.EditorMonospaceEnabled)
|
||||
);
|
||||
const [marginResizers, setMarginResizers] = useState(() =>
|
||||
application.getPreference(PrefKey.EditorResizersEnabled)
|
||||
);
|
||||
const [spellcheck, setSpellcheck] = useState(() =>
|
||||
application.getPreference(PrefKey.EditorSpellcheck)
|
||||
);
|
||||
|
||||
const toggleMonospaceFont = () => {
|
||||
setMonospaceFont(!monospaceFont);
|
||||
application.setPreference(PrefKey.EditorMonospaceEnabled, !monospaceFont);
|
||||
};
|
||||
|
||||
const toggleMarginResizers = () => {
|
||||
setMarginResizers(!marginResizers);
|
||||
application.setPreference(PrefKey.EditorResizersEnabled, !marginResizers);
|
||||
};
|
||||
|
||||
const toggleSpellcheck = () => {
|
||||
setSpellcheck(!spellcheck);
|
||||
application.setPreference(PrefKey.EditorSpellcheck, !spellcheck);
|
||||
};
|
||||
|
||||
return (
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>Tools</Title>
|
||||
<div className="mt-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<Subtitle>Monospace Font</Subtitle>
|
||||
<Text>Toggles the font style in the Plain Text editor.</Text>
|
||||
</div>
|
||||
<Switch onChange={toggleMonospaceFont} checked={monospaceFont} />
|
||||
</div>
|
||||
<HorizontalSeparator classes="mt-5 mb-3" />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<Subtitle>Margin Resizers</Subtitle>
|
||||
<Text>Allows left and right editor margins to be resized.</Text>
|
||||
</div>
|
||||
<Switch
|
||||
onChange={toggleMarginResizers}
|
||||
checked={marginResizers}
|
||||
/>
|
||||
</div>
|
||||
<HorizontalSeparator classes="mt-5 mb-3" />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<Subtitle>Spellcheck</Subtitle>
|
||||
<Text>
|
||||
May degrade performance, especially with long notes. Available
|
||||
in the Plain Text editor and most specialty editors.
|
||||
</Text>
|
||||
</div>
|
||||
<Switch onChange={toggleSpellcheck} checked={spellcheck} />
|
||||
</div>
|
||||
</div>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -1 +1,2 @@
|
||||
export * from './ErrorReporting';
|
||||
export * from './Tools';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './HelpFeedback';
|
||||
export * from './Security';
|
||||
export * from './AccountPreferences';
|
||||
export * from './Listed';
|
||||
export * from './General';
|
||||
|
||||
110
app/assets/javascripts/preferences/panes/listed/BlogItem.tsx
Normal file
110
app/assets/javascripts/preferences/panes/listed/BlogItem.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import { Button } from '@/components/Button';
|
||||
import { HorizontalSeparator } from '@/components/shared/HorizontalSeparator';
|
||||
import { LinkButton, Subtitle } from '@/preferences/components';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import {
|
||||
Action,
|
||||
ButtonType,
|
||||
SNActionsExtension,
|
||||
SNComponent,
|
||||
SNItem,
|
||||
} from '@standardnotes/snjs';
|
||||
import { FunctionalComponent } from 'preact';
|
||||
import { useEffect, useState } from 'preact/hooks';
|
||||
|
||||
type Props = {
|
||||
item: SNComponent;
|
||||
showSeparator: boolean;
|
||||
disabled: boolean;
|
||||
disconnect: (item: SNItem) => Promise<unknown>;
|
||||
application: WebApplication;
|
||||
};
|
||||
|
||||
export const BlogItem: FunctionalComponent<Props> = ({
|
||||
item,
|
||||
showSeparator,
|
||||
disabled,
|
||||
disconnect,
|
||||
application,
|
||||
}) => {
|
||||
const [actions, setActions] = useState<Action[] | undefined>([]);
|
||||
const [isLoadingActions, setIsLoadingActions] = useState(false);
|
||||
const [isDisconnecting, setIsDisconnecting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const loadActions = async () => {
|
||||
setIsLoadingActions(true);
|
||||
application.actionsManager
|
||||
.loadExtensionInContextOfItem(item as SNActionsExtension, item)
|
||||
.then((extension) => {
|
||||
setActions(extension?.actions);
|
||||
})
|
||||
.catch((err) => application.alertService.alert(err))
|
||||
.finally(() => {
|
||||
setIsLoadingActions(false);
|
||||
});
|
||||
};
|
||||
if (!actions || actions.length === 0) loadActions();
|
||||
}, [application.actionsManager, application.alertService, item, actions]);
|
||||
|
||||
const handleDisconnect = () => {
|
||||
setIsDisconnecting(true);
|
||||
application.alertService
|
||||
.confirm(
|
||||
'Disconnecting will result in loss of access to your blog. Ensure your Listed author key is backed up before uninstalling.',
|
||||
`Disconnect blog "${item?.name}"?`,
|
||||
'Disconnect',
|
||||
ButtonType.Danger
|
||||
)
|
||||
.then(async (shouldDisconnect) => {
|
||||
if (shouldDisconnect) {
|
||||
await disconnect(item as SNItem);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
application.alertService.alert(err);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsDisconnecting(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Subtitle>{item?.name}</Subtitle>
|
||||
<div className="flex">
|
||||
{isLoadingActions ? (
|
||||
<div className="sk-spinner small info"></div>
|
||||
) : null}
|
||||
{actions && actions?.length > 0 ? (
|
||||
<>
|
||||
<LinkButton
|
||||
className="mr-2"
|
||||
label="Open Blog"
|
||||
link={
|
||||
actions?.find((action: Action) => action.label === 'Open Blog')
|
||||
?.url || ''
|
||||
}
|
||||
/>
|
||||
<LinkButton
|
||||
className="mr-2"
|
||||
label="Settings"
|
||||
link={
|
||||
actions?.find((action: Action) => action.label === 'Settings')
|
||||
?.url || ''
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
type="danger"
|
||||
label={isDisconnecting ? 'Disconnecting...' : 'Disconnect'}
|
||||
disabled={disabled}
|
||||
onClick={handleDisconnect}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
{showSeparator && <HorizontalSeparator classes="mt-5 mb-3" />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -15,7 +15,7 @@ const EncryptionEnabled: FunctionComponent<{ appState: AppState }> = observer(({
|
||||
const archived = formatCount(count.archived, 'archived notes');
|
||||
const deleted = formatCount(count.deleted, 'trashed notes');
|
||||
|
||||
const checkIcon = <Icon className="success min-w-5 min-h-5" type="check-bold" />;
|
||||
const checkIcon = <Icon className="success min-w-4 min-h-4" type="check-bold" />;
|
||||
const noteIcon = <Icon type="rich-text" className="min-w-5 min-h-5" />;
|
||||
const tagIcon = <Icon type="hashtag" className="min-w-5 min-h-5" />;
|
||||
const archiveIcon = <Icon type="archive" className="min-w-5 min-h-5" />;
|
||||
|
||||
@@ -16,9 +16,8 @@ const DisclosureIconButton: FunctionComponent<{
|
||||
<DisclosureButton
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
className={`no-border cursor-pointer bg-transparent hover:brightness-130 p-0 ${
|
||||
className ?? ''
|
||||
}`}
|
||||
className={`no-border cursor-pointer bg-transparent hover:brightness-130 p-0 ${className ?? ''
|
||||
}`}
|
||||
>
|
||||
<Icon type={icon} />
|
||||
</DisclosureButton>
|
||||
@@ -58,7 +57,7 @@ export const AuthAppInfoTooltip: FunctionComponent = () => {
|
||||
/>
|
||||
<DisclosurePanel>
|
||||
<div
|
||||
className={`bg-black color-white text-center rounded shadow-overlay
|
||||
className={`bg-inverted-default color-inverted-default text-center rounded shadow-overlay
|
||||
py-1.5 px-2 absolute w-103 -top-10 -left-51`}
|
||||
>
|
||||
Some apps, like Google Authenticator, do not back up and restore
|
||||
|
||||
@@ -3,5 +3,5 @@ import { FunctionComponent } from 'preact';
|
||||
export const Bullet: FunctionComponent<{ className?: string }> = ({
|
||||
className = '',
|
||||
}) => (
|
||||
<div className={`min-w-1 min-h-1 rounded-full bg-black ${className} mr-2`} />
|
||||
<div className={`min-w-1 min-h-1 rounded-full bg-inverted-default ${className} mr-2`} />
|
||||
);
|
||||
|
||||
@@ -59,42 +59,6 @@
|
||||
.sn-component(ng-if='self.note')
|
||||
#editor-menu-bar.sk-app-bar.no-edges
|
||||
.left
|
||||
.sk-app-bar-item(
|
||||
click-outside=`self.setMenuState('showOptionsMenu', false)`,
|
||||
is-open='self.state.showOptionsMenu',
|
||||
ng-class="{'selected' : self.state.showOptionsMenu}",
|
||||
ng-click="self.toggleMenu('showOptionsMenu')"
|
||||
)
|
||||
.sk-label Options
|
||||
.sk-menu-panel.dropdown-menu(ng-if='self.state.showOptionsMenu')
|
||||
.sk-menu-panel-section
|
||||
.sk-menu-panel-header
|
||||
.sk-menu-panel-header-title Global Display
|
||||
menu-row(
|
||||
action="self.selectedMenuItem(true); self.toggleWebPrefKey(self.prefKeyMonospace)"
|
||||
circle="self.state.monospaceFont ? 'success' : 'neutral'",
|
||||
desc="'Toggles the font style for the default editor'",
|
||||
disabled='self.state.editorComponent',
|
||||
label="'Monospace Font'",
|
||||
subtitle="self.state.editorComponent ? 'Not available with editor extensions' : null"
|
||||
)
|
||||
menu-row(
|
||||
action="self.selectedMenuItem(true); self.toggleWebPrefKey(self.prefKeySpellcheck)"
|
||||
circle="self.state.spellcheck ? 'success' : 'neutral'",
|
||||
desc="'Toggles spellcheck for the default editor'",
|
||||
disabled='self.state.editorComponent',
|
||||
label="'Spellcheck'",
|
||||
subtitle=`
|
||||
self.state.editorComponent
|
||||
? 'Not available with editor extensions'
|
||||
: (self.state.isDesktop ? 'May degrade editor performance' : null)
|
||||
`)
|
||||
menu-row(
|
||||
action="self.selectedMenuItem(true); self.toggleWebPrefKey(self.prefKeyMarginResizers)"
|
||||
circle="self.state.marginResizersEnabled ? 'success' : 'neutral'",
|
||||
desc="'Allows for editor left and right margins to be resized'",
|
||||
label="'Margin Resizers'"
|
||||
)
|
||||
.sk-app-bar-item(
|
||||
click-outside=`self.setMenuState('showEditorMenu', false)`
|
||||
is-open='self.state.showEditorMenu',
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import {
|
||||
STRING_SAVING_WHILE_DOCUMENT_HIDDEN,
|
||||
} from './../../strings';
|
||||
import { STRING_SAVING_WHILE_DOCUMENT_HIDDEN } from './../../strings';
|
||||
import { Editor } from '@/ui_models/editor';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { PanelPuppet, WebDirective } from '@/types';
|
||||
@@ -61,7 +59,6 @@ type EditorState = {
|
||||
isDesktop?: boolean;
|
||||
syncTakingTooLong: boolean;
|
||||
showActionsMenu: boolean;
|
||||
showOptionsMenu: boolean;
|
||||
showEditorMenu: boolean;
|
||||
showHistoryMenu: boolean;
|
||||
spellcheck: boolean;
|
||||
@@ -202,7 +199,7 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
});
|
||||
this.autorun(() => {
|
||||
this.setState({
|
||||
showProtectedWarning: this.appState.notes.showProtectedWarning
|
||||
showProtectedWarning: this.appState.notes.showProtectedWarning,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -216,7 +213,6 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
spellcheck: true,
|
||||
syncTakingTooLong: false,
|
||||
showActionsMenu: false,
|
||||
showOptionsMenu: false,
|
||||
showEditorMenu: false,
|
||||
showHistoryMenu: false,
|
||||
noteStatus: undefined,
|
||||
@@ -272,11 +268,11 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
async handleEditorNoteChange() {
|
||||
this.cancelPendingSetStatus();
|
||||
const note = this.editor.note;
|
||||
const showProtectedWarning = note.protected && !this.application.hasProtectionSources();
|
||||
const showProtectedWarning =
|
||||
note.protected && !this.application.hasProtectionSources();
|
||||
this.setShowProtectedWarning(showProtectedWarning);
|
||||
await this.setState({
|
||||
showActionsMenu: false,
|
||||
showOptionsMenu: false,
|
||||
showEditorMenu: false,
|
||||
showHistoryMenu: false,
|
||||
noteStatus: undefined,
|
||||
@@ -364,12 +360,7 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
}
|
||||
|
||||
closeAllMenus(exclude?: string) {
|
||||
const allMenus = [
|
||||
'showOptionsMenu',
|
||||
'showEditorMenu',
|
||||
'showActionsMenu',
|
||||
'showHistoryMenu',
|
||||
];
|
||||
const allMenus = ['showEditorMenu', 'showActionsMenu', 'showHistoryMenu'];
|
||||
const menuState: any = {};
|
||||
for (const candidate of allMenus) {
|
||||
if (candidate !== exclude) {
|
||||
@@ -591,7 +582,7 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
}
|
||||
|
||||
clickedTextArea() {
|
||||
this.setMenuState('showOptionsMenu', false);
|
||||
this.closeAllMenus();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
@@ -607,12 +598,6 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
this.lastEditorFocusEventSource = undefined;
|
||||
}
|
||||
|
||||
selectedMenuItem(hide: boolean) {
|
||||
if (hide) {
|
||||
this.setMenuState('showOptionsMenu', false);
|
||||
}
|
||||
}
|
||||
|
||||
setShowProtectedWarning(show: boolean) {
|
||||
this.appState.notes.setShowProtectedWarning(show);
|
||||
}
|
||||
@@ -757,13 +742,10 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
/** @components */
|
||||
|
||||
registerComponentHandler() {
|
||||
this.unregisterComponent = this.application.componentManager!.registerHandler(
|
||||
{
|
||||
this.unregisterComponent =
|
||||
this.application.componentManager!.registerHandler({
|
||||
identifier: 'editor',
|
||||
areas: [
|
||||
ComponentArea.EditorStack,
|
||||
ComponentArea.Editor,
|
||||
],
|
||||
areas: [ComponentArea.EditorStack, ComponentArea.Editor],
|
||||
contextRequestHandler: (componentUuid) => {
|
||||
const currentEditor = this.state.editorComponent;
|
||||
if (
|
||||
@@ -778,8 +760,7 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
this.closeAllMenus();
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async reloadStackComponents() {
|
||||
@@ -809,9 +790,8 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
}
|
||||
|
||||
async toggleStackComponentForCurrentItem(component: SNComponent) {
|
||||
const hidden = this.application.componentManager!.isComponentHidden(
|
||||
component
|
||||
);
|
||||
const hidden =
|
||||
this.application.componentManager!.isComponentHidden(component);
|
||||
if (hidden || !component.active) {
|
||||
this.application.componentManager!.setComponentHidden(component, false);
|
||||
await this.associateComponentWithCurrentNote(component);
|
||||
@@ -844,16 +824,14 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
}
|
||||
|
||||
registerKeyboardShortcuts() {
|
||||
this.removeTrashKeyObserver = this.application
|
||||
.io
|
||||
.addKeyObserver({
|
||||
key: KeyboardKey.Backspace,
|
||||
notTags: ['INPUT', 'TEXTAREA'],
|
||||
modifiers: [KeyboardModifier.Meta],
|
||||
onKeyDown: () => {
|
||||
this.deleteNote(false);
|
||||
},
|
||||
});
|
||||
this.removeTrashKeyObserver = this.application.io.addKeyObserver({
|
||||
key: KeyboardKey.Backspace,
|
||||
notTags: ['INPUT', 'TEXTAREA'],
|
||||
modifiers: [KeyboardModifier.Meta],
|
||||
onKeyDown: () => {
|
||||
this.deleteNote(false);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setScrollPosition() {
|
||||
@@ -883,39 +861,37 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
const editor = document.getElementById(
|
||||
ElementIds.NoteTextEditor
|
||||
)! as HTMLInputElement;
|
||||
this.removeTabObserver = this.application
|
||||
.io
|
||||
.addKeyObserver({
|
||||
element: editor,
|
||||
key: KeyboardKey.Tab,
|
||||
onKeyDown: (event) => {
|
||||
if (document.hidden || this.note.locked || event.shiftKey) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
/** Using document.execCommand gives us undo support */
|
||||
const insertSuccessful = document.execCommand(
|
||||
'insertText',
|
||||
false,
|
||||
'\t'
|
||||
);
|
||||
if (!insertSuccessful) {
|
||||
/** document.execCommand works great on Chrome/Safari but not Firefox */
|
||||
const start = editor.selectionStart!;
|
||||
const end = editor.selectionEnd!;
|
||||
const spaces = ' ';
|
||||
/** Insert 4 spaces */
|
||||
editor.value =
|
||||
editor.value.substring(0, start) +
|
||||
spaces +
|
||||
editor.value.substring(end);
|
||||
/** Place cursor 4 spaces away from where the tab key was pressed */
|
||||
editor.selectionStart = editor.selectionEnd = start + 4;
|
||||
}
|
||||
this.editorValues.text = editor.value;
|
||||
this.save(this.note, copyEditorValues(this.editorValues), true);
|
||||
},
|
||||
});
|
||||
this.removeTabObserver = this.application.io.addKeyObserver({
|
||||
element: editor,
|
||||
key: KeyboardKey.Tab,
|
||||
onKeyDown: (event) => {
|
||||
if (document.hidden || this.note.locked || event.shiftKey) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
/** Using document.execCommand gives us undo support */
|
||||
const insertSuccessful = document.execCommand(
|
||||
'insertText',
|
||||
false,
|
||||
'\t'
|
||||
);
|
||||
if (!insertSuccessful) {
|
||||
/** document.execCommand works great on Chrome/Safari but not Firefox */
|
||||
const start = editor.selectionStart!;
|
||||
const end = editor.selectionEnd!;
|
||||
const spaces = ' ';
|
||||
/** Insert 4 spaces */
|
||||
editor.value =
|
||||
editor.value.substring(0, start) +
|
||||
spaces +
|
||||
editor.value.substring(end);
|
||||
/** Place cursor 4 spaces away from where the tab key was pressed */
|
||||
editor.selectionStart = editor.selectionEnd = start + 4;
|
||||
}
|
||||
this.editorValues.text = editor.value;
|
||||
this.save(this.note, copyEditorValues(this.editorValues), true);
|
||||
},
|
||||
});
|
||||
|
||||
editor.addEventListener('scroll', this.setScrollPosition);
|
||||
editor.addEventListener('input', this.resetScrollPosition);
|
||||
|
||||
@@ -1,28 +1,37 @@
|
||||
.sn-component
|
||||
#footer-bar.sk-app-bar.no-edges.no-bottom-edge
|
||||
.left
|
||||
.sk-app-bar-item(
|
||||
.sk-app-bar-item.ml-0(
|
||||
click-outside='ctrl.clickOutsideAccountMenu()',
|
||||
is-open='ctrl.showAccountMenu',
|
||||
ng-click='ctrl.accountMenuPressed()'
|
||||
)
|
||||
.sk-app-bar-item-column
|
||||
.sk-circle.small(
|
||||
ng-class="ctrl.hasError ? 'danger' : (ctrl.user ? 'info' : 'neutral')"
|
||||
)
|
||||
.w-8.h-full.flex.items-center.justify-center.cursor-pointer.rounded-full(
|
||||
ng-class="ctrl.showAccountMenu ? 'bg-border' : '' "
|
||||
)
|
||||
.w-5.h-5(
|
||||
ng-class="ctrl.hasError ? 'danger' : (ctrl.user ? 'info' : 'neutral')"
|
||||
)
|
||||
.sk-app-bar-item-column
|
||||
.sk-label.title(ng-class='{red: ctrl.hasError}') Account
|
||||
icon(
|
||||
type="account-circle"
|
||||
class-name="hover:color-info w-5 h-5 max-h-5"
|
||||
)
|
||||
account-menu(
|
||||
ng-click='$event.stopPropagation()',
|
||||
app-state='ctrl.appState'
|
||||
application='ctrl.application'
|
||||
ng-if='ctrl.showAccountMenu',
|
||||
)
|
||||
.sk-app-bar-item(
|
||||
.sk-app-bar-item.ml-0-important(
|
||||
ng-click='ctrl.clickPreferences()'
|
||||
ng-if='ctrl.appState.enableUnfinishedFeatures'
|
||||
)
|
||||
.sk-label.title Preferences
|
||||
.w-8.h-full.flex.items-center.justify-center.cursor-pointer
|
||||
.h-5
|
||||
icon(
|
||||
type="tune"
|
||||
class-name="rounded hover:color-info"
|
||||
)
|
||||
.sk-app-bar-item
|
||||
a.no-decoration.sk-label.title(
|
||||
href='https://standardnotes.com/help',
|
||||
|
||||
@@ -34,17 +34,6 @@
|
||||
border-bottom: 2px solid var(--sn-stylekit-info-color);
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
fill: var(--sn-stylekit-secondary-foreground-color);
|
||||
|
||||
&:hover {
|
||||
fill: var(--sn-stylekit-info-color) !important;
|
||||
color: var(--sn-stylekit-info-color) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#account-switcher-icon {
|
||||
|
||||
@@ -115,7 +115,7 @@ p {
|
||||
background-color: var(--sn-stylekit-background-color);
|
||||
}
|
||||
|
||||
$footer-height: 32px;
|
||||
$footer-height: 2rem;
|
||||
|
||||
#resizer-overlay {
|
||||
position: absolute;
|
||||
|
||||
@@ -154,6 +154,13 @@
|
||||
@extend .font-bold;
|
||||
}
|
||||
|
||||
.ml-0-important {
|
||||
margin-left: 0rem !important;
|
||||
}
|
||||
|
||||
.ml-3 {
|
||||
margin-left: 0.75rem;
|
||||
}
|
||||
.ml-4 {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
@@ -222,10 +229,22 @@
|
||||
min-height: 1.5rem;
|
||||
}
|
||||
|
||||
.max-h-5 {
|
||||
max-height: 1.25rem;
|
||||
}
|
||||
|
||||
.border-danger {
|
||||
border-color: var(--sn-stylekit-danger-color);
|
||||
}
|
||||
|
||||
.bg-inverted-default {
|
||||
background-color: var(--sn-stylekit-contrast-foreground-color);
|
||||
}
|
||||
|
||||
.color-inverted-default {
|
||||
color: var(--sn-stylekit-background-color);
|
||||
}
|
||||
|
||||
.pt-1 {
|
||||
padding-top: 0.25rem;
|
||||
}
|
||||
@@ -256,3 +275,7 @@
|
||||
padding-top: 2.25rem;
|
||||
padding-bottom: 2.25rem;
|
||||
}
|
||||
|
||||
.select-none {
|
||||
user-select: none;
|
||||
}
|
||||
Reference in New Issue
Block a user