Browse Source

format code

dev
thuhole 5 years ago
parent
commit
7636e45c00
  1. 55
      .eslintrc
  2. 7
      .prettierrc.js
  3. 252
      src/App.js
  4. 159
      src/AudioWidget.js
  5. 615
      src/Common.js
  6. 504
      src/Config.js
  7. 1809
      src/Flows.js
  8. 36
      src/Markdown.js
  9. 135
      src/Message.js
  10. 207
      src/PressureHelper.js
  11. 97
      src/Sidebar.js
  12. 289
      src/Title.js
  13. 1229
      src/UserAction.js
  14. 311
      src/cache.js
  15. 37
      src/color_picker.js
  16. 353
      src/flows_api.js
  17. 26
      src/registerServiceWorker.js
  18. 54
      src/text_splitter.js

55
.eslintrc

@ -0,0 +1,55 @@
{
"env": {
"browser": true,
"es6": true,
"node": true
},
"extends": [
"plugin:react/recommended",
"plugin:prettier/recommended",
"prettier/react"
],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly",
"React": true
},
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 2018,
"sourceType": "module"
},
"plugins": [
"prettier",
"react"
],
"settings": {
"react": {
"version": "detect"
}
},
"ignorePatterns": [
"src/infrastructure/",
"src/react-lazyload/"
],
"rules": {
"prettier/prettier": "warn",
"react/jsx-indent": [
"error",
2,
{
"indentLogicalExpressions": true
}
],
"react/prop-types": "off",
"react/jsx-no-target-blank": "off",
"no-unused-vars": [
"warn",
{
"args": "none"
}
]
}
}

7
.prettierrc.js

@ -0,0 +1,7 @@
module.exports = {
trailingComma: 'all',
tabWidth: 2,
semi: true,
singleQuote: true,
endOfLine: 'auto',
}

252
src/App.js

@ -1,131 +1,151 @@
import React, {Component} from 'react'; import React, { Component } from 'react';
import {Flow} from './Flows'; import { Flow } from './Flows';
import {Title} from './Title'; import { Title } from './Title';
import {Sidebar} from './Sidebar'; import { Sidebar } from './Sidebar';
import {PressureHelper} from './PressureHelper'; import { PressureHelper } from './PressureHelper';
import {TokenCtx} from './UserAction'; import { TokenCtx } from './UserAction';
import {load_config,bgimg_style} from './Config'; import { load_config, bgimg_style } from './Config';
import {listen_darkmode} from './infrastructure/functions'; import { listen_darkmode } from './infrastructure/functions';
import {LoginPopup, TitleLine} from './infrastructure/widgets'; import { LoginPopup, TitleLine } from './infrastructure/widgets';
const MAX_SIDEBAR_STACK_SIZE=10; const MAX_SIDEBAR_STACK_SIZE = 10;
function DeprecatedAlert(props) { function DeprecatedAlert(props) {
return ( return <div id="global-hint-container" style={{ display: 'none' }} />;
<div id="global-hint-container" style={{display: 'none'}} />
);
} }
class App extends Component { class App extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
load_config(); load_config();
listen_darkmode({default: undefined, light: false, dark: true}[window.config.color_scheme]); listen_darkmode(
this.state={ { default: undefined, light: false, dark: true }[
sidebar_stack: [[null,null]], // list of [status, content] window.config.color_scheme
mode: 'list', // list, single, search, attention ],
search_text: null, );
flow_render_key: +new Date(), this.state = {
token: localStorage['TOKEN']||null, sidebar_stack: [[null, null]], // list of [status, content]
}; mode: 'list', // list, single, search, attention
this.show_sidebar_bound=this.show_sidebar.bind(this); search_text: null,
this.set_mode_bound=this.set_mode.bind(this); flow_render_key: +new Date(),
this.on_pressure_bound=this.on_pressure.bind(this); token: localStorage['TOKEN'] || null,
// a silly self-deceptive approach to ban guests, enough to fool those muggles };
// document cookie 'pku_ip_flag=yes' this.show_sidebar_bound = this.show_sidebar.bind(this);
this.inthu_flag=window[atob('ZG9jdW1lbnQ')][atob('Y29va2ll')].indexOf(atob('dGh1X2lwX2ZsYWc9eWVz'))!==-1; this.set_mode_bound = this.set_mode.bind(this);
} this.on_pressure_bound = this.on_pressure.bind(this);
// a silly self-deceptive approach to ban guests, enough to fool those muggles
// document cookie 'pku_ip_flag=yes'
this.inthu_flag =
window[atob('ZG9jdW1lbnQ')][atob('Y29va2ll')].indexOf(
atob('dGh1X2lwX2ZsYWc9eWVz'),
) !== -1;
}
static is_darkmode() { static is_darkmode() {
if(window.config.color_scheme==='dark') return true; if (window.config.color_scheme === 'dark') return true;
if(window.config.color_scheme==='light') return false; if (window.config.color_scheme === 'light') return false;
else { // 'default' else {
return window.matchMedia('(prefers-color-scheme: dark)').matches; // 'default'
} return window.matchMedia('(prefers-color-scheme: dark)').matches;
} }
}
on_pressure() { on_pressure() {
if(this.state.sidebar_stack.length>1) if (this.state.sidebar_stack.length > 1)
this.show_sidebar(null,null,'clear'); this.show_sidebar(null, null, 'clear');
else else this.set_mode('list', null);
this.set_mode('list',null); }
}
show_sidebar(title,content,mode='push') { show_sidebar(title, content, mode = 'push') {
this.setState((prevState)=>{ this.setState((prevState) => {
let ns=prevState.sidebar_stack.slice(); let ns = prevState.sidebar_stack.slice();
if(mode==='push') { if (mode === 'push') {
if(ns.length>MAX_SIDEBAR_STACK_SIZE) if (ns.length > MAX_SIDEBAR_STACK_SIZE) ns.splice(1, 1);
ns.splice(1,1); ns = ns.concat([[title, content]]);
ns=ns.concat([[title,content]]); } else if (mode === 'pop') {
} else if(mode==='pop') { if (ns.length === 1) return;
if(ns.length===1) return; ns.pop();
ns.pop(); } else if (mode === 'replace') {
} else if(mode==='replace') { ns.pop();
ns.pop(); ns = ns.concat([[title, content]]);
ns=ns.concat([[title,content]]); } else if (mode === 'clear') {
} else if(mode==='clear') { ns = [[null, null]];
ns=[[null,null]]; } else throw new Error('bad show_sidebar mode');
} else return {
throw new Error('bad show_sidebar mode'); sidebar_stack: ns,
return { };
sidebar_stack: ns, });
}; }
});
}
set_mode(mode,search_text) { set_mode(mode, search_text) {
this.setState({ this.setState({
mode: mode, mode: mode,
search_text: search_text, search_text: search_text,
flow_render_key: +new Date(), flow_render_key: +new Date(),
}); });
} }
render() { render() {
return ( return (
<TokenCtx.Provider value={{ <TokenCtx.Provider
value: this.state.token, value={{
set_value: (x)=>{ value: this.state.token,
localStorage['TOKEN']=x||''; set_value: (x) => {
this.setState({ localStorage['TOKEN'] = x || '';
token: x, this.setState({
}); token: x,
}, });
}}> },
<PressureHelper callback={this.on_pressure_bound} /> }}
<div className="bg-img" style={bgimg_style()} /> >
<Title show_sidebar={this.show_sidebar_bound} set_mode={this.set_mode_bound} /> <PressureHelper callback={this.on_pressure_bound} />
<TokenCtx.Consumer>{(token)=>( <div className="bg-img" style={bgimg_style()} />
<div className="left-container"> <Title
<DeprecatedAlert token={token.value} /> show_sidebar={this.show_sidebar_bound}
{!token.value && set_mode={this.set_mode_bound}
<div className="flow-item-row aux-margin"> />
<div className="box box-tip"> <TokenCtx.Consumer>
<p> {(token) => (
<LoginPopup token_callback={token.set_value}>{(do_popup)=>( <div className="left-container">
<a onClick={do_popup}> <DeprecatedAlert token={token.value} />
<span className="icon icon-login" /> {!token.value && (
&nbsp;登录到 T大树洞 <div className="flow-item-row aux-margin">
</a> <div className="box box-tip">
)}</LoginPopup> <p>
</p> <LoginPopup token_callback={token.set_value}>
</div> {(do_popup) => (
</div> <a onClick={do_popup}>
} <span className="icon icon-login" />
{this.inthu_flag||token.value ? &nbsp;登录到 T大树洞
<Flow key={this.state.flow_render_key} show_sidebar={this.show_sidebar_bound} </a>
mode={this.state.mode} search_text={this.state.search_text} token={token.value} )}
/> : </LoginPopup>
<TitleLine text="请登录后查看内容" /> </p>
} </div>
<br /> </div>
</div> )}
)}</TokenCtx.Consumer> {this.inthu_flag || token.value ? (
<Sidebar show_sidebar={this.show_sidebar_bound} stack={this.state.sidebar_stack} /> <Flow
</TokenCtx.Provider> key={this.state.flow_render_key}
); show_sidebar={this.show_sidebar_bound}
} mode={this.state.mode}
search_text={this.state.search_text}
token={token.value}
/>
) : (
<TitleLine text="请登录后查看内容" />
)}
<br />
</div>
)}
</TokenCtx.Consumer>
<Sidebar
show_sidebar={this.show_sidebar_bound}
stack={this.state.sidebar_stack}
/>
</TokenCtx.Provider>
);
}
} }
export default App; export default App;

159
src/AudioWidget.js

