diff --git a/components/app.js b/components/app.js index a03684a..3987749 100644 --- a/components/app.js +++ b/components/app.js @@ -13,6 +13,7 @@ import AuthForm from "./auth-form.js"; import RegisterForm from "./register-form.js"; import VerifyForm from "./verify-form.js"; import SettingsForm from "./settings-form.js"; +import SwitcherForm from "./switcher-form.js"; import Composer from "./composer.js"; import ScrollManager from "./scroll-manager.js"; import Dialog from "./dialog.js"; @@ -226,6 +227,7 @@ export default class App extends Component { this.handleOpenSettingsClick = this.handleOpenSettingsClick.bind(this); this.handleSettingsChange = this.handleSettingsChange.bind(this); this.handleSettingsDisconnect = this.handleSettingsDisconnect.bind(this); + this.handleSwitchSubmit = this.handleSwitchSubmit.bind(this); this.state.settings = { ...this.state.settings, @@ -1903,6 +1905,13 @@ export default class App extends Component { this.disconnectAll(); } + handleSwitchSubmit(buf) { + this.dismissDialog(); + if (buf) { + this.switchBuffer(buf); + } + } + componentDidMount() { this.baseTitle = document.title; setupKeybindings(this); @@ -2090,6 +2099,17 @@ export default class App extends Component { `; break; + case "switch": + dialog = html` + <${Dialog} title="Switch to a channel or user" onDismiss=${this.dismissDialog}> + <${SwitcherForm} + buffers=${this.state.buffers} + servers=${this.state.servers} + bouncerNetworks=${this.state.bouncerNetworks} + onSubmit=${this.handleSwitchSubmit}/> + + `; + break; } let error = null; diff --git a/components/switcher-form.js b/components/switcher-form.js new file mode 100644 index 0000000..8838191 --- /dev/null +++ b/components/switcher-form.js @@ -0,0 +1,141 @@ +import { html, Component } from "../lib/index.js"; +import { BufferType, getBufferURL, getServerName } from "../state.js"; + +class SwitcherItem extends Component { + constructor(props) { + super(props); + + this.handleClick = this.handleClick.bind(this); + } + + handleClick(event) { + event.preventDefault(); + this.props.onClick(); + } + + render() { + let class_ = this.props.selected ? "selected" : ""; + + return html` +
  • + + + ${getServerName(this.props.server, this.props.bouncerNetwork)} + + ${this.props.buffer.name} + +
  • + `; + } +} + +export default class SwitcherForm extends Component { + state = { + query: "", + selected: 0, + }; + + constructor(props) { + super(props); + + this.handleInput = this.handleInput.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + this.handleKeyUp = this.handleKeyUp.bind(this); + } + + getSuggestions() { + let query = this.state.query.toLowerCase(); + + let l = []; + for (let buf of this.props.buffers.values()) { + if (buf.type === BufferType.SERVER) { + continue; + } + if (query !== "" && !buf.name.toLowerCase().includes(query)) { + continue; + } + l.push(buf); + if (l.length >= 20) { + break; + } + } + return l; + } + + handleInput(event) { + let target = event.target; + this.setState({ [target.name]: target.value }); + } + + handleSubmit(event) { + event.preventDefault(); + this.props.onSubmit(this.getSuggestions()[this.state.selected]); + } + + handleKeyUp(event) { + switch (event.key) { + case "ArrowUp": + event.stopPropagation(); + this.move(-1); + break; + case "ArrowDown": + event.stopPropagation(); + this.move(1); + break; + } + } + + move(delta) { + let numSuggestions = this.getSuggestions().length; + this.setState((state) => { + return { + selected: (state.selected + delta + numSuggestions) % numSuggestions, + }; + }); + } + + render() { + let items = this.getSuggestions().map((buf, i) => { + let server = this.props.servers.get(buf.server); + + let bouncerNetwork = null; + if (server.bouncerNetID) { + bouncerNetwork = this.props.bouncerNetworks.get(server.bouncerNetID); + } + + return html` + <${SwitcherItem} + buffer=${buf} + server=${server} + bouncerNetwork=${bouncerNetwork} + selected=${this.state.selected === i} + onClick=${() => this.props.onSubmit(buf)} + /> + `; + }); + + return html` +
    + + +
    + `; + } +} diff --git a/keybindings.js b/keybindings.js index dc76c5a..c8c4984 100644 --- a/keybindings.js +++ b/keybindings.js @@ -94,6 +94,14 @@ export const keybindings = [ } }, }, + { + key: "k", + ctrlKey: true, + description: "Switch to a buffer", + execute: (app) => { + app.openDialog("switch"); + }, + }, ]; export function setup(app) { diff --git a/style.css b/style.css index 57857ec..2e7728c 100644 --- a/style.css +++ b/style.css @@ -352,7 +352,8 @@ form input[type="text"], form input[type="username"], form input[type="password"], form input[type="url"], -form input[type="email"] { +form input[type="email"], +form input[type="search"] { box-sizing: border-box; width: 100%; font-family: inherit; @@ -561,6 +562,29 @@ kbd { border-radius: 3px; } +ul.switcher-list { + list-style-type: none; + margin: 0; + padding: 0; + margin-top: 10px; +} +ul.switcher-list li a { + display: inline-block; + width: 100%; + padding: 5px 10px; + margin: 4px 0; + box-sizing: border-box; + text-decoration: none; + color: inherit; +} +ul.switcher-list li a.selected { + background-color: rgba(0, 0, 0, 0.1); +} +ul.switcher-list .server { + float: right; + opacity: 0.8; +} + @media (prefers-color-scheme: dark) { html { scrollbar-color: var(--gray) transparent; @@ -588,7 +612,8 @@ kbd { form input[type="username"], form input[type="password"], form input[type="url"], - form input[type="email"] { + form input[type="email"], + form input[type="search"] { color: #ffffff; background: var(--sidebar-background); border: 1px solid #495057; @@ -598,7 +623,8 @@ kbd { form input[type="username"]:focus, form input[type="password"]:focus, form input[type="url"]:focus, - form input[type="email"]:focus { + form input[type="email"]:focus, + form input[type="search"]:focus { outline: 0; border-color: #3897ff; } @@ -677,6 +703,10 @@ kbd { border: 1px solid var(--outline-color); box-shadow: inset 0 -1px 0 var(--outline-color); } + + ul.switcher-list li a.selected { + background-color: rgba(255, 255, 255, 0.1); + } } @media (max-width: 640px) {