Merge conflicts resolved

This commit is contained in:
Manoj Vivek 2020-08-28 19:51:53 +05:30
commit 92a6f35ca1
27 changed files with 728 additions and 29144 deletions

View file

@ -307,6 +307,15 @@
"contributions": [
"code"
]
},
{
"login": "mrfelfel",
"name": "mrfelfel",
"avatar_url": "https://avatars0.githubusercontent.com/u/19575588?v=4",
"profile": "https://github.com/mrfelfel",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 5,

12
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1,12 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: responsively
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View file

@ -138,6 +138,7 @@ Thanks go to these wonderful people ([emoji key](https://allcontributors.org/doc
<td align="center"><a href="https://github.com/JayArya"><img src="https://avatars0.githubusercontent.com/u/42388314?v=4" width="100px;" alt=""/><br /><sub><b>Jayant Arya</b></sub></a><br /><a href="https://github.com/responsively-org/responsively-app/commits?author=JayArya" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/JohnRawlins"><img src="https://avatars3.githubusercontent.com/u/42707277?v=4" width="100px;" alt=""/><br /><sub><b>John Rawlins</b></sub></a><br /><a href="https://github.com/responsively-org/responsively-app/commits?author=JohnRawlins" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/lepasq"><img src="https://avatars3.githubusercontent.com/u/53230128?v=4" width="100px;" alt=""/><br /><sub><b>lepasq</b></sub></a><br /><a href="https://github.com/responsively-org/responsively-app/commits?author=lepasq" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/mrfelfel"><img src="https://avatars0.githubusercontent.com/u/19575588?v=4" width="100px;" alt=""/><br /><sub><b>mrfelfel</b></sub></a><br /><a href="https://github.com/responsively-org/responsively-app/commits?author=mrfelfel" title="Code">💻</a></td>
</tr>
</table>

View file

@ -1,4 +1,3 @@
// @flow
import React from 'react';
import cx from 'classnames';
import FavIconOff from '@material-ui/icons/StarBorder';
@ -20,6 +19,7 @@ import UrlSearchResults from '../UrlSearchResults';
import commonStyles from '../common.styles.css';
import styles from './style.css';
import debounce from 'lodash/debounce';
import {notifyPermissionToHandleReloadOrNewAddress} from '../../utils/permissionUtils.js';
type Props = {
address: string,
@ -38,9 +38,9 @@ class AddressBar extends React.Component<Props> {
this.state = {
userTypedAddress: props.address,
previousAddress: props.address,
finalUrlResult: null,
suggestionList: [],
canShowSuggestions: false,
cursor: 0,
cursor: null,
};
this.inputRef = React.createRef();
}
@ -64,14 +64,18 @@ class AddressBar extends React.Component<Props> {
}
render() {
const {
suggestionList,
canShowSuggestions,
cursor,
userTypedAddress,
} = this.state;
const showSuggestions =
canShowSuggestions && !this._isSuggestionListEmpty();
return (
<div
className={`${styles.addressBarContainer} ${
this.state.finalUrlResult
? this.state.finalUrlResult.length && this.state.canShowSuggestions
? styles.active
: ''
: ''
showSuggestions ? styles.active : ''
}`}
>
<input
@ -81,7 +85,7 @@ class AddressBar extends React.Component<Props> {
name="address"
className={styles.addressInput}
placeholder="https://your-website.com"
value={this.state.userTypedAddress}
value={userTypedAddress}
onKeyDown={this._handleKeyDown}
onChange={this._handleInputChange}
/>
@ -100,9 +104,7 @@ class AddressBar extends React.Component<Props> {
>
<div
className={cx(commonStyles.flexAlignVerticalMiddle)}
onClick={() =>
this.props.toggleBookmark(this.state.userTypedAddress)
}
onClick={() => this.props.toggleBookmark(userTypedAddress)}
>
<Icon
type={this.props.isBookmarked ? 'starFull' : 'star'}
@ -172,70 +174,110 @@ class AddressBar extends React.Component<Props> {
</Tooltip>
</div>
</div>
{this.state.finalUrlResult?.length && this.state.canShowSuggestions ? (
{showSuggestions ? (
<UrlSearchResults
filteredSearchResults={this.state.finalUrlResult}
cursorIndex={this.state.cursor}
filteredSearchResults={suggestionList}
cursorIndex={cursor}
handleUrlChange={this._onSearchedUrlClick}
/>
) : (
''
)}
) : null}
</div>
);
}
_handleInputChange = e => {
this.setState(
{userTypedAddress: e.target.value, canShowSuggestions: true, cursor: 0},
() => {
const {value} = e.target;
if (value) {
this.setState({userTypedAddress: value, canShowSuggestions: true}, () => {
this._filterExistingUrl();
}
);
});
} else {
this.setState({userTypedAddress: value, suggestionList: []}, () => {
this._hideSuggestions();
});
}
};
_handleKeyDown = e => {
const {cursor, suggestionList} = this.state;
if (e.key === 'Enter') {
this.inputRef.current.blur();
this.setState(
{
finalUrlResult: [],
suggestionList: [],
canShowSuggestions: false,
cursor: null,
},
() => {
this._onChange();
}
);
} else if (e.key === 'ArrowUp' && this.state.cursor > 0) {
this.setState(prevState => ({
cursor: prevState.cursor - 1,
userTypedAddress: this.state.finalUrlResult[prevState.cursor - 1].url,
canShowSuggestions: true,
}));
} else if (
e.key === 'ArrowDown' &&
this.state.cursor < this.state.finalUrlResult.length - 1
) {
this.setState(prevState => ({
cursor: prevState.cursor + 1,
userTypedAddress: this.state.finalUrlResult[prevState.cursor + 1].url,
canShowSuggestions: true,
}));
} else if (e.key === 'ArrowUp' && !this._isSuggestionListEmpty()) {
// if the suggestion list just opened or the first suggestion is selected set the cursor to the last suggestion
if (cursor === null || cursor === 0) {
this._openSuggestionListAndSetCursorAt(suggestionList.length - 1);
// if cursor is down move it up by subtracting 1
} else if (cursor > 0) {
this._handleSuggestionSelection(-1);
}
} else if (e.key === 'ArrowDown' && !this._isSuggestionListEmpty()) {
// if the suggestion list just opened or the last suggestion is selected set the cursor to the first suggestion
if (cursor === null || cursor === suggestionList.length - 1) {
this._openSuggestionListAndSetCursorAt(0);
// if cursor is up move it down by adding 1
} else if (cursor < suggestionList.length - 1) {
this._handleSuggestionSelection(1);
}
} else if (e.key === 'Escape') {
this.setState(prevState => ({
canShowSuggestions: false,
}));
this._hideSuggestions();
}
};
_hideSuggestions = () => {
this.setState({
canShowSuggestions: false,
cursor: null,
});
};
/**
* Open suggestion list and set current selection at cursor position.
* @param {number} cursor Cursor position.
*/
_openSuggestionListAndSetCursorAt = cursor => {
this.setState(prevState => ({
cursor,
userTypedAddress: this.state.suggestionList[cursor].url,
canShowSuggestions: true,
}));
};
/**
* Handles the suggestion selection on arrow key up and down.
* @param {number} direction Indicates the direction. 1 for down, -1 for up.
*/
_handleSuggestionSelection = direction => {
const modifier = 1 * direction;
this.setState(prevState => ({
cursor: prevState.cursor + modifier,
userTypedAddress: this.state.suggestionList[prevState.cursor + modifier]
.url,
canShowSuggestions: true,
}));
};
_isSuggestionListEmpty = () => this.state.suggestionList.length === 0;
_handleClickOutside = () => {
this._hideSuggestions();
};
_onChange = () => {
if (!this.state.userTypedAddress) {
if (!this.state.userTypedAddress || !this.props.onChange) {
return;
}
return (
this.props.onChange &&
this.props.onChange(this._normalize(this.state.userTypedAddress), true)
);
notifyPermissionToHandleReloadOrNewAddress();
this.props.onChange(this._normalize(this.state.userTypedAddress), true);
};
_onSearchedUrlClick = (url, index) => {
@ -245,7 +287,7 @@ class AddressBar extends React.Component<Props> {
this.setState({
userTypedAddress: url,
finalUrlResult: [],
suggestionList: [],
});
};
@ -264,14 +306,10 @@ class AddressBar extends React.Component<Props> {
_filterExistingUrl = debounce(() => {
const finalResult = searchUrlUtils(this.state.userTypedAddress);
this.setState({finalUrlResult: finalResult});
this.setState({suggestionList: finalResult.slice(0, MAX_SUGGESTIONS)});
}, 300);
_handleClickOutside = () => {
this.setState({
finalUrlResult: [],
});
};
}
const MAX_SUGGESTIONS = 8;
export default AddressBar;

View file

@ -43,9 +43,13 @@ import {lightIconsColor, themeColor} from '../../../constants/colors';
const useStyles = makeStyles(theme => ({
fab: {
position: 'absolute',
position: 'absolute !important',
top: theme.spacing(10),
right: theme.spacing(3),
color: '#fff !important',
backgroundColor: `${themeColor} !important`,
borderRadius: '24px !important',
padding: '0 16px !important',
},
extendedIcon: {
marginRight: theme.spacing(1),

View file

@ -59,6 +59,7 @@ export default function DeviceList({
endAdornment: (
<InputAdornment>
<IconButton
className={styles.searchActiveIcon}
onClick={() => {
setSearchOpen(false);
setSearchText('');

View file

@ -17,4 +17,9 @@
.searchIcon {
margin: 0 14px 0 !important;
color: white !important;
}
.searchActiveIcon {
color: white !important;
}

View file

@ -45,9 +45,26 @@ export default function DeviceManager(props) {
acc[val.id] = val;
return acc;
}, {});
const inactiveDevices = props.browser.allDevices.filter(
device => !activeDevicesById[device.id]
);
const currentInactiveDevicesById = devices.inactive.reduce((acc, val) => {
acc[val.id] = val;
return acc;
}, {});
const devicesById = props.browser.allDevices.reduce((acc, val) => {
acc[val.id] = val;
return acc;
}, {});
const inactiveDevices = [
...props.browser.allDevices.filter(
device =>
!activeDevicesById[device.id] &&
!currentInactiveDevicesById[device.id]
),
...devices.inactive.filter(device => devicesById[device.id]),
];
setDevices({active: activeDevices, inactive: inactiveDevices});
}, [props.browser.devices, props.browser.allDevices]);
@ -75,9 +92,21 @@ export default function DeviceManager(props) {
source.droppableId === 'inactive'
? devices.inactiveFiltered[source.index]
: sourceList[source.index];
let idx = destination.index;
if (destination.droppableId === 'inactive') {
idx =
destination.index < devices.inactiveFiltered.length
? devices.inactive.findIndex(
d => d.id === devices.inactiveFiltered[destination.index].id
)
: devices.inactive.length;
}
sourceList.splice(sourceList.indexOf(itemDragged), 1);
destinationList.splice(destination.index, 0, itemDragged);
destinationList.splice(idx, 0, itemDragged);
updateDevices(devices);
};

View file

@ -6,6 +6,7 @@ import {ToastContainer} from 'react-toastify';
import AddressBar from '../../containers/AddressBar';
import ScrollControlsContainer from '../../containers/ScrollControlsContainer';
import HttpAuthDialog from '../HttpAuthDialog';
import PermissionPopup from '../PermissionPopup';
import styles from './style.module.css';
import NavigationControlsContainer from '../../containers/NavigationControlsContainer';
@ -20,6 +21,7 @@ const Header = () => (
</Grid>
<Grid item style={{flex: 1}}>
<AddressBar />
<PermissionPopup />
</Grid>
<Grid item>
<ScrollControlsContainer />

View file

@ -12,6 +12,7 @@ import commonStyles from '../common.styles.css';
import {iconsColor} from '../../constants/colors';
import Cross from '../icons/Cross';
import Reload from '../icons/Reload';
import {notifyPermissionToHandleReloadOrNewAddress} from '../../utils/permissionUtils.js';
class NavigationControls extends Component {
componentDidMount() {
@ -58,7 +59,10 @@ class NavigationControls extends Component {
<Tooltip title="Reload">
<div
className={commonStyles.flexAlignVerticalMiddle}
onClick={this.props.triggerNavigationReload}
onClick={() => {
notifyPermissionToHandleReloadOrNewAddress();
this.props.triggerNavigationReload();
}}
>
<Reload {...iconProps} padding={4} height={24} width={24} />
</div>
@ -82,7 +86,12 @@ class NavigationControls extends Component {
})}
/>
<Tooltip title="Back">
<div onClick={this.props.triggerNavigationBack}>
<div
onClick={() => {
notifyPermissionToHandleReloadOrNewAddress();
this.props.triggerNavigationBack();
}}
>
<Icon type="arrowLeft" size="30px" {...iconProps} />
</div>
</Tooltip>
@ -100,7 +109,12 @@ class NavigationControls extends Component {
})}
/>
<Tooltip title="Forward">
<div onClick={this.props.triggerNavigationForward}>
<div
onClick={() => {
notifyPermissionToHandleReloadOrNewAddress();
this.props.triggerNavigationForward();
}}
>
<Icon type="arrowRight" size="30px" {...iconProps} />
{/* <ArrowRightIcon {...iconProps} /> */}
</div>
@ -113,7 +127,11 @@ class NavigationControls extends Component {
<Tooltip title="Go to Homepage">
<div
className={commonStyles.flexAlignVerticalMiddle}
onClick={this.props.goToHomepage}
onClick={() => {
if (this.props.address !== this.props.homepage)
notifyPermissionToHandleReloadOrNewAddress();
this.props.goToHomepage();
}}
>
<HomeIcon {...iconProps} padding={5} />
</div>

View file

@ -0,0 +1,145 @@
import React, {useState, useEffect} from 'react';
import {ipcRenderer} from 'electron';
import Button from '@material-ui/core/Button';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText';
import DialogTitle from '@material-ui/core/DialogTitle';
import IconButton from '@material-ui/core/IconButton';
import CloseIcon from '@material-ui/icons/Close';
import Tooltip from '@material-ui/core/Tooltip';
import {makeStyles} from '@material-ui/core/styles';
import styles from './styles.module.css';
import cx from 'classnames';
import pubsub from 'pubsub.js';
import {
HIDE_PERMISSION_POPUP_DUE_TO_RELOAD,
PERMISSION_MANAGEMENT_PREFERENCE_CHANGED,
} from '../../constants/pubsubEvents';
import {
getPermissionPageTitle,
getPermissionRequestMessage,
} from '../../utils/permissionUtils.js';
import {PERMISSION_MANAGEMENT_OPTIONS} from '../../constants/permissionsManagement';
const useStyles = makeStyles(theme => ({
closeButton: {
position: 'absolute',
right: theme.spacing(1),
top: theme.spacing(1),
color: theme.palette.grey[500],
},
}));
function getMessage(info) {
if (info == null) return '';
return (
<>
<strong>{getPermissionPageTitle(info.url)}</strong>&nbsp;
{getPermissionRequestMessage(info.permission, info.details)}
</>
);
}
export default function PermissionPopup() {
const classes = useStyles();
const [permissionInfos, setPermissionInfos] = useState([]);
useEffect(() => {
const promptHandler = (event, args) => {
setPermissionInfos(prev => [...prev, args]);
};
const reloadHandler = () => {
setPermissionInfos([]);
};
const subscription = pubsub.subscribe(
HIDE_PERMISSION_POPUP_DUE_TO_RELOAD,
reloadHandler
);
ipcRenderer.on('permission-prompt', promptHandler);
return () => {
ipcRenderer.removeListener('permission-prompt', promptHandler);
pubsub.unsubscribe(subscription);
};
}, []);
useEffect(() => {
const preferenceChangedHandler = newPreference => {
if (newPreference === PERMISSION_MANAGEMENT_OPTIONS.ASK_ALWAYS) return;
if (newPreference === PERMISSION_MANAGEMENT_OPTIONS.ALLOW_ALWAYS) {
permissionInfos.forEach(info => {
ipcRenderer.send('permission-response', {...info, allowed: true});
});
setPermissionInfos([]);
} else if (newPreference === PERMISSION_MANAGEMENT_OPTIONS.DENY_ALWAYS) {
permissionInfos.forEach(info => {
ipcRenderer.send('permission-response', {...info, allowed: false});
});
setPermissionInfos([]);
}
};
const subscription = pubsub.subscribe(
PERMISSION_MANAGEMENT_PREFERENCE_CHANGED,
preferenceChangedHandler
);
return () => {
pubsub.unsubscribe(subscription);
};
}, [permissionInfos]);
function handleClose(allowed) {
ipcRenderer.send('permission-response', {...permissionInfos[0], allowed});
setPermissionInfos(permissionInfos.slice(1));
}
return (
<div
className={cx(styles.permissionPopup, {
[styles.permissionPopupActive]: permissionInfos.length !== 0,
})}
>
<h4 className={styles.permissionPopupTitle}>
Permission Request
<Tooltip title="Ignore" placement="left">
<IconButton
aria-label="close"
className={classes.closeButton}
onClick={() => handleClose(null)}
size="small"
>
<CloseIcon />
</IconButton>
</Tooltip>
</h4>
<DialogContent className={styles.permissionPopupMsgContainer}>
<DialogContentText className={styles.permissionPopupMsg}>
{getMessage(permissionInfos[0])}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
variant="contained"
color="secondary"
type="submit"
onClick={() => handleClose(false)}
size="small"
>
Deny
</Button>
<Button
variant="contained"
color="primary"
type="submit"
onClick={() => handleClose(true)}
size="small"
>
Allow
</Button>
</DialogActions>
</div>
);
}

View file

@ -0,0 +1,30 @@
.permissionPopup {
position: absolute;
background-color: #252526;
border-radius: 4px;
box-shadow: 0 11px 15px -7px rgba(0, 0, 0, 0.2),
0 24px 38px 3px rgba(0, 0, 0, 0.14), 0 9px 46px 8px rgba(0, 0, 0, 0.12);
width: 20em;
transition: opacity 0.5s ease-out;
opacity: 0;
height: 0;
overflow: hidden;
}
.permissionPopupActive {
opacity: 1;
height: auto;
}
.permissionPopupTitle {
margin: 14px;
}
.permissionPopupMsg {
margin-bottom: 0 !important;
font-size: 0.9rem !important;
}
.permissionPopupMsgContainer {
padding: 0 14px !important;
}

View file

@ -1,4 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import styles from './style.css';
import DefaultFavIcon from '@material-ui/icons/Public';
@ -10,53 +11,70 @@ const UrlSearchResults = ({
}) => (
<div className={cx(styles.searchBarSuggestionsContainer)}>
<ul className={cx(styles.searchBarSuggestionsListUl)}>
{filteredSearchResults?.map((eachResult, index) => {
{filteredSearchResults.map((eachResult, index) => {
const favicon = eachResult.pageMeta?.favicons?.[0];
const title = eachResult.pageMeta?.title;
const url = eachResult.url;
return (
index < 8 && (
<li key={url}>
<div
className={cx(styles.searchBarSuggestionsListItems, {
[styles.searchBarSuggestionsActiveListItems]:
cursorIndex === index,
})}
onClick={() => handleUrlChange(eachResult.url, index)}
>
<div className={cx(styles.pageFavIconWrapper)}>
{favicon ? (
<img
className={cx(styles.pageFavIcon)}
src={favicon}
onError={event => {
event.target.style.display = 'none';
event.target.nextSibling.style.display = 'block';
}}
/>
) : (
<div className={cx(styles.pageDefaultFavIconWrapper)}>
<DefaultFavIcon fontSize="inherit" />
</div>
)}
<div
style={{display: 'none'}}
className={cx(styles.pageDefaultFavIconWrapperClassName)}
>
<li key={url}>
<div
className={cx(styles.searchBarSuggestionsListItems, {
[styles.searchBarSuggestionsActiveListItems]:
cursorIndex === index,
})}
onClick={() => handleUrlChange(eachResult.url, index)}
>
<div className={cx(styles.pageFavIconWrapper)}>
{favicon ? (
<img
className={cx(styles.pageFavIcon)}
src={favicon}
onError={event => {
event.target.style.display = 'none';
event.target.nextSibling.style.display = 'block';
}}
/>
) : (
<div className={cx(styles.pageDefaultFavIconWrapper)}>
<DefaultFavIcon fontSize="inherit" />
</div>
</div>
<div className={cx(styles.pageTitleAndUrlContainer)}>
<span className={cx(styles.pageTitle)}>{title}</span>
<span className={cx(styles.pageUrl)}>{url}</span>
)}
<div
style={{display: 'none'}}
className={cx(styles.pageDefaultFavIconWrapperClassName)}
>
<DefaultFavIcon fontSize="inherit" />
</div>
</div>
</li>
)
<div className={cx(styles.pageTitleAndUrlContainer)}>
<span className={cx(styles.pageTitle)}>{title}</span>
<span className={cx(styles.pageUrl)}>{url}</span>
</div>
</div>
</li>
);
})}
</ul>
</div>
);
UrlSearchResults.propTypes = {
filteredSearchResults: PropTypes.arrayOf(
PropTypes.shape({
url: PropTypes.string.isRequired,
pageMeta: PropTypes.shape({
title: PropTypes.string,
favicons: PropTypes.arrayOf(PropTypes.string),
}),
})
),
cursorIndex: PropTypes.number,
handleUrlChange: PropTypes.func.isRequired,
};
UrlSearchResults.defaultProps = {
filteredSearchResults: [],
cursorIndex: null,
};
export default UrlSearchResults;

View file

@ -5,6 +5,7 @@ import Checkbox from '@material-ui/core/Checkbox';
import TextField from '@material-ui/core/TextField';
import Input from '@material-ui/core/Input';
import SettingsIcon from '@material-ui/icons/Settings';
import Select from 'react-select';
import commonStyles from '../common.styles.css';
import styles from './styles.module.css';
@ -12,6 +13,50 @@ import {DEVTOOLS_MODES} from '../../constants/previewerLayouts';
import ScreenShotSavePreference from '../ScreenShotSavePreference/index';
import {userPreferenceSettings} from '../../settings/userPreferenceSettings';
import {SCREENSHOT_MECHANISM} from '../../constants/values';
import {notifyPermissionPreferenceChanged} from '../../utils/permissionUtils.js';
import {PERMISSION_MANAGEMENT_OPTIONS} from '../../constants/permissionsManagement';
const selectStyles = {
control: selectStyles => ({...selectStyles, backgroundColor: '#ffffff10'}),
option: (selectStyles, {data, isDisabled, isFocused, isSelected}) => {
const color = 'white';
return {
...selectStyles,
backgroundColor: isDisabled
? null
: isSelected
? '#ffffff40'
: isFocused
? '#ffffff20'
: null,
color: 'white',
':active': {
...selectStyles[':active'],
backgroundColor: !isDisabled && '#ffffff40',
},
};
},
input: selectStyles => ({...selectStyles}),
placeholder: selectStyles => ({...selectStyles}),
singleValue: (selectStyles, {data}) => ({...selectStyles, color: 'white'}),
menu: selectStyles => ({...selectStyles, background: '#4b4b4b', zIndex: 100}),
};
const permissionsOptions = [
{
value: PERMISSION_MANAGEMENT_OPTIONS.ALLOW_ALWAYS,
label: PERMISSION_MANAGEMENT_OPTIONS.ALLOW_ALWAYS,
},
{
value: PERMISSION_MANAGEMENT_OPTIONS.DENY_ALWAYS,
label: PERMISSION_MANAGEMENT_OPTIONS.DENY_ALWAYS,
},
{
value: PERMISSION_MANAGEMENT_OPTIONS.ASK_ALWAYS,
label: PERMISSION_MANAGEMENT_OPTIONS.ASK_ALWAYS,
},
];
export default function UserPreference({
devToolsConfig,
@ -22,6 +67,7 @@ export default function UserPreference({
const onChange = (field, value) => {
onUserPreferencesChange({...userPreferences, [field]: value});
};
return (
<div className={cx(commonStyles.sidebarContentSection)}>
<div className={cx(commonStyles.sidebarContentSectionTitleBar)}>
@ -164,6 +210,28 @@ export default function UserPreference({
onScreenShotSaveLocationChange={onChange}
/>
</div>
<div className={cx(commonStyles.sidebarContentSectionContainer)}>
<div className={styles.sectionHeader}>Permissions</div>
<div className={styles.permissionsSelectorContainer}>
<Select
options={permissionsOptions}
value={
permissionsOptions.find(
x => x.value === userPreferences?.permissionManagement
) || permissionsOptions[0]
}
onChange={val => {
notifyPermissionPreferenceChanged(val.value);
onChange('permissionManagement', val.value);
}}
styles={selectStyles}
/>
<p className={styles.permissionsSelectorSmallNote}>
<strong>Note:</strong> To ensure this behaviour you should restart
Responsively
</p>
</div>
</div>
</div>
);
}

View file

@ -31,3 +31,19 @@
height: 1px;
background-color: #636363;
}
.permissionsSelectorContainer {
margin-top: 10px;
}
.permissionsSelectorSmallNote {
color: rgba(255, 255, 255, 0.7);
margin: 0;
font-size: 0.75rem;
margin-top: 10px;
text-align: left;
font-family: 'Roboto', 'Helvetica', 'Arial', sans-serif;
font-weight: 400;
line-height: 1.66;
letter-spacing: 0.03333em;
}

View file

@ -0,0 +1,9 @@
const PERMISSION_MANAGEMENT_OPTIONS = {
ALLOW_ALWAYS: 'Allow always',
DENY_ALWAYS: 'Deny always',
ASK_ALWAYS: 'Ask always',
};
Object.freeze(PERMISSION_MANAGEMENT_OPTIONS);
export {PERMISSION_MANAGEMENT_OPTIONS};

View file

@ -17,3 +17,8 @@ export const OPEN_CONSOLE_FOR_DEVICE = 'OPEN_CONSOLE_FOR_DEVICE';
// status bar events
export const STATUS_BAR_VISIBILITY_CHANGE = 'status-bar-visibility-change';
export const HIDE_PERMISSION_POPUP_DUE_TO_RELOAD =
'HIDE_PERMISSION_POPUP_DUE_TO_RELOAD';
export const PERMISSION_MANAGEMENT_PREFERENCE_CHANGED =
'PERMISSION_MANAGEMENT_PREFERENCE_CHANGED';

View file

@ -10,8 +10,10 @@ function mapStateToProps(state) {
const {
navigatorStatus: {backEnabled, forwardEnabled},
devices,
address,
homepage,
} = state.browser;
return {backEnabled, forwardEnabled, devices};
return {backEnabled, forwardEnabled, devices, address, homepage};
}
function mapDispatchToProps(dispatch) {

View file

@ -20,6 +20,7 @@ import electron, {
webContents,
shell,
dialog,
session,
} from 'electron';
import settings from 'electron-settings';
import log from 'electron-log';
@ -46,9 +47,11 @@ import {
watchFiles,
} from './utils/browserSync';
import {getHostFromURL} from './utils/urlUtils';
import {getPermissionSettingPreference} from './utils/permissionUtils';
import browserSync from 'browser-sync';
import {captureOnSentry} from './utils/logUtils';
import appMetadata from './services/db/appMetadata';
import {PERMISSION_MANAGEMENT_OPTIONS} from './constants/permissionsManagement';
const path = require('path');
const chokidar = require('chokidar');
@ -72,6 +75,7 @@ let devToolsView = null;
let fileToOpen = null;
const httpAuthCallbacks = {};
const permissionCallbacks = {};
if (process.env.NODE_ENV === 'production') {
const sourceMapSupport = require('source-map-support');
@ -319,6 +323,98 @@ const createWindow = async () => {
onResize();
});
session.defaultSession.setPermissionRequestHandler(
(webContents, permission, callback, details) => {
const preferences = getPermissionSettingPreference();
const reqUrl = webContents.getURL();
if (permissionCallbacks[reqUrl] == null) permissionCallbacks[reqUrl] = {};
if (permissionCallbacks[reqUrl][permission] == null) {
permissionCallbacks[reqUrl][permission] = {
called: false,
allowed: null,
callbacks: [],
};
}
const entry = permissionCallbacks[reqUrl][permission];
if (preferences === PERMISSION_MANAGEMENT_OPTIONS.ALLOW_ALWAYS) {
entry.callbacks.forEach(callback => callback(true));
entry.callbacks = [];
entry.allowed = true;
entry.called = true;
return callback(true);
}
if (preferences === PERMISSION_MANAGEMENT_OPTIONS.DENY_ALWAYS) {
entry.callbacks.forEach(callback => callback(false));
entry.callbacks = [];
entry.allowed = false;
entry.called = true;
return callback(false);
}
if (entry.called) {
if (entry.allowed == null) return;
return callback(entry.allowed);
}
if (entry.callbacks.length === 0) {
entry.callbacks.push(callback);
mainWindow.webContents.send('permission-prompt', {
url: reqUrl,
permission,
details,
});
} else {
entry.callbacks.push(callback);
}
}
);
session.defaultSession.setPermissionCheckHandler(
(webContents, permission) => {
const reqUrl = webContents.getURL();
let entry = permissionCallbacks[reqUrl];
if (entry != null) entry = entry[permission];
if (entry == null || !entry.called) {
return null;
}
return entry.allowed;
}
);
ipcMain.on('permission-response', (evnt, ...args) => {
if (args[0] == null) return;
const {url, permission, allowed} = args[0];
let entry = permissionCallbacks[url];
if (entry != null) entry = entry[permission];
if (entry != null && !entry.called) {
entry.called = true;
entry.allowed = allowed;
if (allowed != null)
entry.callbacks.forEach(callback => callback(allowed));
entry.callbacks = [];
}
});
ipcMain.on('reset-ignored-permissions', evnt => {
Object.entries(permissionCallbacks).forEach(([_, permissions]) => {
Object.entries(permissions).forEach(([_, entry]) => {
if (entry.called && entry.allowed == null) entry.called = false;
entry.callbacks = [];
});
});
});
ipcMain.on('start-watching-file', async (event, fileInfo) => {
let path = fileInfo.path.replace('file://', '');
if (process.platform === 'win32') {

View file

@ -10,6 +10,7 @@ import {
} from 'electron';
import * as os from 'os';
import fs from 'fs';
import url from 'url';
import {pkg} from './utils/generalUtils';
import {
getAllShortcuts,
@ -131,7 +132,12 @@ export default class MenuBuilder {
win.center();
win.loadURL(`file://${__dirname}/shortcuts.html`);
win.loadURL(
url.format({
protocol: 'file',
pathname: path.join(__dirname, 'shortcuts.html'),
})
);
win.once('ready-to-show', () => {
win.show();
@ -189,13 +195,16 @@ export default class MenuBuilder {
const selected = dialog.showOpenDialogSync({
filters: [{name: 'HTML', extensions: ['htm', 'html']}],
});
if (!selected || !selected.length || !selected[0]) {
return;
}
let filePath = selected[0];
if (!filePath.startsWith('file://')) {
filePath = `file://${filePath}`;
}
filePath = url.format({
protocol: 'file',
pathname: filePath,
});
this.mainWindow.webContents.send('address-change', filePath);
},
},

View file

@ -1,8 +1,6 @@
// @flow
import {ipcRenderer, remote} from 'electron';
import settings from 'electron-settings';
import {isIfStatement} from 'typescript';
import trimStart from 'lodash/trimStart';
import {
NEW_ADDRESS,
NEW_ZOOM_LEVEL,
@ -16,7 +14,6 @@ import {
NEW_HOMEPAGE,
NEW_USER_PREFERENCES,
DELETE_CUSTOM_DEVICE,
TOGGLE_BOOKMARK,
NEW_DEV_TOOLS_CONFIG,
NEW_INSPECTOR_STATUS,
NEW_WINDOW_SIZE,
@ -120,6 +117,7 @@ type UserPreferenceType = {
zoomLevel: number,
removeFixedPositionedElements: boolean,
screenshotMechanism: string,
permissionManagement: 'Ask always' | 'Allow always' | 'Deny always',
};
type FilterFieldType = FILTER_FIELDS.OS | FILTER_FIELDS.DEVICE_TYPE;

View file

@ -1,6 +1,7 @@
import settings from 'electron-settings';
import {ACTIVE_DEVICES, USER_PREFERENCES} from '../constants/settingKeys';
import {SCREENSHOT_MECHANISM} from '../constants/values';
import {PERMISSION_MANAGEMENT_OPTIONS} from '../constants/permissionsManagement';
export function migrateDeviceSchema() {
if (settings.get('USER_PREFERENCES')) {
@ -11,6 +12,7 @@ export function migrateDeviceSchema() {
_handleScreenshotFixedElementsPreferences();
_handleScreenshotMechanismPreferences();
_handleDeviceSchema();
_handlePermissionsDefaultPreferences();
}
const _handleDeviceSchema = () => {
@ -46,4 +48,16 @@ const _handleScreenshotMechanismPreferences = () => {
settings.set(USER_PREFERENCES, userPreferences);
};
const _handlePermissionsDefaultPreferences = () => {
const userPreferences = settings.get(USER_PREFERENCES) || {};
if (userPreferences.permissionManagement != null) {
return;
}
userPreferences.permissionManagement =
PERMISSION_MANAGEMENT_OPTIONS.ALLOW_ALWAYS;
settings.set(USER_PREFERENCES, userPreferences);
};
export default {migrateDeviceSchema};

View file

@ -96,7 +96,6 @@
if (lo === 'escape') return 'Esc';
if (lo === 'prtsc') return 'PrtSc';
if (lo.startsWith('num')) return getNumPadName(k.slice(3));
console.log('k', k);
return firstToUpperCase(k);
}
function mapAccelerator(acc) {

View file

@ -0,0 +1,76 @@
import pubsub from 'pubsub.js';
import {ipcRenderer} from 'electron';
import {
HIDE_PERMISSION_POPUP_DUE_TO_RELOAD,
PERMISSION_MANAGEMENT_PREFERENCE_CHANGED,
} from '../constants/pubsubEvents';
import {PERMISSION_MANAGEMENT_OPTIONS} from '../constants/permissionsManagement';
import {USER_PREFERENCES} from '../constants/settingKeys';
import settings from 'electron-settings';
const path = require('path');
export function getPermissionPageTitle(url) {
if (url == null || url.length === 0) return 'The webpage';
try {
if (url.startsWith('file://')) {
return decodeURIComponent(path.basename(url));
}
return new URL(url).hostname;
} catch {
return url;
}
}
export function getDeviceText(device) {
if (device === 'audio') return 'microphone';
if (device === 'video') return 'camera';
return device;
}
// simulating the behavior of google chrome
export function getPermissionRequestMessage(permission, details) {
if (permission === 'notifications') return 'wants to show notifications';
if (permission === 'geolocation') return 'wants to know your location';
if (permission === 'fullscreen') return 'wants to go to full screen';
if (permission === 'pointerLock')
return 'wants to control your pointer movements';
// see https:github.com/electron/electron/pull/23333
if (permission.includes('clipboard') || permission === 'unknown')
return 'wants to see text and images copied to the clipboard';
if (permission === 'media') {
let mediaTypes = details?.mediaTypes;
if (mediaTypes != null && mediaTypes.length !== 0) {
mediaTypes = mediaTypes.map(getDeviceText);
if (mediaTypes.length === 1) return `wants to use your ${mediaTypes[0]}`;
const last = mediaTypes.pop();
return `wants to use your ${mediaTypes.join(', ')} and ${last}`;
}
return 'wants to use some of your media devices';
}
if (permission.includes('midi')) return 'wants to use your MIDI devices';
return `is requesting permission for "${permission}"`;
}
export function notifyPermissionToHandleReloadOrNewAddress() {
pubsub.publish(HIDE_PERMISSION_POPUP_DUE_TO_RELOAD);
ipcRenderer.send('reset-ignored-permissions');
}
export function notifyPermissionPreferenceChanged(newPreference) {
pubsub.publish(PERMISSION_MANAGEMENT_PREFERENCE_CHANGED, [newPreference]);
}
export function getPermissionSettingPreference() {
return (
settings.get(USER_PREFERENCES)?.permissionManagement ||
PERMISSION_MANAGEMENT_OPTIONS.ALLOW_ALWAYS
);
}

File diff suppressed because it is too large Load diff

View file

@ -11,11 +11,15 @@ const populateContributors = () => {
}
function generateFooterRow(json) {
if (json == null || !json.length) {
throw new Error('Malformed data');
}
const humanContributors = json.filter((contributor) => contributor.type === "User");
let html = '';
document.getElementById(
'github-contributors__thanks',
).innerText = `Thanks to all of our ${json.length} contributors! 🎉👏`;
json.forEach((contributor) => {
).innerText = `Thanks to all of our ${humanContributors.length} contributors! 🎉👏`;
humanContributors.forEach((contributor) => {
html += generateProfile(contributor);
});
document.getElementById('github-contributors__users').innerHTML = html;
@ -23,7 +27,7 @@ const populateContributors = () => {
function getContr() {
fetch(
'https://api.github.com/repos/responsively-org/responsively-app/contributors',
'https://api.github.com/repos/responsively-org/responsively-app/contributors?per_page=100',
)
.then((response) => response.text())
.then((text) => JSON.parse(text))

View file

@ -29,6 +29,15 @@
"dismissText": "Dismiss",
"minOpenCount": 0
},
{
"id": "app-growth-udate",
"title": "It is been 100 days since the app is launched!",
"text": "We have some stats published about it, check it out.",
"link": "https://twitter.com/vivek_jonam/status/1298212909118562304",
"okText": "Open",
"dismissText": "Dismiss",
"minOpenCount": 1
},
{
"id": "twitter-handle-intro",
"title": "Keep yourselves informed!",