@ -1,91 +1,92 @@
import React, {Component} from 'react'; import React, { Component } from 'react';
import load from 'load-script'; import load from 'load-script';
window.audio_cache={}; window.audio_cache = {};
function load_amrnb() { function load_amrnb() {
return new Promise((resolve,reject)=>{ return new Promise((resolve, reject) => {
if(window.AMR) if (window.AMR) resolve();
resolve(); else
else load('static/amr_all.min.js', (err) => {
load('static/amr_all.min.js', (err)=>{ if (err) reject(err);
if(err) else resolve();
reject(err); });
else });
resolve();
});
});
} }
export class AudioWidget extends Component { export class AudioWidget extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state={ this.state = {
url: this.props.src, url: this.props.src,
state: 'waiting', state: 'waiting',
data: null, data: null,
}; };
}
load() {
if (window.audio_cache[this.state.url]) {
this.setState({
state: 'loaded',
data: window.audio_cache[this.state.url],
});
return;
} }
load() { console.log('fetching audio', this.state.url);
if(window.audio_cache[this.state.url]) { this.setState({
this.setState({ state: 'loading',
state: 'loaded', });
data: window.audio_cache[this.state.url], Promise.all([fetch(this.state.url), load_amrnb()]).then((res) => {
}); res[0].blob().then((blob) => {
const reader = new FileReader();
reader.onload = (event) => {
const raw = new window.AMR().decode(event.target.result);
if (!raw) {
alert('audio decoding failed');
return; return;
} }
const wave = window.PCMData.encode({
console.log('fetching audio',this.state.url); sampleRate: 8000,
this.setState({ channelCount: 1,
state: 'loading', bytesPerSample: 2,
}); data: raw,
Promise.all([ });
fetch(this.state.url), const binary_wave = new Uint8Array(wave.length);
load_amrnb(), for (let i = 0; i < wave.length; i++)
]) binary_wave[i] = wave.charCodeAt(i);
.then((res)=>{
res[0].blob().then((blob)=>{
const reader=new FileReader();
reader.onload=(event)=>{
const raw=new window.AMR().decode(event.target.result);
if(!raw) {
alert('audio decoding failed');
return;
}
const wave=window.PCMData.encode({
sampleRate: 8000,
channelCount: 1,
bytesPerSample: 2,
data: raw
});
const binary_wave=new Uint8Array(wave.length);
for(let i=0;i<wave.length;i++)
binary_wave[i]=wave.charCodeAt(i);
const objurl=URL.createObjectURL(new Blob([binary_wave], {type: 'audio/wav'})); const objurl = URL.createObjectURL(
window.audio_cache[this.state.url]=objurl; new Blob([binary_wave], { type: 'audio/wav' }),
this.setState({ );
state: 'loaded', window.audio_cache[this.state.url] = objurl;
data: objurl, this.setState({
}); state: 'loaded',
}; data: objurl,
reader.readAsBinaryString(blob); });
}); };
this.setState({ reader.readAsBinaryString(blob);
state: 'decoding', });
}); this.setState({
}); state: 'decoding',
} });
});
}
render() { render() {
if(this.state.state==='waiting') if (this.state.state === 'waiting')
return (<p><a onClick={this.load.bind(this)}>加载音频</a></p>); return (
if(this.state.state==='loading') <p>
return (<p>正在下载</p>); <a onClick={this.load.bind(this)}>加载音频</a>
else if(this.state.state==='decoding') </p>
return (<p>正在解码</p>); );
else if(this.state.state==='loaded') if (this.state.state === 'loading') return <p>正在下载</p>;
return (<p><audio src={this.state.data} controls /></p>); else if (this.state.state === 'decoding') return <p>正在解码</p>;
} else if (this.state.state === 'loaded')
} return (
<p>
<audio src={this.state.data} controls />
</p>
);
}
}

615
src/Common.js

@ -1,310 +1,415 @@
import React, {Component, PureComponent} from 'react'; import React, { Component, PureComponent } from 'react';
import {format_time,Time,TitleLine} from './infrastructure/widgets'; import { format_time, Time, TitleLine } from './infrastructure/widgets';
import {THUHOLE_API_ROOT} from './flows_api'; import { THUHOLE_API_ROOT } from './flows_api';
import HtmlToReact from 'html-to-react' import HtmlToReact from 'html-to-react';
import './Common.css'; import './Common.css';
import { URL_PID_RE, URL_RE, PID_RE, NICKNAME_RE, split_text } from './text_splitter'; import {
URL_PID_RE,
URL_RE,
PID_RE,
NICKNAME_RE,
split_text,
} from './text_splitter';
import renderMd from './Markdown' import renderMd from './Markdown';
export {format_time,Time,TitleLine}; export { format_time, Time, TitleLine };
export const API_BASE=THUHOLE_API_ROOT+'services/thuhole'; export const API_BASE = THUHOLE_API_ROOT + 'services/thuhole';
// https://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex // https://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex
function escape_regex(string) { function escape_regex(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
} }
export function build_highlight_re(txt,split,option='g') { export function build_highlight_re(txt, split, option = 'g') {
return txt ? new RegExp(`(${txt.split(split).filter((x)=>!!x).map(escape_regex).join('|')})`,option) : /^$/g; return txt
? new RegExp(
`(${txt
.split(split)
.filter((x) => !!x)
.map(escape_regex)
.join('|')})`,
option,
)
: /^$/g;
} }
export function ColoredSpan(props) { export function ColoredSpan(props) {
return ( return (
<span className="colored-span" style={{ <span
'--coloredspan-bgcolor-light': props.colors[0], className="colored-span"
'--coloredspan-bgcolor-dark': props.colors[1], style={{
}}>{props.children}</span> '--coloredspan-bgcolor-light': props.colors[0],
) '--coloredspan-bgcolor-dark': props.colors[1],
}}
>
{props.children}
</span>
);
} }
function normalize_url(url) { function normalize_url(url) {
return /^https?:\/\//.test(url) ? url : 'http://'+url; return /^https?:\/\//.test(url) ? url : 'http://' + url;
} }
export class HighlightedText extends PureComponent { export class HighlightedText extends PureComponent {
render() { render() {
return ( return (
<pre> <pre>
{this.props.parts.map((part,idx)=>{ {this.props.parts.map((part, idx) => {
let [rule,p]=part; let [rule, p] = part;
return ( return (
<span key={idx}>{ <span key={idx}>
rule==='url_pid' ? <span className="url-pid-link" title={p}>/##</span> : {rule === 'url_pid' ? (
rule==='url' ? <a href={normalize_url(p)} target="_blank" rel="noopener">{p}</a> : <span className="url-pid-link" title={p}>
rule==='pid' ? <a href={'#'+p} onClick={(e)=>{e.preventDefault(); this.props.show_pid(p.substring(1));}}>{p}</a> : /##
rule==='nickname' ? <ColoredSpan colors={this.props.color_picker.get(p)}>{p}</ColoredSpan> : </span>
rule==='search' ? <span className="search-query-highlight">{p}</span> : ) : rule === 'url' ? (
p <a href={normalize_url(p)} target="_blank" rel="noopener">
}</span> {p}
); </a>
})} ) : rule === 'pid' ? (
</pre> <a
) href={'#' + p}
} onClick={(e) => {
e.preventDefault();
this.props.show_pid(p.substring(1));
}}
>
{p}
</a>
) : rule === 'nickname' ? (
<ColoredSpan colors={this.props.color_picker.get(p)}>
{p}
</ColoredSpan>
) : rule === 'search' ? (
<span className="search-query-highlight">{p}</span>
) : (
p
)}
</span>
);
})}
</pre>
);
}
} }
// props: text, show_pid, color_picker // props: text, show_pid, color_picker
export class HighlightedMarkdown extends Component { export class HighlightedMarkdown extends Component {
render() { render() {
const props = this.props const props = this.props;
const processDefs = new HtmlToReact.ProcessNodeDefinitions(React) const processDefs = new HtmlToReact.ProcessNodeDefinitions(React);
const processInstructions = [ const processInstructions = [
{ {
shouldProcessNode: (node) => node.name === 'img', // disable images shouldProcessNode: (node) => node.name === 'img', // disable images
processNode (node, children, index) { processNode(node, children, index) {
return (<div key={index}>[图片]</div>) return <div key={index}>[图片]</div>;
} },
}, },
{ {
shouldProcessNode: (node) => (/^h[123456]$/.test(node.name)), shouldProcessNode: (node) => /^h[123456]$/.test(node.name),
processNode (node, children, index) { processNode(node, children, index) {
let currentLevel = +(node.name[1]) let currentLevel = +node.name[1];
if (currentLevel < 3) currentLevel = 3; if (currentLevel < 3) currentLevel = 3;
const HeadingTag = `h${currentLevel}` const HeadingTag = `h${currentLevel}`;
return ( return <HeadingTag key={index}>{children}</HeadingTag>;
<HeadingTag key={index}>{children}</HeadingTag> },
) },
} {
}, shouldProcessNode: (node) => node.name === 'a',
{ processNode(node, children, index) {
shouldProcessNode: (node) => node.name === 'a', return (
processNode (node, children, index) { <a
return ( href={normalize_url(node.attribs.href)}
<a href={normalize_url(node.attribs.href)} target="_blank" rel="noopenner noreferrer" className="ext-link" key={index}> target="_blank"
{children} rel="noopenner noreferrer"
<span className="icon icon-new-tab" /> className="ext-link"
</a> key={index}
) >
} {children}
}, <span className="icon icon-new-tab" />
{ </a>
shouldProcessNode (node) { );
return node.type === 'text' && (!node.parent || !node.parent.attribs || node.parent.attribs['encoding'] != "application/x-tex") // pid, nickname, search },
}, },
processNode (node, children, index) { {
const originalText = node.data shouldProcessNode(node) {
const splitted = split_text(originalText, [ return (
['url_pid', URL_PID_RE], node.type === 'text' &&
['url',URL_RE], (!node.parent ||
['pid',PID_RE], !node.parent.attribs ||
['nickname',NICKNAME_RE], node.parent.attribs['encoding'] != 'application/x-tex')
]) ); // pid, nickname, search
},
processNode(node, children, index) {
const originalText = node.data;
const splitted = split_text(originalText, [
['url_pid', URL_PID_RE],
['url', URL_RE],
['pid', PID_RE],
['nickname', NICKNAME_RE],
]);
return ( return (
<React.Fragment key={index}> <React.Fragment key={index}>
{splitted.map(([rule, p], idx) => { {splitted.map(([rule, p], idx) => {
return (<span key={idx}> return (
{ <span key={idx}>
rule==='url_pid' ? <span className="url-pid-link" title={p}>/##</span> : {rule === 'url_pid' ? (
rule==='url' ? <a href={normalize_url(p)} className="ext-link" target="_blank" rel="noopener noreferrer"> <span className="url-pid-link" title={p}>
{p} /##
<span className="icon icon-new-tab" /> </span>
</a> : ) : rule === 'url' ? (
rule==='pid' ? <a href={'#'+p} onClick={(e)=>{e.preventDefault(); props.show_pid(p.substring(1));}}>{p}</a> : <a
rule==='nickname' ? <ColoredSpan colors={props.color_picker.get(p)}>{p}</ColoredSpan> : href={normalize_url(p)}
rule==='search' ? <span className="search-query-highlight">{p}</span> : className="ext-link"
p} target="_blank"
</span>) rel="noopener noreferrer"
})} >
</React.Fragment> {p}
) <span className="icon icon-new-tab" />
} </a>
}, ) : rule === 'pid' ? (
{ <a
shouldProcessNode: () => true, href={'#' + p}
processNode: processDefs.processDefaultNode onClick={(e) => {
} e.preventDefault();
] props.show_pid(p.substring(1));
const parser = new HtmlToReact.Parser() }}
if (props.author && props.text.match(/^(?:#+ |>|```|\t|\s*-|\s*\d+\.)/)) { >
const renderedMarkdown = renderMd(props.text) {p}
return ( </a>
<> ) : rule === 'nickname' ? (
{props.author} <ColoredSpan colors={props.color_picker.get(p)}>
{parser.parseWithInstructions(renderedMarkdown, node => node.type !== 'script', processInstructions) || ''} {p}
</> </ColoredSpan>
) ) : rule === 'search' ? (
} else { <span className="search-query-highlight">{p}</span>
let rawMd = props.text ) : (
if (props.author) rawMd = props.author + ' ' + rawMd p
const renderedMarkdown = renderMd(rawMd) )}
return (parser.parseWithInstructions(renderedMarkdown, node => node.type !== 'script', processInstructions) || null) </span>
} );
})}
</React.Fragment>
);
},
},
{
shouldProcessNode: () => true,
processNode: processDefs.processDefaultNode,
},
];
const parser = new HtmlToReact.Parser();
if (props.author && props.text.match(/^(?:#+ |>|```|\t|\s*-|\s*\d+\.)/)) {
const renderedMarkdown = renderMd(props.text);
return (
<>
{props.author}
{parser.parseWithInstructions(
renderedMarkdown,
(node) => node.type !== 'script',
processInstructions,
) || ''}
</>
);
} else {
let rawMd = props.text;
if (props.author) rawMd = props.author + ' ' + rawMd;
const renderedMarkdown = renderMd(rawMd);
return (
parser.parseWithInstructions(
renderedMarkdown,
(node) => node.type !== 'script',
processInstructions,
) || null
);
} }
}
} }
window.TEXTAREA_BACKUP={}; window.TEXTAREA_BACKUP = {};
export class SafeTextarea extends Component { export class SafeTextarea extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state={ this.state = {
text: '', text: '',
}; };
this.on_change_bound=this.on_change.bind(this); this.on_change_bound = this.on_change.bind(this);
this.on_keydown_bound=this.on_keydown.bind(this); this.on_keydown_bound = this.on_keydown.bind(this);
this.clear=this.clear.bind(this); this.clear = this.clear.bind(this);
this.area_ref=React.createRef(); this.area_ref = React.createRef();
this.change_callback=props.on_change||(()=>{}); this.change_callback = props.on_change || (() => {});
this.submit_callback=props.on_submit||(()=>{}); this.submit_callback = props.on_submit || (() => {});
} }
componentDidMount() {
this.setState({
text: window.TEXTAREA_BACKUP[this.props.id]||''
},()=>{
this.change_callback(this.state.text);
});
}
componentWillUnmount() { componentDidMount() {
window.TEXTAREA_BACKUP[this.props.id]=this.state.text; this.setState(
{
text: window.TEXTAREA_BACKUP[this.props.id] || '',
},
() => {
this.change_callback(this.state.text); this.change_callback(this.state.text);
} },
);
}
on_change(event) { componentWillUnmount() {
this.setState({ window.TEXTAREA_BACKUP[this.props.id] = this.state.text;
text: event.target.value, this.change_callback(this.state.text);
}); }
this.change_callback(event.target.value);
}
on_keydown(event) {
if(event.key==='Enter' && event.ctrlKey && !event.altKey) {
event.preventDefault();
this.submit_callback();
}
}
clear() { on_change(event) {
this.setState({ this.setState({
text: '', text: event.target.value,
}); });
} this.change_callback(event.target.value);
set(text) { }
this.change_callback(text); on_keydown(event) {
this.setState({ if (event.key === 'Enter' && event.ctrlKey && !event.altKey) {
text: text, event.preventDefault();
}); this.submit_callback();
}
get() {
return this.state.text;
}
focus() {
this.area_ref.current.focus();
} }
}
render() { clear() {
return ( this.setState({
<textarea ref={this.area_ref} onChange={this.on_change_bound} value={this.state.text} onKeyDown={this.on_keydown_bound} /> text: '',
) });
} }
set(text) {
this.change_callback(text);
this.setState({
text: text,
});
}
get() {
return this.state.text;
}
focus() {
this.area_ref.current.focus();
}
render() {
return (
<textarea
ref={this.area_ref}
onChange={this.on_change_bound}
value={this.state.text}
onKeyDown={this.on_keydown_bound}
/>
);
}
} }
let pwa_prompt_event=null; let pwa_prompt_event = null;
window.addEventListener('beforeinstallprompt', (e) => { window.addEventListener('beforeinstallprompt', (e) => {
console.log('pwa: received before install prompt'); console.log('pwa: received before install prompt');
pwa_prompt_event=e; pwa_prompt_event = e;
}); });
export function PromotionBar(props) { export function PromotionBar(props) {
let is_ios=/iPhone|iPad|iPod/i.test(window.navigator.userAgent); let is_ios = /iPhone|iPad|iPod/i.test(window.navigator.userAgent);
let is_installed=(window.matchMedia('(display-mode: standalone)').matches) || (window.navigator.standalone); let is_installed =
window.matchMedia('(display-mode: standalone)').matches ||
window.navigator.standalone;
if(is_installed) if (is_installed) return null;
return null;
if(is_ios) if (is_ios)
// noinspection JSConstructorReturnsPrimitive // noinspection JSConstructorReturnsPrimitive
return !navigator.standalone ? ( return !navigator.standalone ? (
<div className="box promotion-bar"> <div className="box promotion-bar">
<span className="icon icon-about" />&nbsp; <span className="icon icon-about" />
Safari 把树洞 <b>添加到主屏幕</b> &nbsp; Safari 把树洞 <b>添加到主屏幕</b>
</div> </div>
) : null; ) : null;
else // noinspection JSConstructorReturnsPrimitive
// noinspection JSConstructorReturnsPrimitive else
return pwa_prompt_event ? ( return pwa_prompt_event ? (
<div className="box promotion-bar"> <div className="box promotion-bar">
<span className="icon icon-about" />&nbsp; <span className="icon icon-about" />
把网页版树洞 <b><a onClick={()=>{ &nbsp; 把网页版树洞{' '}
if(pwa_prompt_event) <b>
pwa_prompt_event.prompt(); <a
}}>安装到桌面</a></b> onClick={() => {
</div> if (pwa_prompt_event) pwa_prompt_event.prompt();
) : null; }}
>
安装到桌面
</a>
</b>{' '}
更好用
</div>
) : null;
} }
export class ClickHandler extends PureComponent { export class ClickHandler extends PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.state={ this.state = {
moved: true, moved: true,
init_y: 0, init_y: 0,
init_x: 0, init_x: 0,
}; };
this.on_begin_bound=this.on_begin.bind(this); this.on_begin_bound = this.on_begin.bind(this);
this.on_move_bound=this.on_move.bind(this); this.on_move_bound = this.on_move.bind(this);
this.on_end_bound=this.on_end.bind(this); this.on_end_bound = this.on_end.bind(this);
this.MOVE_THRESHOLD=3; this.MOVE_THRESHOLD = 3;
this.last_fire=0; this.last_fire = 0;
} }
on_begin(e) { on_begin(e) {
//console.log('click',(e.touches?e.touches[0]:e).screenY,(e.touches?e.touches[0]:e).screenX); //console.log('click',(e.touches?e.touches[0]:e).screenY,(e.touches?e.touches[0]:e).screenX);
this.setState({ this.setState({
moved: false, moved: false,
init_y: (e.touches?e.touches[0]:e).screenY, init_y: (e.touches ? e.touches[0] : e).screenY,
init_x: (e.touches?e.touches[0]:e).screenX, init_x: (e.touches ? e.touches[0] : e).screenX,
}); });
} }
on_move(e) { on_move(e) {
if(!this.state.moved) { if (!this.state.moved) {
let mvmt=Math.abs((e.touches?e.touches[0]:e).screenY-this.state.init_y)+Math.abs((e.touches?e.touches[0]:e).screenX-this.state.init_x); let mvmt =
//console.log('move',mvmt); Math.abs((e.touches ? e.touches[0] : e).screenY - this.state.init_y) +
if(mvmt>this.MOVE_THRESHOLD) Math.abs((e.touches ? e.touches[0] : e).screenX - this.state.init_x);
this.setState({ //console.log('move',mvmt);
moved: true, if (mvmt > this.MOVE_THRESHOLD)
});
}
}
on_end(event) {
//console.log('end');
if(!this.state.moved)
this.do_callback(event);
this.setState({ this.setState({
moved: true, moved: true,
}); });
} }
}
on_end(event) {
//console.log('end');
if (!this.state.moved) this.do_callback(event);
this.setState({
moved: true,
});
}
do_callback(event) { do_callback(event) {
if(this.last_fire+100>+new Date()) return; if (this.last_fire + 100 > +new Date()) return;
this.last_fire=+new Date(); this.last_fire = +new Date();
this.props.callback(event); this.props.callback(event);
} }
render() { render() {
return ( return (
<div onTouchStart={this.on_begin_bound} onMouseDown={this.on_begin_bound} <div
onTouchMove={this.on_move_bound} onMouseMove={this.on_move_bound} onTouchStart={this.on_begin_bound}
onClick={this.on_end_bound} > onMouseDown={this.on_begin_bound}
{this.props.children} onTouchMove={this.on_move_bound}
</div> onMouseMove={this.on_move_bound}
) onClick={this.on_end_bound}
} >
{this.props.children}
</div>
);
}
} }

