import React, { 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,
TAG_RE,
} from './text_splitter';
import {
format_time,
build_highlight_re,
Time,
TitleLine,
ClickHandler,
ColoredSpan,
HighlightedMarkdown,
} from './Common';
import './Flows.css';
import LazyLoad, { forceCheck } from './react-lazyload/src';
import { TokenCtx, ReplyForm } from './UserAction';
import { API } 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 };
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 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,
,
'replace',
);
}
};
}
class Reply extends PureComponent {
constructor(props) {
super(props);
}
render() {
const {
info, color_picker, show_pid, do_filter_name, do_delete,
do_report, do_block
} = this.props;
const author = info.name,
replyText = info.text;
return (
{!!do_filter_name && (
{
do_filter_name(info.name);
}}
>
)}
{(
{info.name}
)}
{!!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 = {
hot_score: props.info.hot_score || 0,
cw: props.info.cw || '',
};
}
copy_link(event) {
event.preventDefault();
copy(
`${event.target.href}${
this.props.info.cw ? ' 【' + this.props.info.cw + '】' : ''
}\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.cw ? '【' + r.cw + '】' : '') + r.text)
.join('\n'),
);
}
on_hot_score_change(event) {
this.setState({
hot_score: event.target.value
});
}
on_cw_change(event) {
this.setState({
cw: event.target.value,
});
}
render() {
const {
info, is_quote, cached, attention, can_del, do_filter_name, do_delete,
do_edit_cw, do_edit_score, timestamp, img_clickable, color_picker,
show_pid, do_vote, do_block
} = this.props;
const { cw, hot_score } = this.state;
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}
{!!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 !== undefined && (do_edit_score ? (
<>
>
) : (
hot score: {info.hot_score}
))}
{ info.poll && (
{})}
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);
});
}
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);
if (reason !== null) {
API.report(this.state.info.pid, reason, this.props.token)
.then((json) => {
alert('举报成功');
})
.catch((e) => {
alert('举报失败');
console.error(e);
});
}
}
block(name, type, id, on_complete) {
if (confirm(`确定拉黑${name}吗?后续将不会收到其发布的任何内容`)) {
API.block(type, id, this.props.token)
.then((json) => {
let data = json.data;
alert(`操作成功,其成为危险用户进度 ${data.curr}/${data.threshold}`)
!!on_complete && on_complete();
})
.catch((e) => {
alert('拉黑失败');
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;
}
make_do_edit_cw(token) {
const do_edit_cw = (cw, id) => {
API.update_cw(cw, id, token)
.then((json) => {
alert('已更新\n刷新列表显示新版本');
})
.catch((e) => {
alert('更新失败\n' + e);
console.error(e);
});
}
return do_edit_cw;
}
make_do_edit_score(token) {
const do_edit_score = (score, id) => {
API.update_score(score, id, token)
.then((json) => {
console.log('已更新');
})
.catch((e) => {
alert('更新失败\n' + e);
console.error(e);
});
}
return do_edit_score;
}
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.make_do_edit_cw(this.props.token)}
do_edit_score={this.make_do_edit_score(this.props.token)}
do_vote={this.do_vote.bind(this)}
do_block={() => {this.block(
'洞主', 'post', this.state.info.pid, () => {window.location.reload();}
)}}
/>
);
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, i) => !reply.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 &&
!props.search_param &&
(window.config.whitelist_cw.indexOf('*')==-1 && window.config.whitelist_cw.indexOf(props.info.cw)==-1) &&
props.mode !== 'attention' && props.mode !== 'attention_finished';
this.state = {
replies: [],
reply_status: 'done',
reply_error: null,
info: Object.assign({}, props.info, { variant: {} }),
hidden: window.config.block_words_v2.some((word) =>
props.info.text.includes(word),
) && !props.info.can_del || this.needFold,
attention:
props.attention_override === null ? false : props.attention_override,
cached: true, // default no display anything
};
this.color_picker = new ColorPicker();
}
componentDidMount() {
// cache from getlist, so always to this to update attention
if (true || parseInt(this.state.info.reply, 10)) {
this.load_replies(null, /*update_count=*/ false);
}
}
// reveal() {
// this.setState({ hidden: false });
// }
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() {
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],
['tag', TAG_RE],
];
if (this.props.search_param) {
hl_rules.push([
'search',
!!this.props.search_param.match(/\/.+\//)
? build_highlight_re(this.props.search_param, ' ', 'gi', true) // Use regex
: build_highlight_re(this.props.search_param, ' ', 'gi'), // Don't use regex
]);
}
let parts = split_text(this.state.info.text, hl_rules);
//console.log('hl:', parts,this.state.info.pid);
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 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.blocked && (
))}
{this.state.replies.length > PREVIEW_REPLY_COUNT && (
还有 {this.state.replies.length - PREVIEW_REPLY_COUNT} 条
)}
);
if (this.state.hidden) {
return this.needFold && (
{
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.cw !== null && (
{this.props.info.cw}
)}
{this.needFold ? '已折叠' : '已屏蔽'}
);
}
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 (
);
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.blocked && (
{!!(
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 = {
submode: this.props.mode == 'list' ? (
window.LIST_SUBMOD_BACKUP !== undefined ? window.LIST_SUBMOD_BACKUP : (
(window.config.by_c ? 1 : 0)
)
) : 0,
}
}
get_submode_names(mode) {
switch(mode) {
case('list'):
return ['最新', '最近回复', '近期热门', '随机'];
case('attention'):
return ['线上', '本地']
}
return []
}
set_submode(submode) {
if (this.props.mode == 'list')
window.LIST_SUBMOD_BACKUP = submode;
this.setState({
submode: submode,
});
}
render() {
const { submode } = this.state;
const submode_names = this.get_submode_names(this.props.mode)
return (
<>
{submode_names.map((name, idx) => (
{name}
))}
>
)
}
}
class SubFlow extends PureComponent {
constructor(props) {
super(props);
this.state = {
mode: props.mode,
search_param: props.search_text,
loaded_pages: 0,
chunks: {
title: '',
data: [],
},
export_text: '',
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);
console.log(err.to_string);
this.setState((prev, props) => ({
loaded_pages: prev.loaded_pages - 1,
loading_status: 'failed',
error_msg: prev.loaded_pages > 1 ? '找不到更多了' : '' + 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);
cache();
if (this.state.mode === 'list') {
API.get_list(page, this.props.token, this.props.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;
}
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',
}));
})
.catch(failed);
} else if (this.state.mode === 'search' && this.state.search_param) {
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) => {
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 (this.state.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().reverse();
save_attentions();
}
})
.catch(failed);
} else if (this.props.submode === 1) {
this.setState({
title: 'Attention List: Local',
data: [],
export_text: `以下是浏览器本地保存的关注列表,将在下个版本提供直接展示\n\n#${
window.saved_attentions.join('\n#')
}`
});
}
} 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,
export_text: "以下是你关注的洞及摘要,复制保存到本地吧。\n\n" + this.state.chunks.data.map(
p => `#${p.pid}: ${
this.trunc_string(p.text.replaceAll('\n', ' '), 50)
}`).join('\n\n')
});
}
render() {
const should_deletion_detect = localStorage['DELETION_DETECT'] === 'on';
return (
{this.state.mode === 'attention_finished' && this.props.submode == 0 && (
)}
{this.state.export_text && (
)}
{this.state.loading_status === 'failed' && (
)}
Loading...
) : (
'🄯 2020 copyleft: hole_thu'
)
}
/>
);
}
}