prepare for merging https://github.com/AllanChain/PKUHoleCommunity/
This commit is contained in:
18
src/App.js
18
src/App.js
@@ -60,15 +60,32 @@ class App extends Component {
|
||||
this.setState((prevState) => {
|
||||
let ns = prevState.sidebar_stack.slice();
|
||||
if (mode === 'push') {
|
||||
if (ns.length === 1) {
|
||||
document.body.style.top = `-${window.scrollY}px`;
|
||||
document.body.style.position = 'fixed';
|
||||
document.body.style.width = '100vw'; // Be responsive with fixed position
|
||||
}
|
||||
if (ns.length > MAX_SIDEBAR_STACK_SIZE) ns.splice(1, 1);
|
||||
ns = ns.concat([[title, content]]);
|
||||
} else if (mode === 'pop') {
|
||||
if (ns.length === 1) return;
|
||||
if (ns.length === 2) {
|
||||
const scrollY = document.body.style.top;
|
||||
document.body.style.position = '';
|
||||
document.body.style.top = '';
|
||||
document.body.style.width = '';
|
||||
window.scrollTo(0, parseInt(scrollY || '0') * -1);
|
||||
}
|
||||
ns.pop();
|
||||
} else if (mode === 'replace') {
|
||||
ns.pop();
|
||||
ns = ns.concat([[title, content]]);
|
||||
} else if (mode === 'clear') {
|
||||
const scrollY = document.body.style.top;
|
||||
document.body.style.position = '';
|
||||
document.body.style.top = '';
|
||||
document.body.style.width = '';
|
||||
window.scrollTo(0, parseInt(scrollY || '0') * -1);
|
||||
ns = [[null, null]];
|
||||
} else throw new Error('bad show_sidebar mode');
|
||||
return {
|
||||
@@ -103,6 +120,7 @@ class App extends Component {
|
||||
<Title
|
||||
show_sidebar={this.show_sidebar_bound}
|
||||
set_mode={this.set_mode_bound}
|
||||
mode={this.state.mode}
|
||||
/>
|
||||
<TokenCtx.Consumer>
|
||||
{(token) => (
|
||||
|
||||
@@ -24,17 +24,30 @@ function escape_regex(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||
}
|
||||
|
||||
export function build_highlight_re(txt, split, option = 'g') {
|
||||
return txt
|
||||
? new RegExp(
|
||||
`(${txt
|
||||
.split(split)
|
||||
.filter((x) => !!x)
|
||||
.map(escape_regex)
|
||||
.join('|')})`,
|
||||
option,
|
||||
)
|
||||
: /^$/g;
|
||||
export function build_highlight_re(
|
||||
txt,
|
||||
split = ' ',
|
||||
option = 'g',
|
||||
isRegex = false,
|
||||
) {
|
||||
if (isRegex) {
|
||||
try {
|
||||
return new RegExp('(' + txt.slice(1, -1) + ')', option);
|
||||
} catch (e) {
|
||||
return /^$/g;
|
||||
}
|
||||
} else {
|
||||
return txt
|
||||
? new RegExp(
|
||||
`(${txt
|
||||
.split(split)
|
||||
.filter((x) => !!x)
|
||||
.map(escape_regex)
|
||||
.join('|')})`,
|
||||
option,
|
||||
)
|
||||
: /^$/g;
|
||||
}
|
||||
}
|
||||
|
||||
export function ColoredSpan(props) {
|
||||
@@ -142,7 +155,7 @@ export class HighlightedMarkdown extends Component {
|
||||
node.type === 'text' &&
|
||||
(!node.parent ||
|
||||
!node.parent.attribs ||
|
||||
node.parent.attribs['encoding'] != 'application/x-tex')
|
||||
node.parent.attribs['encoding'] !== 'application/x-tex')
|
||||
); // pid, nickname, search
|
||||
},
|
||||
processNode(node, children, index) {
|
||||
|
||||
@@ -4,6 +4,23 @@
|
||||
position: sticky;
|
||||
}
|
||||
|
||||
.config-description {
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
.config-select {
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
.config-textarea {
|
||||
margin-top: 0.5em;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-width: 100%;
|
||||
height: 7em;
|
||||
min-height: 2em;
|
||||
}
|
||||
|
||||
.bg-preview {
|
||||
height: 18em;
|
||||
width: 32em;
|
||||
@@ -11,4 +28,4 @@
|
||||
max-width: 100%;
|
||||
margin: .5em auto 1em;
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,.4);
|
||||
}
|
||||
}
|
||||
|
||||
137
src/Config.js
137
src/Config.js
@@ -1,4 +1,4 @@
|
||||
import React, { Component, PureComponent } from 'react';
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import './Config.css';
|
||||
|
||||
@@ -26,7 +26,7 @@ const DEFAULT_CONFIG = {
|
||||
pressure: false,
|
||||
easter_egg: true,
|
||||
color_scheme: 'default',
|
||||
fold: true,
|
||||
block_words: [],
|
||||
};
|
||||
|
||||
export function load_config() {
|
||||
@@ -117,7 +117,11 @@ class ConfigBackground extends PureComponent {
|
||||
<div>
|
||||
<p>
|
||||
<b>背景图片:</b>
|
||||
<select value={img_select} onChange={this.on_select.bind(this)}>
|
||||
<select
|
||||
className="config-select"
|
||||
value={img_select}
|
||||
onChange={this.on_select.bind(this)}
|
||||
>
|
||||
{Object.keys(BUILTIN_IMGS).map((key) => (
|
||||
<option key={key} value={key}>
|
||||
{BUILTIN_IMGS[key]}
|
||||
@@ -127,6 +131,7 @@ class ConfigBackground extends PureComponent {
|
||||
<option value="##color">纯色背景……</option>
|
||||
</select>
|
||||
|
||||
<small>#background_img</small>
|
||||
{img_select === '##other' && (
|
||||
<input
|
||||
type="url"
|
||||
@@ -182,6 +187,7 @@ class ConfigColorScheme extends PureComponent {
|
||||
<p>
|
||||
<b>夜间模式:</b>
|
||||
<select
|
||||
className="config-select"
|
||||
value={this.state.color_scheme}
|
||||
onChange={this.on_select.bind(this)}
|
||||
>
|
||||
@@ -189,14 +195,105 @@ class ConfigColorScheme extends PureComponent {
|
||||
<option value="light">始终浅色模式</option>
|
||||
<option value="dark">始终深色模式</option>
|
||||
</select>
|
||||
<small>#color_scheme</small>
|
||||
<small>#color_scheme</small>
|
||||
</p>
|
||||
<p className="config-description">
|
||||
选择浅色或深色模式,深色模式下将会调暗图片亮度
|
||||
</p>
|
||||
<p>选择浅色或深色模式,深色模式下将会调暗图片亮度</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ConfigTextArea extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
[props.id]: window.config[props.id],
|
||||
};
|
||||
}
|
||||
|
||||
save_changes() {
|
||||
this.props.callback({
|
||||
[this.props.id]: this.props.sift(this.state[this.props.id]),
|
||||
});
|
||||
}
|
||||
|
||||
on_change(e) {
|
||||
let value = this.props.parse(e.target.value);
|
||||
this.setState(
|
||||
{
|
||||
[this.props.id]: value,
|
||||
},
|
||||
this.save_changes.bind(this),
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<label>
|
||||
<p>
|
||||
<b>{this.props.name}</b> <small>#{this.props.id}</small>
|
||||
</p>
|
||||
<p className="config-description">{this.props.description}</p>
|
||||
<textarea
|
||||
name={'config-' + this.props.id}
|
||||
id={`config-textarea-${this.props.id}`}
|
||||
className="config-textarea"
|
||||
value={this.props.display(this.state[this.props.id])}
|
||||
onChange={this.on_change.bind(this)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* class ConfigBlockWords extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
block_words: window.config.block_words,
|
||||
};
|
||||
}
|
||||
|
||||
save_changes() {
|
||||
this.props.callback({
|
||||
block_words: this.state.block_words.filter((v) => v),
|
||||
});
|
||||
}
|
||||
|
||||
on_change(e) {
|
||||
// Filter out those blank lines
|
||||
let value = e.target.value.split('\n');
|
||||
this.setState(
|
||||
{
|
||||
block_words: value,
|
||||
},
|
||||
this.save_changes.bind(this),
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
{' '}
|
||||
<b>设置屏蔽词 </b>
|
||||
</p>
|
||||
<p>
|
||||
<textarea
|
||||
className="block-words"
|
||||
value={this.state.block_words.join('\n')}
|
||||
onChange={this.on_change.bind(this)}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} */
|
||||
|
||||
class ConfigSwitch extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -230,11 +327,11 @@ class ConfigSwitch extends PureComponent {
|
||||
checked={this.state.switch}
|
||||
onChange={this.on_change.bind(this)}
|
||||
/>
|
||||
<b>{this.props.name}</b>
|
||||
<small>#{this.props.id}</small>
|
||||
<b>{this.props.name}</b>
|
||||
<small>#{this.props.id}</small>
|
||||
</label>
|
||||
</p>
|
||||
<p>{this.props.description}</p>
|
||||
<p className="config-description">{this.props.description}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -285,9 +382,29 @@ export class ConfigUI extends PureComponent {
|
||||
</p>
|
||||
</div>
|
||||
<div className="box">
|
||||
<ConfigBackground callback={this.save_changes_bound} />
|
||||
<ConfigBackground
|
||||
id="background"
|
||||
callback={this.save_changes_bound}
|
||||
/>
|
||||
<hr />
|
||||
<ConfigColorScheme callback={this.save_changes_bound} />
|
||||
<ConfigColorScheme
|
||||
id="color-scheme"
|
||||
callback={this.save_changes_bound}
|
||||
/>
|
||||
<hr />
|
||||
{/* <ConfigBlockWords
|
||||
id="block-words"
|
||||
callback={this.save_changes_bound}
|
||||
/> */}
|
||||
<ConfigTextArea
|
||||
id="block_words"
|
||||
callback={this.save_changes_bound}
|
||||
name="设置屏蔽词"
|
||||
description={'包含屏蔽词的树洞会被折叠,每行写一个屏蔽词'}
|
||||
display={(array) => array.join('\n')}
|
||||
sift={(array) => array.filter((v) => v)}
|
||||
parse={(string) => string.split('\n')}
|
||||
/>
|
||||
<hr />
|
||||
<ConfigSwitch
|
||||
callback={this.save_changes_bound}
|
||||
|
||||
211
src/Flows.js
211
src/Flows.js
@@ -1,4 +1,4 @@
|
||||
import React, { Component, PureComponent } from 'react';
|
||||
import React, { PureComponent } from 'react';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { ColorPicker } from './color_picker';
|
||||
import {
|
||||
@@ -19,11 +19,11 @@ import {
|
||||
HighlightedMarkdown,
|
||||
} from './Common';
|
||||
import './Flows.css';
|
||||
import LazyLoad from './react-lazyload/src';
|
||||
import LazyLoad, { forceCheck } from './react-lazyload/src';
|
||||
import { AudioWidget } from './AudioWidget';
|
||||
import { TokenCtx, ReplyForm } from './UserAction';
|
||||
|
||||
import { API, THUHOLE_API_ROOT } from './flows_api';
|
||||
import { API } from './flows_api';
|
||||
|
||||
const IMAGE_BASE = 'https://img.thuhole.com/';
|
||||
const IMAGE_BAK_BASE = 'https://img2.thuhole.com/';
|
||||
@@ -52,7 +52,7 @@ window.LATEST_POST_ID = parseInt(localStorage['_LATEST_POST_ID'], 10) || 0;
|
||||
const DZ_NAME = '洞主';
|
||||
|
||||
function load_single_meta(show_sidebar, token) {
|
||||
return (pid, replace = false) => {
|
||||
return async (pid, replace = false) => {
|
||||
let color_picker = new ColorPicker();
|
||||
let title_elem = '树洞 #' + pid;
|
||||
show_sidebar(
|
||||
@@ -60,56 +60,44 @@ function load_single_meta(show_sidebar, token) {
|
||||
<div className="box box-tip">正在加载 #{pid}</div>,
|
||||
replace ? 'replace' : 'push',
|
||||
);
|
||||
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,
|
||||
<FlowSidebar
|
||||
key={+new Date()}
|
||||
info={single.data}
|
||||
replies={replies.data}
|
||||
attention={replies.attention}
|
||||
token={token}
|
||||
show_sidebar={show_sidebar}
|
||||
color_picker={color_picker}
|
||||
deletion_detect={localStorage['DELETION_DETECT'] === 'on'}
|
||||
/>,
|
||||
'replace',
|
||||
);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
show_sidebar(
|
||||
title_elem,
|
||||
<div className="box box-tip">
|
||||
<p>
|
||||
<a
|
||||
onClick={() => load_single_meta(show_sidebar, token)(pid, true)}
|
||||
>
|
||||
重新加载
|
||||
</a>
|
||||
</p>
|
||||
<p>{'' + e}</p>
|
||||
</div>,
|
||||
'replace',
|
||||
);
|
||||
});
|
||||
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,
|
||||
<FlowSidebar
|
||||
key={+new Date()}
|
||||
info={single.data}
|
||||
replies={replies.data}
|
||||
attention={replies.attention}
|
||||
token={token}
|
||||
show_sidebar={show_sidebar}
|
||||
color_picker={color_picker}
|
||||
deletion_detect={localStorage['DELETION_DETECT'] === 'on'}
|
||||
/>,
|
||||
'replace',
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
show_sidebar(
|
||||
title_elem,
|
||||
<div className="box box-tip">
|
||||
<p>
|
||||
<a onClick={() => load_single_meta(show_sidebar, token)(pid, true)}>
|
||||
重新加载
|
||||
</a>
|
||||
</p>
|
||||
<p>{'' + e}</p>
|
||||
</div>,
|
||||
'replace',
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -213,6 +201,9 @@ class FlowItem extends PureComponent {
|
||||
parseInt(props.info.pid, 10) > window.LATEST_POST_ID && (
|
||||
<div className="flow-item-dot" />
|
||||
)}
|
||||
{!!this.props.attention && !this.props.cached && (
|
||||
<div className="flow-item-dot" />
|
||||
)}
|
||||
<div className="box-header">
|
||||
{!!this.props.do_filter_name && (
|
||||
<span
|
||||
@@ -446,9 +437,7 @@ class FlowSidebar extends PureComponent {
|
||||
}
|
||||
|
||||
toggle_rev() {
|
||||
this.setState((prevState) => ({
|
||||
rev: !prevState.rev,
|
||||
}));
|
||||
this.setState((prevState) => ({ rev: !prevState.rev }), forceCheck);
|
||||
}
|
||||
|
||||
show_reply_bar(name, event) {
|
||||
@@ -479,9 +468,10 @@ class FlowSidebar extends PureComponent {
|
||||
: 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 view_mode_key =
|
||||
// (this.state.rev ? 'y-' : 'n-') + (this.state.filter_name || 'null');
|
||||
|
||||
let replies_cnt = { [DZ_NAME]: 1 };
|
||||
replies_to_show.forEach((r) => {
|
||||
@@ -600,9 +590,9 @@ class FlowSidebar extends PureComponent {
|
||||
条回复被删除
|
||||
</div>
|
||||
)}
|
||||
{replies_to_show.map((reply) => (
|
||||
{replies_to_show.map((reply, i) => (
|
||||
<LazyLoad
|
||||
key={reply.cid + view_mode_key}
|
||||
key={i}
|
||||
offset={1500}
|
||||
height="5em"
|
||||
overflow={true}
|
||||
@@ -630,7 +620,7 @@ class FlowSidebar extends PureComponent {
|
||||
</LazyLoad>
|
||||
))}
|
||||
{this.state.rev && main_thread_elem}
|
||||
{!!this.props.token ? (
|
||||
{this.props.token ? (
|
||||
<ReplyForm
|
||||
pid={this.state.info.pid}
|
||||
token={this.props.token}
|
||||
@@ -653,8 +643,12 @@ class FlowItemRow extends PureComponent {
|
||||
reply_status: 'done',
|
||||
reply_error: null,
|
||||
info: Object.assign({}, props.info, { variant: {} }),
|
||||
hidden: window.config.block_words.some((word) =>
|
||||
props.info.text.includes(word),
|
||||
),
|
||||
attention:
|
||||
props.attention_override === null ? false : props.attention_override,
|
||||
cached: true, // default no display anything
|
||||
};
|
||||
this.color_picker = new ColorPicker();
|
||||
}
|
||||
@@ -665,6 +659,10 @@ class FlowItemRow extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
reveal() {
|
||||
this.setState({ hidden: false });
|
||||
}
|
||||
|
||||
load_replies(callback, update_count = true) {
|
||||
console.log('fetching reply', this.state.info.pid);
|
||||
this.setState({
|
||||
@@ -677,7 +675,7 @@ class FlowItemRow extends PureComponent {
|
||||
this.color_picker,
|
||||
parseInt(this.state.info.reply),
|
||||
)
|
||||
.then((json) => {
|
||||
.then(({ data: json, cached }) => {
|
||||
this.setState(
|
||||
(prev, props) => ({
|
||||
replies: json.data,
|
||||
@@ -695,6 +693,7 @@ class FlowItemRow extends PureComponent {
|
||||
attention: !!json.attention,
|
||||
reply_status: 'done',
|
||||
reply_error: null,
|
||||
cached,
|
||||
}),
|
||||
callback,
|
||||
);
|
||||
@@ -740,11 +739,14 @@ class FlowItemRow extends PureComponent {
|
||||
['pid', PID_RE],
|
||||
['nickname', NICKNAME_RE],
|
||||
];
|
||||
if (this.props.search_param)
|
||||
if (this.props.search_param) {
|
||||
hl_rules.push([
|
||||
'search',
|
||||
build_highlight_re(this.props.search_param, ' ', 'gi'),
|
||||
!!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);
|
||||
|
||||
let quote_id = null;
|
||||
@@ -787,6 +789,7 @@ class FlowItemRow extends PureComponent {
|
||||
color_picker={this.color_picker}
|
||||
show_pid={show_pid}
|
||||
replies={this.state.replies}
|
||||
cached={this.state.cached}
|
||||
fold={needFold}
|
||||
/>
|
||||
{!needFold && (
|
||||
@@ -826,6 +829,54 @@ class FlowItemRow extends PureComponent {
|
||||
</div>
|
||||
);
|
||||
|
||||
if (this.state.hidden) {
|
||||
return (
|
||||
<div
|
||||
className="flow-item-row flow-item-row-with-prompt"
|
||||
onClick={() => this.reveal()}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
'flow-item' + (this.props.is_quote ? ' flow-item-quote' : '')
|
||||
}
|
||||
>
|
||||
{!!this.props.is_quote && (
|
||||
<div className="quote-tip black-outline">
|
||||
<div>
|
||||
<span className="icon icon-quote" />
|
||||
</div>
|
||||
<div>
|
||||
<small>提到</small>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="box">
|
||||
<div className="box-header">
|
||||
{!!this.props.do_filter_name && (
|
||||
<span
|
||||
className="reply-header-badge clickable"
|
||||
onClick={() => {
|
||||
this.props.do_filter_name(DZ_NAME);
|
||||
}}
|
||||
>
|
||||
<span className="icon icon-locate" />
|
||||
</span>
|
||||
)}
|
||||
<code className="box-id">#{this.props.info.pid}</code>
|
||||
|
||||
{this.props.info.tag !== null && (
|
||||
<span className="box-header-tag">{this.props.info.tag}</span>
|
||||
)}
|
||||
<Time stamp={this.props.info.timestamp} />
|
||||
<span className="box-header-badge">已隐藏</span>
|
||||
<div style={{ clear: 'both' }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return !needFold && quote_id ? (
|
||||
<div>
|
||||
{res}
|
||||
@@ -1061,12 +1112,38 @@ export class Flow extends PureComponent {
|
||||
})
|
||||
.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);
|
||||
API.get_attention(this.props.token)
|
||||
.then((json) => {
|
||||
this.setState({
|
||||
chunks: {
|
||||
title: 'Attention List',
|
||||
data: json.data,
|
||||
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',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Component, PureComponent } from 'react';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { THUHOLE_API_ROOT, get_json, API_VERSION_PARAM } from './flows_api';
|
||||
import { Time } from './Common';
|
||||
|
||||
@@ -65,10 +65,10 @@ export class MessageViewer extends PureComponent {
|
||||
);
|
||||
else if (this.state.loading_status === 'done')
|
||||
return this.state.msg.map((msg) => (
|
||||
<div className="box">
|
||||
<div className="box" key={msg.timestamp}>
|
||||
<div className="box-header">
|
||||
<Time stamp={msg.timestamp} short={false} />
|
||||
<b>{msg.title}</b>
|
||||
<b>{msg.title}</b>
|
||||
</div>
|
||||
<div className="box-content">
|
||||
<pre>{msg.content}</pre>
|
||||
|
||||
@@ -31,14 +31,21 @@
|
||||
user-select: text;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
/* think twice before you use 100vh
|
||||
https://dev.to/peiche/100vh-behavior-on-chrome-2hm8
|
||||
*/
|
||||
height: 100%;
|
||||
background-color: rgba(255,255,255,.7);
|
||||
overflow-y: auto;
|
||||
padding-top: 3em;
|
||||
padding-bottom: 1em;
|
||||
/* padding-bottom: 1em; */ /* move to sidebar-content */
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
backdrop-filter: blur(0px); /* fix scroll performance issues */
|
||||
}
|
||||
|
||||
.root-dark-mode .sidebar {
|
||||
background-color: hsla(0,0%,5%,.4);
|
||||
}
|
||||
@@ -118,15 +125,27 @@
|
||||
pointer-events: initial;
|
||||
}
|
||||
|
||||
.sidebar, .sidebar-title {
|
||||
|
||||
/* move all padding to sidebar-content - the scrolling div (overflow-y: auto) */
|
||||
/* .sidebar, */
|
||||
.sidebar-content,
|
||||
.sidebar-title {
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1300px) {
|
||||
.sidebar, .sidebar-title {
|
||||
left: calc(100% - 550px);
|
||||
width: 550px;
|
||||
width: 550px;/*
|
||||
padding-left: .5em;
|
||||
padding-right: .5em; */
|
||||
}
|
||||
.sidebar-content, .sidebar-title {
|
||||
padding-left: .5em;
|
||||
padding-right: .5em;
|
||||
}
|
||||
@@ -135,6 +154,10 @@
|
||||
.sidebar, .sidebar-title {
|
||||
left: 27px;
|
||||
width: calc(100% - 27px);
|
||||
/* padding-left: .25em;
|
||||
padding-right: .25em; */
|
||||
}
|
||||
.sidebar-content, .sidebar-title {
|
||||
padding-left: .25em;
|
||||
padding-right: .25em;
|
||||
}
|
||||
@@ -142,8 +165,19 @@
|
||||
|
||||
.sidebar-flow-item {
|
||||
display: block;
|
||||
/*overflow-x: hidden;*/
|
||||
}
|
||||
.sidebar-flow-item .box {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-content-show {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar-content-hide{
|
||||
/* will make lazyload working correctly */
|
||||
height: 0;
|
||||
padding: 0;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
import React, { Component, PureComponent } from 'react';
|
||||
import React, { PureComponent } from 'react';
|
||||
import './Sidebar.css';
|
||||
|
||||
export class Sidebar extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.sidebar_ref = React.createRef();
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
do_close() {
|
||||
this.props.show_sidebar(null, null, 'clear');
|
||||
}
|
||||
@@ -24,9 +17,24 @@ export class Sidebar extends PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
let [cur_title, cur_content] = this.props.stack[
|
||||
this.props.stack.length - 1
|
||||
];
|
||||
// hide old contents to remember state
|
||||
let contents = this.props.stack.map(
|
||||
({ 1: content }, i) =>
|
||||
content && (
|
||||
<div
|
||||
key={i}
|
||||
className={
|
||||
'sidebar-content ' +
|
||||
(i === this.props.stack.length - 1
|
||||
? 'sidebar-content-show'
|
||||
: 'sidebar-content-hide')
|
||||
}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
),
|
||||
);
|
||||
let cur_title = this.props.stack[this.props.stack.length - 1][0];
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
@@ -42,9 +50,7 @@ export class Sidebar extends PureComponent {
|
||||
e.target.click();
|
||||
}}
|
||||
/>
|
||||
<div ref={this.sidebar_ref} className="sidebar">
|
||||
{cur_content}
|
||||
</div>
|
||||
<div className="sidebar">{contents}</div>
|
||||
<div className="sidebar-title">
|
||||
<a className="no-underline" onClick={this.do_close_bound}>
|
||||
|
||||
|
||||
12
src/Title.js
12
src/Title.js
@@ -1,4 +1,4 @@
|
||||
import React, { Component, PureComponent } from 'react';
|
||||
import React, { PureComponent } from 'react';
|
||||
// import {AppSwitcher} from './infrastructure/widgets';
|
||||
import { InfoSidebar, PostForm } from './UserAction';
|
||||
import { TokenCtx } from './UserAction';
|
||||
@@ -67,7 +67,11 @@ class ControlBar extends PureComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
const mode = this.state.search_text.startsWith('#') ? 'single' : 'search';
|
||||
const mode = this.state.search_text.startsWith('#')
|
||||
? 'single'
|
||||
: this.props.mode !== 'attention'
|
||||
? 'search'
|
||||
: 'attention';
|
||||
this.set_mode(mode, this.state.search_text || '');
|
||||
}
|
||||
}
|
||||
@@ -112,7 +116,9 @@ class ControlBar extends PureComponent {
|
||||
<input
|
||||
className="control-search"
|
||||
value={this.state.search_text}
|
||||
placeholder="搜索 或 #树洞号"
|
||||
placeholder={`${
|
||||
this.props.mode === 'attention' ? '在关注列表中' : ''
|
||||
}搜索 或 #PID`}
|
||||
onChange={this.on_change_bound}
|
||||
onKeyPress={this.on_keypress_bound}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Component, PureComponent } from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
API_BASE,
|
||||
SafeTextarea,
|
||||
|
||||
@@ -114,6 +114,7 @@ class Cache {
|
||||
data_str: this.encrypt(pid, data),
|
||||
last_access: +new Date(),
|
||||
});
|
||||
console.log('comment cache put', pid);
|
||||
if (++this.added_items_since_maintenance === MAINTENANCE_STEP)
|
||||
setTimeout(this.maintenance.bind(this), 1);
|
||||
});
|
||||
@@ -126,7 +127,7 @@ class Cache {
|
||||
const tx = this.db.transaction(['comment'], 'readwrite');
|
||||
const store = tx.objectStore('comment');
|
||||
let req = store.delete(pid);
|
||||
//console.log('comment cache delete',pid);
|
||||
console.log('comment cache delete', pid);
|
||||
req.onerror = () => {
|
||||
console.warn('comment cache delete failed ', pid);
|
||||
return resolve();
|
||||
|
||||
206
src/flows_api.js
206
src/flows_api.js
@@ -13,127 +13,98 @@ export { get_json };
|
||||
|
||||
const SEARCH_PAGESIZE = 50;
|
||||
|
||||
const handle_response = async (response, notify = false) => {
|
||||
let json = await get_json(response);
|
||||
if (json.code !== 0) {
|
||||
if (json.msg) {
|
||||
if (notify) alert(json.msg);
|
||||
else throw new Error(json.msg);
|
||||
} else throw new Error(JSON.stringify(json));
|
||||
}
|
||||
return json;
|
||||
};
|
||||
|
||||
const parse_replies = (replies, color_picker) =>
|
||||
replies
|
||||
.sort((a, b) => parseInt(a.cid, 10) - parseInt(b.cid, 10))
|
||||
.map((info) => {
|
||||
info._display_color = color_picker.get(info.name);
|
||||
info.variant = {};
|
||||
return info;
|
||||
});
|
||||
|
||||
export const API = {
|
||||
load_replies: (pid, token, color_picker, cache_version) => {
|
||||
load_replies: async (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;
|
||||
});
|
||||
let response = await fetch(
|
||||
API_BASE + '/api.php?action=getcomment&pid=' + pid + token_param(token),
|
||||
);
|
||||
let json = await handle_response(response);
|
||||
// Why delete then put ??
|
||||
cache().put(pid, cache_version, json);
|
||||
json.data = parse_replies(json.data, color_picker);
|
||||
return json;
|
||||
},
|
||||
|
||||
load_replies_with_cache: (pid, token, color_picker, cache_version) => {
|
||||
load_replies_with_cache: async (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);
|
||||
});
|
||||
let json = await cache().get(pid, cache_version);
|
||||
if (json) {
|
||||
json.data = parse_replies(json.data, color_picker);
|
||||
return { data: json, cached: true };
|
||||
} else {
|
||||
json = await API.load_replies(pid, token, color_picker, cache_version);
|
||||
return { data: json, cached: !json };
|
||||
}
|
||||
},
|
||||
|
||||
set_attention: (pid, attention, token) => {
|
||||
set_attention: async (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',
|
||||
let response = await fetch(
|
||||
API_BASE + '/api.php?action=attention' + token_param(token),
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: data,
|
||||
},
|
||||
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;
|
||||
});
|
||||
);
|
||||
// Delete cache to update `attention` on next reload
|
||||
cache().delete(pid);
|
||||
return handle_response(response, true);
|
||||
},
|
||||
|
||||
report: (pid, reason, token) => {
|
||||
report: async (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',
|
||||
let response = await fetch(
|
||||
API_BASE + '/api.php?action=report' + token_param(token),
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: data,
|
||||
},
|
||||
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;
|
||||
});
|
||||
);
|
||||
return handle_response(response, true);
|
||||
},
|
||||
|
||||
get_list: (page, token) => {
|
||||
return fetch(
|
||||
get_list: async (page, token) => {
|
||||
let response = await 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;
|
||||
});
|
||||
);
|
||||
return handle_response(response);
|
||||
},
|
||||
|
||||
get_search: (page, keyword, token) => {
|
||||
return fetch(
|
||||
get_search: async (page, keyword, token) => {
|
||||
let response = await fetch(
|
||||
API_BASE +
|
||||
'/api.php?action=search' +
|
||||
'&pagesize=' +
|
||||
@@ -143,40 +114,21 @@ export const API = {
|
||||
'&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;
|
||||
});
|
||||
);
|
||||
return handle_response(response);
|
||||
},
|
||||
|
||||
get_single: (pid, token) => {
|
||||
return fetch(
|
||||
get_single: async (pid, token) => {
|
||||
let response = await 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;
|
||||
});
|
||||
);
|
||||
return handle_response(response);
|
||||
},
|
||||
|
||||
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;
|
||||
});
|
||||
get_attention: async (token) => {
|
||||
let response = await fetch(
|
||||
API_BASE + '/api.php?action=getattention' + token_param(token),
|
||||
);
|
||||
return handle_response(response);
|
||||
},
|
||||
};
|
||||
|
||||
Submodule src/infrastructure updated: 43b48087b1...e3c39ff4e3
Reference in New Issue
Block a user