From 24ca01a10c4b4597d2d362d734bce6c9ae5a9b8d Mon Sep 17 00:00:00 2001 From: James Acklin Date: Sat, 3 Apr 2021 17:30:18 -0400 Subject: [PATCH 01/18] interface: adds clickable instructions to ImageInput, removes truncation in settings fixes urbit/landscape#695, fixes urbit/landscape#581 --- .../components/lib/BackgroundPicker.tsx | 20 +++-- .../src/views/components/ImageInput.tsx | 74 ++++++++++++++----- 2 files changed, 68 insertions(+), 26 deletions(-) diff --git a/pkg/interface/src/views/apps/settings/components/lib/BackgroundPicker.tsx b/pkg/interface/src/views/apps/settings/components/lib/BackgroundPicker.tsx index 698843ae4..26b7a0f50 100644 --- a/pkg/interface/src/views/apps/settings/components/lib/BackgroundPicker.tsx +++ b/pkg/interface/src/views/apps/settings/components/lib/BackgroundPicker.tsx @@ -1,32 +1,36 @@ import React, { ReactElement } from 'react'; import { - Box, Text, Row, Label, Col, - ManagedRadioButtonField as Radio, + ManagedRadioButtonField as Radio } from '@tlon/indigo-react'; import GlobalApi from '~/logic/api/global'; import { ImageInput } from '~/views/components/ImageInput'; import { ColorInput } from '~/views/components/ColorInput'; -import { StorageState } from '~/types'; export type BgType = 'none' | 'url' | 'color'; export function BackgroundPicker({ bgType, bgUrl, - api, + api }: { bgType: BgType; bgUrl?: string; api: GlobalApi; }): ReactElement { const rowSpace = { my: 0, alignItems: 'center' }; - const colProps = { my: 3, mr: 4, gapY: 1 }; + const colProps = { + my: 3, + mr: 4, + gapY: 1, + minWidth: '266px', + width: ['100%', '288px'] + }; return ( @@ -40,7 +44,7 @@ export function BackgroundPicker({ id="bgUrl" placeholder="Drop or upload a file, or paste a link here" name="bgUrl" - url={bgUrl || ""} + url={bgUrl || ''} /> @@ -48,13 +52,13 @@ export function BackgroundPicker({ Set a hex-based background - + diff --git a/pkg/interface/src/views/components/ImageInput.tsx b/pkg/interface/src/views/components/ImageInput.tsx index 21f40e4cc..159266a01 100644 --- a/pkg/interface/src/views/components/ImageInput.tsx +++ b/pkg/interface/src/views/components/ImageInput.tsx @@ -8,10 +8,10 @@ import { Button, Label, ErrorLabel, - BaseInput + BaseInput, + Text } from '@tlon/indigo-react'; -import { StorageState } from '~/types'; import useStorage from '~/logic/lib/useStorage'; type ImageInputProps = Parameters[0] & { @@ -21,12 +21,9 @@ type ImageInputProps = Parameters[0] & { }; export function ImageInput(props: ImageInputProps): ReactElement { - const { id, label, caption, placeholder } = props; - + const { id, label, caption } = props; const { uploadDefault, canUpload, uploading } = useStorage(); - const [field, meta, { setValue, setError }] = useField(id); - const ref = useRef(null); const onImageUpload = useCallback(async () => { @@ -43,10 +40,55 @@ export function ImageInput(props: ImageInputProps): ReactElement { } }, [ref.current, uploadDefault, canUpload, setValue]); - const onClick = useCallback(() => { + const clickUploadButton = useCallback(() => { ref.current?.click(); }, [ref]); + const Prompt = () => ( + + Paste a link here, or{' '} + + upload + {' '} + a file + + ); + + const Uploading = () => ( + + Uploading... + + ); + + const ErrorRetry = () => ( + + Error, please{' '} + + {' '} + retry + + + ); + return ( @@ -55,25 +97,21 @@ export function ImageInput(props: ImageInputProps): ReactElement { {caption} ) : null} - + + {!field.value && !uploading && meta.error === undefined ? () : null} + {uploading && meta.error === undefined ? () : null} + {meta.touched && meta.error !== undefined ? () : null} {canUpload && ( <> + display='none' + onClick={clickUploadButton} + /> Date: Sun, 11 Apr 2021 17:26:35 -0400 Subject: [PATCH 02/18] leap: prettier js/tsx fixes urbit/landscape#598 --- .../src/views/components/leap/Omnibox.tsx | 368 ++++++++++-------- .../src/views/components/leap/OmniboxInput.js | 41 +- .../views/components/leap/OmniboxResult.js | 259 ++++++++---- 3 files changed, 410 insertions(+), 258 deletions(-) diff --git a/pkg/interface/src/views/components/leap/Omnibox.tsx b/pkg/interface/src/views/components/leap/Omnibox.tsx index 6884a1692..9f578f2c9 100644 --- a/pkg/interface/src/views/components/leap/Omnibox.tsx +++ b/pkg/interface/src/views/components/leap/Omnibox.tsx @@ -1,4 +1,10 @@ -import React, { useMemo, useRef, useCallback, useEffect, useState } from 'react'; +import React, { + useMemo, + useRef, + useCallback, + useEffect, + useState, +} from 'react'; import { useLocation, useHistory } from 'react-router-dom'; import * as ob from 'urbit-ob'; import Mousetrap from 'mousetrap'; @@ -13,9 +19,9 @@ import { deSig } from '~/logic/lib/util'; import { withLocalState } from '~/logic/state/local'; import defaultApps from '~/logic/lib/default-apps'; -import {useOutsideClick} from '~/logic/lib/useOutsideClick'; -import {Portal} from '../Portal'; -import useSettingsState, {SettingsState} from '~/logic/state/settings'; +import { useOutsideClick } from '~/logic/lib/useOutsideClick'; +import { Portal } from '../Portal'; +import useSettingsState, { SettingsState } from '~/logic/state/settings'; import { Tile } from '~/types'; import useContactState from '~/logic/state/contact'; import useGroupState from '~/logic/state/group'; @@ -30,22 +36,29 @@ interface OmniboxProps { notifications: number; } -const SEARCHED_CATEGORIES = ['commands', 'ships', 'other', 'groups', 'subscriptions', 'apps']; +const SEARCHED_CATEGORIES = [ + 'commands', + 'ships', + 'other', + 'groups', + 'subscriptions', + 'apps', +]; const settingsSel = (s: SettingsState) => s.leap; export function Omnibox(props: OmniboxProps) { const location = useLocation(); const history = useHistory(); const leapConfig = useSettingsState(settingsSel); - const omniboxRef = useRef(null) + const omniboxRef = useRef(null); const inputRef = useRef(null); const [query, setQuery] = useState(''); const [selected, setSelected] = useState<[] | [string, string]>([]); - const contactState = useContactState(state => state.contacts); - const notifications = useHarkState(state => state.notifications); - const invites = useInviteState(state => state.invites); - const tiles = useLaunchState(state => state.tiles); + const contactState = useContactState((state) => state.contacts); + const notifications = useHarkState((state) => state.notifications); + const invites = useInviteState((state) => state.invites); + const tiles = useLaunchState((state) => state.tiles); const contacts = useMemo(() => { const maybeShip = `~${deSig(query)}`; @@ -54,13 +67,14 @@ export function Omnibox(props: OmniboxProps) { : contactState; }, [contactState, query]); - const groups = useGroupState(state => state.groups); - const associations = useMetadataState(state => state.associations); + const groups = useGroupState((state) => state.groups); + const associations = useMetadataState((state) => state.associations); - const selectedGroup = useMemo( - () => location.pathname.startsWith('/~landscape/ship/') - ? '/' + location.pathname.split('/').slice(2,5).join('/') - : null, + const selectedGroup = useMemo( + () => + location.pathname.startsWith('/~landscape/ship/') + ? '/' + location.pathname.split('/').slice(2, 5).join('/') + : null, [location.pathname] ); @@ -71,16 +85,9 @@ export function Omnibox(props: OmniboxProps) { tiles, selectedGroup, groups, - leapConfig, + leapConfig ); - }, [ - selectedGroup, - leapConfig, - contacts, - associations, - groups, - tiles - ]); + }, [selectedGroup, leapConfig, contacts, associations, groups, tiles]); const onOutsideClick = useCallback(() => { props.show && props.toggle(); @@ -90,7 +97,7 @@ export function Omnibox(props: OmniboxProps) { // handle omnibox show useEffect(() => { - if(!props.show) { + if (!props.show) { return; } Mousetrap.bind('escape', props.toggle); @@ -104,29 +111,37 @@ export function Omnibox(props: OmniboxProps) { }, [props.show]); const initialResults = useMemo(() => { - return new Map(SEARCHED_CATEGORIES.map((category) => { - if (category === 'other') { - return ['other', index.get('other').filter(({ app }) => app !== 'tutorial')]; - } - return [category, []]; - })); + return new Map( + SEARCHED_CATEGORIES.map((category) => { + if (category === 'other') { + return [ + 'other', + index.get('other').filter(({ app }) => app !== 'tutorial'), + ]; + } + return [category, []]; + }) + ); }, [index]); const results = useMemo(() => { - if(query.length <= 1) { + if (query.length <= 1) { return initialResults; } const q = query.toLowerCase(); const resultsMap = new Map(); SEARCHED_CATEGORIES.map((category) => { const categoryIndex = index.get(category); - resultsMap.set(category, + resultsMap.set( + category, categoryIndex.filter((result) => { return ( result.title.toLowerCase().includes(q) || result.link.toLowerCase().includes(q) || result.app.toLowerCase().includes(q) || - (result.host !== null ? result.host.toLowerCase().includes(q) : false) + (result.host !== null + ? result.host.toLowerCase().includes(q) + : false) ); }) ); @@ -134,21 +149,26 @@ export function Omnibox(props: OmniboxProps) { return resultsMap; }, [query, index]); - const navigate = useCallback((app: string, link: string) => { - props.toggle(); - if (defaultApps.includes(app.toLowerCase()) - || app === 'profile' - || app === 'messages' - || app === 'tutorial' - || app === 'Links' - || app === 'Terminal' - || app === 'home' - || app === 'inbox') { - history.push(link); - } else { - window.location.href = link; - } - }, [history, props.toggle]); + const navigate = useCallback( + (app: string, link: string) => { + props.toggle(); + if ( + defaultApps.includes(app.toLowerCase()) || + app === 'profile' || + app === 'messages' || + app === 'tutorial' || + app === 'Links' || + app === 'Terminal' || + app === 'home' || + app === 'inbox' + ) { + history.push(link); + } else { + window.location.href = link; + } + }, + [history, props.toggle] + ); const setPreviousSelected = useCallback(() => { const flattenedResults = Array.from(results.values()).flat(); @@ -193,55 +213,57 @@ export function Omnibox(props: OmniboxProps) { } }, [selected, results]); - const control = useCallback((evt) => { - if (evt.key === 'Escape') { - if (query.length > 0) { - setQuery(''); - return; - } else if (props.show) { - props.toggle(); - return; + const control = useCallback( + (evt) => { + if (evt.key === 'Escape') { + if (query.length > 0) { + setQuery(''); + return; + } else if (props.show) { + props.toggle(); + return; + } } - } - if ( - evt.key === 'ArrowUp' || - (evt.shiftKey && evt.key === 'Tab')) { + if (evt.key === 'ArrowUp' || (evt.shiftKey && evt.key === 'Tab')) { evt.preventDefault(); - setPreviousSelected(); - return; - } - if (evt.key === 'ArrowDown' || evt.key === 'Tab') { - evt.preventDefault(); - setNextSelected(); - return; - } - if (evt.key === 'Enter') { - evt.preventDefault(); - if (selected.length) { - navigate(selected[0], selected[1]); - } else if (Array.from(results.values()).flat().length === 0) { + setPreviousSelected(); return; - } else { - navigate( - Array.from(results.values()).flat()[0].app, - Array.from(results.values()).flat()[0].link); } - } - }, [ - props.toggle, - selected, - navigate, - query, - props.show, - results, - setPreviousSelected, - setNextSelected - ]); + if (evt.key === 'ArrowDown' || evt.key === 'Tab') { + evt.preventDefault(); + setNextSelected(); + return; + } + if (evt.key === 'Enter') { + evt.preventDefault(); + if (selected.length) { + navigate(selected[0], selected[1]); + } else if (Array.from(results.values()).flat().length === 0) { + return; + } else { + navigate( + Array.from(results.values()).flat()[0].app, + Array.from(results.values()).flat()[0].link + ); + } + } + }, + [ + props.toggle, + selected, + navigate, + query, + props.show, + results, + setPreviousSelected, + setNextSelected, + ] + ); useEffect(() => { const flattenedResultLinks = Array.from(results.values()) .flat() - .map(result => [result.app, result.link]); + .map((result) => [result.app, result.link]); if (!flattenedResultLinks.includes(selected)) { setSelected(flattenedResultLinks[0] || []); } @@ -252,88 +274,104 @@ export function Omnibox(props: OmniboxProps) { }, []); // Sort Omnibox results alphabetically - const sortResults = (a: Record<'title', string>, b: Record<'title', string>) => { + const sortResults = ( + a: Record<'title', string>, + b: Record<'title', string> + ) => { // Do not sort unless searching (preserves order of menu actions) - if (query === '') { return 0 }; - if (a.title < b.title) { return -1 }; - if (a.title > b.title) { return 1 }; + if (query === '') { + return 0; + } + if (a.title < b.title) { + return -1; + } + if (a.title > b.title) { + return 1; + } return 0; - } + }; const renderResults = useCallback(() => { - return - {SEARCHED_CATEGORIES - .map(category => Object({ category, categoryResults: results.get(category) })) - .filter(category => category.categoryResults.length > 0) - .map(({ category, categoryResults }, i) => { - const categoryTitle = (category === 'other') - ? null : {category.charAt(0).toUpperCase() + category.slice(1)}; - const sel = selected?.length ? selected[1] : ''; - return ( - {categoryTitle} - {categoryResults - .sort(sortResults) - .map((result, i2) => ( - navigate(result.app, result.link)} - selected={sel} - /> - ))} - - ); - }) - } - ; + return ( + + {SEARCHED_CATEGORIES.map((category) => + Object({ category, categoryResults: results.get(category) }) + ) + .filter((category) => category.categoryResults.length > 0) + .map(({ category, categoryResults }, i) => { + const categoryTitle = + category === 'other' ? null : ( + + + {category.charAt(0).toUpperCase() + category.slice(1)} + + + ); + const sel = selected?.length ? selected[1] : ''; + return ( + + {categoryTitle} + {categoryResults.sort(sortResults).map((result, i2) => ( + navigate(result.app, result.link)} + selected={sel} + /> + ))} + + ); + })} + + ); }, [results, navigate, selected, contactState, notifications, invites]); return ( - - - + + { + omniboxRef.current = el; + }} + > + { - omniboxRef.current = el; -}} - > - { - inputRef.current = el; -}} - control={e => control(e)} - search={search} - query={query} - /> - {renderResults()} - - - - - ); - } + inputRef.current = el; + }} + control={(e) => control(e)} + search={search} + query={query} + /> + {renderResults()} + + + + + ); +} export default withLocalState(Omnibox, ['toggleOmnibox', 'omniboxShown']); diff --git a/pkg/interface/src/views/components/leap/OmniboxInput.js b/pkg/interface/src/views/components/leap/OmniboxInput.js index a4beb6a7c..20fa9be95 100644 --- a/pkg/interface/src/views/components/leap/OmniboxInput.js +++ b/pkg/interface/src/views/components/leap/OmniboxInput.js @@ -1,4 +1,3 @@ - import React, { Component } from 'react'; import { BaseInput } from '@tlon/indigo-react'; @@ -6,33 +5,31 @@ export class OmniboxInput extends Component { render() { const { props } = this; return ( - { - this.input = el; + { + this.input = el; if (el && document.activeElement.isSameNode(el)) { el.blur(); el.focus(); } - } - } - width='100%' - p='2' - backgroundColor='white' - color='black' - border='1px solid transparent' - borderRadius='2' - maxWidth='calc(600px - 1.15rem)' - fontSize='1' - style={{ boxSizing: 'border-box' }} - placeholder='Search...' - onKeyDown={props.control} - onChange={props.search} - spellCheck={false} - value={props.query} - /> + }} + width='100%' + p='2' + backgroundColor='white' + color='black' + border='1px solid transparent' + borderRadius='2' + maxWidth='calc(600px - 1.15rem)' + fontSize='1' + style={{ boxSizing: 'border-box' }} + placeholder='Search...' + onKeyDown={props.control} + onChange={props.search} + spellCheck={false} + value={props.query} + /> ); } } export default OmniboxInput; - diff --git a/pkg/interface/src/views/components/leap/OmniboxResult.js b/pkg/interface/src/views/components/leap/OmniboxResult.js index c44114688..ed862db59 100644 --- a/pkg/interface/src/views/components/leap/OmniboxResult.js +++ b/pkg/interface/src/views/components/leap/OmniboxResult.js @@ -13,7 +13,7 @@ export class OmniboxResult extends Component { super(props); this.state = { isSelected: false, - hovered: false + hovered: false, }; this.setHover = this.setHover.bind(this); this.result = React.createRef(); @@ -21,52 +21,143 @@ export class OmniboxResult extends Component { componentDidUpdate(prevProps) { const { props, state } = this; - if (prevProps && + if ( + prevProps && !state.hovered && prevProps.selected !== props.selected && props.selected === props.link - ) { - this.result.current.scrollIntoView({ block: 'nearest' }); - } + ) { + this.result.current.scrollIntoView({ block: 'nearest' }); + } } getIcon(icon, selected, link, invites, notifications, text, color) { - const iconFill = (this.state.hovered || (selected === link)) ? 'white' : 'black'; - const bulletFill = (this.state.hovered || (selected === link)) ? 'white' : 'blue'; + const iconFill = + this.state.hovered || selected === link ? 'white' : 'black'; + const bulletFill = + this.state.hovered || selected === link ? 'white' : 'blue'; - const inviteCount = [].concat(...Object.values(invites).map(obj => Object.values(obj))); + const inviteCount = [].concat( + ...Object.values(invites).map((obj) => Object.values(obj)) + ); let graphic =
; - if (defaultApps.includes(icon.toLowerCase()) - || icon.toLowerCase() === 'links' - || icon.toLowerCase() === 'terminal') - { - icon = (icon === 'Link') ? 'Collection' : - (icon === 'Terminal') ? 'Dojo' : icon; - graphic = ; + if ( + defaultApps.includes(icon.toLowerCase()) || + icon.toLowerCase() === 'links' || + icon.toLowerCase() === 'terminal' + ) { + icon = + icon === 'Link' ? 'Collection' : icon === 'Terminal' ? 'Dojo' : icon; + graphic = ( + + ); } else if (icon === 'inbox') { - graphic = - - {(notifications > 0 || inviteCount.length > 0) && ( - - )} - ; + graphic = ( + + + {(notifications > 0 || inviteCount.length > 0) && ( + + )} + + ); } else if (icon === 'logout') { - graphic = ; + graphic = ( + + ); } else if (icon === 'profile') { text = text.startsWith('Profile') ? window.ship : text; - graphic = ; + graphic = ( + + ); } else if (icon === 'home') { - graphic = ; + graphic = ( + + ); } else if (icon === 'notifications') { - graphic = ; + graphic = ( + + ); } else if (icon === 'messages') { - graphic = ; + graphic = ( + + ); } else if (icon === 'tutorial') { - graphic = ; - } - else { - graphic = ; + graphic = ( + + ); + } else { + graphic = ( + + ); } return graphic; @@ -77,53 +168,79 @@ export class OmniboxResult extends Component { } render() { - const { icon, text, subtext, link, navigate, selected, invites, notificationsCount, contacts } = this.props; + const { + icon, + text, + subtext, + link, + navigate, + selected, + invites, + notificationsCount, + contacts, + } = this.props; - const color = contacts?.[text] ? `#${uxToHex(contacts[text].color)}` : "#000000"; - const graphic = this.getIcon(icon, selected, link, invites, notificationsCount, text, color); + const color = contacts?.[text] + ? `#${uxToHex(contacts[text].color)}` + : '#000000'; + const graphic = this.getIcon( + icon, + selected, + link, + invites, + notificationsCount, + text, + color + ); return ( this.setHover(true)} - onMouseLeave={() => this.setHover(false)} - backgroundColor={ - this.state.hovered || selected === link ? 'blue' : 'white' - } - onClick={navigate} - width="100%" - justifyContent="space-between" - ref={this.result} + py='2' + px='2' + cursor='pointer' + onMouseEnter={() => this.setHover(true)} + onMouseLeave={() => this.setHover(false)} + backgroundColor={ + this.state.hovered || selected === link ? 'blue' : 'white' + } + onClick={navigate} + width='100%' + justifyContent='space-between' + ref={this.result} > - - {graphic} - - {text.startsWith("~") ? cite(text) : text} - + {graphic} + + {text.startsWith('~') ? cite(text) : text} + - {subtext} @@ -136,4 +253,4 @@ export default withState(OmniboxResult, [ [useInviteState], [useHarkState, ['notificationsCount']], [useContactState] -]); \ No newline at end of file +]); From 4a3bc6fa22cce7063bbf11d2135bc55113067aba Mon Sep 17 00:00:00 2001 From: James Acklin Date: Sun, 11 Apr 2021 21:58:25 -0400 Subject: [PATCH 03/18] leap: set selection to currently-hovered item fixes urbit/landscape#598 --- .../src/views/components/leap/Omnibox.tsx | 34 +++++++++++-------- .../views/components/leap/OmniboxResult.js | 3 +- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/pkg/interface/src/views/components/leap/Omnibox.tsx b/pkg/interface/src/views/components/leap/Omnibox.tsx index 9f578f2c9..cc62fe776 100644 --- a/pkg/interface/src/views/components/leap/Omnibox.tsx +++ b/pkg/interface/src/views/components/leap/Omnibox.tsx @@ -3,15 +3,13 @@ import React, { useRef, useCallback, useEffect, - useState, + useState } from 'react'; import { useLocation, useHistory } from 'react-router-dom'; import * as ob from 'urbit-ob'; import Mousetrap from 'mousetrap'; import { Box, Row, Text } from '@tlon/indigo-react'; -import { Associations, Contacts, Groups, Invites } from '@urbit/api'; - import makeIndex from '~/logic/lib/omnibox'; import OmniboxInput from './OmniboxInput'; import OmniboxResult from './OmniboxResult'; @@ -22,7 +20,6 @@ import defaultApps from '~/logic/lib/default-apps'; import { useOutsideClick } from '~/logic/lib/useOutsideClick'; import { Portal } from '../Portal'; import useSettingsState, { SettingsState } from '~/logic/state/settings'; -import { Tile } from '~/types'; import useContactState from '~/logic/state/contact'; import useGroupState from '~/logic/state/group'; import useHarkState from '~/logic/state/hark'; @@ -46,7 +43,7 @@ const SEARCHED_CATEGORIES = [ ]; const settingsSel = (s: SettingsState) => s.leap; -export function Omnibox(props: OmniboxProps) { +export function Omnibox(props: OmniboxProps): ReactElement { const location = useLocation(); const history = useHistory(); const leapConfig = useSettingsState(settingsSel); @@ -55,10 +52,10 @@ export function Omnibox(props: OmniboxProps) { const [query, setQuery] = useState(''); const [selected, setSelected] = useState<[] | [string, string]>([]); - const contactState = useContactState((state) => state.contacts); - const notifications = useHarkState((state) => state.notifications); - const invites = useInviteState((state) => state.invites); - const tiles = useLaunchState((state) => state.tiles); + const contactState = useContactState(state => state.contacts); + const notifications = useHarkState(state => state.notifications); + const invites = useInviteState(state => state.invites); + const tiles = useLaunchState(state => state.tiles); const contacts = useMemo(() => { const maybeShip = `~${deSig(query)}`; @@ -67,8 +64,8 @@ export function Omnibox(props: OmniboxProps) { : contactState; }, [contactState, query]); - const groups = useGroupState((state) => state.groups); - const associations = useMetadataState((state) => state.associations); + const groups = useGroupState(state => state.groups); + const associations = useMetadataState(state => state.associations); const selectedGroup = useMemo( () => @@ -256,14 +253,14 @@ export function Omnibox(props: OmniboxProps) { props.show, results, setPreviousSelected, - setNextSelected, + setNextSelected ] ); useEffect(() => { const flattenedResultLinks = Array.from(results.values()) .flat() - .map((result) => [result.app, result.link]); + .map(result => [result.app, result.link]); if (!flattenedResultLinks.includes(selected)) { setSelected(flattenedResultLinks[0] || []); } @@ -291,6 +288,12 @@ export function Omnibox(props: OmniboxProps) { return 0; }; + // Handler to set selection on mouse hover + const setSelection = (app, link) => { + // TODO: Cancel this event if we are navigating by keyboard + setSelected([app, link]); + }; + const renderResults = useCallback(() => { return ( - {SEARCHED_CATEGORIES.map((category) => + {SEARCHED_CATEGORIES.map(category => Object({ category, categoryResults: results.get(category) }) ) - .filter((category) => category.categoryResults.length > 0) + .filter(category => category.categoryResults.length > 0) .map(({ category, categoryResults }, i) => { const categoryTitle = category === 'other' ? null : ( @@ -325,6 +328,7 @@ export function Omnibox(props: OmniboxProps) { subtext={result.host} link={result.link} navigate={() => navigate(result.app, result.link)} + setSelection={() => setSelection(result.app, result.link)} selected={sel} /> ))} diff --git a/pkg/interface/src/views/components/leap/OmniboxResult.js b/pkg/interface/src/views/components/leap/OmniboxResult.js index ed862db59..ea0c1b402 100644 --- a/pkg/interface/src/views/components/leap/OmniboxResult.js +++ b/pkg/interface/src/views/components/leap/OmniboxResult.js @@ -178,6 +178,7 @@ export class OmniboxResult extends Component { invites, notificationsCount, contacts, + setSelection } = this.props; const color = contacts?.[text] @@ -198,7 +199,7 @@ export class OmniboxResult extends Component { py='2' px='2' cursor='pointer' - onMouseEnter={() => this.setHover(true)} + onMouseEnter={() => this.setHover(true), setSelection} onMouseLeave={() => this.setHover(false)} backgroundColor={ this.state.hovered || selected === link ? 'blue' : 'white' From 1f70fdbf2da89f479684ffd426423a735e6908e3 Mon Sep 17 00:00:00 2001 From: James Acklin Date: Mon, 12 Apr 2021 14:51:41 -0400 Subject: [PATCH 04/18] leap: set selection on mousemove, not hover (account for scrolling) fixes urbit/landscape#598 --- pkg/interface/src/views/components/leap/Omnibox.tsx | 12 +++++------- .../src/views/components/leap/OmniboxResult.js | 4 ++-- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/pkg/interface/src/views/components/leap/Omnibox.tsx b/pkg/interface/src/views/components/leap/Omnibox.tsx index cc62fe776..71f2224b4 100644 --- a/pkg/interface/src/views/components/leap/Omnibox.tsx +++ b/pkg/interface/src/views/components/leap/Omnibox.tsx @@ -210,6 +210,10 @@ export function Omnibox(props: OmniboxProps): ReactElement { } }, [selected, results]); + const setSelection = (app, link) => { + setSelected([app, link]); + }; + const control = useCallback( (evt) => { if (evt.key === 'Escape') { @@ -288,12 +292,6 @@ export function Omnibox(props: OmniboxProps): ReactElement { return 0; }; - // Handler to set selection on mouse hover - const setSelection = (app, link) => { - // TODO: Cancel this event if we are navigating by keyboard - setSelected([app, link]); - }; - const renderResults = useCallback(() => { return ( { inputRef.current = el; }} - control={(e) => control(e)} + control={e => control(e)} search={search} query={query} /> diff --git a/pkg/interface/src/views/components/leap/OmniboxResult.js b/pkg/interface/src/views/components/leap/OmniboxResult.js index ea0c1b402..0d899c9f3 100644 --- a/pkg/interface/src/views/components/leap/OmniboxResult.js +++ b/pkg/interface/src/views/components/leap/OmniboxResult.js @@ -13,7 +13,7 @@ export class OmniboxResult extends Component { super(props); this.state = { isSelected: false, - hovered: false, + hovered: false }; this.setHover = this.setHover.bind(this); this.result = React.createRef(); @@ -199,7 +199,7 @@ export class OmniboxResult extends Component { py='2' px='2' cursor='pointer' - onMouseEnter={() => this.setHover(true), setSelection} + onMouseMove={() => setSelection()} onMouseLeave={() => this.setHover(false)} backgroundColor={ this.state.hovered || selected === link ? 'blue' : 'white' From 92c39dc526dc8a95a0d012ec9009fc00bfaddf00 Mon Sep 17 00:00:00 2001 From: James Acklin Date: Mon, 12 Apr 2021 15:12:03 -0400 Subject: [PATCH 05/18] leap: hide pointer while navigating by keyboard fixes urbit/landscape#598 --- pkg/interface/src/views/components/leap/Omnibox.tsx | 9 +++++++-- pkg/interface/src/views/components/leap/OmniboxResult.js | 3 ++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/pkg/interface/src/views/components/leap/Omnibox.tsx b/pkg/interface/src/views/components/leap/Omnibox.tsx index 71f2224b4..17005edb9 100644 --- a/pkg/interface/src/views/components/leap/Omnibox.tsx +++ b/pkg/interface/src/views/components/leap/Omnibox.tsx @@ -39,7 +39,7 @@ const SEARCHED_CATEGORIES = [ 'other', 'groups', 'subscriptions', - 'apps', + 'apps' ]; const settingsSel = (s: SettingsState) => s.leap; @@ -56,6 +56,7 @@ export function Omnibox(props: OmniboxProps): ReactElement { const notifications = useHarkState(state => state.notifications); const invites = useInviteState(state => state.invites); const tiles = useLaunchState(state => state.tiles); + const [leapCursor, setLeapCursor] = useState('pointer'); const contacts = useMemo(() => { const maybeShip = `~${deSig(query)}`; @@ -113,7 +114,7 @@ export function Omnibox(props: OmniboxProps): ReactElement { if (category === 'other') { return [ 'other', - index.get('other').filter(({ app }) => app !== 'tutorial'), + index.get('other').filter(({ app }) => app !== 'tutorial') ]; } return [category, []]; @@ -211,6 +212,7 @@ export function Omnibox(props: OmniboxProps): ReactElement { }, [selected, results]); const setSelection = (app, link) => { + setLeapCursor('pointer'); setSelected([app, link]); }; @@ -228,11 +230,13 @@ export function Omnibox(props: OmniboxProps): ReactElement { if (evt.key === 'ArrowUp' || (evt.shiftKey && evt.key === 'Tab')) { evt.preventDefault(); setPreviousSelected(); + setLeapCursor('none'); return; } if (evt.key === 'ArrowDown' || evt.key === 'Tab') { evt.preventDefault(); setNextSelected(); + setLeapCursor('none'); return; } if (evt.key === 'Enter') { @@ -325,6 +329,7 @@ export function Omnibox(props: OmniboxProps): ReactElement { text={result.title} subtext={result.host} link={result.link} + cursor={leapCursor} navigate={() => navigate(result.app, result.link)} setSelection={() => setSelection(result.app, result.link)} selected={sel} diff --git a/pkg/interface/src/views/components/leap/OmniboxResult.js b/pkg/interface/src/views/components/leap/OmniboxResult.js index 0d899c9f3..d49a971e5 100644 --- a/pkg/interface/src/views/components/leap/OmniboxResult.js +++ b/pkg/interface/src/views/components/leap/OmniboxResult.js @@ -173,6 +173,7 @@ export class OmniboxResult extends Component { text, subtext, link, + cursor, navigate, selected, invites, @@ -198,7 +199,7 @@ export class OmniboxResult extends Component { setSelection()} onMouseLeave={() => this.setHover(false)} backgroundColor={ From d6727c042c7af60b187ffb5d1dba9a9a1201a378 Mon Sep 17 00:00:00 2001 From: James Acklin Date: Mon, 12 Apr 2021 16:42:09 -0400 Subject: [PATCH 06/18] profile: imageinput instance fixes fixes urbit/landscape#695 --- .../src/views/apps/profile/components/EditProfile.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/interface/src/views/apps/profile/components/EditProfile.tsx b/pkg/interface/src/views/apps/profile/components/EditProfile.tsx index 0648d7326..d053007d2 100644 --- a/pkg/interface/src/views/apps/profile/components/EditProfile.tsx +++ b/pkg/interface/src/views/apps/profile/components/EditProfile.tsx @@ -64,20 +64,20 @@ export function ProfileHeaderImageEdit(props: any): ReactElement { {contact?.cover ? (
{editCover ? ( - + ) : ( - )}
) : ( - + )} ); From e80101ab0a316d64f4bd9ea4143821a8fdca8985 Mon Sep 17 00:00:00 2001 From: James Acklin Date: Mon, 12 Apr 2021 22:23:19 -0400 Subject: [PATCH 07/18] leap: yank own ~patp from results fixes urbit/landscape#598 --- pkg/interface/src/views/components/leap/Omnibox.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/interface/src/views/components/leap/Omnibox.tsx b/pkg/interface/src/views/components/leap/Omnibox.tsx index 17005edb9..0e5ef9db6 100644 --- a/pkg/interface/src/views/components/leap/Omnibox.tsx +++ b/pkg/interface/src/views/components/leap/Omnibox.tsx @@ -144,6 +144,9 @@ export function Omnibox(props: OmniboxProps): ReactElement { }) ); }); + resultsMap.set('ships', resultsMap.get('ships').filter(ship => ( + ship.title === `~${window.ship}` ? null : (ship) + ))); return resultsMap; }, [query, index]); From 62ab69f319b5e0605ad0cea0c1c0e0d5414c2f98 Mon Sep 17 00:00:00 2001 From: James Acklin Date: Tue, 13 Apr 2021 08:22:39 -0400 Subject: [PATCH 08/18] leap: yank window.ship from contacts fixes urbit/landscape#598 --- pkg/interface/src/views/components/leap/Omnibox.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pkg/interface/src/views/components/leap/Omnibox.tsx b/pkg/interface/src/views/components/leap/Omnibox.tsx index 0e5ef9db6..bdd5c39ae 100644 --- a/pkg/interface/src/views/components/leap/Omnibox.tsx +++ b/pkg/interface/src/views/components/leap/Omnibox.tsx @@ -8,6 +8,7 @@ import React, { import { useLocation, useHistory } from 'react-router-dom'; import * as ob from 'urbit-ob'; import Mousetrap from 'mousetrap'; +import { omit } from 'lodash'; import { Box, Row, Text } from '@tlon/indigo-react'; import makeIndex from '~/logic/lib/omnibox'; @@ -60,9 +61,10 @@ export function Omnibox(props: OmniboxProps): ReactElement { const contacts = useMemo(() => { const maybeShip = `~${deSig(query)}`; + const selflessContactState = omit(contactState, `~${window.ship}`); return ob.isValidPatp(maybeShip) - ? { ...contactState, [maybeShip]: {} } - : contactState; + ? { ...selflessContactState, [maybeShip]: {} } + : selflessContactState; }, [contactState, query]); const groups = useGroupState(state => state.groups); @@ -144,9 +146,6 @@ export function Omnibox(props: OmniboxProps): ReactElement { }) ); }); - resultsMap.set('ships', resultsMap.get('ships').filter(ship => ( - ship.title === `~${window.ship}` ? null : (ship) - ))); return resultsMap; }, [query, index]); From 8e866f326263ddd1f1cfe1faf8ce107439d0ffdc Mon Sep 17 00:00:00 2001 From: James Acklin Date: Tue, 13 Apr 2021 16:34:50 -0400 Subject: [PATCH 09/18] leap: belt-and-suspenders patp match fixes urbit/landscape#598 --- pkg/interface/src/views/components/leap/Omnibox.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/interface/src/views/components/leap/Omnibox.tsx b/pkg/interface/src/views/components/leap/Omnibox.tsx index bdd5c39ae..a6a09ebb7 100644 --- a/pkg/interface/src/views/components/leap/Omnibox.tsx +++ b/pkg/interface/src/views/components/leap/Omnibox.tsx @@ -62,7 +62,7 @@ export function Omnibox(props: OmniboxProps): ReactElement { const contacts = useMemo(() => { const maybeShip = `~${deSig(query)}`; const selflessContactState = omit(contactState, `~${window.ship}`); - return ob.isValidPatp(maybeShip) + return ob.isValidPatp(maybeShip) && maybeShip !== `~${window.ship}` ? { ...selflessContactState, [maybeShip]: {} } : selflessContactState; }, [contactState, query]); From 685ef12e8fd8441f959699117ab21a6fbe8df79a Mon Sep 17 00:00:00 2001 From: Pax Dickinson Date: Wed, 14 Apr 2021 15:19:31 -0400 Subject: [PATCH 10/18] docker: add exec to final urbit invocation --- nix/pkgs/docker-image/default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/pkgs/docker-image/default.nix b/nix/pkgs/docker-image/default.nix index 467c06ac4..2b63d0498 100644 --- a/nix/pkgs/docker-image/default.nix +++ b/nix/pkgs/docker-image/default.nix @@ -42,7 +42,7 @@ let dirs=( $dirnames ) dirname=''${dirnames[0]} - urbit $ttyflag -p ${toString amesPort} $dirname + exec urbit $ttyflag -p ${toString amesPort} $dirname ''; From bbafcc6d6fe3c9e3f3bbf92934ad0fe786174a3f Mon Sep 17 00:00:00 2001 From: Tyler Brown Cifu Shuster Date: Wed, 14 Apr 2021 13:03:36 -0700 Subject: [PATCH 11/18] sidebar: typesafe notifications check --- .../src/views/landscape/components/Sidebar/Apps.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/interface/src/views/landscape/components/Sidebar/Apps.tsx b/pkg/interface/src/views/landscape/components/Sidebar/Apps.tsx index 7326e210f..522c40186 100644 --- a/pkg/interface/src/views/landscape/components/Sidebar/Apps.tsx +++ b/pkg/interface/src/views/landscape/components/Sidebar/Apps.tsx @@ -18,7 +18,11 @@ export function useGraphModule( } const notifications = graphUnreads?.[s]?.['/']?.notifications; - if ( notifications > 0 ) { + if ( + notifications && + ((typeof notifications === 'number' && notifications > 0) + || notifications.length) + ) { return 'notification'; } From 95b4f4007b9fe96cf9e8d8f70229107eb60a8c78 Mon Sep 17 00:00:00 2001 From: James Acklin Date: Wed, 14 Apr 2021 16:31:21 -0400 Subject: [PATCH 12/18] imageInput: eliminate closures as components fixes urbit/landscape#695 --- .../src/views/components/ImageInput.tsx | 136 ++++++++++-------- 1 file changed, 77 insertions(+), 59 deletions(-) diff --git a/pkg/interface/src/views/components/ImageInput.tsx b/pkg/interface/src/views/components/ImageInput.tsx index 159266a01..447adf40c 100644 --- a/pkg/interface/src/views/components/ImageInput.tsx +++ b/pkg/interface/src/views/components/ImageInput.tsx @@ -7,7 +7,6 @@ import { Row, Button, Label, - ErrorLabel, BaseInput, Text } from '@tlon/indigo-react'; @@ -20,6 +19,71 @@ type ImageInputProps = Parameters[0] & { placeholder?: string; }; +const prompt = (field, uploading, meta, clickUploadButton) => { + if (!field.value && !uploading && meta.error === undefined) { + return ( + + Paste a link here, or{' '} + + upload + {' '} + a file + + ); + } + return null; +}; + +const uploadingStatus = (uploading, meta) => { + if (uploading && meta.error === undefined) { + return ( + + Uploading... + + ); + } + return null; +}; + +const errorRetry = (meta, uploading, clickUploadButton) => { + if (meta.error !== undefined) { + return ( + + {meta.error}{', '}please{' '} + + retry + + + ); + } + return null; +}; + export function ImageInput(props: ImageInputProps): ReactElement { const { id, label, caption } = props; const { uploadDefault, canUpload, uploading } = useStorage(); @@ -43,52 +107,6 @@ export function ImageInput(props: ImageInputProps): ReactElement { const clickUploadButton = useCallback(() => { ref.current?.click(); }, [ref]); - - const Prompt = () => ( - - Paste a link here, or{' '} - - upload - {' '} - a file - - ); - - const Uploading = () => ( - - Uploading... - - ); - - const ErrorRetry = () => ( - - Error, please{' '} - - {' '} - retry - - - ); - return ( @@ -97,15 +115,18 @@ export function ImageInput(props: ImageInputProps): ReactElement { {caption} ) : null} - - {!field.value && !uploading && meta.error === undefined ? () : null} - {uploading && meta.error === undefined ? () : null} - {meta.touched && meta.error !== undefined ? () : null} - + + {prompt(field, uploading, meta, clickUploadButton)} + {uploadingStatus(uploading, meta)} + {errorRetry(meta, uploading, clickUploadButton)} + + + {canUpload && ( <>