import React, {Component, PureComponent} from 'react'; import copy from 'copy-to-clipboard'; import {ColorPicker} from './color_picker'; import {split_text, NICKNAME_RE, PID_RE, URL_RE, URL_PID_RE} from './text_splitter'; import {format_time, build_highlight_re, Time, TitleLine, HighlightedText, ClickHandler} from './Common'; import './Flows.css'; import LazyLoad from './react-lazyload/src'; import {AudioWidget} from './AudioWidget'; import {TokenCtx, ReplyForm} from './UserAction'; import {API, PKUHELPER_ROOT} from './flows_api'; const IMAGE_BASE=PKUHELPER_ROOT+'services/pkuhole/images/'; const AUDIO_BASE=PKUHELPER_ROOT+'services/pkuhole/audios/'; // troubleshotting performance problem with http2 //const IMAGE_BASE='http://pkuhelper.pku.edu.cn/services/pkuhole/images/'; //const AUDIO_BASE='http://pkuhelper.pku.edu.cn/services/pkuhole/audios/'; const SEARCH_PAGESIZE=50; const CLICKABLE_TAGS={a: true, audio: true}; const PREVIEW_REPLY_COUNT=10; const QUOTE_BLACKLIST=['23333','233333','66666','666666','10086','10000','100000','99999','999999','55555','555555']; window.LATEST_POST_ID=parseInt(localStorage['_LATEST_POST_ID'],10)||0; function load_single_meta(show_sidebar,token,parents) { return (pid)=>{ let title_elem=; const color_picker=new ColorPicker(); show_sidebar( title_elem,
正在加载 #{pid}
); API.get_single(pid,token) .then((single)=>{ single.data.variant={}; return new Promise((resolve,reject)=>{ API.load_replies_with_cache(pid,token,color_picker,parseInt(single.data.reply)) .then((replies)=>{resolve([single,replies])}) .catch(reject); }); }) .then((res)=>{ let [single,replies]=res; show_sidebar( title_elem, ) }) .catch((e)=>{ console.error(e); show_sidebar( title_elem,

load_single_meta(show_sidebar,token,parents)(pid)}>重新加载

{''+e}

); }) }; } class Reply extends PureComponent { constructor(props) { super(props); } render() { let parts=split_text(this.props.info.text,[ ['url_pid',URL_PID_RE], ['url',URL_RE], ['pid',PID_RE], ['nickname',NICKNAME_RE], ]); return (
#{this.props.info.cid}   {this.props.info.tag!==null && {this.props.info.tag} }
); } } class FlowItem extends PureComponent { constructor(props) { super(props); } copy_link(event) { event.preventDefault(); copy( `${event.target.href}${this.props.info.tag ? ' 【'+this.props.info.tag+'】' : ''}\n`+ `${this.props.info.text}${this.props.info.type==='image'?' [图片]':this.props.info.type==='audio'?' [语音]':''}\n`+ `(${format_time(new Date(this.props.info.timestamp*1000))} ${this.props.info.likenum}关注 ${this.props.info.reply}回复)\n`+ this.props.replies.map((r)=>( (r.tag ? '【'+r.tag+'】' : '')+ r.text )).join('\n') ); } render() { let props=this.props; let parts=props.parts||split_text(props.info.text,[ ['url_pid',URL_PID_RE], ['url',URL_RE], ['pid',PID_RE], ['nickname',NICKNAME_RE], ]); return (
{!!props.is_quote &&
提到
}
{!!window.LATEST_POST_ID && parseInt(props.info.pid,10)>window.LATEST_POST_ID &&
}
{!!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.type==='image' &&

{props.img_clickable ? : }

} {props.info.type==='audio' && }
{!!(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, loading_status: 'done', error_msg: null, dz_only: false, }; this.color_picker=props.color_picker; this.syncState=props.sync_state||(()=>{}); this.reply_ref=React.createRef(); } /*componentWillReceiveProps(nextProps) { this.setState({ attention: nextProps.attention, info: nextProps.info, replies: nextProps.replies, loading_status: 'done', }); this.color_picker=nextProps.color_picker; this.syncState=nextProps.sync_state||(()=>{}); }*/ // refactored to use key instead 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, }), ()=>{ 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.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: 'done', attention: next_attention, }); this.syncState({ attention: next_attention, }); }) .catch((e)=>{ this.setState({ loading_status: 'done' }); 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); }) } } toggle_dz_only() { this.setState((prevState)=>({ dz_only: !prevState.dz_only, })); } show_reply_bar(name,event) { if(this.reply_ref.current && !event.target.closest('a')) { let text=this.reply_ref.current.get(); if(/^\s*(?:Re (?:|洞主|(?:[A-Z][a-z]+ )?(?:[A-Z][a-z]+)):)?\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 (

加载中……

); let show_pid=load_single_meta(this.props.show_sidebar,this.props.token,this.props.parents.concat([this.state.info.pid])); let replies_to_show=this.state.dz_only ? this.state.replies.filter((r)=>r.islz) : this.state.replies; return (
{!!this.props.token && 举报  /  } 刷新回复  /  {this.state.dz_only ? '查看全部' : '只看洞主'} {!!this.props.token &&  /  { this.toggle_attention(); }}> {this.state.attention ?  已关注 :  未关注 } }
{this.show_reply_bar('',e);}}> {this.set_variant(null,variant);}} /> {!!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);}} /> ))} {!!this.props.token ? :
登录后可以回复树洞
}
) } } function FlowSidebarTitle(props) { let last_pid=props.parents.length ? props.parents[props.parents.length-1] : null; return ( 树洞  {!!last_pid && load_single_meta(props.show_sidebar,props.token,props.parents.slice(0,-1))(last_pid)}> #{last_pid}  →  } #{props.pid} ); } class FlowItemRow extends PureComponent { constructor(props) { super(props); this.state={ replies: [], reply_status: 'done', info: Object.assign({},props.info,{variant: {}}), attention: false, }; this.color_picker=new ColorPicker(); } 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', }); 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', }),callback); }) .catch((e)=>{ console.error(e); this.setState({ replies: [], reply_status: 'failed', },callback); }); } show_sidebar() { this.props.show_sidebar( , ); } 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,' ')]); 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) if(mode==='pid' && QUOTE_BLACKLIST.indexOf(content)===-1 && parseInt(content){ if(!CLICKABLE_TAGS[event.target.tagName.toLowerCase()]) this.show_sidebar(); }}>
{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 quote_id ? (
{res}
) : res; } } class FlowItemQuote extends PureComponent { constructor(props) { super(props); this.state={ loading_status: 'empty', error_msg: null, info: null, }; } 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, }); }); }); } 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 ( ); } } 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} 条被删除
}
))}
)}
); } 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; } 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.forEach((x)=>{ if(parseInt(x.pid,10)>(parseInt(localStorage['_LATEST_POST_ID'],10)||0)) localStorage['_LATEST_POST_ID']=x.pid; }); 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(SEARCH_PAGESIZE*page,this.state.search_param,this.props.token) .then((json)=>{ const finished=json.data.length{ 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; } this.setState((prev,props)=>({ loaded_pages: prev.loaded_pages+1, loading_status: 'loading', error_msg: null, })); } } on_scroll(event) { if(event.target===document) { const avail=document.body.scrollHeight-window.scrollY-window.innerHeight; if(avail {this.state.loading_status==='failed' && }  Loading... : '© xmcp' } /> ); } }