mirror of
https://github.com/urbit/shrub.git
synced 2024-12-01 06:35:32 +03:00
interface: links uses graph-store
This commit is contained in:
parent
39ad9f4c60
commit
e60e5fcdbd
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
@ -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'] : '';
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
) ;
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
79
pkg/interface/src/views/apps/links/components/link-detail.js
Normal file
79
pkg/interface/src/views/apps/links/components/link-detail.js
Normal 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>
|
||||
);
|
||||
}
|
||||
|
66
pkg/interface/src/views/apps/links/components/link-input.js
Normal file
66
pkg/interface/src/views/apps/links/components/link-input.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
71
pkg/interface/src/views/apps/links/components/link-list.js
Normal file
71
pkg/interface/src/views/apps/links/components/link-list.js
Normal 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>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
@ -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;
|
@ -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
|
||||
|
@ -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 (
|
||||
|
Loading…
Reference in New Issue
Block a user