interface: links uses graph-store

This commit is contained in:
Logan Allen 2020-09-10 10:57:07 -05:00
parent 39ad9f4c60
commit e60e5fcdbd
16 changed files with 460 additions and 880 deletions

View File

@ -45,7 +45,7 @@ export default class GraphApi extends BaseApi<StoreState> {
children: { empty: null }
};
this.storeAction({
return this.storeAction({
'add-nodes': {
resource: { ship, name },
nodes

View File

@ -110,5 +110,7 @@ export default class GlobalStore extends BaseStore<StoreState> {
this.linkListenReducer.reduce(data, this.state);
this.connReducer.reduce(data, this.state);
GraphReducer(data, this.state);
console.log(this.state);
}
}

View File

@ -10,8 +10,8 @@ import { Skeleton } from './components/skeleton';
import { NewScreen } from './components/new';
import { SettingsScreen } from './components/settings';
import { MessageScreen } from './components/lib/message-screen';
import { Links } from './components/links-list';
import { LinkDetail } from './components/link';
import { LinkList } from './components/link-list';
import { LinkDetail } from './components/link-detail';
import {
makeRoutePath,
@ -20,10 +20,6 @@ import {
} from '~/logic/lib/util';
export class LinksApp extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
// preload spinner asset
new Image().src = '/~landscape/img/Spinner.png';
@ -44,6 +40,7 @@ export class LinksApp extends Component {
const groups = props.groups ? props.groups : {};
const associations = props.associations ? props.associations : { link: {}, contacts: {} };
const graphKeys = props.graphKeys || new Set([]);
const graphs = props.graphs || {};
const invites = props.invites ?
props.invites : {};
@ -91,14 +88,15 @@ export class LinksApp extends Component {
);
}}
/>
<Route exact path="/~link/join/:resource"
<Route exact path="/~link/join/:ship/:name"
render={ (props) => {
const resourcePath = '/' + props.match.params.resource;
const resource =
`${props.match.params.ship}/${props.match.params.name}`;
const autoJoin = () => {
try {
api.links.joinCollection(resourcePath);
props.history.push(makeRoutePath(resourcePath));
api.links.joinCollection(resource);
props.history.push(`/~link/${resource}`);
} catch(err) {
setTimeout(autoJoin, 2000);
}
@ -106,46 +104,9 @@ export class LinksApp extends Component {
autoJoin();
}}
/>
<Route exact path="/~link/(popout)?/:resource/members"
render={(props) => {
const popout = props.match.url.includes('/popout/');
const resourcePath = '/' + props.match.params.resource;
const resource = associations.link[resourcePath] || { metadata: {} };
const contactDetails = contacts[resource['group-path']] || {};
const group = groups[resource['group-path']] || new Set([]);
const amOwner = amOwnerOfGroup(resource['group-path']);
return (
<Skeleton
associations={associations}
invites={invites}
groups={groups}
selected={resourcePath}
sidebarShown={sidebarShown}
api={api}>
<MemberScreen
sidebarShown={sidebarShown}
resource={resource}
contacts={contacts}
contactDetails={contactDetails}
groupPath={resource['group-path']}
group={group}
groups={groups}
associations={associations}
amOwner={amOwner}
resourcePath={resourcePath}
popout={popout}
api={api}
{...props} />
</Skeleton>
);
}}
/>
<Route exact path="/~link/(popout)?/:resource/settings"
<Route exact path="/~link/(popout)?/:ship/:name/settings"
render={ (props) => {
const popout = props.match.url.includes('/popout/');
const resourcePath = '/' + props.match.params.resource;
const resource = associations.link[resourcePath] || false;
const contactDetails = contacts[resource['group-path']] || {};
@ -177,89 +138,108 @@ export class LinksApp extends Component {
);
}}
/>
<Route exact path="/~link/(popout)?/:resource/:page?"
render={ (props) => {
const resourcePath = '/' + props.match.params.resource;
const resource = associations.link[resourcePath] || { metadata: {} };
<Route exact path="/~link/(popout)?/:ship/:name"
render={ (props) => {
const resourcePath =
`${props.match.params.ship}/${props.match.params.name}`;
const resource =
associations.link[resourcePath] ?
associations.link[resourcePath] : { metadata: {} };
const contactDetails = contacts[resource['group-path']] || {};
const popout = props.match.url.includes('/popout/');
const graph = graphs[resourcePath] || null;
const amOwner = amOwnerOfGroup(resource['group-path']);
const contactDetails = contacts[resource['group-path']] || {};
const page = props.match.params.page || 0;
const popout = props.match.url.includes('/popout/');
return (
<Skeleton
associations={associations}
invites={invites}
groups={groups}
selected={resourcePath}
sidebarShown={sidebarShown}
sidebarHideMobile={true}
popout={popout}
api={api}>
<Links
{...props}
contacts={contactDetails}
page={page}
resourcePath={resourcePath}
resource={resource}
amOwner={amOwner}
popout={popout}
sidebarShown={sidebarShown}
api={api}
hideNicknames={hideNicknames}
hideAvatars={hideAvatars} />
</Skeleton>
if (!graph) {
api.graph.getGraph(
`~${props.match.params.ship}`,
props.match.params.name
);
}}
/>
<Route exact path="/~link/(popout)?/:resource/:page/:index/:encodedUrl/:commentpage?"
render={ (props) => {
const resourcePath = '/' + props.match.params.resource;
const resource = associations.link[resourcePath] || { metadata: {} };
}
const amOwner = amOwnerOfGroup(resource['group-path']);
const popout = props.match.url.includes('/popout/');
const contactDetails = contacts[resource['group-path']] || {};
const index = props.match.params.index || 0;
const page = props.match.params.page || 0;
const url = base64urlDecode(props.match.params.encodedUrl);
return (
<Skeleton
associations={associations}
invites={invites}
groups={groups}
selected={resourcePath}
sidebarShown={sidebarShown}
sidebarHideMobile={true}
return (
<Skeleton
associations={associations}
invites={invites}
groups={groups}
selected={resourcePath}
sidebarShown={sidebarShown}
sidebarHideMobile={true}
popout={popout}
api={api}
graphKeys={graphKeys}>
<LinkList
{...props}
api={api}
graph={graph}
popout={popout}
api={api}>
<LinkDetail
{...props}
resource={resource}
page={page}
url={url}
linkIndex={index}
contacts={contactDetails}
resourcePath={resourcePath}
groupPath={resource['group-path']}
amOwner={amOwner}
popout={popout}
sidebarShown={sidebarShown}
api={api}
hideAvatars={hideAvatars}
hideNicknames={hideNicknames} />
</Skeleton>
metadata={resource.metadata}
contacts={contactDetails}
hideAvatars={hideAvatars}
hideNicknames={hideNicknames}
sidebarShown={sidebarShown}
ship={props.match.params.ship}
name={props.match.params.name}
/>
</Skeleton>
);
}}
/>
<Route exact path="/~link/(popout)?/:ship/:name/:index"
render={ (props) => {
const resourcePath =
`${props.match.params.ship}/${props.match.params.name}`;
const resource =
associations.link[resourcePath] ?
associations.link[resourcePath] : { metadata: {} };
const popout = props.match.url.includes('/popout/');
const contactDetails = contacts[resource['group-path']] || {};
console.log(props.match.params.index.split('-'));
const indexArr = props.match.params.index.split('-');
const graph = graphs[resourcePath] || null;
if (indexArr.length <= 1) {
return <div>Malformed URL</div>;
}
const index = parseInt(indexArr[1], 10);
const node = !!graph ? graph.get(index) : null;
if (!graph) {
api.graph.getGraph(
`~${props.match.params.ship}`,
props.match.params.name
);
}}
/>
}
return (
<Skeleton
associations={associations}
invites={invites}
groups={groups}
selected={resourcePath}
sidebarShown={sidebarShown}
sidebarHideMobile={true}
popout={popout}
graphKeys={graphKeys}
api={api}>
<LinkDetail
{...props}
node={node}
ship={props.match.params.ship}
name={props.match.params.name}
resource={resource}
contacts={contactDetails}
popout={popout}
sidebarShown={sidebarShown}
api={api}
hideAvatars={hideAvatars}
hideNicknames={hideNicknames} />
</Skeleton>
);
}}
/>
</Switch>
</>
);

View File

@ -23,9 +23,7 @@ export const ChannelSidebar = (props) => {
alphabetiseAssociations(props.associations.contacts) : {};
const groupedChannels = {};
console.log(props.graphKeys);
[...props.graphKeys].map((path) => {
console.log(props.associations.link);
const groupPath = props.associations.link[path] ?
props.associations.link[path]['group-path'] : '';

View File

@ -1,57 +1,21 @@
import React, { Component } from 'react';
import React from 'react';
import { CommentItem } from './comment-item';
import { CommentsPagination } from './comments-pagination';
import { getContactDetails } from '~/logic/lib/util';
export class Comments extends Component {
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {
const page = this.props.commentPage;
if (!this.props.comments ||
!this.props.comments[page] ||
this.props.comments.local[page]
) {
this.setState({ requested: this.props.commentPage });
this.props.api.links.getCommentsPage(
this.props.resourcePath,
this.props.url,
this.props.commentPage);
}
}
render() {
const props = this.props;
export const Comments = (props) => {
const { hideNicknames, hideAvatars } = props;
console.log(props);
const page = props.commentPage;
const commentsObj = props.comments
? props.comments
: {};
const commentsPage = commentsObj[page]
? commentsObj[page]
: {};
const total = props.comments
? props.comments.totalPages
: 1;
const { hideNicknames, hideAvatars } = props;
const commentsList = Object.keys(commentsPage)
/*const commentsList = Object.keys(commentsPage)
.map((entry) => {
const commentObj = commentsPage[entry];
const { ship, time, udon } = commentObj;
const contacts = props.contacts
? props.contacts
: {};
const contacts = props.contacts ? props.contacts : {};
const { nickname, color, member, avatar } = getContactDetails(contacts[ship]);
const { nickname, color, member, avatar } =
getContactDetails(contacts[ship]);
const nameClass = nickname && !hideNicknames ? 'inter' : 'mono';
@ -70,23 +34,24 @@ export class Comments extends Component {
hideAvatars={hideAvatars}
/>
);
});
return (
<div>
{commentsList}
<CommentsPagination
key={props.resourcePath + props.commentPage}
resourcePath={props.resourcePath}
popout={props.popout}
linkPage={props.linkPage}
linkIndex={props.linkIndex}
url={props.url}
commentPage={props.commentPage}
total={total}
/>
</div>
);
}
});*/
return (
<div>
{ Array.from(props.comments.values()).map((comment) => {
/*const commentObj = commentsPage[entry];
const { ship, time, udon } = commentObj;
const contacts = props.contacts ? props.contacts : {};
const { nickname, color, member, avatar } =
getContactDetails(contacts[ship]);
const nameClass = nickname && !hideNicknames ? 'inter' : 'mono';*/
console.log(comment);
return (<div></div>);
})
}
</div>
);
}
export default Comments;

View File

@ -1,150 +0,0 @@
import React, { Component } from 'react';
import { cite } from '~/logic/lib/util';
import moment from 'moment';
export class LinkPreview extends Component {
constructor(props) {
super(props);
this.state = {
timeSinceLinkPost: this.getTimeSinceLinkPost(),
embed: ''
};
}
componentDidUpdate(prevProps) {
if (prevProps !== this.props) {
if (this.state.timeSinceLinkPost === '') {
this.setState({
timeSinceLinkPost: this.getTimeSinceLinkPost()
});
}
}
}
componentDidMount() {
this.updateTimeSinceNewestMessageInterval = setInterval(() => {
this.setState({
timeSinceLinkPost: this.getTimeSinceLinkPost()
});
}, 60000);
// check for soundcloud for fetching embed
const soundcloudRegex = new RegExp(String(/(https?:\/\/(?:www.)?soundcloud.com\/[\w-]+\/?(?:sets\/)?[\w-]+)/.source)
);
const isSoundcloud = soundcloudRegex.exec(this.props.url);
if (isSoundcloud && this.state.embed === '') {
fetch(
'https://soundcloud.com/oembed?format=json&url=' +
encodeURIComponent(this.props.url))
.then((response) => {
return response.json();
})
.then((json) => {
this.setState({ embed: json.html });
});
} else if (!isSoundcloud) {
this.setState({ embed: '' });
}
}
componentWillUnmount() {
if (this.updateTimeSinceNewestMessageInterval) {
clearInterval(this.updateTimeSinceNewestMessageInterval);
this.updateTimeSinceNewestMessageInterval = null;
}
}
getTimeSinceLinkPost() {
const time = this.props.time;
return time
? moment.unix(time / 1000).from(moment.utc())
: '';
}
render() {
const { props } = this;
const 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}/
);
let hostname = URLparser.exec(props.url);
if (hostname) {
hostname = hostname[4];
}
const imgMatch = /(jpg|img|png|gif|tiff|jpeg|JPG|IMG|PNG|TIFF|GIF|webp|WEBP|webm|WEBM)$/.exec(
props.url
);
const youTubeRegex = new RegExp(
String(/(?:https?:\/\/(?:[a-z]+.)?)/.source) + // protocol
/(?:youtu\.?be(?:\.com)?\/)(?:embed\/)?/.source + // short and long-links
/(?:(?:(?:(?:watch\?)?(?:time_continue=(?:[0-9]+))?.+v=)?([a-zA-Z0-9_-]+))(?:\?t\=(?:[0-9a-zA-Z]+))?)/.source // id
);
const ytMatch = youTubeRegex.exec(props.url);
let embed = '';
if (imgMatch) {
embed = <a href={props.url}
target="_blank"
style={{ width: 'max-content' }}
>
<img src={props.url} style={{ maxHeight: '500px', maxWidth: '100%' }} />
</a>;
}
if (ytMatch) {
embed = (
<iframe
ref="iframe"
width="560"
height="315"
src={`https://www.youtube.com/embed/${ytMatch[1]}`}
frameBorder="0"
allow="picture-in-picture, fullscreen"
></iframe>
);
}
const showNickname = props.nickname && !props.hideNicknames;
const nameClass = showNickname ? 'inter' : 'mono';
return (
<div className="pb6 w-100">
<div
className={'w-100 tc ' + (ytMatch ? 'links embed-container' : '')}
>
{embed || <div dangerouslySetInnerHTML={{ __html: this.state.embed }} />}
</div>
<div className="flex flex-column ml2 pt6 flex-auto">
<a href={props.url} className="w-100 flex" target="_blank" rel="noopener noreferrer">
<p className="f8 truncate">
{props.title}
</p>
<span className="gray2 ml2 f8 dib v-btm flex-shrink-0">{hostname} </span>
</a>
<div className="w-100 pt1">
<span className={'f9 pr2 white-d dib ' + nameClass}
title={props.ship}
>
{showNickname ? props.nickname : cite(props.ship)}
</span>
<span className="f9 inter gray2 pr3 dib">
{this.state.timeSinceLinkPost}
</span>
<span className="f9 inter gray2 dib">{props.comments}</span>
</div>
</div>
</div>
);
}
}
export default LinkPreview;

View File

@ -3,113 +3,54 @@ import moment from 'moment';
import { Sigil } from '~/logic/lib/sigil';
import { Link } from 'react-router-dom';
import { makeRoutePath, cite } from '~/logic/lib/util';
import { cite } from '~/logic/lib/util';
export class LinkItem extends Component {
constructor(props) {
super(props);
this.state = {
timeSinceLinkPost: this.getTimeSinceLinkPost()
};
this.markPostAsSeen = this.markPostAsSeen.bind(this);
}
export const LinkItem = (props) => {
const {
node,
nickname,
resource,
hideAvatars,
hideNicknames
} = props;
componentDidMount() {
this.updateTimeSinceNewestMessageInterval = setInterval( () => {
this.setState({ timeSinceLinkPost: this.getTimeSinceLinkPost() });
}, 60000);
}
const author = node.post.author;
const index = node.post.index.split('/').join('-');
const size = node.children ? node.children.size : 0;
const contents = node.post.contents;
componentWillUnmount() {
if (this.updateTimeSinceNewestMessageInterval) {
clearInterval(this.updateTimeSinceNewestMessageInterval);
this.updateTimeSinceNewestMessageInterval = null;
}
}
const showAvatar = props.avatar && !hideAvatars;
const showNickname = nickname && !hideNicknames;
getTimeSinceLinkPost() {
return this.props.timestamp ?
moment.unix(this.props.timestamp / 1000).from(moment.utc())
: '';
}
const mono = showNickname ? 'inter white-d' : 'mono white-d';
markPostAsSeen() {
this.props.api.links.seenLink(this.props.resourcePath, this.props.url);
}
const img = showAvatar
? <img src={props.avatar} height={38} width={38} className="dib" />
: <Sigil ship={`~${author}`} size={38} color={'#' + props.color} />;
render() {
const props = this.props;
const 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}/);
let hostname = URLparser.exec(props.url);
const seenState = props.seen
? 'gray2'
: 'green2 pointer';
if (hostname) {
hostname = hostname[4];
}
const comments = props.comments + ' comment' + ((props.comments === 1) ? '' : 's');
const member = this.props.member || false;
const showAvatar = props.avatar && !props.hideAvatars;
const showNickname = props.nickname && !props.hideNicknames;
const mono = showNickname ? 'inter white-d' : 'mono white-d';
const img = showAvatar
? <img src={this.props.avatar} height={38} width={38} className="dib" />
: <Sigil
ship={'~' + props.ship}
size={38}
color={'#' + props.color}
classes={(member ? 'mix-blend-diff' : '')}
/>;
return (
<div className="w-100 pv3 flex bg-white bg-gray0-d lh-solid">
return (
<div className="w-100 pv3 flex bg-white bg-gray0-d lh-solid">
{img}
<div className="flex flex-column ml2 flex-auto">
<a href={props.url}
className="w-100 flex"
target="_blank"
rel="noopener noreferrer"
onClick={this.markPostAsSeen}
>
<p className="f8 truncate">{props.title}
</p>
<span className="gray2 dib v-btm ml2 f8 flex-shrink-0">{hostname} </span>
</a>
<div className="w-100">
<span className={'f9 pr2 dib ' + mono}
title={props.ship}
>
{showNickname
? props.nickname
: cite(props.ship)}
</span>
<span
className={seenState + ' f9 inter pr3 dib'}
onClick={this.markPostAsSeen}
>
{this.state.timeSinceLinkPost}
<div className="flex flex-column ml2 flex-auto">
<a href={contents[1].url}
className="w-100 flex"
target="_blank"
rel="noopener noreferrer">
<p className="f8 truncate">{props.title}</p>
<span className="gray2 dib v-btm ml2 f8 flex-shrink-0">
{contents[0].text}
</span>
<Link to=
{makeRoutePath(props.resourcePath, props.popout, props.page, props.url, props.linkIndex)}
onClick={this.markPostAsSeen}
>
<span className="f9 inter gray2 dib">
{comments}
</span>
</Link>
</div>
</a>
<div className="w-100">
<span className={'f9 pr2 pl2 dib ' + mono} title={author}>
{ showNickname ? nickname : cite(author) }
</span>
<Link to={`/~link/${resource}/${index}`}>
<span className="f9 inter gray2 dib">{size} comments</span>
</Link>
</div>
</div>
);
}
</div>
);
}
export default LinkItem;

View File

@ -0,0 +1,45 @@
import React from 'react';
import { cite } from '~/logic/lib/util';
import moment from 'moment';
export const LinkPreview = (props) => {
const showNickname = props.nickname && !props.hideNicknames;
const nameClass = showNickname ? 'inter' : 'mono';
const author = props.post.author;
const timeSent =
moment.unix(props.post['time-sent'] / 1000).format('hh:mm a');
const title = props.post.contents[0].text;
const url = props.post.contents[1].url;
return (
<div className="pb6 w-100">
<div className="flex flex-column ml2 pt6 flex-auto">
<a href={url}
className="w-100 flex"
target="_blank"
rel="noopener noreferrer">
<p className="f8 truncate">{title}</p>
<span className="gray2 ml2 f8 dib v-btm flex-shrink-0">
{url}
</span>
</a>
<div className="w-100 pt1">
<span className={
'f9 pr2 white-d dib ' + nameClass
}
title={author}>
{showNickname ? props.nickname : cite(`~${author}`)}
</span>
<span className="f9 inter gray2 pr3 dib">{timeSent}</span>
<span className="f9 inter gray2 dib">
{props.commentNumber} comments
</span>
</div>
</div>
</div>
);
}

View File

@ -1,5 +1,7 @@
import React, { Component } from 'react';
import { Spinner } from '~/views/components/Spinner';
import { createPost } from '~/logic/api/graph';
export class LinkSubmit extends Component {
constructor() {
@ -7,7 +9,6 @@ export class LinkSubmit extends Component {
this.state = {
linkValue: '',
linkTitle: '',
linkValid: false,
submitFocus: false,
disabled: false
};
@ -21,39 +22,24 @@ export class LinkSubmit extends Component {
? this.state.linkTitle
: this.state.linkValue;
this.setState({ disabled: true });
this.props.api.links.postLink(this.props.resourcePath, link, title).then((r) => {
this.setState({
disabled: false,
linkValue: '',
linkTitle: '',
linkValid: false
let post = createPost([
{ text: title },
{ url: link }
]);
this.props.api.graph.addPost(`~${this.props.ship}`, this.props.name, post)
.then((r) => {
this.setState({
disabled: false,
linkValue: '',
linkTitle: '',
});
});
});
}
setLinkValid(link) {
const 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}/
);
const validURL = URLparser.exec(link);
if (!validURL) {
const checkProtocol = URLparser.exec('http://' + link);
if (checkProtocol) {
this.setState({ linkValid: true });
this.setState({ linkValue: 'http://' + link });
} else {
this.setState({ linkValid: false });
}
} else if (validURL) {
this.setState({ linkValid: true });
}
}
setLinkValue(event) {
this.setState({ linkValue: event.target.value });
this.setLinkValid(event.target.value);
}
setLinkTitle(event) {
@ -61,8 +47,7 @@ export class LinkSubmit extends Component {
}
render() {
const activeClasses = (this.state.linkValid && !this.state.disabled)
? 'green2 pointer' : 'gray2';
const activeClasses = (!this.state.disabled) ? 'green2 pointer' : 'gray2';
const focus = (this.state.submitFocus)
? 'b--black b--white-d'
@ -116,7 +101,7 @@ export class LinkSubmit extends Component {
className={
'absolute bg-gray0-d f8 ml2 flex-shrink-0 ' + activeClasses
}
disabled={!this.state.linkValid || this.state.disabled}
disabled={this.state.disabled}
onClick={this.onClickPost.bind(this)}
style={{
bottom: 12,
@ -125,9 +110,12 @@ export class LinkSubmit extends Component {
>
Post
</button>
<Spinner awaiting={this.state.disabled} classes="mt3 absolute right-0" text="Posting to collection..." />
<Spinner
awaiting={this.state.disabled}
classes="mt3 absolute right-0"
text="Posting to collection..." />
</div>
) ;
);
}
}

View File

@ -0,0 +1,79 @@
import React, { Component } from 'react';
import { TabBar } from '~/views/components/chat-link-tabbar';
import { LinkPreview } from './lib/link-preview';
import { LinkSubmit } from './lib/link-submit';
import { SidebarSwitcher } from '~/views/components/SidebarSwitch';
import { Link } from 'react-router-dom';
import { Comments } from './lib/comments';
import { getContactDetails } from '~/logic/lib/util';
export const LinkDetail = (props) => {
console.log(props);
if (!props.node) {
// TODO: something
return (
<div>
Not found
</div>
);
}
const { nickname } = getContactDetails(props.contacts[ship]);
const our = getContactDetails(props.contacts[window.ship]);
const resourcePath = `${props.ship}/${props.name}`;
const title = props.resource.metadata.title || resourcePath;
return (
<div className="h-100 w-100 overflow-hidden flex flex-column">
<div
className={
'pl4 pt2 flex relative overflow-x-scroll ' +
'overflow-x-auto-l overflow-x-auto-xl flex-shrink-0 ' +
'bb bn-m bn-l bn-xl b--gray4'
}
style={{ height: 48 }}>
<SidebarSwitcher
sidebarShown={props.sidebarShown}
popout={props.popout}
api={props.api}
/>
<Link
className="dib f9 fw4 pt2 gray2 lh-solid"
to="/~link">
{`<- ${title}`}
</Link>
<TabBar
location={props.location}
popout={props.popout}
popoutHref={"/~link"}
settings={"/~link"}
/>
</div>
<div className="w-100 mt2 flex justify-center overflow-y-scroll ph4 pb4">
<div className="w-100 mw7">
<LinkPreview
resourcePath={resourcePath}
post={props.node.post}
nickname={nickname}
hideNicknames={props.hideNicknames}
commentNumber={props.node.children.size} />
<div className="flex">
<LinkSubmit
name={props.name}
ship={props.ship}
api={props.api} />
</div>
<Comments
comments={props.node.children}
resourcePath={resourcePath}
contacts={props.contacts}
popout={props.popout}
api={props.api}
hideAvatars={props.hideAvatars}
hideNicknames={props.hideNicknames} />
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,66 @@
import React, { Component } from 'react';
export class LinkDetail extends Component {
constructor(props) {
super(props);
this.state = {
comment: '',
data: props.data,
disabled: false
};
}
onClickPost() {
const url = this.props.url || '';
// TODO: use graph API
this.props.api.links.postComment(
this.props.resourcePath,
url,
this.state.comment
).then(() => {
this.setState({ comment: '', disabled: false });
});
}
render() {
const activeClasses = this.state.comment
? 'black white-d pointer'
: 'gray2 b--gray2';
return (
<div className="relative">
<div className='relative ba br1 mt6 mb6'>
<textarea
className="w-100 bg-gray0-d white-d f8 pa2 pr8"
style={{
resize: 'none',
height: 75
}}
placeholder="Leave a comment on this link"
onChange={this.setComment}
value={this.state.comment}
/>
<button
className={
'f8 bg-gray0-d ml2 absolute ' + activeClasses
}
disabled={!this.state.comment || this.state.disabled}
onClick={this.onClickPost.bind(this)}
style={{
bottom: 12,
right: 8
}}>
Post
</button>
</div>
<Spinner
awaiting={this.state.disabled}
classes="absolute pt5 right-0"
text="Posting comment..." />
</div>
);
}
}

View File

@ -0,0 +1,71 @@
import React, { Component } from 'react';
import { TabBar } from '~/views/components/chat-link-tabbar';
import { SidebarSwitcher } from '~/views/components/SidebarSwitch';
import { Link } from 'react-router-dom';
import { LinkItem } from './lib/link-item';
import { LinkSubmit } from './lib/link-submit';
import { getContactDetails } from '~/logic/lib/util';
export const LinkList = (props) => {
const resource = `${props.ship}/${props.name}`;
const title = props.metadata.title || resource;
if (!props.graph) {
return (
<div>Not found</div>
);
}
return (
<div className="h-100 w-100 overflow-hidden flex flex-column">
<div
className="w-100 dn-m dn-l dn-xl inter pt4 pb6 pl3 f8"
style={{ height: '1rem' }}>
<Link to="/~link">{'⟵ All Channels'}</Link>
</div>
<div className={
'pl4 pt2 flex relative overflow-x-scroll' +
'overflow-x-auto-l overflow-x-auto-xl flex-shrink-0' +
'bb b--gray4 b--gray1-d bg-gray0-d'
}
style={{ height: 48 }}>
<SidebarSwitcher
sidebarShown={props.sidebarShown}
popout={props.popout}
api={props.api} />
<h2 className='dib f9 fw4 lh-solid v-top pt2'>{title}</h2>
<TabBar
location={props.location}
popout={props.popout}
popoutHref={`/~link/popout/${resource}`}
settings={`/~link/${resource}/settings`}
/>
</div>
<div className="w-100 mt6 flex justify-center overflow-y-scroll ph4 pb4">
<div className="w-100 mw7">
<div className="flex">
<LinkSubmit
name={props.name}
ship={props.ship}
api={props.api} />
</div>
{ Array.from(props.graph.values()).map((node) => {
return (
<LinkItem
resource={resource}
node={node}
nickname={props.metadata.nickname}
hideAvatars={props.hideAvatars}
hideNicknames={props.hideNicknames}
/>
);
})
}
</div>
</div>
</div>
);
}

View File

@ -1,231 +0,0 @@
import React, { Component } from 'react';
import { TabBar } from '~/views/components/chat-link-tabbar';
import { LinkPreview } from './lib/link-detail-preview';
import { SidebarSwitcher } from '~/views/components/SidebarSwitch';
import { Link } from 'react-router-dom';
import { Comments } from './lib/comments';
import { Spinner } from '~/views/components/Spinner';
import { LoadingScreen } from './loading';
import { makeRoutePath, getContactDetails } from '~/logic/lib/util';
import CommentItem from './lib/comment-item';
export class LinkDetail extends Component {
constructor(props) {
super(props);
this.state = {
comment: '',
data: props.data,
commentFocus: false,
pending: new Set(),
disabled: false
};
this.setComment = this.setComment.bind(this);
}
updateData(submission) {
this.setState({
data: submission
});
}
componentDidMount() {
this.componentDidUpdate();
}
componentDidUpdate(prevProps) {
// if we have no preloaded data, and we aren't expecting it, get it
if ((!this.state.data.title) && (this.props.api)) {
this.props.api?.links.getSubmission(
this.props.resourcePath, this.props.url, this.updateData.bind(this)
);
}
if (prevProps) {
if (this.props.url !== prevProps.url) {
this.updateData(this.props.data);
}
if (prevProps.comments && prevProps.comments['0'] &&
this.props.comments && this.props.comments['0']) {
const prevFirstComment = prevProps.comments['0'][0];
const thisFirstComment = this.props.comments['0'][0];
if ((prevFirstComment && prevFirstComment.udon) &&
(thisFirstComment && thisFirstComment.udon)) {
if (this.state.pending.has(thisFirstComment.udon)) {
const pending = this.state.pending;
pending.delete(thisFirstComment.udon);
this.setState({
pending: pending
});
}
}
}
}
}
onClickPost() {
const url = this.props.url || '';
const pending = this.state.pending;
pending.add(this.state.comment);
this.setState({ pending: pending, disabled: true });
this.props.api.links.postComment(
this.props.resourcePath,
url,
this.state.comment
).then(() => {
this.setState({ comment: '', disabled: false });
});
}
setComment(event) {
this.setState({ comment: event.target.value });
}
render() {
const props = this.props;
const data = this.state.data || props.data;
if (!data.ship) {
return <LoadingScreen />;
}
const ship = data.ship || 'zod';
const title = data.title || '';
const url = data.url || '';
const commentCount = props.comments
? props.comments.totalItems
: data.commentCount || 0;
const comments = commentCount + ' comment' + (commentCount === 1 ? '' : 's');
const { nickname } = getContactDetails(props.contacts[ship]);
const activeClasses = this.state.comment
? 'black white-d pointer'
: 'gray2 b--gray2';
const focus = (this.state.commentFocus)
? 'b--black b--white-d'
: 'b--gray4 b--gray2-d';
const our = getContactDetails(props.contacts[window.ship]);
const pendingArray = Array.from(this.state.pending).map((com, i) => {
return(
<CommentItem
key={i}
color={our.color}
nickname={our.nickname}
ship={window.ship}
pending={true}
content={com}
member={our.member}
time={new Date().getTime()}
/>
);
});
return (
<div className="h-100 w-100 overflow-hidden flex flex-column">
<div
className={'pl4 pt2 flex relative overflow-x-scroll ' +
'overflow-x-auto-l overflow-x-auto-xl flex-shrink-0 ' +
'bb bn-m bn-l bn-xl b--gray4'}
style={{ height: 48 }}
>
<SidebarSwitcher
sidebarShown={props.sidebarShown}
popout={props.popout}
api={this.props.api}
/>
<Link
className="dib f9 fw4 pt2 gray2 lh-solid"
to={makeRoutePath(props.resourcePath, props.popout, props.page)}
>
{`<- ${props.resource.metadata.title}`}
</Link>
<TabBar
location={props.location}
popout={props.popout}
popoutHref={makeRoutePath(props.resourcePath, true, props.page)}
settings={makeRoutePath(props.resourcePath, props.popout) + '/settings'}
/>
</div>
<div className="w-100 mt2 flex justify-center overflow-y-scroll ph4 pb4">
<div className="w-100 mw7">
<LinkPreview
title={title}
url={url}
comments={comments}
nickname={nickname}
ship={ship}
resourcePath={props.resourcePath}
page={props.page}
linkIndex={props.linkIndex}
time={this.state.data.time}
hideNicknames={props.hideNicknames}
/>
<div className="relative">
<div className={'relative ba br1 mt6 mb6 ' + focus}>
<textarea
className="w-100 bg-gray0-d white-d f8 pa2 pr8"
style={{
resize: 'none',
height: 75
}}
placeholder="Leave a comment on this link"
onChange={this.setComment}
onKeyDown={(e) => {
if (
(e.getModifierState('Control') || e.metaKey) &&
e.key === 'Enter'
) {
this.onClickPost();
}
}}
onFocus={() => this.setState({ commentFocus: true })}
onBlur={() => this.setState({ commentFocus: false })}
value={this.state.comment}
/>
<button
className={
'f8 bg-gray0-d ml2 absolute ' + activeClasses
}
disabled={!this.state.comment || this.state.disabled}
onClick={this.onClickPost.bind(this)}
style={{
bottom: 12,
right: 8
}}
>
Post
</button>
</div>
<Spinner awaiting={this.state.disabled} classes="absolute pt5 right-0" text="Posting comment..." />
{pendingArray}
</div>
<Comments
resourcePath={props.resourcePath}
key={props.resourcePath + props.commentPage}
comments={props.comments}
commentPage={props.commentPage}
contacts={props.contacts}
popout={props.popout}
url={props.url}
linkPage={props.page}
linkIndex={props.linkIndex}
api={props.api}
hideAvatars={props.hideAvatars}
hideNicknames={props.hideNicknames}
/>
</div>
</div>
</div>
);
}
}
export default LinkDetail;

View File

@ -1,162 +0,0 @@
import React, { Component } from 'react';
import { LoadingScreen } from './loading';
import { MessageScreen } from './lib/message-screen';
import { TabBar } from '~/views/components/chat-link-tabbar';
import { SidebarSwitcher } from '~/views/components/SidebarSwitch';
import { Link } from 'react-router-dom';
import { LinkItem } from './lib/link-item';
import { LinkSubmit } from './lib/link-submit';
import { Pagination } from './lib/pagination';
import { makeRoutePath, getContactDetails } from '~/logic/lib/util';
export class Links extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.componentDidUpdate();
}
componentDidUpdate(prevProps) {
const linkPage = this.props.page;
// if we just navigated to this particular page,
// and don't have links for it yet,
// or the links we have might not be complete,
// request the links for that page.
if ( ((!prevProps || // first load?
linkPage !== prevProps.page || // already waiting on response?
this.props.resourcePath !== prevProps.resourcePath // new page?
) ||
(prevProps.api !== this.props.api)) // api prop instantiated?
&&
!this.props.links[linkPage] || // don't have info?
this.props.links.local[linkPage] // waiting on post confirmation?
) {
this.props.api?.links.getPage(this.props.resourcePath, this.props.page);
}
}
render() {
const props = this.props;
if (!props.resource.metadata.title) {
return <LoadingScreen />;
}
const linkPage = props.page;
const links = props.links[linkPage]
? props.links[linkPage]
: {};
const currentPage = props.page
? Number(props.page)
: 0;
const totalPages = props.links
? Number(props.links.totalPages)
: 1;
let LinkList = (<LoadingScreen />);
if (props.links && props.links.totalItems === 0) {
LinkList = (
<MessageScreen text="Start by posting a link to this collection." />
);
} else if (Object.keys(links).length > 0) {
LinkList = Object.keys(links)
.map((linkIndex) => {
const linksObj = props.links[linkPage];
const { title, url, time, ship } = linksObj[linkIndex];
const seen = props.seen[url];
const commentCount = props.comments[url]
? props.comments[url].totalItems
: linksObj[linkIndex].commentCount || 0;
const { nickname, color, member, avatar } = getContactDetails(props.contacts[ship]);
return (
<LinkItem
key={time}
title={title}
page={props.page}
linkIndex={linkIndex}
url={url}
timestamp={time}
seen={seen}
nickname={nickname}
ship={ship}
color={color}
avatar={avatar}
member={member}
comments={commentCount}
resourcePath={props.resourcePath}
popout={props.popout}
api={props.api}
hideNicknames={props.hideNicknames}
hideAvatars={props.hideAvatars}
/>
);
});
}
return (
<div
className="h-100 w-100 overflow-hidden flex flex-column"
>
<div
className="w-100 dn-m dn-l dn-xl inter pt4 pb6 pl3 f8"
style={{ height: '1rem' }}
>
<Link to="/~link">{'⟵ All Channels'}</Link>
</div>
<div
className={`pl4 pt2 flex relative overflow-x-scroll
overflow-x-auto-l overflow-x-auto-xl flex-shrink-0
bb b--gray4 b--gray1-d bg-gray0-d`}
style={{ height: 48 }}
>
<SidebarSwitcher
sidebarShown={props.sidebarShown}
popout={props.popout}
api={this.props.api}
/>
<Link to={makeRoutePath(props.resourcePath, props.popout, props.page)} className="pt2">
<h2 className={'dib f9 fw4 lh-solid v-top'}>
{props.resource.metadata.title}
</h2>
</Link>
<TabBar
location={props.location}
popout={props.popout}
popoutHref={makeRoutePath(props.resourcePath, true, props.page)}
settings={makeRoutePath(props.resourcePath, props.popout) + '/settings'}
/>
</div>
<div className="w-100 mt6 flex justify-center overflow-y-scroll ph4 pb4">
<div className="w-100 mw7">
<div className="flex">
<LinkSubmit resourcePath={props.resourcePath} api={this.props.api} />
</div>
<div className="pb4">
{LinkList}
<Pagination
{...props}
key={props.resourcePath + props.page}
popout={props.popout}
resourcePath={props.resourcePath}
currentPage={currentPage}
totalPages={totalPages}
/>
</div>
</div>
</div>
</div>
);
}
}
export default Links;

View File

@ -21,7 +21,6 @@ export class SettingsScreen extends Component {
};
this.renderDelete = this.renderDelete.bind(this);
this.markAllAsSeen = this.markAllAsSeen.bind(this);
this.changeLoading = this.changeLoading.bind(this);
}
@ -77,10 +76,6 @@ export class SettingsScreen extends Component {
});
}
markAllAsSeen() {
this.props.api.links.seenLink(this.props.resourcePath);
}
renderRemove() {
return (
<div className="w-100 fl mt3">
@ -206,11 +201,6 @@ export class SettingsScreen extends Component {
<h2 className="f8 pb2">Collection Settings</h2>
<p className="f8 mt3 lh-copy db">Mark all links as read</p>
<p className="f9 gray2 db mb4">Mark all links in this collection as read.</p>
<a className="dib f9 black gray4-d bg-gray0-d ba pa2 b--black b--gray1-d pointer"
onClick={this.markAllAsSeen}
>
Mark all as read
</a>
{this.renderRemove()}
{this.renderDelete()}
<MetadataSettings

View File

@ -5,9 +5,7 @@ import ErrorBoundary from '~/views/components/ErrorBoundary';
export class Skeleton extends Component {
render() {
const { props } = this;
const rightPanelHide = props.rightPanelHide
? 'dn-s' : '';
const rightPanelHide = props.rightPanelHide ? 'dn-s' : '';
const popout = props.popout ? props.popout : false;
const popoutWindow = (popout)
@ -16,7 +14,7 @@ export class Skeleton extends Component {
const popoutBorder = (popout)
? '' : 'ba-m ba-l ba-xl b--gray4 b--gray1-d br1';
const linkInvites = ('/link' in props.invites)
const linkInvites = ('/link' in props.invites)
? props.invites['/link'] : {};
return (