diff --git a/common/action-manager.js b/common/action-manager.js index acb571f1c..5813579f7 100644 --- a/common/action-manager.js +++ b/common/action-manager.js @@ -18,6 +18,7 @@ const am = new ActionManager([ "NOTIFY_BLOCK_URL", "NOTIFY_UNBLOCK_URL", "NOTIFY_UNBLOCK_ALL", + "NOTIFY_BOOKMARK_ADD", "NOTIFY_BOOKMARK_DELETE", "NOTIFY_HISTORY_DELETE", "NOTIFY_HISTORY_DELETE_CANCELLED", @@ -27,7 +28,8 @@ const am = new ActionManager([ "SEARCH_STATE_RESPONSE", "NOTIFY_ROUTE_CHANGE", "NOTIFY_PERFORMANCE", - "NOTIFY_USER_EVENT" + "NOTIFY_USER_EVENT", + "NOTIFY_OPEN_WINDOW" ]); // This is a a set of actions that have sites in them, @@ -118,8 +120,12 @@ function RequestExperiments() { return RequestExpect("EXPERIMENTS_REQUEST", "EXPERIMENTS_RESPONSE"); } -function NotifyBookmarkDelete(data) { - return Notify("NOTIFY_BOOKMARK_DELETE", data); +function NotifyBookmarkAdd(url) { + return Notify("NOTIFY_BOOKMARK_ADD", url); +} + +function NotifyBookmarkDelete(bookmarkGuid) { + return Notify("NOTIFY_BOOKMARK_DELETE", bookmarkGuid); } function NotifyHistoryDelete(data) { @@ -167,6 +173,10 @@ function NotifyEvent(data) { return Notify("NOTIFY_USER_EVENT", data); } +function NotifyOpenWindow(data) { + return Notify("NOTIFY_OPEN_WINDOW", data); +} + am.defineActions({ Notify, Response, @@ -182,12 +192,14 @@ am.defineActions({ NotifyBlockURL, NotifyUnblockURL, NotifyUnblockAll, + NotifyBookmarkAdd, NotifyBookmarkDelete, NotifyHistoryDelete, NotifyPerformSearch, NotifyRouteChange, NotifyPerf, - NotifyEvent + NotifyEvent, + NotifyOpenWindow }); module.exports = am; diff --git a/common/event-constants.js b/common/event-constants.js index 5c5e12476..341ce2c65 100644 --- a/common/event-constants.js +++ b/common/event-constants.js @@ -11,16 +11,19 @@ const constants = { defaultPage: DEFAULT_PAGE, pages: new Set(Array.from(urlPatternToPageMap.values()).concat(DEFAULT_PAGE)), events: new Set([ + "BLOCK", + "BOOKMARK_ADD", + "BOOKMARK_DELETE", "CLICK", "DELETE", - "BOOKMARK_DELETE", - "BLOCK", - "UNBLOCK", - "UNBLOCK_ALL", - "SHARE", "LOAD_MORE", "LOAD_MORE_SCROLL", - "SEARCH" + "OPEN_NEW_WINDOW", + "OPEN_PRIVATE_WINDOW", + "SEARCH", + "SHARE", + "UNBLOCK", + "UNBLOCK_ALL" ]), sources: new Set([ "TOP_SITES", diff --git a/content-src/components/ActivityFeed/ActivityFeed.js b/content-src/components/ActivityFeed/ActivityFeed.js index 268183d6f..08abccf41 100644 --- a/content-src/components/ActivityFeed/ActivityFeed.js +++ b/content-src/components/ActivityFeed/ActivityFeed.js @@ -3,7 +3,8 @@ const {connect} = require("react-redux"); const {justDispatch} = require("selectors/selectors"); const {actions} = require("common/action-manager"); const SiteIcon = require("components/SiteIcon/SiteIcon"); -const DeleteMenu = require("components/DeleteMenu/DeleteMenu"); +const LinkMenu = require("components/LinkMenu/LinkMenu"); +const LinkMenuButton = require("components/LinkMenuButton/LinkMenuButton"); const {prettyUrl, getRandomFromTimestamp} = require("lib/utils"); const moment = require("moment"); const classNames = require("classnames"); @@ -33,9 +34,6 @@ const ActivityFeedItem = React.createClass({ showDate: false }; }, - onDeleteClick() { - this.setState({showContextMenu: true}); - }, render() { const site = this.props; const title = site.title || site.provider_display || (site.parsedUrl && site.parsedUrl.hostname); @@ -63,7 +61,7 @@ const ActivityFeedItem = React.createClass({ dateLabel = moment(date).format("h:mm A"); } - return (
  • + return (
  • -
    -
    -
    this.props.onShare(site.url)}>
    -
    alert("Sorry. We are still working on this feature.")}>
    -
    - this.setState({showContextMenu: true})} /> + this.setState({showContextMenu: val})} - url={site.url} - bookmarkGuid={site.bookmarkGuid} + allowBlock={this.props.page === "NEW_TAB"} + site={site} page={this.props.page} - index={this.props.index} source={this.props.source} + index={this.props.index} />
  • ); } diff --git a/content-src/components/ActivityFeed/ActivityFeed.scss b/content-src/components/ActivityFeed/ActivityFeed.scss index a68ebd2b3..0dbffc6f8 100644 --- a/content-src/components/ActivityFeed/ActivityFeed.scss +++ b/content-src/components/ActivityFeed/ActivityFeed.scss @@ -21,6 +21,7 @@ } .feed-item { + @include link-menu-button; display: flex; align-items: center; position: relative; @@ -132,7 +133,7 @@ } } - &.fixed, + &.active, &:hover { background: $feed-hover-color; border: 1px solid $faintest-black; diff --git a/content-src/components/ContextMenu/ContextMenu.js b/content-src/components/ContextMenu/ContextMenu.js index 602bf7578..61fae0632 100644 --- a/content-src/components/ContextMenu/ContextMenu.js +++ b/content-src/components/ContextMenu/ContextMenu.js @@ -21,10 +21,21 @@ const ContextMenu = React.createClass({ return (); @@ -34,9 +45,13 @@ const ContextMenu = React.createClass({ ContextMenu.propTypes = { visible: React.PropTypes.bool, onUpdate: React.PropTypes.func.isRequired, + onUserEvent: React.PropTypes.func, options: React.PropTypes.arrayOf(React.PropTypes.shape({ - label: React.PropTypes.string.isRequired, - onClick: React.PropTypes.func.isRequired + type: React.PropTypes.string, + label: React.PropTypes.string, + onClick: React.PropTypes.func, + userEvent: React.PropTypes.string, + ref: React.PropTypes.string })).isRequired }; diff --git a/content-src/components/ContextMenu/ContextMenu.scss b/content-src/components/ContextMenu/ContextMenu.scss index 01b4df8df..6986b857b 100644 --- a/content-src/components/ContextMenu/ContextMenu.scss +++ b/content-src/components/ContextMenu/ContextMenu.scss @@ -3,8 +3,9 @@ position: absolute; font-size: $context-menu-font-size; box-shadow: $context-menu-shadow; - top: 100%; - right: 0; + top: ($link-menu-button-size / 4); + left: 100%; + margin-left: 5px; z-index: 10000; background: $bg-grey; border-radius: $context-menu-border-radius; @@ -15,6 +16,11 @@ list-style: none; > li { + &.separator { + margin: $context-menu-outer-padding 0; + border-bottom: 1px solid $context-menu-border-color; + } + > a { cursor: pointer; color: inherit; diff --git a/content-src/components/LinkMenu/LinkMenu.js b/content-src/components/LinkMenu/LinkMenu.js new file mode 100644 index 000000000..8e606f139 --- /dev/null +++ b/content-src/components/LinkMenu/LinkMenu.js @@ -0,0 +1,106 @@ +const React = require("react"); +const {connect} = require("react-redux"); +const ContextMenu = require("components/ContextMenu/ContextMenu"); +const {actions} = require("common/action-manager"); +const {FIRST_RUN_TYPE} = require("lib/first-run-data"); + +const LinkMenu = React.createClass({ + getDefaultProps() { + return { + visible: false, + allowBlock: true + }; + }, + userEvent(event) { + const {page, source, index, Experiments, dispatch} = this.props; + if (page && source) { + let payload = { + event, + page: page, + source: source, + action_position: index + }; + if (["BLOCK", "DELETE"].includes(event) && Experiments.data.reverseMenuOptions) { + payload.experiment_id = Experiments.data.id; + } + dispatch(actions.NotifyEvent(payload)); + } + }, + getOptions() { + const {site, allowBlock, Experiments, dispatch} = this.props; + const isNotDefault = site.type !== FIRST_RUN_TYPE; + + // Don't add delete options for default links + // that show up if your history is empty + const deleteOptions = isNotDefault ? [ + {type: "separator"}, + allowBlock && { + ref: "dismiss", + label: "Dismiss", + userEvent: "BLOCK", + onClick: () => dispatch(actions.NotifyBlockURL(site.url)) + }, + { + ref: "delete", + label: "Delete from History", + userEvent: "DELETE", + onClick: () => dispatch(actions.NotifyHistoryDelete(site.url)) + } + ] : []; + + if (Experiments.data.reverseMenuOptions) { + deleteOptions.reverse(); + } + + return [ + (site.bookmarkGuid ? { + ref: "removeBookmark", + label: "Remove Bookmark", + userEvent: "BOOKMARK_DELETE", + onClick: () => dispatch(actions.NotifyBookmarkDelete(site.bookmarkGuid)) + } : { + ref: "addBookmark", + label: "Bookmark", + userEvent: "BOOKMARK_ADD", + onClick: () => dispatch(actions.NotifyBookmarkAdd(site.url)) + }), + {type: "separator"}, + { + ref: "openWindow", + label: "Open in a New Window", + userEvent: "OPEN_NEW_WINDOW", + onClick: () => dispatch(actions.NotifyOpenWindow({url: site.url})) + }, + { + ref: "openPrivate", + label: "Open in a Private Window", + userEvent: "OPEN_PRIVATE_WINDOW", + onClick: () => dispatch(actions.NotifyOpenWindow({url: site.url, isPrivate: true})) + }] + .concat(deleteOptions).filter(o => o); + }, + render() { + return (); + } +}); + +LinkMenu.propTypes = { + visible: React.PropTypes.bool, + onUpdate: React.PropTypes.func.isRequired, + allowBlock: React.PropTypes.bool, + site: React.PropTypes.shape({ + url: React.PropTypes.string.isRequired, + bookmarkGuid: React.PropTypes.string + }).isRequired, + + // This is for events + page: React.PropTypes.string, + source: React.PropTypes.string, + index: React.PropTypes.number +}; + +module.exports = connect(({Experiments}) => ({Experiments}))(LinkMenu); diff --git a/content-src/components/LinkMenuButton/LinkMenuButton.js b/content-src/components/LinkMenuButton/LinkMenuButton.js new file mode 100644 index 000000000..01f0fa9ad --- /dev/null +++ b/content-src/components/LinkMenuButton/LinkMenuButton.js @@ -0,0 +1,15 @@ +const React = require("react"); + +const LinkMenuButton = React.createClass({ + render() { + return (); + } +}); + +LinkMenuButton.propTypes = { + onClick: React.PropTypes.func.isRequired +}; + +module.exports = LinkMenuButton; diff --git a/content-src/components/LinkMenuButton/LinkMenuButton.scss b/content-src/components/LinkMenuButton/LinkMenuButton.scss new file mode 100644 index 000000000..4cedea771 --- /dev/null +++ b/content-src/components/LinkMenuButton/LinkMenuButton.scss @@ -0,0 +1,21 @@ +.link-menu-button { + cursor: pointer; + position: absolute; + top: -($link-menu-button-size / 2); + right: -($link-menu-button-size / 2); + width: $link-menu-button-size; + height: $link-menu-button-size; + background-color: $white; + background-image: url('img/glyph-more-16.svg'); + background-position: center center; + background-repeat: no-repeat; + background-clip: padding-box; + border: $link-menu-button-border; + border-radius: 100%; + box-shadow: $link-menu-button-boxshadow; + transform: scale(0.25); + opacity: 0; + transition-property: transform, opacity; + transition-duration: 200ms; + z-index: 399; +} diff --git a/content-src/components/Spotlight/Spotlight.js b/content-src/components/Spotlight/Spotlight.js index d2253ada8..a9c49a59e 100644 --- a/content-src/components/Spotlight/Spotlight.js +++ b/content-src/components/Spotlight/Spotlight.js @@ -4,9 +4,9 @@ const {justDispatch} = require("selectors/selectors"); const {actions} = require("common/action-manager"); const moment = require("moment"); const SiteIcon = require("components/SiteIcon/SiteIcon"); -const DeleteMenu = require("components/DeleteMenu/DeleteMenu"); +const LinkMenu = require("components/LinkMenu/LinkMenu"); +const LinkMenuButton = require("components/LinkMenuButton/LinkMenuButton"); const classNames = require("classnames"); -const {FIRST_RUN_TYPE} = require("lib/first-run-data"); const DEFAULT_LENGTH = 3; @@ -66,15 +66,11 @@ const SpotlightItem = React.createClass({
    -