Merge pull request #2240 from urbit/m/link-fe

link fe: various fixes and improvements
This commit is contained in:
matildepark 2020-02-06 15:58:42 -05:00 committed by GitHub
commit 9e0923f579
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 60360 additions and 171 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)

File diff suppressed because one or more lines are too long

View File

@ -16,6 +16,9 @@ class UrbitApi {
decline: this.inviteDecline.bind(this),
invite: this.inviteInvite.bind(this)
};
this.bind = this.bind.bind(this);
this.bindLinkView = this.bindLinkView.bind(this);
}
bind(path, method, ship = this.authTokens.ship, app, success, fail, quit) {
@ -39,6 +42,13 @@ class UrbitApi {
});
}
bindLinkView(path, result, fail, quit) {
this.bind.bind(this)(
path, 'PUT', this.authTokens.ship, 'link-view',
result, fail, quit
);
}
action(appl, mark, data) {
return new Promise((resolve, reject) => {
window.urb.poke(ship, appl, mark, data,
@ -91,13 +101,82 @@ class UrbitApi {
});
}
getComments(path, url) {
return this.getCommentsPage.bind(this)(path, url, 0);
getCommentsPage(path, url, page) {
const strictUrl = this.encodeUrl(url);
const endpoint = '/json/' + page + '/discussions/' + strictUrl + path;
this.bindLinkView(endpoint,
(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
);
}
getCommentsPage(path, url, page) {
//TODO factor out
getPage(path, page) {
const endpoint = '/json/' + page + '/submissions' + path;
this.bindLinkView(endpoint,
(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.bindLinkView(endpoint,
(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 +207,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

@ -12,10 +12,11 @@ export class CommentsPagination extends Component {
? "dib"
: "dn";
let nextDisplay = (Number(props.commentPage + 1) < Number(props.total))
let nextDisplay = ((Number(props.commentPage) + 1) < Number(props.total))
? "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

@ -6,14 +6,18 @@ import { uxToHex } from '../../lib/util';
import { api } from '../../api';
export class Comments extends Component {
constructor(props) {
super(props);
this.state = {};
}
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)
let page = this.props.commentPage;
if (!this.props.comments ||
!this.props.comments[page] ||
this.props.comments.local[page]
) {
this.setState({requested: this.props.commentPage});
api.getCommentsPage(
this.props.path,
this.props.url,
@ -21,24 +25,10 @@ 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);
}
}
}
}
render() {
let props = this.props;
let page = "page" + props.commentPage;
let page = props.commentPage;
let commentsObj = !!props.comments
? props.comments
@ -50,7 +40,7 @@ export class Comments extends Component {
let total = !!props.comments
? props.comments.totalPages
: {};
: 1;
let commentsList = Object.keys(commentsPage)
.map((entry) => {
@ -93,6 +83,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.linkIndex + "/" + encodedUrl}
className="v-top">
<span className="f9 inter gray2">
{comments}

View File

@ -12,40 +12,38 @@ export class LinkDetail extends Component {
constructor(props) {
super(props);
this.state = {
timeSinceLinkPost: this.getTimeSinceLinkPost(),
comment: ""
timeSinceLinkPost: this.getTimeSinceLinkPost(props.data),
comment: "",
data: props.data
};
this.setComment = this.setComment.bind(this);
}
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);
updateData(submission) {
this.setState({
data: submission,
timeSinceLinkPost: this.getTimeSinceLinkPost(submission)
});
}
componentDidMount() {
// if we have no preloaded data, and we aren't expecting it, get it
if (!this.state.data.title) {
api.getSubmission(
this.props.path, this.props.url, this.updateData.bind(this)
);
}
// start the time-since update timer
this.updateTimeSinceNewestMessageInterval = setInterval( () => {
this.setState({timeSinceLinkPost: this.getTimeSinceLinkPost()});
this.setState({timeSinceLinkPost: this.getTimeSinceLinkPost(this.state.data)});
}, 60000);
}
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.updateData(this.props.data);
}
}
@ -56,21 +54,19 @@ export class LinkDetail extends Component {
}
}
getTimeSinceLinkPost() {
return !!this.props.data.timestamp ?
moment.unix(this.props.data.timestamp / 1000).from(moment.utc())
getTimeSinceLinkPost(data) {
return !!data.time ?
moment.unix(data.time / 1000).from(moment.utc())
: '';
}
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) {
@ -85,11 +81,12 @@ export class LinkDetail extends Component {
render() {
let props = this.props;
let popout = (props.popout) ? "/popout" : "";
let path = props.path + "/" + props.page + "/" + props.link;
let routePath = props.path + "/" + props.page + "/" + props.linkIndex + "/" + window.btoa(props.url);
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 +96,20 @@ export class LinkDetail extends Component {
hostname = hostname[4];
}
let commentCount = props.data.commentCount || 0;
const commentCount = props.comments
? props.comments.totalItems
: 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)
@ -135,7 +134,7 @@ export class LinkDetail extends Component {
<LinksTabBar
{...props}
popout={popout}
path={path}/>
path={routePath}/>
</div>
<div className="w-100 mt2 flex justify-center overflow-y-scroll ph4 pb4">
<div className="w-100 mw7">
@ -161,7 +160,7 @@ export class LinkDetail extends Component {
<span className="f9 inter gray2 pr3 v-mid">
{this.state.timeSinceLinkPost}
</span>
<Link to={"/~link" + props.path + "/" + props.page + "/" + props.link} className="v-top">
<Link to={"/~link" + props.path + "/" + props.page + "/" + props.linkIndex + "/" + window.btoa(props.url)} className="v-top">
<span className="f9 inter gray2">
{comments}
</span>
@ -198,9 +197,9 @@ 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}
linkIndex={props.linkIndex}
/>
</div>
</div>

View File

@ -17,8 +17,11 @@ export class Links extends Component {
}
componentDidUpdate() {
let linkPage = "page" + this.props.page;
if ((this.props.page != 0) && (!this.props.links[linkPage])) {
const linkPage = this.props.page;
if ( (this.props.page != 0) &&
(!this.props.links[linkPage] ||
this.props.links.local[linkPage])
) {
api.getPage(this.props.path, this.props.page);
}
}
@ -27,7 +30,7 @@ export class Links extends Component {
let props = this.props;
let popout = (props.popout) ? "/popout" : "";
let channel = props.path.substr(1);
let linkPage = "page" + props.page;
let linkPage = props.page;
let links = !!props.links[linkPage]
? props.links[linkPage]
@ -42,11 +45,15 @@ export class Links extends Component {
: 1;
let LinkList = Object.keys(links)
.map((link) => {
.map((linkIndex) => {
let linksObj = props.links[linkPage];
let { title, url, timestamp, ship, commentCount } = linksObj[link];
let { title, url, time, ship } = linksObj[linkIndex];
let members = {};
const commentCount = props.comments[url]
? props.comments[url].totalItems
: linksObj[linkIndex].commentCount || 0;
if (!props.members[ship]) {
members[ship] = {'nickname': '', 'avatar': 'TODO', 'color': '0x0'};
} else {
@ -67,12 +74,12 @@ export class Links extends Component {
return (
<LinkItem
key={timestamp}
key={time}
title={title}
page={props.page}
index={link}
linkIndex={linkIndex}
url={url}
timestamp={timestamp}
timestamp={time}
nickname={nickname}
ship={ship}
color={color}
@ -113,7 +120,7 @@ export class Links extends Component {
<LinksTabBar
{...props}
popout={popout}
path={props.path}/>
path={props.path + "/" + props.page}/>
</div>
<div className="w-100 mt2 flex justify-center overflow-y-scroll ph4 pb4">
<div className="w-100 mw7">

View File

@ -69,6 +69,10 @@ export class Root extends Component {
let channelLinks = !!links[groupPath]
? links[groupPath]
: {local: {}};
let channelComments = !!comments[groupPath]
? comments[groupPath]
: {};
return (
@ -85,6 +89,7 @@ export class Root extends Component {
{...props}
members={groupMembers}
links={channelLinks}
comments={channelComments}
page={page}
path={groupPath}
popout={popout}
@ -94,7 +99,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,15 +110,16 @@ 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]
? links[groupPath]["page" + page][index]
? !!links[groupPath][page]
? links[groupPath][page][index]
: {}
: {};
let coms = !comments[groupPath]
? undefined
: comments[groupPath][data.url];
: comments[groupPath][url];
let commentPage = props.match.params.commentpage || 0;
@ -130,7 +136,8 @@ export class Root extends Component {
<LinkDetail
{...props}
page={page}
link={index}
url={url}
linkIndex={index}
members={groupMembers}
path={groupPath}
popout={popout}

View File

@ -0,0 +1,70 @@
//NOTE copied from /pkg/interface/contacts/src/js/reducers/contact-update.js
import _ from 'lodash';
export class ContactUpdateReducer {
reduce(json, state) {
let data = _.get(json, 'contact-update', false);
if (data) {
this.create(data, state);
this.delete(data, state);
this.add(data, state);
this.remove(data, state);
this.edit(data, state);
}
}
create(json, state) {
let data = _.get(json, 'create', false);
if (data) {
state.contacts[data.path] = {};
}
}
delete(json, state) {
let data = _.get(json, 'delete', false);
if (data) {
delete state.contacts[data.path];
}
}
add(json, state) {
let data = _.get(json, 'add', false);
if (
data &&
(data.path in state.contacts)
) {
state.contacts[data.path][data.ship] = data.contact;
}
}
remove(json, state) {
let data = _.get(json, 'remove', false);
if (
data &&
(data.path in state.contacts) &&
(data.ship in state.contacts[data.path])
) {
delete state.contacts[data.path][data.ship];
}
}
edit(json, state) {
let data = _.get(json, 'edit', false);
if (
data &&
(data.path in state.contacts) &&
(data.ship in state.contacts[data.path])
) {
let edit = Object.keys(data['edit-field']);
if (edit.length !== 1) {
return;
}
state.contacts[data.path][data.ship][edit[0]] =
data['edit-field'][edit[0]];
}
}
}

View File

@ -14,24 +14,6 @@ export class InitialReducer {
state.groups[group] = new Set(data[group]);
}
}
data = _.get(json, 'link', false);
if (data) {
let name = Object.keys(data)[0];
let initial = {};
initial[name] = {};
initial[name]["total-pages"] = data[name]["total-pages"];
initial[name]["total-items"] = data[name]["total-items"];
initial[name]["page0"] = data[name]["page"];
if (!!state.links[name]) {
let origin = state.links[name];
_.extend(initial[name], origin);
} else {
state.links[name] = {};
}
state.links[name] = initial[name];
}
}
}

View File

@ -1,5 +1,8 @@
import _ from 'lodash';
// page size as expected from link-view.
// must change in parallel with the +page-size in /app/link-view to
// ensure sane behavior.
const PAGE_SIZE = 25;
export class LinkUpdateReducer {
@ -24,16 +27,17 @@ export class LinkUpdateReducer {
for (var path of Object.keys(data)) {
const here = data[path];
const page = "page" + here.pageNumber;
const page = here.pageNumber;
// if we didn't have any state for this path yet, initialize.
if (!state.links[path]) {
state.links[path] = {};
state.links[path] = {local: {}};
}
// since data contains an up-to-date full version of the page,
// we can safely overwrite the one in state.
state.links[path][page] = here.page;
state.links[path].local[page] = false;
state.links[path].totalPages = here.totalPages;
state.links[path].totalItems = here.totalItems;
}
@ -77,20 +81,21 @@ export class LinkUpdateReducer {
const path = data.path;
const url = data.url;
const page = "page" + data.pageNumber;
const page = data.pageNumber;
// if we didn't have any state for this path yet, initialize.
if (!state.comments[path]) {
state.comments[path] = {};
}
if (!state.comments[path][url]) {
state.comments[path][url] = {};
state.comments[path][url] = {local: {}};
}
let here = state.comments[path][url];
// since data contains an up-to-date full version of the page,
// we can safely overwrite the one in state.
here[page] = data.page;
here.local[page] = false;
here.totalPages = data.totalPages;
here.totalItems = data.totalItems;
}
@ -117,24 +122,25 @@ export class LinkUpdateReducer {
//
_addNewItems(items, pages = {}, page = 0) {
//TODO kinda want to refactor this, have it just be number indexes
const i = "page" + page;
//TODO but if there's more on the page than just the things we're
// pushing onto it, we won't load that in. should do an
// additional check (+ maybe load) on page-nav, right?
_addNewItems(items, pages = {local: {}}, page = 0) {
const i = page;
if (!pages[i]) {
pages[i] = [];
// if we know this page exists in the backend, flag it as "local",
// so that we know to initiate a "fetch the rest" request when we want
// to display the page.
pages.local[i] = (page < pages.totalPages);
}
pages[i] = items.concat(pages[i]);
pages.totalItems = pages.totalItems + items.length;
if (pages[i].length <= PAGE_SIZE) {
pages.totalPages = page + 1;
pages.totalItems = (page * PAGE_SIZE) + pages[i].length;
pages.totalPages = Math.ceil(pages.totalItems / PAGE_SIZE);
return pages;
}
// overflow into next page
const tail = pages[i].slice(PAGE_SIZE);
pages[i].length = PAGE_SIZE;
pages.totalItems = pages.totalItems - tail.length;
return this._addNewItems(tail, pages, page+1);
}

View File

@ -1,4 +1,5 @@
import { InitialReducer } from '/reducers/initial';
import { ContactUpdateReducer } from '/reducers/contact-update.js';
import { PermissionUpdateReducer } from '/reducers/permission-update';
import { LinkUpdateReducer } from '/reducers/link-update';
import { LocalReducer } from '/reducers/local.js';
@ -18,6 +19,7 @@ class Store {
};
this.initialReducer = new InitialReducer();
this.contactUpdateReducer = new ContactUpdateReducer();
this.permissionUpdateReducer = new PermissionUpdateReducer();
this.localReducer = new LocalReducer();
this.linkUpdateReducer = new LinkUpdateReducer();
@ -38,6 +40,7 @@ class Store {
console.log('event', json);
this.initialReducer.reduce(json, this.state);
this.contactUpdateReducer.reduce(json, this.state);
this.permissionUpdateReducer.reduce(json, this.state);
this.localReducer.reduce(json, this.state);
this.linkUpdateReducer.reduce(json, this.state);