forked from newthuhole/hole_thu_frontend
prepare for merging https://github.com/AllanChain/PKUHoleCommunity/
This commit is contained in:
@@ -3,5 +3,5 @@ module.exports = {
|
||||
tabWidth: 2,
|
||||
semi: true,
|
||||
singleQuote: true,
|
||||
endOfLine: 'auto',
|
||||
}
|
||||
endOfLine: 'auto'
|
||||
};
|
||||
|
||||
7381
package-lock.json
generated
7381
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
28
package.json
28
package.json
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "webhole",
|
||||
"version": "0.1.0",
|
||||
"version": "0.3.1",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"copy-to-clipboard": "^3.0.8",
|
||||
"copy-to-clipboard": "^3.3.1",
|
||||
"fix-orientation": "^1.1.0",
|
||||
"gh-pages": "^3.0.0",
|
||||
"highlight.js": "^10.1.1",
|
||||
@@ -12,11 +12,11 @@
|
||||
"markdown-it": "^11.0.0",
|
||||
"markdown-it-katex": "^2.0.3",
|
||||
"pressure": "^2.1.2",
|
||||
"react": "^16.9.0",
|
||||
"react-dom": "^16.9.0",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.0",
|
||||
"react-google-recaptcha-v3": "^1.5.2",
|
||||
"react-scripts": "^3.1.1",
|
||||
"react-timeago": "^4.1.9"
|
||||
"react-scripts": "^3.4.1",
|
||||
"react-timeago": "^4.4.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
@@ -24,9 +24,10 @@
|
||||
"test": "echo 'skipped react-scripts test --env=jsdom'",
|
||||
"predeploy": "",
|
||||
"deploy": "gh-pages -d build -m $(TZ=Asia/Shanghai date +\"%y%m%d%H%M%S\")",
|
||||
"eject": "react-scripts eject"
|
||||
"eject": "react-scripts eject",
|
||||
"lint": "eslint --fix src/*.js"
|
||||
},
|
||||
"homepage": "https://://cdn.jsdelivr.net/gh/thuhole/webhole@gh-pages",
|
||||
"homepage": ".",
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
@@ -38,5 +39,16 @@
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-plugin-import": "^2.21.2",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^3.1.3",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.20.0",
|
||||
"eslint-plugin-standard": "^4.0.1",
|
||||
"prettier": "^2.0.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
@font-face {
|
||||
font-family: 'icomoon';
|
||||
src: url('icomoon.eot?f9daqg');
|
||||
src: url('icomoon.eot?f9daqg#iefix') format('embedded-opentype'),
|
||||
url('icomoon.ttf?f9daqg') format('truetype'),
|
||||
url('icomoon.woff?f9daqg') format('woff'),
|
||||
url('icomoon.svg?f9daqg#icomoon') format('svg');
|
||||
src:
|
||||
url('icomoon.ttf?8qh3rt') format('truetype'),
|
||||
url('icomoon.woff?8qh3rt') format('woff'),
|
||||
url('icomoon.svg?8qh3rt#icomoon') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
[class^="icon-"], [class*=" icon-"] {
|
||||
.icon {
|
||||
/* use !important to prevent issues with browser extensions that change fonts */
|
||||
/*noinspection CssNoGenericFontName*/
|
||||
font-family: 'icomoon' !important;
|
||||
speak: never;
|
||||
speak: none;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
vertical-align: -.0625em;
|
||||
|
||||
/* Better Font Rendering =========== */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
@@ -93,6 +94,7 @@
|
||||
}
|
||||
.icon-order-rev:before {
|
||||
content: "\ea46";
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.icon-github:before {
|
||||
content: "\eab0";
|
||||
|
||||
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;
|
||||
|
||||
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