new custom comment servers ux

This commit is contained in:
zeppi 2022-02-02 01:19:39 -05:00 committed by jessopb
parent f370aa8db1
commit d13397d4dd
12 changed files with 296 additions and 50 deletions

8
flow-typed/Settings.js vendored Normal file
View file

@ -0,0 +1,8 @@
declare type CommentServerDetails = {
name: string,
url: string,
}
declare type WalletServerDetails = {
};

View file

@ -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.", "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.", "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", "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--" "--end--": "--end--"
} }

View file

@ -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;

View file

@ -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;

View file

@ -7,11 +7,13 @@ import SettingCommentsServer from './view';
const select = (state) => ({ const select = (state) => ({
customServerEnabled: makeSelectClientSetting(SETTINGS.CUSTOM_COMMENTS_SERVER_ENABLED)(state), customServerEnabled: makeSelectClientSetting(SETTINGS.CUSTOM_COMMENTS_SERVER_ENABLED)(state),
customServerUrl: makeSelectClientSetting(SETTINGS.CUSTOM_COMMENTS_SERVER_URL)(state), customServerUrl: makeSelectClientSetting(SETTINGS.CUSTOM_COMMENTS_SERVER_URL)(state),
customCommentServers: makeSelectClientSetting(SETTINGS.CUSTOM_COMMENTS_SERVERS)(state),
}); });
const perform = (dispatch) => ({ const perform = (dispatch) => ({
setCustomServerEnabled: (val) => dispatch(doSetClientSetting(SETTINGS.CUSTOM_COMMENTS_SERVER_ENABLED, val, true)), setCustomServerEnabled: (val) => dispatch(doSetClientSetting(SETTINGS.CUSTOM_COMMENTS_SERVER_ENABLED, val, true)),
setCustomServerUrl: (url) => dispatch(doSetClientSetting(SETTINGS.CUSTOM_COMMENTS_SERVER_URL, url, 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); export default connect(select, perform)(SettingCommentsServer);

View file

@ -1,70 +1,101 @@
// @flow // @flow
import { COMMENT_SERVER_NAME } from 'config'; import { COMMENT_SERVER_API } from 'config'; // COMMENT_SERVER_NAME,
import React from 'react'; import React from 'react';
import Comments from 'comments'; import Comments from 'comments';
import { FormField } from 'component/common/form'; import ItemPanel from 'component/common/item-panel';
import ItemInputRow from 'component/common/item-panel-input-row';
const DEBOUNCE_TEXT_INPUT_MS = 500; import Button from 'component/button';
type Props = { type Props = {
customServerEnabled: boolean, customServerEnabled: boolean,
customServerUrl: string, customServerUrl: string,
setCustomServerEnabled: (boolean) => void, setCustomServerEnabled: (boolean) => void,
setCustomServerUrl: (string) => 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,
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 servers = JSON.parse(customServersString);
// if customServerUrl is not in servers, make sure it is.
}, [customServerUrl, customServersString, setCustomServers]);
// 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);
}
}; };
function SettingCommentsServer(props: Props) { const handleAddServer = (serverItem: CommentServerDetails) => {
const { customServerEnabled, customServerUrl, setCustomServerEnabled, setCustomServerUrl } = props; const newCustomServers = customCommentServers.slice();
const [url, setUrl] = React.useState(customServerUrl); newCustomServers.push(serverItem);
setCustomServers(newCustomServers);
handleSelectServer(serverItem);
};
React.useEffect(() => { const handleRemoveServer = (serverItem) => {
const timer = setTimeout(() => { handleSelectServer(defaultServer);
Comments.setServerUrl(customServerEnabled ? url : undefined); const newCustomServers = customCommentServers.slice().filter((server) => {
if (url !== customServerUrl) { return server.url !== serverItem.url;
setCustomServerUrl(url); });
} setCustomServers(newCustomServers);
}, DEBOUNCE_TEXT_INPUT_MS); };
return () => clearTimeout(timer);
}, [url, customServerUrl, customServerEnabled, setCustomServerUrl]);
return ( return (
<React.Fragment> <React.Fragment>
<fieldset-section> <div className={'fieldset-section'}>
<FormField <ItemPanel onClick={handleSelectServer} active={!customServerEnabled} serverDetails={defaultServer} />
type="radio" {!!customCommentServers.length && <label>{__('Custom Servers')}</label>}
name="use_default_comments_server" {customCommentServers.map((server) => (
label={__('Default comments server (%name%)', { name: COMMENT_SERVER_NAME })} <ItemPanel
checked={!customServerEnabled} key={server.url}
onChange={(e) => { active={customServerEnabled && customServerUrl === server.url}
if (e.target.checked) { onClick={handleSelectServer}
setCustomServerEnabled(false); serverDetails={server}
} onRemove={handleRemoveServer}
}}
/>
<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>
<div className={'fieldset-section'}>
{!addServer && (
<div className="section__actions">
<Button type="button" button="link" onClick={() => setAddServer(true)} label={__('Add A Server')} />
</div> </div>
)} )}
</fieldset-section> {addServer && <ItemInputRow update={handleAddServer} onCancel={setAddServer} />}
</div>
</React.Fragment> </React.Fragment>
); );
} }

View file

@ -41,6 +41,7 @@ export const VIDEO_THEATER_MODE = 'video_theater_mode';
export const VIDEO_PLAYBACK_RATE = 'video_playback_rate'; export const VIDEO_PLAYBACK_RATE = 'video_playback_rate';
export const CUSTOM_COMMENTS_SERVER_ENABLED = 'custom_comments_server_enabled'; export const CUSTOM_COMMENTS_SERVER_ENABLED = 'custom_comments_server_enabled';
export const CUSTOM_COMMENTS_SERVER_URL = 'custom_comments_server_url'; 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_ENABLED = 'custom_share_url_enabled';
export const CUSTOM_SHARE_URL = 'custom_share_url'; export const CUSTOM_SHARE_URL = 'custom_share_url';
export const ENABLE_PRERELEASE_UPDATES = 'enable_prerelease_updates'; export const ENABLE_PRERELEASE_UPDATES = 'enable_prerelease_updates';

View file

@ -45,6 +45,7 @@ const defaultState = {
[SETTINGS.DESKTOP_WINDOW_ZOOM]: 1, [SETTINGS.DESKTOP_WINDOW_ZOOM]: 1,
[SETTINGS.CUSTOM_COMMENTS_SERVER_ENABLED]: false, [SETTINGS.CUSTOM_COMMENTS_SERVER_ENABLED]: false,
[SETTINGS.CUSTOM_COMMENTS_SERVER_URL]: '', [SETTINGS.CUSTOM_COMMENTS_SERVER_URL]: '',
[SETTINGS.CUSTOM_COMMENTS_SERVERS]: [],
[SETTINGS.CUSTOM_SHARE_URL_ENABLED]: false, [SETTINGS.CUSTOM_SHARE_URL_ENABLED]: false,
[SETTINGS.CUSTOM_SHARE_URL]: '', [SETTINGS.CUSTOM_SHARE_URL]: '',

View file

@ -68,3 +68,4 @@
@import 'component/wallet-tip-send'; @import 'component/wallet-tip-send';
@import 'component/swipe-list'; @import 'component/swipe-list';
@import 'component/utils'; @import 'component/utils';
@import 'component/item-panel';

View file

@ -293,8 +293,10 @@ input[type='number'] {
fieldset-group { fieldset-group {
+ fieldset-group { + fieldset-group {
&:not(.fieldset-group--row) {
margin-top: var(--spacing-s); margin-top: var(--spacing-s);
} }
}
&.fieldset-group--smushed { &.fieldset-group--smushed {
justify-content: flex-start; justify-content: flex-start;
@ -339,6 +341,9 @@ fieldset-group {
align-items: flex-end; align-items: flex-end;
justify-content: center; justify-content: center;
} }
&:not(.fieldset-group--row) {
}
} }
// This is a special case where the prefix appears "inside" the input // This is a special case where the prefix appears "inside" the input

View file

@ -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;
}

View file

@ -250,8 +250,10 @@
} }
fieldset-group { fieldset-group {
&:not(.fieldset-group--row) {
margin-top: var(--spacing-m); margin-top: var(--spacing-m);
} }
}
.tags__input-wrapper { .tags__input-wrapper {
.tag__input { .tag__input {