diff --git a/pkg/interface/chat/src/js/components/lib/chat-input.js b/pkg/interface/chat/src/js/components/lib/chat-input.js
index 67cc62ed0..292f09578 100644
--- a/pkg/interface/chat/src/js/components/lib/chat-input.js
+++ b/pkg/interface/chat/src/js/components/lib/chat-input.js
@@ -2,9 +2,9 @@ import React, { Component } from 'react';
import _ from 'lodash';
import moment from 'moment';
import Mousetrap from 'mousetrap';
-import cn from 'classnames';
import { Sigil } from '/components/lib/icons/sigil';
+import { ShipSearch } from '/components/lib/ship-search';
import { uuid, uxToHex, hexToRgba } from '/lib/util';
@@ -25,77 +25,6 @@ function getAdvance(a, b) {
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 (
-
- {state.patpSuggestions.length !== 0 && (
-
- )}
+
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}
+ >
+
+ {nickname && (
+
+ {nickname}
+
+ )}
+
{"~" + ship}
+
{status}
+
+ );
+}
+
+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 (
+
+ {suggestions.slice(0, 5).map(ship => (
+
+ ))}
+
+ );
+ }
+}
+
+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 (
+
+
+
+
+ );
+ }
+}
+
+// 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]);
+// }
diff --git a/pkg/interface/chat/src/js/components/new-dm.js b/pkg/interface/chat/src/js/components/new-dm.js
index f88a3777a..684a81274 100644
--- a/pkg/interface/chat/src/js/components/new-dm.js
+++ b/pkg/interface/chat/src/js/components/new-dm.js
@@ -101,10 +101,6 @@ export class NewDmScreen extends Component {
render() {
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 (
New DM
-
With who?
-
-
{
const ship = props.match.params.ship;
diff --git a/pkg/interface/chat/src/js/components/sidebar.js b/pkg/interface/chat/src/js/components/sidebar.js
index 497ad4dbd..82863bf25 100644
--- a/pkg/interface/chat/src/js/components/sidebar.js
+++ b/pkg/interface/chat/src/js/components/sidebar.js
@@ -5,21 +5,35 @@ import Welcome from '/components/lib/welcome.js';
import { alphabetiseAssociations } from '../lib/util';
import { SidebarInvite } from '/components/lib/sidebar-invite';
import { GroupItem } from '/components/lib/group-item';
+import { ShipSearchInput } from '/components/lib/ship-search';
export class Sidebar extends Component {
+
+ constructor() {
+ super();
+ this.state = {
+ dmOverlay: false
+ };
+ }
onClickNew() {
this.props.history.push('/~chat/new');
}
onClickDm() {
- this.props.history.push('/~chat/new/dm');
+ this.setState(({ dmOverlay }) => ({ dmOverlay: !dmOverlay }) )
}
onClickJoin() {
this.props.history.push('/~chat/join')
}
+ goDm(ship) {
+ this.setState({ dmOverlay: false }, () => {
+ this.props.history.push(`/~chat/new/dm/~${ship}`)
+ });
+ }
+
render() {
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 (
New Chat
+
+
+ { state.dmOverlay && (
+
+ )}
DM
+
+