From ab21f67ba61f767413b03f75dc500fd31814a488 Mon Sep 17 00:00:00 2001 From: Fang Date: Thu, 6 Feb 2020 14:39:50 +0100 Subject: [PATCH] link: support loading individual submissions On the frontend, updates the route path to include the (base64-encoded) url. Uses that and the load-single functionality to support loading directly into a submission page, which fetches just the requested submission. Also ensures we don't open duplicate comment subscriptions. --- pkg/arvo/app/link-view.hoon | 27 ++++ pkg/interface/link/src/js/api.js | 133 ++++++++++-------- .../js/components/lib/comments-pagination.js | 3 + .../link/src/js/components/lib/comments.js | 25 ++-- .../link/src/js/components/lib/link-item.js | 4 +- pkg/interface/link/src/js/components/link.js | 55 ++++---- pkg/interface/link/src/js/components/root.js | 6 +- 7 files changed, 150 insertions(+), 103 deletions(-) diff --git a/pkg/arvo/app/link-view.hoon b/pkg/arvo/app/link-view.hoon index 8da8502e5e..2cb87b4a01 100644 --- a/pkg/arvo/app/link-view.hoon +++ b/pkg/arvo/app/link-view.hoon @@ -7,6 +7,7 @@ :: /json/[p]/submissions pages for all groups :: /json/[p]/submissions/[some-group] page for one group :: /json/[p]/discussions/[wood-url]/[some-group] page for url in group +:: /json/[n]/submission/[wood-url]/[some-group] nth matching submission :: /+ *link, *server, default-agent, verb :: @@ -83,6 +84,10 @@ [%submissions ^] :_ this (give-initial-submissions:do p t.t.t.path) + :: + [%submission @ ^] + :_ this + (give-specific-submission:do p (break-discussion-path t.t.t.path)) :: [%discussions @ ^] :_ this @@ -258,6 +263,28 @@ :- %discussions (build-discussion-path path url.submission) :: +++ give-specific-submission + |= [n=@ud =path =url] + :_ [%give %kick ~ ~]~ + =; =json + [%give %fact ~ %json !>(json)] + %+ frond:enjs:format 'submission' + ^- json + =; sub=(unit submission) + ?~ sub ~ + (submission:en-json u.sub) + =/ =submissions + =- (~(got by -) path) + %+ scry-for (map ^path submissions) + [%submissions path] + |- + ?~ submissions ~ + =* sub i.submissions + ?. =(url.sub url) + $(submissions t.submissions) + ?: =(0 n) `sub + $(n (dec n), submissions t.submissions) +:: ++ give-initial-discussions |= [p=@ud =path =url] ^- (list card) diff --git a/pkg/interface/link/src/js/api.js b/pkg/interface/link/src/js/api.js index b40141e30c..53e7a9ff86 100644 --- a/pkg/interface/link/src/js/api.js +++ b/pkg/interface/link/src/js/api.js @@ -96,8 +96,81 @@ class UrbitApi { } getCommentsPage(path, url, page) { - //TODO factor out - // encode the url into @ta-safe format, using logic from +wood + const strictUrl = this.encodeUrl(url); + const endpoint = '/json/' + page + '/discussions/' + strictUrl + path; + this.bind.bind(this)(endpoint, 'PUT', this.authTokens.ship, 'link-view', + (res) => { + if (res.data['initial-discussions']) { + // these aren't returned with the response, + // so this ensures the reducers know them. + res.data['initial-discussions'].path = path; + res.data['initial-discussions'].url = url; + } + store.handleEvent(res); + }, + console.error, + ()=>{} // no-op on quit + ); + } + + getPage(path, page) { + const endpoint = '/json/' + page + '/submissions' + path; + this.bind.bind(this)(endpoint, 'PUT', this.authTokens.ship, 'link-view', + (dat)=>{store.handleEvent(dat)}, + console.error, + ()=>{} // no-op on quit + ); + } + + getSubmission(path, url, callback) { + const strictUrl = this.encodeUrl(url); + const endpoint = '/json/0/submission/' + strictUrl + path; + this.bind.bind(this)(endpoint, 'PUT', this.authTokens.ship, 'link-view', + (res) => { + if (res.data.submission) { + callback(res.data.submission) + } else { + console.error('unexpected submission response', res); + } + }, + console.error, + ()=>{} // no-op on quit + ); + } + + linkAction(data) { + return this.action("link-store", "link-action", data); + } + + postLink(path, url, title) { + return this.linkAction({ + 'save': { path, url, title } + }); + } + + postComment(path, url, comment) { + return this.linkAction({ + 'note': { path, url, udon: comment } + }); + } + + sidebarToggle() { + let sidebarBoolean = true; + if (store.state.sidebarShown === true) { + sidebarBoolean = false; + } + store.handleEvent({ + data: { + local: { + 'sidebarToggle': sidebarBoolean + } + } + }); + } + + //TODO into lib? + // encode the url into @ta-safe format, using logic from +wood + encodeUrl(url) { let strictUrl = ''; for (let i = 0; i < url.length; i++) { const char = url[i]; @@ -128,61 +201,7 @@ class UrbitApi { } strictUrl = strictUrl + add; } - strictUrl = '~.' + strictUrl; - - const endpoint = '/json/' + page + '/discussions/' + strictUrl + path; - this.bind.bind(this)(endpoint, 'PUT', this.authTokens.ship, 'link-view', - (res) => { - if (res.data['initial-discussions']) { - // these aren't returned with the response, - // so this ensures the reducers know them. - res.data['initial-discussions'].path = path; - res.data['initial-discussions'].url = url; - } - store.handleEvent(res); - }, - console.error, - ()=>{} // no-op on quit - ); - } - - getPage(path, page) { - const endpoint = '/json/' + page + '/submissions' + path; - this.bind.bind(this)(endpoint, 'PUT', this.authTokens.ship, 'link-view', - (dat)=>{store.handleEvent(dat)}, - console.error, - ()=>{} // no-op on quit - ); - } - - linkAction(data) { - return this.action("link-store", "link-action", data); - } - - postLink(path, url, title) { - return this.linkAction({ - 'save': { path, url, title } - }); - } - - postComment(path, url, comment, page, index) { - return this.linkAction({ - 'note': { path, url, udon: comment } - }); - } - - sidebarToggle() { - let sidebarBoolean = true; - if (store.state.sidebarShown === true) { - sidebarBoolean = false; - } - store.handleEvent({ - data: { - local: { - 'sidebarToggle': sidebarBoolean - } - } - }); + return '~.' + strictUrl; } } diff --git a/pkg/interface/link/src/js/components/lib/comments-pagination.js b/pkg/interface/link/src/js/components/lib/comments-pagination.js index 8c6f9356db..18e914f124 100644 --- a/pkg/interface/link/src/js/components/lib/comments-pagination.js +++ b/pkg/interface/link/src/js/components/lib/comments-pagination.js @@ -16,6 +16,7 @@ export class CommentsPagination extends Component { ? "dib" : "dn"; + let encodedUrl = window.btoa(props.url); let popout = (props.popout) ? "/popout" : ""; return ( @@ -27,6 +28,7 @@ export class CommentsPagination extends Component { + props.path + "/" + props.linkPage + "/" + props.linkIndex + + "/" + encodedUrl + "/comments" + prevPage}> <- Previous Page @@ -37,6 +39,7 @@ export class CommentsPagination extends Component { + props.path + "/" + props.linkPage + "/" + props.linkIndex + + "/" + encodedUrl + "/comments" + nextPage}> Next Page -> diff --git a/pkg/interface/link/src/js/components/lib/comments.js b/pkg/interface/link/src/js/components/lib/comments.js index 0b1dcb8311..9785edacb2 100644 --- a/pkg/interface/link/src/js/components/lib/comments.js +++ b/pkg/interface/link/src/js/components/lib/comments.js @@ -9,11 +9,8 @@ export class Comments extends Component { componentDidMount() { let page = "page" + this.props.commentPage; - let comments = !!this.props.comments; - if ((page !== "page0") && - (!comments || !this.props.comments[page]) && - (this.props.path && this.props.url) - ) { + if (!this.props.comments || !this.props.comments[page]) { + this.setState({requested: this.props.commentPage}); api.getCommentsPage( this.props.path, this.props.url, @@ -22,15 +19,16 @@ export class Comments extends Component { } componentDidUpdate(prevProps) { - let page = "page" + this.props.commentPage; if (prevProps !== this.props) { - if (!!this.props.comments) { - if ((page !== "page0") && !this.props.comments[page] && this.props.url) { - api.getCommentsPage( - this.props.path, - this.props.url, - this.props.commentPage); - } + let page = "page" + this.props.commentPage; + if ( (!this.props.comments || !this.props.comments[page]) && + (this.state.requested !== this.props.commentPage)) { + this.setState({requested: this.props.commentPage}); + api.getCommentsPage( + this.props.path, + this.props.url, + this.props.commentPage + ); } } } @@ -93,6 +91,7 @@ export class Comments extends Component { popout={props.popout} linkPage={props.linkPage} linkIndex={props.linkIndex} + url={props.url} commentPage={props.commentPage} total={total}/> diff --git a/pkg/interface/link/src/js/components/lib/link-item.js b/pkg/interface/link/src/js/components/lib/link-item.js index 9eea287b1a..71ff588948 100644 --- a/pkg/interface/link/src/js/components/lib/link-item.js +++ b/pkg/interface/link/src/js/components/lib/link-item.js @@ -45,6 +45,8 @@ export class LinkItem extends Component { hostname = hostname[4]; } + let encodedUrl = window.btoa(props.url); + let comments = props.comments + " comment" + ((props.comments === 1) ? "" : "s"); return ( @@ -68,7 +70,7 @@ export class LinkItem extends Component { : "~" + props.ship} {this.state.timeSinceLinkPost} {comments} diff --git a/pkg/interface/link/src/js/components/link.js b/pkg/interface/link/src/js/components/link.js index 446f965b65..47e7259054 100644 --- a/pkg/interface/link/src/js/components/link.js +++ b/pkg/interface/link/src/js/components/link.js @@ -13,21 +13,21 @@ export class LinkDetail extends Component { super(props); this.state = { timeSinceLinkPost: this.getTimeSinceLinkPost(), - comment: "" + comment: "", + data: props.data }; this.setComment = this.setComment.bind(this); } + updateData(submission) { + this.setState({data: submission}); + } + componentDidMount() { // if we have no preloaded data, and we aren't expecting it, get it - if (this.props.page != 0 && (!this.props.data || !this.props.data.url)) { - api.getPage(this.props.path, this.props.page); - - // if we have preloaded our data, - // but no comments, grab the comments - } else if (!this.props.comments && this.props.data.url) { - api.getCommentsPage(this.props.path, this.props.data.url, this.props.commentPage); + if (!this.props.data.url || !this.props.url) { + api.getSubmission(this.props.path, this.props.url, this.updateData.bind(this)); } this.updateTimeSinceNewestMessageInterval = setInterval( () => { @@ -36,17 +36,13 @@ export class LinkDetail extends Component { } componentDidUpdate(prevProps) { - // if we came to this page *directly*, - // load the comments -- DidMount will fail - if ( (this.props.data.url !== prevProps.data.url) && - (!this.props.comments && this.props.data.url) - ) { - api.getCommentsPage(this.props.path, this.props.data.url, this.props.commentPage); - } - if (this.props.data.timestamp !== prevProps.data.timestamp) { this.setState({timeSinceLinkPost: this.getTimeSinceLinkPost()}) } + + if (this.props.url !== prevProps.url) { + this.setState({data: this.props.data}); + } } componentWillUnmount() { @@ -63,15 +59,13 @@ export class LinkDetail extends Component { } onClickPost() { - let url = this.props.data.url || ""; + let url = this.props.url || ""; let request = api.postComment( this.props.path, url, - this.state.comment, - this.props.page, - this.props.link - ); + this.state.comment + ); if (request) { this.setState({comment: ""}) @@ -87,9 +81,10 @@ export class LinkDetail extends Component { let popout = (props.popout) ? "/popout" : ""; let path = props.path + "/" + props.page + "/" + props.link; - let ship = props.data.ship || "zod"; - let title = props.data.title || ""; - let url = props.data.url || ""; + const data = this.state.data || props.data; + let ship = data.ship || "zod"; + let title = data.title || ""; + let url = data.url || ""; let URLparser = new RegExp(/((?:([\w\d\.-]+)\:\/\/?){1}(?:(www)\.?){0,1}(((?:[\w\d-]+\.)*)([\w\d-]+\.[\w\d]+))){1}(?:\:(\d+)){0,1}((\/(?:(?:[^\/\s\?]+\/)*))(?:([^\?\/\s#]+?(?:.[^\?\s]+){0,1}){0,1}(?:\?([^\s#]+)){0,1})){0,1}(?:#([^#\s]+)){0,1}/); @@ -99,18 +94,18 @@ export class LinkDetail extends Component { hostname = hostname[4]; } - let commentCount = props.data.commentCount || 0; + let commentCount = data.commentCount || 0; let comments = commentCount + " comment" + ((commentCount === 1) ? "" : "s"); - let nickname = !!props.members[props.data.ship] - ? props.members[props.data.ship].nickname + let nickname = !!props.members[ship] + ? props.members[ship].nickname : ""; let nameClass = nickname ? "inter" : "mono"; - let color = !!props.members[props.data.ship] - ? uxToHex(props.members[props.data.ship].color) + let color = !!props.members[ship] + ? uxToHex(props.members[ship].color) : "000000"; let activeClasses = (this.state.comment) @@ -198,7 +193,7 @@ export class LinkDetail extends Component { commentPage={props.commentPage} members={props.members} popout={props.popout} - url={props.data.url} + url={props.url} linkPage={props.page} linkIndex={props.link} /> diff --git a/pkg/interface/link/src/js/components/root.js b/pkg/interface/link/src/js/components/root.js index a4c72a20ce..52b028c90f 100644 --- a/pkg/interface/link/src/js/components/root.js +++ b/pkg/interface/link/src/js/components/root.js @@ -94,7 +94,7 @@ export class Root extends Component { ) }} /> - { let groupPath = `/${props.match.params.ship}/${props.match.params.channel}`; @@ -105,6 +105,7 @@ export class Root extends Component { let index = props.match.params.index || 0; let page = props.match.params.page || 0; + let url = window.atob(props.match.params.encodedUrl); let data = !!links[groupPath] ? !!links[groupPath]["page" + page] @@ -113,7 +114,7 @@ export class Root extends Component { : {}; let coms = !comments[groupPath] ? undefined - : comments[groupPath][data.url]; + : comments[groupPath][url]; let commentPage = props.match.params.commentpage || 0; @@ -130,6 +131,7 @@ export class Root extends Component {