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

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) => ({
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);

View file

@ -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,
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 { customServerEnabled, customServerUrl, setCustomServerEnabled, setCustomServerUrl } = props;
const [url, setUrl] = React.useState(customServerUrl);
const handleAddServer = (serverItem: CommentServerDetails) => {
const newCustomServers = customCommentServers.slice();
newCustomServers.push(serverItem);
setCustomServers(newCustomServers);
handleSelectServer(serverItem);
};
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 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>
);
}

View file

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

View file

@ -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]: '',

View file

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

View file

@ -293,8 +293,10 @@ input[type='number'] {
fieldset-group {
+ fieldset-group {
&:not(.fieldset-group--row) {
margin-top: var(--spacing-s);
}
}
&.fieldset-group--smushed {
justify-content: flex-start;
@ -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

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 {
&:not(.fieldset-group--row) {
margin-top: var(--spacing-m);
}
}
.tags__input-wrapper {
.tag__input {