mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-12-11 08:55:23 +03:00
Merge pull request #2714 from liam-fitzgerald/lf/profile-overlay
chat: profile overlay and DMs
This commit is contained in:
commit
8968190e55
@ -169,6 +169,15 @@ h2 {
|
|||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.shadow-6 {
|
||||||
|
box-shadow: 2px 4px 20px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brt2 {
|
||||||
|
border-radius: 0.25rem 0.25rem 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.green3 {
|
.green3 {
|
||||||
color: #7ea899;
|
color: #7ea899;
|
||||||
}
|
}
|
||||||
@ -363,6 +372,9 @@ pre.CodeMirror-placeholder.CodeMirror-line-like { color: var(--gray); }
|
|||||||
.b--white-d {
|
.b--white-d {
|
||||||
border-color: #fff;
|
border-color: #fff;
|
||||||
}
|
}
|
||||||
|
.b--green2-d {
|
||||||
|
border-color: #2aa779;
|
||||||
|
}
|
||||||
.bb-d {
|
.bb-d {
|
||||||
border-bottom-width: 1px;
|
border-bottom-width: 1px;
|
||||||
border-bottom-style: solid;
|
border-bottom-style: solid;
|
||||||
|
@ -288,6 +288,7 @@ export class ChatScreen extends Component {
|
|||||||
paddingTop={paddingTop}
|
paddingTop={paddingTop}
|
||||||
paddingBot={paddingBot}
|
paddingBot={paddingBot}
|
||||||
pending={!!msg.pending}
|
pending={!!msg.pending}
|
||||||
|
group={props.association}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
if(unread > 0 && i === unread) {
|
if(unread > 0 && i === unread) {
|
||||||
@ -327,7 +328,7 @@ export class ChatScreen extends Component {
|
|||||||
|
|
||||||
if (navigator.userAgent.includes("Firefox")) {
|
if (navigator.userAgent.includes("Firefox")) {
|
||||||
return (
|
return (
|
||||||
<div className="overflow-y-scroll h-100" onScroll={this.onScroll} ref={e => { this.scrollContainer = e; }}>
|
<div className="relative overflow-y-scroll h-100" onScroll={this.onScroll} ref={e => { this.scrollContainer = e; }}>
|
||||||
<div
|
<div
|
||||||
className="bg-white bg-gray0-d pt3 pb2 flex flex-column-reverse"
|
className="bg-white bg-gray0-d pt3 pb2 flex flex-column-reverse"
|
||||||
style={{ resize: "vertical" }}
|
style={{ resize: "vertical" }}
|
||||||
@ -358,7 +359,7 @@ export class ChatScreen extends Component {
|
|||||||
else {
|
else {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="overflow-y-scroll bg-white bg-gray0-d pt3 pb2 flex flex-column-reverse"
|
className="overflow-y-scroll bg-white bg-gray0-d pt3 pb2 flex flex-column-reverse relative"
|
||||||
style={{ height: "100%", resize: "vertical" }}
|
style={{ height: "100%", resize: "vertical" }}
|
||||||
onScroll={this.onScroll}
|
onScroll={this.onScroll}
|
||||||
>
|
>
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import cn from 'classnames';
|
|
||||||
import { UnControlled as CodeEditor } from 'react-codemirror2';
|
import { UnControlled as CodeEditor } from 'react-codemirror2';
|
||||||
import CodeMirror from 'codemirror';
|
|
||||||
|
|
||||||
import 'codemirror/mode/markdown/markdown';
|
import 'codemirror/mode/markdown/markdown';
|
||||||
import 'codemirror/addon/display/placeholder';
|
import 'codemirror/addon/display/placeholder';
|
||||||
|
|
||||||
import { Sigil } from '/components/lib/icons/sigil';
|
import { Sigil } from '/components/lib/icons/sigil';
|
||||||
|
import { ShipSearch } from '/components/lib/ship-search';
|
||||||
|
|
||||||
import { uxToHex, hexToRgba } from '/lib/util';
|
import { uxToHex } from '/lib/util';
|
||||||
|
|
||||||
const MARKDOWN_CONFIG = {
|
const MARKDOWN_CONFIG = {
|
||||||
name: 'markdown',
|
name: 'markdown',
|
||||||
@ -32,78 +31,6 @@ const MARKDOWN_CONFIG = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function ChatInputSuggestion({ ship, contacts, selected, onSelect }) {
|
|
||||||
const contact = contacts[ship];
|
|
||||||
let color = '#000000';
|
|
||||||
let sigilClass = 'v-mid mix-blend-diff';
|
|
||||||
let nickname;
|
|
||||||
const nameStyle = {};
|
|
||||||
const isSelected = ship === selected;
|
|
||||||
if (contact) {
|
|
||||||
const hex = uxToHex(contact.color);
|
|
||||||
color = `#${hex}`;
|
|
||||||
nameStyle.color = hexToRgba(hex, .7);
|
|
||||||
nameStyle.textShadow = '0px 0px 0px #000';
|
|
||||||
nameStyle.filter = 'contrast(1.3) saturate(1.5)';
|
|
||||||
sigilClass = 'v-mid';
|
|
||||||
nickname = contact.nickname;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
onClick={() => onSelect(ship)}
|
|
||||||
className={cn(
|
|
||||||
'f8 pv1 ph3 pointer hover-bg-gray1-d hover-bg-gray4 relative flex items-center',
|
|
||||||
{
|
|
||||||
'white-d bg-gray0-d bg-white': !isSelected,
|
|
||||||
'black-d bg-gray1-d bg-gray4': isSelected
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
key={ship}
|
|
||||||
>
|
|
||||||
<Sigil
|
|
||||||
ship={'~' + ship}
|
|
||||||
size={24}
|
|
||||||
color={color}
|
|
||||||
classes={sigilClass}
|
|
||||||
/>
|
|
||||||
{ nickname && (
|
|
||||||
<p style={nameStyle} className="dib ml4 b" >{nickname}</p>)
|
|
||||||
}
|
|
||||||
<div className="mono gray2 ml4">
|
|
||||||
{'~' + ship}
|
|
||||||
</div>
|
|
||||||
<p className="nowrap ml4">
|
|
||||||
{status}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ChatInputSuggestions({ suggestions, onSelect, selected, contacts }) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
bottom: '90%',
|
|
||||||
left: '48px'
|
|
||||||
}}
|
|
||||||
className={
|
|
||||||
'absolute black white-d bg-white bg-gray0-d ' +
|
|
||||||
'w7 pv3 z-1 mt1 ba b--gray1-d b--gray4'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{suggestions.map(ship =>
|
|
||||||
(<ChatInputSuggestion
|
|
||||||
onSelect={onSelect}
|
|
||||||
key={ship}
|
|
||||||
selected={selected}
|
|
||||||
contacts={contacts}
|
|
||||||
ship={ship}
|
|
||||||
/>)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ChatInput extends Component {
|
export class ChatInput extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -111,8 +38,7 @@ export class ChatInput extends Component {
|
|||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
message: '',
|
message: '',
|
||||||
patpSuggestions: [],
|
patpSearch: null
|
||||||
selectedSuggestion: null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.textareaRef = React.createRef();
|
this.textareaRef = React.createRef();
|
||||||
@ -121,10 +47,8 @@ export class ChatInput extends Component {
|
|||||||
this.messageChange = this.messageChange.bind(this);
|
this.messageChange = this.messageChange.bind(this);
|
||||||
|
|
||||||
this.patpAutocomplete = this.patpAutocomplete.bind(this);
|
this.patpAutocomplete = this.patpAutocomplete.bind(this);
|
||||||
this.nextAutocompleteSuggestion = this.nextAutocompleteSuggestion.bind(this);
|
|
||||||
this.completePatp = this.completePatp.bind(this);
|
this.completePatp = this.completePatp.bind(this);
|
||||||
|
this.clearSearch = this.clearSearch.bind(this);
|
||||||
this.clearSuggestions = this.clearSuggestions.bind(this);
|
|
||||||
|
|
||||||
this.toggleCode = this.toggleCode.bind(this);
|
this.toggleCode = this.toggleCode.bind(this);
|
||||||
|
|
||||||
@ -185,52 +109,20 @@ export class ChatInput extends Component {
|
|||||||
this.setState({ selectedSuggestion: patpSuggestions[idx] });
|
this.setState({ selectedSuggestion: patpSuggestions[idx] });
|
||||||
}
|
}
|
||||||
|
|
||||||
patpAutocomplete(message, fresh = false) {
|
patpAutocomplete(message) {
|
||||||
const match = /~([a-zA-Z\-]*)$/.exec(message);
|
const match = /~([a-zA-Z\-]*)$/.exec(message);
|
||||||
|
|
||||||
if (!match ) {
|
if (!match ) {
|
||||||
this.setState({ patpSuggestions: [] });
|
this.setState({ patpSearch: null });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.setState({ patpSearch: match[1].toLowerCase() });
|
||||||
|
|
||||||
const needle = match[1].toLowerCase();
|
|
||||||
|
|
||||||
const matchString = (hay) => {
|
|
||||||
hay = hay.toLowerCase();
|
|
||||||
|
|
||||||
return hay.startsWith(needle)
|
|
||||||
|| _.some(_.words(hay), s => s.startsWith(needle));
|
|
||||||
};
|
|
||||||
|
|
||||||
const contacts = _.chain(this.props.contacts)
|
|
||||||
.defaultTo({})
|
|
||||||
.map((details, ship) => ({ ...details, ship }))
|
|
||||||
.filter(({ nickname, ship }) => matchString(nickname) || matchString(ship))
|
|
||||||
.map('ship')
|
|
||||||
.value();
|
|
||||||
|
|
||||||
const suggestions = _.chain(this.props.envelopes)
|
|
||||||
.defaultTo([])
|
|
||||||
.map('author')
|
|
||||||
.uniq()
|
|
||||||
.reverse()
|
|
||||||
.filter(matchString)
|
|
||||||
.union(contacts)
|
|
||||||
.filter(s => s.length < 28) // exclude comets
|
|
||||||
.take(5)
|
|
||||||
.value();
|
|
||||||
|
|
||||||
const newState = {
|
|
||||||
patpSuggestions: suggestions,
|
|
||||||
selectedSuggestion: suggestions[0]
|
|
||||||
};
|
|
||||||
|
|
||||||
this.setState(newState);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clearSuggestions() {
|
clearSearch() {
|
||||||
this.setState({
|
this.setState({
|
||||||
patpSuggestions: []
|
patpSearch: null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,13 +139,13 @@ export class ChatInput extends Component {
|
|||||||
const lastCol = this.editor.getLineHandle(lastRow).text.length;
|
const lastCol = this.editor.getLineHandle(lastRow).text.length;
|
||||||
this.editor.setCursor(lastRow, lastCol);
|
this.editor.setCursor(lastRow, lastCol);
|
||||||
this.setState({
|
this.setState({
|
||||||
patpSuggestions: []
|
patpSearch: null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
messageChange(editor, data, value) {
|
messageChange(editor, data, value) {
|
||||||
const { patpSuggestions } = this.state;
|
const { patpSearch } = this.state;
|
||||||
if(patpSuggestions.length !== 0) {
|
if(patpSearch !== null) {
|
||||||
this.patpAutocomplete(value, false);
|
this.patpAutocomplete(value, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -384,7 +276,12 @@ export class ChatInput extends Component {
|
|||||||
const sigilClass = props.ownerContact
|
const sigilClass = props.ownerContact
|
||||||
? '' : 'mix-blend-diff';
|
? '' : 'mix-blend-diff';
|
||||||
|
|
||||||
const completeActive = this.state.patpSuggestions.length !== 0;
|
const candidates = _.chain(this.props.envelopes)
|
||||||
|
.defaultTo([])
|
||||||
|
.map('author')
|
||||||
|
.uniq()
|
||||||
|
.reverse()
|
||||||
|
.value();
|
||||||
|
|
||||||
const codeTheme = state.code ? ' code' : '';
|
const codeTheme = state.code ? ' code' : '';
|
||||||
|
|
||||||
@ -398,33 +295,11 @@ export class ChatInput extends Component {
|
|||||||
placeholder: state.code ? 'Code...' : props.placeholder,
|
placeholder: state.code ? 'Code...' : props.placeholder,
|
||||||
extraKeys: {
|
extraKeys: {
|
||||||
Tab: cm =>
|
Tab: cm =>
|
||||||
completeActive
|
this.patpAutocomplete(cm.getValue(), true),
|
||||||
? this.nextAutocompleteSuggestion()
|
|
||||||
: this.patpAutocomplete(cm.getValue(), true),
|
|
||||||
'Shift-Tab': cm =>
|
|
||||||
completeActive
|
|
||||||
? this.nextAutocompleteSuggestion(true)
|
|
||||||
: CodeMirror.Pass,
|
|
||||||
'Up': cm =>
|
|
||||||
completeActive
|
|
||||||
? this.nextAutocompleteSuggestion(true)
|
|
||||||
: CodeMirror.Pass,
|
|
||||||
'Escape': cm =>
|
|
||||||
completeActive
|
|
||||||
? this.clearSuggestions(true)
|
|
||||||
: CodeMirror.Pass,
|
|
||||||
'Down': cm =>
|
|
||||||
completeActive
|
|
||||||
? this.nextAutocompleteSuggestion()
|
|
||||||
: CodeMirror.Pass,
|
|
||||||
'Enter': cm =>
|
'Enter': cm =>
|
||||||
completeActive
|
this.messageSubmit(),
|
||||||
? this.completePatp(state.selectedSuggestion)
|
|
||||||
: this.messageSubmit(),
|
|
||||||
'Shift-3': cm =>
|
'Shift-3': cm =>
|
||||||
cm.getValue().length === 0
|
this.toggleCode()
|
||||||
? this.toggleCode()
|
|
||||||
: CodeMirror.Pass
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -432,15 +307,15 @@ export class ChatInput extends Component {
|
|||||||
<div className="pa3 cf flex black white-d bt b--gray4 b--gray1-d bg-white bg-gray0-d relative"
|
<div className="pa3 cf flex black white-d bt b--gray4 b--gray1-d bg-white bg-gray0-d relative"
|
||||||
style={{ flexGrow: 1 }}
|
style={{ flexGrow: 1 }}
|
||||||
>
|
>
|
||||||
{state.patpSuggestions.length !== 0 && (
|
<ShipSearch
|
||||||
<ChatInputSuggestions
|
popover
|
||||||
onSelect={this.completePatp}
|
onSelect={this.completePatp}
|
||||||
suggestions={state.patpSuggestions}
|
onClear={this.clearSearch}
|
||||||
selected={state.selectedSuggestion}
|
|
||||||
contacts={props.contacts}
|
contacts={props.contacts}
|
||||||
|
candidates={candidates}
|
||||||
|
searchTerm={this.state.patpSearch}
|
||||||
|
cm={this.editor}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="fl"
|
className="fl"
|
||||||
style={{
|
style={{
|
||||||
|
@ -22,7 +22,8 @@ export class Sigil extends Component {
|
|||||||
patp: props.ship,
|
patp: props.ship,
|
||||||
renderer: reactRenderer,
|
renderer: reactRenderer,
|
||||||
size: props.size,
|
size: props.size,
|
||||||
colors: [props.color, "white"]
|
colors: [props.color, "white"],
|
||||||
|
class: props.svgClass
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -289,7 +289,7 @@ export class InviteSearch extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { props, state } = this;
|
const { props, state } = this;
|
||||||
let searchDisabled = false;
|
let searchDisabled = props.disabled;
|
||||||
if (props.invites.groups) {
|
if (props.invites.groups) {
|
||||||
if (props.invites.groups.length > 0) {
|
if (props.invites.groups.length > 0) {
|
||||||
searchDisabled = true;
|
searchDisabled = true;
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Sigil } from '/components/lib/icons/sigil';
|
import { Sigil } from '/components/lib/icons/sigil';
|
||||||
|
import { ProfileOverlay } from '/components/lib/profile-overlay';
|
||||||
|
import { OverlaySigil } from '/components/lib/overlay-sigil';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { Route, Link } from 'react-router-dom'
|
import { Route, Link } from 'react-router-dom'
|
||||||
import { uxToHex, cite, writeText } from '/lib/util';
|
import { uxToHex, cite, writeText } from '/lib/util';
|
||||||
@ -53,6 +55,7 @@ export class Message extends Component {
|
|||||||
iframe.setAttribute('src', iframe.getAttribute('data-src'));
|
iframe.setAttribute('src', iframe.getAttribute('data-src'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
renderContent() {
|
renderContent() {
|
||||||
const { props } = this;
|
const { props } = this;
|
||||||
let letter = props.msg.letter;
|
let letter = props.msg.letter;
|
||||||
@ -190,20 +193,20 @@ export class Message extends Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={this.containerRef}
|
||||||
className={
|
className={
|
||||||
"w-100 f7 pl3 pt4 pr3 cf flex lh-copy " + " " + pending
|
"w-100 f7 pl3 pt4 pr3 cf flex lh-copy " + " " + pending
|
||||||
}
|
}
|
||||||
style={{
|
style={{
|
||||||
minHeight: "min-content"
|
minHeight: "min-content"
|
||||||
}}>
|
}}>
|
||||||
<div className="fl mr3 v-top bg-white bg-gray0-d">
|
<OverlaySigil
|
||||||
<Sigil
|
|
||||||
ship={props.msg.author}
|
ship={props.msg.author}
|
||||||
size={24}
|
contact={contact}
|
||||||
color={color}
|
color={color}
|
||||||
classes={sigilClass}
|
sigilClass={sigilClass}
|
||||||
/>
|
group={props.group}
|
||||||
</div>
|
className="fl pr3 v-top bg-white bg-gray0-d" />
|
||||||
<div
|
<div
|
||||||
className="fr clamp-message white-d"
|
className="fr clamp-message white-d"
|
||||||
style={{ flexGrow: 1, marginTop: -8 }}>
|
style={{ flexGrow: 1, marginTop: -8 }}>
|
||||||
|
99
pkg/interface/chat/src/js/components/lib/overlay-sigil.js
Normal file
99
pkg/interface/chat/src/js/components/lib/overlay-sigil.js
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import React, { Component } from "react";
|
||||||
|
import { Sigil } from "/components/lib/icons/sigil";
|
||||||
|
import {
|
||||||
|
ProfileOverlay,
|
||||||
|
OVERLAY_HEIGHT
|
||||||
|
} from "/components/lib/profile-overlay";
|
||||||
|
|
||||||
|
export class OverlaySigil extends Component {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.state = {
|
||||||
|
clicked: false,
|
||||||
|
captured: false,
|
||||||
|
topSpace: 0,
|
||||||
|
bottomSpace: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
this.containerRef = React.createRef();
|
||||||
|
|
||||||
|
this.profileShow = this.profileShow.bind(this);
|
||||||
|
this.profileHide = this.profileHide.bind(this);
|
||||||
|
this.updateContainerInterval = setInterval(
|
||||||
|
this.updateContainerOffset.bind(this),
|
||||||
|
1000
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.updateContainerOffset();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (this.updateContainerInterval) {
|
||||||
|
clearInterval(this.updateContainerInterval);
|
||||||
|
this.updateContainerInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
profileShow() {
|
||||||
|
this.setState({ profileClicked: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
profileHide() {
|
||||||
|
this.setState({ profileClicked: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
updateContainerOffset() {
|
||||||
|
if (this.containerRef && this.containerRef.current) {
|
||||||
|
const parent = this.containerRef.current.offsetParent;
|
||||||
|
const { offsetTop } = this.containerRef.current;
|
||||||
|
|
||||||
|
|
||||||
|
let bottomSpace, topSpace;
|
||||||
|
|
||||||
|
if(navigator.userAgent.includes('Firefox')) {
|
||||||
|
topSpace = offsetTop - parent.scrollTop - OVERLAY_HEIGHT / 2;
|
||||||
|
bottomSpace = parent.clientHeight - topSpace - OVERLAY_HEIGHT;
|
||||||
|
} else {
|
||||||
|
topSpace = offsetTop + parent.scrollHeight - parent.clientHeight - parent.scrollTop;
|
||||||
|
bottomSpace = parent.clientHeight - topSpace - OVERLAY_HEIGHT;
|
||||||
|
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
topSpace,
|
||||||
|
bottomSpace
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { props, state } = this;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={this.profileShow}
|
||||||
|
className={props.className + " pointer relative"}
|
||||||
|
ref={this.containerRef}
|
||||||
|
style={{ height: "24px" }}
|
||||||
|
>
|
||||||
|
{state.profileClicked && (
|
||||||
|
<ProfileOverlay
|
||||||
|
ship={props.ship}
|
||||||
|
contact={props.contact}
|
||||||
|
color={props.color}
|
||||||
|
topSpace={state.topSpace}
|
||||||
|
bottomSpace={state.bottomSpace}
|
||||||
|
group={props.group}
|
||||||
|
onDismiss={this.profileHide}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Sigil
|
||||||
|
ship={props.ship}
|
||||||
|
size={24}
|
||||||
|
color={props.color}
|
||||||
|
classes={props.sigilClass}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
97
pkg/interface/chat/src/js/components/lib/profile-overlay.js
Normal file
97
pkg/interface/chat/src/js/components/lib/profile-overlay.js
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
export class ProfileOverlay extends Component {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.popoverRef = React.createRef();
|
||||||
|
this.onDocumentClick = this.onDocumentClick.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
document.addEventListener("mousedown", this.onDocumentClick);
|
||||||
|
document.addEventListener("touchstart", this.onDocumentClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
document.removeEventListener("mousedown", this.onDocumentClick);
|
||||||
|
document.removeEventListener("touchstart", this.onDocumentClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
onDocumentClick(event) {
|
||||||
|
const { popoverRef } = this;
|
||||||
|
// Do nothing if clicking ref's element or descendent elements
|
||||||
|
if (!popoverRef.current || popoverRef.current.contains(event.target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.onDismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { contact, ship, color, topSpace, bottomSpace, group } = this.props;
|
||||||
|
|
||||||
|
let top, bottom;
|
||||||
|
if (topSpace < OVERLAY_HEIGHT / 2) {
|
||||||
|
top = `0px`;
|
||||||
|
}
|
||||||
|
if (bottomSpace < OVERLAY_HEIGHT / 2) {
|
||||||
|
bottom = `0px`;
|
||||||
|
}
|
||||||
|
if (!(top || bottom)) {
|
||||||
|
bottom = `-${Math.round(OVERLAY_HEIGHT / 2)}px`;
|
||||||
|
}
|
||||||
|
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}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={this.popoverRef}
|
||||||
|
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>
|
||||||
|
<div className="pv3 pl3 pr2">
|
||||||
|
{contact && contact.nickname && (
|
||||||
|
<div className="b white-d">{contact.nickname}</div>
|
||||||
|
)}
|
||||||
|
<div className="mono gray2">{cite(`~${ship}`)}</div>
|
||||||
|
{!isOwn && (
|
||||||
|
<Link
|
||||||
|
to={`/~chat/new/dm/~${ship}`}
|
||||||
|
className="b--green0 b--green2-d b--solid ba green2 mt3 tc pa2 pointer db"
|
||||||
|
>
|
||||||
|
Send Message
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{isOwn && (
|
||||||
|
<a
|
||||||
|
href={identityHref}
|
||||||
|
className="b--black b--white-d ba black white-d mt3 tc pa2 pointer db"
|
||||||
|
>
|
||||||
|
Edit Group Identity
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
370
pkg/interface/chat/src/js/components/lib/ship-search.js
Normal file
370
pkg/interface/chat/src/js/components/lib/ship-search.js
Normal file
@ -0,0 +1,370 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import urbitOb from 'urbit-ob';
|
||||||
|
import Mousetrap from 'mousetrap';
|
||||||
|
|
||||||
|
import cn from 'classnames';
|
||||||
|
import { Sigil } from '/components/lib/icons/sigil';
|
||||||
|
import { hexToRgba, uxToHex, deSig } from '/lib/util';
|
||||||
|
|
||||||
|
function ShipSearchItem({ ship, contacts, selected, onSelect }) {
|
||||||
|
const contact = contacts[ship];
|
||||||
|
let color = '#000000';
|
||||||
|
let sigilClass = 'v-mid mix-blend-diff';
|
||||||
|
let nickname;
|
||||||
|
const nameStyle = {};
|
||||||
|
const isSelected = ship === selected;
|
||||||
|
if (contact) {
|
||||||
|
const hex = uxToHex(contact.color);
|
||||||
|
color = `#${hex}`;
|
||||||
|
nameStyle.color = hexToRgba(hex, 0.7);
|
||||||
|
nameStyle.textShadow = '0px 0px 0px #000';
|
||||||
|
nameStyle.filter = 'contrast(1.3) saturate(1.5)';
|
||||||
|
sigilClass = 'v-mid';
|
||||||
|
nickname = contact.nickname;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={() => onSelect(ship)}
|
||||||
|
className={cn(
|
||||||
|
'f8 pv1 ph3 pointer hover-bg-gray1-d hover-bg-gray4 relative flex items-center',
|
||||||
|
{
|
||||||
|
'white-d bg-gray0-d bg-white': !isSelected,
|
||||||
|
'black-d bg-gray1-d bg-gray4': isSelected
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
key={ship}
|
||||||
|
>
|
||||||
|
<Sigil ship={'~' + ship} size={24} color={color} classes={sigilClass} />
|
||||||
|
{nickname && (
|
||||||
|
<p style={nameStyle} className="dib ml4 b">
|
||||||
|
{nickname}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div className="mono gray2 gray4-d ml4">{'~' + ship}</div>
|
||||||
|
<p className="nowrap ml4">{status}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ShipSearch extends Component {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
selected: null,
|
||||||
|
suggestions: [],
|
||||||
|
bound: false
|
||||||
|
};
|
||||||
|
|
||||||
|
this.keymap = {
|
||||||
|
Tab: cm =>
|
||||||
|
this.nextAutocompleteSuggestion(),
|
||||||
|
'Shift-Tab': cm =>
|
||||||
|
this.nextAutocompleteSuggestion(true),
|
||||||
|
'Up': cm =>
|
||||||
|
this.nextAutocompleteSuggestion(true),
|
||||||
|
'Escape': cm =>
|
||||||
|
this.props.onClear(),
|
||||||
|
'Down': cm =>
|
||||||
|
this.nextAutocompleteSuggestion(),
|
||||||
|
'Enter': (cm) => {
|
||||||
|
if(this.props.searchTerm !== null) {
|
||||||
|
this.props.onSelect(this.state.selected);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'Shift-3': cm =>
|
||||||
|
this.toggleCode()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if(this.props.searchTerm !== null) {
|
||||||
|
this.updateSuggestions(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const { props, state } = this;
|
||||||
|
|
||||||
|
if(!state.bound && props.inputRef) {
|
||||||
|
this.bindShortcuts();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(props.searchTerm === null) {
|
||||||
|
if(state.suggestions.length > 0) {
|
||||||
|
this.setState({ suggestions: [] });
|
||||||
|
}
|
||||||
|
this.unbindShortcuts();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
props.searchTerm === null &&
|
||||||
|
props.searchTerm !== prevProps.searchTerm &&
|
||||||
|
props.searchTerm.startsWith(prevProps.searchTerm)
|
||||||
|
) {
|
||||||
|
this.updateSuggestions();
|
||||||
|
} else if (prevProps.searchTerm !== props.searchTerm) {
|
||||||
|
this.updateSuggestions(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSuggestions(isStale = false) {
|
||||||
|
const needle = this.props.searchTerm;
|
||||||
|
const matchString = (hay) => {
|
||||||
|
hay = hay.toLowerCase();
|
||||||
|
|
||||||
|
return (
|
||||||
|
hay.startsWith(needle) ||
|
||||||
|
_.some(_.words(hay), s => s.startsWith(needle))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
let candidates = this.state.suggestions;
|
||||||
|
|
||||||
|
if (isStale || this.state.suggestions.length === 0) {
|
||||||
|
const contacts = _.chain(this.props.contacts)
|
||||||
|
.defaultTo({})
|
||||||
|
.map((details, ship) => ({ ...details, ship }))
|
||||||
|
.filter(
|
||||||
|
({ nickname, ship }) => matchString(nickname) || matchString(ship)
|
||||||
|
)
|
||||||
|
.map('ship')
|
||||||
|
.value();
|
||||||
|
|
||||||
|
const exactMatch = urbitOb.isValidPatp(`~${needle}`) ? [needle] : [];
|
||||||
|
|
||||||
|
candidates = _.chain(this.props.candidates)
|
||||||
|
.defaultTo([])
|
||||||
|
.union(contacts)
|
||||||
|
.union(exactMatch)
|
||||||
|
.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
const suggestions = _.chain(candidates)
|
||||||
|
.filter(matchString)
|
||||||
|
.filter(s => s.length < 28) // exclude comets
|
||||||
|
.value();
|
||||||
|
|
||||||
|
this.bindShortcuts();
|
||||||
|
this.setState({ suggestions, selected: suggestions[0] });
|
||||||
|
}
|
||||||
|
|
||||||
|
bindCmShortcuts() {
|
||||||
|
if(!this.props.cm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.props.cm.addKeyMap(this.keymap);
|
||||||
|
}
|
||||||
|
|
||||||
|
unbindCmShortcuts() {
|
||||||
|
if(!this.props.cm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.props.cm.removeKeyMap(this.keymap);
|
||||||
|
}
|
||||||
|
|
||||||
|
bindShortcuts() {
|
||||||
|
if (this.state.bound) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.props.inputRef) {
|
||||||
|
return this.bindCmShortcuts();
|
||||||
|
}
|
||||||
|
this.setState({ bound: true });
|
||||||
|
if (!this.mousetrap) {
|
||||||
|
this.mousetrap = new Mousetrap(this.props.inputRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mousetrap.bind('enter', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (this.state.selected) {
|
||||||
|
this.unbindShortcuts();
|
||||||
|
this.props.onSelect(this.state.selected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.mousetrap.bind('tab', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.nextAutocompleteSuggestion(false);
|
||||||
|
});
|
||||||
|
this.mousetrap.bind(['up', 'shift+tab'], (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.nextAutocompleteSuggestion(true);
|
||||||
|
});
|
||||||
|
this.mousetrap.bind('down', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.nextAutocompleteSuggestion(false);
|
||||||
|
});
|
||||||
|
this.mousetrap.bind('esc', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.props.onClear();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
unbindShortcuts() {
|
||||||
|
if(!this.props.inputRef) {
|
||||||
|
this.unbindCmShortcuts();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.state.bound) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ bound: false });
|
||||||
|
this.mousetrap.unbind('enter');
|
||||||
|
this.mousetrap.unbind('tab');
|
||||||
|
this.mousetrap.unbind(['up', 'shift+tab']);
|
||||||
|
this.mousetrap.unbind('down');
|
||||||
|
this.mousetrap.unbind('esc');
|
||||||
|
}
|
||||||
|
|
||||||
|
nextAutocompleteSuggestion(backward = false) {
|
||||||
|
const { suggestions } = this.state;
|
||||||
|
let idx = suggestions.findIndex(s => s === this.state.selected);
|
||||||
|
|
||||||
|
idx = backward ? idx - 1 : idx + 1;
|
||||||
|
idx = idx % suggestions.length;
|
||||||
|
if (idx < 0) {
|
||||||
|
idx = suggestions.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ selected: suggestions[idx] });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { onSelect, contacts, popover, className } = this.props;
|
||||||
|
const { selected, suggestions } = this.state;
|
||||||
|
|
||||||
|
if (suggestions.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const popoverClasses = (popover && ' absolute ') || ' ';
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
popover
|
||||||
|
? {
|
||||||
|
bottom: '90%',
|
||||||
|
left: '48px'
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
}
|
||||||
|
className={
|
||||||
|
'black white-d bg-white bg-gray0-d ' +
|
||||||
|
'w7 pv3 z-1 mt1 ba b--gray1-d b--gray4' +
|
||||||
|
popoverClasses +
|
||||||
|
className || ''
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{suggestions.slice(0, 5).map(ship => (
|
||||||
|
<ShipSearchItem
|
||||||
|
onSelect={onSelect}
|
||||||
|
key={ship}
|
||||||
|
selected={selected}
|
||||||
|
contacts={contacts}
|
||||||
|
ship={ship}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ShipSearchInput extends Component {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.state = {
|
||||||
|
searchTerm: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
this.inputRef = null;
|
||||||
|
this.popoverRef = null;
|
||||||
|
|
||||||
|
this.search = this.search.bind(this);
|
||||||
|
|
||||||
|
this.onClick = this.onClick.bind(this);
|
||||||
|
this.setInputRef = this.setInputRef.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClick(event) {
|
||||||
|
const { popoverRef } = this;
|
||||||
|
// Do nothing if clicking ref's element or descendent elements
|
||||||
|
if (!popoverRef || popoverRef.contains(event.target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.onClear();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
document.addEventListener('mousedown', this.onClick);
|
||||||
|
document.addEventListener('touchstart', this.onClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
document.removeEventListener('mousedown', this.onClick);
|
||||||
|
document.removeEventListener('touchstart', this.onClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
setInputRef(ref) {
|
||||||
|
this.inputRef = ref;
|
||||||
|
if(ref) {
|
||||||
|
ref.focus();
|
||||||
|
}
|
||||||
|
// update this.inputRef prop
|
||||||
|
this.forceUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
search(e) {
|
||||||
|
const searchTerm = e.target.value;
|
||||||
|
this.setState({ searchTerm });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { state, props } = this;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref => (this.popoverRef = ref)}
|
||||||
|
style={{ top: '150%', left: '-80px' }}
|
||||||
|
className="b--gray2 b--solid ba absolute bg-white bg-gray0-d"
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
style={{ resize: 'none', maxWidth: '200px' }}
|
||||||
|
className="ma2 pa2 b--gray4 ba b--solid w7 db bg-gray0-d white-d"
|
||||||
|
rows={1}
|
||||||
|
autocapitalise="none"
|
||||||
|
autoFocus={
|
||||||
|
/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(
|
||||||
|
navigator.userAgent
|
||||||
|
)
|
||||||
|
? false
|
||||||
|
: true
|
||||||
|
}
|
||||||
|
placeholder="Search for a ship"
|
||||||
|
value={state.searchTerm}
|
||||||
|
onChange={this.search}
|
||||||
|
ref={this.setInputRef}
|
||||||
|
/>
|
||||||
|
<ShipSearch
|
||||||
|
contacts={props.contacts}
|
||||||
|
candidates={props.candidates}
|
||||||
|
searchTerm={deSig(state.searchTerm)}
|
||||||
|
inputRef={this.inputRef}
|
||||||
|
onSelect={props.onSelect}
|
||||||
|
onClear={props.onClear}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
125
pkg/interface/chat/src/js/components/new-dm.js
Normal file
125
pkg/interface/chat/src/js/components/new-dm.js
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import React, { Component } from "react";
|
||||||
|
import classnames from "classnames";
|
||||||
|
import { InviteSearch } from "./lib/invite-search";
|
||||||
|
import { Spinner } from "./lib/icons/icon-spinner";
|
||||||
|
import { Route, Link } from "react-router-dom";
|
||||||
|
import { uuid, isPatTa, deSig } from "/lib/util";
|
||||||
|
import urbitOb from "urbit-ob";
|
||||||
|
|
||||||
|
export class NewDmScreen extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
ship: null,
|
||||||
|
idError: false,
|
||||||
|
inviteError: false,
|
||||||
|
allowHistory: true,
|
||||||
|
awaiting: false
|
||||||
|
};
|
||||||
|
|
||||||
|
this.setInvite = this.setInvite.bind(this);
|
||||||
|
this.onClickCreate = this.onClickCreate.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { props } = this;
|
||||||
|
if (props.autoCreate && urbitOb.isValidPatp(props.autoCreate)) {
|
||||||
|
this.setState(
|
||||||
|
{
|
||||||
|
error: false,
|
||||||
|
success: true,
|
||||||
|
ship: props.autoCreate.slice(1),
|
||||||
|
awaiting: true
|
||||||
|
},
|
||||||
|
this.onClickCreate
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps, prevState) {
|
||||||
|
const { props, state } = this;
|
||||||
|
|
||||||
|
if (prevProps !== props) {
|
||||||
|
let station = `/~${window.ship}/${state.idName}`;
|
||||||
|
if (station in props.inbox) {
|
||||||
|
props.history.push("/~chat/room" + station);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setInvite(value) {
|
||||||
|
this.setState({
|
||||||
|
groups: [],
|
||||||
|
ship: value.ships[0]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickCreate() {
|
||||||
|
const { props, state } = this;
|
||||||
|
|
||||||
|
let station = `/~/~${window.ship}/dm--${state.ship}`;
|
||||||
|
|
||||||
|
let theirStation = `/~/~${state.ship}/dm--${window.ship}`;
|
||||||
|
|
||||||
|
if (station in props.inbox) {
|
||||||
|
props.history.push(`/~chat/room${station}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (theirStation in props.inbox) {
|
||||||
|
props.history.push(`/~chat/room${theirStation}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState(
|
||||||
|
{
|
||||||
|
error: false,
|
||||||
|
success: true,
|
||||||
|
group: [],
|
||||||
|
ship: [],
|
||||||
|
awaiting: true
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
let groupPath = station;
|
||||||
|
let submit = props.api.chatView.create(
|
||||||
|
`~${window.ship} <-> ~${state.ship}`,
|
||||||
|
"",
|
||||||
|
station,
|
||||||
|
groupPath,
|
||||||
|
"village",
|
||||||
|
state.ship !== window.ship ? [`~${state.ship}`] : [],
|
||||||
|
true
|
||||||
|
);
|
||||||
|
submit.then(() => {
|
||||||
|
this.setState({ awaiting: false });
|
||||||
|
props.history.push(`/~chat/room${station}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { props, state } = this;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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">New DM</h2>
|
||||||
|
<div className="w-100">
|
||||||
|
<Spinner
|
||||||
|
awaiting={this.state.awaiting}
|
||||||
|
classes="mt4"
|
||||||
|
text="Creating chat..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -16,7 +16,7 @@ export class NewScreen extends Component {
|
|||||||
idName: '',
|
idName: '',
|
||||||
groups: [],
|
groups: [],
|
||||||
ships: [],
|
ships: [],
|
||||||
security: 'village',
|
security: 'channel',
|
||||||
idError: false,
|
idError: false,
|
||||||
inviteError: false,
|
inviteError: false,
|
||||||
allowHistory: true,
|
allowHistory: true,
|
||||||
@ -26,7 +26,6 @@ export class NewScreen extends Component {
|
|||||||
|
|
||||||
this.titleChange = this.titleChange.bind(this);
|
this.titleChange = this.titleChange.bind(this);
|
||||||
this.descriptionChange = this.descriptionChange.bind(this);
|
this.descriptionChange = this.descriptionChange.bind(this);
|
||||||
this.securityChange = this.securityChange.bind(this);
|
|
||||||
this.allowHistoryChange = this.allowHistoryChange.bind(this);
|
this.allowHistoryChange = this.allowHistoryChange.bind(this);
|
||||||
this.setInvite = this.setInvite.bind(this);
|
this.setInvite = this.setInvite.bind(this);
|
||||||
this.createGroupChange = this.createGroupChange.bind(this);
|
this.createGroupChange = this.createGroupChange.bind(this);
|
||||||
@ -65,17 +64,6 @@ export class NewScreen extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
securityChange(event) {
|
|
||||||
if (this.state.createGroup) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (event.target.checked) {
|
|
||||||
this.setState({security: "village"});
|
|
||||||
} else if (!event.target.checked) {
|
|
||||||
this.setState({security: "channel"});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
createGroupChange(event) {
|
createGroupChange(event) {
|
||||||
if (event.target.checked) {
|
if (event.target.checked) {
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -85,6 +73,7 @@ export class NewScreen extends Component {
|
|||||||
} else {
|
} else {
|
||||||
this.setState({
|
this.setState({
|
||||||
createGroup: !!event.target.checked,
|
createGroup: !!event.target.checked,
|
||||||
|
security: 'channel'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -124,6 +113,10 @@ export class NewScreen extends Component {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if(state.ships.length === 1 && state.security === 'village' && !state.createGroup) {
|
||||||
|
props.history.push(`/~chat/new/dm/${aud[0]}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
this.setState({
|
this.setState({
|
||||||
inviteError: true,
|
inviteError: true,
|
||||||
@ -279,18 +272,6 @@ export class NewScreen extends Component {
|
|||||||
setInvite={this.setInvite}
|
setInvite={this.setInvite}
|
||||||
/>
|
/>
|
||||||
{createGroupToggle}
|
{createGroupToggle}
|
||||||
<div className="mv7">
|
|
||||||
<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
|
<button
|
||||||
onClick={this.onClickCreate.bind(this)}
|
onClick={this.onClickCreate.bind(this)}
|
||||||
className={createClasses}>
|
className={createClasses}>
|
||||||
|
@ -13,6 +13,7 @@ import { MemberScreen } from '/components/member';
|
|||||||
import { SettingsScreen } from '/components/settings';
|
import { SettingsScreen } from '/components/settings';
|
||||||
import { NewScreen } from '/components/new';
|
import { NewScreen } from '/components/new';
|
||||||
import { JoinScreen } from '/components/join';
|
import { JoinScreen } from '/components/join';
|
||||||
|
import { NewDmScreen } from '/components/new-dm';
|
||||||
|
|
||||||
|
|
||||||
export class Root extends Component {
|
export class Root extends Component {
|
||||||
@ -92,6 +93,34 @@ export class Root extends Component {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path="/~chat/new/dm/:ship"
|
||||||
|
render={props => {
|
||||||
|
const ship = props.match.params.ship;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Skeleton
|
||||||
|
associations={associations}
|
||||||
|
invites={invites}
|
||||||
|
sidebarHideOnMobile={true}
|
||||||
|
sidebar={renderChannelSidebar(props)}
|
||||||
|
sidebarShown={state.sidebarShown}
|
||||||
|
>
|
||||||
|
<NewDmScreen
|
||||||
|
api={api}
|
||||||
|
inbox={state.inbox || {}}
|
||||||
|
permissions={state.permissions || {}}
|
||||||
|
contacts={state.contacts || {}}
|
||||||
|
associations={associations.contacts}
|
||||||
|
chatSynced={state.chatSynced || {}}
|
||||||
|
autoCreate={ship}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</Skeleton>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
path="/~chat/new"
|
path="/~chat/new"
|
||||||
|
@ -5,17 +5,35 @@ import Welcome from '/components/lib/welcome.js';
|
|||||||
import { alphabetiseAssociations } from '../lib/util';
|
import { alphabetiseAssociations } from '../lib/util';
|
||||||
import { SidebarInvite } from '/components/lib/sidebar-invite';
|
import { SidebarInvite } from '/components/lib/sidebar-invite';
|
||||||
import { GroupItem } from '/components/lib/group-item';
|
import { GroupItem } from '/components/lib/group-item';
|
||||||
|
import { ShipSearchInput } from '/components/lib/ship-search';
|
||||||
|
|
||||||
|
|
||||||
export class Sidebar extends Component {
|
export class Sidebar extends Component {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.state = {
|
||||||
|
dmOverlay: false
|
||||||
|
};
|
||||||
|
}
|
||||||
onClickNew() {
|
onClickNew() {
|
||||||
this.props.history.push('/~chat/new');
|
this.props.history.push('/~chat/new');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onClickDm() {
|
||||||
|
this.setState(({ dmOverlay }) => ({ dmOverlay: !dmOverlay }) )
|
||||||
|
}
|
||||||
|
|
||||||
onClickJoin() {
|
onClickJoin() {
|
||||||
this.props.history.push('/~chat/join')
|
this.props.history.push('/~chat/join')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
goDm(ship) {
|
||||||
|
this.setState({ dmOverlay: false }, () => {
|
||||||
|
this.props.history.push(`/~chat/new/dm/~${ship}`)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { props, state } = this;
|
const { props, state } = this;
|
||||||
|
|
||||||
@ -97,6 +115,14 @@ export class Sidebar extends Component {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
const candidates = state.dmOverlay
|
||||||
|
? _.chain(this.props.contacts)
|
||||||
|
.values()
|
||||||
|
.map(_.keys)
|
||||||
|
.flatten()
|
||||||
|
.uniq()
|
||||||
|
.value()
|
||||||
|
: [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -108,6 +134,25 @@ export class Sidebar extends Component {
|
|||||||
onClick={this.onClickNew.bind(this)}>
|
onClick={this.onClickNew.bind(this)}>
|
||||||
New Chat
|
New Chat
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<div className="dib relative mr4">
|
||||||
|
{ state.dmOverlay && (
|
||||||
|
<ShipSearchInput
|
||||||
|
className="absolute"
|
||||||
|
contacts={{}}
|
||||||
|
candidates={candidates}
|
||||||
|
onSelect={this.goDm.bind(this)}
|
||||||
|
onClear={this.onClickDm.bind(this)}
|
||||||
|
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<a
|
||||||
|
className="f9 pointer green2 gray4-d"
|
||||||
|
onClick={this.onClickDm.bind(this)}>
|
||||||
|
DM
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
className="dib f9 pointer gray4-d"
|
className="dib f9 pointer gray4-d"
|
||||||
onClick={this.onClickJoin.bind(this)}>
|
onClick={this.onClickJoin.bind(this)}>
|
||||||
|
Loading…
Reference in New Issue
Block a user