unread count

This commit is contained in:
刘浩远 2020-06-12 13:11:40 +08:00
parent 69f5a21038
commit 6e7f2cf915
6 changed files with 107 additions and 9 deletions

16
dist/styles.css vendored
View File

@ -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;

BIN
screenshot.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

View File

@ -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>

View File

@ -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))

View File

@ -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 })

View File

@ -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
}
}