504
src/Config.js

@ -1,255 +1,327 @@
import React, {Component, PureComponent} from 'react'; import React, { Component, PureComponent } from 'react';
import './Config.css'; import './Config.css';
const BUILTIN_IMGS={ const BUILTIN_IMGS = {
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/gbp.jpg': '寻觅繁星(默认)', 'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/gbp.jpg':
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/eriri.jpg': '平成著名画师', '寻觅繁星(默认)',
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/yurucamp.jpg': '露营天下第一', 'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/eriri.jpg':
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/minecraft.jpg': '麦恩·库拉夫特', '平成著名画师',
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/cyberpunk.jpg': '赛博城市', 'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/yurucamp.jpg':
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/bj.jpg': '城市的星光', '露营天下第一',
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/sif.jpg': '梦开始的地方', 'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/minecraft.jpg':
'麦恩·库拉夫特',
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/cyberpunk.jpg':
'赛博城市',
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/bj.jpg':
'城市的星光',
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/sif.jpg':
'梦开始的地方',
}; };
const DEFAULT_CONFIG={ const DEFAULT_CONFIG = {
background_img: 'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/gbp.jpg', background_img:
background_color: '#113366', 'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/gbp.jpg',
pressure: false, background_color: '#113366',
easter_egg: true, pressure: false,
color_scheme: 'default', easter_egg: true,
fold: true color_scheme: 'default',
fold: true,
}; };
export function load_config() { export function load_config() {
let config=Object.assign({},DEFAULT_CONFIG); let config = Object.assign({}, DEFAULT_CONFIG);
let loaded_config; let loaded_config;
try { try {
loaded_config=JSON.parse(localStorage['hole_config']||'{}'); loaded_config = JSON.parse(localStorage['hole_config'] || '{}');
} catch(e) { } catch (e) {
alert('设置加载失败,将重置为默认设置!\n'+e); alert('设置加载失败,将重置为默认设置!\n' + e);
delete localStorage['hole_config']; delete localStorage['hole_config'];
loaded_config={}; loaded_config = {};
} }
// unrecognized configs are removed // unrecognized configs are removed
Object.keys(loaded_config).forEach((key)=>{ Object.keys(loaded_config).forEach((key) => {
if(config[key]!==undefined) if (config[key] !== undefined) config[key] = loaded_config[key];
config[key]=loaded_config[key]; });
});
console.log('config loaded',config); console.log('config loaded', config);
window.config=config; window.config = config;
} }
export function save_config() { export function save_config() {
localStorage['hole_config']=JSON.stringify(window.config); localStorage['hole_config'] = JSON.stringify(window.config);
load_config(); load_config();
} }
export function bgimg_style(img,color) { export function bgimg_style(img, color) {
if(img===undefined) img=window.config.background_img; if (img === undefined) img = window.config.background_img;
if(color===undefined) color=window.config.background_color; if (color === undefined) color = window.config.background_color;
return { return {
background: 'transparent center center', background: 'transparent center center',
backgroundImage: img===null ? 'unset' : 'url("'+encodeURI(img)+'")', backgroundImage: img === null ? 'unset' : 'url("' + encodeURI(img) + '")',
backgroundColor: color, backgroundColor: color,
backgroundSize: 'cover', backgroundSize: 'cover',
}; };
} }
class ConfigBackground extends PureComponent { class ConfigBackground extends PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.state={ this.state = {
img: window.config.background_img, img: window.config.background_img,
color: window.config.background_color, color: window.config.background_color,
}; };
} }
save_changes() { save_changes() {
this.props.callback({ this.props.callback({
background_img: this.state.img, background_img: this.state.img,
background_color: this.state.color, background_color: this.state.color,
}); });
} }
on_select(e) { on_select(e) {
let value=e.target.value; let value = e.target.value;
this.setState({ this.setState(
img: value==='##other' ? '' : {
value==='##color' ? null : value, img: value === '##other' ? '' : value === '##color' ? null : value,
},this.save_changes.bind(this)); },
} this.save_changes.bind(this),
on_change_img(e) { );
this.setState({ }
img: e.target.value, on_change_img(e) {
},this.save_changes.bind(this)); this.setState(
} {
on_change_color(e) { img: e.target.value,
this.setState({ },
color: e.target.value, this.save_changes.bind(this),
},this.save_changes.bind(this)); );
} }
on_change_color(e) {
this.setState(
{
color: e.target.value,
},
this.save_changes.bind(this),
);
}
render() { render() {
let img_select= this.state.img===null ? '##color' : let img_select =
Object.keys(BUILTIN_IMGS).indexOf(this.state.img)===-1 ? '##other' : this.state.img; this.state.img === null
return ( ? '##color'
<div> : Object.keys(BUILTIN_IMGS).indexOf(this.state.img) === -1
<p> ? '##other'
<b>背景图片</b> : this.state.img;
<select value={img_select} onChange={this.on_select.bind(this)}> return (
{Object.keys(BUILTIN_IMGS).map((key)=>( <div>
<option key={key} value={key}>{BUILTIN_IMGS[key]}</option> <p>
))} <b>背景图片</b>
<option value="##other">输入图片网址</option> <select value={img_select} onChange={this.on_select.bind(this)}>
<option value="##color">纯色背景</option> {Object.keys(BUILTIN_IMGS).map((key) => (
</select> <option key={key} value={key}>
&nbsp; {BUILTIN_IMGS[key]}
{img_select==='##other' && </option>
<input type="url" placeholder="图片网址" value={this.state.img} onChange={this.on_change_img.bind(this)} /> ))}
} <option value="##other">输入图片网址</option>
{img_select==='##color' && <option value="##color">纯色背景</option>
<input type="color" value={this.state.color} onChange={this.on_change_color.bind(this)} /> </select>
} &nbsp;
</p> {img_select === '##other' && (
<div className="bg-preview" style={bgimg_style(this.state.img,this.state.color)} /> <input
</div> type="url"
); placeholder="图片网址"
} value={this.state.img}
onChange={this.on_change_img.bind(this)}
/>
)}
{img_select === '##color' && (
<input
type="color"
value={this.state.color}
onChange={this.on_change_color.bind(this)}
/>
)}
</p>
<div
className="bg-preview"
style={bgimg_style(this.state.img, this.state.color)}
/>
</div>
);
}
} }
class ConfigColorScheme extends PureComponent { class ConfigColorScheme extends PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.state={ this.state = {
color_scheme: window.config.color_scheme, color_scheme: window.config.color_scheme,
}; };
} }
save_changes() { save_changes() {
this.props.callback({ this.props.callback({
color_scheme: this.state.color_scheme, color_scheme: this.state.color_scheme,
}); });
} }
on_select(e) { on_select(e) {
let value=e.target.value; let value = e.target.value;
this.setState({ this.setState(
color_scheme: value, {
},this.save_changes.bind(this)); color_scheme: value,
} },
this.save_changes.bind(this),
);
}
render() { render() {
return ( return (
<div> <div>
<p> <p>
<b>夜间模式</b> <b>夜间模式</b>
<select value={this.state.color_scheme} onChange={this.on_select.bind(this)}> <select
<option value="default">跟随系统</option> value={this.state.color_scheme}
<option value="light">始终浅色模式</option> onChange={this.on_select.bind(this)}
<option value="dark">始终深色模式</option> >
</select> <option value="default">跟随系统</option>
&nbsp; <small>#color_scheme</small> <option value="light">始终浅色模式</option>
</p> <option value="dark">始终深色模式</option>
<p> </select>
选择浅色或深色模式深色模式下将会调暗图片亮度 &nbsp; <small>#color_scheme</small>
</p> </p>
</div> <p>选择浅色或深色模式深色模式下将会调暗图片亮度</p>
) </div>
} );
}
} }
class ConfigSwitch extends PureComponent { class ConfigSwitch extends PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.state={ this.state = {
switch: window.config[this.props.id], switch: window.config[this.props.id],
}; };
} }
on_change(e) { on_change(e) {
let val=e.target.checked; let val = e.target.checked;
this.setState({ this.setState(
switch: val, {
},()=>{ switch: val,
this.props.callback({ },
[this.props.id]: val, () => {
}); this.props.callback({
[this.props.id]: val,
}); });
} },
);
}
render() { render() {
return ( return (
<div> <div>
<p> <p>
<label> <label>
<input name={'config-'+this.props.id} type="checkbox" checked={this.state.switch} onChange={this.on_change.bind(this)} /> <input
<b>{this.props.name}</b> name={'config-' + this.props.id}
&nbsp; <small>#{this.props.id}</small> type="checkbox"
</label> checked={this.state.switch}
</p> onChange={this.on_change.bind(this)}
<p> />
{this.props.description} <b>{this.props.name}</b>
</p> &nbsp; <small>#{this.props.id}</small>
</div> </label>
); </p>
} <p>{this.props.description}</p>
</div>
);
}
} }
export class ConfigUI extends PureComponent { export class ConfigUI extends PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.save_changes_bound=this.save_changes.bind(this); this.save_changes_bound = this.save_changes.bind(this);
} }
save_changes(chg) { save_changes(chg) {
console.log(chg); console.log(chg);
Object.keys(chg).forEach((key)=>{ Object.keys(chg).forEach((key) => {
window.config[key]=chg[key]; window.config[key] = chg[key];
}); });
save_config(); save_config();
} }
reset_settings() { reset_settings() {
if(window.confirm('重置所有设置?')) { if (window.confirm('重置所有设置?')) {
window.config={}; window.config = {};
save_config(); save_config();
window.location.reload(); window.location.reload();
}
} }
}
render() { render() {
return ( return (
<div> <div>
<div className="box config-ui-header"> <div className="box config-ui-header">
<p>这些功能仍在测试可能不稳定<a onClick={this.reset_settings.bind(this)}>全部重置</a></p> <p>
<p><b>修改设置后 <a onClick={()=>{window.location.reload()}}>刷新页面</a> </b></p> 这些功能仍在测试可能不稳定
</div> <a onClick={this.reset_settings.bind(this)}>全部重置</a>
<div className="box"> </p>
<ConfigBackground callback={this.save_changes_bound} /> <p>
<hr /> <b>
<ConfigColorScheme callback={this.save_changes_bound} /> 修改设置后{' '}
<hr /> <a
<ConfigSwitch callback={this.save_changes_bound} id="pressure" name="快速返回" onClick={() => {
description="短暂按住 Esc 键或重压屏幕(3D Touch)可以快速返回或者刷新树洞" window.location.reload();
/> }}
<hr /> >
<ConfigSwitch callback={this.save_changes_bound} id="easter_egg" name="允许彩蛋" 刷新页面
description="在某些情况下显示彩蛋" </a>{' '}
/> 方可生效
<hr /> </b>
<ConfigSwitch callback={this.save_changes_bound} id="fold" name="折叠树洞" </p>
description="在时间线中折叠可能引起不适的树洞" </div>
/> <div className="box">
<hr /> <ConfigBackground callback={this.save_changes_bound} />
<p> <hr />
新功能建议或问题反馈请在&nbsp; <ConfigColorScheme callback={this.save_changes_bound} />
<a href="https://github.com/thuhole/thuhole-go-backend/issues" target="_blank">GitHub <span className="icon icon-github" /></a> <hr />
&nbsp;提出 <ConfigSwitch
</p> callback={this.save_changes_bound}
</div> id="pressure"
</div> name="快速返回"
) description="短暂按住 Esc 键或重压屏幕(3D Touch)可以快速返回或者刷新树洞"
} />
} <hr />
<ConfigSwitch
callback={this.save_changes_bound}
id="easter_egg"
name="允许彩蛋"
description="在某些情况下显示彩蛋"
/>
<hr />
<ConfigSwitch
callback={this.save_changes_bound}
id="fold"
name="折叠树洞"
description="在时间线中折叠可能引起不适的树洞"
/>
<hr />
<p>
新功能建议或问题反馈请在&nbsp;
<a
href="https://github.com/thuhole/thuhole-go-backend/issues"
target="_blank"
>
GitHub <span className="icon icon-github" />
</a>
&nbsp;提出
</p>
</div>
</div>
);
}
}

