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.
This commit is contained in:
Fang 2020-02-06 14:39:50 +01:00
parent 9727fab259
commit ab21f67ba6
No known key found for this signature in database
GPG Key ID: EB035760C1BBA972
7 changed files with 150 additions and 103 deletions

View File

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

View File

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

View File

@ -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}>
&#60;- Previous Page
</Link>
@ -37,6 +39,7 @@ export class CommentsPagination extends Component {
+ props.path
+ "/" + props.linkPage
+ "/" + props.linkIndex
+ "/" + encodedUrl
+ "/comments" + nextPage}>
Next Page ->
</Link>

View File

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

View File

@ -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}</span>
<span className="f9 inter gray2 pr3 v-mid">{this.state.timeSinceLinkPost}</span>
<Link to=
{"/~link" + props.popout + "/" + props.channel + "/" + props.page + "/" + props.index}
{"/~link" + props.popout + "/" + props.channel + "/" + props.page + "/" + props.index + "/" + encodedUrl}
className="v-top">
<span className="f9 inter gray2">
{comments}

View File

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

View File

@ -94,7 +94,7 @@ export class Root extends Component {
)
}}
/>
<Route exact path="/~link/(popout)?/:ship/:channel/:page/:index/(comments)?/:commentpage?"
<Route exact path="/~link/(popout)?/:ship/:channel/:page/:index/:encodedUrl/(comments)?/:commentpage?"
render={ (props) => {
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 {
<LinkDetail
{...props}
page={page}
url={url}
link={index}
members={groupMembers}
path={groupPath}