diff --git a/flow-typed/Settings.js b/flow-typed/Settings.js new file mode 100644 index 000000000..89fc59118 --- /dev/null +++ b/flow-typed/Settings.js @@ -0,0 +1,8 @@ +declare type CommentServerDetails = { + name: string, + url: string, +} + +declare type WalletServerDetails = { + +}; diff --git a/static/app-strings.json b/static/app-strings.json index 5da607612..9125daa73 100644 --- a/static/app-strings.json +++ b/static/app-strings.json @@ -2269,5 +2269,11 @@ "Automatic Hosting lets you delegate some amount of storage for the network to automatically download and host.": "Automatic Hosting lets you delegate some amount of storage for the network to automatically download and host.", "Help improve the P2P data network (and make LBRY happy) by hosting data.": "Help improve the P2P data network (and make LBRY happy) by hosting data.", "Limit Hosting of Content History": "Limit Hosting of Content History", + "Remove custom comment server": "Remove custom comment server", + "Use Https": "Use Https", + "Server URL": "Server URL", + "Use https": "Use https", + "Custom Servers": "Custom Servers", + "Add A Server": "Add A Server", "--end--": "--end--" } diff --git a/ui/component/common/item-panel-input-row.jsx b/ui/component/common/item-panel-input-row.jsx new file mode 100644 index 000000000..e561669e0 --- /dev/null +++ b/ui/component/common/item-panel-input-row.jsx @@ -0,0 +1,104 @@ +// @flow +import React, { useState, useEffect } from 'react'; +import Button from 'component/button'; +import { Form, FormField } from 'component/common/form'; + +type Props = { + update: (CommentServerDetails) => void, + onCancel: (boolean) => void, +}; + +const VALID_IPADDRESS_REGEX = new RegExp( + '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\.)){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$' +); +const VALID_HOSTNAME_REGEX = new RegExp( + '^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(\\.))+([A-Za-z]|[A-Za-z][A-Za-z]*[A-Za-z])$' +); + +const VALID_ENDPOINT_REGEX = new RegExp('^((\\/)([a-zA-Z0-9]+))+$'); + +const isValidServerString = (serverString) => { + const si = serverString.indexOf('/'); + const pi = serverString.indexOf(':'); + const path = si === -1 ? '' : serverString.slice(si); + console.log('path', path); + const hostMaybePort = si === -1 ? serverString : serverString.slice(0, si); + const host = pi === -1 ? hostMaybePort : hostMaybePort.slice(0, pi); + const port = pi === -1 ? '' : hostMaybePort.slice(pi + 1); + console.log('port', port); + const portInt = parseInt(port); + + return ( + (host === 'localhost' || VALID_IPADDRESS_REGEX.test(host) || VALID_HOSTNAME_REGEX.test(host)) && + (!path || VALID_ENDPOINT_REGEX.test(path)) && + // eslint-disable-next-line + (pi === -1 || (port && typeof portInt === 'number' && portInt === portInt)) + ); // NaN !== NaN +}; + +function ServerInputRow(props: Props) { + const { update, onCancel } = props; + const [nameString, setNameString] = useState(''); + const [hostString, setHostString] = useState(''); + const [useHttps, setUseHttps] = useState(true); + + const getHostString = () => { + return `${useHttps ? 'https://' : 'http://'}${hostString}`; + }; + + const [validServerString, setValidServerString] = useState(false); + + useEffect(() => { + setValidServerString(isValidServerString(hostString)); + }, [hostString, validServerString, setValidServerString]); + + function onSubmit() { + const updateValue = { url: getHostString(), name: nameString }; + update(updateValue); + setHostString(''); + setNameString(''); + } + + return ( + <Form onSubmit={onSubmit}> + <div className="itemPanel--input"> + <FormField + type="text" + label={__('Name')} + placeholder={'My Server'} + value={nameString} + onChange={(e) => setNameString(e.target.value)} + /> + <div className="fieldset-group fieldset-group--smushed fieldset-group--disabled-prefix fieldset-group--row"> + <div className={'fieldset-section'}> + <label htmlFor="serverUrl">{__('URL')}</label> + <div className="form-field__prefix">{`${useHttps ? 'https://' : 'http://'}`}</div> + </div> + <FormField + type="text" + placeholder={'code.freezepeach.fun'} + value={hostString} + onChange={(e) => setHostString(e.target.value)} + name={'serverUrl'} + /> + </div> + </div> + <div className="itemPanel--input"> + <FormField + label={'Use Https'} + name="use_https" + type="checkbox" + checked={useHttps} + onChange={() => setUseHttps(!useHttps)} + /> + </div> + + <div className="section__actions"> + <Button type="submit" button="primary" label={__('Add')} disabled={!validServerString || !nameString} /> + <Button type="button" button="link" onClick={() => onCancel(false)} label={__('Cancel')} /> + </div> + </Form> + ); +} + +export default ServerInputRow; diff --git a/ui/component/common/item-panel.jsx b/ui/component/common/item-panel.jsx new file mode 100644 index 000000000..b95caf60a --- /dev/null +++ b/ui/component/common/item-panel.jsx @@ -0,0 +1,43 @@ +// @flow +import React from 'react'; +import * as ICONS from 'constants/icons'; +import Button from 'component/button'; +import classnames from 'classnames'; + +type Props = { + onClick: (CommentServerDetails) => void, + onRemove?: (CommentServerDetails) => void, + active: boolean, + serverDetails: CommentServerDetails, +}; + +/* + [ https://myserver.com x ] + [ https://myserver.com x (selected)] + + [ https://myserver.com:50001 x (selected)] + */ + +const ItemPanel = (props: Props) => { + const { onClick, active, serverDetails, onRemove } = props; + + return ( + <div onClick={() => onClick(serverDetails)} className={classnames('itemPanel', { 'itemPanel--active': active })}> + <div className={'itemPanel__details'}> + <div className={'itemPanel__name'}>{`${serverDetails.name}`}</div> + <div className={'itemPanel__url'}>{`${serverDetails.url}`}</div> + </div> + {onRemove && ( + <Button + button="close" + title={__('Remove custom comment server')} + icon={ICONS.REMOVE} + onClick={() => onRemove(serverDetails)} + /> + )} + {!onRemove && <div />} + </div> + ); +}; + +export default ItemPanel; diff --git a/ui/component/settingCommentsServer/index.js b/ui/component/settingCommentsServer/index.js index deee05b7f..eaac73ed3 100644 --- a/ui/component/settingCommentsServer/index.js +++ b/ui/component/settingCommentsServer/index.js @@ -7,11 +7,13 @@ import SettingCommentsServer from './view'; const select = (state) => ({ customServerEnabled: makeSelectClientSetting(SETTINGS.CUSTOM_COMMENTS_SERVER_ENABLED)(state), customServerUrl: makeSelectClientSetting(SETTINGS.CUSTOM_COMMENTS_SERVER_URL)(state), + customCommentServers: makeSelectClientSetting(SETTINGS.CUSTOM_COMMENTS_SERVERS)(state), }); const perform = (dispatch) => ({ setCustomServerEnabled: (val) => dispatch(doSetClientSetting(SETTINGS.CUSTOM_COMMENTS_SERVER_ENABLED, val, true)), setCustomServerUrl: (url) => dispatch(doSetClientSetting(SETTINGS.CUSTOM_COMMENTS_SERVER_URL, url, true)), + setCustomServers: (servers) => dispatch(doSetClientSetting(SETTINGS.CUSTOM_COMMENTS_SERVERS, servers, true)), }); export default connect(select, perform)(SettingCommentsServer); diff --git a/ui/component/settingCommentsServer/view.jsx b/ui/component/settingCommentsServer/view.jsx index 3cbde7e8c..14ce4ff27 100644 --- a/ui/component/settingCommentsServer/view.jsx +++ b/ui/component/settingCommentsServer/view.jsx @@ -1,70 +1,101 @@ // @flow -import { COMMENT_SERVER_NAME } from 'config'; +import { COMMENT_SERVER_API } from 'config'; // COMMENT_SERVER_NAME, import React from 'react'; import Comments from 'comments'; -import { FormField } from 'component/common/form'; - -const DEBOUNCE_TEXT_INPUT_MS = 500; +import ItemPanel from 'component/common/item-panel'; +import ItemInputRow from 'component/common/item-panel-input-row'; +import Button from 'component/button'; type Props = { customServerEnabled: boolean, customServerUrl: string, setCustomServerEnabled: (boolean) => void, setCustomServerUrl: (string) => void, + setCustomServers: (Array<CommentServerDetails>) => void, + customCommentServers: Array<CommentServerDetails>, }; - +const defaultServer = { name: 'Default', url: COMMENT_SERVER_API }; function SettingCommentsServer(props: Props) { - const { customServerEnabled, customServerUrl, setCustomServerEnabled, setCustomServerUrl } = props; - const [url, setUrl] = React.useState(customServerUrl); + const { + customServerEnabled, + customServerUrl, + setCustomServerEnabled, + setCustomServerUrl, + customCommentServers, + setCustomServers, + } = props; + const [addServer, setAddServer] = React.useState(false); + const customServersString = JSON.stringify(customCommentServers); + + // "migrate" to make sure any currently set custom server is in saved list React.useEffect(() => { - const timer = setTimeout(() => { - Comments.setServerUrl(customServerEnabled ? url : undefined); - if (url !== customServerUrl) { - setCustomServerUrl(url); - } - }, DEBOUNCE_TEXT_INPUT_MS); + // const servers = JSON.parse(customServersString); + // if customServerUrl is not in servers, make sure it is. + }, [customServerUrl, customServersString, setCustomServers]); - return () => clearTimeout(timer); - }, [url, customServerUrl, customServerEnabled, setCustomServerUrl]); + // React.useEffect(() => { + // const timer = setTimeout(() => { + // Comments.setServerUrl(customServerEnabled ? url : undefined); + // if (url !== customServerUrl) { + // setCustomServerUrl(url); + // } + // }, DEBOUNCE_TEXT_INPUT_MS); + // + // return () => clearTimeout(timer); + // }, [url, customServerUrl, customServerEnabled, setCustomServerUrl]); + + const handleSelectServer = (serverItem: CommentServerDetails) => { + if (serverItem.url !== COMMENT_SERVER_API) { + alert(`set ${serverItem.url}`); + Comments.setServerUrl(serverItem.url); + setCustomServerUrl(serverItem.url); + setCustomServerEnabled(true); + } else { + alert('reset'); + Comments.setServerUrl(undefined); + setCustomServerEnabled(false); + } + }; + + const handleAddServer = (serverItem: CommentServerDetails) => { + const newCustomServers = customCommentServers.slice(); + newCustomServers.push(serverItem); + setCustomServers(newCustomServers); + handleSelectServer(serverItem); + }; + + const handleRemoveServer = (serverItem) => { + handleSelectServer(defaultServer); + const newCustomServers = customCommentServers.slice().filter((server) => { + return server.url !== serverItem.url; + }); + setCustomServers(newCustomServers); + }; return ( <React.Fragment> - <fieldset-section> - <FormField - type="radio" - name="use_default_comments_server" - label={__('Default comments server (%name%)', { name: COMMENT_SERVER_NAME })} - checked={!customServerEnabled} - onChange={(e) => { - if (e.target.checked) { - setCustomServerEnabled(false); - } - }} - /> - <FormField - type="radio" - name="use_custom_comments_server" - label={__('Custom comments server')} - checked={customServerEnabled} - onChange={(e) => { - if (e.target.checked) { - setCustomServerEnabled(true); - } - }} - /> - - {customServerEnabled && ( - <div className="section__body"> - <FormField - type="text" - placeholder="https://comment.mysite.com" - value={url} - onChange={(e) => setUrl(e.target.value)} - /> + <div className={'fieldset-section'}> + <ItemPanel onClick={handleSelectServer} active={!customServerEnabled} serverDetails={defaultServer} /> + {!!customCommentServers.length && <label>{__('Custom Servers')}</label>} + {customCommentServers.map((server) => ( + <ItemPanel + key={server.url} + active={customServerEnabled && customServerUrl === server.url} + onClick={handleSelectServer} + serverDetails={server} + onRemove={handleRemoveServer} + /> + ))} + </div> + <div className={'fieldset-section'}> + {!addServer && ( + <div className="section__actions"> + <Button type="button" button="link" onClick={() => setAddServer(true)} label={__('Add A Server')} /> </div> )} - </fieldset-section> + {addServer && <ItemInputRow update={handleAddServer} onCancel={setAddServer} />} + </div> </React.Fragment> ); } diff --git a/ui/constants/settings.js b/ui/constants/settings.js index 72363de04..86d0b7e20 100644 --- a/ui/constants/settings.js +++ b/ui/constants/settings.js @@ -41,6 +41,7 @@ export const VIDEO_THEATER_MODE = 'video_theater_mode'; export const VIDEO_PLAYBACK_RATE = 'video_playback_rate'; export const CUSTOM_COMMENTS_SERVER_ENABLED = 'custom_comments_server_enabled'; export const CUSTOM_COMMENTS_SERVER_URL = 'custom_comments_server_url'; +export const CUSTOM_COMMENTS_SERVERS = 'custom_comments_servers'; export const CUSTOM_SHARE_URL_ENABLED = 'custom_share_url_enabled'; export const CUSTOM_SHARE_URL = 'custom_share_url'; export const ENABLE_PRERELEASE_UPDATES = 'enable_prerelease_updates'; diff --git a/ui/redux/reducers/settings.js b/ui/redux/reducers/settings.js index 4e013e35e..bbdd22c09 100644 --- a/ui/redux/reducers/settings.js +++ b/ui/redux/reducers/settings.js @@ -45,6 +45,7 @@ const defaultState = { [SETTINGS.DESKTOP_WINDOW_ZOOM]: 1, [SETTINGS.CUSTOM_COMMENTS_SERVER_ENABLED]: false, [SETTINGS.CUSTOM_COMMENTS_SERVER_URL]: '', + [SETTINGS.CUSTOM_COMMENTS_SERVERS]: [], [SETTINGS.CUSTOM_SHARE_URL_ENABLED]: false, [SETTINGS.CUSTOM_SHARE_URL]: '', diff --git a/ui/scss/all.scss b/ui/scss/all.scss index 8ecba7ad6..c68b9ab87 100644 --- a/ui/scss/all.scss +++ b/ui/scss/all.scss @@ -68,3 +68,4 @@ @import 'component/wallet-tip-send'; @import 'component/swipe-list'; @import 'component/utils'; +@import 'component/item-panel'; diff --git a/ui/scss/component/_form-field.scss b/ui/scss/component/_form-field.scss index 2d7a88c85..cddd06630 100644 --- a/ui/scss/component/_form-field.scss +++ b/ui/scss/component/_form-field.scss @@ -293,7 +293,9 @@ input[type='number'] { fieldset-group { + fieldset-group { - margin-top: var(--spacing-s); + &:not(.fieldset-group--row) { + margin-top: var(--spacing-s); + } } &.fieldset-group--smushed { @@ -339,6 +341,9 @@ fieldset-group { align-items: flex-end; justify-content: center; } + + &:not(.fieldset-group--row) { + } } // This is a special case where the prefix appears "inside" the input diff --git a/ui/scss/component/_item-panel.scss b/ui/scss/component/_item-panel.scss new file mode 100644 index 000000000..51c2bdace --- /dev/null +++ b/ui/scss/component/_item-panel.scss @@ -0,0 +1,42 @@ +.itemPanel { + padding: var(--spacing-m); + margin-bottom: var(--spacing-m); + width: 100%; + background-color: var(--color-card-background); + border-radius: var(--card-radius); + overflow: hidden; + border: 1px solid var(--color-border); + display: flex; + justify-content: space-between; + .button--close { + position: unset; + } + &:last-child { + margin-bottom: 0; + } +} + +.itemPanel__details { + display: flex; + flex-direction: row; + flex-wrap: wrap; + @media (max-width: $breakpoint-small) { + flex-direction: column; + } +} +.itemPanel__name { + min-width: 100px; + width: 100px; +} +.itemPanel__url { + text-overflow: ellipsis; +} + +.itemPanel--active { + color: var(--color-button-toggle-text); + background-color: var(--color-button-toggle-bg); +} + +.itemPanel--input { + padding: 0 0 var(--spacing-s) 0; +} diff --git a/ui/scss/component/section.scss b/ui/scss/component/section.scss index d66a716d0..56c4d12f9 100644 --- a/ui/scss/component/section.scss +++ b/ui/scss/component/section.scss @@ -250,7 +250,9 @@ } fieldset-group { - margin-top: var(--spacing-m); + &:not(.fieldset-group--row) { + margin-top: var(--spacing-m); + } } .tags__input-wrapper {