1809
src/Flows.js

File diff suppressed because it is too large Load Diff

36
src/Markdown.js

@ -1,29 +1,33 @@
import MarkdownIt from 'markdown-it' import MarkdownIt from 'markdown-it';
import MarkdownItKaTeX from 'markdown-it-katex' import MarkdownItKaTeX from 'markdown-it-katex';
import hljs from 'highlight.js' import hljs from 'highlight.js';
import 'highlight.js/styles/atom-one-dark.css' import 'highlight.js/styles/atom-one-dark.css';
import './Markdown.css' import './Markdown.css';
import 'katex/dist/katex.min.css' import 'katex/dist/katex.min.css';
let md = new MarkdownIt({ let md = new MarkdownIt({
html: false, html: false,
linkify: false, linkify: false,
breaks: true, breaks: true,
inline: true, inline: true,
highlight (str, lang) { highlight(str, lang) {
if (lang && hljs.getLanguage(lang)) { if (lang && hljs.getLanguage(lang)) {
try { try {
return '<pre class="hljs"><code>' + return (
hljs.highlight(lang, str, true).value + '<pre class="hljs"><code>' +
'</code></pre>'; hljs.highlight(lang, str, true).value +
'</code></pre>'
);
} catch (__) {} } catch (__) {}
} }
return '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>'; return (
} '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>'
);
},
}).use(MarkdownItKaTeX, { }).use(MarkdownItKaTeX, {
"throwOnError" : false, throwOnError: false,
"errorColor" : "#aa0000" errorColor: '#aa0000',
}) });
export default (text) => md.render(text) export default (text) => md.render(text);

135
src/Message.js

