refactor context menu as function component

This commit is contained in:
Bruce Liu 2025-03-02 21:41:11 -08:00
parent ba9edbd65b
commit 5ecf189ed6
5 changed files with 576 additions and 694 deletions

View File

@ -13,38 +13,25 @@ import {
ContextualMenuItemType, ContextualMenuItemType,
DirectionalHint, DirectionalHint,
} from "office-ui-fabric-react/lib/ContextualMenu" } from "office-ui-fabric-react/lib/ContextualMenu"
import { ContextMenuType } from "../scripts/models/app" import { closeContextMenu, ContextMenuType } from "../scripts/models/app"
import { RSSItem } from "../scripts/models/item" import {
import { ContextReduxProps } from "../containers/context-menu-container" markAllRead,
markRead,
markUnread,
RSSItem,
toggleHidden,
toggleStarred,
} from "../scripts/models/item"
import { ViewType, ImageCallbackTypes, ViewConfigs } from "../schema-types" import { ViewType, ImageCallbackTypes, ViewConfigs } from "../schema-types"
import { FilterType } from "../scripts/models/feed" import { FilterType } from "../scripts/models/feed"
import { useAppDispatch, useAppSelector } from "../scripts/reducer"
export type ContextMenuProps = ContextReduxProps & { import {
type: ContextMenuType setViewConfigs,
event?: MouseEvent | string showItem,
position?: [number, number] switchFilter,
item?: RSSItem switchView,
feedId?: string toggleFilter,
text?: string } from "../scripts/models/page"
url?: string
viewType?: ViewType
viewConfigs?: ViewConfigs
filter?: FilterType
sids?: number[]
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
setViewConfigs: (configs: ViewConfigs) => void
switchFilter: (filter: FilterType) => void
toggleFilter: (filter: FilterType) => void
markAllRead: (sids?: number[], date?: Date, before?: boolean) => void
fetchItems: (sids: number[]) => void
settings: (sids: number[]) => void
close: () => void
}
export const shareSubmenu = (item: RSSItem): IContextualMenuItem[] => [ export const shareSubmenu = (item: RSSItem): IContextualMenuItem[] => [
{ key: "qr", url: item.link, onRender: renderShareQR }, { key: "qr", url: item.link, onRender: renderShareQR },
@ -69,21 +56,42 @@ function getSearchItem(text: string): IContextualMenuItem {
} }
} }
export class ContextMenu extends React.Component<ContextMenuProps> { export function ContextMenu() {
getItems = (): IContextualMenuItem[] => { const { type } = useAppSelector(state => state.app.contextMenu)
switch (this.props.type) {
switch (type) {
case ContextMenuType.Hidden:
return null
case ContextMenuType.Item: case ContextMenuType.Item:
return [ return <ItemContextMenu />
case ContextMenuType.Text:
return <TextContextMenu />
case ContextMenuType.Image:
return <ImageContextMenu />
case ContextMenuType.View:
return <ViewContextMenu />
case ContextMenuType.Group:
return <GroupContextMenu />
case ContextMenuType.MarkRead:
return <MarkReadContextMenu />
}
}
function ItemContextMenu() {
const dispatch = useAppDispatch()
const viewConfigs = useAppSelector(state => state.page.viewConfigs)
const target = useAppSelector(state => state.app.contextMenu.target)
const item = target[0] as RSSItem
const feedId = target[1] as string
const menuItems: IContextualMenuItem[] = [
{ {
key: "showItem", key: "showItem",
text: intl.get("context.read"), text: intl.get("context.read"),
iconProps: { iconName: "TextDocument" }, iconProps: { iconName: "TextDocument" },
onClick: () => { onClick: () => {
this.props.markRead(this.props.item) dispatch(markRead(item))
this.props.showItem( dispatch(showItem(feedId, item))
this.props.feedId,
this.props.item
)
}, },
}, },
{ {
@ -91,28 +99,27 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
text: intl.get("openExternal"), text: intl.get("openExternal"),
iconProps: { iconName: "NavigateExternalInline" }, iconProps: { iconName: "NavigateExternalInline" },
onClick: e => { onClick: e => {
this.props.markRead(this.props.item) dispatch(markRead(item))
window.utils.openExternal( window.utils.openExternal(item.link, platformCtrl(e))
this.props.item.link,
platformCtrl(e)
)
}, },
}, },
{ {
key: "markAsRead", key: "markAsRead",
text: this.props.item.hasRead text: item.hasRead
? intl.get("article.markUnread") ? intl.get("article.markUnread")
: intl.get("article.markRead"), : intl.get("article.markRead"),
iconProps: this.props.item.hasRead iconProps: item.hasRead
? { ? {
iconName: "RadioBtnOn", iconName: "RadioBtnOn",
style: { fontSize: 14, textAlign: "center" }, style: { fontSize: 14, textAlign: "center" },
} }
: { iconName: "StatusCircleRing" }, : { iconName: "StatusCircleRing" },
onClick: () => { onClick: () => {
if (this.props.item.hasRead) if (item.hasRead) {
this.props.markUnread(this.props.item) dispatch(markUnread(item))
else this.props.markRead(this.props.item) } else {
dispatch(markRead(item))
}
}, },
split: true, split: true,
subMenuProps: { subMenuProps: {
@ -124,11 +131,9 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
iconName: "Down", iconName: "Down",
style: { fontSize: 14 }, style: { fontSize: 14 },
}, },
onClick: () => onClick: () => {
this.props.markAllRead( dispatch(markAllRead(null, item.date))
null, },
this.props.item.date
),
}, },
{ {
key: "markAbove", key: "markAbove",
@ -137,40 +142,35 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
iconName: "Up", iconName: "Up",
style: { fontSize: 14 }, style: { fontSize: 14 },
}, },
onClick: () => onClick: () => {
this.props.markAllRead( dispatch(markAllRead(null, item.date, false))
null, },
this.props.item.date,
false
),
}, },
], ],
}, },
}, },
{ {
key: "toggleStarred", key: "toggleStarred",
text: this.props.item.starred text: item.starred
? intl.get("article.unstar") ? intl.get("article.unstar")
: intl.get("article.star"), : intl.get("article.star"),
iconProps: { iconProps: {
iconName: this.props.item.starred iconName: item.starred ? "FavoriteStar" : "FavoriteStarFill",
? "FavoriteStar"
: "FavoriteStarFill",
}, },
onClick: () => { onClick: () => {
this.props.toggleStarred(this.props.item) dispatch(toggleStarred(item))
}, },
}, },
{ {
key: "toggleHidden", key: "toggleHidden",
text: this.props.item.hidden text: item.hidden
? intl.get("article.unhide") ? intl.get("article.unhide")
: intl.get("article.hide"), : intl.get("article.hide"),
iconProps: { iconProps: {
iconName: this.props.item.hidden ? "View" : "Hide3", iconName: item.hidden ? "View" : "Hide3",
}, },
onClick: () => { onClick: () => {
this.props.toggleHidden(this.props.item) dispatch(toggleHidden(item))
}, },
}, },
{ {
@ -182,24 +182,24 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
text: intl.get("context.share"), text: intl.get("context.share"),
iconProps: { iconName: "Share" }, iconProps: { iconName: "Share" },
subMenuProps: { subMenuProps: {
items: shareSubmenu(this.props.item), items: shareSubmenu(item),
}, },
}, },
{ {
key: "copyTitle", key: "copyTitle",
text: intl.get("context.copyTitle"), text: intl.get("context.copyTitle"),
onClick: () => { onClick: () => {
window.utils.writeClipboard(this.props.item.title) window.utils.writeClipboard(item.title)
}, },
}, },
{ {
key: "copyURL", key: "copyURL",
text: intl.get("context.copyURL"), text: intl.get("context.copyURL"),
onClick: () => { onClick: () => {
window.utils.writeClipboard(this.props.item.link) window.utils.writeClipboard(item.link)
}, },
}, },
...(this.props.viewConfigs !== undefined ...(viewConfigs !== undefined
? [ ? [
{ {
key: "divider_2", key: "divider_2",
@ -212,50 +212,46 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
items: [ items: [
{ {
key: "showCover", key: "showCover",
text: intl.get( text: intl.get("context.showCover"),
"context.showCover"
),
canCheck: true, canCheck: true,
checked: Boolean( checked: Boolean(
this.props.viewConfigs & viewConfigs & ViewConfigs.ShowCover
ViewConfigs.ShowCover
), ),
onClick: () => onClick: () =>
this.props.setViewConfigs( dispatch(
this.props.viewConfigs ^ setViewConfigs(
viewConfigs ^
ViewConfigs.ShowCover ViewConfigs.ShowCover
)
), ),
}, },
{ {
key: "showSnippet", key: "showSnippet",
text: intl.get( text: intl.get("context.showSnippet"),
"context.showSnippet"
),
canCheck: true, canCheck: true,
checked: Boolean( checked: Boolean(
this.props.viewConfigs & viewConfigs & ViewConfigs.ShowSnippet
ViewConfigs.ShowSnippet
), ),
onClick: () => onClick: () =>
this.props.setViewConfigs( dispatch(
this.props.viewConfigs ^ setViewConfigs(
viewConfigs ^
ViewConfigs.ShowSnippet ViewConfigs.ShowSnippet
)
), ),
}, },
{ {
key: "fadeRead", key: "fadeRead",
text: intl.get( text: intl.get("context.fadeRead"),
"context.fadeRead"
),
canCheck: true, canCheck: true,
checked: Boolean( checked: Boolean(
this.props.viewConfigs & viewConfigs & ViewConfigs.FadeRead
ViewConfigs.FadeRead
), ),
onClick: () => onClick: () =>
this.props.setViewConfigs( dispatch(
this.props.viewConfigs ^ setViewConfigs(
ViewConfigs.FadeRead viewConfigs ^ ViewConfigs.FadeRead
)
), ),
}, },
], ],
@ -264,26 +260,35 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
] ]
: []), : []),
] ]
case ContextMenuType.Text: { return <ContextMenuBase menuItems={menuItems} />
const items: IContextualMenuItem[] = this.props.text }
function TextContextMenu() {
const target = useAppSelector(state => state.app.contextMenu.target) as [
string,
string
]
const text = target[0]
const url = target[1]
const menuItems: IContextualMenuItem[] = text
? [ ? [
{ {
key: "copyText", key: "copyText",
text: intl.get("context.copy"), text: intl.get("context.copy"),
iconProps: { iconName: "Copy" }, iconProps: { iconName: "Copy" },
onClick: () => { onClick: () => {
window.utils.writeClipboard(this.props.text) window.utils.writeClipboard(text)
}, },
}, },
getSearchItem(this.props.text), getSearchItem(text),
] ]
: [] : []
if (this.props.url) { if (url) {
items.push({ menuItems.push({
key: "urlSection", key: "urlSection",
itemType: ContextualMenuItemType.Section, itemType: ContextualMenuItemType.Section,
sectionProps: { sectionProps: {
topDivider: items.length > 0, topDivider: menuItems.length > 0,
items: [ items: [
{ {
key: "openInBrowser", key: "openInBrowser",
@ -292,10 +297,7 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
iconName: "NavigateExternalInline", iconName: "NavigateExternalInline",
}, },
onClick: e => { onClick: e => {
window.utils.openExternal( window.utils.openExternal(url, platformCtrl(e))
this.props.url,
platformCtrl(e)
)
}, },
}, },
{ {
@ -303,19 +305,18 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
text: intl.get("context.copyURL"), text: intl.get("context.copyURL"),
iconProps: { iconName: "Link" }, iconProps: { iconName: "Link" },
onClick: () => { onClick: () => {
window.utils.writeClipboard( window.utils.writeClipboard(url)
this.props.url
)
}, },
}, },
], ],
}, },
}) })
} }
return items return <ContextMenuBase menuItems={menuItems} />
} }
case ContextMenuType.Image:
return [ function ImageContextMenu() {
const menuItems: IContextualMenuItem[] = [
{ {
key: "openInBrowser", key: "openInBrowser",
text: intl.get("openExternal"), text: intl.get("openExternal"),
@ -326,9 +327,7 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
ImageCallbackTypes.OpenExternalBg ImageCallbackTypes.OpenExternalBg
) )
} else { } else {
window.utils.imageCallback( window.utils.imageCallback(ImageCallbackTypes.OpenExternal)
ImageCallbackTypes.OpenExternal
)
} }
}, },
}, },
@ -337,9 +336,7 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
text: intl.get("context.saveImageAs"), text: intl.get("context.saveImageAs"),
iconProps: { iconName: "SaveTemplate" }, iconProps: { iconName: "SaveTemplate" },
onClick: () => { onClick: () => {
window.utils.imageCallback( window.utils.imageCallback(ImageCallbackTypes.SaveAs)
ImageCallbackTypes.SaveAs
)
}, },
}, },
{ {
@ -355,14 +352,19 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
text: intl.get("context.copyImageURL"), text: intl.get("context.copyImageURL"),
iconProps: { iconName: "Link" }, iconProps: { iconName: "Link" },
onClick: () => { onClick: () => {
window.utils.imageCallback( window.utils.imageCallback(ImageCallbackTypes.CopyLink)
ImageCallbackTypes.CopyLink
)
}, },
}, },
] ]
case ContextMenuType.View: return <ContextMenuBase menuItems={menuItems} />
return [ }
function ViewContextMenu() {
const dispatch = useAppDispatch()
const viewType = useAppSelector(state => state.page.viewType)
const filter = useAppSelector(state => state.page.filter.type)
const menuItems: IContextualMenuItem[] = [
{ {
key: "section_1", key: "section_1",
itemType: ContextualMenuItemType.Section, itemType: ContextualMenuItemType.Section,
@ -375,44 +377,32 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
text: intl.get("context.cardView"), text: intl.get("context.cardView"),
iconProps: { iconName: "GridViewMedium" }, iconProps: { iconName: "GridViewMedium" },
canCheck: true, canCheck: true,
checked: checked: viewType === ViewType.Cards,
this.props.viewType === ViewType.Cards, onClick: () => dispatch(switchView(ViewType.Cards)),
onClick: () =>
this.props.switchView(ViewType.Cards),
}, },
{ {
key: "listView", key: "listView",
text: intl.get("context.listView"), text: intl.get("context.listView"),
iconProps: { iconName: "BacklogList" }, iconProps: { iconName: "BacklogList" },
canCheck: true, canCheck: true,
checked: checked: viewType === ViewType.List,
this.props.viewType === ViewType.List, onClick: () => dispatch(switchView(ViewType.List)),
onClick: () =>
this.props.switchView(ViewType.List),
}, },
{ {
key: "magazineView", key: "magazineView",
text: intl.get("context.magazineView"), text: intl.get("context.magazineView"),
iconProps: { iconName: "Articles" }, iconProps: { iconName: "Articles" },
canCheck: true, canCheck: true,
checked: checked: viewType === ViewType.Magazine,
this.props.viewType === onClick: () => dispatch(switchView(ViewType.Magazine)),
ViewType.Magazine,
onClick: () =>
this.props.switchView(
ViewType.Magazine
),
}, },
{ {
key: "compactView", key: "compactView",
text: intl.get("context.compactView"), text: intl.get("context.compactView"),
iconProps: { iconName: "BulletedList" }, iconProps: { iconName: "BulletedList" },
canCheck: true, canCheck: true,
checked: checked: viewType === ViewType.Compact,
this.props.viewType === onClick: () => dispatch(switchView(ViewType.Compact)),
ViewType.Compact,
onClick: () =>
this.props.switchView(ViewType.Compact),
}, },
], ],
}, },
@ -430,13 +420,10 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
iconProps: { iconName: "ClearFilter" }, iconProps: { iconName: "ClearFilter" },
canCheck: true, canCheck: true,
checked: checked:
(this.props.filter & (filter & ~FilterType.Toggles) ==
~FilterType.Toggles) ==
FilterType.Default, FilterType.Default,
onClick: () => onClick: () =>
this.props.switchFilter( dispatch(switchFilter(FilterType.Default)),
FilterType.Default
),
}, },
{ {
key: "unreadOnly", key: "unreadOnly",
@ -450,13 +437,10 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
}, },
canCheck: true, canCheck: true,
checked: checked:
(this.props.filter & (filter & ~FilterType.Toggles) ==
~FilterType.Toggles) ==
FilterType.UnreadOnly, FilterType.UnreadOnly,
onClick: () => onClick: () =>
this.props.switchFilter( dispatch(switchFilter(FilterType.UnreadOnly)),
FilterType.UnreadOnly
),
}, },
{ {
key: "starredOnly", key: "starredOnly",
@ -464,13 +448,10 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
iconProps: { iconName: "FavoriteStarFill" }, iconProps: { iconName: "FavoriteStarFill" },
canCheck: true, canCheck: true,
checked: checked:
(this.props.filter & (filter & ~FilterType.Toggles) ==
~FilterType.Toggles) ==
FilterType.StarredOnly, FilterType.StarredOnly,
onClick: () => onClick: () =>
this.props.switchFilter( dispatch(switchFilter(FilterType.StarredOnly)),
FilterType.StarredOnly
),
}, },
], ],
}, },
@ -493,28 +474,18 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
children: "Aa", children: "Aa",
}, },
canCheck: true, canCheck: true,
checked: !( checked: !(filter & FilterType.CaseInsensitive),
this.props.filter &
FilterType.CaseInsensitive
),
onClick: () => onClick: () =>
this.props.toggleFilter( dispatch(toggleFilter(FilterType.CaseInsensitive)),
FilterType.CaseInsensitive
),
}, },
{ {
key: "fullSearch", key: "fullSearch",
text: intl.get("context.fullSearch"), text: intl.get("context.fullSearch"),
iconProps: { iconName: "Breadcrumb" }, iconProps: { iconName: "Breadcrumb" },
canCheck: true, canCheck: true,
checked: Boolean( checked: Boolean(filter & FilterType.FullSearch),
this.props.filter &
FilterType.FullSearch
),
onClick: () => onClick: () =>
this.props.toggleFilter( dispatch(toggleFilter(FilterType.FullSearch)),
FilterType.FullSearch
),
}, },
], ],
}, },
@ -523,36 +494,52 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
key: "showHidden", key: "showHidden",
text: intl.get("context.showHidden"), text: intl.get("context.showHidden"),
canCheck: true, canCheck: true,
checked: Boolean( checked: Boolean(filter & FilterType.ShowHidden),
this.props.filter & FilterType.ShowHidden onClick: () => dispatch(toggleFilter(FilterType.ShowHidden)),
),
onClick: () =>
this.props.toggleFilter(FilterType.ShowHidden),
}, },
] ]
case ContextMenuType.Group: return <ContextMenuBase menuItems={menuItems} />
return [ }
function GroupContextMenu() {
const dispatch = useAppDispatch()
const sids = useAppSelector(
state => state.app.contextMenu.target
) as number[]
const menuItems: IContextualMenuItem[] = [
{ {
key: "markAllRead", key: "markAllRead",
text: intl.get("nav.markAllRead"), text: intl.get("nav.markAllRead"),
iconProps: { iconName: "CheckMark" }, iconProps: { iconName: "CheckMark" },
onClick: () => this.props.markAllRead(this.props.sids), onClick: () => {
dispatch(markAllRead(sids))
},
}, },
{ {
key: "refresh", key: "refresh",
text: intl.get("nav.refresh"), text: intl.get("nav.refresh"),
iconProps: { iconName: "Sync" }, iconProps: { iconName: "Sync" },
onClick: () => this.props.fetchItems(this.props.sids), onClick: () => {
dispatch(markAllRead(sids))
},
}, },
{ {
key: "manage", key: "manage",
text: intl.get("context.manageSources"), text: intl.get("context.manageSources"),
iconProps: { iconName: "Settings" }, iconProps: { iconName: "Settings" },
onClick: () => this.props.settings(this.props.sids), onClick: () => {
dispatch(markAllRead(sids))
},
}, },
] ]
case ContextMenuType.MarkRead: return <ContextMenuBase menuItems={menuItems} />
return [ }
function MarkReadContextMenu() {
const dispatch = useAppDispatch()
const menuItems: IContextualMenuItem[] = [
{ {
key: "section_1", key: "section_1",
itemType: ContextualMenuItemType.Section, itemType: ContextualMenuItemType.Section,
@ -563,7 +550,9 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
key: "all", key: "all",
text: intl.get("allArticles"), text: intl.get("allArticles"),
iconProps: { iconName: "ReceiptCheck" }, iconProps: { iconName: "ReceiptCheck" },
onClick: () => this.props.markAllRead(), onClick: () => {
dispatch(markAllRead())
},
}, },
{ {
key: "1d", key: "1d",
@ -571,7 +560,7 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
onClick: () => { onClick: () => {
let date = new Date() let date = new Date()
date.setTime(date.getTime() - 86400000) date.setTime(date.getTime() - 86400000)
this.props.markAllRead(null, date) dispatch(markAllRead(null, date))
}, },
}, },
{ {
@ -579,10 +568,8 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
text: intl.get("app.daysAgo", { days: 3 }), text: intl.get("app.daysAgo", { days: 3 }),
onClick: () => { onClick: () => {
let date = new Date() let date = new Date()
date.setTime( date.setTime(date.getTime() - 3 * 86400000)
date.getTime() - 3 * 86400000 dispatch(markAllRead(null, date))
)
this.props.markAllRead(null, date)
}, },
}, },
{ {
@ -590,35 +577,35 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
text: intl.get("app.daysAgo", { days: 7 }), text: intl.get("app.daysAgo", { days: 7 }),
onClick: () => { onClick: () => {
let date = new Date() let date = new Date()
date.setTime( date.setTime(date.getTime() - 7 * 86400000)
date.getTime() - 7 * 86400000 dispatch(markAllRead(null, date))
)
this.props.markAllRead(null, date)
}, },
}, },
], ],
}, },
}, },
] ]
default: return <ContextMenuBase menuItems={menuItems} />
return []
}
} }
render() { function ContextMenuBase({
return this.props.type == ContextMenuType.Hidden ? null : ( menuItems,
}: Readonly<{ menuItems: IContextualMenuItem[] }>) {
const { event, position } = useAppSelector(state => state.app.contextMenu)
const dispatch = useAppDispatch()
return (
<ContextualMenu <ContextualMenu
directionalHint={DirectionalHint.bottomLeftEdge} directionalHint={DirectionalHint.bottomLeftEdge}
items={this.getItems()} items={menuItems}
target={ target={
this.props.event || event ||
(this.props.position && { (position && {
left: this.props.position[0], left: position[0],
top: this.props.position[1], top: position[1],
}) })
} }
onDismiss={this.props.close} onDismiss={() => dispatch(closeContextMenu())}
/> />
) )
} }
}

