mirror of
https://github.com/urbit/shrub.git
synced 2024-11-30 22:15:47 +03:00
chat: add group and peer search, new.js rework
This commit is contained in:
parent
a9434743a0
commit
761d71cfee
File diff suppressed because one or more lines are too long
BIN
pkg/arvo/app/chat/img/search.png
Normal file
BIN
pkg/arvo/app/chat/img/search.png
Normal file
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
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
274
pkg/interface/chat/src/js/components/lib/invite-search.js
Normal file
274
pkg/interface/chat/src/js/components/lib/invite-search.js
Normal 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;
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -101,6 +101,7 @@ export class Root extends Component {
|
||||
setSpinner={this.setSpinner}
|
||||
api={api}
|
||||
inbox={state.inbox || {}}
|
||||
groups={state.groups || {}}
|
||||
{...props}
|
||||
/>
|
||||
</Skeleton>
|
||||
|
Loading…
Reference in New Issue
Block a user