Browse Source

format code

dev
thuhole 5 years ago
parent
commit
7636e45c00
  1. 55
      .eslintrc
  2. 7
      .prettierrc.js
  3. 74
      src/App.js
  4. 39
      src/AudioWidget.js
  5. 265
      src/Common.js
  6. 172
      src/Config.js
  7. 775
      src/Flows.js
  8. 32
      src/Markdown.js
  9. 37
      src/Message.js
  10. 51
      src/PressureHelper.js
  11. 39
      src/Sidebar.js
  12. 105
      src/Title.js
  13. 433
      src/UserAction.js
  14. 43
      src/cache.js
  15. 3
      src/color_picker.js
  16. 55
      src/flows_api.js
  17. 18
      src/registerServiceWorker.js
  18. 18
      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',
}

74
src/App.js

@ -11,16 +11,18 @@ import {LoginPopup, TitleLine} from './infrastructure/widgets';
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]);
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
@ -33,13 +35,17 @@ class App extends Component {
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;
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'
else {
// 'default'
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
}
@ -47,16 +53,14 @@ class App extends Component {
on_pressure() {
if (this.state.sidebar_stack.length > 1)
this.show_sidebar(null, null, 'clear');
else
this.set_mode('list',null);
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);
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;
@ -66,8 +70,7 @@ class App extends Component {
ns = ns.concat([[title, content]]);
} else if (mode === 'clear') {
ns = [[null, null]];
} else
throw new Error('bad show_sidebar mode');
} else throw new Error('bad show_sidebar mode');
return {
sidebar_stack: ns,
};
@ -84,7 +87,8 @@ class App extends Component {
render() {
return (
<TokenCtx.Provider value={{
<TokenCtx.Provider
value={{
value: this.state.token,
set_value: (x) => {
localStorage['TOKEN'] = x || '';
@ -92,37 +96,53 @@ class App extends Component {
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)=>(
<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 &&
{!token.value && (
<div className="flow-item-row aux-margin">
<div className="box box-tip">
<p>
<LoginPopup token_callback={token.set_value}>{(do_popup)=>(
<LoginPopup token_callback={token.set_value}>
{(do_popup) => (
<a onClick={do_popup}>
<span className="icon icon-login" />
&nbsp;登录到 T大树洞
</a>
)}</LoginPopup>
)}
</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}
/> :
)}
{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.Consumer>
<Sidebar
show_sidebar={this.show_sidebar_bound}
stack={this.state.sidebar_stack}
/>
</TokenCtx.Provider>
);
}

39
src/AudioWidget.js

@ -5,14 +5,11 @@ window.audio_cache={};
function load_amrnb() {
return new Promise((resolve, reject) => {
if(window.AMR)
resolve();
if (window.AMR) resolve();
else
load('static/amr_all.min.js', (err) => {
if(err)
reject(err);
else
resolve();
if (err) reject(err);
else resolve();
});
});
}
@ -40,11 +37,7 @@ export class AudioWidget extends Component {
this.setState({
state: 'loading',
});
Promise.all([
fetch(this.state.url),
load_amrnb(),
])
.then((res)=>{
Promise.all([fetch(this.state.url), load_amrnb()]).then((res) => {
res[0].blob().then((blob) => {
const reader = new FileReader();
reader.onload = (event) => {
@ -57,13 +50,15 @@ export class AudioWidget extends Component {
sampleRate: 8000,
channelCount: 1,
bytesPerSample: 2,
data: raw
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(
new Blob([binary_wave], { type: 'audio/wav' }),
);
window.audio_cache[this.state.url] = objurl;
this.setState({
state: 'loaded',
@ -80,12 +75,18 @@ export class AudioWidget extends Component {
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>);
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>);
return (
<p>
<audio src={this.state.data} controls />
</p>
);
}
}

265
src/Common.js

@ -2,12 +2,18 @@ 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 };
@ -19,19 +25,32 @@ function escape_regex(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;
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={{
<span
className="colored-span"
style={{
'--coloredspan-bgcolor-light': props.colors[0],
'--coloredspan-bgcolor-dark': props.colors[1],
}}>{props.children}</span>
)
}}
>
{props.children}
</span>
);
}
function normalize_url(url) {
return /^https?:\/\//.test(url) ? url : 'http://' + url;
}
@ -43,107 +62,172 @@ export class HighlightedText extends PureComponent {
{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> :
<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>
)}
</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 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>)
}
return <div key={index}>[图片]</div>;
},
},
{
shouldProcessNode: (node) => (/^h[123456]$/.test(node.name)),
shouldProcessNode: (node) => /^h[123456]$/.test(node.name),
processNode(node, children, index) {
let currentLevel = +(node.name[1])
let currentLevel = +node.name[1];
if (currentLevel < 3) currentLevel = 3;
const HeadingTag = `h${currentLevel}`
return (
<HeadingTag key={index}>{children}</HeadingTag>
)
}
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}>
<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
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 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">
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>)
</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()
processNode: processDefs.processDefaultNode,
},
];
const parser = new HtmlToReact.Parser();
if (props.author && props.text.match(/^(?:#+ |>|```|\t|\s*-|\s*\d+\.)/)) {
const renderedMarkdown = renderMd(props.text)
const renderedMarkdown = renderMd(props.text);
return (
<>
{props.author}
{parser.parseWithInstructions(renderedMarkdown, node => node.type !== 'script', processInstructions) || ''}
{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)
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
);
}
}
}
@ -165,11 +249,14 @@ export class SafeTextarea extends Component {
}
componentDidMount() {
this.setState({
text: window.TEXTAREA_BACKUP[this.props.id]||''
},()=>{
this.setState(
{
text: window.TEXTAREA_BACKUP[this.props.id] || '',
},
() => {
this.change_callback(this.state.text);
});
},
);
}
componentWillUnmount() {
@ -210,8 +297,13 @@ export class SafeTextarea extends Component {
render() {
return (
<textarea ref={this.area_ref} onChange={this.on_change_bound} value={this.state.text} onKeyDown={this.on_keydown_bound} />
)
<textarea
ref={this.area_ref}
onChange={this.on_change_bound}
value={this.state.text}
onKeyDown={this.on_keydown_bound}
/>
);
}
}
@ -223,28 +315,36 @@ window.addEventListener('beforeinstallprompt', (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_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>
<span className="icon icon-about" />
&nbsp; Safari 把树洞 <b>添加到主屏幕</b>
</div>
) : null;
else
// 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>
<span className="icon icon-about" />
&nbsp; 把网页版树洞{' '}
<b>
<a
onClick={() => {
if (pwa_prompt_event) pwa_prompt_event.prompt();
}}
>
安装到桌面
</a>
</b>{' '}
更好用
</div>
) : null;
}
@ -275,7 +375,9 @@ export class ClickHandler extends PureComponent {
}
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);
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({
@ -285,8 +387,7 @@ export class ClickHandler extends PureComponent {
}
on_end(event) {
//console.log('end');
if(!this.state.moved)
this.do_callback(event);
if (!this.state.moved) this.do_callback(event);
this.setState({
moved: true,
});
@ -300,11 +401,15 @@ export class ClickHandler extends PureComponent {
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} >
<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>
)
);
}
}

172
src/Config.js

@ -3,22 +3,30 @@ 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': '梦开始的地方',
'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_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
fold: true,
};
export function load_config() {
@ -34,8 +42,7 @@ export function load_config() {
// unrecognized configs are removed
Object.keys(loaded_config).forEach((key) => {
if(config[key]!==undefined)
config[key]=loaded_config[key];
if (config[key] !== undefined) config[key] = loaded_config[key];
});
console.log('config loaded', config);
@ -75,45 +82,71 @@ class ConfigBackground extends PureComponent {
on_select(e) {
let value = e.target.value;
this.setState({
img: value==='##other' ? '' :
value==='##color' ? null : value,
},this.save_changes.bind(this));
this.setState(
{
img: value === '##other' ? '' : value === '##color' ? null : value,
},
this.save_changes.bind(this),
);
}
on_change_img(e) {
this.setState({
this.setState(
{
img: e.target.value,
},this.save_changes.bind(this));
},
this.save_changes.bind(this),
);
}
on_change_color(e) {
this.setState({
this.setState(
{
color: e.target.value,
},this.save_changes.bind(this));
},
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;
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 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)} />
}
{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
className="bg-preview"
style={bgimg_style(this.state.img, this.state.color)}
/>
</div>
);
}
@ -135,9 +168,12 @@ class ConfigColorScheme extends PureComponent {
on_select(e) {
let value = e.target.value;
this.setState({
this.setState(
{
color_scheme: value,
},this.save_changes.bind(this));
},
this.save_changes.bind(this),
);
}
render() {
@ -145,18 +181,19 @@ class ConfigColorScheme extends PureComponent {
<div>
<p>
<b>夜间模式</b>
<select value={this.state.color_scheme} onChange={this.on_select.bind(this)}>
<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>
<p>选择浅色或深色模式深色模式下将会调暗图片亮度</p>
</div>
)
);
}
}
@ -170,13 +207,16 @@ class ConfigSwitch extends PureComponent {
on_change(e) {
let val = e.target.checked;
this.setState({
this.setState(
{
switch: val,
},()=>{
},
() => {
this.props.callback({
[this.props.id]: val,
});
});
},
);
}
render() {
@ -184,14 +224,17 @@ class ConfigSwitch extends PureComponent {
<div>
<p>
<label>
<input name={'config-'+this.props.id} type="checkbox" checked={this.state.switch} onChange={this.on_change.bind(this)} />
<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>
<p>{this.props.description}</p>
</div>
);
}
@ -223,33 +266,62 @@ export class ConfigUI extends PureComponent {
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>
<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="快速返回"
<ConfigSwitch
callback={this.save_changes_bound}
id="pressure"
name="快速返回"
description="短暂按住 Esc 键或重压屏幕(3D Touch)可以快速返回或者刷新树洞"
/>
<hr />
<ConfigSwitch callback={this.save_changes_bound} id="easter_egg" name="允许彩蛋"
<ConfigSwitch
callback={this.save_changes_bound}
id="easter_egg"
name="允许彩蛋"
description="在某些情况下显示彩蛋"
/>
<hr />
<ConfigSwitch callback={this.save_changes_bound} id="fold" name="折叠树洞"
<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>
<a
href="https://github.com/thuhole/thuhole-go-backend/issues"
target="_blank"
>
GitHub <span className="icon icon-github" />
</a>
&nbsp;提出
</p>
</div>
</div>
)
);
}
}

