- {!!props.is_quote &&
-
- }
-
- {!!window.LATEST_POST_ID && parseInt(props.info.pid,10)>window.LATEST_POST_ID &&
-
- }
-
- {!!this.props.do_filter_name &&
-
{this.props.do_filter_name(DZ_NAME);}}>
-
-
- }
- {!!parseInt(props.info.likenum,10) &&
-
- {props.info.likenum}
-
-
- }
- {!!parseInt(props.info.reply,10) &&
-
- {props.info.reply}
-
-
- }
-
#{props.info.pid}
-
- {(props.info.tag!==null && props.info.tag!=='折叠') &&
-
- {props.info.tag}
-
- }
-
-
-
- {!!(props.attention && props.info.variant.latest_reply) &&
-
最新回复
- }
-
+ render() {
+ let props = this.props;
+ return (
+
+ {!!props.is_quote && (
+
+ )}
+
+ {!!window.LATEST_POST_ID &&
+ parseInt(props.info.pid, 10) > window.LATEST_POST_ID && (
+
+ )}
+
+ {!!this.props.do_filter_name && (
+
{
+ this.props.do_filter_name(DZ_NAME);
+ }}
+ >
+
+
+ )}
+ {!!parseInt(props.info.likenum, 10) && (
+
+ {props.info.likenum}
+
+
+ )}
+ {!!parseInt(props.info.reply, 10) && (
+
+ {props.info.reply}
+
+
+ )}
+
+
+ #{props.info.pid}
+
+
+
+ {props.info.tag !== null && props.info.tag !== '折叠' && (
+
{props.info.tag}
+ )}
+
+
+
+ {!!(props.attention && props.info.variant.latest_reply) && (
+
+ 最新回复{' '}
+
+
+ )}
+
+
+ );
+ }
}
class FlowSidebar extends PureComponent {
- constructor(props) {
- super(props);
- this.state={
- attention: props.attention,
- info: props.info,
- replies: props.replies,
+ constructor(props) {
+ super(props);
+ this.state = {
+ attention: props.attention,
+ info: props.info,
+ replies: props.replies,
+ loading_status: 'done',
+ error_msg: null,
+ filter_name: null,
+ rev: false,
+ };
+ this.color_picker = props.color_picker;
+ this.syncState = props.sync_state || (() => {});
+ this.reply_ref = React.createRef();
+ }
+
+ set_variant(cid, variant) {
+ this.setState(
+ (prev) => {
+ if (cid)
+ return {
+ replies: prev.replies.map((reply) => {
+ if (reply.cid === cid)
+ return Object.assign({}, reply, {
+ variant: Object.assign({}, reply.variant, variant),
+ });
+ else return reply;
+ }),
+ };
+ else
+ return {
+ info: Object.assign({}, prev.info, {
+ variant: Object.assign({}, prev.info.variant, variant),
+ }),
+ };
+ },
+ function () {
+ this.syncState({
+ info: this.state.info,
+ replies: this.state.replies,
+ });
+ },
+ );
+ }
+
+ load_replies(update_count = true) {
+ this.setState({
+ loading_status: 'loading',
+ error_msg: null,
+ });
+ API.load_replies(
+ this.state.info.pid,
+ this.props.token,
+ this.color_picker,
+ null,
+ )
+ .then((json) => {
+ this.setState(
+ (prev, props) => ({
+ replies: json.data,
+ info: update_count
+ ? Object.assign({}, prev.info, {
+ reply: '' + json.data.length,
+ })
+ : prev.info,
+ attention: !!json.attention,
loading_status: 'done',
error_msg: null,
- filter_name: null,
- rev: false,
- };
- this.color_picker=props.color_picker;
- this.syncState=props.sync_state||(()=>{});
- this.reply_ref=React.createRef();
- }
-
- set_variant(cid,variant) {
- this.setState((prev)=>{
- if(cid)
- return {
- replies: prev.replies.map((reply)=>{
- if(reply.cid===cid)
- return Object.assign({},reply,{variant: Object.assign({},reply.variant,variant)});
- else
- return reply;
- }),
- };
- else
- return {
- info: Object.assign({},prev.info,{variant: Object.assign({},prev.info.variant,variant)}),
- }
- },function() {
+ }),
+ () => {
this.syncState({
- info: this.state.info,
- replies: this.state.replies,
+ replies: this.state.replies,
+ attention: this.state.attention,
+ info: this.state.info,
});
+ if (this.state.replies.length)
+ this.set_variant(null, {
+ latest_reply: Math.max.apply(
+ null,
+ this.state.replies.map((r) => parseInt(r.timestamp)),
+ ),
+ });
+ },
+ );
+ })
+ .catch((e) => {
+ console.error(e);
+ this.setState({
+ replies: [],
+ loading_status: 'done',
+ error_msg: '' + e,
});
- }
+ });
+ }
- load_replies(update_count=true) {
+ toggle_attention() {
+ this.setState({
+ loading_status: 'loading',
+ });
+ const next_attention = !this.state.attention;
+ API.set_attention(this.state.info.pid, next_attention, this.props.token)
+ .then((json) => {
this.setState({
- loading_status: 'loading',
- error_msg: null,
+ loading_status: 'done',
+ attention: next_attention,
});
- API.load_replies(this.state.info.pid,this.props.token,this.color_picker,null)
- .then((json)=>{
- this.setState((prev,props)=>({
- replies: json.data,
- info: update_count ? Object.assign({}, prev.info, {
- reply: ''+json.data.length,
- }) : prev.info,
- attention: !!json.attention,
- loading_status: 'done',
- error_msg: null,
- }), ()=>{
- this.syncState({
- replies: this.state.replies,
- attention: this.state.attention,
- info: this.state.info,
- });
- if(this.state.replies.length)
- this.set_variant(null,{latest_reply: Math.max.apply(null,this.state.replies.map((r)=>parseInt(r.timestamp)))});
- });
- })
- .catch((e)=>{
- console.error(e);
- this.setState({
- replies: [],
- loading_status: 'done',
- error_msg: ''+e,
- });
- });
- }
-
- toggle_attention() {
+ this.syncState({
+ attention: next_attention,
+ });
+ })
+ .catch((e) => {
this.setState({
- loading_status: 'loading',
+ loading_status: 'done',
});
- const next_attention=!this.state.attention;
- API.set_attention(this.state.info.pid,next_attention,this.props.token)
- .then((json)=>{
- this.setState({
- loading_status: 'done',
- attention: next_attention,
- });
- this.syncState({
- attention: next_attention,
- });
- })
- .catch((e)=>{
- this.setState({
- loading_status: 'done'
- });
- alert('设置关注失败');
- console.error(e);
- });
- }
+ alert('设置关注失败');
+ console.error(e);
+ });
+ }
- report() {
- let reason=prompt(`举报 #${this.state.info.pid} 的理由:`);
- if(reason!==null) {
- API.report(this.state.info.pid,reason,this.props.token)
- .then((json)=>{
- alert('举报成功');
- })
- .catch((e)=>{
- alert('举报失败');
- console.error(e);
- })
- }
+ report() {
+ let reason = prompt(`举报 #${this.state.info.pid} 的理由:`);
+ if (reason !== null) {
+ API.report(this.state.info.pid, reason, this.props.token)
+ .then((json) => {
+ alert('举报成功');
+ })
+ .catch((e) => {
+ alert('举报失败');
+ console.error(e);
+ });
}
+ }
- set_filter_name(name) {
- this.setState((prevState)=>({
- filter_name: name===prevState.filter_name ? null : name,
- }));
- }
+ set_filter_name(name) {
+ this.setState((prevState) => ({
+ filter_name: name === prevState.filter_name ? null : name,
+ }));
+ }
- toggle_rev() {
- this.setState((prevState)=>({
- rev: !prevState.rev,
- }));
- }
+ toggle_rev() {
+ this.setState((prevState) => ({
+ rev: !prevState.rev,
+ }));
+ }
- show_reply_bar(name,event) {
- if(this.reply_ref.current && !event.target.closest('a, .clickable')) {
- let text=this.reply_ref.current.get();
- if(/^\s*(?:Re (?:|洞主|(?:[A-Z][a-z]+ )?(?:[A-Z][a-z]+)|You Win(?: \d+)?):)?\s*$/.test(text)) {// text is nearly empty so we can replace it
- let should_text='Re '+name+': ';
- if(should_text===this.reply_ref.current.get())
- this.reply_ref.current.set('');
- else
- this.reply_ref.current.set(should_text);
- }
- }
+ show_reply_bar(name, event) {
+ if (this.reply_ref.current && !event.target.closest('a, .clickable')) {
+ let text = this.reply_ref.current.get();
+ if (
+ /^\s*(?:Re (?:|洞主|(?:[A-Z][a-z]+ )?(?:[A-Z][a-z]+)|You Win(?: \d+)?):)?\s*$/.test(
+ text,
+ )
+ ) {
+ // text is nearly empty so we can replace it
+ let should_text = 'Re ' + name + ': ';
+ if (should_text === this.reply_ref.current.get())
+ this.reply_ref.current.set('');
+ else this.reply_ref.current.set(should_text);
+ }
}
+ }
- render() {
- if(this.state.loading_status==='loading')
- return (
加载中……
);
+ render() {
+ if (this.state.loading_status === 'loading')
+ return
加载中……
;
- let show_pid=load_single_meta(this.props.show_sidebar,this.props.token);
+ let show_pid = load_single_meta(this.props.show_sidebar, this.props.token);
- let replies_to_show=this.state.filter_name ? this.state.replies.filter((r)=>r.name===this.state.filter_name) : this.state.replies.slice();
- if(this.state.rev) replies_to_show.reverse();
+ let replies_to_show = this.state.filter_name
+ ? this.state.replies.filter((r) => r.name === this.state.filter_name)
+ : this.state.replies.slice();
+ if (this.state.rev) replies_to_show.reverse();
- // key for lazyload elem
- let view_mode_key=(this.state.rev ? 'y-' : 'n-')+(this.state.filter_name||'null');
+ // key for lazyload elem
+ let view_mode_key =
+ (this.state.rev ? 'y-' : 'n-') + (this.state.filter_name || 'null');
- let replies_cnt={[DZ_NAME]:1};
- replies_to_show.forEach((r)=>{
- if(replies_cnt[r.name]===undefined)
- replies_cnt[r.name]=0;
- replies_cnt[r.name]++;
- });
+ let replies_cnt = { [DZ_NAME]: 1 };
+ replies_to_show.forEach((r) => {
+ if (replies_cnt[r.name] === undefined) replies_cnt[r.name] = 0;
+ replies_cnt[r.name]++;
+ });
- // hide main thread when filtered
- let main_thread_elem=(this.state.filter_name && this.state.filter_name!==DZ_NAME) ? null : (
-
{this.show_reply_bar('',e);}}>
- {this.set_variant(null,variant);}}
- do_filter_name={replies_cnt[DZ_NAME]>1 ? this.set_filter_name.bind(this) : null}
- />
-
- );
+ // hide main thread when filtered
+ let main_thread_elem =
+ this.state.filter_name && this.state.filter_name !== DZ_NAME ? null : (
+
{
+ this.show_reply_bar('', e);
+ }}
+ >
+ {
+ this.set_variant(null, variant);
+ }}
+ do_filter_name={
+ replies_cnt[DZ_NAME] > 1 ? this.set_filter_name.bind(this) : null
+ }
+ />
+
+ );
- return (
-
-
- {!!this.state.filter_name &&
-
- }
- {!this.state.rev &&
- main_thread_elem
- }
- {!!this.state.error_msg &&
-
-
回复加载失败
-
{this.state.error_msg}
-
- }
- {(this.props.deletion_detect && parseInt(this.state.info.reply)>this.state.replies.length) && !!this.state.replies.length &&
-
- {parseInt(this.state.info.reply)-this.state.replies.length} 条回复被删除
-
- }
- {replies_to_show.map((reply)=>(
-
- {this.show_reply_bar(reply.name,e);}}>
- {this.set_variant(reply.cid,variant);}}
- do_filter_name={replies_cnt[reply.name]>1 ? this.set_filter_name.bind(this) : null}
- />
-
-
- ))}
- {this.state.rev &&
- main_thread_elem
- }
- {!!this.props.token ?
-
:
-
登录后可以回复树洞
- }
+ return (
+
+
+ {!!this.state.filter_name && (
+
+ )}
+ {!this.state.rev && main_thread_elem}
+ {!!this.state.error_msg && (
+
+
回复加载失败
+
{this.state.error_msg}
+
+ )}
+ {this.props.deletion_detect &&
+ parseInt(this.state.info.reply) > this.state.replies.length &&
+ !!this.state.replies.length && (
+
+ {parseInt(this.state.info.reply) - this.state.replies.length}{' '}
+ 条回复被删除
- )
- }
+ )}
+ {replies_to_show.map((reply) => (
+
+ {
+ this.show_reply_bar(reply.name, e);
+ }}
+ >
+ {
+ this.set_variant(reply.cid, variant);
+ }}
+ do_filter_name={
+ replies_cnt[reply.name] > 1
+ ? this.set_filter_name.bind(this)
+ : null
+ }
+ />
+
+
+ ))}
+ {this.state.rev && main_thread_elem}
+ {!!this.props.token ? (
+
+ ) : (
+
登录后可以回复树洞
+ )}
+
+ );
+ }
}
class FlowItemRow extends PureComponent {
- constructor(props) {
- super(props);
- this.state={
- replies: [],
- reply_status: 'done',
- reply_error: null,
- info: Object.assign({},props.info,{variant: {}}),
- attention: props.attention_override===null ? false : props.attention_override,
- };
- this.color_picker=new ColorPicker();
- }
+ constructor(props) {
+ super(props);
+ this.state = {
+ replies: [],
+ reply_status: 'done',
+ reply_error: null,
+ info: Object.assign({}, props.info, { variant: {} }),
+ attention:
+ props.attention_override === null ? false : props.attention_override,
+ };
+ this.color_picker = new ColorPicker();
+ }
- componentDidMount() {
- if(parseInt(this.state.info.reply,10)) {
- this.load_replies(null,/*update_count=*/false);
- }
+ componentDidMount() {
+ if (parseInt(this.state.info.reply, 10)) {
+ this.load_replies(null, /*update_count=*/ false);
}
+ }
- load_replies(callback,update_count=true) {
- console.log('fetching reply',this.state.info.pid);
- this.setState({
- reply_status: 'loading',
+ load_replies(callback, update_count = true) {
+ console.log('fetching reply', this.state.info.pid);
+ this.setState({
+ reply_status: 'loading',
+ reply_error: null,
+ });
+ API.load_replies_with_cache(
+ this.state.info.pid,
+ this.props.token,
+ this.color_picker,
+ parseInt(this.state.info.reply),
+ )
+ .then((json) => {
+ this.setState(
+ (prev, props) => ({
+ replies: json.data,
+ info: Object.assign({}, prev.info, {
+ reply: update_count ? '' + json.data.length : prev.info.reply,
+ variant: json.data.length
+ ? {
+ latest_reply: Math.max.apply(
+ null,
+ json.data.map((r) => parseInt(r.timestamp)),
+ ),
+ }
+ : {},
+ }),
+ attention: !!json.attention,
+ reply_status: 'done',
reply_error: null,
- });
- API.load_replies_with_cache(this.state.info.pid,this.props.token,this.color_picker,parseInt(this.state.info.reply))
- .then((json)=>{
- this.setState((prev,props)=>({
- replies: json.data,
- info: Object.assign({}, prev.info, {
- reply: update_count ? ''+json.data.length : prev.info.reply,
- variant: json.data.length ? {
- latest_reply: Math.max.apply(null,json.data.map((r)=>parseInt(r.timestamp))),
- } : {},
- }),
- attention: !!json.attention,
- reply_status: 'done',
- reply_error: null,
- }),callback);
- })
- .catch((e)=>{
- console.error(e);
- this.setState({
- replies: [],
- reply_status: 'failed',
- reply_error: ''+e,
- },callback);
- });
- }
-
- show_sidebar() {
- this.props.show_sidebar(
- '树洞 #'+this.state.info.pid,
-
+ }),
+ callback,
);
- }
-
- render() {
- let show_pid=load_single_meta(this.props.show_sidebar,this.props.token,[this.state.info.pid]);
-
- let hl_rules=[
- ['url_pid',URL_PID_RE],
- ['url',URL_RE],
- ['pid',PID_RE],
- ['nickname',NICKNAME_RE],
- ];
- if(this.props.search_param)
- hl_rules.push(['search',build_highlight_re(this.props.search_param,' ','gi')]);
- let parts=split_text(this.state.info.text,hl_rules);
-
- let quote_id=null;
- if(!this.props.is_quote)
- for(let [mode,content] of parts) {
- content = content.length > 0 ? content.substring(1) : content
- if(mode==='pid' && QUOTE_BLACKLIST.indexOf(content)===-1 && parseInt(content)
-1) && (this.props.search_param === '热榜' || !this.props.search_param) && window.config.fold
-
- let res=(
- {
- if(!CLICKABLE_TAGS[event.target.tagName.toLowerCase()])
- this.show_sidebar();
- }}>
-
- {(!needFold) &&
- {this.state.reply_status==='loading' &&
加载中
}
- {this.state.reply_status==='failed' &&
-
- }
- {this.state.replies.slice(0,PREVIEW_REPLY_COUNT).map((reply)=>(
-
- ))}
- {this.state.replies.length>PREVIEW_REPLY_COUNT &&
-
还有 {this.state.replies.length-PREVIEW_REPLY_COUNT} 条
- }
-
}
-
+ })
+ .catch((e) => {
+ console.error(e);
+ this.setState(
+ {
+ replies: [],
+ reply_status: 'failed',
+ reply_error: '' + e,
+ },
+ callback,
);
+ });
+ }
- return ((!needFold) && quote_id) ? (
-
- {res}
-
-
- ) : res;
- }
+ show_sidebar() {
+ this.props.show_sidebar(
+ '树洞 #' + this.state.info.pid,
+ ,
+ );
+ }
+
+ render() {
+ let show_pid = load_single_meta(this.props.show_sidebar, this.props.token, [
+ this.state.info.pid,
+ ]);
+
+ let hl_rules = [
+ ['url_pid', URL_PID_RE],
+ ['url', URL_RE],
+ ['pid', PID_RE],
+ ['nickname', NICKNAME_RE],
+ ];
+ if (this.props.search_param)
+ hl_rules.push([
+ 'search',
+ build_highlight_re(this.props.search_param, ' ', 'gi'),
+ ]);
+ let parts = split_text(this.state.info.text, hl_rules);
+
+ let quote_id = null;
+ if (!this.props.is_quote)
+ for (let [mode, content] of parts) {
+ content = content.length > 0 ? content.substring(1) : content;
+ if (
+ mode === 'pid' &&
+ QUOTE_BLACKLIST.indexOf(content) === -1 &&
+ parseInt(content) < parseInt(this.state.info.pid)
+ )
+ if (quote_id === null) quote_id = parseInt(content);
+ else {
+ quote_id = null;
+ break;
+ }
+ }
+ let needFold =
+ FOLD_TAGS.indexOf(this.state.info.tag) > -1 &&
+ (this.props.search_param === '热榜' || !this.props.search_param) &&
+ window.config.fold;
+
+ let res = (
+ {
+ if (!CLICKABLE_TAGS[event.target.tagName.toLowerCase()])
+ this.show_sidebar();
+ }}
+ >
+
+ {!needFold && (
+
+ {this.state.reply_status === 'loading' && (
+
加载中
+ )}
+ {this.state.reply_status === 'failed' && (
+
+ )}
+ {this.state.replies.slice(0, PREVIEW_REPLY_COUNT).map((reply) => (
+
+ ))}
+ {this.state.replies.length > PREVIEW_REPLY_COUNT && (
+
+ 还有 {this.state.replies.length - PREVIEW_REPLY_COUNT} 条
+
+ )}
+
+ )}
+
+ );
+
+ return !needFold && quote_id ? (
+
+ {res}
+
+
+ ) : (
+ res
+ );
+ }
}
class FlowItemQuote extends PureComponent {
- constructor(props) {
- super(props);
- this.state={
- loading_status: 'empty',
- error_msg: null,
- info: null,
- };
- }
+ constructor(props) {
+ super(props);
+ this.state = {
+ loading_status: 'empty',
+ error_msg: null,
+ info: null,
+ };
+ }
- componentDidMount() {
- this.load();
- }
+ componentDidMount() {
+ this.load();
+ }
- load() {
- this.setState({
- loading_status: 'loading',
- },()=>{
- API.get_single(this.props.pid,this.props.token)
- .then((json)=>{
- this.setState({
- loading_status: 'done',
- info: json.data,
- });
- })
- .catch((err)=>{
- if((''+err).indexOf('没有这条树洞')!==-1)
- this.setState({
- loading_status: 'empty',
- });
- else
- this.setState({
- loading_status: 'error',
- error_msg: ''+err,
- });
- });
- });
- }
+ load() {
+ this.setState(
+ {
+ loading_status: 'loading',
+ },
+ () => {
+ API.get_single(this.props.pid, this.props.token)
+ .then((json) => {
+ this.setState({
+ loading_status: 'done',
+ info: json.data,
+ });
+ })
+ .catch((err) => {
+ if (('' + err).indexOf('没有这条树洞') !== -1)
+ this.setState({
+ loading_status: 'empty',
+ });
+ else
+ this.setState({
+ loading_status: 'error',
+ error_msg: '' + err,
+ });
+ });
+ },
+ );
+ }
- render() {
- if(this.state.loading_status==='empty')
- return null;
- else if(this.state.loading_status==='loading')
- return (
-
-
-
- 提到了 #{this.props.pid}
-
-
- );
- else if(this.state.loading_status==='error')
- return (
-
-
-
重新加载
-
{this.state.error_msg}
-
-
- );
- else // 'done'
- return (
-
- );
- }
+ render() {
+ if (this.state.loading_status === 'empty') return null;
+ else if (this.state.loading_status === 'loading')
+ return (
+
+
+
+ 提到了 #{this.props.pid}
+
+
+ );
+ else if (this.state.loading_status === 'error')
+ return (
+
+
+
+ 重新加载
+
+
{this.state.error_msg}
+
+
+ );
+ // 'done'
+ else
+ return (
+
+ );
+ }
}
function FlowChunk(props) {
- return (
- {({value: token})=>(
-
- {!!props.title &&
}
- {props.list.map((info,ind)=>(
-
-
- {!!(props.deletion_detect && props.mode==='list' && ind && props.list[ind-1].pid-info.pid>1) &&
-
-
- {props.list[ind-1].pid-info.pid-1} 条被删除
-
-
- }
-
-
-
- ))}
-
- )}
- );
+ return (
+
+ {({ value: token }) => (
+
+ {!!props.title &&
}
+ {props.list.map((info, ind) => (
+
+
+ {!!(
+ props.deletion_detect &&
+ props.mode === 'list' &&
+ ind &&
+ props.list[ind - 1].pid - info.pid > 1
+ ) && (
+
+
+ {props.list[ind - 1].pid - info.pid - 1} 条被删除
+
+
+ )}
+
+
+
+ ))}
+
+ )}
+
+ );
}
export class Flow extends PureComponent {
- constructor(props) {
- super(props);
- this.state={
- mode: props.mode,
- search_param: props.search_text,
- loaded_pages: 0,
- chunks: {
- title: '',
- data: [],
- },
- loading_status: 'done',
- error_msg: null,
- };
- this.on_scroll_bound=this.on_scroll.bind(this);
- window.LATEST_POST_ID=parseInt(localStorage['_LATEST_POST_ID'],10)||0;
- }
+ constructor(props) {
+ super(props);
+ this.state = {
+ mode: props.mode,
+ search_param: props.search_text,
+ loaded_pages: 0,
+ chunks: {
+ title: '',
+ data: [],
+ },
+ loading_status: 'done',
+ error_msg: null,
+ };
+ this.on_scroll_bound = this.on_scroll.bind(this);
+ window.LATEST_POST_ID = parseInt(localStorage['_LATEST_POST_ID'], 10) || 0;
+ }
- load_page(page) {
- const failed=(err)=>{
- console.error(err);
- this.setState((prev,props)=>({
- loaded_pages: prev.loaded_pages-1,
- loading_status: 'failed',
- error_msg: ''+err,
- }));
- };
-
- if(page>this.state.loaded_pages+1)
- throw new Error('bad page');
- if(page===this.state.loaded_pages+1) {
- console.log('fetching page',page);
- if(this.state.mode==='list') {
- API.get_list(page,this.props.token)
- .then((json)=>{
- if(page===1 && json.data.length) { // update latest_post_id
- let max_id=-1;
- json.data.forEach((x)=>{
- if(parseInt(x.pid,10)>max_id)
- max_id=parseInt(x.pid,10);
- });
- localStorage['_LATEST_POST_ID']=''+max_id;
- }
- this.setState((prev,props)=>({
- chunks: {
- title: 'News Feed',
- data: prev.chunks.data.concat(json.data.filter((x)=>(
- prev.chunks.data.length===0 ||
- !(prev.chunks.data.slice(-100).some((p)=>p.pid===x.pid))
- ))),
- },
- loading_status: 'done',
- }));
- })
- .catch(failed);
- } else if(this.state.mode==='search') {
- API.get_search(page,this.state.search_param,this.props.token)
- .then((json)=>{
- const finished=json.data.length===0;
- this.setState((prev,props)=>({
- chunks: {
- title: 'Result for "'+this.state.search_param+'"',
- data: prev.chunks.data.concat(json.data.filter((x)=>(
- prev.chunks.data.length===0 ||
- !(prev.chunks.data.slice(-100).some((p)=>p.pid===x.pid))
- ))),
- },
- mode: finished ? 'search_finished' : 'search',
- loading_status: 'done',
- }));
- })
- .catch(failed);
- } else if(this.state.mode==='single') {
- const pid=parseInt(this.state.search_param.substr(1),10);
- API.get_single(pid,this.props.token)
- .then((json)=>{
- this.setState({
- chunks: {
- title: 'PID = '+pid,
- data: [json.data],
- },
- mode: 'single_finished',
- loading_status: 'done',
- });
- })
- .catch(failed);
- } else if(this.state.mode==='attention') {
- API.get_attention(this.props.token)
- .then((json)=>{
- this.setState({
- chunks: {
- title: 'Attention List',
- data: json.data,
- },
- mode: 'attention_finished',
- loading_status: 'done',
- });
- })
- .catch(failed);
- } else {
- console.log('nothing to load');
- return;
- }
+ load_page(page) {
+ const failed = (err) => {
+ console.error(err);
+ this.setState((prev, props) => ({
+ loaded_pages: prev.loaded_pages - 1,
+ loading_status: 'failed',
+ error_msg: '' + err,
+ }));
+ };
- this.setState((prev,props)=>({
- loaded_pages: prev.loaded_pages+1,
- loading_status: 'loading',
- error_msg: null,
+ if (page > this.state.loaded_pages + 1) throw new Error('bad page');
+ if (page === this.state.loaded_pages + 1) {
+ console.log('fetching page', page);
+ if (this.state.mode === 'list') {
+ API.get_list(page, this.props.token)
+ .then((json) => {
+ if (page === 1 && json.data.length) {
+ // update latest_post_id
+ let max_id = -1;
+ json.data.forEach((x) => {
+ if (parseInt(x.pid, 10) > max_id) max_id = parseInt(x.pid, 10);
+ });
+ localStorage['_LATEST_POST_ID'] = '' + max_id;
+ }
+ this.setState((prev, props) => ({
+ chunks: {
+ title: 'News Feed',
+ data: prev.chunks.data.concat(
+ json.data.filter(
+ (x) =>
+ prev.chunks.data.length === 0 ||
+ !prev.chunks.data
+ .slice(-100)
+ .some((p) => p.pid === x.pid),
+ ),
+ ),
+ },
+ loading_status: 'done',
}));
- }
- }
+ })
+ .catch(failed);
+ } else if (this.state.mode === 'search') {
+ API.get_search(page, this.state.search_param, this.props.token)
+ .then((json) => {
+ const finished = json.data.length === 0;
+ this.setState((prev, props) => ({
+ chunks: {
+ title: 'Result for "' + this.state.search_param + '"',
+ data: prev.chunks.data.concat(
+ json.data.filter(
+ (x) =>
+ prev.chunks.data.length === 0 ||
+ !prev.chunks.data
+ .slice(-100)
+ .some((p) => p.pid === x.pid),
+ ),
+ ),
+ },
+ mode: finished ? 'search_finished' : 'search',
+ loading_status: 'done',
+ }));
+ })
+ .catch(failed);
+ } else if (this.state.mode === 'single') {
+ const pid = parseInt(this.state.search_param.substr(1), 10);
+ API.get_single(pid, this.props.token)
+ .then((json) => {
+ this.setState({
+ chunks: {
+ title: 'PID = ' + pid,
+ data: [json.data],
+ },
+ mode: 'single_finished',
+ loading_status: 'done',
+ });
+ })
+ .catch(failed);
+ } else if (this.state.mode === 'attention') {
+ API.get_attention(this.props.token)
+ .then((json) => {
+ this.setState({
+ chunks: {
+ title: 'Attention List',
+ data: json.data,
+ },
+ mode: 'attention_finished',
+ loading_status: 'done',
+ });
+ })
+ .catch(failed);
+ } else {
+ console.log('nothing to load');
+ return;
+ }
- on_scroll(event) {
- if(event.target===document) {
- const avail=document.body.scrollHeight-window.scrollY-window.innerHeight;
- if(avail ({
+ loaded_pages: prev.loaded_pages + 1,
+ loading_status: 'loading',
+ error_msg: null,
+ }));
}
+ }
- componentDidMount() {
- this.load_page(1);
- window.addEventListener('scroll',this.on_scroll_bound);
- window.addEventListener('resize',this.on_scroll_bound);
- }
- componentWillUnmount() {
- window.removeEventListener('scroll',this.on_scroll_bound);
- window.removeEventListener('resize',this.on_scroll_bound);
+ on_scroll(event) {
+ if (event.target === document) {
+ const avail =
+ document.body.scrollHeight - window.scrollY - window.innerHeight;
+ if (avail < window.innerHeight && this.state.loading_status === 'done')
+ this.load_page(this.state.loaded_pages + 1);
}
+ }
- render() {
- const should_deletion_detect=localStorage['DELETION_DETECT']==='on';
- return (
-
-
- {this.state.loading_status==='failed' &&
-
- }
-
Loading... :
- '© thuhole'
- } />
+ componentDidMount() {
+ this.load_page(1);
+ window.addEventListener('scroll', this.on_scroll_bound);
+ window.addEventListener('resize', this.on_scroll_bound);
+ }
+ componentWillUnmount() {
+ window.removeEventListener('scroll', this.on_scroll_bound);
+ window.removeEventListener('resize', this.on_scroll_bound);
+ }
+
+ render() {
+ const should_deletion_detect = localStorage['DELETION_DETECT'] === 'on';
+ return (
+
+
+ {this.state.loading_status === 'failed' && (
+
+
- );
- }
-}
\ No newline at end of file
+
+ )}
+
+
+ Loading...
+
+ ) : (
+ '© thuhole'
+ )
+ }
+ />
+
+ );
+ }
+}
diff --git a/src/Markdown.js b/src/Markdown.js
index ef3d7be..a3b7a15 100644
--- a/src/Markdown.js
+++ b/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 '' +
- hljs.highlight(lang, str, true).value +
- '
';
+ return (
+ '' +
+ hljs.highlight(lang, str, true).value +
+ '
'
+ );
} catch (__) {}
}
- return '' + md.utils.escapeHtml(str) + '
';
- }
+ return (
+ '' + md.utils.escapeHtml(str) + '
'
+ );
+ },
}).use(MarkdownItKaTeX, {
- "throwOnError" : false,
- "errorColor" : "#aa0000"
-})
+ throwOnError: false,
+ errorColor: '#aa0000',
+});
-export default (text) => md.render(text)
\ No newline at end of file
+export default (text) => md.render(text);
diff --git a/src/Message.js b/src/Message.js
index 1af97bb..7e98712 100644
--- a/src/Message.js
+++ b/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 (加载中……
);
- else if(this.state.loading_status==='failed')
- return ();
- else if(this.state.loading_status==='done')
- return this.state.msg.map((msg)=>(
-
- ));
- else
- return null;
- }
-}
\ No newline at end of file
+ render() {
+ if (this.state.loading_status === 'loading')
+ return 加载中……
;
+ else if (this.state.loading_status === 'failed')
+ return (
+
+ );
+ else if (this.state.loading_status === 'done')
+ return this.state.msg.map((msg) => (
+
+ ));
+ else return null;
+ }
+}
diff --git a/src/PressureHelper.js b/src/PressureHelper.js
index 278562f..a93f347 100644
--- a/src/PressureHelper.js
+++ b/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 (
-
- )
- }
-}
\ No newline at end of file
+ render() {
+ const pad = MULTIPLIER * (this.state.level - THRESHOLD) - BORDER_WIDTH;
+ return (
+
+ );
+ }
+}
diff --git a/src/Sidebar.js b/src/Sidebar.js
index 693e2be..eec3264 100644
--- a/src/Sidebar.js
+++ b/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 (
-
-
{e.preventDefault();e.target.click();}} />
-
- {cur_content}
-
-
-
- {this.props.stack.length>2 &&
-
- }
- {cur_title}
-
-
- );
- }
-}
\ No newline at end of file
+ render() {
+ let [cur_title, cur_content] = this.props.stack[
+ this.props.stack.length - 1
+ ];
+ return (
+
+
{
+ e.preventDefault();
+ e.target.click();
+ }}
+ />
+
+ {cur_content}
+
+
+
+
+
+
+
+ {this.props.stack.length > 2 && (
+
+
+
+
+
+ )}
+ {cur_title}
+
+
+ );
+ }
+}
diff --git a/src/Title.js b/src/Title.js
index 9d6321a..5958c19 100644
--- a/src/Title.js
+++ b/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);
- }
-
- 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'});
- });
- }
- }
+ 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);
+ }
- on_change(event) {
- this.setState({
- search_text: event.target.value,
- });
+ 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_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;
- }
+ on_change(event) {
+ this.setState({
+ search_text: event.target.value,
+ });
+ }
- 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 (
-
{({value: token})=>(
-
- )}
- )
- }
+ do_attention() {
+ window.scrollTo(0, 0);
+ this.setState({
+ search_text: '',
+ });
+ this.set_mode('attention', null);
+ }
+
+ render() {
+ return (
+
+ {({ value: token }) => (
+
+ )}
+
+ );
+ }
}
export function Title(props) {
- return (
-
- {/*
*/}
-
-
-
- props.show_sidebar(
- 'T大树洞',
-
- )}>
- T大树洞
-
-
-
-
-
+ return (
+
+ {/*
*/}
+
+
+
+
+ props.show_sidebar(
+ 'T大树洞',
+ ,
+ )
+ }
+ >
+ T大树洞
+
+
- )
-}
\ No newline at end of file
+
+
+
+ );
+}
diff --git a/src/UserAction.js b/src/UserAction.js
index 53d7f78..10a6b01 100644
--- a/src/UserAction.js
+++ b/src/UserAction.js
@@ -1,24 +1,35 @@
-import React, {Component, PureComponent} from 'react';
-import {API_BASE, SafeTextarea, PromotionBar, HighlightedMarkdown} from './Common';
-import {MessageViewer} from './Message';
-import {LoginPopup} from './infrastructure/widgets';
-import {ColorPicker} from './color_picker';
-import {ConfigUI} from './Config';
+import React, { Component, PureComponent } from 'react';
+import {
+ API_BASE,
+ SafeTextarea,
+ PromotionBar,
+ HighlightedMarkdown,
+} from './Common';
+import { MessageViewer } from './Message';
+import { LoginPopup } from './infrastructure/widgets';
+import { ColorPicker } from './color_picker';
+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 { cache } from './cache';
+import {
+ API_VERSION_PARAM,
+ THUHOLE_API_ROOT,
+ API,
+ get_json,
+ token_param,
+} from './flows_api';
import './UserAction.css';
-const BASE64_RATE=4/3;
-const MAX_IMG_DIAM=8000;
-const MAX_IMG_PX=5000000;
-const MAX_IMG_FILESIZE=450000*BASE64_RATE;
+const BASE64_RATE = 4 / 3;
+const MAX_IMG_DIAM = 8000;
+const MAX_IMG_PX = 5000000;
+const MAX_IMG_FILESIZE = 450000 * BASE64_RATE;
-export const TokenCtx=React.createContext({
- value: null,
- set_value: ()=>{},
+export const TokenCtx = React.createContext({
+ value: null,
+ set_value: () => {},
});
// class LifeInfoBox extends Component {
@@ -196,559 +207,711 @@ export const TokenCtx=React.createContext({
// }
export function InfoSidebar(props) {
- return (
-
-
-
-
-
-
-
- 联系我们:thuhole at protonmail dot com
-
-
-
-
- T大树洞 网页版 by @thuhole,
- 基于
- GPLv3
- 协议在 GitHub 开源
-
-
- T大树洞 网页版的诞生离不开
- P大树洞网页版 by @xmcp
- 、
- React
- 、
- IcoMoon
- 等开源项目
-
-
- 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.
-
-
- 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
- GNU General Public License
- for more details.
-
-
-
- );
+ return (
+
+
+
+
+
+
+
联系我们:thuhole at protonmail dot com
+
+
+
+ T大树洞 网页版 by @thuhole, 基于
+
+ GPLv3
+
+ 协议在{' '}
+
+ GitHub
+ {' '}
+ 开源
+
+
+ T大树洞 网页版的诞生离不开
+
+ P大树洞网页版 by @xmcp
+
+ 、
+
+ React
+
+ 、
+
+ IcoMoon
+
+ 等开源项目
+
+
+ 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.
+
+
+ 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
+
+ GNU General Public License
+
+ for more details.
+
+
+
+ );
}
class ResetUsertokenWidget extends Component {
- constructor(props) {
- super(props);
- this.state={
- loading_status: 'done',
- };
- }
+ constructor(props) {
+ super(props);
+ this.state = {
+ loading_status: 'done',
+ };
+ }
+
+ do_reset() {
+ if (
+ window.confirm(
+ '您正在重置 UserToken!\n您的账号将会在【所有设备】上注销,您需要手动重新登录!',
+ )
+ ) {
+ let uid = window.prompt(
+ '您正在重置 UserToken!\n请输入您的学号以确认身份:',
+ );
+ if (uid)
+ this.setState(
+ {
+ loading_status: 'loading',
+ },
+ () => {
+ fetch(THUHOLE_API_ROOT + 'api_xmcp/hole/reset_usertoken', {
+ method: 'post',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ user_token: this.props.token,
+ uid: uid,
+ }),
+ })
+ .then(get_json)
+ .then((json) => {
+ if (json.error) throw new Error(json.error);
+ else alert('重置成功!您需要在所有设备上重新登录。');
- do_reset() {
- if(window.confirm('您正在重置 UserToken!\n您的账号将会在【所有设备】上注销,您需要手动重新登录!')) {
- let uid=window.prompt('您正在重置 UserToken!\n请输入您的学号以确认身份:');
- if(uid)
this.setState({
- loading_status: 'loading',
- },()=>{
- fetch(THUHOLE_API_ROOT+'api_xmcp/hole/reset_usertoken', {
- method: 'post',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- user_token: this.props.token,
- uid: uid,
- }),
- })
- .then(get_json)
- .then((json)=>{
- if(json.error)
- throw new Error(json.error);
- else
- alert('重置成功!您需要在所有设备上重新登录。');
-
- this.setState({
- loading_status: 'done',
- });
- })
- .catch((e)=>{
- alert('重置失败:'+e);
- this.setState({
- loading_status: 'done',
- });
- })
+ loading_status: 'done',
});
- }
- }
-
- render() {
- if(this.state.loading_status==='done')
- return (
重置 );
- else if(this.state.loading_status==='loading')
- return (
);
+ })
+ .catch((e) => {
+ alert('重置失败:' + e);
+ this.setState({
+ loading_status: 'done',
+ });
+ });
+ },
+ );
}
+ }
+
+ render() {
+ if (this.state.loading_status === 'done')
+ return
重置 ;
+ else if (this.state.loading_status === 'loading')
+ return (
+
+
+
+ );
+ }
}
export class LoginForm extends Component {
- copy_token(token) {
- if(copy(token))
- alert('复制成功!\n请一定不要泄露哦');
- }
+ copy_token(token) {
+ if (copy(token)) alert('复制成功!\n请一定不要泄露哦');
+ }
- render() {
- return (
-
{(token)=>
+ render() {
+ return (
+
+ {(token) => (
+
+ {/*{!!token.value &&*/}
+ {/*
*/}
+ {/*}*/}
+
+ {token.value ? (
- {/*{!!token.value &&*/}
- {/*
*/}
- {/*}*/}
-
- {token.value ?
-
-
- 您已登录。
- {token.set_value(null);}}>
- 注销
-
-
-
- {/*
*/}
- {/*根据计算中心要求,访问授权三个月内有效,过期需重新登录。*/}
- {/*T大树洞将会单向加密(i.e. 哈希散列)您的邮箱后再存入数据库,因此您的发帖具有较强的匿名性。具体可见我们的后端开源代码 。*/}
- {/*
*/}
-
- {this.props.show_sidebar(
- '系统消息',
-
- )}}>查看系统消息
- 当您发送的内容违规时,我们将用系统消息提示您
-
-
- 复制 User Token
- 复制 User Token 可以在新设备登录,切勿告知他人。若怀疑被盗号请重新邮箱验证码登录以重置Token。{/*,若怀疑被盗号请尽快 */}
-
-
:
-
{(do_popup)=>(
-
-
-
-
- 登录
-
-
-
- T大树洞 面向T大学生,通过T大邮箱验证您的身份并提供服务。
-
-
- )}
- }
-
+
+ 您已登录。
+ {
+ token.set_value(null);
+ }}
+ >
+ 注销
+
+
+
+ {/*
*/}
+ {/*根据计算中心要求,访问授权三个月内有效,过期需重新登录。*/}
+ {/*T大树洞将会单向加密(i.e. 哈希散列)您的邮箱后再存入数据库,因此您的发帖具有较强的匿名性。具体可见我们的后端开源代码 。*/}
+ {/*
*/}
+
+ {
+ this.props.show_sidebar(
+ '系统消息',
+ ,
+ );
+ }}
+ >
+ 查看系统消息
+
+
+ 当您发送的内容违规时,我们将用系统消息提示您
+
+
+
+ 复制 User Token
+
+
+ 复制 User Token
+ 可以在新设备登录,切勿告知他人。若怀疑被盗号请重新邮箱验证码登录以重置Token。
+ {/*,若怀疑被盗号请尽快 */}
+
- }
- )
- }
+ ) : (
+
+ {(do_popup) => (
+
+
+
+
+ 登录
+
+
+
+
+ T大树洞
+ 面向T大学生,通过T大邮箱验证您的身份并提供服务。
+
+
+
+ )}
+
+ )}
+
+
+ )}
+
+ );
+ }
}
export class ReplyForm extends Component {
- constructor(props) {
- super(props);
- this.state={
- text: '',
- loading_status: 'done',
- preview: false,
- };
- 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.color_picker=new ColorPicker();
+ constructor(props) {
+ super(props);
+ this.state = {
+ text: '',
+ loading_status: 'done',
+ preview: false,
+ };
+ 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.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 (this.area_ref.current) {
+ e.preventDefault();
+ this.area_ref.current.focus();
+ }
}
-
- global_keypress_handler(e) {
- 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();
- }
+ }
+ componentDidMount() {
+ document.addEventListener('keypress', this.global_keypress_handler_bound);
+ }
+ componentWillUnmount() {
+ document.removeEventListener(
+ 'keypress',
+ this.global_keypress_handler_bound,
+ );
+ }
+
+ on_change(value) {
+ this.setState({
+ text: value,
+ });
+ }
+
+ on_submit(event) {
+ if (event) event.preventDefault();
+ if (this.state.loading_status === 'loading') return;
+ this.setState({
+ loading_status: 'loading',
+ });
+
+ let data = new URLSearchParams();
+ 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),
+ {
+ 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));
}
- }
- componentDidMount() {
- document.addEventListener('keypress',this.global_keypress_handler_bound);
- }
- componentWillUnmount() {
- document.removeEventListener('keypress',this.global_keypress_handler_bound);
- }
- on_change(value) {
this.setState({
- text: value,
+ loading_status: 'done',
+ text: '',
+ preview: false,
});
- }
-
- on_submit(event) {
- if(event) event.preventDefault();
- if(this.state.loading_status==='loading')
- return;
+ this.area_ref.current.clear();
+ this.props.on_complete();
+ })
+ .catch((e) => {
+ console.error(e);
+ alert('回复失败');
this.setState({
- loading_status: 'loading',
+ loading_status: 'done',
});
+ });
+ }
- let data=new URLSearchParams();
- 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), {
- 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));
- }
-
- this.setState({
- loading_status: 'done',
- text: '',
- preview: false,
- });
- this.area_ref.current.clear();
- this.props.on_complete();
- })
- .catch((e)=>{
- console.error(e);
- alert('回复失败');
- this.setState({
- loading_status: 'done',
- });
- });
- }
-
- toggle_preview() {
- this.setState({
- preview: !this.state.preview
- });
- }
+ toggle_preview() {
+ this.setState({
+ preview: !this.state.preview,
+ });
+ }
- render() {
- return (
-
- )
- }
+ render() {
+ return (
+
+ );
+ }
}
export class PostForm extends Component {
- constructor(props) {
- super(props);
- this.state={
- text: '',
- loading_status: 'done',
- img_tip: null,
- preview: false,
- };
- this.img_ref=React.createRef();
- this.area_ref=React.createRef();
- this.on_change_bound=this.on_change.bind(this);
- this.on_img_change_bound=this.on_img_change.bind(this);
- this.color_picker=new ColorPicker();
- }
-
- componentDidMount() {
- if(this.area_ref.current)
- this.area_ref.current.focus();
- }
+ constructor(props) {
+ super(props);
+ this.state = {
+ text: '',
+ loading_status: 'done',
+ img_tip: null,
+ preview: false,
+ };
+ this.img_ref = React.createRef();
+ this.area_ref = React.createRef();
+ this.on_change_bound = this.on_change.bind(this);
+ this.on_img_change_bound = this.on_img_change.bind(this);
+ this.color_picker = new ColorPicker();
+ }
+
+ componentDidMount() {
+ if (this.area_ref.current) this.area_ref.current.focus();
+ }
+
+ on_change(value) {
+ this.setState({
+ text: value,
+ });
+ }
+
+ do_post(text, img) {
+ let data = new URLSearchParams();
+ 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);
+
+ fetch(API_BASE + '/api.php?action=dopost' + 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) {
+ if (json.msg) alert(json.msg);
+ throw new Error(JSON.stringify(json));
+ }
- on_change(value) {
this.setState({
- text: value,
+ loading_status: 'done',
+ text: '',
+ preview: false,
});
- }
-
- do_post(text,img) {
- let data=new URLSearchParams();
- 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);
-
- fetch(API_BASE+'/api.php?action=dopost'+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) {
- if(json.msg) alert(json.msg);
- throw new Error(JSON.stringify(json));
- }
-
- this.setState({
- loading_status: 'done',
- text: '',
- preview: false,
- });
- this.area_ref.current.clear();
- this.props.on_complete();
- })
- .catch((e)=>{
- console.error(e);
- alert('发表失败');
- this.setState({
- loading_status: 'done',
- });
- });
- }
-
- proc_img(file) {
- return new Promise((resolve,reject)=>{
- function return_url(url) {
- const idx=url.indexOf(';base64,');
- if(idx===-1)
- throw new Error('img not base64 encoded');
-
- return url.substr(idx+8);
- }
-
- let reader=new FileReader();
- function on_got_img(url) {
- const image = new Image();
- image.onload=(()=>{
- let width=image.width;
- let height=image.height;
- let compressed=false;
-
- if(width>MAX_IMG_DIAM) {
- height=height*MAX_IMG_DIAM/width;
- width=MAX_IMG_DIAM;
- compressed=true;
- }
- if(height>MAX_IMG_DIAM) {
- 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);
- height/=rate;
- width/=rate;
- compressed=true;
- }
- console.log('chosen img size',width,height);
-
- let canvas=document.createElement('canvas');
- let ctx=canvas.getContext('2d');
- canvas.width=width;
- 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) {
- 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;
- }
- if(quality_l>=.101) {
- console.log('chosen img quality',quality);
- resolve({
- img: return_url(new_url),
- quality: quality,
- width: Math.round(width),
- height: Math.round(height),
- compressed: compressed,
- });
- } else {
- reject('图片过大,无法上传');
- }
- });
- image.src=url;
- }
- reader.onload=(event)=>{
- fixOrientation(event.target.result,{},(fixed_dataurl)=>{
- on_got_img(fixed_dataurl);
- });
- };
- reader.readAsDataURL(file);
+ this.area_ref.current.clear();
+ this.props.on_complete();
+ })
+ .catch((e) => {
+ console.error(e);
+ alert('发表失败');
+ this.setState({
+ loading_status: 'done',
});
- }
-
- on_img_change() {
- if(this.img_ref.current && this.img_ref.current.files.length)
- 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)`,
- });
- })
- .catch((e)=>{
- this.setState({
- img_tip: `图片无效:${e}`,
- });
- });
- });
- else
- this.setState({
- img_tip: null,
+ });
+ }
+
+ proc_img(file) {
+ return new Promise((resolve, reject) => {
+ function return_url(url) {
+ const idx = url.indexOf(';base64,');
+ if (idx === -1) throw new Error('img not base64 encoded');
+
+ return url.substr(idx + 8);
+ }
+
+ let reader = new FileReader();
+ function on_got_img(url) {
+ const image = new Image();
+ image.onload = () => {
+ let width = image.width;
+ let height = image.height;
+ let compressed = false;
+
+ if (width > MAX_IMG_DIAM) {
+ height = (height * MAX_IMG_DIAM) / width;
+ width = MAX_IMG_DIAM;
+ compressed = true;
+ }
+ if (height > MAX_IMG_DIAM) {
+ 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);
+ height /= rate;
+ width /= rate;
+ compressed = true;
+ }
+ console.log('chosen img size', width, height);
+
+ let canvas = document.createElement('canvas');
+ let ctx = canvas.getContext('2d');
+ canvas.width = width;
+ canvas.height = height;
+ ctx.drawImage(image, 0, 0, width, height);
+
+ 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;
+ }
+ if (quality_l >= 0.101) {
+ console.log('chosen img quality', quality);
+ resolve({
+ img: return_url(new_url),
+ quality: quality,
+ width: Math.round(width),
+ height: Math.round(height),
+ compressed: compressed,
});
- }
-
- on_submit(event) {
- if(event) event.preventDefault();
- if(this.state.loading_status==='loading')
- return;
- if(this.img_ref.current.files.length) {
- this.setState({
- loading_status: 'processing',
- });
- this.proc_img(this.img_ref.current.files[0])
- .then((d)=>{
- this.setState({
- loading_status: 'loading',
- });
- this.do_post(this.state.text,d.img);
- })
- .catch((e)=>{
- alert(e);
- });
- } else {
- this.setState({
- loading_status: 'loading',
+ } else {
+ reject('图片过大,无法上传');
+ }
+ };
+ image.src = url;
+ }
+ reader.onload = (event) => {
+ fixOrientation(event.target.result, {}, (fixed_dataurl) => {
+ on_got_img(fixed_dataurl);
+ });
+ };
+ reader.readAsDataURL(file);
+ });
+ }
+
+ on_img_change() {
+ if (this.img_ref.current && this.img_ref.current.files.length)
+ 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)`,
+ });
+ })
+ .catch((e) => {
+ this.setState({
+ img_tip: `图片无效:${e}`,
+ });
});
- this.do_post(this.state.text,null);
- }
- }
-
- toggle_preview() {
- this.setState({
- preview: !this.state.preview
+ },
+ );
+ else
+ this.setState({
+ img_tip: null,
+ });
+ }
+
+ on_submit(event) {
+ if (event) event.preventDefault();
+ if (this.state.loading_status === 'loading') return;
+ if (this.img_ref.current.files.length) {
+ this.setState({
+ loading_status: 'processing',
+ });
+ this.proc_img(this.img_ref.current.files[0])
+ .then((d) => {
+ this.setState({
+ loading_status: 'loading',
+ });
+ this.do_post(this.state.text, d.img);
+ })
+ .catch((e) => {
+ alert(e);
});
+ } else {
+ this.setState({
+ loading_status: 'loading',
+ });
+ this.do_post(this.state.text, null);
}
+ }
- render() {
- return (
-
- )
- }
-}
\ No newline at end of file
+ render() {
+ return (
+
+ );
+ }
+}
diff --git a/src/cache.js b/src/cache.js
index 79d87b0..c38a116 100644
--- a/src/cache.js
+++ b/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{
- 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);
+ });
+ }
- 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;
+ 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();
+ }
};
- 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;
-}
\ No newline at end of file
+ if (!window.hole_cache) window.hole_cache = new Cache();
+ return window.hole_cache;
+}
diff --git a/src/color_picker.js b/src/color_picker.js
index 7fa51d7..afcf240 100644
--- a/src/color_picker.js
+++ b/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%)`,
+ ];
}
-}
\ No newline at end of file
+ return this.names[name];
+ }
+}
diff --git a/src/flows_api.js b/src/flows_api.js
index f897d64..2e8d3ed 100644
--- a/src/flows_api.js
+++ b/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;
- });
- },
-};
\ No newline at end of file
+
+ 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;
+ });
+ },
+};
diff --git a/src/registerServiceWorker.js b/src/registerServiceWorker.js
index ca5a0b6..b8ef96e 100644
--- a/src/registerServiceWorker.js
+++ b/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();
});
}
diff --git a/src/text_splitter.js b/src/text_splitter.js
index 7b4fdea..64cedcd 100644
--- a/src/text_splitter.js
+++ b/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;
}