import React, {Component, PureComponent} from 'react'; import copy from 'copy-to-clipboard'; import {ColorPicker} from './color_picker'; import {format_time, Time, TitleLine, HighlightedText} from './Common'; import './Flows.css'; import LazyLoad from 'react-lazyload'; import {AudioWidget} from './AudioWidget'; import {TokenCtx, ReplyForm} from './UserAction'; import {API} from './flows_api'; const IMAGE_BASE='http://www.pkuhelper.com:10301/services/pkuhole/images/'; const AUDIO_BASE='http://www.pkuhelper.com:10301/services/pkuhole/audios/'; const SEARCH_PAGESIZE=50; const CLICKABLE_TAGS={a: true, audio: true}; const PREVIEW_REPLY_COUNT=10; window.LATEST_POST_ID=parseInt(localStorage['_LATEST_POST_ID'],10)||0; function load_single_meta(show_sidebar,token) { return (pid)=>{ const color_picker=new ColorPicker(); show_sidebar( '帖子详情',
正在加载 #{pid}
); Promise.all([ API.get_single(pid,token), API.load_replies(pid,token,color_picker), ]) .then((res)=>{ const [single,replies]=res; single.data.variant={}; show_sidebar( '帖子详情', ) }) .catch((e)=>{ console.error(e); show_sidebar( '帖子详情',
load_single_meta(show_sidebar,token)}>重新加载
); }) }; } class Reply extends PureComponent { constructor(props) { super(props); } render() { return (
#{this.props.info.cid} 
); } } class FlowItem extends PureComponent { constructor(props) { super(props); } copy_link(event) { event.preventDefault(); copy( `${event.target.href}\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.text)).join('\n') ); } render() { let props=this.props; return (
{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.type==='image' &&

{props.img_clickable ? : }

} {props.info.type==='audio' && }
); } } class FlowSidebar extends PureComponent { constructor(props) { super(props); this.state={ attention: props.attention, info: props.info, replies: props.replies, loading_status: 'done', }; this.color_picker=props.color_picker; this.show_pid=load_single_meta(this.props.show_sidebar,this.props.token); 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: variant}); else return reply; }), }; else return { info: 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', }); API.load_replies(this.state.info.pid,this.props.token,this.color_picker) .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', }), ()=>{ this.syncState({ replies: this.state.replies, attention: this.state.attention, info: this.state.info, }); }); }) .catch((e)=>{ console.error(e); this.setState({ replies: [], loading_status: 'done', }); }); } 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); }) } } star_brush() { let count=prompt('Count:'); if(count) { let reqs=[]; for(let i=parseInt(count);i;i--) reqs.push(API.set_attention(this.state.info.pid,false,this.props.token)); Promise.all(reqs) .then(()=>{ alert('Completed!') }) .catch((e)=>{ alert('Failed!\n\n'+e); }) } } do_reply(name,event) { if(event.target.tagName.toLowerCase()!=='a') { let text=this.reply_ref.current.get(); if(/^\s*(Re (洞主|\b[A-Z][a-z]+){0,2}:)?\s*$/.test(text)) // text is nearly empty so we can replace it this.reply_ref.current.set_and_focus('Re '+name+': '); } } render() { const star_brush=localStorage['STAR_BRUSH']==='on'; if(this.state.loading_status==='loading') return (

加载中……

); return (
{(star_brush && !!this.props.token) && -  /  } {!!this.props.token && 举报  /  } 刷新回复 {!!this.props.token &&  /  { this.toggle_attention(); }}> {this.state.attention ?  已关注 :  未关注 } }
{this.do_reply('',e);}}> {this.set_variant(null,variant);}} />
{(this.props.deletion_detect && parseInt(this.state.info.reply)>this.state.replies.length) &&
{parseInt(this.state.info.reply)-this.state.replies.length} 条回复被删除
} {this.state.replies.map((reply)=>(
{this.do_reply(reply.name,e);}}> {this.set_variant(reply.cid,variant);}} />
))} {!!this.props.token ? :
登录后可以回复树洞
}
) } } class FlowItemRow extends PureComponent { constructor(props) { super(props); this.state={ replies: [], reply_status: 'done', info: props.info, attention: false, }; this.state.info.variant={}; this.color_picker=new ColorPicker(); this.show_pid=load_single_meta(this.props.show_sidebar,this.props.token); } 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(this.state.info.pid,this.props.token,this.color_picker) .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, reply_status: 'done', }),callback); }) .catch((e)=>{ console.error(e); this.setState({ replies: [], reply_status: 'failed', },callback); }); } show_sidebar() { this.props.show_sidebar( '帖子详情', ); } render() { return (
{ 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} 条
}
); } } 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', }; 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', })); }; 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)=>{ 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', })); } } 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' } />
); } }