775
src/Flows.js

File diff suppressed because it is too large Load Diff

32
src/Markdown.js

@ -1,10 +1,10 @@
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,
@ -14,16 +14,20 @@ let md = new MarkdownIt({
highlight(str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return '<pre class="hljs"><code>' +
return (
'<pre class="hljs"><code>' +
hljs.highlight(lang, str, true).value +
'</code></pre>';
'</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);

37
src/Message.js

@ -17,14 +17,20 @@ export class MessageViewer extends PureComponent {
load() {
if (this.state.loading_status === 'loading') return;
this.setState({
this.setState(
{
loading_status: 'loading',
},()=>{
fetch(THUHOLE_API_ROOT+'api_xmcp/hole/system_msg?user_token='+encodeURIComponent(this.props.token)+API_VERSION_PARAM())
},
() => {
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);
if (json.error) throw new Error(json.error);
else
this.setState({
loading_status: 'done',
@ -37,16 +43,26 @@ export class MessageViewer extends PureComponent {
this.setState({
loading_status: 'failed',
});
})
});
},
);
}
render() {
if (this.state.loading_status === 'loading')
return (<p className="box box-tip">加载中</p>);
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>);
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">
@ -59,7 +75,6 @@ export class MessageViewer extends PureComponent {
</div>
</div>
));
else
return null;
else return null;
}
}

51
src/PressureHelper.js

@ -3,7 +3,7 @@ import Pressure from 'pressure';
import './PressureHelper.css';
const THRESHOLD=.4;
const THRESHOLD = 0.4;
const MULTIPLIER = 25;
const BORDER_WIDTH = 500; // also change css!
@ -38,13 +38,14 @@ export class PressureHelper extends Component {
componentDidMount() {
if (window.config.pressure) {
Pressure.set(document.body, {
Pressure.set(
document.body,
{
change: (force) => {
if (!this.state.fired) {
if(force>=.999) {
if (force >= 0.999) {
this.do_fire();
}
else
} else
this.setState({
level: force,
});
@ -56,29 +57,32 @@ export class PressureHelper extends Component {
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({
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();
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) => {
@ -98,16 +102,19 @@ export class PressureHelper extends Component {
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={{
<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,
}} />
)
}}
/>
);
}
}

39
src/Sidebar.js

@ -12,8 +12,7 @@ export class Sidebar extends PureComponent {
componentDidUpdate(nextProps) {
if (this.props.stack !== nextProps.stack) {
//console.log('sidebar top');
if(this.sidebar_ref.current)
this.sidebar_ref.current.scrollTop=0;
if (this.sidebar_ref.current) this.sidebar_ref.current.scrollTop = 0;
}
}
@ -25,18 +24,40 @@ export class Sidebar extends PureComponent {
}
render() {
let [cur_title,cur_content]=this.props.stack[this.props.stack.length-1];
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
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>
}
<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>

105
src/Title.js

@ -26,11 +26,14 @@ class ControlBar extends PureComponent {
let text = decodeURIComponent(window.location.hash).substr(1);
if (text.lastIndexOf('?') !== -1)
text = text.substr(0, text.lastIndexOf('?')); // fuck wechat '#param?nsukey=...'
this.setState({
this.setState(
{
search_text: text,
}, ()=>{
},
() => {
this.on_keypress({ key: 'Enter' });
});
},
);
}
}
@ -46,10 +49,20 @@ class ControlBar extends PureComponent {
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.');
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.');
alert(
'Clear Flag ' +
flag_res[1] +
'\nYou may need to refresh this webpage.',
);
}
return;
}
@ -77,47 +90,70 @@ class ControlBar extends PureComponent {
render() {
return (
<TokenCtx.Consumer>{({value: token})=>(
<TokenCtx.Consumer>
{({ value: token }) => (
<div className="control-bar">
<a className="no-underline control-btn" onClick={this.do_refresh_bound}>
<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}>
{!!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}
)}
<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={()=>{
<a
className="no-underline control-btn"
onClick={() => {
this.props.show_sidebar(
'T大树洞',
<InfoSidebar show_sidebar={this.props.show_sidebar} />
)
}}>
<InfoSidebar show_sidebar={this.props.show_sidebar} />,
);
}}
>
<span className={'icon icon-' + (token ? 'about' : 'login')} />
<span className="control-btn-label">{token ? '账户' : '登录'}</span>
<span className="control-btn-label">
{token ? '账户' : '登录'}
</span>
</a>
{!!token &&
<a className="no-underline control-btn" onClick={()=>{
{!!token && (
<a
className="no-underline control-btn"
onClick={() => {
this.props.show_sidebar(
'发表树洞',
<PostForm token={token} on_complete={()=>{
<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>
)
)}
</TokenCtx.Consumer>
);
}
}
@ -128,16 +164,23 @@ export function Title(props) {
<div className="aux-margin">
<div className="title">
<p className="centered-line">
<span onClick={()=>props.show_sidebar(
<span
onClick={() =>
props.show_sidebar(
'T大树洞',
<InfoSidebar show_sidebar={props.show_sidebar} />
)}>
<InfoSidebar show_sidebar={props.show_sidebar} />,
)
}
>
T大树洞
</span>
</p>
</div>
<ControlBar show_sidebar={props.show_sidebar} set_mode={props.set_mode} />
<ControlBar
show_sidebar={props.show_sidebar}
set_mode={props.set_mode}
/>
</div>
</div>
)
);
}

433
src/UserAction.js

@ -1,5 +1,10 @@
import React, { Component, PureComponent } from 'react';
import {API_BASE, SafeTextarea, PromotionBar, HighlightedMarkdown} from './Common';
import {
API_BASE,
SafeTextarea,
PromotionBar,
HighlightedMarkdown,
} from './Common';
import { MessageViewer } from './Message';
import { LoginPopup } from './infrastructure/widgets';
import { ColorPicker } from './color_picker';
@ -7,7 +12,13 @@ import {ConfigUI} from './Config';
import fixOrientation from 'fix-orientation';
import copy from 'copy-to-clipboard';
import { cache } from './cache';
import {API_VERSION_PARAM, THUHOLE_API_ROOT, API, get_json, token_param} from './flows_api';
import {
API_VERSION_PARAM,
THUHOLE_API_ROOT,
API,
get_json,
token_param,
} from './flows_api';
import './UserAction.css';
@ -201,26 +212,35 @@ export function InfoSidebar(props) {
<PromotionBar />
<LoginForm show_sidebar={props.show_sidebar} />
<div className="box list-menu">
<a onClick={()=>{props.show_sidebar(
'设置',
<ConfigUI />
)}}>
<span className="icon icon-settings" /><label>设置</label>
<a
onClick={() => {
props.show_sidebar('设置', <ConfigUI />);
}}
>
<span className="icon icon-settings" />
<label>设置</label>
</a>
&nbsp;&nbsp;
<a href="https://thuhole.com/policy.html" target="_blank">
<span className="icon icon-textfile" /><label>树洞规范试行</label>
<span className="icon icon-textfile" />
<label>树洞规范试行</label>
</a>
&nbsp;&nbsp;
<a href="https://github.com/thuhole/thuhole-go-backend/issues" target="_blank">
<span className="icon icon-github" /><label>意见反馈</label>
<a
href="https://github.com/thuhole/thuhole-go-backend/issues"
target="_blank"
>
<span className="icon icon-github" />
<label>意见反馈</label>
</a>
</div>
<div className="box help-desc-box">
<p>
<a onClick={()=>{
<a
onClick={() => {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.getRegistrations()
navigator.serviceWorker
.getRegistrations()
.then((registrations) => {
for (let registration of registrations) {
console.log('unregister', registration);
@ -232,42 +252,67 @@ export function InfoSidebar(props) {
setTimeout(() => {
window.location.reload(true);
}, 200);
}}>强制检查更新</a>
当前版本{process.env.REACT_APP_BUILD_INFO||'---'} {process.env.NODE_ENV} 会自动在后台检查更新并在下次访问时更新
}}
>
强制检查更新
</a>
当前版本{process.env.REACT_APP_BUILD_INFO || '---'}{' '}
{process.env.NODE_ENV} 会自动在后台检查更新并在下次访问时更新
</p>
</div>
<div className="box help-desc-box">
<p>
联系我们thuhole at protonmail dot com
</p>
<p>联系我们thuhole at protonmail dot com</p>
</div>
<div className="box help-desc-box">
<p>
T大树洞 网页版 by @thuhole
基于&nbsp;
<a href="https://www.gnu.org/licenses/gpl-3.0.zh-cn.html" target="_blank">GPLv3</a>
&nbsp;协议在 <a href="https://github.com/thuhole/webhole" target="_blank">GitHub</a>
T大树洞 网页版 by @thuhole 基于&nbsp;
<a
href="https://www.gnu.org/licenses/gpl-3.0.zh-cn.html"
target="_blank"
>
GPLv3
</a>
&nbsp;协议在{' '}
<a href="https://github.com/thuhole/webhole" target="_blank">
GitHub
</a>{' '}
开源
</p>
<p>
T大树洞 网页版的诞生离不开&nbsp;
<a href="https://github.com/pkuhelper-web/webhole" target="_blank" rel="noopener">P大树洞网页版 by @xmcp</a>
<a
href="https://github.com/pkuhelper-web/webhole"
target="_blank"
rel="noopener"
>
P大树洞网页版 by @xmcp
</a>
<a href="https://reactjs.org/" target="_blank" rel="noopener">React</a>
<a href="https://reactjs.org/" target="_blank" rel="noopener">
React
</a>
<a href="https://icomoon.io/#icons" target="_blank" rel="noopener">IcoMoon</a>
<a href="https://icomoon.io/#icons" target="_blank" rel="noopener">
IcoMoon
</a>
&nbsp;等开源项目
</p>
<p>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
the Free Software Foundation, either version 3 of the License, or (at
your option) any later version.
</p>
<p>
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&nbsp;
<a href="https://www.gnu.org/licenses/gpl-3.0.zh-cn.html" target="_blank">GNU General Public License</a>
<a
href="https://www.gnu.org/licenses/gpl-3.0.zh-cn.html"
target="_blank"
>
GNU General Public License
</a>
&nbsp;for more details.
</p>
</div>
@ -284,12 +329,20 @@ class ResetUsertokenWidget extends Component {
}
do_reset() {
if(window.confirm('您正在重置 UserToken!\n您的账号将会在【所有设备】上注销,您需要手动重新登录!')) {
let uid=window.prompt('您正在重置 UserToken!\n请输入您的学号以确认身份:');
if (
window.confirm(
'您正在重置 UserToken!\n您的账号将会在【所有设备】上注销,您需要手动重新登录!',
)
) {
let uid = window.prompt(
'您正在重置 UserToken!\n请输入您的学号以确认身份:',
);
if (uid)
this.setState({
this.setState(
{
loading_status: 'loading',
},()=>{
},
() => {
fetch(THUHOLE_API_ROOT + 'api_xmcp/hole/reset_usertoken', {
method: 'post',
headers: {
@ -302,10 +355,8 @@ class ResetUsertokenWidget extends Component {
})
.then(get_json)
.then((json) => {
if(json.error)
throw new Error(json.error);
else
alert('重置成功!您需要在所有设备上重新登录。');
if (json.error) throw new Error(json.error);
else alert('重置成功!您需要在所有设备上重新登录。');
this.setState({
loading_status: 'done',
@ -316,38 +367,48 @@ class ResetUsertokenWidget extends Component {
this.setState({
loading_status: 'done',
});
})
});
},
);
}
}
render() {
if (this.state.loading_status === 'done')
return (<a onClick={this.do_reset.bind(this)}>重置</a>);
return <a onClick={this.do_reset.bind(this)}>重置</a>;
else if (this.state.loading_status === 'loading')
return (<a><span className="icon icon-loading" /></a>);
return (
<a>
<span className="icon icon-loading" />
</a>
);
}
}
export class LoginForm extends Component {
copy_token(token) {
if(copy(token))
alert('复制成功!\n请一定不要泄露哦');
if (copy(token)) alert('复制成功!\n请一定不要泄露哦');
}
render() {
return (
<TokenCtx.Consumer>{(token)=>
<TokenCtx.Consumer>
{(token) => (
<div>
{/*{!!token.value &&*/}
{/* <LifeInfoBox token={token.value} set_token={token.set_value} />*/}
{/*}*/}
<div className="login-form box">
{token.value ?
{token.value ? (
<div>
<p>
<b>您已登录</b>
<button type="button" onClick={()=>{token.set_value(null);}}>
<button
type="button"
onClick={() => {
token.set_value(null);
}}
>
<span className="icon icon-logout" /> 注销
</button>
<br />
@ -357,18 +418,32 @@ export class LoginForm extends Component {
{/*T大树洞将会单向加密(i.e. 哈希散列)您的邮箱后再存入数据库,因此您的发帖具有较强的匿名性。具体可见我们的<a href="https://github.com/thuhole/thuhole-go-backend/blob/76f56e6b75257b59e552b6bdba77e114151fcad1/src/db.go#L184">后端开源代码</a>。*/}
{/*</p>*/}
<p>
<a onClick={()=>{this.props.show_sidebar(
<a
onClick={() => {
this.props.show_sidebar(
'系统消息',
<MessageViewer token={token.value} />
)}}>查看系统消息</a><br />
<MessageViewer token={token.value} />,
);
}}
>
查看系统消息
</a>
<br />
当您发送的内容违规时我们将用系统消息提示您
</p>
<p>
<a onClick={this.copy_token.bind(this,token.value)}>复制 User Token</a><br />
复制 User Token 可以在新设备登录切勿告知他人若怀疑被盗号请重新邮箱验证码登录以重置Token{/*,若怀疑被盗号请尽快 <ResetUsertokenWidget token={token.value} />*/}
<a onClick={this.copy_token.bind(this, token.value)}>
复制 User Token
</a>
<br />
复制 User Token
可以在新设备登录切勿告知他人若怀疑被盗号请重新邮箱验证码登录以重置Token
{/*,若怀疑被盗号请尽快 <ResetUsertokenWidget token={token.value} />*/}
</p>
</div> :
<LoginPopup token_callback={token.set_value}>{(do_popup)=>(
</div>
) : (
<LoginPopup token_callback={token.set_value}>
{(do_popup) => (
<div>
<p>
<button type="button" onClick={do_popup}>
@ -376,16 +451,21 @@ export class LoginForm extends Component {
&nbsp;登录
</button>
</p>
<p><small>
T大树洞 面向T大学生通过T大邮箱验证您的身份并提供服务
</small></p>
<p>
<small>
T大树洞
面向T大学生通过T大邮箱验证您的身份并提供服务
</small>
</p>
</div>
)}</LoginPopup>
}
)}
</LoginPopup>
)}
</div>
</div>
}</TokenCtx.Consumer>
)
)}
</TokenCtx.Consumer>
);
}
}
@ -399,12 +479,19 @@ export class ReplyForm extends Component {
};
this.on_change_bound = this.on_change.bind(this);
this.area_ref = this.props.area_ref || React.createRef();
this.global_keypress_handler_bound=this.global_keypress_handler.bind(this);
this.global_keypress_handler_bound = this.global_keypress_handler.bind(
this,
);
this.color_picker = new ColorPicker();
}
global_keypress_handler(e) {
if(e.code==='Enter' && !e.ctrlKey && !e.altKey && ['input','textarea'].indexOf(e.target.tagName.toLowerCase())===-1) {
if (
e.code === 'Enter' &&
!e.ctrlKey &&
!e.altKey &&
['input', 'textarea'].indexOf(e.target.tagName.toLowerCase()) === -1
) {
if (this.area_ref.current) {
e.preventDefault();
this.area_ref.current.focus();
@ -415,7 +502,10 @@ export class ReplyForm extends Component {
document.addEventListener('keypress', this.global_keypress_handler_bound);
}
componentWillUnmount() {
document.removeEventListener('keypress',this.global_keypress_handler_bound);
document.removeEventListener(
'keypress',
this.global_keypress_handler_bound,
);
}
on_change(value) {
@ -426,8 +516,7 @@ export class ReplyForm extends Component {
on_submit(event) {
if (event) event.preventDefault();
if(this.state.loading_status==='loading')
return;
if (this.state.loading_status === 'loading') return;
this.setState({
loading_status: 'loading',
});
@ -436,13 +525,16 @@ export class ReplyForm extends Component {
data.append('pid', this.props.pid);
data.append('text', this.state.text);
data.append('user_token', this.props.token);
fetch(API_BASE+'/api.php?action=docomment'+token_param(this.props.token), {
fetch(
API_BASE + '/api.php?action=docomment' + token_param(this.props.token),
{
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: data,
})
},
)
.then(get_json)
.then((json) => {
if (json.code !== 0) {
@ -469,33 +561,55 @@ export class ReplyForm extends Component {
toggle_preview() {
this.setState({
preview: !this.state.preview
preview: !this.state.preview,
});
}
render() {
return (
<form onSubmit={this.on_submit.bind(this)} className={'reply-form box'+(this.state.text?' reply-sticky':'')}>
{
this.state.preview ?
<div className='reply-preview'>
<HighlightedMarkdown text={this.state.text} color_picker={this.color_picker} show_pid={()=>{}} />
</div> :
<SafeTextarea ref={this.area_ref} id={this.props.pid} on_change={this.on_change_bound} on_submit={this.on_submit.bind(this)} />
}
<button type='button' onClick={()=>{this.toggle_preview()}}>
{this.state.preview? <span className="icon icon-eye-blocked" />: <span className="icon icon-eye" />}
<form
onSubmit={this.on_submit.bind(this)}
className={'reply-form box' + (this.state.text ? ' reply-sticky' : '')}
>
{this.state.preview ? (
<div className="reply-preview">
<HighlightedMarkdown
text={this.state.text}
color_picker={this.color_picker}
show_pid={() => {}}
/>
</div>
) : (
<SafeTextarea
ref={this.area_ref}
id={this.props.pid}
on_change={this.on_change_bound}
on_submit={this.on_submit.bind(this)}
/>
)}
<button
type="button"
onClick={() => {
this.toggle_preview();
}}
>
{this.state.preview ? (
<span className="icon icon-eye-blocked" />
) : (
<span className="icon icon-eye" />
)}
</button>
{this.state.loading_status==='loading' ?
{this.state.loading_status === 'loading' ? (
<button disabled="disabled">
<span className="icon icon-loading" />
</button> :
</button>
) : (
<button type="submit">
<span className="icon icon-send" />
</button>
}
)}
</form>
)
);
}
}
@ -516,8 +630,7 @@ export class PostForm extends Component {
}
componentDidMount() {
if(this.area_ref.current)
this.area_ref.current.focus();
if (this.area_ref.current) this.area_ref.current.focus();
}
on_change(value) {
@ -531,8 +644,7 @@ export class PostForm extends Component {
data.append('text', this.state.text);
data.append('type', img ? 'image' : 'text');
data.append('user_token', this.props.token);
if(img)
data.append('data',img);
if (img) data.append('data', img);
fetch(API_BASE + '/api.php?action=dopost' + token_param(this.props.token), {
method: 'POST',
@ -569,8 +681,7 @@ export class PostForm extends Component {
return new Promise((resolve, reject) => {
function return_url(url) {
const idx = url.indexOf(';base64,');
if(idx===-1)
throw new Error('img not base64 encoded');
if (idx === -1) throw new Error('img not base64 encoded');
return url.substr(idx + 8);
}
@ -578,23 +689,23 @@ export class PostForm extends Component {
let reader = new FileReader();
function on_got_img(url) {
const image = new Image();
image.onload=(()=>{
image.onload = () => {
let width = image.width;
let height = image.height;
let compressed = false;
if (width > MAX_IMG_DIAM) {
height=height*MAX_IMG_DIAM/width;
height = (height * MAX_IMG_DIAM) / width;
width = MAX_IMG_DIAM;
compressed = true;
}
if (height > MAX_IMG_DIAM) {
width=width*MAX_IMG_DIAM/height;
width = (width * MAX_IMG_DIAM) / height;
height = MAX_IMG_DIAM;
compressed = true;
}
if (height * width > MAX_IMG_PX) {
let rate=Math.sqrt(height*width/MAX_IMG_PX);
let rate = Math.sqrt((height * width) / MAX_IMG_PX);
height /= rate;
width /= rate;
compressed = true;
@ -607,17 +718,25 @@ export class PostForm extends Component {
canvas.height = height;
ctx.drawImage(image, 0, 0, width, height);
let quality_l=.1,quality_r=.9,quality,new_url;
while(quality_r-quality_l>=.03) {
let quality_l = 0.1,
quality_r = 0.9,
quality,
new_url;
while (quality_r - quality_l >= 0.03) {
quality = (quality_r + quality_l) / 2;
new_url = canvas.toDataURL('image/jpeg', quality);
console.log(quality_l,quality_r,'trying quality',quality,'size',new_url.length);
if(new_url.length<=MAX_IMG_FILESIZE)
quality_l=quality;
else
quality_r=quality;
console.log(
quality_l,
quality_r,
'trying quality',
quality,
'size',
new_url.length,
);
if (new_url.length <= MAX_IMG_FILESIZE) quality_l = quality;
else quality_r = quality;
}
if(quality_l>=.101) {
if (quality_l >= 0.101) {
console.log('chosen img quality', quality);
resolve({
img: return_url(new_url),
@ -629,7 +748,7 @@ export class PostForm extends Component {
} else {
reject('图片过大,无法上传');
}
});
};
image.src = url;
}
reader.onload = (event) => {
@ -643,14 +762,21 @@ export class PostForm extends Component {
on_img_change() {
if (this.img_ref.current && this.img_ref.current.files.length)
this.setState({
img_tip: '(正在处理图片……)'
},()=>{
this.setState(
{
img_tip: '(正在处理图片……)',
},
() => {
this.proc_img(this.img_ref.current.files[0])
.then((d) => {
this.setState({
img_tip: `${d.compressed?'压缩到':'尺寸'} ${d.width}*${d.height} / `+
`质量 ${Math.floor(d.quality*100)}% / ${Math.floor(d.img.length/BASE64_RATE/1000)}KB)`,
img_tip:
`${d.compressed ? '压缩到' : '尺寸'} ${d.width}*${
d.height
} / ` +
`质量 ${Math.floor(d.quality * 100)}% / ${Math.floor(
d.img.length / BASE64_RATE / 1000,
)}KB`,
});
})
.catch((e) => {
@ -658,7 +784,8 @@ export class PostForm extends Component {
img_tip: `图片无效:${e}`,
});
});
});
},
);
else
this.setState({
img_tip: null,
@ -667,8 +794,7 @@ export class PostForm extends Component {
on_submit(event) {
if (event) event.preventDefault();
if(this.state.loading_status==='loading')
return;
if (this.state.loading_status === 'loading') return;
if (this.img_ref.current.files.length) {
this.setState({
loading_status: 'processing',
@ -693,7 +819,7 @@ export class PostForm extends Component {
toggle_preview() {
this.setState({
preview: !this.state.preview
preview: !this.state.preview,
});
}
@ -703,52 +829,89 @@ export class PostForm extends Component {
<div className="post-form-bar">
<label>
图片
<input ref={this.img_ref} type="file" accept="image/*" disabled={this.state.loading_status!=='done'}
<input
ref={this.img_ref}
type="file"
accept="image/*"
disabled={this.state.loading_status !== 'done'}
onChange={this.on_img_change_bound}
/>
</label>
{
this.state.preview ?
<button type='button' onClick={()=>{this.toggle_preview()}}>
{this.state.preview ? (
<button
type="button"
onClick={() => {
this.toggle_preview();
}}
>
<span className="icon icon-eye-blocked" />
&nbsp;编辑
</button> :
<button type='button' onClick={()=>{this.toggle_preview()}}>
</button>
) : (
<button
type="button"
onClick={() => {
this.toggle_preview();
}}
>
<span className="icon icon-eye" />
&nbsp;预览
</button>
}
)}
{
this.state.loading_status!=='done' ?
{this.state.loading_status !== 'done' ? (
<button disabled="disabled">
<span className="icon icon-loading" />
&nbsp;{this.state.loading_status==='processing' ? '处理' : '上传'}
</button> :
&nbsp;
{this.state.loading_status === 'processing' ? '处理' : '上传'}
</button>
) : (
<button type="submit">
<span className="icon icon-send" />
&nbsp;发表
</button>
}
)}
</div>
{!!this.state.img_tip &&
{!!this.state.img_tip && (
<p className="post-form-img-tip">
<a onClick={()=>{this.img_ref.current.value=""; this.on_img_change();}}>删除图片</a>
<a
onClick={() => {
this.img_ref.current.value = '';
this.on_img_change();
}}
>
删除图片
</a>
{this.state.img_tip}
</p>
}
{
this.state.preview ?
<div className='post-preview'>
<HighlightedMarkdown text={this.state.text} color_picker={this.color_picker} show_pid={()=>{}} />
</div> :
<SafeTextarea ref={this.area_ref} id="new_post" on_change={this.on_change_bound} on_submit={this.on_submit.bind(this)} />
}
<p><small>
请遵守<a href="https://thuhole.com/policy.html" target="_blank">树洞管理规范试行</a>
</small></p>
)}
{this.state.preview ? (
<div className="post-preview">
<HighlightedMarkdown
text={this.state.text}
color_picker={this.color_picker}
show_pid={() => {}}
/>
</div>
) : (
<SafeTextarea
ref={this.area_ref}
id="new_post"
on_change={this.on_change_bound}
on_submit={this.on_submit.bind(this)}
/>
)}
<p>
<small>
请遵守
<a href="https://thuhole.com/policy.html" target="_blank">
树洞管理规范试行
</a>
文明发言
</small>
</p>
</form>
)
);
}
}

43
src/cache.js

@ -44,8 +44,7 @@ class Cache {
// use window.hole_cache.decrypt() only after cache is loaded!
decrypt(pid, s) {
let o = '';
if(typeof(s)!==typeof('str'))
return null;
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);
@ -65,8 +64,7 @@ class Cache {
get(pid, target_version) {
pid = parseInt(pid);
return new Promise((resolve, reject) => {
if(!this.db)
return resolve(null);
if (!this.db) return resolve(null);
const tx = this.db.transaction(['comment'], 'readwrite');
const store = tx.objectStore('comment');
const get_req = store.get(pid);
@ -75,14 +73,23 @@ class Cache {
if (!res || !res.data_str) {
//console.log('comment cache miss '+pid);
resolve(null);
} else if(target_version===res.version) { // hit
} else if (target_version === res.version) {
// hit
console.log('comment cache hit', pid);
res.last_access=(+new Date());
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);
} else {
// expired
console.log(
'comment cache expired',
pid,
': ver',
res.version,
'target',
target_version,
);
store.delete(pid);
resolve(null);
}
@ -98,8 +105,7 @@ class Cache {
put(pid, target_version, data) {
pid = parseInt(pid);
return new Promise((resolve, reject) => {
if(!this.db)
return resolve();
if (!this.db) return resolve();
const tx = this.db.transaction(['comment'], 'readwrite');
const store = tx.objectStore('comment');
store.put({
@ -116,8 +122,7 @@ class Cache {
delete(pid) {
pid = parseInt(pid);
return new Promise((resolve, reject) => {
if(!this.db)
return resolve();
if (!this.db) return resolve();
const tx = this.db.transaction(['comment'], 'readwrite');
const store = tx.objectStore('comment');
let req = store.delete(pid);
@ -131,8 +136,7 @@ class Cache {
}
maintenance() {
if(!this.db)
return;
if (!this.db) return;
const tx = this.db.transaction(['comment'], 'readwrite');
const store = tx.objectStore('comment');
let count_req = store.count();
@ -145,8 +149,7 @@ class Cache {
if (cur) {
//console.log('maintenance: delete',cur);
store.delete(cur.primaryKey);
if(--count>MAINTENANCE_COUNT)
cur.continue();
if (--count > MAINTENANCE_COUNT) cur.continue();
}
};
} else {
@ -158,15 +161,13 @@ class Cache {
}
clear() {
if(!this.db)
return;
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();
if (!window.hole_cache) window.hole_cache = new Cache();
return window.hole_cache;
}

3
src/color_picker.js

@ -10,8 +10,7 @@ export class ColorPicker {
get(name) {
name = name.toLowerCase();
if(name==='洞主')
return ['hsl(0,0%,97%)','hsl(0,0%,16%)'];
if (name === '洞主') return ['hsl(0,0%,97%)', 'hsl(0,0%,16%)'];
if (!this.names[name]) {
this.current_h += golden_ratio_conjugate;

55
src/flows_api.js

@ -6,7 +6,7 @@ import {cache} from './cache';
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 };
@ -17,9 +17,11 @@ 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)
API_BASE +
'/api.php?action=getcomment' +
'&pid=' +
pid +
token_param(token),
)
.then(get_json)
.then((json) => {
@ -28,7 +30,9 @@ export const API={
else throw new Error(JSON.stringify(json));
}
cache().delete(pid).then(()=>{
cache()
.delete(pid)
.then(() => {
cache().put(pid, cache_version, json);
});
@ -49,7 +53,8 @@ export const API={
load_replies_with_cache: (pid, token, color_picker, cache_version) => {
pid = parseInt(pid);
return cache().get(pid,cache_version)
return cache()
.get(pid, cache_version)
.then((json) => {
if (json) {
// also change load_replies!
@ -64,9 +69,7 @@ export const API={
});
return json;
}
else
return API.load_replies(pid,token,color_picker,cache_version);
} else return API.load_replies(pid, token, color_picker, cache_version);
});
},
@ -86,8 +89,8 @@ export const API={
.then((json) => {
cache().delete(pid);
if (json.code !== 0) {
if(json.msg && json.msg==='已经关注过了') {}
else {
if (json.msg && json.msg === '已经关注过了') {
} else {
if (json.msg) alert(json.msg);
throw new Error(JSON.stringify(json));
}
@ -120,25 +123,26 @@ export const API={
get_list: (page, token) => {
return fetch(
API_BASE+'/api.php?action=getlist'+
'&p='+page+
token_param(token)
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));
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)
API_BASE +
'/api.php?action=search' +
'&pagesize=' +
SEARCH_PAGESIZE +
'&page=' +
page +
'&keywords=' +
encodeURIComponent(keyword) +
token_param(token),
)
.then(get_json)
.then((json) => {
@ -152,9 +156,7 @@ export const API={
get_single: (pid, token) => {
return fetch(
API_BASE+'/api.php?action=getone'+
'&pid='+pid+
token_param(token)
API_BASE + '/api.php?action=getone' + '&pid=' + pid + token_param(token),
)
.then(get_json)
.then((json) => {
@ -167,10 +169,7 @@ export const API={
},
get_attention: (token) => {
return fetch(
API_BASE+'/api.php?action=getattention'+
token_param(token)
)
return fetch(API_BASE + '/api.php?action=getattention' + token_param(token))
.then(get_json)
.then((json) => {
if (json.code !== 0) {

18
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() {
@ -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();
});
}

18
src/text_splitter.js

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

Loading…
Cancel
Save