@ -1,65 +1,80 @@
import React, {Component, PureComponent} from 'react'; import React, { Component, PureComponent } from 'react';
import {THUHOLE_API_ROOT, get_json, API_VERSION_PARAM} from './flows_api'; import { THUHOLE_API_ROOT, get_json, API_VERSION_PARAM } from './flows_api';
import {Time} from './Common'; import { Time } from './Common';
export class MessageViewer extends PureComponent { export class MessageViewer extends PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.state={ this.state = {
loading_status: 'idle', loading_status: 'idle',
msg: [], msg: [],
}; };
} }
componentDidMount() { componentDidMount() {
this.load(); this.load();
} }
load() { load() {
if(this.state.loading_status==='loading') return; if (this.state.loading_status === 'loading') return;
this.setState({ this.setState(
loading_status: 'loading', {
},()=>{ loading_status: 'loading',
fetch(THUHOLE_API_ROOT+'api_xmcp/hole/system_msg?user_token='+encodeURIComponent(this.props.token)+API_VERSION_PARAM()) },
.then(get_json) () => {
.then((json)=>{ fetch(
if(json.error) THUHOLE_API_ROOT +
throw new Error(json.error); 'api_xmcp/hole/system_msg?user_token=' +
else encodeURIComponent(this.props.token) +
this.setState({ API_VERSION_PARAM(),
loading_status: 'done', )
msg: json.result, .then(get_json)
}); .then((json) => {
}) if (json.error) throw new Error(json.error);
.catch((err)=>{ else
console.error(err); this.setState({
alert(''+err); loading_status: 'done',
this.setState({ msg: json.result,
loading_status: 'failed', });
}); })
}) .catch((err) => {
console.error(err);
alert('' + err);
this.setState({
loading_status: 'failed',
});
});
},
);
}
}); render() {
} if (this.state.loading_status === 'loading')
return <p className="box box-tip">加载中</p>;
render() { else if (this.state.loading_status === 'failed')
if(this.state.loading_status==='loading') return (
return (<p className="box box-tip">加载中</p>); <div className="box box-tip">
else if(this.state.loading_status==='failed') <a
return (<div className="box box-tip"><a onClick={()=>{this.load()}}>重新加载</a></div>); onClick={() => {
else if(this.state.loading_status==='done') this.load();
return this.state.msg.map((msg)=>( }}
<div className="box"> >
<div className="box-header"> 重新加载
<Time stamp={msg.timestamp} short={false} /> </a>
&nbsp; <b>{msg.title}</b> </div>
</div> );
<div className="box-content"> else if (this.state.loading_status === 'done')
<pre>{msg.content}</pre> return this.state.msg.map((msg) => (
</div> <div className="box">
</div> <div className="box-header">
)); <Time stamp={msg.timestamp} short={false} />
else &nbsp; <b>{msg.title}</b>
return null; </div>
} <div className="box-content">
} <pre>{msg.content}</pre>
</div>
</div>
));
else return null;
}
}

207
src/PressureHelper.js

@ -1,113 +1,120 @@
import React, {Component} from 'react'; import React, { Component } from 'react';
import Pressure from 'pressure'; import Pressure from 'pressure';
import './PressureHelper.css'; import './PressureHelper.css';
const THRESHOLD=.4; const THRESHOLD = 0.4;
const MULTIPLIER=25; const MULTIPLIER = 25;
const BORDER_WIDTH=500; // also change css! const BORDER_WIDTH = 500; // also change css!
export class PressureHelper extends Component { export class PressureHelper extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state={ this.state = {
level: 0, level: 0,
fired: false, fired: false,
}; };
this.callback=props.callback; this.callback = props.callback;
this.esc_interval=null; this.esc_interval = null;
} }
do_fire() { do_fire() {
if(this.esc_interval) { if (this.esc_interval) {
clearInterval(this.esc_interval); clearInterval(this.esc_interval);
this.esc_interval=null; this.esc_interval = null;
}
this.setState({
level: 1,
fired: true,
});
this.callback();
window.setTimeout(()=>{
this.setState({
level: 0,
fired: false,
});
},300);
} }
this.setState({
level: 1,
fired: true,
});
this.callback();
window.setTimeout(() => {
this.setState({
level: 0,
fired: false,
});
}, 300);
}
componentDidMount() { componentDidMount() {
if(window.config.pressure) { if (window.config.pressure) {
Pressure.set(document.body, { Pressure.set(
change: (force)=>{ document.body,
if(!this.state.fired) { {
if(force>=.999) { change: (force) => {
this.do_fire(); if (!this.state.fired) {
} if (force >= 0.999) {
else this.do_fire();
this.setState({ } else
level: force, this.setState({
}); level: force,
} });
}, }
end: ()=>{ },
this.setState({ end: () => {
level: 0, this.setState({
fired: false, level: 0,
}); fired: false,
},
}, {
polyfill: false,
only: 'touch',
preventSelect: false,
}); });
},
},
{
polyfill: false,
only: 'touch',
preventSelect: false,
},
);
document.addEventListener('keydown',(e)=>{ document.addEventListener('keydown', (e) => {
if(!e.repeat && e.key==='Escape') { if (!e.repeat && e.key === 'Escape') {
if(this.esc_interval) if (this.esc_interval) clearInterval(this.esc_interval);
clearInterval(this.esc_interval); this.setState(
this.setState({ {
level: THRESHOLD/2, level: THRESHOLD / 2,
},()=>{ },
this.esc_interval=setInterval(()=>{ () => {
let new_level=this.state.level+.1; this.esc_interval = setInterval(() => {
if(new_level>=.999) let new_level = this.state.level + 0.1;
this.do_fire(); if (new_level >= 0.999) this.do_fire();
else else
this.setState({ this.setState({
level: new_level, level: new_level,
}); });
},30); }, 30);
}); },
} );
});
document.addEventListener('keyup',(e)=>{
if(e.key==='Escape') {
if(this.esc_interval) {
clearInterval(this.esc_interval);
this.esc_interval=null;
}
this.setState({
level: 0,
});
}
});
} }
});
document.addEventListener('keyup', (e) => {
if (e.key === 'Escape') {
if (this.esc_interval) {
clearInterval(this.esc_interval);
this.esc_interval = null;
}
this.setState({
level: 0,
});
}
});
} }
}
render() { render() {
const pad=MULTIPLIER*(this.state.level-THRESHOLD)-BORDER_WIDTH; const pad = MULTIPLIER * (this.state.level - THRESHOLD) - BORDER_WIDTH;
return ( return (
<div className={ <div
'pressure-box' className={
+(this.state.fired ? ' pressure-box-fired' : '') 'pressure-box' +
+(this.state.level<=.0001 ? ' pressure-box-empty' : '') (this.state.fired ? ' pressure-box-fired' : '') +
} style={{ (this.state.level <= 0.0001 ? ' pressure-box-empty' : '')
left: pad, }
right: pad, style={{
top: pad, left: pad,
bottom: pad, right: pad,
}} /> top: pad,
) bottom: pad,
} }}
} />
);
}
}

97
src/Sidebar.js

@ -1,45 +1,66 @@
import React, {Component, PureComponent} from 'react'; import React, { Component, PureComponent } from 'react';
import './Sidebar.css'; import './Sidebar.css';
export class Sidebar extends PureComponent { export class Sidebar extends PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.sidebar_ref=React.createRef(); this.sidebar_ref = React.createRef();
this.do_close_bound=this.do_close.bind(this); this.do_close_bound = this.do_close.bind(this);
this.do_back_bound=this.do_back.bind(this); this.do_back_bound = this.do_back.bind(this);
} }
componentDidUpdate(nextProps) { componentDidUpdate(nextProps) {
if(this.props.stack!==nextProps.stack) { if (this.props.stack !== nextProps.stack) {
//console.log('sidebar top'); //console.log('sidebar top');
if(this.sidebar_ref.current) if (this.sidebar_ref.current) this.sidebar_ref.current.scrollTop = 0;
this.sidebar_ref.current.scrollTop=0;
}
} }
}
do_close() { do_close() {
this.props.show_sidebar(null,null,'clear'); this.props.show_sidebar(null, null, 'clear');
} }
do_back() { do_back() {
this.props.show_sidebar(null,null,'pop'); this.props.show_sidebar(null, null, 'pop');
} }
render() { render() {
let [cur_title,cur_content]=this.props.stack[this.props.stack.length-1]; let [cur_title, cur_content] = this.props.stack[
return ( this.props.stack.length - 1
<div className={'sidebar-container '+(cur_title!==null ? 'sidebar-on' : 'sidebar-off')}> ];
<div className="sidebar-shadow" onClick={this.do_back_bound} onTouchEnd={(e)=>{e.preventDefault();e.target.click();}} /> return (
<div ref={this.sidebar_ref} className="sidebar"> <div
{cur_content} className={
</div> 'sidebar-container ' +
<div className="sidebar-title"> (cur_title !== null ? 'sidebar-on' : 'sidebar-off')
<a className="no-underline" onClick={this.do_close_bound}>&nbsp;<span className="icon icon-close" />&nbsp;</a> }
{this.props.stack.length>2 && >
<a className="no-underline" onClick={this.do_back_bound}>&nbsp;<span className="icon icon-back" />&nbsp;</a> <div
} className="sidebar-shadow"
{cur_title} onClick={this.do_back_bound}
</div> onTouchEnd={(e) => {
</div> e.preventDefault();
); e.target.click();
} }}
} />
<div ref={this.sidebar_ref} className="sidebar">
{cur_content}
</div>
<div className="sidebar-title">
<a className="no-underline" onClick={this.do_close_bound}>
&nbsp;
<span className="icon icon-close" />
&nbsp;
</a>
{this.props.stack.length > 2 && (
<a className="no-underline" onClick={this.do_back_bound}>
&nbsp;
<span className="icon icon-back" />
&nbsp;
</a>
)}
{cur_title}
</div>
</div>
);
}
}

289
src/Title.js