View File

@ -1,6 +1,5 @@
import * as React from "react" import * as React from "react"
import { connect } from "react-redux" import { connect } from "react-redux"
import { ContextMenuContainer } from "../containers/context-menu-container"
import { closeContextMenu } from "../scripts/models/app" import { closeContextMenu } from "../scripts/models/app"
import PageContainer from "../containers/page-container" import PageContainer from "../containers/page-container"
import MenuContainer from "../containers/menu-container" import MenuContainer from "../containers/menu-container"
@ -8,6 +7,7 @@ import NavContainer from "../containers/nav-container"
import LogMenuContainer from "../containers/log-menu-container" import LogMenuContainer from "../containers/log-menu-container"
import SettingsContainer from "../containers/settings-container" import SettingsContainer from "../containers/settings-container"
import { RootState } from "../scripts/reducer" import { RootState } from "../scripts/reducer"
import { ContextMenu } from "./context-menu"
const Root = ({ locale, dispatch }) => const Root = ({ locale, dispatch }) =>
locale && ( locale && (
@ -20,7 +20,7 @@ const Root = ({ locale, dispatch }) =>
<LogMenuContainer /> <LogMenuContainer />
<MenuContainer /> <MenuContainer />
<SettingsContainer /> <SettingsContainer />
<ContextMenuContainer /> <ContextMenu />
</div> </div>
) )

