use _id & csp update

This commit is contained in:
刘浩远 2020-06-10 11:33:25 +08:00
parent 0128728889
commit 2ff5e13219
22 changed files with 207 additions and 102 deletions

View File

@ -3,12 +3,12 @@
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; img-src *; style-src 'self' 'unsafe-inline'; frame-src *; media-src *">
<title>Hello World!</title>
content="default-src 'none'; script-src-elem 'sha256-34JRfFnY5YiFDB1MABAIxq6OfvlgM/Ba2el4MdA0WoM='; img-src http://* https://*; style-src 'self' 'unsafe-inline'; frame-src http://* https://*; media-src http://* https://*">
<title>Article</title>
<link rel="stylesheet" href="article.css" />
</head>
<body>
<div id="main"></div>
<script src="article.js"></script>
<script integrity="sha256-34JRfFnY5YiFDB1MABAIxq6OfvlgM/Ba2el4MdA0WoM=" src="article.js"></script>
</body>
</html>

View File

@ -3,8 +3,20 @@ function get(name) {
return decodeURIComponent(name[1]);
}
document.documentElement.style.fontSize = get("s") + "px"
let html = decodeURIComponent(window.atob(get("h")))
let domParser = new DOMParser()
let dom = domParser.parseFromString(html, "text/html")
let baseEl = dom.createElement('base')
baseEl.setAttribute('href', get("u").split("/").slice(0, 3).join("/"))
dom.head.append(baseEl)
for (let i of dom.querySelectorAll("img")) {
i.src = i.src
}
for (let s of dom.querySelectorAll("script")) {
s.parentNode.removeChild(s)
}
let main = document.getElementById("main")
main.innerHTML = decodeURIComponent(window.atob(get("h")))
main.innerHTML = dom.body.innerHTML
document.addEventListener("click", event => {
event.preventDefault()
if (event.target.href) post("request-navigation", event.target.href)

1
dist/styles.css vendored
View File

@ -510,6 +510,7 @@ img.favicon {
transition: box-shadow linear .08s;
transform: scale(1);
cursor: pointer;
animation-fill-mode: none;
}
.card:hover {
box-shadow: #0006 0px 5px 40px;

View File

@ -13,6 +13,8 @@ type ArticleProps = {
source: RSSSource
dismiss: () => void
toggleHasRead: (item: RSSItem) => void
toggleStarred: (item: RSSItem) => void
toggleHidden: (item: RSSItem) => void
textMenu: (text: string, position: [number, number]) => void
}
@ -51,6 +53,22 @@ class Article extends React.Component<ArticleProps, ArticleState> {
}))
})
moreMenuProps = (): IContextualMenuProps => ({
items: [
{
key: "openInBrowser",
text: "在浏览器中打开",
iconProps: {iconName: "NavigateExternalInline"},
onClick: this.openInBrowser
},
{
key: "toggleHidden",
text: this.props.item.hidden ? "取消隐藏" : "隐藏文章",
onClick: () => { this.props.toggleHidden(this.props.item) }
}
]
})
ipcHandler = event => {
switch (event.channel) {
case "request-navigation": {
@ -84,7 +102,7 @@ class Article extends React.Component<ArticleProps, ArticleState> {
}
}
componentDidUpdate = (prevProps: ArticleProps) => {
if (prevProps.item.id != this.props.item.id) {
if (prevProps.item._id != this.props.item._id) {
this.setState({loadWebpage: this.props.source.openTarget === SourceOpenTarget.Webpage})
}
this.componentDidMount()
@ -112,7 +130,7 @@ class Article extends React.Component<ArticleProps, ArticleState> {
<p className="title">{this.props.item.title}</p>
<p className="date">{this.props.item.date.toLocaleString("zh-cn", {hour12: false})}</p>
<article dangerouslySetInnerHTML={{__html: this.props.item.content}}></article>
</>))) + "&s=" + this.state.fontSize
</>))) + `&s=${this.state.fontSize}&u=${this.props.item.link}`
render = () => (
<div className="article">
@ -128,11 +146,13 @@ class Article extends React.Component<ArticleProps, ArticleState> {
<CommandBarButton
title={this.props.item.hasRead ? "标为未读" : "标为已读"}
iconProps={this.props.item.hasRead
? {iconName: "RadioBtnOn", style: {fontSize: 14, textAlign: "center"}}
: {iconName: "StatusCircleRing"}}
? {iconName: "StatusCircleRing"}
: {iconName: "RadioBtnOn", style: {fontSize: 14, textAlign: "center"}}}
onClick={() => this.props.toggleHasRead(this.props.item)} />
<CommandBarButton
iconProps={{iconName: "FavoriteStar"}} />
title={this.props.item.starred ? "取消星标" : "标为星标"}
iconProps={{iconName: this.props.item.starred ? "FavoriteStarFill" : "FavoriteStar"}}
onClick={() => this.props.toggleStarred(this.props.item)} />
<CommandBarButton
title="字体大小"
disabled={this.state.loadWebpage}
@ -145,9 +165,10 @@ class Article extends React.Component<ArticleProps, ArticleState> {
iconProps={{iconName: "Globe"}}
onClick={this.toggleWebpage} />
<CommandBarButton
title="在浏览器中打开"
iconProps={{iconName: "NavigateExternalInline", style: {marginTop: -4}}}
onClick={this.openInBrowser} />
title="更多"
iconProps={{iconName: "More"}}
menuIconProps={{style: {display: "none"}}}
menuProps={this.moreMenuProps()} />
</Stack>
<Stack horizontal horizontalAlign="end" style={{width: 112}}>
<CommandBarButton
@ -158,7 +179,7 @@ class Article extends React.Component<ArticleProps, ArticleState> {
</Stack>
<webview
id="article"
key={this.props.item.id + (this.state.loadWebpage ? "_" : "")}
key={this.props.item._id + (this.state.loadWebpage ? "_" : "")}
src={this.state.loadWebpage ? this.props.item.link : this.articleView()}
preload={this.state.loadWebpage ? null : "article/preload.js"}
partition="sandbox" />

View File

@ -2,15 +2,14 @@ import * as React from "react"
import { openExternal } from "../../scripts/utils"
import { RSSSource, SourceOpenTarget } from "../../scripts/models/source"
import { RSSItem } from "../../scripts/models/item"
import { FeedIdType } from "../../scripts/models/feed"
export interface CardProps {
feedId: FeedIdType
feedId: string
item: RSSItem
source: RSSSource
markRead: (item: RSSItem) => void
contextMenu: (feedId: FeedIdType, item: RSSItem, e) => void
showItem: (fid: FeedIdType, item: RSSItem) => void
contextMenu: (feedId: string, item: RSSItem, e) => void
showItem: (fid: string, item: RSSItem) => void
}
export class Card extends React.Component<CardProps> {

View File

@ -5,7 +5,6 @@ import { ContextualMenu, IContextualMenuItem, ContextualMenuItemType, Directiona
import { ContextMenuType } from "../scripts/models/app"
import { RSSItem } from "../scripts/models/item"
import { ContextReduxProps } from "../containers/context-menu-container"
import { FeedIdType } from "../scripts/models/feed"
import { ViewType } from "../scripts/models/page"
export type ContextMenuProps = ContextReduxProps & {
@ -13,12 +12,14 @@ export type ContextMenuProps = ContextReduxProps & {
event?: MouseEvent | string
position?: [number, number]
item?: RSSItem
feedId?: FeedIdType
feedId?: string
text?: string
viewType: ViewType
showItem: (feedId: FeedIdType, item: RSSItem) => void
showItem: (feedId: string, item: RSSItem) => void
markRead: (item: RSSItem) => void
markUnread: (item: RSSItem) => void
toggleStarred: (item: RSSItem) => void
toggleHidden: (item: RSSItem) => void
switchView: (viewType: ViewType) => void
close: () => void
}
@ -59,8 +60,15 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
onClick: () => { this.props.markRead(this.props.item) }
},
{
key: "markBelowAsRead",
text: "将以下标为已读"
key: "toggleStarred",
text: this.props.item.starred ? "取消星标" : "标为星标",
iconProps: { iconName: this.props.item.starred ? "FavoriteStar" : "FavoriteStarFill" },
onClick: () => { this.props.toggleStarred(this.props.item) }
},
{
key: "toggleHidden",
text: this.props.item.hidden ? "取消隐藏" : "隐藏文章",
onClick: () => { this.props.toggleHidden(this.props.item) }
},
{
key: "divider_1",

View File

@ -34,8 +34,8 @@ class CardsFeed extends React.Component<FeedProps> {
{
this.props.items.map((item) => (
<DefaultCard
feedId={this.props.feed.id}
key={item.id}
feedId={this.props.feed._id}
key={item._id}
item={item}
source={this.props.sourceMap[item.source]}
markRead={this.props.markRead}

View File

@ -1,7 +1,7 @@
import * as React from "react"
import { RSSItem } from "../../scripts/models/item"
import { FeedReduxProps } from "../../containers/feed-container"
import { RSSFeed, FeedIdType } from "../../scripts/models/feed"
import { RSSFeed } from "../../scripts/models/feed"
import { ViewType } from "../../scripts/models/page"
import CardsFeed from "./cards-feed"
import ListFeed from "./list-feed"
@ -12,9 +12,9 @@ export type FeedProps = FeedReduxProps & {
items: RSSItem[]
sourceMap: Object
markRead: (item: RSSItem) => void
contextMenu: (feedId: FeedIdType, item: RSSItem, e) => void
contextMenu: (feedId: string, item: RSSItem, e) => void
loadMore: (feed: RSSFeed) => void
showItem: (fid: FeedIdType, item: RSSItem) => void
showItem: (fid: string, item: RSSItem) => void
}
export class Feed extends React.Component<FeedProps> {

View File

@ -10,8 +10,8 @@ class ListFeed extends React.Component<FeedProps> {
{
this.props.items.map((item) => (
<ListCard
feedId={this.props.feed.id}
key={item.id}
feedId={this.props.feed._id}
key={item._id}
item={item}
source={this.props.sourceMap[item.source]}
markRead={this.props.markRead}

View File

@ -1,5 +1,4 @@
import * as React from "react"
import { FeedIdType } from "../scripts/models/feed"
import { FeedContainer } from "../containers/feed-container"
import { AnimationClassNames, Icon } from "@fluentui/react"
import ArticleContainer from "../containers/article-container"
@ -8,8 +7,8 @@ import { ViewType } from "../scripts/models/page"
type PageProps = {
menuOn: boolean
settingsOn: boolean
feeds: FeedIdType[]
itemId: number
feeds: string[]
itemId: string
viewType: ViewType
dismissItem: () => void
offsetItem: (offset: number) => void
@ -34,7 +33,7 @@ class Page extends React.Component<PageProps> {
<FeedContainer viewType={this.props.viewType} feedId={fid} key={fid} />
))}
</div>}
{this.props.itemId >= 0 && (
{this.props.itemId && (
<div className="article-container" onClick={this.props.dismissItem}>
<div className={"article-wrapper " + AnimationClassNames.slideUpIn20} onClick={e => e.stopPropagation()}>
<ArticleContainer itemId={this.props.itemId} />
@ -54,7 +53,7 @@ class Page extends React.Component<PageProps> {
<FeedContainer viewType={this.props.viewType} feedId={fid} key={fid} />
))}
</div>
{this.props.itemId >= 0 && (
{this.props.itemId && (
<div className="side-article-wrapper">
<ArticleContainer itemId={this.props.itemId} />
</div>

View File

@ -1,14 +1,14 @@
import { connect } from "react-redux"
import { createSelector } from "reselect"
import { RootState } from "../scripts/reducer"
import { RSSItem, markUnread, markRead } from "../scripts/models/item"
import { RSSItem, markUnread, markRead, toggleStarred, toggleHidden } from "../scripts/models/item"
import { AppDispatch } from "../scripts/utils"
import { dismissItem } from "../scripts/models/page"
import Article from "../components/article"
import { openTextMenu } from "../scripts/models/app"
type ArticleContainerProps = {
itemId: number
itemId: string
}
const getItem = (state: RootState, props: ArticleContainerProps) => state.items[props.itemId]
@ -28,6 +28,8 @@ const mapDispatchToProps = (dispatch: AppDispatch) => {
return {
dismiss: () => dispatch(dismissItem()),
toggleHasRead: (item: RSSItem) => dispatch(item.hasRead ? markUnread(item) : markRead(item)),
toggleStarred: (item: RSSItem) => dispatch(toggleStarred(item)),
toggleHidden: (item: RSSItem) => dispatch(toggleHidden(item)),
textMenu: (text: string, position: [number, number]) => dispatch(openTextMenu(text, position))
}
}

View File

@ -3,9 +3,8 @@ import { createSelector } from "reselect"
import { RootState } from "../scripts/reducer"
import { ContextMenuType, closeContextMenu } from "../scripts/models/app"
import { ContextMenu } from "../components/context-menu"
import { RSSItem, markRead, markUnread } from "../scripts/models/item"
import { RSSItem, markRead, markUnread, toggleStarred, toggleHidden } from "../scripts/models/item"
import { showItem, switchView, ViewType } from "../scripts/models/page"
import { FeedIdType } from "../scripts/models/feed"
import { setDefaultView } from "../scripts/utils"
const getContext = (state: RootState) => state.app.contextMenu
@ -38,9 +37,11 @@ const mapStateToProps = createSelector(
const mapDispatchToProps = dispatch => {
return {
showItem: (feedId: FeedIdType, item: RSSItem) => dispatch(showItem(feedId, item)),
showItem: (feedId: string, item: RSSItem) => dispatch(showItem(feedId, item)),
markRead: (item: RSSItem) => dispatch(markRead(item)),
markUnread: (item: RSSItem) => dispatch(markUnread(item)),
toggleStarred: (item: RSSItem) => dispatch(toggleStarred(item)),
toggleHidden: (item: RSSItem) => dispatch(toggleHidden(item)),
switchView: (viewType: ViewType) => {
setDefaultView(viewType)
dispatch(switchView(viewType))

View File

@ -3,12 +3,12 @@ import { createSelector } from "reselect"
import { RootState } from "../scripts/reducer"
import { markRead, RSSItem } from "../scripts/models/item"
import { openItemMenu } from "../scripts/models/app"
import { FeedIdType, loadMore, RSSFeed } from "../scripts/models/feed"
import { loadMore, RSSFeed } from "../scripts/models/feed"
import { showItem, ViewType } from "../scripts/models/page"
import { Feed } from "../components/feeds/feed"
interface FeedContainerProps {
feedId: FeedIdType
feedId: string
viewType: ViewType
}
@ -31,9 +31,9 @@ const makeMapStateToProps = () => {
const mapDispatchToProps = dispatch => {
return {
markRead: (item: RSSItem) => dispatch(markRead(item)),
contextMenu: (feedId: FeedIdType, item: RSSItem, e) => dispatch(openItemMenu(item, feedId, e)),
contextMenu: (feedId: string, item: RSSItem, e) => dispatch(openItemMenu(item, feedId, e)),
loadMore: (feed: RSSFeed) => dispatch(loadMore(feed)),
showItem: (fid: FeedIdType, item: RSSItem) => dispatch(showItem(fid, item))
showItem: (fid: string, item: RSSItem) => dispatch(showItem(fid, item))
}
}

View File

@ -7,7 +7,7 @@ import { ViewType } from "../scripts/models/page"
import Nav from "../components/nav"
const getState = (state: RootState) => state.app
const getItemShown = (state: RootState) => (state.page.itemId >= 0) && state.page.viewType !== ViewType.List
const getItemShown = (state: RootState) => state.page.itemId && state.page.viewType !== ViewType.List
const mapStateToProps = createSelector(
[getState, getItemShown],

View File

@ -2,7 +2,7 @@
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src *; style-src 'self' 'unsafe-inline'; font-src 'self' https://static2.sharepointonline.com; connect-src *">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self'; img-src *; style-src 'self' 'unsafe-inline'; font-src 'self' https://static2.sharepointonline.com; connect-src https://* http://*">
<title>Fluent Reader</title>
<link rel="stylesheet" href="styles.css">
</head>

View File

@ -20,5 +20,6 @@ export const idb = new Datastore<RSSItem>({
if (err) window.console.log(err)
}
})
idb.ensureIndex({ fieldName: "id", unique: true })
idb.removeIndex("id")
idb.update({}, {$unset: {id: true}}, {multi: true})
//idb.remove({}, { multi: true })

View File

@ -1,7 +1,7 @@
import { RSSSource, INIT_SOURCES, SourceActionTypes, ADD_SOURCE, UPDATE_SOURCE, DELETE_SOURCE } from "./source"
import { RSSItem, ItemActionTypes, FETCH_ITEMS } from "./item"
import { ActionStatus, AppThunk, getWindowBreakpoint } from "../utils"
import { INIT_FEEDS, FeedActionTypes, ALL, initFeeds, FeedIdType } from "./feed"
import { INIT_FEEDS, FeedActionTypes, ALL, initFeeds } from "./feed"
import { SourceGroupActionTypes, UPDATE_SOURCE_GROUP, ADD_SOURCE_TO_GROUP, DELETE_SOURCE_GROUP, REMOVE_SOURCE_FROM_GROUP } from "./group"
import { PageActionTypes, SELECT_PAGE, PageType, selectAllArticles } from "./page"
@ -51,7 +51,7 @@ export class AppState {
type: ContextMenuType,
event?: MouseEvent | string,
position?: [number, number],
target?: [RSSItem, FeedIdType] | RSSSource | string
target?: [RSSItem, string] | RSSSource | string
}
constructor() {
@ -74,7 +74,7 @@ interface OpenItemMenuAction {
type: typeof OPEN_ITEM_MENU
event: MouseEvent
item: RSSItem
feedId: FeedIdType
feedId: string
}
interface OpenTextMenuAction {
@ -109,7 +109,7 @@ export function closeContextMenu(): ContextMenuActionTypes {
return { type: CLOSE_CONTEXT_MENU }
}
export function openItemMenu(item: RSSItem, feedId: FeedIdType, event: React.MouseEvent): ContextMenuActionTypes {
export function openItemMenu(item: RSSItem, feedId: string, event: React.MouseEvent): ContextMenuActionTypes {
return {
type: OPEN_ITEM_MENU,
event: event.nativeEvent,

View File

@ -6,20 +6,19 @@ import { PageActionTypes, SELECT_PAGE, PageType } from "./page"
export const ALL = "ALL"
export const SOURCE = "SOURCE"
export type FeedIdType = number | string
const LOAD_QUANTITY = 50
export class RSSFeed {
id: FeedIdType
_id: string
loaded: boolean
loading: boolean
allLoaded: boolean
sids: number[]
iids: number[]
iids: string[]
constructor (id: FeedIdType, sids=[]) {
this.id = id
constructor (id: string = null, sids=[]) {
this._id = id
this.sids = sids
this.iids = []
this.loaded = false
@ -44,7 +43,7 @@ export class RSSFeed {
}
export type FeedState = {
[id in FeedIdType]: RSSFeed
[_id: string]: RSSFeed
}
export const INIT_FEEDS = 'INIT_FEEDS'
@ -200,7 +199,7 @@ export function feedReducer(
let nextState = { ...state }
for (let k of Object.keys(state)) {
if (state[k].loaded) {
let iids = action.items.filter(i => state[k].sids.includes(i.source)).map(i => i.id)
let iids = action.items.filter(i => state[k].sids.includes(i.source)).map(i => i._id)
if (iids.length > 0) {
nextState[k] = {
...nextState[k],
@ -217,11 +216,11 @@ export function feedReducer(
switch (action.status) {
case ActionStatus.Success: return {
...state,
[action.feed.id]: {
[action.feed._id]: {
...action.feed,
loaded: true,
allLoaded: action.items.length < LOAD_QUANTITY,
iids: action.items.map(i => i.id)
iids: action.items.map(i => i._id)
}
}
default: return state
@ -230,23 +229,23 @@ export function feedReducer(
switch (action.status) {
case ActionStatus.Request: return {
...state,
[action.feed.id] : {
[action.feed._id] : {
...action.feed,
loading: true
}
}
case ActionStatus.Success: return {
...state,
[action.feed.id] : {
[action.feed._id] : {
...action.feed,
loading: false,
allLoaded: action.items.length < LOAD_QUANTITY,
iids: [...action.feed.iids, ...action.items.map(i => i.id)]
iids: [...action.feed.iids, ...action.items.map(i => i._id)]
}
}
case ActionStatus.Failure: return {
...state,
[action.feed.id] : {
[action.feed._id] : {
...action.feed,
loading: false
}

View File

@ -1,7 +1,7 @@
import fs = require("fs")
import { SourceActionTypes, ADD_SOURCE, DELETE_SOURCE, addSource } from "./source"
import { ActionStatus, AppThunk, domParser, AppDispatch, getWindowBreakpoint } from "../utils"
import { ActionStatus, AppThunk, domParser, AppDispatch } from "../utils"
import { saveSettings } from "./app"
const GROUPS_STORE_KEY = "sourceGroups"

View File

@ -5,7 +5,7 @@ import { FeedActionTypes, INIT_FEED, LOAD_MORE } from "./feed"
import Parser = require("@yang991178/rss-parser")
export class RSSItem {
id: number
_id: string
source: number
title: string
link: string
@ -14,20 +14,25 @@ export class RSSItem {
thumb?: string
content: string
snippet: string
creator: string
categories: string[]
creator?: string
categories?: string[]
hasRead: boolean
starred?: true
hidden?: true
constructor (item: Parser.Item, source: RSSSource) {
this.source = source.sid
this.title = item.title
this.link = item.link
this.date = new Date(item.isoDate)
this.title = item.title || ""
this.link = item.link || ""
this.fetchedDate = new Date()
this.date = item.isoDate ? new Date(item.isoDate) : this.fetchedDate
if (item.thumb) this.thumb = item.thumb
else if (item.image) this.thumb = item.image
else {
let dom = domParser.parseFromString(item.content, "text/html")
let baseEl = dom.createElement('base')
baseEl.setAttribute('href', this.link.split("/").slice(0, 3).join("/"))
dom.head.append(baseEl)
let img = dom.querySelector("img")
if (img && img.src) this.thumb = img.src
}
@ -35,8 +40,8 @@ export class RSSItem {
this.content = item.fullContent
this.snippet = htmlDecode(item.fullContent)
} else {
this.content = item.content
this.snippet = htmlDecode(item.contentSnippet)
this.content = item.content || ""
this.snippet = htmlDecode(item.contentSnippet || "")
}
this.creator = item.creator
this.categories = item.categories
@ -45,12 +50,14 @@ export class RSSItem {
}
export type ItemState = {
[id: number]: RSSItem
[_id: string]: RSSItem
}
export const FETCH_ITEMS = 'FETCH_ITEMS'
export const MARK_READ = "MARK_READ"
export const MARK_UNREAD = "MARK_UNREAD"
export const TOGGLE_STARRED = "TOGGLE_STARRED"
export const TOGGLE_HIDDEN = "TOGGLE_HIDDEN"
interface FetchItemsAction {
type: typeof FETCH_ITEMS
@ -71,7 +78,17 @@ interface MarkUnreadAction {
item: RSSItem
}
export type ItemActionTypes = FetchItemsAction | MarkReadAction | MarkUnreadAction
interface ToggleStarredAction {
type: typeof TOGGLE_STARRED
item: RSSItem
}
interface ToggleHiddenAction {
type: typeof TOGGLE_HIDDEN
item: RSSItem
}
export type ItemActionTypes = FetchItemsAction | MarkReadAction | MarkUnreadAction | ToggleStarredAction | ToggleHiddenAction
export function fetchItemsRequest(fetchCount = 0): ItemActionTypes {
return {
@ -107,20 +124,13 @@ export function fetchItemsIntermediate(): ItemActionTypes {
export function insertItems(items: RSSItem[]): Promise<RSSItem[]> {
return new Promise<RSSItem[]>((resolve, reject) => {
db.idb.find({}).projection({ id: 1 }).sort({ id: -1 }).limit(1).exec((err, docs) => {
items.sort((a, b) => a.date.getTime() - b.date.getTime())
db.idb.insert(items, (err, inserted) => {
if (err) {
reject(err)
} else {
resolve(inserted)
}
let count = (docs.length == 0) ? 0 : (docs[0].id + 1)
items.sort((a, b) => a.date.getTime() - b.date.getTime())
for (let i of items) i.id = count++
db.idb.insert(items, (err) => {
if (err) {
reject(err)
} else {
resolve(items)
}
})
})
})
}
@ -145,8 +155,8 @@ export function fetchItems(): AppThunk<Promise<void>> {
}
})
insertItems(items)
.then(() => {
dispatch(fetchItemsSuccess(items.reverse()))
.then(inserted => {
dispatch(fetchItemsSuccess(inserted.reverse()))
resolve()
})
.catch(err => {
@ -159,30 +169,62 @@ export function fetchItems(): AppThunk<Promise<void>> {
}
}
export const markReadDone = (item: RSSItem): ItemActionTypes => ({
const markReadDone = (item: RSSItem): ItemActionTypes => ({
type: MARK_READ,
item: item
})
export const markUnreadDone = (item: RSSItem): ItemActionTypes => ({
const markUnreadDone = (item: RSSItem): ItemActionTypes => ({
type: MARK_UNREAD,
item: item
})
export function markRead(item: RSSItem): AppThunk {
return (dispatch) => {
db.idb.update({ id: item.id }, { $set: { hasRead: true } })
db.idb.update({ _id: item._id }, { $set: { hasRead: true } })
dispatch(markReadDone(item))
}
}
export function markUnread(item: RSSItem): AppThunk {
return (dispatch) => {
db.idb.update({ id: item.id }, { $set: { hasRead: false } })
db.idb.update({ _id: item._id }, { $set: { hasRead: false } })
dispatch(markUnreadDone(item))
}
}
const toggleStarredDone = (item: RSSItem): ItemActionTypes => ({
type: TOGGLE_STARRED,
item: item
})
export function toggleStarred(item: RSSItem): AppThunk {
return (dispatch) => {
if (item.starred === true) {
db.idb.update({ _id: item._id }, { $unset: { starred: true } })
} else {
db.idb.update({ _id: item._id }, { $set: { starred: true } })
}
dispatch(toggleStarredDone(item))
}
}
const toggleHiddenDone = (item: RSSItem): ItemActionTypes => ({
type: TOGGLE_HIDDEN,
item: item
})
export function toggleHidden(item: RSSItem): AppThunk {
return (dispatch) => {
if (item.hidden === true) {
db.idb.update({ _id: item._id }, { $unset: { hidden: true } })
} else {
db.idb.update({ _id: item._id }, { $set: { hidden: true } })
}
dispatch(toggleHiddenDone(item))
}
}
export function itemReducer(
state: ItemState = {},
action: ItemActionTypes | FeedActionTypes
@ -193,7 +235,7 @@ export function itemReducer(
case ActionStatus.Success: {
let newMap = {}
for (let i of action.items) {
newMap[i.id] = i
newMap[i._id] = i
}
return {...newMap, ...state}
}
@ -202,18 +244,36 @@ export function itemReducer(
case MARK_UNREAD:
case MARK_READ: return {
...state,
[action.item.id] : {
[action.item._id] : {
...action.item,
hasRead: action.type === MARK_READ
}
}
case TOGGLE_STARRED: {
let newItem = { ...action.item }
if (newItem.starred === true) delete newItem.starred
else newItem.starred = true
return {
...state,
[newItem._id]: newItem
}
}
case TOGGLE_HIDDEN: {
let newItem = { ...action.item }
if (newItem.hidden === true) delete newItem.hidden
else newItem.hidden = true
return {
...state,
[newItem._id]: newItem
}
}
case LOAD_MORE:
case INIT_FEED: {
switch (action.status) {
case ActionStatus.Success: {
let nextState = { ...state }
for (let i of action.items) {
nextState[i.id] = i
nextState[i._id] = i
}
return nextState
}

View File

@ -1,4 +1,4 @@
import { ALL, SOURCE, FeedIdType, loadMore } from "./feed"
import { ALL, SOURCE, loadMore } from "./feed"
import { getWindowBreakpoint, AppThunk, getDefaultView } from "../utils"
import { RSSItem, markRead } from "./item"
import { SourceActionTypes, DELETE_SOURCE } from "./source"
@ -34,7 +34,7 @@ interface SwitchViewAction {
interface ShowItemAction {
type: typeof SHOW_ITEM
feedId: FeedIdType
feedId: string
item: RSSItem
}
@ -70,7 +70,7 @@ export function switchView(viewType: ViewType): PageActionTypes {
}
}
export function showItem(feedId: FeedIdType, item: RSSItem): PageActionTypes {
export function showItem(feedId: string, item: RSSItem): PageActionTypes {
return {
type: SHOW_ITEM,
feedId: feedId,
@ -109,8 +109,8 @@ export function showOffsetItem(offset: number): AppThunk {
export class PageState {
viewType = getDefaultView()
feedId = ALL as FeedIdType
itemId = -1
feedId = ALL
itemId = null as string
}
export function pageReducer(
@ -133,16 +133,16 @@ export function pageReducer(
case SWITCH_VIEW: return {
...state,
viewType: action.viewType,
itemId: action.viewType === ViewType.List ? state.itemId : -1
itemId: action.viewType === ViewType.List ? state.itemId : null
}
case SHOW_ITEM: return {
...state,
itemId: action.item.id
itemId: action.item._id
}
case DELETE_SOURCE:
case DISMISS_ITEM: return {
...state,
itemId: -1
itemId: null
}
default: return state
}

View File

@ -2,6 +2,7 @@ import { shell, remote } from "electron"
import { ThunkAction, ThunkDispatch } from "redux-thunk"
import { AnyAction } from "redux"
import { RootState } from "./reducer"
import URL = require("url")
export enum ActionStatus {
Request, Success, Failure, Intermediate
@ -49,6 +50,7 @@ export function setProxy(address = null) {
import ElectronProxyAgent = require("@yang991178/electron-proxy-agent")
import { ViewType } from "./models/page"
import { RSSSource } from "./models/source"
let agent = new ElectronProxyAgent(remote.getCurrentWebContents().session)
export const rssParser = new Parser({
customFields: customFields,