@ -1,143 +1,186 @@
import React, {Component, PureComponent} from 'react'; import React, { Component, PureComponent } from 'react';
// import {AppSwitcher} from './infrastructure/widgets'; // import {AppSwitcher} from './infrastructure/widgets';
import {InfoSidebar, PostForm} from './UserAction'; import { InfoSidebar, PostForm } from './UserAction';
import {TokenCtx} from './UserAction'; import { TokenCtx } from './UserAction';
import './Title.css'; import './Title.css';
const flag_re=/^\/\/setflag ([a-zA-Z0-9_]+)=(.*)$/; const flag_re = /^\/\/setflag ([a-zA-Z0-9_]+)=(.*)$/;
class ControlBar extends PureComponent { class ControlBar extends PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.state={ this.state = {
search_text: '', search_text: '',
}; };
this.set_mode=props.set_mode; this.set_mode = props.set_mode;
this.on_change_bound=this.on_change.bind(this); this.on_change_bound = this.on_change.bind(this);
this.on_keypress_bound=this.on_keypress.bind(this); this.on_keypress_bound = this.on_keypress.bind(this);
this.do_refresh_bound=this.do_refresh.bind(this); this.do_refresh_bound = this.do_refresh.bind(this);
this.do_attention_bound=this.do_attention.bind(this); this.do_attention_bound = this.do_attention.bind(this);
} }
componentDidMount() {
if(window.location.hash) {
let text=decodeURIComponent(window.location.hash).substr(1);
if(text.lastIndexOf('?')!==-1)
text=text.substr(0,text.lastIndexOf('?')); // fuck wechat '#param?nsukey=...'
this.setState({
search_text: text,
}, ()=>{
this.on_keypress({key: 'Enter'});
});
}
}
on_change(event) { componentDidMount() {
this.setState({ if (window.location.hash) {
search_text: event.target.value, let text = decodeURIComponent(window.location.hash).substr(1);
}); if (text.lastIndexOf('?') !== -1)
text = text.substr(0, text.lastIndexOf('?')); // fuck wechat '#param?nsukey=...'
this.setState(
{
search_text: text,
},
() => {
this.on_keypress({ key: 'Enter' });
},
);
} }
}
on_keypress(event) { on_change(event) {
if(event.key==='Enter') { this.setState({
let flag_res=flag_re.exec(this.state.search_text); search_text: event.target.value,
if(flag_res) { });
if(flag_res[2]) { }
localStorage[flag_res[1]]=flag_res[2];
alert('Set Flag '+flag_res[1]+'='+flag_res[2]+'\nYou may need to refresh this webpage.');
} else {
delete localStorage[flag_res[1]];
alert('Clear Flag '+flag_res[1]+'\nYou may need to refresh this webpage.');
}
return;
}
const mode=this.state.search_text.startsWith('#') ? 'single' : 'search'; on_keypress(event) {
this.set_mode(mode,this.state.search_text||''); if (event.key === 'Enter') {
let flag_res = flag_re.exec(this.state.search_text);
if (flag_res) {
if (flag_res[2]) {
localStorage[flag_res[1]] = flag_res[2];
alert(
'Set Flag ' +
flag_res[1] +
'=' +
flag_res[2] +
'\nYou may need to refresh this webpage.',
);
} else {
delete localStorage[flag_res[1]];
alert(
'Clear Flag ' +
flag_res[1] +
'\nYou may need to refresh this webpage.',
);
} }
} return;
}
do_refresh() { const mode = this.state.search_text.startsWith('#') ? 'single' : 'search';
window.scrollTo(0,0); this.set_mode(mode, this.state.search_text || '');
this.setState({
search_text: '',
});
this.set_mode('list',null);
} }
}
do_attention() { do_refresh() {
window.scrollTo(0,0); window.scrollTo(0, 0);
this.setState({ this.setState({
search_text: '', search_text: '',
}); });
this.set_mode('attention',null); this.set_mode('list', null);
} }
render() { do_attention() {
return ( window.scrollTo(0, 0);
<TokenCtx.Consumer>{({value: token})=>( this.setState({
<div className="control-bar"> search_text: '',
<a className="no-underline control-btn" onClick={this.do_refresh_bound}> });
<span className="icon icon-refresh" /> this.set_mode('attention', null);
<span className="control-btn-label">最新</span> }
</a>
{!!token && render() {
<a className="no-underline control-btn" onClick={this.do_attention_bound}> return (
<span className="icon icon-attention" /> <TokenCtx.Consumer>
<span className="control-btn-label">关注</span> {({ value: token }) => (
</a> <div className="control-bar">
} <a
<input className="control-search" value={this.state.search_text} placeholder="搜索 或 #树洞号" className="no-underline control-btn"
onChange={this.on_change_bound} onKeyPress={this.on_keypress_bound} onClick={this.do_refresh_bound}
/> >
<a className="no-underline control-btn" onClick={()=>{ <span className="icon icon-refresh" />
this.props.show_sidebar( <span className="control-btn-label">最新</span>
'T大树洞', </a>
<InfoSidebar show_sidebar={this.props.show_sidebar} /> {!!token && (
) <a
}}> className="no-underline control-btn"
<span className={'icon icon-'+(token ? 'about' : 'login')} /> onClick={this.do_attention_bound}
<span className="control-btn-label">{token ? '账户' : '登录'}</span> >
</a> <span className="icon icon-attention" />
{!!token && <span className="control-btn-label">关注</span>
<a className="no-underline control-btn" onClick={()=>{ </a>
this.props.show_sidebar( )}
'发表树洞', <input
<PostForm token={token} on_complete={()=>{ className="control-search"
this.props.show_sidebar(null,null); value={this.state.search_text}
this.do_refresh(); placeholder="搜索 或 #树洞号"
}} /> onChange={this.on_change_bound}
) onKeyPress={this.on_keypress_bound}
}}> />
<span className="icon icon-plus" /> <a
<span className="control-btn-label">发表</span> className="no-underline control-btn"
</a> onClick={() => {
} this.props.show_sidebar(
</div> 'T大树洞',
)}</TokenCtx.Consumer> <InfoSidebar show_sidebar={this.props.show_sidebar} />,
) );
} }}
>
<span className={'icon icon-' + (token ? 'about' : 'login')} />
<span className="control-btn-label">
{token ? '账户' : '登录'}
</span>
</a>
{!!token && (
<a
className="no-underline control-btn"
onClick={() => {
this.props.show_sidebar(
'发表树洞',
<PostForm
token={token}
on_complete={() => {
this.props.show_sidebar(null, null);
this.do_refresh();
}}
/>,
);
}}
>
<span className="icon icon-plus" />
<span className="control-btn-label">发表</span>
</a>
)}
</div>
)}
</TokenCtx.Consumer>
);
}
} }
export function Title(props) { export function Title(props) {
return ( return (
<div className="title-bar"> <div className="title-bar">
{/* <AppSwitcher appid="hole" /> */} {/* <AppSwitcher appid="hole" /> */}
<div className="aux-margin"> <div className="aux-margin">
<div className="title"> <div className="title">
<p className="centered-line"> <p className="centered-line">
<span onClick={()=>props.show_sidebar( <span
'T大树洞', onClick={() =>
<InfoSidebar show_sidebar={props.show_sidebar} /> props.show_sidebar(
)}> 'T大树洞',
T大树洞 <InfoSidebar show_sidebar={props.show_sidebar} />,
</span> )
</p> }
</div> >
<ControlBar show_sidebar={props.show_sidebar} set_mode={props.set_mode} /> T大树洞
</div> </span>
</p>
</div> </div>
) <ControlBar
} show_sidebar={props.show_sidebar}
set_mode={props.set_mode}
/>
</div>
</div>
);
}

1229
src/UserAction.js

File diff suppressed because it is too large Load Diff

311
src/cache.js

