Skip to content

Commit

Permalink
feat: native listed integration (#846)
Browse files Browse the repository at this point in the history
* feat(wip): native listed integration

* feat(wip): wip

* feat: simplified actions menu structure

* feat: open settings alert upon succesful creation

* fix: handle remove menu row api

* chore(deps): snjs
  • Loading branch information
moughxyz authored Feb 4, 2022
1 parent 4200baa commit 6970a37
Show file tree
Hide file tree
Showing 9 changed files with 290 additions and 411 deletions.
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

0 comments on commit 6970a37

Please sign in to comment.