View File

@ -1,115 +0,0 @@
import { connect } from "react-redux"
import { createSelector } from "reselect"
import { RootState } from "../scripts/reducer"
import {
ContextMenuType,
closeContextMenu,
toggleSettings,
} from "../scripts/models/app"
import { ContextMenu } from "../components/context-menu"
import {
RSSItem,
markRead,
markUnread,
toggleStarred,
toggleHidden,
markAllRead,
fetchItems,
} from "../scripts/models/item"
import {
showItem,
switchView,
switchFilter,
toggleFilter,
setViewConfigs,
} from "../scripts/models/page"
import { ViewType, ViewConfigs } from "../schema-types"
import { FilterType } from "../scripts/models/feed"
const getContext = (state: RootState) => state.app.contextMenu
const getViewType = (state: RootState) => state.page.viewType
const getFilter = (state: RootState) => state.page.filter
const getViewConfigs = (state: RootState) => state.page.viewConfigs
const mapStateToProps = createSelector(
[getContext, getViewType, getFilter, getViewConfigs],
(context, viewType, filter, viewConfigs) => {
switch (context.type) {
case ContextMenuType.Item:
return {
type: context.type,
event: context.event,
viewConfigs: viewConfigs,
item: context.target[0],
feedId: context.target[1],
}
case ContextMenuType.Text:
return {
type: context.type,
position: context.position,
text: context.target[0],
url: context.target[1],
}
case ContextMenuType.View:
return {
type: context.type,
event: context.event,
viewType: viewType,
filter: filter.type,
}
case ContextMenuType.Group:
return {
type: context.type,
event: context.event,
sids: context.target,
}
case ContextMenuType.Image:
return {
type: context.type,
position: context.position,
}
case ContextMenuType.MarkRead:
return {
type: context.type,
event: context.event,
}
default:
return { type: ContextMenuType.Hidden }
}
}
)
const mapDispatchToProps = dispatch => {
return {
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) => {
if (!item.hasRead) {
dispatch(markRead(item))
item.hasRead = true // get around chaining error
}
dispatch(toggleHidden(item))
},
switchView: (viewType: ViewType) => {
window.settings.setDefaultView(viewType)
dispatch(switchView(viewType))
},
setViewConfigs: (configs: ViewConfigs) =>
dispatch(setViewConfigs(configs)),
switchFilter: (filter: FilterType) => dispatch(switchFilter(filter)),
toggleFilter: (filter: FilterType) => dispatch(toggleFilter(filter)),
markAllRead: (sids?: number[], date?: Date, before?: boolean) => {
dispatch(markAllRead(sids, date, before))
},
fetchItems: (sids: number[]) => dispatch(fetchItems(false, sids)),
settings: (sids: number[]) => dispatch(toggleSettings(true, sids)),
close: () => dispatch(closeContextMenu()),
}
}
const connector = connect(mapStateToProps, mapDispatchToProps)
export type ContextReduxProps = typeof connector
export const ContextMenuContainer = connector(ContextMenu)