@ -1,172 +1,173 @@
const HOLE_CACHE_DB_NAME='hole_cache_db'; const HOLE_CACHE_DB_NAME = 'hole_cache_db';
const CACHE_DB_VER=1; const CACHE_DB_VER = 1;
const MAINTENANCE_STEP=150; const MAINTENANCE_STEP = 150;
const MAINTENANCE_COUNT=1000; const MAINTENANCE_COUNT = 1000;
const ENC_KEY=42; const ENC_KEY = 42;
class Cache { class Cache {
constructor() { constructor() {
this.db=null; this.db = null;
this.added_items_since_maintenance=0; this.added_items_since_maintenance = 0;
this.encrypt=this.encrypt.bind(this); this.encrypt = this.encrypt.bind(this);
this.decrypt=this.decrypt.bind(this); this.decrypt = this.decrypt.bind(this);
const open_req=indexedDB.open(HOLE_CACHE_DB_NAME,CACHE_DB_VER); const open_req = indexedDB.open(HOLE_CACHE_DB_NAME, CACHE_DB_VER);
open_req.onerror=console.error.bind(console); open_req.onerror = console.error.bind(console);
open_req.onupgradeneeded=(event)=>{ open_req.onupgradeneeded = (event) => {
console.log('comment cache db upgrade'); console.log('comment cache db upgrade');
const db=event.target.result; const db = event.target.result;
const store=db.createObjectStore('comment',{ const store = db.createObjectStore('comment', {
keyPath: 'pid', keyPath: 'pid',
}); });
store.createIndex('last_access','last_access',{unique: false}); store.createIndex('last_access', 'last_access', { unique: false });
}; };
open_req.onsuccess=(event)=>{ open_req.onsuccess = (event) => {
console.log('comment cache db loaded'); console.log('comment cache db loaded');
this.db=event.target.result; this.db = event.target.result;
setTimeout(this.maintenance.bind(this),1); setTimeout(this.maintenance.bind(this), 1);
}; };
} }
// use window.hole_cache.encrypt() only after cache is loaded! // use window.hole_cache.encrypt() only after cache is loaded!
encrypt(pid,data) { encrypt(pid, data) {
let s=JSON.stringify(data); let s = JSON.stringify(data);
let o=''; let o = '';
for(let i=0,key=(ENC_KEY^pid)%128;i<s.length;i++) { for (let i = 0, key = (ENC_KEY ^ pid) % 128; i < s.length; i++) {
let c=s.charCodeAt(i); let c = s.charCodeAt(i);
let new_key=(key^(c/2))%128; let new_key = (key ^ (c / 2)) % 128;
o+=String.fromCharCode(key^s.charCodeAt(i)); o += String.fromCharCode(key ^ s.charCodeAt(i));
key=new_key; key = new_key;
}
return o;
} }
return o;
}
// use window.hole_cache.decrypt() only after cache is loaded! // use window.hole_cache.decrypt() only after cache is loaded!
decrypt(pid,s) { decrypt(pid, s) {
let o=''; let o = '';
if(typeof(s)!==typeof('str')) if (typeof s !== typeof 'str') return null;
return null;
for(let i=0,key=(ENC_KEY^pid)%128;i<s.length;i++) { for (let i = 0, key = (ENC_KEY ^ pid) % 128; i < s.length; i++) {
let c=key^s.charCodeAt(i); let c = key ^ s.charCodeAt(i);
o+=String.fromCharCode(c); o += String.fromCharCode(c);
key=(key^(c/2))%128; key = (key ^ (c / 2)) % 128;
}
try {
return JSON.parse(o);
} catch(e) {
console.error('decrypt failed');
console.trace(e);
return null;
}
} }
get(pid,target_version) { try {
pid=parseInt(pid); return JSON.parse(o);
return new Promise((resolve,reject)=>{ } catch (e) {
if(!this.db) console.error('decrypt failed');
return resolve(null); console.trace(e);
const tx=this.db.transaction(['comment'],'readwrite'); return null;
const store=tx.objectStore('comment');
const get_req=store.get(pid);
get_req.onsuccess=()=>{
let res=get_req.result;
if(!res || !res.data_str) {
//console.log('comment cache miss '+pid);
resolve(null);
} else if(target_version===res.version) { // hit
console.log('comment cache hit',pid);
res.last_access=(+new Date());
store.put(res);
let data=this.decrypt(pid,res.data_str);
resolve(data); // obj or null
} else { // expired
console.log('comment cache expired',pid,': ver',res.version,'target',target_version);
store.delete(pid);
resolve(null);
}
};
get_req.onerror=(e)=>{
console.warn('comment cache indexeddb open failed');
console.error(e);
resolve(null);
};
});
} }
}
put(pid,target_version,data) { get(pid, target_version) {
pid=parseInt(pid); pid = parseInt(pid);
return new Promise((resolve,reject)=>{ return new Promise((resolve, reject) => {
if(!this.db) if (!this.db) return resolve(null);
return resolve(); const tx = this.db.transaction(['comment'], 'readwrite');
const tx=this.db.transaction(['comment'],'readwrite'); const store = tx.objectStore('comment');
const store=tx.objectStore('comment'); const get_req = store.get(pid);
store.put({ get_req.onsuccess = () => {
pid: pid, let res = get_req.result;
version: target_version, if (!res || !res.data_str) {
data_str: this.encrypt(pid,data), //console.log('comment cache miss '+pid);
last_access: +new Date(), resolve(null);
}); } else if (target_version === res.version) {
if(++this.added_items_since_maintenance===MAINTENANCE_STEP) // hit
setTimeout(this.maintenance.bind(this),1); console.log('comment cache hit', pid);
}); res.last_access = +new Date();
} store.put(res);
let data = this.decrypt(pid, res.data_str);
resolve(data); // obj or null
} else {
// expired
console.log(
'comment cache expired',
pid,
': ver',
res.version,
'target',
target_version,
);
store.delete(pid);
resolve(null);
}
};
get_req.onerror = (e) => {
console.warn('comment cache indexeddb open failed');
console.error(e);
resolve(null);
};
});
}
delete(pid) { put(pid, target_version, data) {
pid=parseInt(pid); pid = parseInt(pid);
return new Promise((resolve,reject)=>{ return new Promise((resolve, reject) => {
if(!this.db) if (!this.db) return resolve();
return resolve(); const tx = this.db.transaction(['comment'], 'readwrite');
const tx=this.db.transaction(['comment'],'readwrite'); const store = tx.objectStore('comment');
const store=tx.objectStore('comment'); store.put({
let req=store.delete(pid); pid: pid,
//console.log('comment cache delete',pid); version: target_version,
req.onerror=()=>{ data_str: this.encrypt(pid, data),
console.warn('comment cache delete failed ',pid); last_access: +new Date(),
return resolve(); });
}; if (++this.added_items_since_maintenance === MAINTENANCE_STEP)
req.onsuccess=()=>resolve(); setTimeout(this.maintenance.bind(this), 1);
}); });
} }
maintenance() { delete(pid) {
if(!this.db) pid = parseInt(pid);
return; return new Promise((resolve, reject) => {
const tx=this.db.transaction(['comment'],'readwrite'); if (!this.db) return resolve();
const store=tx.objectStore('comment'); const tx = this.db.transaction(['comment'], 'readwrite');
let count_req=store.count(); const store = tx.objectStore('comment');
count_req.onsuccess=()=>{ let req = store.delete(pid);
let count=count_req.result; //console.log('comment cache delete',pid);
if(count>MAINTENANCE_COUNT) { req.onerror = () => {
console.log('comment cache db maintenance',count); console.warn('comment cache delete failed ', pid);
store.index('last_access').openKeyCursor().onsuccess=(e)=>{ return resolve();
let cur=e.target.result; };
if(cur) { req.onsuccess = () => resolve();
//console.log('maintenance: delete',cur); });
store.delete(cur.primaryKey); }
if(--count>MAINTENANCE_COUNT)
cur.continue(); maintenance() {
} if (!this.db) return;
}; const tx = this.db.transaction(['comment'], 'readwrite');
} else { const store = tx.objectStore('comment');
console.log('comment cache db no need to maintenance',count); let count_req = store.count();
} count_req.onsuccess = () => {
this.added_items_since_maintenance=0; let count = count_req.result;
if (count > MAINTENANCE_COUNT) {
console.log('comment cache db maintenance', count);
store.index('last_access').openKeyCursor().onsuccess = (e) => {
let cur = e.target.result;
if (cur) {
//console.log('maintenance: delete',cur);
store.delete(cur.primaryKey);
if (--count > MAINTENANCE_COUNT) cur.continue();
}
}; };
count_req.onerror=console.error.bind(console); } else {
} console.log('comment cache db no need to maintenance', count);
}
this.added_items_since_maintenance = 0;
};
count_req.onerror = console.error.bind(console);
}
clear() { clear() {
if(!this.db) if (!this.db) return;
return; indexedDB.deleteDatabase(HOLE_CACHE_DB_NAME);
indexedDB.deleteDatabase(HOLE_CACHE_DB_NAME); console.log('delete comment cache db');
console.log('delete comment cache db'); }
} }
};
export function cache() { export function cache() {
if(!window.hole_cache) if (!window.hole_cache) window.hole_cache = new Cache();
window.hole_cache=new Cache(); return window.hole_cache;
return window.hole_cache; }
}

37
src/color_picker.js

@ -1,26 +1,25 @@
// https://martin.ankerl.com/2009/12/09/how-to-create-random-colors-programmatically/ // https://martin.ankerl.com/2009/12/09/how-to-create-random-colors-programmatically/
const golden_ratio_conjugate=0.618033988749895; const golden_ratio_conjugate = 0.618033988749895;
export class ColorPicker { export class ColorPicker {
constructor() { constructor() {
this.names={}; this.names = {};
this.current_h=Math.random(); this.current_h = Math.random();
} }
get(name) { get(name) {
name=name.toLowerCase(); name = name.toLowerCase();
if(name==='洞主') if (name === '洞主') return ['hsl(0,0%,97%)', 'hsl(0,0%,16%)'];
return ['hsl(0,0%,97%)','hsl(0,0%,16%)'];
if(!this.names[name]) { if (!this.names[name]) {
this.current_h+=golden_ratio_conjugate; this.current_h += golden_ratio_conjugate;
this.current_h%=1; this.current_h %= 1;
this.names[name]=[ this.names[name] = [
`hsl(${this.current_h*360}, 50%, 90%)`, `hsl(${this.current_h * 360}, 50%, 90%)`,
`hsl(${this.current_h*360}, 60%, 20%)`, `hsl(${this.current_h * 360}, 60%, 20%)`,
]; ];
}
return this.names[name];
} }
} return this.names[name];
}
}

353
src/flows_api.js

@ -1,183 +1,182 @@
import {get_json, API_VERSION_PARAM} from './infrastructure/functions'; import { get_json, API_VERSION_PARAM } from './infrastructure/functions';
import {THUHOLE_API_ROOT} from './infrastructure/const'; import { THUHOLE_API_ROOT } from './infrastructure/const';
import {API_BASE} from './Common'; import { API_BASE } from './Common';
import {cache} from './cache'; import { cache } from './cache';
export {THUHOLE_API_ROOT, API_VERSION_PARAM}; export { THUHOLE_API_ROOT, API_VERSION_PARAM };
export function token_param(token) { export function token_param(token) {
return API_VERSION_PARAM()+(token ? ('&user_token='+token) : ''); return API_VERSION_PARAM() + (token ? '&user_token=' + token : '');
} }
export {get_json}; export { get_json };
const SEARCH_PAGESIZE=50; const SEARCH_PAGESIZE = 50;
export const API={ export const API = {
load_replies: (pid,token,color_picker,cache_version)=>{ load_replies: (pid, token, color_picker, cache_version) => {
pid=parseInt(pid); pid = parseInt(pid);
return fetch( return fetch(
API_BASE+'/api.php?action=getcomment'+ API_BASE +
'&pid='+pid+ '/api.php?action=getcomment' +
token_param(token) '&pid=' +
) pid +
.then(get_json) token_param(token),
.then((json)=>{ )
if(json.code!==0) { .then(get_json)
if(json.msg) throw new Error(json.msg); .then((json) => {
else throw new Error(JSON.stringify(json)); if (json.code !== 0) {
} if (json.msg) throw new Error(json.msg);
else throw new Error(JSON.stringify(json));
cache().delete(pid).then(()=>{ }
cache().put(pid,cache_version,json);
}); cache()
.delete(pid)
// also change load_replies_with_cache! .then(() => {
json.data=json.data cache().put(pid, cache_version, json);
.sort((a,b)=>{ });
return parseInt(a.cid,10)-parseInt(b.cid,10);
}) // also change load_replies_with_cache!
.map((info)=>{ json.data = json.data
info._display_color=color_picker.get(info.name); .sort((a, b) => {
info.variant={}; return parseInt(a.cid, 10) - parseInt(b.cid, 10);
return info; })
}); .map((info) => {
info._display_color = color_picker.get(info.name);
return json; info.variant = {};
return info;
});
return json;
});
},
load_replies_with_cache: (pid, token, color_picker, cache_version) => {
pid = parseInt(pid);
return cache()
.get(pid, cache_version)
.then((json) => {
if (json) {
// also change load_replies!
json.data = json.data
.sort((a, b) => {
return parseInt(a.cid, 10) - parseInt(b.cid, 10);
})
.map((info) => {
info._display_color = color_picker.get(info.name);
info.variant = {};
return info;
}); });
},
return json;
load_replies_with_cache: (pid,token,color_picker,cache_version)=> { } else return API.load_replies(pid, token, color_picker, cache_version);
pid=parseInt(pid); });
return cache().get(pid,cache_version) },
.then((json)=>{
if(json) { set_attention: (pid, attention, token) => {
// also change load_replies! let data = new URLSearchParams();
json.data=json.data data.append('user_token', token);
.sort((a,b)=>{ data.append('pid', pid);
return parseInt(a.cid,10)-parseInt(b.cid,10); data.append('switch', attention ? '1' : '0');
}) return fetch(API_BASE + '/api.php?action=attention' + token_param(token), {
.map((info)=>{ method: 'POST',
info._display_color=color_picker.get(info.name); headers: {
info.variant={}; 'Content-Type': 'application/x-www-form-urlencoded',
return info; },
}); body: data,
})
return json; .then(get_json)
} .then((json) => {
else cache().delete(pid);
return API.load_replies(pid,token,color_picker,cache_version); if (json.code !== 0) {
}); if (json.msg && json.msg === '已经关注过了') {
}, } else {
if (json.msg) alert(json.msg);
set_attention: (pid,attention,token)=>{ throw new Error(JSON.stringify(json));
let data=new URLSearchParams(); }
data.append('user_token',token); }
data.append('pid',pid); return json;
data.append('switch',attention ? '1' : '0'); });
return fetch(API_BASE+'/api.php?action=attention'+token_param(token), { },
method: 'POST',
headers: { report: (pid, reason, token) => {
'Content-Type': 'application/x-www-form-urlencoded', let data = new URLSearchParams();
}, data.append('user_token', token);
body: data, data.append('pid', pid);
}) data.append('reason', reason);
.then(get_json) return fetch(API_BASE + '/api.php?action=report' + token_param(token), {
.then((json)=>{ method: 'POST',
cache().delete(pid); headers: {
if(json.code!==0) { 'Content-Type': 'application/x-www-form-urlencoded',
if(json.msg && json.msg==='已经关注过了') {} },
else { body: data,
if(json.msg) alert(json.msg); })
throw new Error(JSON.stringify(json)); .then(get_json)
} .then((json) => {
} if (json.code !== 0) {
return json; if (json.msg) alert(json.msg);
}); throw new Error(JSON.stringify(json));
}, }
return json;
report: (pid,reason,token)=>{ });
let data=new URLSearchParams(); },
data.append('user_token',token);
data.append('pid',pid); get_list: (page, token) => {
data.append('reason',reason); return fetch(
return fetch(API_BASE+'/api.php?action=report'+token_param(token), { API_BASE + '/api.php?action=getlist' + '&p=' + page + token_param(token),
method: 'POST', )
headers: { .then(get_json)
'Content-Type': 'application/x-www-form-urlencoded', .then((json) => {
}, if (json.code !== 0) throw new Error(JSON.stringify(json));
body: data, return json;
}) });
.then(get_json) },
.then((json)=>{
if(json.code!==0) { get_search: (page, keyword, token) => {
if(json.msg) alert(json.msg); return fetch(
throw new Error(JSON.stringify(json)); API_BASE +
} '/api.php?action=search' +
return json; '&pagesize=' +
}); SEARCH_PAGESIZE +
}, '&page=' +
page +
get_list: (page,token)=>{ '&keywords=' +
return fetch( encodeURIComponent(keyword) +
API_BASE+'/api.php?action=getlist'+ token_param(token),
'&p='+page+ )
token_param(token) .then(get_json)
) .then((json) => {
.then(get_json) if (json.code !== 0) {
.then((json)=>{ if (json.msg) throw new Error(json.msg);
if(json.code!==0) throw new Error(JSON.stringify(json));
throw new Error(JSON.stringify(json)); }
return json; return json;
}); });
}, },
get_search: (page,keyword,token)=>{ get_single: (pid, token) => {
return fetch( return fetch(
API_BASE+'/api.php?action=search'+ API_BASE + '/api.php?action=getone' + '&pid=' + pid + token_param(token),
'&pagesize='+SEARCH_PAGESIZE+ )
'&page='+page+ .then(get_json)
'&keywords='+encodeURIComponent(keyword)+ .then((json) => {
token_param(token) if (json.code !== 0) {
) if (json.msg) throw new Error(json.msg);
.then(get_json) else throw new Error(JSON.stringify(json));
.then((json)=>{ }
if(json.code!==0) { return json;
if(json.msg) throw new Error(json.msg); });
throw new Error(JSON.stringify(json)); },
}
return json; get_attention: (token) => {
}); return fetch(API_BASE + '/api.php?action=getattention' + token_param(token))
}, .then(get_json)
.then((json) => {
get_single: (pid,token)=>{ if (json.code !== 0) {
return fetch( if (json.msg) throw new Error(json.msg);
API_BASE+'/api.php?action=getone'+ throw new Error(JSON.stringify(json));
'&pid='+pid+ }
token_param(token) return json;
) });
.then(get_json) },
.then((json)=>{ };
if(json.code!==0) {
if(json.msg) throw new Error(json.msg);
else throw new Error(JSON.stringify(json));
}
return json;
});
},
get_attention: (token)=>{
return fetch(
API_BASE+'/api.php?action=getattention'+
token_param(token)
)
.then(get_json)
.then((json)=>{
if(json.code!==0) {
if(json.msg) throw new Error(json.msg);
throw new Error(JSON.stringify(json));
}
return json;
});
},
};

