From 5ecf189ed6176e9b64e801b38fd70f02bb9e130c Mon Sep 17 00:00:00 2001 From: Bruce Liu Date: Sun, 2 Mar 2025 21:41:11 -0800 Subject: [PATCH] refactor context menu as function component --- src/components/context-menu.tsx | 1115 ++++++++++----------- src/components/root.tsx | 4 +- src/containers/context-menu-container.tsx | 115 --- src/index.tsx | 16 +- src/scripts/reducer.ts | 20 +- 5 files changed, 576 insertions(+), 694 deletions(-) delete mode 100644 src/containers/context-menu-container.tsx diff --git a/src/components/context-menu.tsx b/src/components/context-menu.tsx index e005855..25cda32 100644 --- a/src/components/context-menu.tsx +++ b/src/components/context-menu.tsx @@ -13,38 +13,25 @@ import { ContextualMenuItemType, DirectionalHint, } from "office-ui-fabric-react/lib/ContextualMenu" -import { ContextMenuType } from "../scripts/models/app" -import { RSSItem } from "../scripts/models/item" -import { ContextReduxProps } from "../containers/context-menu-container" +import { closeContextMenu, ContextMenuType } from "../scripts/models/app" +import { + markAllRead, + markRead, + markUnread, + RSSItem, + toggleHidden, + toggleStarred, +} from "../scripts/models/item" import { ViewType, ImageCallbackTypes, ViewConfigs } from "../schema-types" import { FilterType } from "../scripts/models/feed" - -export type ContextMenuProps = ContextReduxProps & { - type: ContextMenuType - event?: MouseEvent | string - position?: [number, number] - item?: RSSItem - feedId?: string - text?: string - 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 -} +import { useAppDispatch, useAppSelector } from "../scripts/reducer" +import { + setViewConfigs, + showItem, + switchFilter, + switchView, + toggleFilter, +} from "../scripts/models/page" export const shareSubmenu = (item: RSSItem): IContextualMenuItem[] => [ { key: "qr", url: item.link, onRender: renderShareQR }, @@ -69,556 +56,556 @@ function getSearchItem(text: string): IContextualMenuItem { } } -export class ContextMenu extends React.Component { - getItems = (): IContextualMenuItem[] => { - switch (this.props.type) { - case ContextMenuType.Item: - return [ +export function ContextMenu() { + const { type } = useAppSelector(state => state.app.contextMenu) + + switch (type) { + case ContextMenuType.Hidden: + return null + case ContextMenuType.Item: + return + case ContextMenuType.Text: + return + case ContextMenuType.Image: + return + case ContextMenuType.View: + return + case ContextMenuType.Group: + return + case ContextMenuType.MarkRead: + return + } +} + +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", + text: intl.get("context.read"), + iconProps: { iconName: "TextDocument" }, + onClick: () => { + dispatch(markRead(item)) + dispatch(showItem(feedId, item)) + }, + }, + { + key: "openInBrowser", + text: intl.get("openExternal"), + iconProps: { iconName: "NavigateExternalInline" }, + onClick: e => { + dispatch(markRead(item)) + window.utils.openExternal(item.link, platformCtrl(e)) + }, + }, + { + key: "markAsRead", + text: item.hasRead + ? intl.get("article.markUnread") + : intl.get("article.markRead"), + iconProps: item.hasRead + ? { + iconName: "RadioBtnOn", + style: { fontSize: 14, textAlign: "center" }, + } + : { iconName: "StatusCircleRing" }, + onClick: () => { + if (item.hasRead) { + dispatch(markUnread(item)) + } else { + dispatch(markRead(item)) + } + }, + split: true, + subMenuProps: { + items: [ { - key: "showItem", - text: intl.get("context.read"), - iconProps: { iconName: "TextDocument" }, + key: "markBelow", + text: intl.get("article.markBelow"), + iconProps: { + iconName: "Down", + style: { fontSize: 14 }, + }, onClick: () => { - this.props.markRead(this.props.item) - this.props.showItem( - this.props.feedId, - this.props.item - ) + dispatch(markAllRead(null, item.date)) }, }, + { + key: "markAbove", + text: intl.get("article.markAbove"), + iconProps: { + iconName: "Up", + style: { fontSize: 14 }, + }, + onClick: () => { + dispatch(markAllRead(null, item.date, false)) + }, + }, + ], + }, + }, + { + key: "toggleStarred", + text: item.starred + ? intl.get("article.unstar") + : intl.get("article.star"), + iconProps: { + iconName: item.starred ? "FavoriteStar" : "FavoriteStarFill", + }, + onClick: () => { + dispatch(toggleStarred(item)) + }, + }, + { + key: "toggleHidden", + text: item.hidden + ? intl.get("article.unhide") + : intl.get("article.hide"), + iconProps: { + iconName: item.hidden ? "View" : "Hide3", + }, + onClick: () => { + dispatch(toggleHidden(item)) + }, + }, + { + key: "divider_1", + itemType: ContextualMenuItemType.Divider, + }, + { + key: "share", + text: intl.get("context.share"), + iconProps: { iconName: "Share" }, + subMenuProps: { + items: shareSubmenu(item), + }, + }, + { + key: "copyTitle", + text: intl.get("context.copyTitle"), + onClick: () => { + window.utils.writeClipboard(item.title) + }, + }, + { + key: "copyURL", + text: intl.get("context.copyURL"), + onClick: () => { + window.utils.writeClipboard(item.link) + }, + }, + ...(viewConfigs !== undefined + ? [ + { + key: "divider_2", + itemType: ContextualMenuItemType.Divider, + }, + { + key: "view", + text: intl.get("context.view"), + subMenuProps: { + items: [ + { + key: "showCover", + text: intl.get("context.showCover"), + canCheck: true, + checked: Boolean( + viewConfigs & ViewConfigs.ShowCover + ), + onClick: () => + dispatch( + setViewConfigs( + viewConfigs ^ + ViewConfigs.ShowCover + ) + ), + }, + { + key: "showSnippet", + text: intl.get("context.showSnippet"), + canCheck: true, + checked: Boolean( + viewConfigs & ViewConfigs.ShowSnippet + ), + onClick: () => + dispatch( + setViewConfigs( + viewConfigs ^ + ViewConfigs.ShowSnippet + ) + ), + }, + { + key: "fadeRead", + text: intl.get("context.fadeRead"), + canCheck: true, + checked: Boolean( + viewConfigs & ViewConfigs.FadeRead + ), + onClick: () => + dispatch( + setViewConfigs( + viewConfigs ^ ViewConfigs.FadeRead + ) + ), + }, + ], + }, + }, + ] + : []), + ] + return +} + +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", + text: intl.get("context.copy"), + iconProps: { iconName: "Copy" }, + onClick: () => { + window.utils.writeClipboard(text) + }, + }, + getSearchItem(text), + ] + : [] + if (url) { + menuItems.push({ + key: "urlSection", + itemType: ContextualMenuItemType.Section, + sectionProps: { + topDivider: menuItems.length > 0, + items: [ { key: "openInBrowser", text: intl.get("openExternal"), - iconProps: { iconName: "NavigateExternalInline" }, + iconProps: { + iconName: "NavigateExternalInline", + }, onClick: e => { - this.props.markRead(this.props.item) - window.utils.openExternal( - this.props.item.link, - platformCtrl(e) - ) - }, - }, - { - key: "markAsRead", - text: this.props.item.hasRead - ? intl.get("article.markUnread") - : intl.get("article.markRead"), - iconProps: this.props.item.hasRead - ? { - iconName: "RadioBtnOn", - style: { fontSize: 14, textAlign: "center" }, - } - : { iconName: "StatusCircleRing" }, - onClick: () => { - if (this.props.item.hasRead) - this.props.markUnread(this.props.item) - else this.props.markRead(this.props.item) - }, - split: true, - subMenuProps: { - items: [ - { - key: "markBelow", - text: intl.get("article.markBelow"), - iconProps: { - iconName: "Down", - style: { fontSize: 14 }, - }, - onClick: () => - this.props.markAllRead( - null, - this.props.item.date - ), - }, - { - key: "markAbove", - text: intl.get("article.markAbove"), - iconProps: { - iconName: "Up", - style: { fontSize: 14 }, - }, - onClick: () => - this.props.markAllRead( - null, - this.props.item.date, - false - ), - }, - ], - }, - }, - { - key: "toggleStarred", - text: this.props.item.starred - ? intl.get("article.unstar") - : intl.get("article.star"), - iconProps: { - iconName: this.props.item.starred - ? "FavoriteStar" - : "FavoriteStarFill", - }, - onClick: () => { - this.props.toggleStarred(this.props.item) - }, - }, - { - key: "toggleHidden", - text: this.props.item.hidden - ? intl.get("article.unhide") - : intl.get("article.hide"), - iconProps: { - iconName: this.props.item.hidden ? "View" : "Hide3", - }, - onClick: () => { - this.props.toggleHidden(this.props.item) - }, - }, - { - key: "divider_1", - itemType: ContextualMenuItemType.Divider, - }, - { - key: "share", - text: intl.get("context.share"), - iconProps: { iconName: "Share" }, - subMenuProps: { - items: shareSubmenu(this.props.item), - }, - }, - { - key: "copyTitle", - text: intl.get("context.copyTitle"), - onClick: () => { - window.utils.writeClipboard(this.props.item.title) + window.utils.openExternal(url, platformCtrl(e)) }, }, { key: "copyURL", text: intl.get("context.copyURL"), - onClick: () => { - window.utils.writeClipboard(this.props.item.link) - }, - }, - ...(this.props.viewConfigs !== undefined - ? [ - { - key: "divider_2", - itemType: ContextualMenuItemType.Divider, - }, - { - key: "view", - text: intl.get("context.view"), - subMenuProps: { - items: [ - { - key: "showCover", - text: intl.get( - "context.showCover" - ), - canCheck: true, - checked: Boolean( - this.props.viewConfigs & - ViewConfigs.ShowCover - ), - onClick: () => - this.props.setViewConfigs( - this.props.viewConfigs ^ - ViewConfigs.ShowCover - ), - }, - { - key: "showSnippet", - text: intl.get( - "context.showSnippet" - ), - canCheck: true, - checked: Boolean( - this.props.viewConfigs & - ViewConfigs.ShowSnippet - ), - onClick: () => - this.props.setViewConfigs( - this.props.viewConfigs ^ - ViewConfigs.ShowSnippet - ), - }, - { - key: "fadeRead", - text: intl.get( - "context.fadeRead" - ), - canCheck: true, - checked: Boolean( - this.props.viewConfigs & - ViewConfigs.FadeRead - ), - onClick: () => - this.props.setViewConfigs( - this.props.viewConfigs ^ - ViewConfigs.FadeRead - ), - }, - ], - }, - }, - ] - : []), - ] - case ContextMenuType.Text: { - const items: IContextualMenuItem[] = this.props.text - ? [ - { - key: "copyText", - text: intl.get("context.copy"), - iconProps: { iconName: "Copy" }, - onClick: () => { - window.utils.writeClipboard(this.props.text) - }, - }, - getSearchItem(this.props.text), - ] - : [] - if (this.props.url) { - items.push({ - key: "urlSection", - itemType: ContextualMenuItemType.Section, - sectionProps: { - topDivider: items.length > 0, - items: [ - { - key: "openInBrowser", - text: intl.get("openExternal"), - iconProps: { - iconName: "NavigateExternalInline", - }, - onClick: e => { - window.utils.openExternal( - this.props.url, - platformCtrl(e) - ) - }, - }, - { - key: "copyURL", - text: intl.get("context.copyURL"), - iconProps: { iconName: "Link" }, - onClick: () => { - window.utils.writeClipboard( - this.props.url - ) - }, - }, - ], - }, - }) - } - return items - } - case ContextMenuType.Image: - return [ - { - key: "openInBrowser", - text: intl.get("openExternal"), - iconProps: { iconName: "NavigateExternalInline" }, - onClick: e => { - if (platformCtrl(e)) { - window.utils.imageCallback( - ImageCallbackTypes.OpenExternalBg - ) - } else { - window.utils.imageCallback( - ImageCallbackTypes.OpenExternal - ) - } - }, - }, - { - key: "saveImageAs", - text: intl.get("context.saveImageAs"), - iconProps: { iconName: "SaveTemplate" }, - onClick: () => { - window.utils.imageCallback( - ImageCallbackTypes.SaveAs - ) - }, - }, - { - key: "copyImage", - text: intl.get("context.copyImage"), - iconProps: { iconName: "FileImage" }, - onClick: () => { - window.utils.imageCallback(ImageCallbackTypes.Copy) - }, - }, - { - key: "copyImageURL", - text: intl.get("context.copyImageURL"), iconProps: { iconName: "Link" }, onClick: () => { - window.utils.imageCallback( - ImageCallbackTypes.CopyLink - ) + window.utils.writeClipboard(url) }, }, - ] - case ContextMenuType.View: - return [ - { - key: "section_1", - itemType: ContextualMenuItemType.Section, - sectionProps: { - title: intl.get("context.view"), - bottomDivider: true, - items: [ - { - key: "cardView", - text: intl.get("context.cardView"), - iconProps: { iconName: "GridViewMedium" }, - canCheck: true, - checked: - this.props.viewType === ViewType.Cards, - onClick: () => - this.props.switchView(ViewType.Cards), - }, - { - key: "listView", - text: intl.get("context.listView"), - iconProps: { iconName: "BacklogList" }, - canCheck: true, - checked: - this.props.viewType === ViewType.List, - onClick: () => - this.props.switchView(ViewType.List), - }, - { - key: "magazineView", - text: intl.get("context.magazineView"), - iconProps: { iconName: "Articles" }, - canCheck: true, - checked: - this.props.viewType === - ViewType.Magazine, - onClick: () => - this.props.switchView( - ViewType.Magazine - ), - }, - { - key: "compactView", - text: intl.get("context.compactView"), - iconProps: { iconName: "BulletedList" }, - canCheck: true, - checked: - this.props.viewType === - ViewType.Compact, - onClick: () => - this.props.switchView(ViewType.Compact), - }, - ], - }, - }, - { - key: "section_2", - itemType: ContextualMenuItemType.Section, - sectionProps: { - title: intl.get("context.filter"), - bottomDivider: true, - items: [ - { - key: "allArticles", - text: intl.get("allArticles"), - iconProps: { iconName: "ClearFilter" }, - canCheck: true, - checked: - (this.props.filter & - ~FilterType.Toggles) == - FilterType.Default, - onClick: () => - this.props.switchFilter( - FilterType.Default - ), - }, - { - key: "unreadOnly", - text: intl.get("context.unreadOnly"), - iconProps: { - iconName: "RadioBtnOn", - style: { - fontSize: 14, - textAlign: "center", - }, - }, - canCheck: true, - checked: - (this.props.filter & - ~FilterType.Toggles) == - FilterType.UnreadOnly, - onClick: () => - this.props.switchFilter( - FilterType.UnreadOnly - ), - }, - { - key: "starredOnly", - text: intl.get("context.starredOnly"), - iconProps: { iconName: "FavoriteStarFill" }, - canCheck: true, - checked: - (this.props.filter & - ~FilterType.Toggles) == - FilterType.StarredOnly, - onClick: () => - this.props.switchFilter( - FilterType.StarredOnly - ), - }, - ], - }, - }, - { - key: "section_3", - itemType: ContextualMenuItemType.Section, - sectionProps: { - title: intl.get("search"), - bottomDivider: true, - items: [ - { - key: "caseSensitive", - text: intl.get("context.caseSensitive"), - iconProps: { - style: { - fontSize: 12, - fontStyle: "normal", - }, - children: "Aa", - }, - canCheck: true, - checked: !( - this.props.filter & - FilterType.CaseInsensitive - ), - onClick: () => - this.props.toggleFilter( - FilterType.CaseInsensitive - ), - }, - { - key: "fullSearch", - text: intl.get("context.fullSearch"), - iconProps: { iconName: "Breadcrumb" }, - canCheck: true, - checked: Boolean( - this.props.filter & - FilterType.FullSearch - ), - onClick: () => - this.props.toggleFilter( - FilterType.FullSearch - ), - }, - ], - }, - }, - { - key: "showHidden", - text: intl.get("context.showHidden"), - canCheck: true, - checked: Boolean( - this.props.filter & FilterType.ShowHidden - ), - onClick: () => - this.props.toggleFilter(FilterType.ShowHidden), - }, - ] - case ContextMenuType.Group: - return [ - { - key: "markAllRead", - text: intl.get("nav.markAllRead"), - iconProps: { iconName: "CheckMark" }, - onClick: () => this.props.markAllRead(this.props.sids), - }, - { - key: "refresh", - text: intl.get("nav.refresh"), - iconProps: { iconName: "Sync" }, - onClick: () => this.props.fetchItems(this.props.sids), - }, - { - key: "manage", - text: intl.get("context.manageSources"), - iconProps: { iconName: "Settings" }, - onClick: () => this.props.settings(this.props.sids), - }, - ] - case ContextMenuType.MarkRead: - return [ - { - key: "section_1", - itemType: ContextualMenuItemType.Section, - sectionProps: { - title: intl.get("nav.markAllRead"), - items: [ - { - key: "all", - text: intl.get("allArticles"), - iconProps: { iconName: "ReceiptCheck" }, - onClick: () => this.props.markAllRead(), - }, - { - key: "1d", - text: intl.get("app.daysAgo", { days: 1 }), - onClick: () => { - let date = new Date() - date.setTime(date.getTime() - 86400000) - this.props.markAllRead(null, date) - }, - }, - { - key: "3d", - text: intl.get("app.daysAgo", { days: 3 }), - onClick: () => { - let date = new Date() - date.setTime( - date.getTime() - 3 * 86400000 - ) - this.props.markAllRead(null, date) - }, - }, - { - key: "7d", - text: intl.get("app.daysAgo", { days: 7 }), - onClick: () => { - let date = new Date() - date.setTime( - date.getTime() - 7 * 86400000 - ) - this.props.markAllRead(null, date) - }, - }, - ], - }, - }, - ] - default: - return [] - } - } - - render() { - return this.props.type == ContextMenuType.Hidden ? null : ( - - ) + ], + }, + }) } + return +} + +function ImageContextMenu() { + const menuItems: IContextualMenuItem[] = [ + { + key: "openInBrowser", + text: intl.get("openExternal"), + iconProps: { iconName: "NavigateExternalInline" }, + onClick: e => { + if (platformCtrl(e)) { + window.utils.imageCallback( + ImageCallbackTypes.OpenExternalBg + ) + } else { + window.utils.imageCallback(ImageCallbackTypes.OpenExternal) + } + }, + }, + { + key: "saveImageAs", + text: intl.get("context.saveImageAs"), + iconProps: { iconName: "SaveTemplate" }, + onClick: () => { + window.utils.imageCallback(ImageCallbackTypes.SaveAs) + }, + }, + { + key: "copyImage", + text: intl.get("context.copyImage"), + iconProps: { iconName: "FileImage" }, + onClick: () => { + window.utils.imageCallback(ImageCallbackTypes.Copy) + }, + }, + { + key: "copyImageURL", + text: intl.get("context.copyImageURL"), + iconProps: { iconName: "Link" }, + onClick: () => { + window.utils.imageCallback(ImageCallbackTypes.CopyLink) + }, + }, + ] + 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", + itemType: ContextualMenuItemType.Section, + sectionProps: { + title: intl.get("context.view"), + bottomDivider: true, + items: [ + { + key: "cardView", + text: intl.get("context.cardView"), + iconProps: { iconName: "GridViewMedium" }, + canCheck: true, + checked: viewType === ViewType.Cards, + onClick: () => dispatch(switchView(ViewType.Cards)), + }, + { + key: "listView", + text: intl.get("context.listView"), + iconProps: { iconName: "BacklogList" }, + canCheck: true, + checked: viewType === ViewType.List, + onClick: () => dispatch(switchView(ViewType.List)), + }, + { + key: "magazineView", + text: intl.get("context.magazineView"), + iconProps: { iconName: "Articles" }, + canCheck: true, + checked: viewType === ViewType.Magazine, + onClick: () => dispatch(switchView(ViewType.Magazine)), + }, + { + key: "compactView", + text: intl.get("context.compactView"), + iconProps: { iconName: "BulletedList" }, + canCheck: true, + checked: viewType === ViewType.Compact, + onClick: () => dispatch(switchView(ViewType.Compact)), + }, + ], + }, + }, + { + key: "section_2", + itemType: ContextualMenuItemType.Section, + sectionProps: { + title: intl.get("context.filter"), + bottomDivider: true, + items: [ + { + key: "allArticles", + text: intl.get("allArticles"), + iconProps: { iconName: "ClearFilter" }, + canCheck: true, + checked: + (filter & ~FilterType.Toggles) == + FilterType.Default, + onClick: () => + dispatch(switchFilter(FilterType.Default)), + }, + { + key: "unreadOnly", + text: intl.get("context.unreadOnly"), + iconProps: { + iconName: "RadioBtnOn", + style: { + fontSize: 14, + textAlign: "center", + }, + }, + canCheck: true, + checked: + (filter & ~FilterType.Toggles) == + FilterType.UnreadOnly, + onClick: () => + dispatch(switchFilter(FilterType.UnreadOnly)), + }, + { + key: "starredOnly", + text: intl.get("context.starredOnly"), + iconProps: { iconName: "FavoriteStarFill" }, + canCheck: true, + checked: + (filter & ~FilterType.Toggles) == + FilterType.StarredOnly, + onClick: () => + dispatch(switchFilter(FilterType.StarredOnly)), + }, + ], + }, + }, + { + key: "section_3", + itemType: ContextualMenuItemType.Section, + sectionProps: { + title: intl.get("search"), + bottomDivider: true, + items: [ + { + key: "caseSensitive", + text: intl.get("context.caseSensitive"), + iconProps: { + style: { + fontSize: 12, + fontStyle: "normal", + }, + children: "Aa", + }, + canCheck: true, + checked: !(filter & FilterType.CaseInsensitive), + onClick: () => + dispatch(toggleFilter(FilterType.CaseInsensitive)), + }, + { + key: "fullSearch", + text: intl.get("context.fullSearch"), + iconProps: { iconName: "Breadcrumb" }, + canCheck: true, + checked: Boolean(filter & FilterType.FullSearch), + onClick: () => + dispatch(toggleFilter(FilterType.FullSearch)), + }, + ], + }, + }, + { + key: "showHidden", + text: intl.get("context.showHidden"), + canCheck: true, + checked: Boolean(filter & FilterType.ShowHidden), + onClick: () => dispatch(toggleFilter(FilterType.ShowHidden)), + }, + ] + return +} + +function GroupContextMenu() { + const dispatch = useAppDispatch() + const sids = useAppSelector( + state => state.app.contextMenu.target + ) as number[] + + const menuItems: IContextualMenuItem[] = [ + { + key: "markAllRead", + text: intl.get("nav.markAllRead"), + iconProps: { iconName: "CheckMark" }, + onClick: () => { + dispatch(markAllRead(sids)) + }, + }, + { + key: "refresh", + text: intl.get("nav.refresh"), + iconProps: { iconName: "Sync" }, + onClick: () => { + dispatch(markAllRead(sids)) + }, + }, + { + key: "manage", + text: intl.get("context.manageSources"), + iconProps: { iconName: "Settings" }, + onClick: () => { + dispatch(markAllRead(sids)) + }, + }, + ] + return +} + +function MarkReadContextMenu() { + const dispatch = useAppDispatch() + + const menuItems: IContextualMenuItem[] = [ + { + key: "section_1", + itemType: ContextualMenuItemType.Section, + sectionProps: { + title: intl.get("nav.markAllRead"), + items: [ + { + key: "all", + text: intl.get("allArticles"), + iconProps: { iconName: "ReceiptCheck" }, + onClick: () => { + dispatch(markAllRead()) + }, + }, + { + key: "1d", + text: intl.get("app.daysAgo", { days: 1 }), + onClick: () => { + let date = new Date() + date.setTime(date.getTime() - 86400000) + dispatch(markAllRead(null, date)) + }, + }, + { + key: "3d", + text: intl.get("app.daysAgo", { days: 3 }), + onClick: () => { + let date = new Date() + date.setTime(date.getTime() - 3 * 86400000) + dispatch(markAllRead(null, date)) + }, + }, + { + key: "7d", + text: intl.get("app.daysAgo", { days: 7 }), + onClick: () => { + let date = new Date() + date.setTime(date.getTime() - 7 * 86400000) + dispatch(markAllRead(null, date)) + }, + }, + ], + }, + }, + ] + return +} + +function ContextMenuBase({ + menuItems, +}: Readonly<{ menuItems: IContextualMenuItem[] }>) { + const { event, position } = useAppSelector(state => state.app.contextMenu) + const dispatch = useAppDispatch() + + return ( + dispatch(closeContextMenu())} + /> + ) } diff --git a/src/components/root.tsx b/src/components/root.tsx index 9b64074..76b58ac 100644 --- a/src/components/root.tsx +++ b/src/components/root.tsx @@ -1,6 +1,5 @@ import * as React from "react" import { connect } from "react-redux" -import { ContextMenuContainer } from "../containers/context-menu-container" import { closeContextMenu } from "../scripts/models/app" import PageContainer from "../containers/page-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 SettingsContainer from "../containers/settings-container" import { RootState } from "../scripts/reducer" +import { ContextMenu } from "./context-menu" const Root = ({ locale, dispatch }) => locale && ( @@ -20,7 +20,7 @@ const Root = ({ locale, dispatch }) => - + ) diff --git a/src/containers/context-menu-container.tsx b/src/containers/context-menu-container.tsx deleted file mode 100644 index b89aba2..0000000 --- a/src/containers/context-menu-container.tsx +++ /dev/null @@ -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) diff --git a/src/index.tsx b/src/index.tsx index cb98426..ac74b7b 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,29 +1,21 @@ import * as React from "react" import * as ReactDOM from "react-dom" import { Provider } from "react-redux" -import { createStore, applyMiddleware } from "redux" -import thunkMiddleware from "redux-thunk" import { initializeIcons } from "@fluentui/react/lib/Icons" -import { rootReducer, RootState } from "./scripts/reducer" import Root from "./components/root" -import { AppDispatch } from "./scripts/utils" import { applyThemeSettings } from "./scripts/settings" import { initApp, openTextMenu } from "./scripts/models/app" +import { rootStore } from "./scripts/reducer" window.settings.setProxy() applyThemeSettings() initializeIcons("icons/") -const store = createStore( - rootReducer, - applyMiddleware(thunkMiddleware) -) - -store.dispatch(initApp()) +rootStore.dispatch(initApp()) window.utils.addMainContextListener((pos, text) => { - store.dispatch(openTextMenu(pos, text)) + rootStore.dispatch(openTextMenu(pos, text)) }) window.fontList = [""] @@ -32,7 +24,7 @@ window.utils.initFontList().then(fonts => { }) ReactDOM.render( - + , document.getElementById("app") diff --git a/src/scripts/reducer.ts b/src/scripts/reducer.ts index d5af841..714e23a 100644 --- a/src/scripts/reducer.ts +++ b/src/scripts/reducer.ts @@ -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 { itemReducer } from "./models/item" @@ -7,6 +8,13 @@ import { appReducer } from "./models/app" import { groupReducer } from "./models/group" import { pageReducer } from "./models/page" import { serviceReducer } from "./models/service" +import { AppDispatch } from "./utils" +import { + TypedUseSelectorHook, + useDispatch, + useSelector, + useStore, +} from "react-redux" export const rootReducer = combineReducers({ sources: sourceReducer, @@ -18,4 +26,14 @@ export const rootReducer = combineReducers({ app: appReducer, }) +export const rootStore = createStore( + rootReducer, + applyMiddleware(thunkMiddleware) +) + +export type AppStore = typeof rootStore export type RootState = ReturnType + +export const useAppDispatch: () => AppDispatch = useDispatch +export const useAppSelector: TypedUseSelectorHook = useSelector +export const useAppStore: () => AppStore = useStore