Skip to content

Commit

Permalink
Add theme approval step if add-on is available
Browse files Browse the repository at this point in the history
If ?theme param is available on initial page load, assume that this is a
shared theme. Populate the editor UI with that theme and also stash it as a
pendingTheme.

If the add-on is not installed, pendingTheme will do nothing and the
editor will display the incoming shared theme.

If the add-on is installed and responds with a stored current theme,
that theme will replace the one in the editor. Then, a permission
dialog with a preview of the pendingTheme will be presented.

If the user hits "skip" on the dialog, pendingTheme is discarded.

If the user hits "apply", the pending theme is pushed into the add-on
and replaces the theme in the editor.

Fixes #27
  • Loading branch information
lmorchard committed Feb 13, 2018
1 parent 36c9901 commit f81e153
Show file tree
Hide file tree
Showing 13 changed files with 292 additions and 29 deletions.
41 changes: 32 additions & 9 deletions src/lib/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,30 @@ import { createActions, handleActions } from 'redux-actions';
import undoable, { ActionCreators } from 'redux-undo';
import { defaultColors } from './constants';

export const defaultTheme = () => ({
images: { headerURL: '' },
colors: defaultColors
});

// Actions that should trigger a theme update in URL history and the add-on
export const themeChangeActions = [
'SET_THEME',
'SET_COLOR',
'SET_BACKGROUND'
];

