import React, { PureComponent, useState } from 'react'; import copy from 'copy-to-clipboard'; import { ColorPicker } from './color_picker'; import { split_text, PID_RE } from './text_splitter'; import { format_time, Time, TitleLine, ClickHandler, ColoredSpan, HighlightedMarkdown, get_push_subscription, } from './Common'; import './Flows.css'; import LazyLoad, { forceCheck } from 'react-lazyload'; import { TokenCtx, ReplyForm } from './UserAction'; import { API, parse_replies } from './flows_api'; import { cache } from './cache'; import { save_attentions } from './Attention'; import Poll from 'react-polls'; /* const IMAGE_BASE = 'https://thimg.yecdn.com/'; const IMAGE_BAK_BASE = 'https://img2.thuhole.com/'; */ const CLICKABLE_TAGS = { a: true, audio: true, button: true }; const PREVIEW_REPLY_COUNT = 10; // const QUOTE_BLACKLIST=['23333','233333','66666','666666','10086','10000','100000','99999','999999','55555','555555']; const QUOTE_BLACKLIST = []; window.LATEST_POST_ID = parseInt(localStorage['_LATEST_POST_ID'], 10) || 0; const DZ_NAME = '洞主'; function check_block(info) { return ( (((window.config.block_tmp || !window.config.show_all_rooms) && info.is_tmp) || window.config.block_words_v4.some((word) => info.text.includes(word)) || (info.cw && window.config.block_words_v4 .concat(window.config.block_cw) .some((word) => info.cw.includes(word)))) && !info.can_del ); } function load_single_meta(show_sidebar, token) { return async (pid, replace = false) => { let color_picker = new ColorPicker(); let title_elem = '树洞 #' + pid; show_sidebar( title_elem,
正在加载 #{pid}
, replace ? 'replace' : 'push', ); try { let single = await API.get_single(pid, token); single.data.variant = {}; let { data: replies } = await API.load_replies_with_cache( pid, token, color_picker, parseInt(single.data.reply), ); show_sidebar( title_elem, , 'replace', ); } catch (e) { console.error(e); show_sidebar( title_elem,

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

{'' + e}

, 'replace', ); } }; } class Reply extends PureComponent { render() { const { info, color_picker, show_pid, do_filter_name, do_delete, do_report, do_block, search_param, } = this.props; const author = info.name, replyText = info.text; this.color_picker = new ColorPicker(); return ( !check_block(info) && (
{!!do_filter_name && ( { do_filter_name(info.name); }} > )}   {{info.name}} {info.author_title && ( {`"${info.author_title}"`} )} {info.is_tmp && ( 临时账号 )} {!!do_delete && !!info.can_del && ( { do_delete('cid', info.cid); }} > {' '} 🗑️{' '} )} {!!do_block && ( {' '} 🚫{' '} )} {!!do_report && ( <>     )} {info.dangerous_user && ( {info.dangerous_user} )} {info.blocked_count && ( {info.blocked_count} )}
) ); } } class FlowItem extends PureComponent { constructor(props) { super(props); this.state = { cw: props.info.cw || '', }; } copy_link(event) { event.preventDefault(); copy( `${event.target.href}${ this.props.info.cw ? ' 【' + this.props.info.cw + '】' : '' }\n\n${this.props.info.text}\n` + `(${format_time(new Date(this.props.info.timestamp * 1000))} ${ this.props.info.likenum }关注 ${this.props.info.reply}回复)\n\n` + this.props.replies.map((r) => `【${r.name}】\n${r.text}\n`).join('\n'), ); } on_cw_change(event) { this.setState({ cw: event.target.value, }); } render() { const { info, is_quote, cached, attention, do_filter_name, do_delete, do_edit_cw, img_clickable, color_picker, show_pid, do_vote, do_block, search_param, } = this.props; const { cw } = this.state; const _display_color = color_picker.get(DZ_NAME, info.is_tmp); return (
{!!is_quote && (
{/*
*/} {/* 提到*/} {/*
*/}
)}
{!!window.LATEST_POST_ID && parseInt(info.pid, 10) > window.LATEST_POST_ID && (
)} {!!attention && !cached &&
}
{!!do_filter_name && ( { do_filter_name(DZ_NAME); }} > )} {!!parseInt(info.likenum, 10) && ( {info.likenum}  )} {!!parseInt(info.reply, 10) && ( {info.reply}  )} #{info.pid} {info.room_id !== parseInt(window.config.room) && ( <> @{info.room_id} )}   {info.author_title && ( {`"${info.author_title}"`} )} {info.is_tmp && ( 临时账号 )} {info.is_reported && R } {!!do_delete && !!info.can_del && ( { do_delete('pid', info.pid); }} > {' '} 🗑️{' '} )} {!!do_block && ( {' '} 🚫{' '} )} {info.dangerous_user && ( {info.dangerous_user} )} {info.blocked_count && ( {info.blocked_count} )} {info.cw !== null && (!do_edit_cw || !info.can_del) && ( {info.cw} )} {!!do_edit_cw && !!info.can_del && (
)} {info.allow_search && 📢 }
{!!info.hot_score && ( hot score: {info.hot_score} )}
{info.poll && (
a.votes).join('_')} question={''} answers={info.poll.answers} onVote={do_vote} customStyles={{ theme: 'cyan' }} noStorage={true} vote={localStorage['VOTE_RECORD:' + info.pid] || info.poll.vote} />
)} {!!(attention && 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, 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, likenum: '' + json.likenum, }) : 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() { const prev_info = this.state.info; const pid = prev_info.pid; API.set_attention(pid, !this.state.attention, this.props.token) .then((json) => { this.setState({ attention: json.attention, info: Object.assign({}, prev_info, { likenum: '' + json.likenum, }), }); let saved_attentions = window.saved_attentions; if (json.attention && !saved_attentions.includes(pid)) { saved_attentions.unshift(pid); } else if (!json.attention && saved_attentions.includes(pid)) { const idx = saved_attentions.indexOf(pid); saved_attentions.splice(idx, 1); } window.saved_attentions = saved_attentions; save_attentions(); this.syncState({ attention: json.attention, info: Object.assign({}, prev_info, { likenum: '' + json.likenum, }), }); }) .catch((e) => { this.setState({ loading_status: 'done', }); alert('设置关注失败\n' + e); console.error(e); }); } toggle_notification() { console.log('set notifi'); get_push_subscription().then((sc) => { if (!sc) { alert('浏览器不支持消息推送,或无法连接到推送服务'); return; } let sc_data = JSON.parse(JSON.stringify(sc)); API.set_notification( this.state.info.pid, true, sc_data.endpoint, sc_data.keys.auth, sc_data.keys.p256dh, this.props.token, ).then((json) => { // TODO }); }); } do_vote(vote) { this.setState({ loading_status: 'loading', error_msg: null, }); API.add_vote(vote, this.state.info.pid, this.props.token) .then((json) => { if (json.code !== 0) return; localStorage['VOTE_RECORD:' + this.state.info.pid] = vote; this.setState( (prev, props) => ({ info: Object.assign({}, prev.info, { poll: json.data }), loading_status: 'done', }), () => { this.syncState({ info: this.state.info, }); }, ); }) .catch((e) => { console.error(e); this.setState({ loading_status: 'done', error_msg: '' + e, }); }); } report(event, text = '') { console.log(text); let reason = prompt(`举报 #${this.state.info.pid} 的理由:`, text); let should_hide = window.confirm('是否认为此洞应该被删除或隐藏?'); if (reason !== null) { API.report(this.state.info.pid, reason, should_hide, this.props.token) .then((json) => { alert('举报成功'); }) .catch((e) => { alert('举报失败\n' + e); console.error(e); }); } } block(name, type, id, on_complete) { if (window.confirm(`确定拉黑${name}吗?后续将不会收到其发布的任何内容`)) { API.block(type, id, this.props.token) .then((json) => { let data = json.data; alert(`操作成功,当前拉黑数 ${data.curr}`); !!on_complete && on_complete(); }) .catch((e) => { alert('拉黑失败\n' + e); console.error(e); }); } } set_filter_name(name) { this.setState((prevState) => ({ filter_name: name === prevState.filter_name ? null : name, })); } toggle_rev() { this.setState((prevState) => ({ rev: !prevState.rev }), forceCheck); } 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); } } } make_do_delete(token, on_complete = null) { const do_delete = (type, id) => { let note = prompt(`将删除${type}=${id}, 备注:`, '(无)'); if (note !== null) { API.del(type, id, note, token) .then((json) => { alert('删除成功'); on_complete(); }) .catch((e) => { alert('删除失败\n' + e); console.error(e); }); } }; return do_delete; } do_edit_cw(cw, id) { API.update_cw(cw, id, this.props.token) .then((json) => { this.setState( { info: Object.assign({}, this.state.info, { cw: cw }), }, () => { this.syncState({ info: this.state.info, }); }, ); alert('已更新'); }) .catch((e) => { alert('更新失败\n' + e); console.error(e); }); } render() { if (this.state.loading_status === 'loading') return

加载中……

; 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(); // may not need key, for performance // 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]++; }); // 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 } do_delete={this.make_do_delete(this.props.token, () => { window.location.reload(); })} do_edit_cw={this.do_edit_cw.bind(this)} do_vote={this.do_vote.bind(this)} do_block={() => { this.block('洞主', 'post', this.state.info.pid, () => { window.location.reload(); }); }} /> ); return (
{!!this.props.token && (    )} {(this.state.replies.length >= 1 || this.state.rev) && (    )} {!!this.props.token && (    this.toggle_attention()}> {this.state.attention ? ( ) : ( )} )} {!!this.props.token && !!this.state.attention && (    this.toggle_notification()}> )}
{!!this.state.filter_name && (

{ this.set_filter_name(null); }} > 还原  当前只看  {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, i) => (reply.can_del || !reply.is_blocked) && ( { 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 } do_delete={this.make_do_delete( this.props.token, this.load_replies.bind(this), )} do_block={() => { this.block( reply.name, 'comment', reply.cid, this.load_replies.bind(this), ); }} do_report={(e) => { this.report( e, `评论区${reply.name},评论id ${reply.cid}`, ); }} /> ), )} {this.state.rev && main_thread_elem} {this.props.token ? ( ) : (
登录后可以回复树洞
)}
); } } class FlowItemRow extends PureComponent { constructor(props) { super(props); this.needFold = props.info.cw && window.config.whitelist_cw.indexOf('*') === -1 && window.config.whitelist_cw.indexOf(props.info.cw) === -1 && (props.mode === 'list' || this.props.is_quote); this.has_block_words = check_block(props.info); this.color_picker = new ColorPicker(); this.state = { replies: props.info.comments ? parse_replies(props.info.comments, this.color_picker) : [], reply_status: 'done', reply_error: null, info: Object.assign({}, props.info, { variant: {} }), attention: props.info.attention, cached: true, // default no display anything }; } componentDidMount() { // cache from getlist, so always to this to update attention if (!this.props.info.comments) { //if (true || parseInt(this.state.info.reply, 10)) { this.load_replies(null, /*update_count=*/ false); } } // reveal() { // this.setState({ hidden: false }); // } do_vote(vote) { const { info } = this.state; let current_poll = info.poll; current_poll.vote = vote; current_poll.answers.forEach((ans) => { if (ans.option === vote) ans.votes += 1; }); this.setState( { info: Object.assign({}, info, { poll: current_poll }), }, () => { API.add_vote(vote, info.pid, this.props.token) .then((json) => { if (json.code !== 0) return; localStorage['VOTE_RECORD:' + info.pid] = vote; this.setState((prev, props) => ({ info: Object.assign({}, prev.info, { poll: json.data }), })); }) .catch((e) => { console.error(e); alert('投票失败'); }); }, ); } 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(({ data: json, cached }) => { //console.log('>> update', json, json.attention); 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, cached, }), 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, , ); } render() { const { show_sidebar, token, search_param, is_quote, mode } = this.props; let show_pid = load_single_meta(show_sidebar, token, [this.state.info.pid]); let hl_rules = [['pid', PID_RE]]; let parts = split_text(this.state.info.text, hl_rules); //console.log('hl:', parts,this.state.info.pid); let quote_id = null; if (!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 res = (
{ 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) => (reply.can_del || !reply.is_blocked) && ( ), )} {this.state.replies.length > PREVIEW_REPLY_COUNT && (
还有 {this.state.replies.length - PREVIEW_REPLY_COUNT} 条
)}
); if (this.needFold || this.has_block_words) { return ( (!this.has_block_words || mode !== 'list') && (
{ if (!CLICKABLE_TAGS[event.target.tagName.toLowerCase()]) this.show_sidebar(); }} >
{!!this.props.is_quote && (
{/*
*/} {/* 提到*/} {/*
*/}
)}
{!!this.props.do_filter_name && ( { this.props.do_filter_name(DZ_NAME); }} > )} #{this.props.info.pid}   {this.props.info.author_title && ( {`"${this.props.info.author_title}"`} )} {this.props.info.is_tmp && ( 临时账号 )} {this.props.info.cw !== null && ( {this.props.info.cw} )}
) ); } 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}

); // 'done' else return ( ); } } function FlowChunk(props) { return ( {({ value: token }) => (
{!!props.title && } {props.list.map( (info, ind) => (info.can_del || !info.is_blocked) && (
{!!( props.deletion_detect && props.mode === 'list' && ind && props.list[ind - 1].pid - info.pid > 1 ) && (
{props.list[ind - 1].pid - info.pid - 1} 条被删除
)}
), )}
)}
); } function Announcement(props) { const [show, setShow] = useState(true); function do_close() { window.LAST_ANN = props.text; localStorage['LAST_ANN'] = props.text; setShow(false); } return ( show && ( ) ); } export class Flow extends PureComponent { constructor(props) { super(props); let submode = window[props.mode.toUpperCase() + '_SUBMODE_BACKUP']; if (submode === undefined) { submode = props.mode === 'list' ? (window.config.by_c ? 1 : 0) : 0; } this.state = { submode: submode, announcement: window.ANN, }; } get_submode_names(mode) { switch (mode) { case 'list': return ['最新', '最近回复', '近期热门', '随机']; case 'attention': return ['线上关注', '本地收藏']; case 'search': return ['Tag搜索', '全文搜索', '头衔']; default: return []; } } set_submode(submode) { window[this.props.mode.toUpperCase() + '_SUBMODE_BACKUP'] = submode; this.setState({ submode: submode, }); } update_announcement(text) { if (text !== this.state.announcement) { window.ANN = text; localStorage['ANN'] = text; this.setState({ announcement: text, }); } } render() { const { submode, announcement } = this.state; const { mode, show_sidebar, search_text, token } = this.props; const submode_names = this.get_submode_names(mode); return ( <> {announcement && window.LAST_ANN !== announcement && ( )}
{submode_names.map((name, idx) => ( {name} ))}
this.update_announcement(text)} mode={mode} submode={submode} search_text={search_text} token={token} /> ); } } class SubFlow extends PureComponent { constructor(props) { super(props); this.state = { mode: props.mode, search_param: props.search_text, loaded_pages: 0, chunks: { title: '', data: [], }, local_attention_text: null, 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) { const { mode, search_param } = this.state; const { token, submode, update_announcement } = this.props; console.log('fetching page', page); cache(); if (mode === 'list') { API.get_list(page, token, submode) .then((json) => { if (page === 1 && json.data.length) { // update latest_post_id let max_id = window.LATEST_POST_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; } if (page === 1) { window.IS_ADMIN = json.is_admin; window.IS_CANDIDATE = json.is_candidate; window.TITLE = json.custom_title; window.AUTO_BLCOK = json.auto_block_rank; if ( json.custom_title && window.TITLE_SECRET !== json.title_secret ) { window.TITLE_SECRET = json.title_secret; localStorage['TITLE_SECRET'] = json.title_secret; } } json.data.forEach((x) => { if (x.comments) { let comment_json = { code: 0, attention: x.attention, data: x.comments, }; //console.log('My cache', comment_json, x.pid, x.reply) cache().put(x.pid, parseInt(x.reply, 10), comment_json); } }); 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', }), () => { update_announcement(json.announcement); }, ); }) .catch(failed); } else if (mode === 'search' && search_param) { API.get_search(page, search_param, token, submode) .then((json) => { const finished = json.data.length === 0; this.setState((prev, props) => ({ chunks: { title: 'Result for "' + search_param + '"', data: prev.chunks.data.concat(json.data), }, mode: finished ? 'search_finished' : 'search', loading_status: 'done', })); }) .catch(failed); } else if (mode === 'single') { const pid = parseInt(this.state.search_param.substr(1), 10); API.get_single(pid, this.props.token) .then((json) => { let x = json.data; if (x.comments) { let comment_json = { code: 0, attention: x.attention, data: x.comments, }; //console.log('My cache', comment_json, x.pid, x.reply) cache().put(x.pid, parseInt(x.reply, 10), comment_json); } this.setState({ chunks: { title: 'PID = ' + pid, data: [json.data], }, mode: 'single_finished', loading_status: 'done', }); }) .catch(failed); } else if (mode === 'attention') { let use_search = !!this.state.search_param; let use_regex = use_search && !!this.state.search_param.match(/\/.+\//); let regex_search = /.+/; if (use_regex) { try { regex_search = new RegExp(this.state.search_param.slice(1, -1)); } catch (e) { alert(`请检查正则表达式合法性!\n${e}`); regex_search = /.+/; } } console.log(use_search, use_regex); if (this.props.submode === 0) { API.get_attention(this.props.token) .then((json) => { this.setState({ chunks: { title: `${ use_search ? use_regex ? `Result for RegEx ${regex_search.toString()} in ` : `Result for "${this.state.search_param}" in ` : '' }Attention List`, data: !use_search ? json.data : !use_regex ? json.data.filter((post) => { return this.state.search_param .split(' ') .every((keyword) => post.text.includes(keyword)); }) // Not using regex : json.data.filter( (post) => !!post.text.match(regex_search), ), // Using regex }, mode: 'attention_finished', loading_status: 'done', }); if (!use_search) { window.saved_attentions = Array.from( new Set([ ...window.saved_attentions, ...json.data.map((post) => post.pid), ]), ).sort((a, b) => b - a); save_attentions(); } }) .catch(failed); } else if (this.props.submode === 1) { const PERPAGE = 50; let pids = window.saved_attentions .sort((a, b) => b - a) .slice((page - 1) * PERPAGE, page * PERPAGE); if (pids.length) { API.get_multi(pids, this.props.token) .then((json) => { json.data.forEach((x) => { if (x.comments) { let comment_json = { code: 0, attention: x.attention, data: x.comments, }; //console.log('My cache', comment_json, x.pid, x.reply) cache().put(x.pid, parseInt(x.reply, 10), comment_json); } }); this.setState((prev, props) => ({ chunks: { title: 'Attention List: Local', data: prev.chunks.data.concat(json.data), }, loading_status: 'done', })); }) .catch(failed); } else { console.log('local attention finished'); this.setState({ loading_status: 'done', mode: 'attention_finished', }); return; } } } 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 < window.innerHeight && this.state.loading_status === 'done') this.load_page(this.state.loaded_pages + 1); } } 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); } trunc_string(s, max_len) { return s.substr(0, max_len) + (s.length > max_len ? '...' : ''); } gen_export() { this.setState({ can_export: false, local_attention_text: JSON.parse(localStorage['saved_attentions'] || '[]') .map((pid) => `#${pid}`) .join(' '), }); } render() { const should_deletion_detect = localStorage['DELETION_DETECT'] === 'on'; const { mode, chunks, local_attention_text, search_param } = this.state; const { submode, show_sidebar } = this.props; return (
{mode === 'attention' && submode === 1 && local_attention_text === null && ( )} {local_attention_text && ( )} {this.state.loading_status === 'failed' && ( )}  Loading... ) : ( '🄯 2020 copyleft: hole_thu' ) } />
); } } function LocalAttentionEditer(props) { const [text, setText] = useState(props.init_text); const PID_RE = /#(\d+)/g; const save = () => { let pids = [...text.matchAll(PID_RE)] .map((m) => parseInt(m[1])) .sort((a, b) => b - a); console.log(pids); window.saved_attentions = pids; save_attentions(); setText(pids.map((pid) => `#${pid}`).join(' ')); }; return (