Skip to content

Commit

Permalink
feat(ui): use keyboard shortcuts for article page
Browse files Browse the repository at this point in the history
  • Loading branch information
ncarlier committed Apr 13, 2019
1 parent 71d7af4 commit 810fc7a
Show file tree
Hide file tree
Showing 11 changed files with 97 additions and 72 deletions.
5 changes: 4 additions & 1 deletion ui/src/articles/ArticlePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import Panel from '../common/Panel'
import { Category } from '../categories/models'
import ButtonIcon from '../common/ButtonIcon'
import MarkAsButton from './components/MarkAsButton'
import ArticleMenu from './components/ArticleMenu'

type Props = {
category?: Category
Expand Down Expand Up @@ -46,7 +47,9 @@ export default ({ category, match }: AllProps) => {
<>
{article !== null ?
<>
<ArticleHeader article={article} />
<ArticleHeader article={article}>
<ArticleMenu article={article} />
</ArticleHeader>
<ArticleContent article={article} />
<MarkAsButton article={article} floating />
</>
Expand Down
66 changes: 19 additions & 47 deletions ui/src/articles/components/ArchiveLink.tsx
Original file line number Diff line number Diff line change
@@ -1,86 +1,58 @@
import React, { useState, useCallback } from 'react'
import { useMutation, useApolloClient } from 'react-apollo-hooks'
import React, { useCallback } from 'react'
import { useMutation } from 'react-apollo-hooks'

import { Article } from '../models'

import { ArchiveArticle } from '../queries'
import { getGQLError } from '../../common/helpers'
import { GetArchiveServicesResponse } from '../../settings/archive-services/models'
import { GetArchiveServices } from '../../settings/archive-services/queries'
import { ArchiveService } from '../../settings/archive-services/models'
import { IMessageDispatchProps, connectMessageDispatch } from '../../containers/MessageContainer';
import useConfirmModal from '../../hooks/useConfirmModal'
import LinkIcon from '../../common/LinkIcon'
import useKeyboard from '../../hooks/useKeyboard'

type ArchiveArticleFields = {
id: number
archiver: string
noShortcuts?: boolean
}

type Props = {
article: Article
service: ArchiveService
noShortcuts?: boolean
}

type AllProps = Props & IMessageDispatchProps

export const ArchiveLink = (props: AllProps) => {
const {
article,
showMessage
service,
showMessage,
noShortcuts,
} = props

const client = useApolloClient()
const [loading, setLoading] = useState(false)
const archiveArticleMutation = useMutation<ArchiveArticleFields>(ArchiveArticle)
const [showNoArchiveServiceModal] = useConfirmModal(
'Archiving',
<p>
No archive service configured.<br/>
Please configure one in the <a href="/settings/archive-services">setting page</a>.
</p>
)

const archiveArticle = async (alias: string) => {
await archiveArticleMutation({
variables: {id: article.id, archiver: alias}
})
}

const selectAndUseArchiveService = async () => {
const archiveArticle = useCallback(async () => {
try {
setLoading(true)
const { errors, data } = await client.query<GetArchiveServicesResponse>({
query: GetArchiveServices,
await archiveArticleMutation({
variables: {id: article.id, archiver: service.alias}
})
if (data) {
if (data.archivers.length === 0) {
showNoArchiveServiceModal()
} else if (data.archivers.length > 1) {
// TODO: Show choosing modal
showMessage('You have to choose a default service archiver. Abort.')
} else {
await archiveArticle(data.archivers[0].alias)
showMessage(`Article put offline: ${article.title}`)
}
}
setLoading(false)
if (errors) {
throw new Error(errors[0])
}
showMessage(`Article sent to ${service.alias}: ${article.title}`)
} catch (err) {
showMessage(getGQLError(err), true)
}
}

const handleOnClick = useCallback(() => {
selectAndUseArchiveService()
}, [article])

useKeyboard('s', archiveArticle, service.is_default && !noShortcuts)

return (
<LinkIcon
title="Save to your cloud provider"
title={`Save to ${service.alias}`}
icon="backup"
onClick={handleOnClick}>
<span>Save to...</span><small>[s]</small>
onClick={archiveArticle}>
<span>Save to {service.alias}</span>{service.is_default && !noShortcuts && <small className="keyb">[s]</small>}
</LinkIcon>
)
}
Expand Down
5 changes: 4 additions & 1 deletion ui/src/articles/components/ArticleCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import ArticleHeader from '../components/ArticleHeader'
import styles from './ArticleCard.module.css'
import Panel from '../../common/Panel'
import ArticleFooter from './ArticleFooter'
import ArticleMenu from './ArticleMenu'

type Props = {
article: Article
Expand All @@ -19,7 +20,9 @@ export default ({article, readMoreBasePath}: Props) => {

return (
<Panel>
<ArticleHeader article={article} to={readMorePath} />
<ArticleHeader article={article} to={readMorePath}>
<ArticleMenu article={article} noShortcuts />
</ArticleHeader>
<article className={ styles.summary }>
{ article.image &&
<Link
Expand Down
7 changes: 4 additions & 3 deletions ui/src/articles/components/ArticleHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react'
import React, { ReactNode } from 'react'

import { Article } from '../models'

Expand All @@ -12,11 +12,12 @@ import ArticleMenu from './ArticleMenu'
type Props = {
article: Article
to?: History.LocationDescriptor
children?: ReactNode
}

type AllProps = Props

export default ({article, to}: AllProps) => (
export default ({article, to, children}: AllProps) => (
<header className={styles.header}>
<h1>
<small>
Expand All @@ -36,7 +37,7 @@ export default ({article, to}: AllProps) => (
</span>
</h1>
<div className={styles.actions}>
<ArticleMenu article={article} />
{children}
</div>
</header>
)
30 changes: 24 additions & 6 deletions ui/src/articles/components/ArticleMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,47 @@ import { Article } from '../models'
import DropdownMenu from '../../common/DropdownMenu'
import OfflineLink from './OfflineLink'
import ArchiveLink from './ArchiveLink'
import { useQuery } from 'react-apollo-hooks'
import { GetArchiveServicesResponse } from '../../settings/archive-services/models'
import { GetArchiveServices } from '../../settings/archive-services/queries'
import { matchResponse } from '../../common/helpers'
import Loader from '../../common/Loader'

type Props = {
article: Article
noShortcuts?: boolean
}

type AllProps = Props

export default ({ article }: AllProps) => {
export default ({ article, noShortcuts = false }: AllProps) => {

const { data, error, loading } = useQuery<GetArchiveServicesResponse>(GetArchiveServices)

const renderArchiveServices = matchResponse<GetArchiveServicesResponse>({
Loading: () => <li><Loader /></li>,
Error: (err) => <li>{err.message}</li>,
Data: ({archivers}) => archivers.map((service) =>
<li key={`as-${service.id}`}>
<ArchiveLink article={article} service={service} noShortcuts={noShortcuts} />
</li>
),
Other: () => <li>Unknown error</li>
})

return (
<DropdownMenu>
<ul>
{ article.isOffline ?
<li>
<OfflineLink article={article} remove />
<OfflineLink article={article} remove noShortcuts={noShortcuts} />
</li>
:
<li>
<OfflineLink article={article} />
<OfflineLink article={article} noShortcuts={noShortcuts} />
</li>
}
<li>
<ArchiveLink article={article} />
</li>
{renderArchiveServices(data, error, loading)}
</ul>
</DropdownMenu>
)
Expand Down
9 changes: 7 additions & 2 deletions ui/src/articles/components/MarkAsButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { UpdateArticleStatus } from '../queries'
import { getGQLError } from '../../common/helpers'
import { updateCacheAfterUpdateStatus } from '../cache'
import { connectMessageDispatch, IMessageDispatchProps } from '../../containers/MessageContainer';
import useKeyboard from '../../hooks/useKeyboard';

type UpdateArticleStatusFields = {
id: number
Expand Down Expand Up @@ -51,10 +52,14 @@ export const MarkAsButton = (props: AllProps) => {
updateArticleStatus(status)
}, [article])

// Keyboard shortcut is only active for Floating Action Button
useKeyboard('m', handleOnClick, floating)
const kbs = floating ? " [m]" : ""

if (article.status === 'read') {
return (
<ButtonIcon
title="Mark as unread"
title={"Mark as unread" + kbs}
onClick={handleOnClick}
loading={loading}
floating={floating}
Expand All @@ -65,7 +70,7 @@ export const MarkAsButton = (props: AllProps) => {

return (
<ButtonIcon
title="Mark as read"
title={"Mark as read" + kbs}
onClick={handleOnClick}
loading={loading}
floating={floating}
Expand Down
13 changes: 9 additions & 4 deletions ui/src/articles/components/OfflineLink.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@

import React, { useState, ReactNode } from 'react'
import React from 'react'
import { useApolloClient } from 'react-apollo-hooks'

import {Article, GetArticleResponse} from '../models'
import ButtonIcon from '../../common/ButtonIcon'

import { GetFullArticle } from '../queries'
import { connectMessageDispatch, IMessageDispatchProps } from '../../containers/MessageContainer'
import { connectOfflineDispatch, IOfflineDispatchProps } from '../../containers/OfflineContainer'
import ConfirmDialog from '../../common/ConfirmDialog'
import { useModal } from 'react-modal-hook'
import LinkIcon from '../../common/LinkIcon'
import useKeyboard from '../../hooks/useKeyboard'

type Props = {
article: Article
remove?: boolean
noShortcuts?: boolean
}

type AllProps = Props & IMessageDispatchProps & IOfflineDispatchProps
Expand All @@ -23,6 +24,7 @@ export const OfflineLink = (props: AllProps) => {
const {
article,
remove,
noShortcuts,
saveOfflineArticle,
removeOfflineArticle,
showMessage
Expand Down Expand Up @@ -70,14 +72,17 @@ export const OfflineLink = (props: AllProps) => {
</ConfirmDialog>
)
)

useKeyboard('r', showDeleteConfirmModal, !noShortcuts && remove)
useKeyboard('o', putArticleOffline, !noShortcuts && !remove)

if (remove) {
return (
<LinkIcon
title="Remove"
onClick={showDeleteConfirmModal}
icon="delete">
<span>Remove offline</span><small>[r]</small>
<span>Remove offline</span>{!noShortcuts && <small>[r]</small>}
</LinkIcon>
)
}
Expand All @@ -87,7 +92,7 @@ export const OfflineLink = (props: AllProps) => {
title="Put offline"
onClick={putArticleOffline}
icon="signal_wifi_off">
<span>Put offline</span><small>[o]</small>
<span>Put offline</span>{!noShortcuts && <small className="keyb">[o]</small>}
</LinkIcon>
)
}
Expand Down
1 change: 0 additions & 1 deletion ui/src/common/LinkIcon.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,3 @@
color: gray;
padding-left: 0.5em;
}

10 changes: 6 additions & 4 deletions ui/src/hooks/useKeyboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import mousetrap from "mousetrap"

type KeyHandler = (e: ExtendedKeyboardEvent, combo: string) => void

export default (key: string, handler: KeyHandler) => {
export default (key: string, handler: KeyHandler, enable = true) => {
useEffect(() => {
mousetrap.unbind(key)
mousetrap.bind(key, handler)
return () => {
if (enable) {
mousetrap.unbind(key)
mousetrap.bind(key, handler)
return () => {
mousetrap.unbind(key)
}
}
}, [handler])
}
6 changes: 6 additions & 0 deletions ui/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ body {
filter: blur(0);
}

@media (max-width: 512px) {
.keyb {
display: none;
}
}

/*
* TABLE
*/
Expand Down
17 changes: 14 additions & 3 deletions ui/src/offline/OfflineArticlePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import { RouteComponentProps, Redirect } from 'react-router'
import ArticleHeader from '../articles/components/ArticleHeader'
import ArticleContent from '../articles/components/ArticleContent'
import { Article } from '../articles/models'
import { OfflineProps, connectOffline } from '../containers/OfflineContainer';
import { OfflineProps, connectOffline } from '../containers/OfflineContainer'
import ArticleMenu from '../articles/components/ArticleMenu'
import ButtonIcon from '../common/ButtonIcon'

type AllProps = RouteComponentProps<{id: string}> & OfflineProps

Expand All @@ -28,7 +30,9 @@ export const OfflineArticlePage = ({match, offlineArticles, fetchOfflineArticle}
Data: (a) => <>
{a !== null && (a.isOffline = true) ?
<>
<ArticleHeader article={a} />
<ArticleHeader article={a}>
<ArticleMenu article={a} />
</ArticleHeader>
<ArticleContent article={a} />
</>
: <ErrorPanel title="Not found">Article #${id} not found.</ErrorPanel>
Expand All @@ -38,7 +42,14 @@ export const OfflineArticlePage = ({match, offlineArticles, fetchOfflineArticle}
})

return (
<Page title="Offline articles" subtitle={data && data.title}>
<Page title="Offline articles" subtitle={data && data.title}
actions={
<ButtonIcon
to="/offline"
icon="arrow_back"
title="back to the list"
/>
}>
<Panel style={{flex: '1 1 auto'}}>
{render(data, error, loading)}
</Panel>
Expand Down

0 comments on commit 810fc7a

Please sign in to comment.