diff --git a/pkg/interface/src/logic/lib/omnibox.js b/pkg/interface/src/logic/lib/omnibox.ts similarity index 92% rename from pkg/interface/src/logic/lib/omnibox.js rename to pkg/interface/src/logic/lib/omnibox.ts index 1870db33f..e20fd22a7 100644 --- a/pkg/interface/src/logic/lib/omnibox.js +++ b/pkg/interface/src/logic/lib/omnibox.ts @@ -10,8 +10,18 @@ const makeIndexes = () => new Map([ ['other', []] ]); +export interface OmniboxItem { + title: string; + link: string; + app: string; + host: string; + description: string; + shiftLink: string; + shiftDescription: string; +} + // result schematic -const result = function(title, link, app, host, description = 'Open', shiftLink = null, shiftDescription = null) { +const result = function(title: string, link: string, app: string, host: string, description = 'Open', shiftLink = null as string | null, shiftDescription = null as string | null): OmniboxItem { return { 'title': title, 'link': link, @@ -93,7 +103,7 @@ const otherIndex = function(config) { return other; }; -export default function index(contacts, associations, apps, currentGroup, groups, hide) { +export default function index(contacts, associations, apps, currentGroup, groups, hide): Map { const indexes = makeIndexes(); indexes.set('ships', shipIndex(contacts)); // all metadata from all apps is indexed @@ -117,7 +127,7 @@ export default function index(contacts, associations, apps, currentGroup, groups let app = each['app-name']; if (each['app-name'] === 'contacts') { app = 'groups'; - }; + } if (each['app-name'] === 'graph') { app = each.metadata.config.graph; @@ -159,4 +169,4 @@ export default function index(contacts, associations, apps, currentGroup, groups indexes.set('other', otherIndex(hide)); return indexes; -}; +} diff --git a/pkg/interface/src/logic/lib/tokenizeMessage.js b/pkg/interface/src/logic/lib/tokenizeMessage.js index 941742328..19f231316 100644 --- a/pkg/interface/src/logic/lib/tokenizeMessage.js +++ b/pkg/interface/src/logic/lib/tokenizeMessage.js @@ -5,7 +5,7 @@ const URL_REGEX = new RegExp(String(/^([^[\]]*?)(([\w\-\+]+:\/\/)[-a-zA-Z0-9:@;? const PATP_REGEX = /^([\s\S]*?)(~[a-z_-]+)([\s\S]*)/; -const GROUP_REGEX = new RegExp(String(/^([\s\S ]*?)(~[-a-z_]+\/[-a-z]+)([\s\S]*)/.source)); +const GROUP_REGEX = new RegExp(String(/^([\s\S ]*?)(~[-a-z_]+\/[-a-z0-9]+)([\s\S]*)/.source)); const convertToGroupRef = group => `web+urbitgraph://group/${group}`; @@ -33,9 +33,10 @@ const raceRegexes = (str) => { content = { url: link[2] }; } } - if(groupRef && groupRef[1].length < pfix?.length) { + const perma = parsePermalink(convertToGroupRef(groupRef?.[2])); + const [,,host] = perma?.group.split('/') ?? []; + if(groupRef && groupRef[1].length < pfix?.length && Boolean(perma) && urbitOb.isValidPatp(host)) { pfix = groupRef[1]; - const perma = parsePermalink(convertToGroupRef(groupRef[2])); content = permalinkToReference(perma); sfix = groupRef[3]; } diff --git a/pkg/interface/src/logic/lib/tokenizeMessage.test.js b/pkg/interface/src/logic/lib/tokenizeMessage.test.js index 835c9e71b..e65f139d1 100644 --- a/pkg/interface/src/logic/lib/tokenizeMessage.test.js +++ b/pkg/interface/src/logic/lib/tokenizeMessage.test.js @@ -113,4 +113,19 @@ describe('tokenizeMessage', () => { expect(text).toBe('. foo'); expect(url).toBe('https://tlon.io/test'); }); + + it('should ignore malformed group links', () => { + const example = 'test ~zoid/fakegroup'; + const [{ text }, ...rest] = tokenizeMessage(example); + expect(text).toBe(example); + expect(rest.length).toBe(0); + }); + it('should handle groups with numbers', () => { + const example = 'oh no, ~sampel/group-123-abc'; + + const [{ text }, { reference }] = tokenizeMessage(example); + expect(text).toBe('oh no, '); + expect(reference.group).toBe('/ship/~sampel/group-123-abc'); + }); }); + diff --git a/pkg/interface/src/logic/state/embed.tsx b/pkg/interface/src/logic/state/embed.tsx index 4911d00a0..3edd9bbb6 100644 --- a/pkg/interface/src/logic/state/embed.tsx +++ b/pkg/interface/src/logic/state/embed.tsx @@ -11,7 +11,7 @@ export interface EmbedState { fetch: (url: string) => Promise; } -const OEMBED_PROVIDER = 'http://noembed.com/embed'; +const OEMBED_PROVIDER = 'https://noembed.com/embed'; const useEmbedState = create((set, get) => ({ embeds: {}, diff --git a/pkg/interface/src/logic/state/local.tsx b/pkg/interface/src/logic/state/local.tsx index 9b6064106..c9a97304e 100644 --- a/pkg/interface/src/logic/state/local.tsx +++ b/pkg/interface/src/logic/state/local.tsx @@ -26,6 +26,7 @@ export interface LocalState { setTutorialRef: (el: HTMLElement | null) => void; dark: boolean; mobile: boolean; + mdBreak: boolean; background: BackgroundConfig; omniboxShown: boolean; suspendedFocus?: HTMLElement; @@ -45,6 +46,7 @@ export const selectLocalState = const useLocalState = create(persist((set, get) => ({ dark: false, mobile: false, + mdBreak: false, background: undefined, theme: 'auto', hideAvatars: false, diff --git a/pkg/interface/src/stories/LinkBlockItem.stories.tsx b/pkg/interface/src/stories/LinkBlockItem.stories.tsx index b3178109d..927712e17 100644 --- a/pkg/interface/src/stories/LinkBlockItem.stories.tsx +++ b/pkg/interface/src/stories/LinkBlockItem.stories.tsx @@ -22,6 +22,7 @@ export const Image = () => ( ( /> ( - - + + ); @@ -75,6 +75,7 @@ Fallback.parameters = { export const Audio = () => ( ( ().gas([ makeComment( 'ridlur-figbud', - moment().hour(12).minute(34).valueOf(), + Date.now() - 4*HOUR, nodeIndex, [{ text: 'Beautiful' }] ), makeComment( 'roslet-tanner', - moment().hour(12).minute(34).valueOf(), + Date.now() - 3*HOUR, nodeIndex, [{ text: 'where did you find this?' }] ), makeComment( 'fabled-faster', - moment().hour(12).minute(34).valueOf(), + Date.now() - 2*HOUR, + nodeIndex, + [{ text: 'I dont\'t remember lol' }] + ) + ]) +}; + +const twitterNode = { + post: { + index: '/170141184504850861030994857749504231212', + author: 'fabled-faster', + 'time-sent': 1609969377513, + signatures: [], + contents: [ + { text: 'LindyMan' }, + { + url: 'https://twitter.com/PaulSkallas/status/1388896550198317056' + } + ], + hash: null + }, + children: new BigIntOrderedMap().gas([ + makeComment( + 'ridlur-figbud', + Date.now() - 4*HOUR, + nodeIndex, + [{ text: 'Beautiful' }] + ), + makeComment( + 'roslet-tanner', + Date.now() - 3*HOUR, + nodeIndex, + [{ text: 'where did you find this?' }] + ), + + makeComment( + 'fabled-faster', + Date.now() - 2*HOUR, nodeIndex, [{ text: 'I dont\'t remember lol' }] ) @@ -60,7 +98,7 @@ export const Image = () => { s => s.associations.graph['/ship/~bitbet-bolbel/links'] ); return ( - + { ); }; + +export const Twitter = () => { + const association = useMetadataState( + s => s.associations.graph['/ship/~bitbet-bolbel/links'] + ); + return ( + + + + ); +}; Image.parameters = { design: { type: 'figma', diff --git a/pkg/interface/src/views/App.js b/pkg/interface/src/views/App.js index 7c8f028af..e60542773 100644 --- a/pkg/interface/src/views/App.js +++ b/pkg/interface/src/views/App.js @@ -83,15 +83,20 @@ class App extends React.Component { bootstrapApi(); this.props.getShallowChildren(`~${window.ship}`, 'dm-inbox'); const theme = this.getTheme(); - this.themeWatcher = window.matchMedia('(prefers-color-scheme: dark)'); - this.mobileWatcher = window.matchMedia(`(max-width: ${theme.breakpoints[0]})`); - this.themeWatcher.onchange = this.updateTheme; - this.mobileWatcher.onchange = this.updateMobile; setTimeout(() => { // Something about how the store works doesn't like changing it // before the app has actually rendered, hence the timeout. + this.themeWatcher = window.matchMedia('(prefers-color-scheme: dark)'); + this.mobileWatcher = window.matchMedia(`(max-width: ${theme.breakpoints[0]})`); + this.mediumWatcher = window.matchMedia(`(max-width: ${theme.breakpoints[1]})`); + // TODO: addListener is deprecated, but safari 13 requires it + this.themeWatcher.addListener(this.updateTheme); + this.mobileWatcher.addListener(this.updateMobile); + this.mediumWatcher.addListener(this.updateMedium); + this.updateMobile(this.mobileWatcher); this.updateTheme(this.themeWatcher); + this.updateMedium(this.mediumWatcher); }, 500); this.props.getBaseHash(); this.props.getRuntimeLag(); // TODO consider polling periodically @@ -105,8 +110,9 @@ class App extends React.Component { } componentWillUnmount() { - this.themeWatcher.onchange = undefined; - this.mobileWatcher.onchange = undefined; + this.themeWatcher.removeListener(this.updateTheme); + this.mobileWatcher.removeListener(this.updateMobile); + this.mediumWatcher.removeListener(this.updateMedium); } updateTheme(e) { @@ -121,6 +127,12 @@ class App extends React.Component { }); } + updateMedium = (e) => { + this.props.set((state) => { + state.mdBreak = e.matches; + }); + } + getTheme() { const { props } = this; return ((props.dark && props?.display?.theme == 'auto') || diff --git a/pkg/interface/src/views/apps/chat/DmResource.tsx b/pkg/interface/src/views/apps/chat/DmResource.tsx index c3d447646..be71dfd4d 100644 --- a/pkg/interface/src/views/apps/chat/DmResource.tsx +++ b/pkg/interface/src/views/apps/chat/DmResource.tsx @@ -84,7 +84,7 @@ export function DmResource(props: DmResourceProps) { if (newer) { const index = dm.peekLargest()?.[0]; if (!index) { - return true; + return false; } await getYoungerSiblings( `~${window.ship}`, @@ -96,7 +96,7 @@ export function DmResource(props: DmResourceProps) { } else { const index = dm.peekSmallest()?.[0]; if (!index) { - return true; + return false; } await getOlderSiblings( `~${window.ship}`, diff --git a/pkg/interface/src/views/apps/links/components/LinkBlocks.tsx b/pkg/interface/src/views/apps/links/components/LinkBlocks.tsx index 755168967..a378298bf 100644 --- a/pkg/interface/src/views/apps/links/components/LinkBlocks.tsx +++ b/pkg/interface/src/views/apps/links/components/LinkBlocks.tsx @@ -32,7 +32,7 @@ export function LinkBlocks(props: LinkBlocksProps) { const [linkSize, setLinkSize] = useState(250); const linkSizePx = `${linkSize}px`; - const isMobile = useLocalState(s => s.mobile); + const isMobile = useLocalState(s => s.mobile || s.mdBreak); const colCount = useMemo(() => (isMobile ? 2 : 4), [isMobile]); const bind = useResize( useCallback( @@ -46,12 +46,13 @@ export function LinkBlocks(props: LinkBlocksProps) { ); useEffect(() => { - const unreads = useHarkState.getState() - .unreads.graph?.[association.resource]?.['/']?.unreads || new Set(); - Array.from((unreads as Set)).forEach((u) => { + const unreads = + useHarkState.getState().unreads.graph?.[association.resource]?.['/'] + ?.unreads || new Set(); + Array.from(unreads as Set).forEach((u) => { airlock.poke(markEachAsRead(association.resource, '/', u)); }); -}, [association.resource]); + }, [association.resource]); const orm = useMemo(() => { const nodes = [null, ...Array.from(props.graph)]; @@ -62,12 +63,12 @@ export function LinkBlocks(props: LinkBlocksProps) { return [bigInt(i), chunk]; }) ); - }, [props.graph]); + }, [props.graph, colCount]); const renderItem = useCallback( React.forwardRef(({ index }, ref) => { - const chunk = orm.get(index); - const space = [3,4]; + const chunk = orm.get(index) ?? []; + const space = [3, 3, 4]; return ( { if (!block) { return ( - + ); } const [i, node] = block; @@ -115,7 +113,13 @@ export function LinkBlocks(props: LinkBlocksProps) { ); return ( - + - + s.leap; +const CAT_LIMIT = 6; export function Omnibox(props: OmniboxProps): ReactElement { const location = useLocation(); @@ -111,7 +114,7 @@ export function Omnibox(props: OmniboxProps): ReactElement { }, [props.show]); const initialResults = useMemo(() => { - return new Map( + return new Map( SEARCHED_CATEGORIES.map((category) => { if (category === 'other') { return [ @@ -129,7 +132,7 @@ export function Omnibox(props: OmniboxProps): ReactElement { return initialResults; } const q = query.toLowerCase(); - const resultsMap = new Map(); + const resultsMap = new Map(); SEARCHED_CATEGORIES.map((category) => { const categoryIndex = index.get(category); resultsMap.set( @@ -175,7 +178,7 @@ export function Omnibox(props: OmniboxProps): ReactElement { ); const setPreviousSelected = useCallback(() => { - const flattenedResults = Array.from(results.values()).flat(); + const flattenedResults = Array.from(results.values()).map(f.take(CAT_LIMIT)).flat(); const totalLength = flattenedResults.length; if (selected.length) { const currentIndex = flattenedResults.indexOf( @@ -198,7 +201,7 @@ export function Omnibox(props: OmniboxProps): ReactElement { }, [results, selected]); const setNextSelected = useCallback(() => { - const flattenedResults = Array.from(results.values()).flat(); + const flattenedResults = Array.from(results.values()).map(f.take(CAT_LIMIT)).flat(); if (selected.length) { const currentIndex = flattenedResults.indexOf( // @ts-ignore unclear how to give this spread a return signature @@ -309,13 +312,15 @@ export function Omnibox(props: OmniboxProps): ReactElement { return ( {SEARCHED_CATEGORIES.map(category => - Object({ category, categoryResults: results.get(category) }) + ({ + category, + categoryResults: _.take(results.get(category).sort(sortResults), CAT_LIMIT) + }) ) .filter(category => category.categoryResults.length > 0) .map(({ category, categoryResults }, i) => { @@ -331,7 +336,7 @@ export function Omnibox(props: OmniboxProps): ReactElement { return ( {categoryTitle} - {categoryResults.sort(sortResults).map((result, i2) => ( + {categoryResults.map((result, i2) => (