26
src/registerServiceWorker.js

@ -14,8 +14,8 @@ const isLocalhost = Boolean(
window.location.hostname === '[::1]' || window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4. // 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match( window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/,
) ),
); );
export default function register() { export default function register() {
@ -23,10 +23,10 @@ export default function register() {
// The URL constructor is available in all browsers that support SW. // The URL constructor is available in all browsers that support SW.
// const publicUrl = new URL(process.env.PUBLIC_URL, window.location); // const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
// if (publicUrl.origin !== window.location.origin) { // if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin // Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to // from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
// return; // return;
// } // }
window.addEventListener('load', () => { window.addEventListener('load', () => {
@ -41,7 +41,7 @@ export default function register() {
navigator.serviceWorker.ready.then(() => { navigator.serviceWorker.ready.then(() => {
console.log( console.log(
'This web app is being served cache-first by a service ' + 'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://goo.gl/SC7cgQ' 'worker. To learn more, visit https://goo.gl/SC7cgQ',
); );
}); });
} else { } else {
@ -55,7 +55,7 @@ export default function register() {
function registerValidSW(swUrl) { function registerValidSW(swUrl) {
navigator.serviceWorker navigator.serviceWorker
.register(swUrl) .register(swUrl)
.then(registration => { .then((registration) => {
registration.onupdatefound = () => { registration.onupdatefound = () => {
const installingWorker = registration.installing; const installingWorker = registration.installing;
installingWorker.onstatechange = () => { installingWorker.onstatechange = () => {
@ -76,7 +76,7 @@ function registerValidSW(swUrl) {
}; };
}; };
}) })
.catch(error => { .catch((error) => {
console.error('Error during service worker registration:', error); console.error('Error during service worker registration:', error);
}); });
} }
@ -84,14 +84,14 @@ function registerValidSW(swUrl) {
function checkValidServiceWorker(swUrl) { function checkValidServiceWorker(swUrl) {
// Check if the service worker can be found. If it can't reload the page. // Check if the service worker can be found. If it can't reload the page.
fetch(swUrl) fetch(swUrl)
.then(response => { .then((response) => {
// Ensure service worker exists, and that we really are getting a JS file. // Ensure service worker exists, and that we really are getting a JS file.
if ( if (
response.status === 404 || response.status === 404 ||
response.headers.get('content-type').indexOf('javascript') === -1 response.headers.get('content-type').indexOf('javascript') === -1
) { ) {
// No service worker found. Probably a different app. Reload the page. // No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => { navigator.serviceWorker.ready.then((registration) => {
registration.unregister().then(() => { registration.unregister().then(() => {
window.location.reload(); window.location.reload();
}); });
@ -103,14 +103,14 @@ function checkValidServiceWorker(swUrl) {
}) })
.catch(() => { .catch(() => {
console.log( console.log(
'No internet connection found. App is running in offline mode.' 'No internet connection found. App is running in offline mode.',
); );
}); });
} }
export function unregister() { export function unregister() {
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => { navigator.serviceWorker.ready.then((registration) => {
registration.unregister(); registration.unregister();
}); });
} }

54
src/text_splitter.js

@ -1,34 +1,34 @@
// regexp should match the WHOLE segmented part // regexp should match the WHOLE segmented part
// export const PID_RE=/(^|[^\d\u20e3\ufe0e\ufe0f])([2-9]\d{4,5}|1\d{4,6})(?![\d\u20e3\ufe0e\ufe0f])/g; // export const PID_RE=/(^|[^\d\u20e3\ufe0e\ufe0f])([2-9]\d{4,5}|1\d{4,6})(?![\d\u20e3\ufe0e\ufe0f])/g;
export const PID_RE=/(^|[^\d\u20e3\ufe0e\ufe0f])(#\d{1,7})(?![\d\u20e3\ufe0e\ufe0f])/g; export const PID_RE = /(^|[^\d\u20e3\ufe0e\ufe0f])(#\d{1,7})(?![\d\u20e3\ufe0e\ufe0f])/g;
// TODO: fix this re // TODO: fix this re
// export const URL_PID_RE=/((?:https?:\/\/)?thuhole\.com\/?#(?:#|%23)([2-9]\d{4,5}|1\d{4,6}))(?!\d|\u20e3|\ufe0e|\ufe0f)/g; // export const URL_PID_RE=/((?:https?:\/\/)?thuhole\.com\/?#(?:#|%23)([2-9]\d{4,5}|1\d{4,6}))(?!\d|\u20e3|\ufe0e|\ufe0f)/g;
export const URL_PID_RE=/((?:https?:\/\/)?thuhole\.com\/?#(?:#|%23)(\d{1,7}))(?!\d|\u20e3|\ufe0e|\ufe0f)/g; export const URL_PID_RE = /((?:https?:\/\/)?thuhole\.com\/?#(?:#|%23)(\d{1,7}))(?!\d|\u20e3|\ufe0e|\ufe0f)/g;
export const NICKNAME_RE=/(^|[^A-Za-z])((?:(?:Angry|Baby|Crazy|Diligent|Excited|Fat|Greedy|Hungry|Interesting|Jolly|Kind|Little|Magic|Naïve|Old|PKU|Quiet|Rich|Superman|Tough|Undefined|Valuable|Wifeless|Xiangbuchulai|Young|Zombie)\s)?(?:Alice|Bob|Carol|Dave|Eve|Francis|Grace|Hans|Isabella|Jason|Kate|Louis|Margaret|Nathan|Olivia|Paul|Queen|Richard|Susan|Thomas|Uma|Vivian|Winnie|Xander|Yasmine|Zach)|You Win(?: \d+)?|洞主)(?![A-Za-z])/gi; export const NICKNAME_RE = /(^|[^A-Za-z])((?:(?:Angry|Baby|Crazy|Diligent|Excited|Fat|Greedy|Hungry|Interesting|Jolly|Kind|Little|Magic|Naïve|Old|PKU|Quiet|Rich|Superman|Tough|Undefined|Valuable|Wifeless|Xiangbuchulai|Young|Zombie)\s)?(?:Alice|Bob|Carol|Dave|Eve|Francis|Grace|Hans|Isabella|Jason|Kate|Louis|Margaret|Nathan|Olivia|Paul|Queen|Richard|Susan|Thomas|Uma|Vivian|Winnie|Xander|Yasmine|Zach)|You Win(?: \d+)?|洞主)(?![A-Za-z])/gi;
export const URL_RE=/(^|[^.@a-zA-Z0-9_])((?:https?:\/\/)?(?:(?:[\w-]+\.)+[a-zA-Z]{2,3}|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?::\d{1,5})?(?:\/[\w~!@#$%^&*()\-_=+[\]{};:,./?|]*)?)(?![a-zA-Z0-9])/gi; export const URL_RE = /(^|[^.@a-zA-Z0-9_])((?:https?:\/\/)?(?:(?:[\w-]+\.)+[a-zA-Z]{2,3}|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?::\d{1,5})?(?:\/[\w~!@#$%^&*()\-_=+[\]{};:,./?|]*)?)(?![a-zA-Z0-9])/gi;
export function split_text(txt,rules) { export function split_text(txt, rules) {
// rules: [['name',/regex/],...] // rules: [['name',/regex/],...]
// return: [['name','part'],[null,'part'],...] // return: [['name','part'],[null,'part'],...]
txt=[[null,txt]]; txt = [[null, txt]];
rules.forEach((rule)=>{ rules.forEach((rule) => {
let [name,regex]=rule; let [name, regex] = rule;
txt=[].concat.apply([],txt.map((part)=>{ txt = [].concat.apply(
let [rule,content]=part; [],
if(rule) // already tagged by previous rules txt.map((part) => {
return [part]; let [rule, content] = part;
else { if (rule)
return content // already tagged by previous rules
.split(regex) return [part];
.map((seg)=>( else {
regex.test(seg) ? [name,seg] : [null,seg] return content
)) .split(regex)
.filter(([name,seg])=>( .map((seg) => (regex.test(seg) ? [name, seg] : [null, seg]))
name!==null || seg .filter(([name, seg]) => name !== null || seg);
)); }
} }),
})); );
}); });
return txt; return txt;
} }

Loading…
Cancel
Save