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. 157
      src/AudioWidget.js
  5. 615
      src/Common.js
  6. 502
      src/Config.js
  7. 1805
      src/Flows.js
  8. 36
      src/Markdown.js
  9. 133
      src/Message.js
  10. 205
      src/PressureHelper.js
  11. 95
      src/Sidebar.js
  12. 287
      src/Title.js
  13. 1227
      src/UserAction.js
  14. 309
      src/cache.js
  15. 35
      src/color_picker.js
  16. 351
      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 {Flow} from './Flows';
import {Title} from './Title';
import {Sidebar} from './Sidebar';
import {PressureHelper} from './PressureHelper';
import {TokenCtx} from './UserAction';
import {load_config,bgimg_style} from './Config';
import {listen_darkmode} from './infrastructure/functions';
import {LoginPopup, TitleLine} from './infrastructure/widgets';
import React, { Component } from 'react';
import { Flow } from './Flows';
import { Title } from './Title';
import { Sidebar } from './Sidebar';
import { PressureHelper } from './PressureHelper';
import { TokenCtx } from './UserAction';
import { load_config, bgimg_style } from './Config';
import { listen_darkmode } from './infrastructure/functions';
import { LoginPopup, TitleLine } from './infrastructure/widgets';
const MAX_SIDEBAR_STACK_SIZE=10;
const MAX_SIDEBAR_STACK_SIZE = 10;
function DeprecatedAlert(props) {
return (
<div id="global-hint-container" style={{display: 'none'}} />
);
return <div id="global-hint-container" style={{ display: 'none' }} />;
}
class App extends Component {
constructor(props) {
super(props);
load_config();
listen_darkmode({default: undefined, light: false, dark: true}[window.config.color_scheme]);
this.state={
sidebar_stack: [[null,null]], // list of [status, content]
mode: 'list', // list, single, search, attention
search_text: null,
flow_render_key: +new Date(),
token: localStorage['TOKEN']||null,
};
this.show_sidebar_bound=this.show_sidebar.bind(this);
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;
}
constructor(props) {
super(props);
load_config();
listen_darkmode(
{ default: undefined, light: false, dark: true }[
window.config.color_scheme
],
);
this.state = {
sidebar_stack: [[null, null]], // list of [status, content]
mode: 'list', // list, single, search, attention
search_text: null,
flow_render_key: +new Date(),
token: localStorage['TOKEN'] || null,
};
this.show_sidebar_bound = this.show_sidebar.bind(this);
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() {
if(window.config.color_scheme==='dark') return true;
if(window.config.color_scheme==='light') return false;
else { // 'default'
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
static is_darkmode() {
if (window.config.color_scheme === 'dark') return true;
if (window.config.color_scheme === 'light') return false;
else {
// 'default'
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
}
on_pressure() {
if(this.state.sidebar_stack.length>1)
this.show_sidebar(null,null,'clear');
else
this.set_mode('list',null);
}
on_pressure() {
if (this.state.sidebar_stack.length > 1)
this.show_sidebar(null, null, 'clear');
else this.set_mode('list', null);
}
show_sidebar(title,content,mode='push') {
this.setState((prevState)=>{
let ns=prevState.sidebar_stack.slice();
if(mode==='push') {
if(ns.length>MAX_SIDEBAR_STACK_SIZE)
ns.splice(1,1);
ns=ns.concat([[title,content]]);
} else if(mode==='pop') {
if(ns.length===1) return;
ns.pop();
} else if(mode==='replace') {
ns.pop();
ns=ns.concat([[title,content]]);
} else if(mode==='clear') {
ns=[[null,null]];
} else
throw new Error('bad show_sidebar mode');
return {
sidebar_stack: ns,
};
});
}
show_sidebar(title, content, mode = 'push') {
this.setState((prevState) => {
let ns = prevState.sidebar_stack.slice();
if (mode === 'push') {
if (ns.length > MAX_SIDEBAR_STACK_SIZE) ns.splice(1, 1);
ns = ns.concat([[title, content]]);
} else if (mode === 'pop') {
if (ns.length === 1) return;
ns.pop();
} else if (mode === 'replace') {
ns.pop();
ns = ns.concat([[title, content]]);
} else if (mode === 'clear') {
ns = [[null, null]];
} else throw new Error('bad show_sidebar mode');
return {
sidebar_stack: ns,
};
});
}
set_mode(mode,search_text) {
this.setState({
mode: mode,
search_text: search_text,
flow_render_key: +new Date(),
});
}
set_mode(mode, search_text) {
this.setState({
mode: mode,
search_text: search_text,
flow_render_key: +new Date(),
});
}
render() {
return (
<TokenCtx.Provider value={{
value: this.state.token,
set_value: (x)=>{
localStorage['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} />
<TokenCtx.Consumer>{(token)=>(
<div className="left-container">
<DeprecatedAlert token={token.value} />
{!token.value &&
<div className="flow-item-row aux-margin">
<div className="box box-tip">
<p>
<LoginPopup token_callback={token.set_value}>{(do_popup)=>(
<a onClick={do_popup}>
<span className="icon icon-login" />
&nbsp;登录到 T大树洞
</a>
)}</LoginPopup>
</p>
</div>
</div>
}
{this.inthu_flag||token.value ?
<Flow 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>
);
}
render() {
return (
<TokenCtx.Provider
value={{
value: this.state.token,
set_value: (x) => {
localStorage['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}
/>
<TokenCtx.Consumer>
{(token) => (
<div className="left-container">
<DeprecatedAlert token={token.value} />
{!token.value && (
<div className="flow-item-row aux-margin">
<div className="box box-tip">
<p>
<LoginPopup token_callback={token.set_value}>
{(do_popup) => (
<a onClick={do_popup}>
<span className="icon icon-login" />
&nbsp;登录到 T大树洞
</a>
)}
</LoginPopup>
</p>
</div>
</div>
)}
{this.inthu_flag || token.value ? (
<Flow
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;

157
src/AudioWidget.js

@ -1,91 +1,92 @@
import React, {Component} from 'react';
import React, { Component } from 'react';
import load from 'load-script';
window.audio_cache={};
window.audio_cache = {};
function load_amrnb() {
return new Promise((resolve,reject)=>{
if(window.AMR)
resolve();
else
load('static/amr_all.min.js', (err)=>{
if(err)
reject(err);
else
resolve();
});
});
return new Promise((resolve, reject) => {
if (window.AMR) resolve();
else
load('static/amr_all.min.js', (err) => {
if (err) reject(err);
else resolve();
});
});
}
export class AudioWidget extends Component {
constructor(props) {
super(props);
this.state={
url: this.props.src,
state: 'waiting',
data: null,
};
constructor(props) {
super(props);
this.state = {
url: this.props.src,
state: 'waiting',
data: null,
};
}
load() {
if (window.audio_cache[this.state.url]) {
this.setState({
state: 'loaded',
data: window.audio_cache[this.state.url],
});
return;
}
load() {
if(window.audio_cache[this.state.url]) {
this.setState({
state: 'loaded',
data: window.audio_cache[this.state.url],
});
console.log('fetching audio', this.state.url);
this.setState({
state: 'loading',
});
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;
}
console.log('fetching audio',this.state.url);
this.setState({
state: 'loading',
});
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;
}
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 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'}));
window.audio_cache[this.state.url]=objurl;
this.setState({
state: 'loaded',
data: objurl,
});
};
reader.readAsBinaryString(blob);
});
this.setState({
state: 'decoding',
});
});
}
const objurl = URL.createObjectURL(
new Blob([binary_wave], { type: 'audio/wav' }),
);
window.audio_cache[this.state.url] = objurl;
this.setState({
state: 'loaded',
data: objurl,
});
};
reader.readAsBinaryString(blob);
});
this.setState({
state: 'decoding',
});
});
}
render() {
if(this.state.state==='waiting')
return (<p><a onClick={this.load.bind(this)}>加载音频</a></p>);
if(this.state.state==='loading')
return (<p>正在下载</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>);
}
render() {
if (this.state.state === 'waiting')
return (
<p>
<a onClick={this.load.bind(this)}>加载音频</a>
</p>
);
if (this.state.state === 'loading') return <p>正在下载</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 {format_time,Time,TitleLine} from './infrastructure/widgets';
import {THUHOLE_API_ROOT} from './flows_api';
import React, { Component, PureComponent } from 'react';
import { format_time, Time, TitleLine } from './infrastructure/widgets';
import { THUHOLE_API_ROOT } from './flows_api';
import HtmlToReact from 'html-to-react'
import HtmlToReact from 'html-to-react';
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
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') {
return txt ? new RegExp(`(${txt.split(split).filter((x)=>!!x).map(escape_regex).join('|')})`,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;
}
export function ColoredSpan(props) {
return (
<span className="colored-span" style={{
'--coloredspan-bgcolor-light': props.colors[0],
'--coloredspan-bgcolor-dark': props.colors[1],
}}>{props.children}</span>
)
return (
<span
className="colored-span"
style={{
'--coloredspan-bgcolor-light': props.colors[0],
'--coloredspan-bgcolor-dark': props.colors[1],
}}
>
{props.children}
</span>
);
}
function normalize_url(url) {
return /^https?:\/\//.test(url) ? url : 'http://'+url;
return /^https?:\/\//.test(url) ? url : 'http://' + url;
}
export class HighlightedText extends PureComponent {
render() {
return (
<pre>
{this.props.parts.map((part,idx)=>{
let [rule,p]=part;
return (
<span key={idx}>{
rule==='url_pid' ? <span className="url-pid-link" title={p}>/##</span> :
rule==='url' ? <a href={normalize_url(p)} target="_blank" rel="noopener">{p}</a> :
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> :
rule==='search' ? <span className="search-query-highlight">{p}</span> :
p
}</span>
);
})}
</pre>
)
}
render() {
return (
<pre>
{this.props.parts.map((part, idx) => {
let [rule, p] = part;
return (
<span key={idx}>
{rule === 'url_pid' ? (
<span className="url-pid-link" title={p}>
/##
</span>
) : rule === 'url' ? (
<a href={normalize_url(p)} target="_blank" rel="noopener">
{p}
</a>
) : 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>
) : rule === 'search' ? (
<span className="search-query-highlight">{p}</span>
) : (
p
)}
</span>
);
})}
</pre>
);
}
}
// props: text, show_pid, color_picker
export class HighlightedMarkdown extends Component {
render() {
const props = this.props
const processDefs = new HtmlToReact.ProcessNodeDefinitions(React)
const processInstructions = [
{
shouldProcessNode: (node) => node.name === 'img', // disable images
processNode (node, children, index) {
return (<div key={index}>[图片]</div>)
}
},
{
shouldProcessNode: (node) => (/^h[123456]$/.test(node.name)),
processNode (node, children, index) {
let currentLevel = +(node.name[1])
if (currentLevel < 3) currentLevel = 3;
const HeadingTag = `h${currentLevel}`
return (
<HeadingTag key={index}>{children}</HeadingTag>
)
}
},
{
shouldProcessNode: (node) => node.name === 'a',
processNode (node, children, index) {
return (
<a href={normalize_url(node.attribs.href)} target="_blank" rel="noopenner noreferrer" className="ext-link" 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
const splitted = split_text(originalText, [
['url_pid', URL_PID_RE],
['url',URL_RE],
['pid',PID_RE],
['nickname',NICKNAME_RE],
])
render() {
const props = this.props;
const processDefs = new HtmlToReact.ProcessNodeDefinitions(React);
const processInstructions = [
{
shouldProcessNode: (node) => node.name === 'img', // disable images
processNode(node, children, index) {
return <div key={index}>[图片]</div>;
},
},
{
shouldProcessNode: (node) => /^h[123456]$/.test(node.name),
processNode(node, children, index) {
let currentLevel = +node.name[1];
if (currentLevel < 3) currentLevel = 3;
const HeadingTag = `h${currentLevel}`;
return <HeadingTag key={index}>{children}</HeadingTag>;
},
},
{
shouldProcessNode: (node) => node.name === 'a',
processNode(node, children, index) {
return (
<a
href={normalize_url(node.attribs.href)}
target="_blank"
rel="noopenner noreferrer"
className="ext-link"
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;
const splitted = split_text(originalText, [
['url_pid', URL_PID_RE],
['url', URL_RE],
['pid', PID_RE],
['nickname', NICKNAME_RE],
]);
return (
<React.Fragment key={index}>
{splitted.map(([rule, p], idx) => {
return (<span key={idx}>
{
rule==='url_pid' ? <span className="url-pid-link" title={p}>/##</span> :
rule==='url' ? <a href={normalize_url(p)} className="ext-link" target="_blank" rel="noopener noreferrer">
{p}
<span className="icon icon-new-tab" />
</a> :
rule==='pid' ? <a href={'#'+p} onClick={(e)=>{e.preventDefault(); props.show_pid(p.substring(1));}}>{p}</a> :
rule==='nickname' ? <ColoredSpan colors={props.color_picker.get(p)}>{p}</ColoredSpan> :
rule==='search' ? <span className="search-query-highlight">{p}</span> :
p}
</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)
}
return (
<React.Fragment key={index}>
{splitted.map(([rule, p], idx) => {
return (
<span key={idx}>
{rule === 'url_pid' ? (
<span className="url-pid-link" title={p}>
/##
</span>
) : rule === 'url' ? (
<a
href={normalize_url(p)}
className="ext-link"
target="_blank"
rel="noopener noreferrer"
>
{p}
<span className="icon icon-new-tab" />
</a>
) : rule === 'pid' ? (
<a
href={'#' + p}
onClick={(e) => {
e.preventDefault();
props.show_pid(p.substring(1));
}}
>
{p}
</a>
) : rule === 'nickname' ? (
<ColoredSpan colors={props.color_picker.get(p)}>
{p}
</ColoredSpan>
) : rule === 'search' ? (
<span className="search-query-highlight">{p}</span>
) : (
p
)}
</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 {
constructor(props) {
super(props);
this.state={
text: '',
};
this.on_change_bound=this.on_change.bind(this);
this.on_keydown_bound=this.on_keydown.bind(this);
this.clear=this.clear.bind(this);
this.area_ref=React.createRef();
this.change_callback=props.on_change||(()=>{});
this.submit_callback=props.on_submit||(()=>{});
}
componentDidMount() {
this.setState({
text: window.TEXTAREA_BACKUP[this.props.id]||''
},()=>{
this.change_callback(this.state.text);
});
}
constructor(props) {
super(props);
this.state = {
text: '',
};
this.on_change_bound = this.on_change.bind(this);
this.on_keydown_bound = this.on_keydown.bind(this);
this.clear = this.clear.bind(this);
this.area_ref = React.createRef();
this.change_callback = props.on_change || (() => {});
this.submit_callback = props.on_submit || (() => {});
}
componentWillUnmount() {
window.TEXTAREA_BACKUP[this.props.id]=this.state.text;
componentDidMount() {
this.setState(
{
text: window.TEXTAREA_BACKUP[this.props.id] || '',
},
() => {
this.change_callback(this.state.text);
}
},
);
}
on_change(event) {
this.setState({
text: event.target.value,
});
this.change_callback(event.target.value);
}
on_keydown(event) {
if(event.key==='Enter' && event.ctrlKey && !event.altKey) {
event.preventDefault();
this.submit_callback();
}
}
componentWillUnmount() {
window.TEXTAREA_BACKUP[this.props.id] = this.state.text;
this.change_callback(this.state.text);
}
clear() {
this.setState({
text: '',
});
}
set(text) {
this.change_callback(text);
this.setState({
text: text,
});
}
get() {
return this.state.text;
}
focus() {
this.area_ref.current.focus();
on_change(event) {
this.setState({
text: event.target.value,
});
this.change_callback(event.target.value);
}
on_keydown(event) {
if (event.key === 'Enter' && event.ctrlKey && !event.altKey) {
event.preventDefault();
this.submit_callback();
}
}
render() {
return (
<textarea ref={this.area_ref} onChange={this.on_change_bound} value={this.state.text} onKeyDown={this.on_keydown_bound} />
)
}
clear() {
this.setState({
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) => {
console.log('pwa: received before install prompt');
pwa_prompt_event=e;
console.log('pwa: received before install prompt');
pwa_prompt_event = e;
});
export function PromotionBar(props) {
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_ios = /iPhone|iPad|iPod/i.test(window.navigator.userAgent);
let is_installed =
window.matchMedia('(display-mode: standalone)').matches ||
window.navigator.standalone;
if(is_installed)
return null;
if (is_installed) return null;
if(is_ios)
// noinspection JSConstructorReturnsPrimitive
return !navigator.standalone ? (
<div className="box promotion-bar">
<span className="icon icon-about" />&nbsp;
Safari 把树洞 <b>添加到主屏幕</b>
</div>
) : null;
else
// noinspection JSConstructorReturnsPrimitive
return pwa_prompt_event ? (
<div className="box promotion-bar">
<span className="icon icon-about" />&nbsp;
把网页版树洞 <b><a onClick={()=>{
if(pwa_prompt_event)
pwa_prompt_event.prompt();
}}>安装到桌面</a></b>
</div>
) : null;
if (is_ios)
// noinspection JSConstructorReturnsPrimitive
return !navigator.standalone ? (
<div className="box promotion-bar">
<span className="icon icon-about" />
&nbsp; Safari 把树洞 <b>添加到主屏幕</b>
</div>
) : null;
// noinspection JSConstructorReturnsPrimitive
else
return pwa_prompt_event ? (
<div className="box promotion-bar">
<span className="icon icon-about" />
&nbsp; 把网页版树洞{' '}
<b>
<a
onClick={() => {
if (pwa_prompt_event) pwa_prompt_event.prompt();
}}
>
安装到桌面
</a>
</b>{' '}
更好用
</div>
) : null;
}
export class ClickHandler extends PureComponent {
constructor(props) {
super(props);
this.state={
moved: true,
init_y: 0,
init_x: 0,
};
this.on_begin_bound=this.on_begin.bind(this);
this.on_move_bound=this.on_move.bind(this);
this.on_end_bound=this.on_end.bind(this);
constructor(props) {
super(props);
this.state = {
moved: true,
init_y: 0,
init_x: 0,
};
this.on_begin_bound = this.on_begin.bind(this);
this.on_move_bound = this.on_move.bind(this);
this.on_end_bound = this.on_end.bind(this);
this.MOVE_THRESHOLD=3;
this.last_fire=0;
}
this.MOVE_THRESHOLD = 3;
this.last_fire = 0;
}
on_begin(e) {
//console.log('click',(e.touches?e.touches[0]:e).screenY,(e.touches?e.touches[0]:e).screenX);
this.setState({
moved: false,
init_y: (e.touches?e.touches[0]:e).screenY,
init_x: (e.touches?e.touches[0]:e).screenX,
});
}
on_move(e) {
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);
//console.log('move',mvmt);
if(mvmt>this.MOVE_THRESHOLD)
this.setState({
moved: true,
});
}
}
on_end(event) {
//console.log('end');
if(!this.state.moved)
this.do_callback(event);
on_begin(e) {
//console.log('click',(e.touches?e.touches[0]:e).screenY,(e.touches?e.touches[0]:e).screenX);
this.setState({
moved: false,
init_y: (e.touches ? e.touches[0] : e).screenY,
init_x: (e.touches ? e.touches[0] : e).screenX,
});
}
on_move(e) {
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);
//console.log('move',mvmt);
if (mvmt > this.MOVE_THRESHOLD)
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) {
if(this.last_fire+100>+new Date()) return;
this.last_fire=+new Date();
this.props.callback(event);
}
do_callback(event) {
if (this.last_fire + 100 > +new Date()) return;
this.last_fire = +new Date();
this.props.callback(event);
}
render() {
return (
<div onTouchStart={this.on_begin_bound} onMouseDown={this.on_begin_bound}
onTouchMove={this.on_move_bound} onMouseMove={this.on_move_bound}
onClick={this.on_end_bound} >
{this.props.children}
</div>
)
}
render() {
return (
<div
onTouchStart={this.on_begin_bound}
onMouseDown={this.on_begin_bound}
onTouchMove={this.on_move_bound}
onMouseMove={this.on_move_bound}
onClick={this.on_end_bound}
>
{this.props.children}
</div>
);
}
}

502
src/Config.js

@ -1,255 +1,327 @@
import React, {Component, PureComponent} from 'react';
import React, { Component, PureComponent } from 'react';
import './Config.css';
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/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/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 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/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/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={
background_img: 'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/gbp.jpg',
background_color: '#113366',
pressure: false,
easter_egg: true,
color_scheme: 'default',
fold: true
const DEFAULT_CONFIG = {
background_img:
'https://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages/static/bg/gbp.jpg',
background_color: '#113366',
pressure: false,
easter_egg: true,
color_scheme: 'default',
fold: true,
};
export function load_config() {
let config=Object.assign({},DEFAULT_CONFIG);
let loaded_config;
try {
loaded_config=JSON.parse(localStorage['hole_config']||'{}');
} catch(e) {
alert('设置加载失败,将重置为默认设置!\n'+e);
delete localStorage['hole_config'];
loaded_config={};
}
let config = Object.assign({}, DEFAULT_CONFIG);
let loaded_config;
try {
loaded_config = JSON.parse(localStorage['hole_config'] || '{}');
} catch (e) {
alert('设置加载失败,将重置为默认设置!\n' + e);
delete localStorage['hole_config'];
loaded_config = {};
}
// unrecognized configs are removed
Object.keys(loaded_config).forEach((key)=>{
if(config[key]!==undefined)
config[key]=loaded_config[key];
});
// unrecognized configs are removed
Object.keys(loaded_config).forEach((key) => {
if (config[key] !== undefined) config[key] = loaded_config[key];
});
console.log('config loaded',config);
window.config=config;
console.log('config loaded', config);
window.config = config;
}
export function save_config() {
localStorage['hole_config']=JSON.stringify(window.config);
load_config();
localStorage['hole_config'] = JSON.stringify(window.config);
load_config();
}
export function bgimg_style(img,color) {
if(img===undefined) img=window.config.background_img;
if(color===undefined) color=window.config.background_color;
return {
background: 'transparent center center',
backgroundImage: img===null ? 'unset' : 'url("'+encodeURI(img)+'")',
backgroundColor: color,
backgroundSize: 'cover',
};
export function bgimg_style(img, color) {
if (img === undefined) img = window.config.background_img;
if (color === undefined) color = window.config.background_color;
return {
background: 'transparent center center',
backgroundImage: img === null ? 'unset' : 'url("' + encodeURI(img) + '")',
backgroundColor: color,
backgroundSize: 'cover',
};
}
class ConfigBackground extends PureComponent {
constructor(props) {
super(props);
this.state={
img: window.config.background_img,
color: window.config.background_color,
};
}
constructor(props) {
super(props);
this.state = {
img: window.config.background_img,
color: window.config.background_color,
};
}
save_changes() {
this.props.callback({
background_img: this.state.img,
background_color: this.state.color,
});
}
save_changes() {
this.props.callback({
background_img: this.state.img,
background_color: this.state.color,
});
}
on_select(e) {
let value=e.target.value;
this.setState({
img: value==='##other' ? '' :
value==='##color' ? null : value,
},this.save_changes.bind(this));
}
on_change_img(e) {
this.setState({
img: e.target.value,
},this.save_changes.bind(this));
}
on_change_color(e) {
this.setState({
color: e.target.value,
},this.save_changes.bind(this));
}
on_select(e) {
let value = e.target.value;
this.setState(
{
img: value === '##other' ? '' : value === '##color' ? null : value,
},
this.save_changes.bind(this),
);
}
on_change_img(e) {
this.setState(
{
img: e.target.value,
},
this.save_changes.bind(this),
);
}
on_change_color(e) {
this.setState(
{
color: e.target.value,
},
this.save_changes.bind(this),
);
}
render() {
let img_select= this.state.img===null ? '##color' :
Object.keys(BUILTIN_IMGS).indexOf(this.state.img)===-1 ? '##other' : this.state.img;
return (
<div>
<p>
<b>背景图片</b>
<select value={img_select} onChange={this.on_select.bind(this)}>
{Object.keys(BUILTIN_IMGS).map((key)=>(
<option key={key} value={key}>{BUILTIN_IMGS[key]}</option>
))}
<option value="##other">输入图片网址</option>
<option value="##color">纯色背景</option>
</select>
&nbsp;
{img_select==='##other' &&
<input 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>
);
}
render() {
let img_select =
this.state.img === null
? '##color'
: Object.keys(BUILTIN_IMGS).indexOf(this.state.img) === -1
? '##other'
: this.state.img;
return (
<div>
<p>
<b>背景图片</b>
<select value={img_select} onChange={this.on_select.bind(this)}>
{Object.keys(BUILTIN_IMGS).map((key) => (
<option key={key} value={key}>
{BUILTIN_IMGS[key]}
</option>
))}
<option value="##other">输入图片网址</option>
<option value="##color">纯色背景</option>
</select>
&nbsp;
{img_select === '##other' && (
<input
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 {
constructor(props) {
super(props);
this.state={
color_scheme: window.config.color_scheme,
};
}
constructor(props) {
super(props);
this.state = {
color_scheme: window.config.color_scheme,
};
}
save_changes() {
this.props.callback({
color_scheme: this.state.color_scheme,
});
}
save_changes() {
this.props.callback({
color_scheme: this.state.color_scheme,
});
}
on_select(e) {
let value=e.target.value;
this.setState({
color_scheme: value,
},this.save_changes.bind(this));
}
on_select(e) {
let value = e.target.value;
this.setState(
{
color_scheme: value,
},
this.save_changes.bind(this),
);
}
render() {
return (
<div>
<p>
<b>夜间模式</b>
<select value={this.state.color_scheme} onChange={this.on_select.bind(this)}>
<option value="default">跟随系统</option>
<option value="light">始终浅色模式</option>
<option value="dark">始终深色模式</option>
</select>
&nbsp; <small>#color_scheme</small>
</p>
<p>
选择浅色或深色模式深色模式下将会调暗图片亮度
</p>
</div>
)
}
render() {
return (
<div>
<p>
<b>夜间模式</b>
<select
value={this.state.color_scheme}
onChange={this.on_select.bind(this)}
>
<option value="default">跟随系统</option>
<option value="light">始终浅色模式</option>
<option value="dark">始终深色模式</option>
</select>
&nbsp; <small>#color_scheme</small>
</p>
<p>选择浅色或深色模式深色模式下将会调暗图片亮度</p>
</div>
);
}
}
class ConfigSwitch extends PureComponent {
constructor(props) {
super(props);
this.state={
switch: window.config[this.props.id],
};
}
constructor(props) {
super(props);
this.state = {
switch: window.config[this.props.id],
};
}
on_change(e) {
let val=e.target.checked;
this.setState({
switch: val,
},()=>{
this.props.callback({
[this.props.id]: val,
});
on_change(e) {
let val = e.target.checked;
this.setState(
{
switch: val,
},
() => {
this.props.callback({
[this.props.id]: val,
});
}
},
);
}
render() {
return (
<div>
<p>
<label>
<input name={'config-'+this.props.id} type="checkbox" checked={this.state.switch} onChange={this.on_change.bind(this)} />
<b>{this.props.name}</b>
&nbsp; <small>#{this.props.id}</small>
</label>
</p>
<p>
{this.props.description}
</p>
</div>
);
}
render() {
return (
<div>
<p>
<label>
<input
name={'config-' + this.props.id}
type="checkbox"
checked={this.state.switch}
onChange={this.on_change.bind(this)}
/>
<b>{this.props.name}</b>
&nbsp; <small>#{this.props.id}</small>
</label>
</p>
<p>{this.props.description}</p>
</div>
);
}
}
export class ConfigUI extends PureComponent {
constructor(props) {
super(props);
this.save_changes_bound=this.save_changes.bind(this);
}
constructor(props) {
super(props);
this.save_changes_bound = this.save_changes.bind(this);
}
save_changes(chg) {
console.log(chg);
Object.keys(chg).forEach((key)=>{
window.config[key]=chg[key];
});
save_config();
}
save_changes(chg) {
console.log(chg);
Object.keys(chg).forEach((key) => {
window.config[key] = chg[key];
});
save_config();
}
reset_settings() {
if(window.confirm('重置所有设置?')) {
window.config={};
save_config();
window.location.reload();
}
reset_settings() {
if (window.confirm('重置所有设置?')) {
window.config = {};
save_config();
window.location.reload();
}
}
render() {
return (
<div>
<div className="box config-ui-header">
<p>这些功能仍在测试可能不稳定<a onClick={this.reset_settings.bind(this)}>全部重置</a></p>
<p><b>修改设置后 <a onClick={()=>{window.location.reload()}}>刷新页面</a> </b></p>
</div>
<div className="box">
<ConfigBackground callback={this.save_changes_bound} />
<hr />
<ConfigColorScheme callback={this.save_changes_bound} />
<hr />
<ConfigSwitch callback={this.save_changes_bound} id="pressure" 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>
)
}
render() {
return (
<div>
<div className="box config-ui-header">
<p>
这些功能仍在测试可能不稳定
<a onClick={this.reset_settings.bind(this)}>全部重置</a>
</p>
<p>
<b>
修改设置后{' '}
<a
onClick={() => {
window.location.reload();
}}
>
刷新页面
</a>{' '}
方可生效
</b>
</p>
</div>
<div className="box">
<ConfigBackground callback={this.save_changes_bound} />
<hr />
<ConfigColorScheme callback={this.save_changes_bound} />
<hr />
<ConfigSwitch
callback={this.save_changes_bound}
id="pressure"
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>
);
}
}

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

133
src/Message.js

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

205
src/PressureHelper.js

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

95
src/Sidebar.js

@ -1,45 +1,66 @@
import React, {Component, PureComponent} from 'react';
import React, { Component, PureComponent } from 'react';
import './Sidebar.css';
export class Sidebar extends PureComponent {
constructor(props) {
super(props);
this.sidebar_ref=React.createRef();
this.do_close_bound=this.do_close.bind(this);
this.do_back_bound=this.do_back.bind(this);
}
constructor(props) {
super(props);
this.sidebar_ref = React.createRef();
this.do_close_bound = this.do_close.bind(this);
this.do_back_bound = this.do_back.bind(this);
}
componentDidUpdate(nextProps) {
if(this.props.stack!==nextProps.stack) {
//console.log('sidebar top');
if(this.sidebar_ref.current)
this.sidebar_ref.current.scrollTop=0;
}
componentDidUpdate(nextProps) {
if (this.props.stack !== nextProps.stack) {
//console.log('sidebar top');
if (this.sidebar_ref.current) this.sidebar_ref.current.scrollTop = 0;
}
}
do_close() {
this.props.show_sidebar(null,null,'clear');
}
do_back() {
this.props.show_sidebar(null,null,'pop');
}
do_close() {
this.props.show_sidebar(null, null, 'clear');
}
do_back() {
this.props.show_sidebar(null, null, 'pop');
}
render() {
let [cur_title,cur_content]=this.props.stack[this.props.stack.length-1];
return (
<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();}} />
<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>
);
}
render() {
let [cur_title, cur_content] = this.props.stack[
this.props.stack.length - 1
];
return (
<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();
}}
/>
<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>
);
}
}

287
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 {InfoSidebar, PostForm} from './UserAction';
import {TokenCtx} from './UserAction';
import { InfoSidebar, PostForm } from './UserAction';
import { TokenCtx } from './UserAction';
import './Title.css';
const flag_re=/^\/\/setflag ([a-zA-Z0-9_]+)=(.*)$/;
const flag_re = /^\/\/setflag ([a-zA-Z0-9_]+)=(.*)$/;
class ControlBar extends PureComponent {
constructor(props) {
super(props);
this.state={
search_text: '',
};
this.set_mode=props.set_mode;
constructor(props) {
super(props);
this.state = {
search_text: '',
};
this.set_mode = props.set_mode;
this.on_change_bound=this.on_change.bind(this);
this.on_keypress_bound=this.on_keypress.bind(this);
this.do_refresh_bound=this.do_refresh.bind(this);
this.do_attention_bound=this.do_attention.bind(this);
}
this.on_change_bound = this.on_change.bind(this);
this.on_keypress_bound = this.on_keypress.bind(this);
this.do_refresh_bound = this.do_refresh.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'});
});
}
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) {
this.setState({
search_text: event.target.value,
});
}
on_change(event) {
this.setState({
search_text: event.target.value,
});
}
on_keypress(event) {
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;
}
const mode=this.state.search_text.startsWith('#') ? 'single' : 'search';
this.set_mode(mode,this.state.search_text||'');
on_keypress(event) {
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() {
window.scrollTo(0,0);
this.setState({
search_text: '',
});
this.set_mode('list',null);
const mode = this.state.search_text.startsWith('#') ? 'single' : 'search';
this.set_mode(mode, this.state.search_text || '');
}
}
do_attention() {
window.scrollTo(0,0);
this.setState({
search_text: '',
});
this.set_mode('attention',null);
}
do_refresh() {
window.scrollTo(0, 0);
this.setState({
search_text: '',
});
this.set_mode('list', null);
}
render() {
return (
<TokenCtx.Consumer>{({value: token})=>(
<div className="control-bar">
<a className="no-underline control-btn" onClick={this.do_refresh_bound}>
<span className="icon icon-refresh" />
<span className="control-btn-label">最新</span>
</a>
{!!token &&
<a className="no-underline control-btn" onClick={this.do_attention_bound}>
<span className="icon icon-attention" />
<span className="control-btn-label">关注</span>
</a>
}
<input className="control-search" value={this.state.search_text} placeholder="搜索 或 #树洞号"
onChange={this.on_change_bound} onKeyPress={this.on_keypress_bound}
/>
<a className="no-underline control-btn" onClick={()=>{
this.props.show_sidebar(
'T大树洞',
<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>
)
}
do_attention() {
window.scrollTo(0, 0);
this.setState({
search_text: '',
});
this.set_mode('attention', null);
}
render() {
return (
<TokenCtx.Consumer>
{({ value: token }) => (
<div className="control-bar">
<a
className="no-underline control-btn"
onClick={this.do_refresh_bound}
>
<span className="icon icon-refresh" />
<span className="control-btn-label">最新</span>
</a>
{!!token && (
<a
className="no-underline control-btn"
onClick={this.do_attention_bound}
>
<span className="icon icon-attention" />
<span className="control-btn-label">关注</span>
</a>
)}
<input
className="control-search"
value={this.state.search_text}
placeholder="搜索 或 #树洞号"
onChange={this.on_change_bound}
onKeyPress={this.on_keypress_bound}
/>
<a
className="no-underline control-btn"
onClick={() => {
this.props.show_sidebar(
'T大树洞',
<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) {
return (
<div className="title-bar">
{/* <AppSwitcher appid="hole" /> */}
<div className="aux-margin">
<div className="title">
<p className="centered-line">
<span onClick={()=>props.show_sidebar(
'T大树洞',
<InfoSidebar show_sidebar={props.show_sidebar} />
)}>
T大树洞
</span>
</p>
</div>
<ControlBar show_sidebar={props.show_sidebar} set_mode={props.set_mode} />
</div>
return (
<div className="title-bar">
{/* <AppSwitcher appid="hole" /> */}
<div className="aux-margin">
<div className="title">
<p className="centered-line">
<span
onClick={() =>
props.show_sidebar(
'T大树洞',
<InfoSidebar show_sidebar={props.show_sidebar} />,
)
}
>
T大树洞
</span>
</p>
</div>
)
<ControlBar
show_sidebar={props.show_sidebar}
set_mode={props.set_mode}
/>
</div>
</div>
);
}

1227
src/UserAction.js

File diff suppressed because it is too large Load Diff

309
src/cache.js

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

35
src/color_picker.js

@ -1,26 +1,25 @@
// 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 {
constructor() {
this.names={};
this.current_h=Math.random();
}
constructor() {
this.names = {};
this.current_h = Math.random();
}
get(name) {
name=name.toLowerCase();
if(name==='洞主')
return ['hsl(0,0%,97%)','hsl(0,0%,16%)'];
get(name) {
name = name.toLowerCase();
if (name === '洞主') return ['hsl(0,0%,97%)', 'hsl(0,0%,16%)'];
if(!this.names[name]) {
this.current_h+=golden_ratio_conjugate;
this.current_h%=1;
this.names[name]=[
`hsl(${this.current_h*360}, 50%, 90%)`,
`hsl(${this.current_h*360}, 60%, 20%)`,
];
}
return this.names[name];
if (!this.names[name]) {
this.current_h += golden_ratio_conjugate;
this.current_h %= 1;
this.names[name] = [
`hsl(${this.current_h * 360}, 50%, 90%)`,
`hsl(${this.current_h * 360}, 60%, 20%)`,
];
}
return this.names[name];
}
}

351
src/flows_api.js

@ -1,183 +1,182 @@
import {get_json, API_VERSION_PARAM} from './infrastructure/functions';
import {THUHOLE_API_ROOT} from './infrastructure/const';
import {API_BASE} from './Common';
import {cache} from './cache';
import { get_json, API_VERSION_PARAM } from './infrastructure/functions';
import { THUHOLE_API_ROOT } from './infrastructure/const';
import { API_BASE } from './Common';
import { cache } from './cache';
export {THUHOLE_API_ROOT, API_VERSION_PARAM};
export { THUHOLE_API_ROOT, API_VERSION_PARAM };
export function token_param(token) {
return API_VERSION_PARAM()+(token ? ('&user_token='+token) : '');
return API_VERSION_PARAM() + (token ? '&user_token=' + token : '');
}
export {get_json};
const SEARCH_PAGESIZE=50;
export const API={
load_replies: (pid,token,color_picker,cache_version)=>{
pid=parseInt(pid);
return fetch(
API_BASE+'/api.php?action=getcomment'+
'&pid='+pid+
token_param(token)
)
.then(get_json)
.then((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);
});
// also change load_replies_with_cache!
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;
export { get_json };
const SEARCH_PAGESIZE = 50;
export const API = {
load_replies: (pid, token, color_picker, cache_version) => {
pid = parseInt(pid);
return fetch(
API_BASE +
'/api.php?action=getcomment' +
'&pid=' +
pid +
token_param(token),
)
.then(get_json)
.then((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);
});
// also change load_replies_with_cache!
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) => {
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;
});
},
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;
}
else
return API.load_replies(pid,token,color_picker,cache_version);
});
},
set_attention: (pid,attention,token)=>{
let data=new URLSearchParams();
data.append('user_token',token);
data.append('pid',pid);
data.append('switch',attention ? '1' : '0');
return fetch(API_BASE+'/api.php?action=attention'+token_param(token), {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: data,
})
.then(get_json)
.then((json)=>{
cache().delete(pid);
if(json.code!==0) {
if(json.msg && json.msg==='已经关注过了') {}
else {
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);
data.append('reason',reason);
return fetch(API_BASE+'/api.php?action=report'+token_param(token), {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: data,
})
.then(get_json)
.then((json)=>{
if(json.code!==0) {
if(json.msg) alert(json.msg);
throw new Error(JSON.stringify(json));
}
return json;
});
},
get_list: (page,token)=>{
return fetch(
API_BASE+'/api.php?action=getlist'+
'&p='+page+
token_param(token)
)
.then(get_json)
.then((json)=>{
if(json.code!==0)
throw new Error(JSON.stringify(json));
return json;
});
},
get_search: (page,keyword,token)=>{
return fetch(
API_BASE+'/api.php?action=search'+
'&pagesize='+SEARCH_PAGESIZE+
'&page='+page+
'&keywords='+encodeURIComponent(keyword)+
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;
});
},
get_single: (pid,token)=>{
return fetch(
API_BASE+'/api.php?action=getone'+
'&pid='+pid+
token_param(token)
)
.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;
});
},
return json;
} else return API.load_replies(pid, token, color_picker, cache_version);
});
},
set_attention: (pid, attention, token) => {
let data = new URLSearchParams();
data.append('user_token', token);
data.append('pid', pid);
data.append('switch', attention ? '1' : '0');
return fetch(API_BASE + '/api.php?action=attention' + token_param(token), {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: data,
})
.then(get_json)
.then((json) => {
cache().delete(pid);
if (json.code !== 0) {
if (json.msg && json.msg === '已经关注过了') {
} else {
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);
data.append('reason', reason);
return fetch(API_BASE + '/api.php?action=report' + token_param(token), {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: data,
})
.then(get_json)
.then((json) => {
if (json.code !== 0) {
if (json.msg) alert(json.msg);
throw new Error(JSON.stringify(json));
}
return json;
});
},
get_list: (page, token) => {
return fetch(
API_BASE + '/api.php?action=getlist' + '&p=' + page + token_param(token),
)
.then(get_json)
.then((json) => {
if (json.code !== 0) throw new Error(JSON.stringify(json));
return json;
});
},
get_search: (page, keyword, token) => {
return fetch(
API_BASE +
'/api.php?action=search' +
'&pagesize=' +
SEARCH_PAGESIZE +
'&page=' +
page +
'&keywords=' +
encodeURIComponent(keyword) +
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;
});
},
get_single: (pid, token) => {
return fetch(
API_BASE + '/api.php?action=getone' + '&pid=' + pid + token_param(token),
)
.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]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
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() {
@ -23,10 +23,10 @@ export default function register() {
// The URL constructor is available in all browsers that support SW.
// const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
// if (publicUrl.origin !== window.location.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
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
// return;
// 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
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
// return;
// }
window.addEventListener('load', () => {
@ -41,7 +41,7 @@ export default function register() {
navigator.serviceWorker.ready.then(() => {
console.log(
'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 {
@ -55,7 +55,7 @@ export default function register() {
function registerValidSW(swUrl) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
.then((registration) => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
installingWorker.onstatechange = () => {
@ -76,7 +76,7 @@ function registerValidSW(swUrl) {
};
};
})
.catch(error => {
.catch((error) => {
console.error('Error during service worker registration:', error);
});
}
@ -84,14 +84,14 @@ function registerValidSW(swUrl) {
function checkValidServiceWorker(swUrl) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
.then((response) => {
// Ensure service worker exists, and that we really are getting a JS file.
if (
response.status === 404 ||
response.headers.get('content-type').indexOf('javascript') === -1
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
navigator.serviceWorker.ready.then((registration) => {
registration.unregister().then(() => {
window.location.reload();
});
@ -103,14 +103,14 @@ function checkValidServiceWorker(swUrl) {
})
.catch(() => {
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() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
navigator.serviceWorker.ready.then((registration) => {
registration.unregister();
});
}

54
src/text_splitter.js

@ -1,34 +1,34 @@
// 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])(#\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
// 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 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_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 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) {
// rules: [['name',/regex/],...]
// return: [['name','part'],[null,'part'],...]
export function split_text(txt, rules) {
// rules: [['name',/regex/],...]
// return: [['name','part'],[null,'part'],...]
txt=[[null,txt]];
rules.forEach((rule)=>{
let [name,regex]=rule;
txt=[].concat.apply([],txt.map((part)=>{
let [rule,content]=part;
if(rule) // already tagged by previous rules
return [part];
else {
return content
.split(regex)
.map((seg)=>(
regex.test(seg) ? [name,seg] : [null,seg]
))
.filter(([name,seg])=>(
name!==null || seg
));
}
}));
});
return txt;
txt = [[null, txt]];
rules.forEach((rule) => {
let [name, regex] = rule;
txt = [].concat.apply(
[],
txt.map((part) => {
let [rule, content] = part;
if (rule)
// already tagged by previous rules
return [part];
else {
return content
.split(regex)
.map((seg) => (regex.test(seg) ? [name, seg] : [null, seg]))
.filter(([name, seg]) => name !== null || seg);
}
}),
);
});
return txt;
}

Loading…
Cancel
Save