forked from newthuhole/hole_thu_frontend
feature update
- search query highlight - id quote - add some icons
This commit is contained in:
@@ -53,4 +53,9 @@
|
||||
-1px 1px 0 #000,
|
||||
0 1px 0 #000,
|
||||
1px 1px 0 #000;
|
||||
}
|
||||
|
||||
.search-query-highlight {
|
||||
border-bottom: 1px solid black;
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, {Component, PureComponent} from 'react';
|
||||
import {PKUHELPER_ROOT} from './flows_api';
|
||||
import {split_text,NICKNAME_RE,PID_RE,URL_RE} from './text_splitter'
|
||||
|
||||
import TimeAgo from 'react-timeago';
|
||||
import chineseStrings from 'react-timeago/lib/language-strings/zh-CN';
|
||||
@@ -16,6 +15,15 @@ function pad2(x) {
|
||||
return x<10 ? '0'+x : ''+x;
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex
|
||||
function escape_regex(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||
}
|
||||
|
||||
export function build_highlight_re(txt,split) {
|
||||
return txt ? new RegExp(`(${txt.split(split).filter((x)=>!!x).map(escape_regex).join('|')})`,'g') : /^$/g;
|
||||
}
|
||||
|
||||
export function format_time(time) {
|
||||
return `${time.getMonth()+1}-${pad2(time.getDate())} ${time.getHours()}:${pad2(time.getMinutes())}:${pad2(time.getSeconds())}`;
|
||||
}
|
||||
@@ -41,23 +49,19 @@ export function TitleLine(props) {
|
||||
|
||||
export class HighlightedText extends PureComponent {
|
||||
render() {
|
||||
let parts=split_text(this.props.text,[
|
||||
['url',URL_RE],
|
||||
['pid',PID_RE],
|
||||
['nickname',NICKNAME_RE],
|
||||
]);
|
||||
function normalize_url(url) {
|
||||
return /^https?:\/\//.test(url) ? url : 'http://'+url;
|
||||
}
|
||||
return (
|
||||
<pre>
|
||||
{parts.map((part,idx)=>{
|
||||
{this.props.parts.map((part,idx)=>{
|
||||
let [rule,p]=part;
|
||||
return (
|
||||
<span key={idx}>{
|
||||
rule==='url' ? <a href={normalize_url(p)} target="_blank" rel="noopener">{p}</a> :
|
||||
rule==='pid' ? <a href={'##'+p} onClick={(e)=>{e.preventDefault(); this.props.show_pid(p);}}>{p}</a> :
|
||||
rule==='nickname' ? <span style={{backgroundColor: this.props.color_picker.get(p)}}>{p}</span> :
|
||||
rule==='search' ? <span className="search-query-highlight">{p}</span> :
|
||||
p
|
||||
}</span>
|
||||
);
|
||||
|
||||
@@ -169,4 +169,26 @@
|
||||
color: white;
|
||||
background-color: rgba(0,0,0,.6);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.flow-item-row-quote {
|
||||
opacity: .8;
|
||||
filter: brightness(90%);
|
||||
}
|
||||
|
||||
.flow-item-quote>.box {
|
||||
margin-left: 2.5em;
|
||||
max-height: 15em;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.quote-tip {
|
||||
margin-top: .5em;
|
||||
margin-bottom: -10em; /* so that it will not block reply bar */
|
||||
float: left;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 2.5em;
|
||||
text-align: center;
|
||||
color: white;
|
||||
}
|
||||
197
src/Flows.js
197
src/Flows.js
@@ -1,7 +1,8 @@
|
||||
import React, {Component, PureComponent} from 'react';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import {ColorPicker} from './color_picker';
|
||||
import {format_time, Time, TitleLine, HighlightedText, ClickHandler} from './Common';
|
||||
import {split_text,NICKNAME_RE,PID_RE,URL_RE} from './text_splitter';
|
||||
import {format_time, build_highlight_re, Time, TitleLine, HighlightedText, ClickHandler} from './Common';
|
||||
import './Flows.css';
|
||||
import LazyLoad from 'react-lazyload';
|
||||
import {AudioWidget} from './AudioWidget';
|
||||
@@ -62,6 +63,11 @@ class Reply extends PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
let parts=split_text(this.props.info.text,[
|
||||
['url',URL_RE],
|
||||
['pid',PID_RE],
|
||||
['nickname',NICKNAME_RE],
|
||||
]);
|
||||
return (
|
||||
<div className={'flow-reply box'} style={this.props.info._display_color ? {
|
||||
backgroundColor: this.props.info._display_color,
|
||||
@@ -71,7 +77,7 @@ class Reply extends PureComponent {
|
||||
<Time stamp={this.props.info.timestamp} />
|
||||
</div>
|
||||
<div className="box-content">
|
||||
<HighlightedText text={this.props.info.text} color_picker={this.props.color_picker} show_pid={this.props.show_pid} />
|
||||
<HighlightedText parts={parts} color_picker={this.props.color_picker} show_pid={this.props.show_pid} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -95,41 +101,54 @@ class FlowItem extends PureComponent {
|
||||
|
||||
render() {
|
||||
let props=this.props;
|
||||
let parts=props.parts||split_text(props.info.text,[
|
||||
['url',URL_RE],
|
||||
['pid',PID_RE],
|
||||
['nickname',NICKNAME_RE],
|
||||
]);
|
||||
return (
|
||||
<div className="flow-item box">
|
||||
{parseInt(props.info.pid,10)>window.LATEST_POST_ID && <div className="flow-item-dot" /> }
|
||||
<div className="box-header">
|
||||
{!!parseInt(props.info.likenum,10) &&
|
||||
<span className="box-header-badge">
|
||||
{props.info.likenum}
|
||||
<span className={'icon icon-'+(props.attention ? 'star-ok' : 'star')} />
|
||||
</span>
|
||||
}
|
||||
{!!parseInt(props.info.reply,10) &&
|
||||
<span className="box-header-badge">
|
||||
{props.info.reply}
|
||||
<span className="icon icon-reply" />
|
||||
</span>
|
||||
}
|
||||
<code className="box-id"><a href={'##'+props.info.pid} onClick={this.copy_link.bind(this)}>#{props.info.pid}</a></code>
|
||||
|
||||
<Time stamp={props.info.timestamp} />
|
||||
</div>
|
||||
<div className="box-content">
|
||||
<HighlightedText text={props.info.text} color_picker={props.color_picker} show_pid={props.show_pid} />
|
||||
{props.info.type==='image' &&
|
||||
<p className="img">
|
||||
{props.img_clickable ?
|
||||
<a href={IMAGE_BASE+props.info.url} target="_blank"><img src={IMAGE_BASE+props.info.url} /></a> :
|
||||
<img src={IMAGE_BASE+props.info.url} />
|
||||
}
|
||||
</p>
|
||||
}
|
||||
{props.info.type==='audio' && <AudioWidget src={AUDIO_BASE+props.info.url} />}
|
||||
</div>
|
||||
{!!(props.attention && props.info.variant.latest_reply) &&
|
||||
<p className="box-footer">最新回复 <Time stamp={props.info.variant.latest_reply} /></p>
|
||||
<div className={'flow-item'+(props.is_quote ? ' flow-item-quote' : '')}>
|
||||
{!!props.is_quote &&
|
||||
<div className="quote-tip black-outline">
|
||||
<div><span className="icon icon-quote" /></div>
|
||||
<div><small>提到</small></div>
|
||||
</div>
|
||||
}
|
||||
<div className="box">
|
||||
{parseInt(props.info.pid,10)>window.LATEST_POST_ID && <div className="flow-item-dot" /> }
|
||||
<div className="box-header">
|
||||
{!!parseInt(props.info.likenum,10) &&
|
||||
<span className="box-header-badge">
|
||||
{props.info.likenum}
|
||||
<span className={'icon icon-'+(props.attention ? 'star-ok' : 'star')} />
|
||||
</span>
|
||||
}
|
||||
{!!parseInt(props.info.reply,10) &&
|
||||
<span className="box-header-badge">
|
||||
{props.info.reply}
|
||||
<span className="icon icon-reply" />
|
||||
</span>
|
||||
}
|
||||
<code className="box-id"><a href={'##'+props.info.pid} onClick={this.copy_link.bind(this)}>#{props.info.pid}</a></code>
|
||||
|
||||
<Time stamp={props.info.timestamp} />
|
||||
</div>
|
||||
<div className="box-content">
|
||||
<HighlightedText parts={parts} color_picker={props.color_picker} show_pid={props.show_pid} />
|
||||
{props.info.type==='image' &&
|
||||
<p className="img">
|
||||
{props.img_clickable ?
|
||||
<a href={IMAGE_BASE+props.info.url} target="_blank"><img src={IMAGE_BASE+props.info.url} /></a> :
|
||||
<img src={IMAGE_BASE+props.info.url} />
|
||||
}
|
||||
</p>
|
||||
}
|
||||
{props.info.type==='audio' && <AudioWidget src={AUDIO_BASE+props.info.url} />}
|
||||
</div>
|
||||
{!!(props.attention && props.info.variant.latest_reply) &&
|
||||
<p className="box-footer">最新回复 <Time stamp={props.info.variant.latest_reply} /></p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -331,10 +350,9 @@ class FlowItemRow extends PureComponent {
|
||||
this.state={
|
||||
replies: [],
|
||||
reply_status: 'done',
|
||||
info: props.info,
|
||||
info: Object.assign({},props.info,{variant: {}}),
|
||||
attention: false,
|
||||
};
|
||||
this.state.info.variant={};
|
||||
this.color_picker=new ColorPicker();
|
||||
this.show_pid=load_single_meta(this.props.show_sidebar,this.props.token);
|
||||
}
|
||||
@@ -385,12 +403,32 @@ class FlowItemRow extends PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="flow-item-row" onClick={(event)=>{
|
||||
let hl_rules=[
|
||||
['url',URL_RE],
|
||||
['pid',PID_RE],
|
||||
['nickname',NICKNAME_RE],
|
||||
];
|
||||
if(this.props.search_param)
|
||||
hl_rules.push(['search',build_highlight_re(this.props.search_param,' ')]);
|
||||
let parts=split_text(this.state.info.text,hl_rules);
|
||||
|
||||
let quote_id=null;
|
||||
if(!this.props.is_quote && localStorage['DISABLE_QUOTE']!=='on')
|
||||
for(let [mode,content] of parts)
|
||||
if(mode==='pid')
|
||||
if(quote_id===null)
|
||||
quote_id=parseInt(content);
|
||||
else {
|
||||
quote_id=null;
|
||||
break;
|
||||
}
|
||||
|
||||
let res=(
|
||||
<div className={'flow-item-row'+(this.props.is_quote ? ' flow-item-row-quote' : '')} onClick={(event)=>{
|
||||
if(!CLICKABLE_TAGS[event.target.tagName.toLowerCase()])
|
||||
this.show_sidebar();
|
||||
}}>
|
||||
<FlowItem info={this.state.info} attention={this.state.attention} img_clickable={false}
|
||||
<FlowItem parts={parts} info={this.state.info} attention={this.state.attention} img_clickable={false} is_quote={this.props.is_quote}
|
||||
color_picker={this.color_picker} show_pid={this.show_pid} replies={this.state.replies} />
|
||||
<div className="flow-reply-row">
|
||||
{this.state.reply_status==='loading' && <div className="box box-tip">加载中</div>}
|
||||
@@ -406,6 +444,78 @@ class FlowItemRow extends PureComponent {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return quote_id ? (
|
||||
<div>
|
||||
{res}
|
||||
<FlowItemQuote pid={quote_id} show_sidebar={this.props.show_sidebar} token={this.props.token}
|
||||
deletion_detect={this.props.deletion_detect} />
|
||||
</div>
|
||||
) : res;
|
||||
}
|
||||
}
|
||||
|
||||
class FlowItemQuote extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state={
|
||||
loading_status: 'empty',
|
||||
error_msg: null,
|
||||
info: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.load();
|
||||
}
|
||||
|
||||
load() {
|
||||
this.setState({
|
||||
loading_status: 'loading',
|
||||
},()=>{
|
||||
API.get_single(this.props.pid,this.props.token)
|
||||
.then((json)=>{
|
||||
this.setState({
|
||||
loading_status: 'done',
|
||||
info: json.data,
|
||||
});
|
||||
})
|
||||
.catch((err)=>{
|
||||
if((''+err).indexOf('没有这条树洞')!==-1)
|
||||
this.setState({
|
||||
loading_status: 'empty',
|
||||
});
|
||||
else
|
||||
this.setState({
|
||||
loading_status: 'error',
|
||||
error_msg: ''+err,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if(this.state.loading_status==='empty')
|
||||
return null;
|
||||
else if(this.state.loading_status==='loading')
|
||||
return (
|
||||
<div className="box box-tip aux-margin">
|
||||
<span className="icon icon-loading" />
|
||||
提到了 #{this.props.pid}
|
||||
</div>
|
||||
);
|
||||
else if(this.state.loading_status==='error')
|
||||
return (
|
||||
<div className="box box-tip aux-margin">
|
||||
<p><a onClick={this.load.bind(this)}>重新加载</a></p>
|
||||
<p>{this.state.error_msg}</p>
|
||||
</div>
|
||||
);
|
||||
else // 'done'
|
||||
return (
|
||||
<FlowItemRow info={this.state.info} show_sidebar={this.props.show_sidebar} token={this.props.token}
|
||||
is_quote={true} deletion_detect={this.props.deletion_detect} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,7 +535,7 @@ function FlowChunk(props) {
|
||||
</div>
|
||||
}
|
||||
<FlowItemRow info={info} show_sidebar={props.show_sidebar} token={token}
|
||||
deletion_detect={props.deletion_detect} />
|
||||
deletion_detect={props.deletion_detect} search_param={props.search_param} />
|
||||
</div>
|
||||
</LazyLoad>
|
||||
))}
|
||||
@@ -562,8 +672,9 @@ export class Flow extends PureComponent {
|
||||
return (
|
||||
<div className="flow-container">
|
||||
<FlowChunk
|
||||
title={this.state.chunks.title} list={this.state.chunks.data}
|
||||
show_sidebar={this.props.show_sidebar} mode={this.state.mode} deletion_detect={should_deletion_detect}
|
||||
title={this.state.chunks.title} list={this.state.chunks.data} mode={this.state.mode}
|
||||
search_param={this.state.search_param||null}
|
||||
show_sidebar={this.props.show_sidebar} deletion_detect={should_deletion_detect}
|
||||
/>
|
||||
{this.state.loading_status==='failed' &&
|
||||
<div className="box box-tip aux-margin">
|
||||
|
||||
Reference in New Issue
Block a user