diff --git a/README.md b/README.md index 57424a0..2f82131 100644 --- a/README.md +++ b/README.md @@ -46,4 +46,5 @@ React 版 P大树洞,[pkuhelper.pku.edu.cn/hole](http://pkuhelper.pku.edu.cn/h - 检测瀑布流中被删除的树洞和树洞被删除的评论(`//setflag DELETION_DETECT=on`) - 自定义背景图片(`//setflag REPLACE_ERIRI_WITH_URL=http://...`) -- 禁用 3D Touch 功能(`//setflag DISABLE_PRESSURE=on`) \ No newline at end of file +- 禁用 3D Touch 功能(`//setflag DISABLE_PRESSURE=on`) +- 禁用自动显示引用树洞功能(`//setflag DISABLE_QUOTE=on`) \ No newline at end of file diff --git a/public/index.html b/public/index.html index a77cd9f..98e7161 100644 --- a/public/index.html +++ b/public/index.html @@ -6,7 +6,7 @@ - + diff --git a/public/static/fonts_2/icomoon.ttf b/public/static/fonts_2/icomoon.ttf deleted file mode 100644 index 132accf..0000000 Binary files a/public/static/fonts_2/icomoon.ttf and /dev/null differ diff --git a/public/static/fonts_2/icomoon.woff b/public/static/fonts_2/icomoon.woff deleted file mode 100644 index 25b7f5b..0000000 Binary files a/public/static/fonts_2/icomoon.woff and /dev/null differ diff --git a/public/static/fonts_2/icomoon.css b/public/static/fonts_5/icomoon.css similarity index 71% rename from public/static/fonts_2/icomoon.css rename to public/static/fonts_5/icomoon.css index ac5caf3..73f16c3 100644 --- a/public/static/fonts_2/icomoon.css +++ b/public/static/fonts_5/icomoon.css @@ -1,9 +1,9 @@ @font-face { font-family: 'icomoon'; src: - url('icomoon.ttf?y01gys') format('truetype'), - url('icomoon.woff?y01gys') format('woff'), - url('icomoon.svg?y01gys#icomoon') format('svg'); + url('icomoon.ttf?gqzyp6') format('truetype'), + url('icomoon.woff?gqzyp6') format('woff'), + url('icomoon.svg?gqzyp6#icomoon') format('svg'); font-weight: normal; font-style: normal; } @@ -27,15 +27,28 @@ .icon-send:before { content: "\e900"; } +.icon-history:before { + content: "\e94d"; +} .icon-reply:before { content: "\e96b"; } +.icon-quote:before { + content: "\e977"; +} .icon-loading:before { content: "\e979"; } .icon-login:before { content: "\e98d"; } +.icon-stats:before { + content: "\e99b"; +} +.icon-upload:before { + content: "\e9c3"; + font-size: 1.2em; +} .icon-attention:before { content: "\e9d3"; } @@ -54,6 +67,9 @@ .icon-refresh:before { content: "\ea2e"; } +.icon-back:before { + content: "\ea44"; +} .icon-github:before { content: "\eab0"; } diff --git a/public/static/fonts_2/icomoon.svg b/public/static/fonts_5/icomoon.svg similarity index 67% rename from public/static/fonts_2/icomoon.svg rename to public/static/fonts_5/icomoon.svg index c64966a..7e6eec1 100644 --- a/public/static/fonts_2/icomoon.svg +++ b/public/static/fonts_5/icomoon.svg @@ -8,14 +8,19 @@ + + + + + \ No newline at end of file diff --git a/public/static/fonts_5/icomoon.ttf b/public/static/fonts_5/icomoon.ttf new file mode 100644 index 0000000..03e2423 Binary files /dev/null and b/public/static/fonts_5/icomoon.ttf differ diff --git a/public/static/fonts_5/icomoon.woff b/public/static/fonts_5/icomoon.woff new file mode 100644 index 0000000..9913368 Binary files /dev/null and b/public/static/fonts_5/icomoon.woff differ diff --git a/src/Common.css b/src/Common.css index cc3b8bd..23a0400 100644 --- a/src/Common.css +++ b/src/Common.css @@ -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; } \ No newline at end of file diff --git a/src/Common.js b/src/Common.js index c5f2f24..d0fab4b 100644 --- a/src/Common.js +++ b/src/Common.js @@ -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 (
-                {parts.map((part,idx)=>{
+                {this.props.parts.map((part,idx)=>{
                     let [rule,p]=part;
                     return (
                         {
                             rule==='url' ? {p} :
                             rule==='pid' ? {e.preventDefault(); this.props.show_pid(p);}}>{p} :
                             rule==='nickname' ? {p} :
+                            rule==='search' ? {p} :
                             p
                         }
                     );
diff --git a/src/Flows.css b/src/Flows.css
index b758871..9986afe 100644
--- a/src/Flows.css
+++ b/src/Flows.css
@@ -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;
 }
\ No newline at end of file
diff --git a/src/Flows.js b/src/Flows.js
index b83df7a..d29fa6a 100644
--- a/src/Flows.js
+++ b/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 (
             
- +
); @@ -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 ( -
- {parseInt(props.info.pid,10)>window.LATEST_POST_ID &&
} -
- {!!parseInt(props.info.likenum,10) && - - {props.info.likenum}  - - - } - {!!parseInt(props.info.reply,10) && - - {props.info.reply}  - - - } - #{props.info.pid} -   -
-
- - {props.info.type==='image' && -

- {props.img_clickable ? - : - - } -

+
+ {!!props.is_quote && +
+
+
提到
+
+ } +
+ {parseInt(props.info.pid,10)>window.LATEST_POST_ID &&
} +
+ {!!parseInt(props.info.likenum,10) && + + {props.info.likenum}  + + + } + {!!parseInt(props.info.reply,10) && + + {props.info.reply}  + + + } + #{props.info.pid} +   +
+
+ + {props.info.type==='image' && +

+ {props.img_clickable ? + : + + } +

+ } + {props.info.type==='audio' && } +
+ {!!(props.attention && props.info.variant.latest_reply) && +

最新回复

} - {props.info.type==='audio' && }
- {!!(props.attention && props.info.variant.latest_reply) && -

最新回复

- }
); } @@ -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 ( -
{ + 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=( +
{ if(!CLICKABLE_TAGS[event.target.tagName.toLowerCase()]) this.show_sidebar(); }}> -
{this.state.reply_status==='loading' &&
加载中
} @@ -406,6 +444,78 @@ class FlowItemRow extends PureComponent {
); + + return quote_id ? ( +
+ {res} + +
+ ) : res; + } +} + +class FlowItemQuote extends PureComponent { + constructor(props) { + super(props); + this.state={ + loading_status: 'empty', + error_msg: null, + info: null, + }; + } + + componentDidMount() { + this.load(); + } + + load() { + this.setState({ + loading_status: 'loading', + },()=>{ + API.get_single(this.props.pid,this.props.token) + .then((json)=>{ + this.setState({ + loading_status: 'done', + info: json.data, + }); + }) + .catch((err)=>{ + if((''+err).indexOf('没有这条树洞')!==-1) + this.setState({ + loading_status: 'empty', + }); + else + this.setState({ + loading_status: 'error', + error_msg: ''+err, + }); + }); + }); + } + + render() { + if(this.state.loading_status==='empty') + return null; + else if(this.state.loading_status==='loading') + return ( +
+ + 提到了 #{this.props.pid} +
+ ); + else if(this.state.loading_status==='error') + return ( +
+

重新加载

+

{this.state.error_msg}

+
+ ); + else // 'done' + return ( + + ); } } @@ -425,7 +535,7 @@ function FlowChunk(props) {
} + deletion_detect={props.deletion_detect} search_param={props.search_param} />
))} @@ -562,8 +672,9 @@ export class Flow extends PureComponent { return (
{this.state.loading_status==='failed' &&