mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-12-11 08:55:23 +03:00
chat-fe: move new DMs to ship select popover
Refactored the patp autocomplete into a reusable component, and then used that to add the DM popover interface. Also introduces some performance improvements for the popover.
This commit is contained in:
parent
803c7b4816
commit
966155088c
@ -2,9 +2,9 @@ import React, { Component } from 'react';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import Mousetrap from 'mousetrap';
|
import Mousetrap from 'mousetrap';
|
||||||
import cn from 'classnames';
|
|
||||||
|
|
||||||
import { Sigil } from '/components/lib/icons/sigil';
|
import { Sigil } from '/components/lib/icons/sigil';
|
||||||
|
import { ShipSearch } from '/components/lib/ship-search';
|
||||||
|
|
||||||
import { uuid, uxToHex, hexToRgba } from '/lib/util';
|
import { uuid, uxToHex, hexToRgba } from '/lib/util';
|
||||||
|
|
||||||
@ -25,77 +25,6 @@ function getAdvance(a, b) {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChatInputSuggestion({ ship, contacts, selected, onSelect }) {
|
|
||||||
let contact = contacts[ship];
|
|
||||||
let color = "#000000";
|
|
||||||
let sigilClass = "v-mid mix-blend-diff"
|
|
||||||
let nickname;
|
|
||||||
let 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) {
|
||||||
@ -104,8 +33,7 @@ export class ChatInput extends Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
message: '',
|
message: '',
|
||||||
textareaHeight: DEFAULT_INPUT_HEIGHT,
|
textareaHeight: DEFAULT_INPUT_HEIGHT,
|
||||||
patpSuggestions: [],
|
patpSearch: ''
|
||||||
selectedSuggestion: null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.textareaRef = React.createRef();
|
this.textareaRef = React.createRef();
|
||||||
@ -113,13 +41,10 @@ export class ChatInput extends Component {
|
|||||||
this.messageSubmit = this.messageSubmit.bind(this);
|
this.messageSubmit = this.messageSubmit.bind(this);
|
||||||
this.messageChange = this.messageChange.bind(this);
|
this.messageChange = this.messageChange.bind(this);
|
||||||
|
|
||||||
this.onEnter = this.onEnter.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.clearSuggestions = this.clearSuggestions.bind(this);
|
|
||||||
|
|
||||||
// Call once per frame @ 60hz
|
// Call once per frame @ 60hz
|
||||||
this.textareaInput = _.debounce(this.textareaInput.bind(this), 16);
|
this.textareaInput = _.debounce(this.textareaInput.bind(this), 16);
|
||||||
@ -170,139 +95,77 @@ export class ChatInput extends Component {
|
|||||||
this.bindShortcuts();
|
this.bindShortcuts();
|
||||||
}
|
}
|
||||||
|
|
||||||
nextAutocompleteSuggestion(backward = false) {
|
|
||||||
const { patpSuggestions } = this.state;
|
|
||||||
let idx = patpSuggestions.findIndex(s => s === this.state.selectedSuggestion);
|
|
||||||
|
|
||||||
idx = backward ? idx - 1 : idx + 1;
|
|
||||||
idx = idx % patpSuggestions.length;
|
|
||||||
if(idx < 0) {
|
|
||||||
idx = patpSuggestions.length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ selectedSuggestion: patpSuggestions[idx] });
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
patpAutocomplete(message, fresh = false) {
|
patpAutocomplete(message, fresh = false) {
|
||||||
const match = /~([a-zA-Z\-]*)$/.exec(message);
|
const match = /~([a-zA-Z\-]*)$/.exec(message);
|
||||||
|
|
||||||
if (!match ) {
|
if (!match ) {
|
||||||
this.setState({ patpSuggestions: [] })
|
this.bindShortcuts();
|
||||||
|
this.setState({ patpSearch: '' })
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.unbindShortcuts();
|
||||||
|
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();
|
|
||||||
|
|
||||||
let newState = {
|
|
||||||
patpSuggestions: suggestions,
|
|
||||||
selectedSuggestion: suggestions[0]
|
|
||||||
};
|
|
||||||
|
|
||||||
this.setState(newState);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clearSuggestions() {
|
clearSearch() {
|
||||||
this.setState({
|
this.setState({
|
||||||
patpSuggestions: []
|
patpSearch: ''
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
completePatp(suggestion) {
|
completePatp(suggestion) {
|
||||||
|
this.bindShortcuts();
|
||||||
this.setState({
|
this.setState({
|
||||||
message: this.state.message.replace(
|
message: this.state.message.replace(
|
||||||
/[a-zA-Z\-]*$/,
|
/[a-zA-Z\-]*$/,
|
||||||
suggestion
|
suggestion
|
||||||
),
|
),
|
||||||
patpSuggestions: []
|
patpSearch: ''
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onEnter(e) {
|
|
||||||
if (this.state.patpSuggestions.length !== 0) {
|
|
||||||
this.completePatp(this.state.selectedSuggestion);
|
|
||||||
} else {
|
|
||||||
this.messageSubmit(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bindShortcuts() {
|
bindShortcuts() {
|
||||||
let mousetrap = Mousetrap(this.textareaRef.current);
|
if(!this.mousetrap) {
|
||||||
mousetrap.bind('enter', e => {
|
this.mousetrap = new Mousetrap(this.textareaRef.current);
|
||||||
|
}
|
||||||
|
this.mousetrap.bind('enter', e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
this.onEnter(e);
|
if(this.state.patpSearch.length === 0) {
|
||||||
|
this.messageSubmit();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
mousetrap.bind('tab', e => {
|
this.mousetrap.bind('tab', e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if(this.state.patpSuggestions.length === 0) {
|
if(this.state.patpSearch.length === 0) {
|
||||||
this.patpAutocomplete(this.state.message, true);
|
this.patpAutocomplete(this.state.message, true);
|
||||||
} else {
|
|
||||||
this.nextAutocompleteSuggestion(false);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
mousetrap.bind(['up', 'shift+tab'], e => {
|
|
||||||
if(this.state.patpSuggestions.length !== 0) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
this.nextAutocompleteSuggestion(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
mousetrap.bind('down', e => {
|
|
||||||
if(this.state.patpSuggestions.length !== 0) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
this.nextAutocompleteSuggestion(false)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
mousetrap.bind('esc', e => {
|
|
||||||
if(this.state.patpSuggestions.length !== 0) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
this.clearSuggestions();
|
|
||||||
}})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unbindShortcuts() {
|
||||||
|
if(!this.mousetrap) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.mousetrap.unbind('enter')
|
||||||
|
this.mousetrap.unbind('tab')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
messageChange(event) {
|
messageChange(event) {
|
||||||
const message = event.target.value;
|
const message = event.target.value;
|
||||||
this.setState({
|
this.setState({
|
||||||
message
|
message
|
||||||
});
|
});
|
||||||
|
|
||||||
const { patpSuggestions } = this.state;
|
const { patpSearch } = this.state;
|
||||||
if(patpSuggestions.length !== 0) {
|
if(patpSearch.length !== 0) {
|
||||||
this.patpAutocomplete(message, false);
|
this.patpAutocomplete(message, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -431,17 +294,24 @@ export class ChatInput extends Component {
|
|||||||
let sigilClass = !!props.ownerContact
|
let sigilClass = !!props.ownerContact
|
||||||
? "" : "mix-blend-diff";
|
? "" : "mix-blend-diff";
|
||||||
|
|
||||||
|
const candidates = _.chain(this.props.envelopes)
|
||||||
|
.defaultTo([])
|
||||||
|
.map("author")
|
||||||
|
.uniq()
|
||||||
|
.reverse()
|
||||||
|
.value();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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}
|
contacts={props.contacts}
|
||||||
selected={state.selectedSuggestion}
|
candidates={candidates}
|
||||||
contacts={props.contacts}
|
searchTerm={this.state.patpSearch}
|
||||||
/>
|
inputRef={this.textareaRef.current}
|
||||||
)}
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="fl"
|
className="fl"
|
||||||
|
338
pkg/interface/chat/src/js/components/lib/ship-search.js
Normal file
338
pkg/interface/chat/src/js/components/lib/ship-search.js
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
import React, { Component } from "react";
|
||||||
|
import _ from "lodash";
|
||||||
|
import urbitOb from "urbit-ob";
|
||||||
|
|
||||||
|
import cn from "classnames";
|
||||||
|
import { Sigil } from "/components/lib/icons/sigil";
|
||||||
|
import { hexToRgba, uxToHex, deSig } from "/lib/util";
|
||||||
|
|
||||||
|
function ShipSearchItem({ ship, contacts, selected, onSelect }) {
|
||||||
|
let contact = contacts[ship];
|
||||||
|
let color = "#000000";
|
||||||
|
let sigilClass = "v-mid mix-blend-diff";
|
||||||
|
let nickname;
|
||||||
|
let 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.bindShortcuts();
|
||||||
|
if (this.props.suggestEmpty) {
|
||||||
|
this.updateSuggestions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const { props } = this;
|
||||||
|
if (
|
||||||
|
props.searchTerm !== prevProps.searchTerm &&
|
||||||
|
props.searchTerm.startsWith(prevProps.searchTerm)
|
||||||
|
) {
|
||||||
|
this.updateSuggestions();
|
||||||
|
} else if (prevProps.searchTerm !== props.searchTerm) {
|
||||||
|
this.updateSuggestions(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevProps.inputRef !== props.inputRef) {
|
||||||
|
this.bindShortcuts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSuggestions(isStale = false) {
|
||||||
|
const needle = this.props.searchTerm;
|
||||||
|
if (needle.length === 0 && !this.props.suggestEmpty) {
|
||||||
|
this.unbindShortcuts();
|
||||||
|
this.setState({ suggestions: [] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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] });
|
||||||
|
}
|
||||||
|
|
||||||
|
bindShortcuts() {
|
||||||
|
if (!this.props.inputRef || this.state.bound) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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.onDismiss();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
unbindShortcuts() {
|
||||||
|
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 = React.createRef();
|
||||||
|
this.popoverRef = React.createRef();
|
||||||
|
|
||||||
|
this.search = this.search.bind(this);
|
||||||
|
|
||||||
|
this.onClick = this.onClick.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClick(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();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
document.addEventListener("mousedown", this.onClick);
|
||||||
|
document.addEventListener("touchstart", this.onClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
document.removeEventListener("mousedown", this.onClick);
|
||||||
|
document.removeEventListener("touchstart", this.onClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
search(e) {
|
||||||
|
const searchTerm = e.target.value;
|
||||||
|
this.setState({ searchTerm });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { state, props } = this;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={this.popoverRef}
|
||||||
|
style={{ top: "150%", left: "-80px" }}
|
||||||
|
className="b--gray2 b--solid ba absolute bg-white bg-gray0-d shadow-5"
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
style={{ resize: "none" }}
|
||||||
|
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.inputRef}
|
||||||
|
/>
|
||||||
|
<ShipSearch
|
||||||
|
contacts={props.contacts}
|
||||||
|
candidates={props.candidates}
|
||||||
|
searchTerm={deSig(state.searchTerm)}
|
||||||
|
inputRef={this.inputRef.current}
|
||||||
|
onSelect={props.onSelect}
|
||||||
|
onDismiss={props.onDismiss}
|
||||||
|
suggestEmpty
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// import { useEffect } from 'react';
|
||||||
|
|
||||||
|
// // via https:// usehooks.com/useOnClickOutside/
|
||||||
|
// export default function useOnClickOutside(ref, handler) {
|
||||||
|
// useEffect(() => {
|
||||||
|
// const listener = event => {
|
||||||
|
// // Do nothing if clicking ref's element or descendent elements
|
||||||
|
// if (!ref.current || ref.current.contains(event.target)) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// handler(event);
|
||||||
|
// };
|
||||||
|
|
||||||
|
// document.addEventListener('mousedown', listener);
|
||||||
|
// document.addEventListener('touchstart', listener);
|
||||||
|
|
||||||
|
// return () => {
|
||||||
|
// document.removeEventListener('mousedown', listener);
|
||||||
|
// document.removeEventListener('touchstart', listener);
|
||||||
|
// };
|
||||||
|
// }, [ref, handler]);
|
||||||
|
// }
|
@ -101,10 +101,6 @@ export class NewDmScreen extends Component {
|
|||||||
render() {
|
render() {
|
||||||
const { props, state } = this;
|
const { props, state } = this;
|
||||||
|
|
||||||
let createClasses = state.ship
|
|
||||||
? "pointer db f9 green2 bg-gray0-d ba pv3 ph4 b--green2"
|
|
||||||
: "pointer db f9 gray2 ba bg-gray0-d pa2 pv3 ph4 b--gray3";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
@ -117,26 +113,6 @@ export class NewDmScreen extends Component {
|
|||||||
</div>
|
</div>
|
||||||
<h2 className="mb3 f8">New DM</h2>
|
<h2 className="mb3 f8">New DM</h2>
|
||||||
<div className="w-100">
|
<div className="w-100">
|
||||||
<p className="f8 mt4 lh-copy db">With who?</p>
|
|
||||||
<InviteSearch
|
|
||||||
groups={{}}
|
|
||||||
contacts={props.contacts}
|
|
||||||
associations={props.associations}
|
|
||||||
groupResults={false}
|
|
||||||
shipResults={true}
|
|
||||||
disabled={!!state.ship}
|
|
||||||
invites={{
|
|
||||||
groups: [],
|
|
||||||
ships: state.ship ? [state.ship] : []
|
|
||||||
}}
|
|
||||||
setInvite={this.setInvite}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={this.onClickCreate}
|
|
||||||
className={createClasses + " mt4"}
|
|
||||||
>
|
|
||||||
Start Chat
|
|
||||||
</button>
|
|
||||||
<Spinner
|
<Spinner
|
||||||
awaiting={this.state.awaiting}
|
awaiting={this.state.awaiting}
|
||||||
classes="mt4"
|
classes="mt4"
|
||||||
|
@ -95,7 +95,7 @@ export class Root extends Component {
|
|||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
path="/~chat/new/dm/:ship?"
|
path="/~chat/new/dm/:ship"
|
||||||
render={props => {
|
render={props => {
|
||||||
const ship = props.match.params.ship;
|
const ship = props.match.params.ship;
|
||||||
|
|
||||||
|
@ -5,21 +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() {
|
onClickDm() {
|
||||||
this.props.history.push('/~chat/new/dm');
|
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;
|
||||||
|
|
||||||
@ -101,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
|
||||||
@ -112,11 +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)}
|
||||||
|
onDismiss={this.onClickDm.bind(this)}
|
||||||
|
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<a
|
<a
|
||||||
className="dib f9 pointer green2 gray4-d mr4"
|
className="f9 pointer green2 gray4-d"
|
||||||
onClick={this.onClickDm.bind(this)}>
|
onClick={this.onClickDm.bind(this)}>
|
||||||
DM
|
DM
|
||||||
</a>
|
</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