chat: add group and peer search, new.js rework

This commit is contained in:
Matilde Park 2020-01-30 18:40:36 -05:00
parent a9434743a0
commit 761d71cfee
9 changed files with 395 additions and 106 deletions

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 951 B

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -24,14 +24,6 @@ textarea, input, button {
background-color: #fff;
}
.dropdown::after {
content: "⌃";
transform: rotate(180deg);
position: absolute;
right: 12px;
top: 8px;
}
a {
color: #000;
font-weight: 400;
@ -104,6 +96,14 @@ h2 {
word-break: break-all;
}
.focus-b--black:focus {
border-color: #000;
}
.mix-blend-diff {
mix-blend-mode: difference;
}
/* embeds */
.embed-container {
position: relative;
@ -119,6 +119,33 @@ h2 {
height: 100%;
}
.mh-16 {
max-height: 16rem;
}
/* security checkbox */
.security::after {
content: "";
height: 12px;
width: 12px;
background: white;
position: absolute;
top: 2px;
left: 2px;
border-radius: 100%;
}
.security.checked::after {
content: "";
height: 12px;
width: 12px;
background: white;
position: absolute;
top: 2px;
left: 14px;
border-radius: 100%;
}
/* responsive */
@media all and (max-width: 34.375em) {
@ -227,7 +254,13 @@ h2 {
.o-80-d {
opacity: .8;
}
.focus-b--white-d:focus {
border-color: #fff;
}
a {
color: #fff;
}
.hover-black-d:hover {
color: #000;
}
}

View File

@ -6,6 +6,8 @@ export class Sigil extends Component {
render() {
const { props } = this;
let classes = props.classes || "";
if (props.ship.length > 14) {
return (
<div className="bg-black dib" style={{width: props.size, height: props.size}}>
@ -13,7 +15,7 @@ export class Sigil extends Component {
);
} else {
return (
<div className="dib" style={{ flexBasis: 32, backgroundColor: props.color }}>
<div className={"dib " + classes} style={{ flexBasis: 32, backgroundColor: props.color }}>
{sigil({
patp: props.ship,
renderer: reactRenderer,

View File

@ -0,0 +1,274 @@
import React, { Component } from 'react';
import urbitOb from "urbit-ob";
import { Sigil } from "../lib/icons/sigil";
export class InviteSearch extends Component {
constructor(props) {
super(props);
this.state = {
groups: [],
peers: [],
searchValue: "",
searchResults: {
groups: [],
ships: []
},
inviteError: false
}
this.search = this.search.bind(this);
}
componentDidMount() {
this.peerUpdate();
}
componentDidUpdate(prevProps) {
if (prevProps !== this.props) {
this.peerUpdate();
}
}
peerUpdate() {
let groups = Array.from(Object.keys(this.props.groups));
groups = groups.filter(e => !e.startsWith("/~/"));
let peers = [],
peerSet = new Set();
Object.keys(this.props.groups).map(group => {
peerSet.add(...this.props.groups[group]);
});
peers = Array.from(peerSet);
this.setState({ groups: groups, peers: peers });
}
search(event) {
let searchTerm = event.target.value.toLowerCase().replace("~", "");
this.setState({searchValue: event.target.value});
if (searchTerm.length < 2) {
this.setState({searchResults: { groups: [], ships: [] }})
}
if (searchTerm.length > 2) {
if (this.state.inviteError === true) {
this.setState({inviteError: false});
}
let groupMatches = this.state.groups.filter(e => {
return e.includes(searchTerm);
});
let shipMatches = this.state.peers.filter(e => {
return e.includes(searchTerm) && !this.props.invites.ships.includes(e);
});
this.setState({
searchResults: { groups: groupMatches, ships: shipMatches }
});
}
if (event.target.value.includes(",")) {
let isValid = true;
let addedShip = searchTerm.replace(",", "").trim();
if (!urbitOb.isValidPatp("~" + addedShip)) {
isValid = false;
}
if (!isValid) {
this.setState({ inviteError: true, searchValue: "" });
} else if (isValid) {
this.setInvite("addShip", addedShip);
this.setState({searchValue: ""});
}
}
}
setInvite(type, value) {
let { groups, ships } = this.props.invites;
this.setState(
{searchValue: "",
searchResults: {groups: [], ships: []}}
);
switch(type) {
case "deleteGroup":
this.props.setInvite({groups: [], ships: ships});
break;
case "deleteShip":
ships = ships.filter(e => {
return e !== value;
});
this.props.setInvite({groups: groups, ships: ships});
break;
case "addGroup":
this.props.setInvite({groups: [value], ships: []});
break;
case "addShip":
ships.push(value);
if (groups.length > 0) {
return false;
}
this.props.setInvite({groups: groups, ships: ships});
}
}
render() {
const { props, state } = this;
let searchDisabled = false;
if (props.invites.groups) {
if (props.invites.groups.length > 0) {
searchDisabled = true;
}
}
let participants = <div/>
let searchResults = <div/>
let invErrElem = <span />;
if (state.inviteError) {
invErrElem = (
<span className="f9 inter red2 db pt2">
Invited ships must be validly formatted ship names.
</span>
);
}
if ((state.searchResults.groups.length > 0)
|| (state.searchResults.ships.length > 0)) {
let groupHeader = (state.searchResults.groups.length > 0)
? <p className="f9 gray2 ph3">Groups</p> : "";
let shipHeader = (state.searchResults.ships.length > 0)
? <p className="f9 gray2 pv2 ph3">Ships</p> : "";
let groupResults = state.searchResults.groups.map(group => {
return (
<li
key={group}
className={
"list mono white-d f8 pv2 ph3 pointer" +
" hover-bg-gray4 hover-black-d"}
onClick={e => this.setInvite("addGroup", group)}>
{group}
</li>
);
})
let shipResults = state.searchResults.ships.map(ship => {
return (
<li
key={ship}
className={
"list mono white-d f8 pv1 ph3 pointer" +
" hover-bg-gray4 hover-black-d"
}
onClick={e => this.setInvite("addShip", ship)}>
<Sigil
ship={"~" + ship}
size={24}
color="#000000"
classes="mix-blend-diff v-mid"
/>
<span className="v-mid ml2">{"~" + ship}</span>
</li>
);
})
searchResults =
<div className={"absolute bg-white bg-gray0-d white-d" +
" pv3 z-1 w-100 mt1 ba b--white-d overflow-y-scroll mh-16"}>
{groupHeader}
{groupResults}
{shipHeader}
{shipResults}
</div>
}
let groupInvites = props.invites.groups || [];
let shipInvites = props.invites.ships || [];
if (groupInvites.length > 0 || shipInvites.length > 0) {
let groups = groupInvites.map(group => {
return (
<span
key={group}
className={
"f9 mono black pa2 bg-gray5 bg-gray1-d" +
" ba b--gray4 b--gray2-d white-d dib mr2 mt2"}>
{group}
<span className="white-d ml3 mono pointer"
onClick={e => this.setInvite("deleteGroup", group)}>
x
</span>
</span>
);
});
let ships = shipInvites.map(ship => {
return (
<span
key={ship}
className={"f9 mono black pa2 bg-gray5 bg-gray1-d" +
" ba b--gray4 b--gray2-d white-d dib mr2 mt2"}>
{"~" + ship}
<span className="white-d ml3 mono pointer"
onClick={e => this.setInvite("deleteShip", ship)}>x</span>
</span>
);
});
participants = (
<div
className={
"f9 gray2 bb bl br b--gray3 b--gray2-d bg-gray0-d " +
"white-d pa3 db w-100 inter"
}>
<span className="db gray2">Participants</span>
{groups} {ships}
</div>
);
}
return (
<div className="relative">
<img
src="/~chat/img/search.png"
className="absolute invert-d"
style={{
height: 16,
width: 16,
top: 14,
left: 12
}}
/>
<textarea
ref={e => {
this.textarea = e;
}}
className={"f7 ba b--gray3 b--gray2-d bg-gray0-d white-d pa3 w-100"
+ " db focus-b--black focus-b--white-d"}
placeholder="Search for ships or existing groups"
disabled={searchDisabled}
rows={1}
spellCheck={false}
style={{
resize: "none",
paddingLeft: 36
}}
onKeyPress={e => {
if (e.key === "Enter")
e.preventDefault()}}
onChange={this.search}
value={state.searchValue}
/>
{searchResults}
{participants}
{invErrElem}
</div>
);
}
}
export default InviteSearch;

View File

@ -1,5 +1,6 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { InviteSearch } from './lib/invite-search';
import { Route, Link } from 'react-router-dom';
import { uuid, isPatTa, deSig } from '/lib/util';
import urbitOb from 'urbit-ob';
@ -11,18 +12,20 @@ export class NewScreen extends Component {
this.state = {
idName: '',
invites: '',
security: 'village',
securityDescription: 'Invite-only chat. Default membership administration.',
invites: {
groups: [],
ships: []
},
security: 'channel',
idError: false,
inviteError: false,
allowHistory: true
};
this.idChange = this.idChange.bind(this);
this.invChange = this.invChange.bind(this);
this.securityChange = this.securityChange.bind(this);
this.allowHistoryChange = this.allowHistoryChange.bind(this);
this.setInvite = this.setInvite.bind(this);
}
componentDidUpdate(prevProps, prevState) {
@ -34,27 +37,6 @@ export class NewScreen extends Component {
props.history.push('/~chat/room' + station);
}
}
if (prevState.security !== this.state.security) {
let securityText = '';
switch (this.state.security) {
case 'village':
securityText = 'Invite-only chat. Default membership administration.';
break;
case 'channel':
securityText = 'Completely public chat. Default membership administration.';
break;
case 'journal':
securityText = 'Similar to a blog. Publicly readable/subscribable, invited members can write to journal.'
break;
// case 'mailbox':
// securityText = 'Similar to email. Anyone can write to the mailbox, invited members can read messages.'
// break;
}
this.setState({ securityDescription: securityText });
}
}
idChange(event) {
@ -63,12 +45,16 @@ export class NewScreen extends Component {
});
}
invChange(event) {
this.setState({invites: event.target.value});
setInvite(value) {
this.setState({invites: value});
}
securityChange(event) {
this.setState({security: event.target.value});
if (event.target.checked) {
this.setState({security: "village"});
} else if (!event.target.checked) {
this.setState({security: "channel"});
}
}
allowHistoryChange(event) {
@ -106,10 +92,13 @@ export class NewScreen extends Component {
let aud = [];
let isValid = true;
if (state.invites.length > 2) {
aud = state.invites.split(',')
.map((mem) => `~${deSig(mem.trim())}`);
if ((state.invites.groups.length > 0) || (state.invites.ships.length > 0)) {
if (state.invites.groups.length > 0) {
aud = props.groups[state.invites.groups].values();
}
else {
aud = state.invites.ships.map(mem => `~${deSig(mem.trim())}`);
}
aud.forEach((mem) => {
if (!urbitOb.isValidPatp(mem)) {
isValid = false;
@ -141,19 +130,14 @@ export class NewScreen extends Component {
} else if (state.security === 'channel') {
readAud = []; // black list
writeAud = []; // black list
} else if (state.security === 'journal') {
aud.push(`~${window.ship}`);
readAud = []; // black list
writeAud = aud.slice(); // white list
} else if (state.security === 'mailbox') {
aud.push(`~${window.ship}`);
readAud = aud.slice(); // white list
writeAud = []; // black list
}
this.setState({
error: false,
success: true,
invites: ''
invites: {
groups: [],
ships: []
}
}, () => {
props.setSpinner(true);
props.api.chatView.create(
@ -168,87 +152,82 @@ export class NewScreen extends Component {
}
render() {
let createClasses = "pointer db f9 green2 bg-gray0-d ba pa2 b--green2";
let inviteSwitchClasses =
"relative bg-gray4 bg-gray1-d br3 h1 security v-mid z-0";
if (this.state.security === "village") {
inviteSwitchClasses = "relative checked bg-green2 br3 h1 security v-mid z-0";
}
let createClasses = "pointer db f9 green2 bg-gray0-d ba pv3 ph4 b--green2";
if (!this.state.idName) {
createClasses = 'pointer db f9 gray2 ba bg-gray0-d pa2 b--gray3';
createClasses = 'pointer db f9 gray2 ba bg-gray0-d pa2 pv3 ph4 b--gray3';
}
let idErrElem = (<span />);
if (this.state.idError) {
idErrElem = (
<span className="f9 inter red2 db">
<span className="f9 inter red2 db pt2">
Chat must have a valid name.
</span>
);
}
let invErrElem = (<span />);
if (this.state.inviteError) {
invErrElem = (
<span className="f9 inter red2 db">
Invites must be validly formatted ship names.
</span>
);
}
return (
<div className={`h-100 w-100 w-50-l w-50-xl pa3 pt2 overflow-x-hidden
bg-gray0-d white-d flex flex-column`}>
<div
className={
"h-100 w-100 mw6 pa3 pt4 overflow-x-hidden " +
"bg-gray0-d white-d flex flex-column"
}>
<div className="w-100 dn-m dn-l dn-xl inter pt1 pb6 f8">
<Link to="/~chat/">{"⟵ All Chats"}</Link>
</div>
<h2 className="mb3 f8">Create New Chat</h2>
<h2 className="mb3 f8">New Chat</h2>
<div className="w-100">
<p className="f8 mt3 lh-copy db">Chat Name</p>
<p className="f9 gray2 db mb4">
Lowercase alphanumeric characters, dashes, and slashes only
<p className="f8 mt3 lh-copy db">Name</p>
<p className="f9 gray2 db mb2 pt1">
Lowercase alphanumeric characters, dashes, and slashes only
</p>
<textarea
className="f7 ba b--gray3 b--gray2-d bg-gray0-d white-d pa3 db w-100"
placeholder="secret-chat"
rows={1}
style={{
resize: 'none',
resize: "none"
}}
onChange={this.idChange} />
onChange={this.idChange}
/>
{idErrElem}
<p className="f8 mt6 lh-copy db">Chat Type</p>
<p className="f9 gray2 db mb4">Change the chat's visibility and type</p>
<div className="dropdown relative">
<select
style={{WebkitAppearance: "none"}}
className="pa3 f8 bg-white bg-gray0-d white-d br0 w-100 inter"
value={this.state.securityValue}
onChange={this.securityChange}>
<option value="village">Village</option>
<option value="channel">Channel</option>
<option value="journal">Journal</option>
{/* <option value="mailbox">Mailbox</option> */}
</select>
</div>
<p className="f9 gray2 db lh-copy pt2 mb4">{this.state.securityDescription}</p>
<p className="f8 mt4 lh-copy db">Invites</p>
<p className="f9 gray2 db mb4">
Invite participants to this chat
<p className="f8 mt4 lh-copy db">
Invite
<span className="gray3"> (Optional)</span>
</p>
<textarea
ref={e => { this.textarea = e; }}
className="f7 mono ba b--gray3 b--gray2-d bg-gray0-d white-d pa3 mb4 db w-100"
placeholder="~zod, ~bus"
spellCheck="false"
style={{
resize: 'none',
height: 150
}}
onChange={this.invChange} />
{invErrElem}
<p className="f9 gray2 db mb2 pt1">
Selected entities will be able to post to chat
</p>
<InviteSearch
groups={this.props.groups}
invites={this.state.invites}
setInvite={this.setInvite}
/>
<div className="pv7">
<input
type="checkbox"
style={{ WebkitAppearance: "none", width: 28 }}
className={inviteSwitchClasses}
onChange={this.securityChange}
/>
<span className="dib f9 white-d inter ml3">Invite Only Chat</span>
<p className="f9 gray2 pt1" style={{ paddingLeft: 40 }}>
Chat participants must be invited to see chat content
</p>
</div>
<button
onClick={this.onClickCreate.bind(this)}
className={createClasses}
>Start Chat</button>
className={createClasses}>
Start Chat
</button>
</div>
</div>
);
}
}
}

View File

@ -101,6 +101,7 @@ export class Root extends Component {
setSpinner={this.setSpinner}
api={api}
inbox={state.inbox || {}}
groups={state.groups || {}}
{...props}
/>
</Skeleton>