Merge branch 'mp/avatar-incl-displays' (#2788)

* mp/avatar-incl-displays:
  publish: show avatars if set
  link: show avatars if set
  groups: show avatars if set
  chat: display avatars if set

Signed-off-by: Matilde Park <matilde@tlon.io>
This commit is contained in:
Matilde Park 2020-04-23 22:55:27 -04:00
commit d7fb181827
15 changed files with 364 additions and 318 deletions

View File

@ -31,7 +31,6 @@ const MARKDOWN_CONFIG = {
}
};
export class ChatInput extends Component {
constructor(props) {
super(props);
@ -117,7 +116,6 @@ export class ChatInput extends Component {
return;
}
this.setState({ patpSearch: match[1].toLowerCase() });
}
clearSearch() {
@ -276,6 +274,15 @@ export class ChatInput extends Component {
const sigilClass = props.ownerContact
? '' : 'mix-blend-diff';
const img = (props.ownerContact && (props.ownerContact.avatar !== null))
? <img src={props.ownerContact.avatar} height={24} width={24} className="dib" />
: <Sigil
ship={window.ship}
size={24}
color={`#${color}`}
classes={sigilClass}
/>;
const candidates = _.chain(this.props.envelopes)
.defaultTo([])
.map('author')
@ -324,12 +331,7 @@ export class ChatInput extends Component {
height: 24
}}
>
<Sigil
ship={window.ship}
size={24}
color={`#${color}`}
classes={sigilClass}
/>
{img}
</div>
<div
className="fr h-100 flex bg-gray0-d lh-copy pl2 w-100 items-center"

View File

@ -1,11 +1,8 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { Sigil } from '/components/lib/icons/sigil';
import { uxToHex, cite } from '/lib/util';
export class MemberElement extends Component {
onRemove() {
const { props } = this;
props.api.groups.remove([`~${props.ship}`], props.path);
@ -24,7 +21,8 @@ export class MemberElement extends Component {
} else if (window.ship !== props.ship && window.ship === props.owner) {
actionElem = (
<a onClick={this.onRemove.bind(this)}
className="w-20 dib list-ship black white-d f8 pointer">
className="w-20 dib list-ship black white-d f8 pointer"
>
Ban
</a>
);
@ -34,20 +32,24 @@ export class MemberElement extends Component {
);
}
let name = !!props.contact
const name = props.contact
? `${props.contact.nickname} (${cite(props.ship)})` : `${cite(props.ship)}`;
let color = !!props.contact ? uxToHex(props.contact.color) : '000000';
const color = props.contact ? uxToHex(props.contact.color) : '000000';
const img = (props.contact && (props.contact.avatar !== null))
? <img src={props.contact.avatar} height={32} width={32} className="dib" />
: <Sigil ship={props.ship} size={32} color={`#${color}`} />;
return (
<div className="flex mb2">
<Sigil ship={props.ship} size={32} color={`#${color}`} />
{img}
<p className={
"w-70 mono list-ship dib v-mid black white-d ml2 nowrap f8"
}>{name}</p>
'w-70 mono list-ship dib v-mid black white-d ml2 nowrap f8'
}
>{name}</p>
{actionElem}
</div>
);
}
}

View File

@ -1,9 +1,9 @@
import React, { Component } from "react";
import { Sigil } from "/components/lib/icons/sigil";
import React, { Component } from 'react';
import { Sigil } from '/components/lib/icons/sigil';
import {
ProfileOverlay,
OVERLAY_HEIGHT
} from "/components/lib/profile-overlay";
} from '/components/lib/profile-overlay';
export class OverlaySigil extends Component {
constructor() {
@ -49,7 +49,6 @@ export class OverlaySigil extends Component {
const parent = this.containerRef.current.offsetParent;
const { offsetTop } = this.containerRef.current;
let bottomSpace, topSpace;
if(navigator.userAgent.includes('Firefox')) {
@ -58,7 +57,6 @@ export class OverlaySigil extends Component {
} else {
topSpace = offsetTop + parent.scrollHeight - parent.clientHeight - parent.scrollTop;
bottomSpace = parent.clientHeight - topSpace - OVERLAY_HEIGHT;
}
this.setState({
topSpace,
@ -69,12 +67,22 @@ export class OverlaySigil extends Component {
render() {
const { props, state } = this;
return (
const img = (props.contact && (props.contact.avatar !== null))
? <img src={props.contact.avatar} height={24} width={24} className="dib" />
: <Sigil
ship={props.ship}
size={24}
color={props.color}
classes={props.sigilClass}
/>;
return (
<div
onClick={this.profileShow}
className={props.className + " pointer relative"}
className={props.className + ' pointer relative'}
ref={this.containerRef}
style={{ height: "24px" }}
style={{ height: '24px' }}
>
{state.profileClicked && (
<ProfileOverlay
@ -87,12 +95,7 @@ export class OverlaySigil extends Component {
onDismiss={this.profileHide}
/>
)}
<Sigil
ship={props.ship}
size={24}
color={props.color}
classes={props.sigilClass}
/>
{img}
</div>
);
}

View File

@ -1,7 +1,7 @@
import React, { Component } from "react";
import { Link } from "react-router-dom";
import { cite } from "/lib/util";
import { Sigil } from "/components/lib/icons/sigil";
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import { cite } from '/lib/util';
import { Sigil } from '/components/lib/icons/sigil';
export const OVERLAY_HEIGHT = 250;
@ -14,13 +14,13 @@ export class ProfileOverlay extends Component {
}
componentDidMount() {
document.addEventListener("mousedown", this.onDocumentClick);
document.addEventListener("touchstart", this.onDocumentClick);
document.addEventListener('mousedown', this.onDocumentClick);
document.addEventListener('touchstart', this.onDocumentClick);
}
componentWillUnmount() {
document.removeEventListener("mousedown", this.onDocumentClick);
document.removeEventListener("touchstart", this.onDocumentClick);
document.removeEventListener('mousedown', this.onDocumentClick);
document.removeEventListener('touchstart', this.onDocumentClick);
}
onDocumentClick(event) {
@ -38,21 +38,31 @@ export class ProfileOverlay extends Component {
let top, bottom;
if (topSpace < OVERLAY_HEIGHT / 2) {
top = `0px`;
top = '0px';
}
if (bottomSpace < OVERLAY_HEIGHT / 2) {
bottom = `0px`;
bottom = '0px';
}
if (!(top || bottom)) {
bottom = `-${Math.round(OVERLAY_HEIGHT / 2)}px`;
}
const containerStyle = { top, bottom, left: "100%" };
const containerStyle = { top, bottom, left: '100%' };
const isOwn = window.ship === ship;
const identityHref = group["group-path"].startsWith("/~/")
? "/~groups/me"
: `/~groups/view${group["group-path"]}/${window.ship}`;
const identityHref = group['group-path'].startsWith('/~/')
? '/~groups/me'
: `/~groups/view${group['group-path']}/${window.ship}`;
const img = (contact && (contact.avatar !== null))
? <img src={contact.avatar} height={160} width={160} className="brt2 dib" />
: <Sigil
ship={ship}
size={160}
color={color}
classes="brt2"
svgClass="brt2"
/>;
return (
<div
@ -60,14 +70,8 @@ export class ProfileOverlay extends Component {
style={containerStyle}
className="flex-col shadow-6 br2 bg-white bg-gray0-d inter absolute z-1 f9 lh-solid"
>
<div style={{ height: "160px" }}>
<Sigil
ship={ship}
size={160}
color={color}
classes="brt2"
svgClass="brt2"
/>
<div style={{ height: '160px', width: '160px' }}>
{img}
</div>
<div className="pv3 pl3 pr2">
{contact && contact.nickname && (

View File

@ -116,7 +116,7 @@ export class ContactCard extends Component {
if (
(state.avatarToSet === '') ||
(
!!props.contact.avatar &&
Boolean(props.contact.avatar) &&
'url' in props.contact.avatar &&
state.avatarToSet === props.contact.avatar.url
)
@ -129,7 +129,7 @@ export class ContactCard extends Component {
awaiting: true,
type: 'Saving to group'
}, (() => {
api.contactEdit(props.path, ship, {
api.contactEdit(props.path, ship, {
avatar: {
url: state.avatarToSet
}
@ -235,14 +235,6 @@ export class ContactCard extends Component {
}
break;
}
case 'removeAvatar': {
this.setState({ awaiting: true, type: 'Removing from group' }, (() => {
api.contactEdit(props.path, ship, { avatar: null }).then(() => {
this.setState({ awaiting: false });
});
}));
break;
}
case 'removeEmail': {
this.setState({ emailToSet: '', awaiting: true, type: 'Removing from group' }, (() => {
api.contactEdit(props.path, ship, { email: '' }).then(() => {
@ -404,9 +396,10 @@ export class ContactCard extends Component {
const avatar = (hasAvatar)
? <span>
<img className="dib h-auto"
<img className="dib h-auto"
width={128}
src={props.contact.avatar} />
src={props.contact.avatar}
/>
<EditElement
title="Avatar Image URL"
defaultValue={defaultValue.avatar}
@ -519,7 +512,7 @@ export class ContactCard extends Component {
const hexColor = uxToHex(currentColor);
const avatar =
('avatar' in props.contact && props.contact.avatar !== 'TODO') ?
('avatar' in props.contact && props.contact.avatar !== null) ?
<img className="dib h-auto" width={128} src={props.contact.avatar} /> :
<Sigil
ship={props.ship}

View File

@ -3,33 +3,39 @@ import { Route, Link } from 'react-router-dom';
import { Sigil } from '../lib/icons/sigil';
import { uxToHex, cite } from '../../lib/util';
export class ContactItem extends Component {
render() {
const { props } = this;
let selectedClass = (props.selected) ? "bg-gray4 bg-gray1-d" : "";
let hexColor = uxToHex(props.color);
let name = (props.nickname) ? props.nickname : cite(props.ship);
const selectedClass = (props.selected) ? 'bg-gray4 bg-gray1-d' : '';
const hexColor = uxToHex(props.color);
const name = (props.nickname) ? props.nickname : cite(props.ship);
const prefix = props.share ? 'share' : 'view';
const suffix = !props.share ? `/${props.ship}` : '';
const img = (props.avatar !== null)
? <img className="dib" src={props.avatar} height={32} width={32} />
: <Sigil
ship={props.ship}
color={'#' + hexColor}
size={32}
key={`${props.ship}.sidebar.${hexColor}`}
/>;
let prefix = props.share ? 'share' : 'view';
let suffix = !props.share ? `/${props.ship}` : '';
return (
<Link to={`/~groups/${prefix}` + props.path + suffix}>
<div className=
{"pl4 pt1 pb1 f9 flex justify-start content-center " + selectedClass}
{'pl4 pt1 pb1 f9 flex justify-start content-center ' + selectedClass}
>
<Sigil
ship={props.ship}
color={"#" + hexColor}
size={32}
key={`${props.ship}.sidebar.${hexColor}`} />
{img}
<p
className={
"f9 w-70 dib v-mid ml2 nowrap " +
((props.nickname) ? "" : "mono")}
'f9 w-70 dib v-mid ml2 nowrap ' +
((props.nickname) ? '' : 'mono')}
style={{ paddingTop: 6 }}
title={props.ship}>
title={props.ship}
>
{name}
</p>
</div>

View File

@ -1,5 +1,5 @@
import React, { Component } from 'react';
import { Route, Link } from 'react-router-dom';
import { Link } from 'react-router-dom';
import { ContactItem } from '/components/lib/contact-item';
import { ShareSheet } from '/components/lib/share-sheet';
import { Sigil } from '../lib/icons/sigil';
@ -11,29 +11,29 @@ export class ContactSidebar extends Component {
super(props);
this.state = {
awaiting: false
}
};
}
render() {
const { props } = this;
let group = new Set(Array.from(props.group));
let responsiveClasses =
props.activeDrawer === "contacts" ? "db" : "dn db-ns";
const group = new Set(Array.from(props.group));
const responsiveClasses =
props.activeDrawer === 'contacts' ? 'db' : 'dn db-ns';
let me = (window.ship in props.contacts)
const me = (window.ship in props.contacts)
? props.contacts[window.ship]
: (window.ship in props.defaultContacts)
? props.defaultContacts[window.ship]
: { color: '0x0', nickname: null };
: { color: '0x0', nickname: null, avatar: null };
let shareSheet =
const shareSheet =
!(window.ship in props.contacts) ?
( <ShareSheet
ship={window.ship}
nickname={me.nickname}
color={me.color}
path={props.path}
selected={props.path + "/" + window.ship === props.selectedContact}
selected={props.path + '/' + window.ship === props.selectedContact}
/>
) : (
<>
@ -41,27 +41,29 @@ export class ContactSidebar extends Component {
<ContactItem
ship={window.ship}
nickname={me.nickname}
avatar={me.avatar}
color={me.color}
path={props.path}
selected={props.path + "/" + window.ship === props.selectedContact}
selected={props.path + '/' + window.ship === props.selectedContact}
/>
</>
);
group.delete(window.ship);
let contactItems =
const contactItems =
Object.keys(props.contacts)
.filter(c => c !== window.ship)
.map((contact) => {
group.delete(contact);
let path = props.path + "/" + contact;
let obj = props.contacts[contact];
const path = props.path + '/' + contact;
const obj = props.contacts[contact];
return (
<ContactItem
key={contact}
ship={contact}
nickname={obj.nickname}
color={obj.color}
avatar={obj.avatar}
path={props.path}
selected={path === props.selectedContact}
share={false}
@ -69,62 +71,68 @@ export class ContactSidebar extends Component {
);
});
let adminOpt = (props.path.includes(`~${window.ship}/`))
? "dib" : "dn";
const adminOpt = (props.path.includes(`~${window.ship}/`))
? 'dib' : 'dn';
let groupItems =
const groupItems =
Array.from(group).map((member) => {
return (
<div
key={member}
className={"pl4 pt1 pb1 f9 flex justify-start content-center " +
"bg-white bg-gray0-d relative"}>
className={'pl4 pt1 pb1 f9 flex justify-start content-center ' +
'bg-white bg-gray0-d relative'}
>
<Sigil
ship={member}
color="#000000"
size={32}
classes="mix-blend-diff"
/>
/>
<p className="f9 w-70 dib v-mid ml2 nowrap mono truncate"
style={{ paddingTop: 6, color: '#aaaaaa' }}
title={member}>
title={member}
>
{cite(member)}
</p>
<p className={"v-mid f9 mh3 red2 pointer " + adminOpt}
style={{paddingTop: 6}}
<p className={'v-mid f9 mh3 red2 pointer ' + adminOpt}
style={{ paddingTop: 6 }}
onClick={() => {
this.setState({awaiting: true}, (() => {
this.setState({ awaiting: true }, (() => {
props.api.groupRemove(props.path, [`~${member}`])
.then(() => {
this.setState({awaiting: false})
})
}))
}}>
this.setState({ awaiting: false });
});
}));
}}
>
Remove
</p>
</div>
);
});
let detailHref = `/~groups/detail${props.path}`
const detailHref = `/~groups/detail${props.path}`;
return (
<div className={"bn br-m br-l br-xl b--gray4 b--gray1-d lh-copy h-100 " +
"flex-basis-100-s flex-basis-30-ns mw5-m mw5-l mw5-xl relative " +
"overflow-hidden flex-shrink-0 " + responsiveClasses}>
<div className={'bn br-m br-l br-xl b--gray4 b--gray1-d lh-copy h-100 ' +
'flex-basis-100-s flex-basis-30-ns mw5-m mw5-l mw5-xl relative ' +
'overflow-hidden flex-shrink-0 ' + responsiveClasses}
>
<div className="pt3 pb5 pl3 f8 db dn-m dn-l dn-xl">
<Link to="/~groups/">{"⟵ All Groups"}</Link>
<Link to="/~groups/">{'⟵ All Groups'}</Link>
</div>
<div className="overflow-auto h-100">
<Link
to={"/~groups/add" + props.path}
to={'/~groups/add' + props.path}
className={((props.path.includes(window.ship))
? "dib"
: "dn")}>
? 'dib'
: 'dn')}
>
<p className="f9 pl4 pt0 pt4-m pt4-l pt4-xl green2 bn">Add to Group</p>
</Link>
<Link to={detailHref}
className="dib dn-m dn-l dn-xl f9 pl4 pt0 pt4-m pt4-l pt4-xl gray2 bn">Channels</Link>
className="dib dn-m dn-l dn-xl f9 pl4 pt0 pt4-m pt4-l pt4-xl gray2 bn"
>Channels</Link>
{shareSheet}
<h2 className="f9 pt4 pr4 pb2 pl4 gray2 c-default">Members</h2>
{contactItems}

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react'
import React, { Component } from 'react';
import { Sigil } from './icons/sigil';
import { cite } from '../../lib/util';
import moment from 'moment';
@ -13,7 +13,7 @@ export class CommentItem extends Component {
componentDidMount() {
this.updateTimeSinceNewestMessageInterval = setInterval( () => {
this.setState({timeSinceComment: this.getTimeSinceComment()});
this.setState({ timeSinceComment: this.getTimeSinceComment() });
}, 60000);
}
@ -25,30 +25,35 @@ export class CommentItem extends Component {
}
getTimeSinceComment() {
return !!this.props.time ?
return this.props.time ?
moment.unix(this.props.time / 1000).from(moment.utc())
: '';
}
render() {
let props = this.props;
const props = this.props;
let member = this.props.member || false;
const member = props.member || false;
let pending = !!this.props.pending ? "o-60" : "";
const pending = props.pending ? 'o-60' : '';
const img = (props.avatar)
? <img src={props.avatar} height={36} width={36} className="dib" />
: <Sigil
ship={'~' + props.ship}
size={36}
color={'#' + props.color}
classes={(member ? 'mix-blend-diff' : '')}
/>;
return (
<div className={"w-100 pv3 " + pending}>
<div className={'w-100 pv3 ' + pending}>
<div className="flex bg-white bg-gray0-d">
<Sigil
ship={"~" + props.ship}
size={36}
color={"#" + props.color}
classes={(member ? "mix-blend-diff" : "")}
/>
{img}
<p className="gray2 f9 flex items-center ml2">
<span className={"black white-d " + props.nameClass}
title={props.ship}>
<span className={'black white-d ' + props.nameClass}
title={props.ship}
>
{props.nickname ? props.nickname : cite(props.ship)}
</span>
<span className="ml2">
@ -58,8 +63,8 @@ export class CommentItem extends Component {
</div>
<p className="inter f8 pv3 white-d">{props.content}</p>
</div>
)
);
}
}
export default CommentItem
export default CommentItem;

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react'
import React, { Component } from 'react';
import { CommentItem } from './comment-item';
import { CommentsPagination } from './comments-pagination';
@ -12,12 +12,12 @@ export class Comments extends Component {
}
componentDidMount() {
let page = this.props.commentPage;
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.setState({ requested: this.props.commentPage });
api.getCommentsPage(
this.props.resourcePath,
this.props.url,
@ -26,35 +26,34 @@ export class Comments extends Component {
}
render() {
let props = this.props;
const props = this.props;
let page = props.commentPage;
const page = props.commentPage;
let commentsObj = !!props.comments
const commentsObj = props.comments
? props.comments
: {};
let commentsPage = !!commentsObj[page]
const commentsPage = commentsObj[page]
? commentsObj[page]
: {};
let total = !!props.comments
const total = props.comments
? props.comments.totalPages
: 1;
let commentsList = Object.keys(commentsPage)
const commentsList = Object.keys(commentsPage)
.map((entry) => {
const commentObj = commentsPage[entry];
const { ship, time, udon } = commentObj;
let commentObj = commentsPage[entry]
let { ship, time, udon } = commentObj;
let contacts = !!props.contacts
const contacts = props.contacts
? props.contacts
: {};
const {nickname, color, member} = getContactDetails(contacts[ship]);
const { nickname, color, member, avatar } = getContactDetails(contacts[ship]);
let nameClass = nickname ? "inter" : "mono";
const nameClass = nickname ? 'inter' : 'mono';
return(
<CommentItem
@ -65,10 +64,11 @@ export class Comments extends Component {
nickname={nickname}
nameClass={nameClass}
color={color}
avatar={avatar}
member={member}
/>
)
})
);
});
return (
<div>
{commentsList}
@ -80,10 +80,11 @@ export class Comments extends Component {
linkIndex={props.linkIndex}
url={props.url}
commentPage={props.commentPage}
total={total}/>
total={total}
/>
</div>
)
);
}
}
export default Comments;
export default Comments;

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react'
import React, { Component } from 'react';
import moment from 'moment';
import { Sigil } from '/components/lib/icons/sigil';
@ -16,7 +16,7 @@ export class LinkItem extends Component {
componentDidMount() {
this.updateTimeSinceNewestMessageInterval = setInterval( () => {
this.setState({timeSinceLinkPost: this.getTimeSinceLinkPost()});
this.setState({ timeSinceLinkPost: this.getTimeSinceLinkPost() });
}, 60000);
}
@ -28,7 +28,7 @@ export class LinkItem extends Component {
}
getTimeSinceLinkPost() {
return !!this.props.timestamp ?
return this.props.timestamp ?
moment.unix(this.props.timestamp / 1000).from(moment.utc())
: '';
}
@ -38,61 +38,68 @@ export class LinkItem extends Component {
}
render() {
const props = this.props;
let props = this.props;
const mono = (props.nickname) ? 'inter white-d' : 'mono white-d';
let mono = (props.nickname) ? "inter white-d" : "mono white-d";
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}/);
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";
? 'gray2'
: 'green2 pointer';
const seenAction = props.seen
? ()=>{}
: this.markPostAsSeen
? () => {}
: this.markPostAsSeen;
if (hostname) {
hostname = hostname[4];
}
let comments = props.comments + " comment" + ((props.comments === 1) ? "" : "s");
const comments = props.comments + ' comment' + ((props.comments === 1) ? '' : 's');
let member = this.props.member || false;
const member = this.props.member || false;
const img = (this.props.avatar)
? <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">
<Sigil
ship={"~" + props.ship}
size={38}
color={"#" + props.color}
classes={(member ? "mix-blend-diff" : "")}
/>
{img}
<div className="flex flex-column ml2 flex-auto">
<a href={props.url}
className="w-100 flex"
target="_blank"
onClick={this.markPostAsSeen}>
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 pt1">
<span className={"f9 pr2 dib " + mono}
title={props.ship}>
<span className={'f9 pr2 dib ' + mono}
title={props.ship}
>
{(props.nickname)
? props.nickname
: cite(props.ship)}
</span>
<span
className={seenState + " f9 inter pr3 dib"}
onClick={this.markPostAsSeen}>
className={seenState + ' f9 inter pr3 dib'}
onClick={this.markPostAsSeen}
>
{this.state.timeSinceLinkPost}
</span>
<Link to=
{makeRoutePath(props.resourcePath, props.popout, props.page, props.url, props.linkIndex)}
onClick={this.markPostAsSeen}>
onClick={this.markPostAsSeen}
>
<span className="f9 inter gray2 dib">
{comments}
</span>
@ -100,8 +107,8 @@ export class LinkItem extends Component {
</div>
</div>
</div>
)
);
}
}
export default LinkItem
export default LinkItem;

View File

@ -1,14 +1,10 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { Sigil } from '/components/lib/icons/sigil';
import { uxToHex, cite } from '/lib/util';
export class MemberElement extends Component {
onRemove() {
const { props } = this;
//TODO don't really need to use link-view here, but should we anyway?
api.groups.remove(props.groupPath, [`~${props.ship}`]);
}
@ -25,7 +21,8 @@ export class MemberElement extends Component {
} else if (props.amOwner && window.ship !== props.ship) {
actionElem = (
<a onClick={this.onRemove.bind(this)}
className="w-20 dib list-ship black white-d f8 pointer">
className="w-20 dib list-ship black white-d f8 pointer"
>
Ban
</a>
);
@ -35,16 +32,21 @@ export class MemberElement extends Component {
);
}
let name = !!props.contact
const name = props.contact
? `${props.contact.nickname} (${cite(props.ship)})`
: `${cite(props.ship)}`;
let color = !!props.contact ? uxToHex(props.contact.color) : '000000';
const color = props.contact ? uxToHex(props.contact.color) : '000000';
const img = props.contact.avatar
? <img src={props.contact.avatar} height={32} width={32} className="dib" />
: <Sigil ship={props.ship} size={32} color={`#${color}`} />;
return (
<div className="flex mb2">
<Sigil ship={props.ship} size={32} color={`#${color}`} />
<p className={"w-70 mono list-ship dib v-mid black white-d ml2 nowrap f8"}
title={props.ship}>
{img}
<p className={'w-70 mono list-ship dib v-mid black white-d ml2 nowrap f8'}
title={props.ship}
>
{name}
</p>
{actionElem}

View File

@ -1,9 +1,9 @@
import React, { Component } from 'react'
import React, { Component } from 'react';
import { LinksTabBar } from './lib/links-tabbar';
import { LinkPreview } from './lib/link-detail-preview';
import { SidebarSwitcher } from '/components/lib/icons/icon-sidebar-switch.js';
import { api } from '../api';
import { Route, Link } from 'react-router-dom';
import { Link } from 'react-router-dom';
import { Comments } from './lib/comments';
import { Spinner } from './lib/icons/icon-spinner';
import { LoadingScreen } from './loading';
@ -14,7 +14,7 @@ export class LinkDetail extends Component {
constructor(props) {
super(props);
this.state = {
comment: "",
comment: '',
data: props.data,
commentFocus: false,
pending: new Set(),
@ -43,14 +43,14 @@ export class LinkDetail extends Component {
if (this.props.url !== prevProps.url) {
this.updateData(this.props.data);
}
if (prevProps.comments && prevProps.comments["0"] &&
this.props.comments && this.props.comments["0"]) {
let prevFirstComment = prevProps.comments["0"][0];
let thisFirstComment = this.props.comments["0"][0];
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)) {
let pending = this.state.pending;
const pending = this.state.pending;
pending.delete(thisFirstComment.udon);
this.setState({
pending: pending
@ -61,9 +61,9 @@ export class LinkDetail extends Component {
}
onClickPost() {
let url = this.props.url || "";
const url = this.props.url || '';
let pending = this.state.pending;
const pending = this.state.pending;
pending.add(this.state.comment);
this.setState({ pending: pending, disabled: true });
@ -72,9 +72,8 @@ export class LinkDetail extends Component {
url,
this.state.comment
).then(() => {
this.setState({ comment: "", disabled: false });
this.setState({ comment: '', disabled: false });
});
}
setComment(event) {
@ -82,37 +81,37 @@ export class LinkDetail extends Component {
}
render() {
let props = this.props;
const props = this.props;
const data = this.state.data || props.data;
if (!data.ship) {
return <LoadingScreen/>;
return <LoadingScreen />;
}
let ship = data.ship || "zod";
let title = data.title || "";
let url = data.url || "";
const ship = data.ship || 'zod';
const title = data.title || '';
const url = data.url || '';
const commentCount = props.comments
? props.comments.totalItems
: data.commentCount || 0;
let comments = commentCount + " comment" + (commentCount === 1 ? "" : "s");
const comments = commentCount + ' comment' + (commentCount === 1 ? '' : 's');
const { nickname } = getContactDetails(props.contacts[ship]);
let activeClasses = this.state.comment
? "black white-d pointer"
: "gray2 b--gray2";
const activeClasses = this.state.comment
? 'black white-d pointer'
: 'gray2 b--gray2';
let focus = (this.state.commentFocus)
? "b--black b--white-d"
: "b--gray4 b--gray2-d";
const focus = (this.state.commentFocus)
? 'b--black b--white-d'
: 'b--gray4 b--gray2-d';
let our = getContactDetails(props.contacts[window.ship]);
const our = getContactDetails(props.contacts[window.ship]);
let pendingArray = Array.from(this.state.pending).map((com, i) => {
const pendingArray = Array.from(this.state.pending).map((com, i) => {
return(
<CommentItem
key={i}
@ -124,23 +123,25 @@ export class LinkDetail extends Component {
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 }}>
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}
/>
<Link
className="dib f9 fw4 pt2 gray2 lh-solid"
to={makeRoutePath(props.resourcePath, props.popout, props.page)}>
to={makeRoutePath(props.resourcePath, props.popout, props.page)}
>
{`<- ${props.resource.metadata.title}`}
</Link>
<LinksTabBar {...props} popout={props.popout} resourcePath={props.resourcePath} />
@ -159,19 +160,19 @@ export class LinkDetail extends Component {
time={this.state.data.time}
/>
<div className="relative">
<div className={"relative ba br1 mt6 mb6 " + focus}>
<div className={'relative ba br1 mt6 mb6 ' + focus}>
<textarea
className="w-100 bg-gray0-d white-d f8 pa2 pr8"
style={{
resize: "none",
resize: 'none',
height: 75
}}
placeholder="Leave a comment on this link"
onChange={this.setComment}
onKeyDown={e => {
onKeyDown={(e) => {
if (
(e.getModifierState("Control") || e.metaKey) &&
e.key === "Enter"
(e.getModifierState('Control') || e.metaKey) &&
e.key === 'Enter'
) {
this.onClickPost();
}
@ -182,14 +183,15 @@ export class LinkDetail extends Component {
/>
<button
className={
"f8 bg-gray0-d ml2 absolute " + activeClasses
'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>

View File

@ -1,16 +1,15 @@
import React, { Component } from 'react'
import React, { Component } from 'react';
import { LoadingScreen } from './loading';
import { MessageScreen } from '/components/lib/message-screen';
import { LinksTabBar } from './lib/links-tabbar';
import { SidebarSwitcher } from '/components/lib/icons/icon-sidebar-switch.js';
import { Route, Link } from "react-router-dom";
import { Route, Link } from 'react-router-dom';
import { LinkItem } from '/components/lib/link-item.js';
import { LinkSubmit } from '/components/lib/link-submit.js';
import { Pagination } from '/components/lib/pagination.js';
import { makeRoutePath, getContactDetails } from '../lib/util';
//TODO Avatar support once it's in
export class Links extends Component {
constructor(props) {
super(props);
@ -38,44 +37,44 @@ export class Links extends Component {
}
render() {
let props = this.props;
const props = this.props;
if (!props.resource.metadata.title) {
return <LoadingScreen/>;
return <LoadingScreen />;
}
let linkPage = props.page;
const linkPage = props.page;
let links = !!props.links[linkPage]
const links = props.links[linkPage]
? props.links[linkPage]
: {};
let currentPage = !!props.page
const currentPage = props.page
? Number(props.page)
: 0;
let totalPages = !!props.links
const totalPages = props.links
? Number(props.links.totalPages)
: 1;
let LinkList = (<LoadingScreen/>);
let LinkList = (<LoadingScreen />);
if (props.links && props.links.totalItems === 0) {
LinkList = (
<MessageScreen text="Start by posting a link to this collection."/>
<MessageScreen text="Start by posting a link to this collection." />
);
} else if (Object.keys(links).length > 0) {
LinkList = Object.keys(links)
.map((linkIndex) => {
let linksObj = props.links[linkPage];
let { title, url, time, ship } = linksObj[linkIndex];
const linksObj = props.links[linkPage];
const { title, url, time, ship } = linksObj[linkIndex];
const seen = props.seen[url];
let members = {};
const members = {};
const commentCount = props.comments[url]
? props.comments[url].totalItems
: linksObj[linkIndex].commentCount || 0;
const {nickname, color, member} = getContactDetails(props.contacts[ship]);
const { nickname, color, member, avatar } = getContactDetails(props.contacts[ship]);
return (
<LinkItem
@ -89,33 +88,38 @@ export class Links extends Component {
nickname={nickname}
ship={ship}
color={color}
avatar={avatar}
member={member}
comments={commentCount}
resourcePath={props.resourcePath}
popout={props.popout}
/>
)
);
});
}
return (
<div
className="h-100 w-100 overflow-hidden flex flex-column">
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>
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 }}>
style={{ height: 48 }}
>
<SidebarSwitcher
sidebarShown={props.sidebarShown}
popout={props.popout}/>
popout={props.popout}
/>
<Link to={makeRoutePath(props.resourcePath, props.popout, props.page)} className="pt2">
<h2 className={`dib f9 fw4 lh-solid v-top`}>
<h2 className={'dib f9 fw4 lh-solid v-top'}>
{props.resource.metadata.title}
</h2>
</Link>
@ -123,12 +127,13 @@ export class Links extends Component {
{...props}
popout={props.popout}
page={props.page}
resourcePath={props.resourcePath}/>
resourcePath={props.resourcePath}
/>
</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}/>
<LinkSubmit resourcePath={props.resourcePath} />
</div>
<div className="pb4">
{LinkList}
@ -144,8 +149,8 @@ export class Links extends Component {
</div>
</div>
</div>
)
);
}
}
export default Links;
export default Links;

View File

@ -1,6 +1,3 @@
import _ from 'lodash';
import classnames from 'classnames';
export function makeRoutePath(
resource, popout = false, page = 0, url = null, index = 0, compage = 0
) {
@ -19,7 +16,8 @@ export function makeRoutePath(
}
export function amOwnerOfGroup(groupPath) {
if (!groupPath) return false;
if (!groupPath)
return false;
const groupOwner = /(\/~)?\/~([a-z-]{3,})\/.*/.exec(groupPath)[2];
return (window.ship === groupOwner);
}
@ -28,12 +26,13 @@ export function getContactDetails(contact) {
const member = !contact;
contact = contact || {
'nickname': '',
'avatar': 'TODO',
'avatar': null,
'color': '0x0'
};
const nickname = contact.nickname || '';
const color = uxToHex(contact.color || '0x0');
return {nickname, color, member};
const avatar = contact.avatar || null;
return { nickname, color, member, avatar };
}
// encodes string into base64url,
@ -55,8 +54,8 @@ export function base64urlDecode(string) {
}
export function isPatTa(str) {
const r = /^[a-z,0-9,\-,\.,_,~]+$/.exec(str)
return !!r;
const r = /^[a-z,0-9,\-,\.,_,~]+$/.exec(str);
return Boolean(r);
}
// encode the string into @ta-safe format, using logic from +wood.
@ -86,7 +85,7 @@ export function stringToTa(string) {
) {
add = char;
} else {
//TODO behavior for unicode doesn't match +wood's,
// TODO behavior for unicode doesn't match +wood's,
// but we can probably get away with that for now.
add = '~' + charCode.toString(16) + '.';
}
@ -103,13 +102,13 @@ export function stringToTa(string) {
(javascript Date object)
*/
export function daToDate(st) {
var dub = function(n) {
return parseInt(n) < 10 ? "0" + parseInt(n) : n.toString();
const dub = function(n) {
return parseInt(n) < 10 ? '0' + parseInt(n) : n.toString();
};
var da = st.split('..');
var bigEnd = da[0].split('.');
var lilEnd = da[1].split('.');
var ds = `${bigEnd[0].slice(1)}-${dub(bigEnd[1])}-${dub(bigEnd[2])}T${dub(lilEnd[0])}:${dub(lilEnd[1])}:${dub(lilEnd[2])}Z`;
const da = st.split('..');
const bigEnd = da[0].split('.');
const lilEnd = da[1].split('.');
const ds = `${bigEnd[0].slice(1)}-${dub(bigEnd[1])}-${dub(bigEnd[2])}T${dub(lilEnd[0])}:${dub(lilEnd[1])}:${dub(lilEnd[2])}Z`;
return new Date(ds);
}
@ -121,8 +120,8 @@ export function daToDate(st) {
*/
export function dateToDa(d, mil) {
  var fil = function(n) {
    return n >= 10 ? n : "0" + n;
  const fil = function(n) {
    return n >= 10 ? n : '0' + n;
  };
  return (
    `~${d.getUTCFullYear()}.` +
@ -131,7 +130,7 @@ export function dateToDa(d, mil) {
    `${fil(d.getUTCHours())}.` +
    `${fil(d.getUTCMinutes())}.` +
    `${fil(d.getUTCSeconds())}` +
`${mil ? "..0000" : ""}`
`${mil ? '..0000' : ''}`
  );
}
@ -149,41 +148,41 @@ export function uxToHex(ux) {
// trim patps to match dojo, chat-cli
export function cite(ship) {
let patp = ship, shortened = "";
if (patp.startsWith("~")) {
let patp = ship, shortened = '';
if (patp.startsWith('~')) {
patp = patp.substr(1);
}
// comet
if (patp.length === 56) {
shortened = "~" + patp.slice(0, 6) + "_" + patp.slice(50, 56);
shortened = '~' + patp.slice(0, 6) + '_' + patp.slice(50, 56);
return shortened;
}
// moon
if (patp.length === 27) {
shortened = "~" + patp.slice(14, 20) + "^" + patp.slice(21, 27);
shortened = '~' + patp.slice(14, 20) + '^' + patp.slice(21, 27);
return shortened;
}
return `~${patp}`;
}
export function alphabetiseAssociations(associations) {
let result = {};
const result = {};
Object.keys(associations).sort((a, b) => {
let aName = a.substr(1);
let bName = b.substr(1);
if (associations[a].metadata && associations[a].metadata.title) {
aName = associations[a].metadata.title !== ""
aName = associations[a].metadata.title !== ''
? associations[a].metadata.title
: a.substr(1);
}
if (associations[b].metadata && associations[b].metadata.title) {
bName = associations[b].metadata.title !== ""
bName = associations[b].metadata.title !== ''
? associations[b].metadata.title
: b.substr(1);
}
return aName.toLowerCase().localeCompare(bName.toLowerCase());
}).map((each) => {
result[each] = associations[each];
})
});
return result;
}
}

View File

@ -3,10 +3,9 @@ import moment from 'moment';
import { Sigil } from './icons/sigil';
import { CommentInput } from './comment-input';
import { uxToHex, cite } from '../../lib/util';
import { Spinner } from './icons/icon-spinner';
export class CommentItem extends Component {
constructor(props){
constructor(props) {
super(props);
this.state = {
@ -20,7 +19,7 @@ export class CommentItem extends Component {
past: function(input) {
return input === 'just now'
? input
: input + ' ago'
: input + ' ago';
},
s : 'just now',
future : 'in %s',
@ -33,15 +32,14 @@ export class CommentItem extends Component {
M : '1 month',
MM : '%d months',
y : '1 year',
yy : '%d years',
yy : '%d years'
}
});
}
commentEdit() {
let commentPath = Object.keys(this.props.comment)[0];
let commentBody = this.props.comment[commentPath].content;
const commentPath = Object.keys(this.props.comment)[0];
const commentBody = this.props.comment[commentPath].content;
this.setState({ commentBody });
this.props.onEdit();
}
@ -53,7 +51,7 @@ export class CommentItem extends Component {
commentChange(e) {
this.setState({
commentBody: e.target.value
})
});
}
onUpdate() {
@ -61,28 +59,39 @@ export class CommentItem extends Component {
}
render() {
let pending = !!this.props.pending ? "o-60" : "";
let commentData = this.props.comment[Object.keys(this.props.comment)[0]];
let content = commentData.content.split("\n").map((line, i)=> {
const pending = this.props.pending ? 'o-60' : '';
const commentData = this.props.comment[Object.keys(this.props.comment)[0]];
const content = commentData.content.split('\n').map((line, i) => {
return (
<p className="mb2" key={i}>{line}</p>
)
);
});
let date = moment(commentData["date-created"]).fromNow();
const date = moment(commentData['date-created']).fromNow();
let contact = !!(commentData.author.substr(1) in this.props.contacts)
const contact = commentData.author.substr(1) in this.props.contacts
? this.props.contacts[commentData.author.substr(1)] : false;
let name = commentData.author;
let color = "#000000";
let classes = "mix-blend-diff";
let color = '#000000';
let classes = 'mix-blend-diff';
let avatar = null;
if (contact) {
name = (contact.nickname.length > 0)
? contact.nickname : commentData.author;
color = `#${uxToHex(contact.color)}`;
classes = "";
classes = '';
avatar = contact.avatar;
}
const img = (avatar !== null)
? <img src={avatar} height={24} width={24} className="dib" />
: <Sigil
ship={commentData.author}
size={24}
color={color}
classes={classes}
/>;
if (name === commentData.author) {
name = cite(commentData.author);
}
@ -93,17 +102,13 @@ export class CommentItem extends Component {
|| window.ship !== commentData.author.slice(1);
return (
<div className={"mb8 " + pending}>
<div className={'mb8 ' + pending}>
<div className="flex mv3 bg-white bg-gray0-d">
<Sigil
ship={commentData.author}
size={24}
color={color}
classes={classes}
/>
<div className={"f9 mh2 pt1 " +
(contact.nickname ? null : "mono")}
title={commentData.author}>
{img}
<div className={'f9 mh2 pt1 ' +
(contact.nickname ? null : 'mono')}
title={commentData.author}
>
{name}
</div>
<div className="f9 gray3 pt1">{date}</div>
@ -121,11 +126,14 @@ export class CommentItem extends Component {
<div className="f8 lh-solid mb2">
{ !editing && content }
{ editing && (
<CommentInput style={{resize:'vertical'}}
ref={(el) => {this.focusTextArea(el)}}
<CommentInput style={{ resize:'vertical' }}
ref={(el) => {
this.focusTextArea(el);
}}
onChange={this.commentChange}
value={this.state.commentBody}
onSubmit={this.onUpdate.bind(this)}>
onSubmit={this.onUpdate.bind(this)}
>
</CommentInput>
)}
</div>
@ -139,10 +147,9 @@ export class CommentItem extends Component {
</div>
</div>
)}
</div>
)
);
}
}
export default CommentItem
export default CommentItem;