diff --git a/dist/styles.css b/dist/styles.css index 2615609..b652e70 100644 --- a/dist/styles.css +++ b/dist/styles.css @@ -240,6 +240,22 @@ nav.menu-on .btn-group .btn.system, nav.item-on .btn-group .btn.system { margin: 2px 8px; user-select: none; } +.menu .link-stack { + overflow: hidden; +} +.menu .link-text { + margin-top: 0px; + margin-right: 4px; + margin-bottom: 0px; + margin-left: 4px; + text-align: left; + text-overflow: ellipsis; + overflow: hidden; + flex-grow: 1; +} +.menu .unread-count { + color: var(--neutralSecondary); +} .settings-container { position: fixed; diff --git a/screenshot.jpg b/screenshot.jpg new file mode 100644 index 0000000..d724ad0 Binary files /dev/null and b/screenshot.jpg differ diff --git a/src/components/menu.tsx b/src/components/menu.tsx index 95471f3..fa0e888 100644 --- a/src/components/menu.tsx +++ b/src/components/menu.tsx @@ -5,7 +5,7 @@ import { Nav, INavLink, INavLinkGroup } from "office-ui-fabric-react/lib/Nav" import { SourceGroup } from "../scripts/models/group" import { SourceState, RSSSource } from "../scripts/models/source" import { ALL } from "../scripts/models/feed" -import { AnimationClassNames } from "@fluentui/react" +import { AnimationClassNames, Stack } from "@fluentui/react" export type MenuProps = { status: boolean, @@ -20,6 +20,8 @@ export type MenuProps = { } export class Menu extends React.Component<MenuProps> { + countOverflow = (count: number) => count >= 1000 ? "999+" : String(count) + getItems = (): INavLinkGroup[] => [ { links: [ @@ -31,6 +33,7 @@ export class Menu extends React.Component<MenuProps> { }, { name: intl.get("allArticles"), + ariaLabel: this.countOverflow(Object.values(this.props.sources).map(s => s.unreadCount).reduce((a, b) => a + b, 0)), key: ALL, icon: "TextDocument", onClick: this.props.allArticles, @@ -43,13 +46,15 @@ export class Menu extends React.Component<MenuProps> { getGroups = (): INavLinkGroup[] => [{ links: this.props.groups.filter(g => g.sids.length > 0).map((g, i) => { if (g.isMultiple) { + let sources = g.sids.map(sid => this.props.sources[sid]) return { name: g.name, + ariaLabel: this.countOverflow(sources.map(s => s.unreadCount).reduce((a, b) => a + b, 0)), key: "g-" + i, url: null, isExpanded: true, onClick: () => this.props.selectSourceGroup(g, "g-" + i), - links: g.sids.map(sid => this.props.sources[sid]).map(this.getSource) + links: sources.map(this.getSource) } } else { return this.getSource(this.props.sources[g.sids[0]]) @@ -59,6 +64,7 @@ export class Menu extends React.Component<MenuProps> { getSource = (s: RSSSource): INavLink => ({ name: s.name, + ariaLabel: this.countOverflow(s.unreadCount), key: "s-" + s.sid, onClick: () => this.props.selectSource(s), iconProps: s.iconurl ? this.getIconStyle(s.iconurl) : null, @@ -74,6 +80,16 @@ export class Menu extends React.Component<MenuProps> { } }) + _onRenderLink = (link: INavLink): JSX.Element => { + return ( + <Stack className="link-stack" horizontal grow> + <div className="link-text">{link.name}</div> + {link.ariaLabel !== "0" && <div className="unread-count">{link.ariaLabel}</div>} + </Stack> + ) + return ; + }; + render() { return this.props.status && ( <div className="menu-container" onClick={this.props.toggleMenu} style={{display: this.props.display ? "block" : "none"}}> @@ -84,11 +100,13 @@ export class Menu extends React.Component<MenuProps> { </div> <div className="nav-wrapper"> <Nav + onRenderLink={this._onRenderLink} groups={this.getItems()} selectedKey={this.props.selected} /> <p className={"subs-header " + AnimationClassNames.slideDownIn10}>{intl.get("menu.subscriptions")}</p> <Nav selectedKey={this.props.selected} + onRenderLink={this._onRenderLink} groups={this.getGroups()} /> </div> </div> diff --git a/src/containers/context-menu-container.tsx b/src/containers/context-menu-container.tsx index dfa3a17..e32edf0 100644 --- a/src/containers/context-menu-container.tsx +++ b/src/containers/context-menu-container.tsx @@ -44,7 +44,13 @@ const mapDispatchToProps = dispatch => { markRead: (item: RSSItem) => dispatch(markRead(item)), markUnread: (item: RSSItem) => dispatch(markUnread(item)), toggleStarred: (item: RSSItem) => dispatch(toggleStarred(item)), - toggleHidden: (item: RSSItem) => dispatch(toggleHidden(item)), + toggleHidden: (item: RSSItem) => { + if(!item.hasRead) { + dispatch(markRead(item)) + item.hasRead = true // get around chaining error + } + dispatch(toggleHidden(item)) + }, switchView: (viewType: ViewType) => { setDefaultView(viewType) dispatch(switchView(viewType)) diff --git a/src/scripts/db.ts b/src/scripts/db.ts index 05b17ca..caca713 100644 --- a/src/scripts/db.ts +++ b/src/scripts/db.ts @@ -20,6 +20,7 @@ export const idb = new Datastore<RSSItem>({ if (err) window.console.log(err) } }) +idb.ensureIndex({ fieldName: "source" }) //idb.removeIndex("id") //idb.update({}, {$unset: {id: true}}, {multi: true}) //idb.remove({}, { multi: true }) \ No newline at end of file diff --git a/src/scripts/models/source.ts b/src/scripts/models/source.ts index 48ce2db..b593531 100644 --- a/src/scripts/models/source.ts +++ b/src/scripts/models/source.ts @@ -1,7 +1,7 @@ import Parser = require("@yang991178/rss-parser") import * as db from "../db" import { rssParser, faviconPromise, ActionStatus, AppThunk } from "../utils" -import { RSSItem, insertItems } from "./item" +import { RSSItem, insertItems, ItemActionTypes, FETCH_ITEMS, MARK_READ, MARK_UNREAD } from "./item" import { SourceGroup } from "./group" import { saveSettings } from "./app" @@ -16,6 +16,7 @@ export class RSSSource { name: string description: string openTarget: SourceOpenTarget + unreadCount: number constructor(url: string, name: string = null) { this.url = url @@ -139,17 +140,35 @@ export function initSourcesFailure(err): SourceActionTypes { } } +function unreadCount(source: RSSSource): Promise<RSSSource> { + return new Promise<RSSSource>((resolve, reject) => { + db.idb.count({ source: source.sid, hasRead: false }, (err, n) => { + if (err) { + reject(err) + } else { + source.unreadCount = n + resolve(source) + } + }) + }) +} + export function initSources(): AppThunk<Promise<void>> { return (dispatch) => { dispatch(initSourcesRequest()) return new Promise<void>((resolve, reject) => { - db.sdb.find({}).sort({ sid: 1 }).exec((err, docs) => { + db.sdb.find({}).sort({ sid: 1 }).exec((err, sources) => { if (err) { dispatch(initSourcesFailure(err)) reject(err) } else { - dispatch(initSourcesSuccess(docs)) - resolve() + let p = sources.map(s => unreadCount(s)) + Promise.all(p) + .then(values => { + dispatch(initSourcesSuccess(values)) + resolve() + }) + .catch(err => reject(err)) } }) }) @@ -197,6 +216,7 @@ export function addSource(url: string, name: string = null, batch = false): AppT if (err) { reject(err) } else { + source.unreadCount = feed.items.length dispatch(addSourceSuccess(source, batch)) RSSSource.checkItems(source, feed.items, db.idb) .then(items => insertItems(items)) @@ -228,7 +248,10 @@ export function updateSourceDone(source: RSSSource): SourceActionTypes { export function updateSource(source: RSSSource): AppThunk { return (dispatch) => { - db.sdb.update({ sid: source.sid }, { $set: { ...source }}, {}, err => { + let sourceCopy = { ...source } + delete sourceCopy.sid + delete sourceCopy.unreadCount + db.sdb.update({ sid: source.sid }, { $set: { ...sourceCopy }}, {}, err => { if (!err) { dispatch(updateSourceDone(source)) } @@ -268,7 +291,7 @@ export function deleteSource(source: RSSSource): AppThunk { export function sourceReducer( state: SourceState = {}, - action: SourceActionTypes + action: SourceActionTypes | ItemActionTypes ): SourceState { switch (action.type) { case INIT_SOURCES: @@ -298,6 +321,40 @@ export function sourceReducer( delete state[action.source.sid] return { ...state } } + case FETCH_ITEMS: { + switch (action.status) { + case ActionStatus.Success: { + let updateMap = new Map<number, number>() + for (let item of action.items) { + updateMap.set( + item.source, + updateMap.has(item.source) ? (updateMap.get(item.source) + 1) : 1) + } + let nextState = {} as SourceState + for (let s in state) { + let sid = parseInt(s) + if (updateMap.has(sid)) { + nextState[sid] = { + ...state[sid], + unreadCount: state[sid].unreadCount + updateMap.get(sid) + } as RSSSource + } else { + nextState[sid] = state[sid] + } + } + return nextState + } + default: return state + } + } + case MARK_UNREAD: + case MARK_READ: return { + ...state, + [action.item.source]: { + ...state[action.item.source], + unreadCount: state[action.item.source].unreadCount + (action.type === MARK_UNREAD ? 1 : -1) + } as RSSSource + } default: return state } } \ No newline at end of file