export const actions = {
ui: createActions({}, 'SET_SELECTED_COLOR', 'SET_HAS_EXTENSION'),
ui: createActions(
{},
'SET_SELECTED_COLOR',
'SET_HAS_EXTENSION',
'SET_PENDING_THEME',
'CLEAR_PENDING_THEME'
),
theme: {
...createActions(
{},
'SET_THEME',
'SET_COLORS',
'SET_COLOR',
'SET_BACKGROUND'
themeChangeActions
),
// HACK: Seems like redux-undo doesn't have sub-tree specific undo/redo
// actions - but let's fake it for now.
Expand All @@ -23,6 +38,8 @@ export const actions = {
export const selectors = {
hasExtension: state => state.ui.hasExtension,
selectedColor: state => state.ui.selectedColor,
hasPendingTheme: state => state.ui.pendingTheme !== null,
pendingTheme: state => state.ui.pendingTheme,
theme: state => state.theme.present,
themeCanUndo: state => state.theme.past.length > 0,
themeCanRedo: state => state.theme.future.length > 0
Expand All @@ -31,6 +48,14 @@ export const selectors = {
export const reducers = {
ui: handleActions(
{
SET_PENDING_THEME: (state, { payload: { theme } }) => ({
...state,
pendingTheme: theme
}),
CLEAR_PENDING_THEME: (state) => ({
...state,
pendingTheme: null
}),
SET_SELECTED_COLOR: (state, { payload: { name } }) => ({
...state,
selectedColor: name
Expand All @@ -41,6 +66,7 @@ export const reducers = {
})
},
{
pendingTheme: null,
selectedColor: 'toolbar',
hasExtension: false
}
Expand All @@ -58,10 +84,7 @@ export const reducers = {
images: { ...state.images, headerURL: url }
})
},
{
images: { headerURL: '' },
colors: defaultColors
}
defaultTheme()
))
};

Expand Down
5 changes: 5 additions & 0 deletions src/lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,8 @@ export const colorToCSS = color => {
? `hsl(${h}, ${s}%, ${l}%)`
: `hsla(${h}, ${s}%, ${l}%, ${color.a * 0.01})`;
};

export const themesEqual = (themeA, themeB) =>
// HACK: "deep equal" via stringify
// http://www.mattzeunert.com/2016/01/28/javascript-deep-equal.html
JSON.stringify(themeA) === JSON.stringify(themeB);
51 changes: 40 additions & 11 deletions src/web/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ import { Provider } from 'react-redux';
import queryString from 'query-string';
import Clipboard from 'clipboard';

// import { makeLog } from '../lib/utils';
import { makeLog } from '../lib/utils';
import { CHANNEL_NAME } from '../lib/constants';
import { createAppStore, actions, selectors } from '../lib/store';
import { createAppStore, actions, selectors, themeChangeActions } from '../lib/store';
import App from './lib/components/App';

import './index.scss';

// const log = makeLog('web');
const log = makeLog('web');

const clipboard = new Clipboard('.clipboardButton');

Expand All @@ -25,6 +25,9 @@ const PING_PERIOD = 1000;
const MAX_OUTSTANDING_PINGS = 2;
let outstandingPings = 0;

// TODO: Need better validation / error handling in this encode/decode?
// Maybe rewrite without using JsonUrl - Issue #37

const jsonCodec = JsonUrl('lzma');

const urlEncodeTheme = theme =>
Expand All @@ -33,6 +36,9 @@ const urlEncodeTheme = theme =>
return `${protocol}//${host}${pathname}?theme=${value}`;
});

const urlDecodeTheme = themeString =>
jsonCodec.decompress(themeString);

const postMessage = (type, data = {}) =>
window.postMessage(
{ ...data, type, channel: `${CHANNEL_NAME}-extension` },
Expand All @@ -41,14 +47,17 @@ const postMessage = (type, data = {}) =>

const updateExtensionThemeMiddleware = ({ getState }) => next => action => {
const returnValue = next(action);
postMessage('setTheme', { theme: selectors.theme(getState()) });
const meta = action.meta || {};
if (!meta.skipAddon && themeChangeActions.includes(action.type)) {
postMessage('setTheme', { theme: selectors.theme(getState()) });
}
return returnValue;
};

const updateHistoryMiddleware = ({ getState }) => next => action => {
const returnValue = next(action);
if (!action.meta || !action.meta.popstate) {
// Only update history if this action wasn't from popstate event.
const meta = action.meta || {};
if (!meta.skipHistory && themeChangeActions.includes(action.type)) {
const theme = selectors.theme(getState());
urlEncodeTheme(theme).then(url =>
window.history.pushState({ theme }, '', url));
Expand All @@ -61,14 +70,17 @@ const composeEnhancers = composeWithDevTools({});
const store = createAppStore(
{},
composeEnhancers(
applyMiddleware(updateExtensionThemeMiddleware, updateHistoryMiddleware)
applyMiddleware(
updateExtensionThemeMiddleware,
updateHistoryMiddleware
)
)
);

window.addEventListener('popstate', ({ state: { theme } }) =>
store.dispatch({
...actions.theme.setTheme({ theme }),
meta: { popstate: true }
meta: { skipHistory: true }
})
);

Expand All @@ -86,7 +98,10 @@ window.addEventListener('message', ({ source, data: message }) => {
}
}
if (message.type === 'fetchedTheme') {
store.dispatch(actions.theme.setTheme({ theme: message.theme }));
store.dispatch({
...actions.theme.setTheme({ theme: message.theme }),
meta: { fromAddon: true }
});
}
}
});
Expand All @@ -113,9 +128,23 @@ render(

const params = queryString.parse(window.location.search);
if (!params.theme) {
// Fire off a message to request current theme from the add-on.
postMessage('fetchTheme');
} else {
jsonCodec.decompress(params.theme).then(theme => {
store.dispatch(actions.theme.setTheme({ theme }));
log('Received shared theme');
// TODO: figure out if this param theme matches theme from add-on and ignore if so
urlDecodeTheme(params.theme).then(theme => {
// Set the pending theme - only matters if add-on is installed
store.dispatch(actions.ui.setPendingTheme({ theme }));
// Set the current editor theme - but skip notifying the add-on
store.dispatch({
...actions.theme.setTheme({ theme }),
meta: {
skipHistory: true,
skipAddon: true
}
});
// Fire off a message to request current theme from the add-on.
postMessage('fetchTheme');
});
}
2 changes: 2 additions & 0 deletions src/web/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
--white: #fff;
--large-unit-box-shadow: 0 5px 10px 5px rgba(0, 0, 0, .1);
--large-unit-border-radius: 4px;
--medium-unit-box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1);
--medium-unit-border-radius: 3.19px;
--small-unit-box-shadow: 0 2px 2px rgba(0, 0, 0, .1);
--small-unit-border-radius: 2px;
}
Expand Down
12 changes: 9 additions & 3 deletions src/web/lib/_common.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ $grid: 4px;
text-decoration: none;
transition: background 50ms;
width: 200px;
background: rgba(12,12,13,0.10);

&:hover {
background: rgba(12,12,13,0.20);
}

&:active {
background: rgba(12,12,13,0.30);
}
}

@mixin buttonPrimary {
Expand Down Expand Up @@ -45,6 +54,3 @@ $grid: 4px;
background: var(--white);
overflow: hidden;
}



13 changes: 11 additions & 2 deletions src/web/lib/components/App/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import ThemeColorsEditor from '../ThemeColorsEditor';
import PresetThemeSelector from '../PresetThemeSelector';
import ThemeBackgroundPicker from '../ThemeBackgroundPicker';
import ExtensionInstallButton from '../ExtensionInstallButton';
import SharedThemeDialog from '../SharedThemeDialog';
import ThemeUrl from '../ThemeUrl';

import './index.scss';
Expand All @@ -18,13 +19,16 @@ const mapStateToProps = state => ({
themeCanUndo: selectors.themeCanUndo(state),
themeCanRedo: selectors.themeCanRedo(state),
hasExtension: selectors.hasExtension(state),
selectedColor: selectors.selectedColor(state)
selectedColor: selectors.selectedColor(state),
hasPendingTheme: selectors.hasPendingTheme(state),
pendingTheme: selectors.pendingTheme(state)
});

const mapDispatchToProps = dispatch => ({
setBackground: args => dispatch(actions.theme.setBackground(args)),
setColor: args => dispatch(actions.theme.setColor(args)),
setTheme: args => dispatch(actions.theme.setTheme(args)),
clearPendingTheme: () => dispatch(actions.ui.clearPendingTheme()),
setSelectedColor: args => dispatch(actions.ui.setSelectedColor(args)),
undo: () => dispatch(actions.theme.undo()),
redo: () => dispatch(actions.theme.redo())
Expand All @@ -40,13 +44,18 @@ export const AppComponent = ({
hasExtension,
selectedColor,
setColor,
pendingTheme,
hasPendingTheme,
clearPendingTheme,
setTheme,
setSelectedColor,
setBackground,
undo,
redo
}) =>
<div className="app">
{hasExtension && hasPendingTheme &&
<SharedThemeDialog {...{ pendingTheme, setTheme, clearPendingTheme }} />}
<AppBackground {...{ theme }} />
{!hasExtension && <ExtensionInstallButton {...{ addonUrl }} />}
<div className="app-content">
Expand All @@ -67,7 +76,7 @@ export const AppComponent = ({
themeCanUndo,
themeCanRedo
}} />
<PresetThemeSelector {...{ setSelectedColor, setTheme }}/>
<PresetThemeSelector {...{ setTheme }}/>
<ThemeBackgroundPicker {...{ theme, setBackground }} />
</div>
</div>;
Expand Down
3 changes: 1 addition & 2 deletions src/web/lib/components/BrowserPreview/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const BrowserPreview = ({
setTheme = null
}) => {
const clickSelectColor = name => e => {
if (size === 'large') {
if (setSelectedColor) {
setSelectedColor({ name });
e.stopPropagation();
}
Expand All @@ -30,7 +30,6 @@ export const BrowserPreview = ({
setTheme({ theme });
e.stopPropagation();
}

return false;
};

Expand Down
79 changes: 79 additions & 0 deletions src/web/lib/components/BrowserPreview/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,85 @@
}
}

.doll--medium {
@include unit('medium');
.doll__tabbar {
flex: 0 0 $grid * 10;
padding: 0 $grid * 8;

li {
flex: 0 0 $grid * 30;
padding: $grid * 2;
}

.title {
border-radius: 5px;
height: 10px;
width: 160px;
}
}

.doll__toolbar-wrapper {
background-position-y: -80px;
flex: 0 0 $grid * 19;
}

.doll__toolbar {
height: 63px;
padding: 0 $grid * 2.5 0 $grid * 6.5;
}

.doll__field {
font-size: $grid * 5;
height: $grid * 10.5;
margin: $grid $grid * 2.5 $grid $grid * 7.5;

.doll__location {
border-radius: 5px;
height: 10px;
margin: 0 0 0 $grid * 10;
}

.doll__button {
flex: 0 0 18px;
height: 18px;
margin: 16px;
padding: 0;

.doll__button-inner {
flex: 0 0 18px;
height: 18px;
}
}
}

.doll__button {
padding: 0 $grid * 3.5;

.doll__button-inner {
height: 24px;
width: 24px;
}

> div > div {
align-items: center;
display: flex;
justify-content: center;
}

img, svg {
width: 24px;
height: 24px;
}
}

.doll__content {
display: flex;
flex: 1;
padding: $grid * 4;
}
}

.doll__tabbar {
display: flex;
list-style-type: none;
Expand Down
Loading

0 comments on commit f81e153

Please sign in to comment.