View File

@ -1,29 +1,21 @@
import * as React from "react" import * as React from "react"
import * as ReactDOM from "react-dom" import * as ReactDOM from "react-dom"
import { Provider } from "react-redux" import { Provider } from "react-redux"
import { createStore, applyMiddleware } from "redux"
import thunkMiddleware from "redux-thunk"
import { initializeIcons } from "@fluentui/react/lib/Icons" import { initializeIcons } from "@fluentui/react/lib/Icons"
import { rootReducer, RootState } from "./scripts/reducer"
import Root from "./components/root" import Root from "./components/root"
import { AppDispatch } from "./scripts/utils"
import { applyThemeSettings } from "./scripts/settings" import { applyThemeSettings } from "./scripts/settings"
import { initApp, openTextMenu } from "./scripts/models/app" import { initApp, openTextMenu } from "./scripts/models/app"
import { rootStore } from "./scripts/reducer"
window.settings.setProxy() window.settings.setProxy()
applyThemeSettings() applyThemeSettings()
initializeIcons("icons/") initializeIcons("icons/")
const store = createStore( rootStore.dispatch(initApp())
rootReducer,
applyMiddleware<AppDispatch, RootState>(thunkMiddleware)
)
store.dispatch(initApp())
window.utils.addMainContextListener((pos, text) => { window.utils.addMainContextListener((pos, text) => {
store.dispatch(openTextMenu(pos, text)) rootStore.dispatch(openTextMenu(pos, text))
}) })
window.fontList = [""] window.fontList = [""]
@ -32,7 +24,7 @@ window.utils.initFontList().then(fonts => {
}) })
ReactDOM.render( ReactDOM.render(
<Provider store={store}> <Provider store={rootStore}>
<Root /> <Root />
</Provider>, </Provider>,
document.getElementById("app") document.getElementById("app")

View File

@ -1,4 +1,5 @@
import { combineReducers } from "redux" import { applyMiddleware, combineReducers, createStore } from "redux"
import thunkMiddleware from "redux-thunk"
import { sourceReducer } from "./models/source" import { sourceReducer } from "./models/source"
import { itemReducer } from "./models/item" import { itemReducer } from "./models/item"
@ -7,6 +8,13 @@ import { appReducer } from "./models/app"
import { groupReducer } from "./models/group" import { groupReducer } from "./models/group"
import { pageReducer } from "./models/page" import { pageReducer } from "./models/page"
import { serviceReducer } from "./models/service" import { serviceReducer } from "./models/service"
import { AppDispatch } from "./utils"
import {
TypedUseSelectorHook,
useDispatch,
useSelector,
useStore,
} from "react-redux"
export const rootReducer = combineReducers({ export const rootReducer = combineReducers({
sources: sourceReducer, sources: sourceReducer,
@ -18,4 +26,14 @@ export const rootReducer = combineReducers({
app: appReducer, app: appReducer,
}) })
export const rootStore = createStore(
rootReducer,
applyMiddleware<AppDispatch, RootState>(thunkMiddleware)
)
export type AppStore = typeof rootStore
export type RootState = ReturnType<typeof rootReducer> export type RootState = ReturnType<typeof rootReducer>
export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
export const useAppStore: () => AppStore = useStore