Skip to content

feat: native listed integration #846

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

Merged
merged 8 commits into from
Feb 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
441 changes: 190 additions & 251 deletions app/assets/javascripts/components/ActionsMenu.tsx

Large diffs are not rendered by default.

6 changes: 2 additions & 4 deletions app/assets/javascripts/components/HistoryMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -252,8 +252,7 @@ export class HistoryMenu extends PureComponent<Props, HistoryState> {
return (
<MenuRow
key={index}
action={this.openSessionRevision}
actionArgs={[revision]}
action={() => this.openSessionRevision(revision)}
label={revision.previewTitle()}
>
<div
Expand Down Expand Up @@ -298,8 +297,7 @@ export class HistoryMenu extends PureComponent<Props, HistoryState> {
return (
<MenuRow
key={index}
action={this.openRemoteRevision}
actionArgs={[revision]}
action={() => this.openRemoteRevision(revision)}
label={this.previewRemoteHistoryTitle(revision)}
/>
);
Expand Down
17 changes: 6 additions & 11 deletions app/assets/javascripts/components/MenuRow.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { Component } from 'preact';

type RowProps = {
action?: (...args: any[]) => void;
actionArgs?: any[];
export type MenuRowProps = {
action?: () => void;
buttonAction?: () => void;
buttonClass?: string;
buttonText?: string;
Expand All @@ -15,24 +14,21 @@ type RowProps = {
label: string;
spinnerClass?: string;
stylekitClass?: string;
subRows?: RowProps[];
subRows?: MenuRowProps[];
subtitle?: string;
};

type Props = RowProps;
type Props = MenuRowProps;

export class MenuRow extends Component<Props> {
onClick = ($event: Event) => {
if (this.props.disabled || !this.props.action) {
return;
}

$event.stopPropagation();

if (this.props.actionArgs) {
this.props.action(...this.props.actionArgs);
} else {
this.props.action();
}
this.props.action();
};

clickAccessoryButton = ($event: Event) => {
Expand Down Expand Up @@ -81,7 +77,6 @@ export class MenuRow extends Component<Props> {
return (
<MenuRow
action={row.action}
actionArgs={row.actionArgs}
label={row.label}
spinnerClass={row.spinnerClass}
subtitle={row.subtitle}
Expand Down
2 changes: 1 addition & 1 deletion app/assets/javascripts/components/NoteView/NoteView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1112,7 +1112,7 @@ export class NoteView extends PureComponent<Props, State> {
<div className="sk-label">Actions</div>
{this.state.showActionsMenu && (
<ActionsMenu
item={this.note}
note={this.note}
application={this.application}
/>
)}
Expand Down
112 changes: 56 additions & 56 deletions app/assets/javascripts/preferences/panes/Listed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,72 +5,74 @@ import {
Title,
Subtitle,
Text,
LinkButton,
} from '../components';
import { observer } from 'mobx-react-lite';
import { WebApplication } from '@/ui_models/application';
import { ContentType, SNActionsExtension } from '@standardnotes/snjs';
import { SNItem } from '@standardnotes/snjs/dist/@types/models/core/item';
import { ButtonType, ListedAccount } from '@standardnotes/snjs';
import { useCallback, useEffect, useState } from 'preact/hooks';
import { BlogItem } from './listed/BlogItem';
import { ListedAccountItem } from './listed/BlogItem';
import { Button } from '@/components/Button';

type Props = {
application: WebApplication;
};

export const Listed = observer(({ application }: Props) => {
const [items, setItems] = useState<SNActionsExtension[]>([]);
const [isDeleting, setIsDeleting] = useState(false);
const [accounts, setAccounts] = useState<ListedAccount[]>([]);
const [requestingAccount, setRequestingAccount] = useState<boolean>();

const reloadItems = useCallback(() => {
const components = application
.getItems(ContentType.ActionsExtension)
.filter((item) =>
(item as SNActionsExtension).url.includes('listed')
) as SNActionsExtension[];
setItems(components);
const reloadAccounts = useCallback(async () => {
setAccounts(await application.getListedAccounts());
}, [application]);

useEffect(() => {
reloadItems();
}, [reloadItems]);
reloadAccounts();
}, [reloadAccounts]);

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);
});
});
};
const registerNewAccount = useCallback(() => {
setRequestingAccount(true);

const requestAccount = async () => {
const account = await application.requestNewListedAccount();
if (account) {
const openSettings = await application.alertService.confirm(
`Your new Listed blog has been successfully created!` +
` You can publish a new post to your blog from Standard Notes via the` +
` <i>Actions</i> menu in the editor pane. Open your blog settings to begin setting it up.`,
undefined,
'Open Settings',
ButtonType.Info,
'Later'
);
reloadAccounts();
if (openSettings) {
const info = await application.getListedAccountInfo(account);
if (info) {
application.deviceInterface.openUrl(info?.settings_url);
}
}
}
setRequestingAccount(false);
};

requestAccount();
}, [application, reloadAccounts]);

return (
<PreferencesPane>
{items.length > 0 && (
{accounts.length > 0 && (
<PreferencesGroup>
<PreferencesSegment>
<Title>
Your {items.length === 1 ? 'Blog' : 'Blogs'} on Listed
Your {accounts.length === 1 ? 'Blog' : 'Blogs'} on Listed
</Title>
<div className="h-2 w-full" />
{items.map((item, index, array) => {
{accounts.map((item, index, array) => {
return (
<BlogItem
item={item}
<ListedAccountItem
account={item}
showSeparator={index !== array.length - 1}
disabled={isDeleting}
disconnect={disconnectListedBlog}
key={item.uuid}
key={item.authorId}
application={application}
/>
);
Expand All @@ -95,21 +97,19 @@ export const Listed = observer(({ application }: Props) => {
</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}
<PreferencesSegment>
<Subtitle>Get Started</Subtitle>
<Text>Create a free Listed author account to get started.</Text>
<Button
className="mt-3"
type="normal"
disabled={requestingAccount}
label={
requestingAccount ? 'Creating account...' : 'Create New Author'
}
onClick={registerNewAccount}
/>
</PreferencesSegment>
</PreferencesGroup>
</PreferencesPane>
);
Expand Down
93 changes: 20 additions & 73 deletions app/assets/javascripts/preferences/panes/listed/BlogItem.tsx
Original file line number Diff line number Diff line change
@@ -1,107 +1,54 @@
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,
SNItem,
} from '@standardnotes/snjs';
import { ListedAccount, ListedAccountInfo } from '@standardnotes/snjs';
import { FunctionalComponent } from 'preact';
import { useEffect, useState } from 'preact/hooks';

type Props = {
item: SNActionsExtension;
account: ListedAccount;
showSeparator: boolean;
disabled: boolean;
disconnect: (item: SNItem) => Promise<unknown>;
application: WebApplication;
};

export const BlogItem: FunctionalComponent<Props> = ({
item,
export const ListedAccountItem: FunctionalComponent<Props> = ({
account,
showSeparator,
disabled,
disconnect,
application,
}) => {
const [actions, setActions] = useState<Action[] | undefined>([]);
const [isLoadingActions, setIsLoadingActions] = useState(false);
const [isDisconnecting, setIsDisconnecting] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [accountInfo, setAccountInfo] = useState<ListedAccountInfo>();

useEffect(() => {
const loadActions = async () => {
setIsLoadingActions(true);
application.actionsManager
.loadExtensionInContextOfItem(item, item)
.then((extension) => {
setActions(extension?.actions);
})
.catch((err) => application.alertService.alert(err))
.finally(() => {
setIsLoadingActions(false);
});
const loadAccount = async () => {
setIsLoading(true);
const info = await application.getListedAccountInfo(account);
setAccountInfo(info);
setIsLoading(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);
});
};
loadAccount();
}, [account, application]);

return (
<>
<Subtitle>{item?.name}</Subtitle>
<Subtitle className="em">{accountInfo?.display_name}</Subtitle>
<div className="mb-2" />
<div className="flex">
{isLoadingActions ? (
<div className="sk-spinner small info"></div>
) : null}
{actions && actions?.length > 0 ? (
{isLoading ? <div className="sk-spinner small info"></div> : null}
{accountInfo && (
<>
<LinkButton
className="mr-2"
label="Open Blog"
link={
actions?.find((action: Action) => action.label === 'Open Blog')
?.url || ''
}
link={accountInfo.author_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}
link={accountInfo.settings_url}
/>
</>
) : null}
)}
</div>
{showSeparator && <HorizontalSeparator classes="mt-5 mb-3" />}
</>
Expand Down
20 changes: 10 additions & 10 deletions app/assets/javascripts/ui_models/app_state/actions_menu_state.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import { UuidString } from "@standardnotes/snjs";
import { action, makeObservable, observable } from "mobx";
import { UuidString } from '@standardnotes/snjs';
import { action, makeObservable, observable } from 'mobx';

export class ActionsMenuState {
hiddenExtensions: Record<UuidString, boolean> = {};
hiddenSections: Record<UuidString, boolean> = {};

constructor() {
makeObservable(this, {
hiddenExtensions: observable,
toggleExtensionVisibility: action,
hiddenSections: observable,
toggleSectionVisibility: action,
reset: action,
});
}

toggleExtensionVisibility = (uuid: UuidString): void => {
this.hiddenExtensions[uuid] = !this.hiddenExtensions[uuid];
}
toggleSectionVisibility = (uuid: UuidString): void => {
this.hiddenSections[uuid] = !this.hiddenSections[uuid];
};

reset = (): void => {
this.hiddenExtensions = {};
}
this.hiddenSections = {};
};
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
"@reach/tooltip": "^0.16.2",
"@standardnotes/components": "1.4.4",
"@standardnotes/features": "1.26.1",
"@standardnotes/snjs": "2.49.4",
"@standardnotes/snjs": "2.50.0",
"@standardnotes/settings": "^1.11.2",
"@standardnotes/sncrypto-web": "1.6.2",
"mobx": "^6.3.5",
Expand Down
Loading