Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add Listed pane in preferences #651

Merged
merged 20 commits into from
Sep 29, 2021
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/assets/javascripts/preferences/PreferencesView.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { RoundIconButton } from '@/components/RoundIconButton';
import { TitleBar, Title } from '@/components/TitleBar';
import { FunctionComponent } from 'preact';
import { AccountPreferences, HelpAndFeedback, Security } from './panes';
import { AccountPreferences, HelpAndFeedback, Listed, Security } from './panes';
import { observer } from 'mobx-react-lite';
import { PreferencesMenu } from './PreferencesMenu';
import { PreferencesMenuView } from './PreferencesMenuView';
Expand Down Expand Up @@ -40,7 +40,7 @@ const PaneSelector: FunctionComponent<
/>
);
case 'listed':
return null;
return <Listed application={props.application} />;
case 'shortcuts':
return null;
case 'accessibility':
Expand Down
13 changes: 7 additions & 6 deletions app/assets/javascripts/preferences/components/Content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
border-1 border-gray-300 px-4 py-1.75 font-bold text-sm fit-content \
focus:bg-contrast hover:bg-contrast `;

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>
);
14 changes: 12 additions & 2 deletions app/assets/javascripts/preferences/panes/HelpFeedback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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/"
/>
Expand All @@ -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"
/>
Expand All @@ -93,7 +99,11 @@ export const HelpAndFeedback: FunctionComponent = () => (
<Text>
Send an email to [email protected] and we’ll sort it out.
</Text>
<LinkButton link="mailto: [email protected]" label="Email us" />
<LinkButton
className="mt-3"
link="mailto: [email protected]"
label="Email us"
/>
</PreferencesSegment>
</PreferencesGroup>
</PreferencesPane>
Expand Down
110 changes: 110 additions & 0 deletions app/assets/javascripts/preferences/panes/Listed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
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 'react';
amanharwara marked this conversation as resolved.
Show resolved Hide resolved
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) => {
setIsDeleting(true);
application
.deleteItem(item)
.then(() => {
reloadItems();
setIsDeleting(false);
})
.catch((err) => {
console.error(err);
amanharwara marked this conversation as resolved.
Show resolved Hide resolved
});
};

return (
<PreferencesPane>
{items.length > 0 && (
amanharwara marked this conversation as resolved.
Show resolved Hide resolved
<PreferencesGroup>
<PreferencesSegment>
<Title>
Your {items.length === 1 ? 'Blog' : 'Blogs'} on Listed
</Title>
<div className="h-2 w-full" />
{items.map((item: any, 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>
)}
</PreferencesGroup>
</PreferencesPane>
);
});
1 change: 1 addition & 0 deletions app/assets/javascripts/preferences/panes/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './HelpFeedback';
export * from './Security';
export * from './AccountPreferences';
export * from './Listed';
79 changes: 79 additions & 0 deletions app/assets/javascripts/preferences/panes/listed/BlogItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
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, SNComponent, SNItem } from '@standardnotes/snjs/dist/@types';
import { JSXInternal } from 'preact/src/jsx';
import React, { useState } from 'react';
amanharwara marked this conversation as resolved.
Show resolved Hide resolved

type Props = {
item: SNComponent;
showSeparator: boolean;
disabled: boolean;
disconnect: (item: SNItem) => void;
application: WebApplication;
};

export const BlogItem = ({
item,
showSeparator,
disabled,
disconnect,
application,
}: Props): JSXInternal.Element => {
amanharwara marked this conversation as resolved.
Show resolved Hide resolved
const applicationAlertService = application.alertService;

const [isDisconnecting, setIsDisconnecting] = useState(false);

const handleDisconnect = () => {
setIsDisconnecting(true);
applicationAlertService
.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',
1
)
.then((shouldDisconnect) => {
if (shouldDisconnect) {
disconnect(item);
} else {
setIsDisconnecting(false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setIsDisconnecting(false) would need to go in a finally block to ensure it executes even if there's an error

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Putting it in the finally block turns it off even before it finishes disconnecting, since the deleteItem() function is async. I've just put setIsDisconnecting(false) in the catch block to execute it even if there is an error.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we await disconnect in that case? Might be a good idea to refactor these to use async/await which is a bit easier to read I think

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would require the disconnect function to be promisified. That is actually how I had originally implemented it, but I thought I was overcomplicating it, so I removed the promise.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@antsgar So I've turned the disconnect function into a Promise and using await disconnect() in the BlogItem and setIsDisconnecting() inside the .finally() block.

}
})
.catch((err) => console.error(err));
};

return (
<React.Fragment>
<Subtitle>{item.name}</Subtitle>
<div className="flex">
<LinkButton
className="mr-2"
label="Open Blog"
link={
(item as any).package_info.actions.find(
amanharwara marked this conversation as resolved.
Show resolved Hide resolved
(action: Action) => action.label === 'Open Blog'
).url
}
/>
<LinkButton
className="mr-2"
label="Settings"
link={
(item as any).package_info.actions.find(
(action: Action) => action.label === 'Settings'
).url
}
/>
<Button
type="danger"
label={isDisconnecting ? 'Disconnecting...' : 'Disconnect'}
disabled={disabled}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could use the isDiconnecting state variable here and get rid of the isDeleting one on the parent component

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the deletion happens asynchronously, I've kept the isDeleting on the parent so the other Disconnect buttons can stay disabled while one is disconnecting to avoid any errors.

onClick={handleDisconnect}
/>
</div>
{showSeparator && <HorizontalSeparator classes="mt-5 mb-3" />}
</React.Fragment>
);
};