thuhole 5 years ago
parent
commit
98881eca9a
  1. 4
      .prettierrc.js
  2. 9759
      package-lock.json
  3. 28
      package.json
  4. 16
      public/static/fonts_7/icomoon.css
  5. 18
      src/App.js
  6. 17
      src/Common.js
  7. 17
      src/Config.css
  8. 137
      src/Config.js
  9. 149
      src/Flows.js
  10. 6
      src/Message.js
  11. 42
      src/Sidebar.css
  12. 36
      src/Sidebar.js
  13. 12
      src/Title.js
  14. 2
      src/UserAction.js
  15. 3
      src/cache.js
  16. 170
      src/flows_api.js
  17. 2
      src/infrastructure

4
.prettierrc.js

@ -3,5 +3,5 @@ module.exports = {
tabWidth: 2,
semi: true,
singleQuote: true,
endOfLine: 'auto',
}
endOfLine: 'auto'
};

9759
package-lock.json generated

File diff suppressed because it is too large Load Diff

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"
}
}

16
public/static/fonts_7/icomoon.css

@ -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

@ -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) => (

17
src/Common.js

@ -24,7 +24,19 @@ function escape_regex(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
export function build_highlight_re(txt, split, 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
@ -35,6 +47,7 @@ export function build_highlight_re(txt, split, option = 'g') {
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) {

17
src/Config.css

@ -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

@ -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>
&nbsp;
<small>#background_img</small>&nbsp;
{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>
&nbsp; <small>#color_scheme</small>
&nbsp;<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>&nbsp;<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>
&nbsp; <small>#{this.props.id}</small>
&nbsp;<b>{this.props.name}</b>
&nbsp;<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}

149
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,24 +60,15 @@ function load_single_meta(show_sidebar, token) {
<div className="box box-tip">正在加载 #{pid}</div>,
replace ? 'replace' : 'push',
);
API.get_single(pid, token)
.then((single) => {
try {
let single = await API.get_single(pid, token);
single.data.variant = {};
return new Promise((resolve, reject) => {
API.load_replies_with_cache(
let { data: replies } = await 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
@ -92,16 +83,13 @@ function load_single_meta(show_sidebar, token) {
/>,
'replace',
);
})
.catch((e) => {
} 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 onClick={() => load_single_meta(show_sidebar, token)(pid, true)}>
重新加载
</a>
</p>
@ -109,7 +97,7 @@ function load_single_meta(show_sidebar, token) {
</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>
&nbsp;
{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',

6
src/Message.js

@ -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} />
&nbsp; <b>{msg.title}</b>
<b>{msg.title}</b>
</div>
<div className="box-content">
<pre>{msg.content}</pre>

42
src/Sidebar.css

@ -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;
}

36
src/Sidebar.js

@ -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}>
&nbsp;

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}
/>

2
src/UserAction.js

@ -1,4 +1,4 @@
import React, { Component, PureComponent } from 'react';
import React, { Component } from 'react';
import {
API_BASE,
SafeTextarea,

3
src/cache.js

@ -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();

170
src/flows_api.js

@ -13,127 +13,98 @@ export { get_json };
const SEARCH_PAGESIZE = 50;
export const API = {
load_replies: (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) => {
const handle_response = async (response, notify = false) => {
let json = await get_json(response);
if (json.code !== 0) {
if (json.msg) throw new Error(json.msg);
else throw new Error(JSON.stringify(json));
if (json.msg) {
if (notify) alert(json.msg);
else throw new Error(json.msg);
} else throw new Error(JSON.stringify(json));
}
return 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);
})
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: async (pid, token, color_picker, cache_version) => {
pid = parseInt(pid);
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) => {
let json = await cache().get(pid, cache_version);
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);
});
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), {
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,
})
.then(get_json)
.then((json) => {
},
);
// Delete cache to update `attention` on next reload
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;
});
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), {
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,
})
.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);
},
};

2
src/infrastructure

@ -1 +1 @@
Subproject commit 43b48087b181beef7b6d69163914d6ca799a12b4
Subproject commit e3c39ff4e3efaad45e16796d9eb511345d4cbcf4
Loading…
Cancel
Save