Merge pull request #5071 from urbit/lf/fixes-more

interface: handle malformed group links, fix mixed content errors on embeds
This commit is contained in:
matildepark 2021-07-05 21:08:05 -04:00 committed by GitHub
commit b1b78bce5e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 154 additions and 48 deletions

View File

@ -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<string, OmniboxItem[]> {
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;
};
}

View File

@ -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];
}

View File

@ -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');
});
});

View File

@ -11,7 +11,7 @@ export interface EmbedState {
fetch: (url: string) => Promise<any>;
}
const OEMBED_PROVIDER = 'http://noembed.com/embed';
const OEMBED_PROVIDER = 'https://noembed.com/embed';
const useEmbedState = create<EmbedState>((set, get) => ({
embeds: {},

View File

@ -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<LocalStateZus>(persist((set, get) => ({
dark: false,
mobile: false,
mdBreak: false,
background: undefined,
theme: 'auto',
hideAvatars: false,

View File

@ -22,6 +22,7 @@ export const Image = () => (
<Row flexWrap="wrap" m="2" width="700px" backgroundColor="white">
<LinkBlockItem
summary
size="250px"
m="2"
node={createLink(
'Gas',
@ -30,6 +31,7 @@ export const Image = () => (
/>
<LinkBlockItem
summary
size="250px"
m="2"
node={createLink(
'Ocean',
@ -57,10 +59,8 @@ Image.parameters = {
export const Fallback = () => (
<Col gapY="2" p="2" width="500px" backgroundColor="white">
<LinkBlockItem
node={createLink('', 'https://www.are.na/edouard-urcades/edouard')}
/>
<LinkBlockItem node={createLink('', 'https://thejaymo.net')} />
<LinkBlockItem size="250px" node={createLink('', 'https://www.are.na/edouard-urcades/edouard')} />
<LinkBlockItem size="250px" node={createLink('', 'https://thejaymo.net')} />
</Col>
);
@ -75,6 +75,7 @@ Fallback.parameters = {
export const Audio = () => (
<Col gapY="2" p="2" width="500px" backgroundColor="white">
<LinkBlockItem
size="250px"
node={createLink(
'Artist · Track',
'https://rovnys-public.s3.amazonaws.com/urbit-from-the-outside-in-1.m4a'
@ -94,12 +95,14 @@ Audio.parameters = {
export const Youtube = () => (
<Col gapY="2" p="2" width="500px" backgroundColor="white">
<LinkBlockItem
size="400px"
node={createLink(
'Artist · Track',
'https://www.youtube.com/watch?v=M04AKTCDavc&t=1s'
)}
/>
<LinkBlockItem
size="250px"
summary
node={createLink(
'Artist · Track',

View File

@ -8,7 +8,8 @@ import BigIntOrderedMap from '@urbit/api/lib/BigIntOrderedMap';
import { GraphNode } from '@urbit/api';
import useMetadataState from '~/logic/state/metadata';
import { makeComment } from '~/logic/lib/fixtures';
import moment from 'moment';
const HOUR = 60*60 * 1000;
export default {
title: 'Collections/LinkDetail',
@ -35,20 +36,57 @@ const node = {
children: new BigIntOrderedMap<GraphNode>().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<GraphNode>().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 (
<Box width="1166px" p="1" backgroundColor="white">
<Box width="100%" height="100%" p="1" backgroundColor="white">
<LinkDetail
baseUrl="/"
node={node}
@ -69,6 +107,21 @@ export const Image = () => {
</Box>
);
};
export const Twitter = () => {
const association = useMetadataState(
s => s.associations.graph['/ship/~bitbet-bolbel/links']
);
return (
<Box height="100%" width="100%" border="1" borderColor="lightGray" maxWidth="1166px" backgroundColor="white">
<LinkDetail
baseUrl="/"
node={twitterNode}
association={association}
/>
</Box>
);
};
Image.parameters = {
design: {
type: 'figma',

View File

@ -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') ||

View File

@ -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}`,

View File

@ -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<HTMLDivElement>(
useCallback(
@ -46,12 +46,13 @@ export function LinkBlocks(props: LinkBlocksProps) {
);
useEffect(() => {
const unreads = useHarkState.getState()
.unreads.graph?.[association.resource]?.['/']?.unreads || new Set<string>();
Array.from((unreads as Set<string>)).forEach((u) => {
const unreads =
useHarkState.getState().unreads.graph?.[association.resource]?.['/']
?.unreads || new Set<string>();
Array.from(unreads as Set<string>).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<any, any>(({ index }, ref) => {
const chunk = orm.get(index);
const space = [3,4];
const chunk = orm.get(index) ?? [];
const space = [3, 3, 4];
return (
<Row
@ -82,10 +83,7 @@ export function LinkBlocks(props: LinkBlocksProps) {
{chunk.map((block) => {
if (!block) {
return (
<LinkBlockInput
size={linkSizePx}
association={association}
/>
<LinkBlockInput size={linkSizePx} association={association} />
);
}
const [i, node] = block;
@ -115,7 +113,13 @@ export function LinkBlocks(props: LinkBlocksProps) {
);
return (
<Col overflowX="hidden" overflowY="auto" height="calc(100% - 48px)" {...bind}>
<Col
width="100%"
overflowX="hidden"
overflowY="auto"
height="calc(100% - 48px)"
{...bind}
>
<BlockScroller
origin="top"
offset={0}

View File

@ -21,7 +21,7 @@ export function LinkDetail(props: LinkDetailProps) {
return (
/* @ts-ignore indio props?? */
<Row height="100%" width="100%" flexDirection={['column', 'column', 'row']} {...rest}>
<LinkBlockItem minWidth="0" minHeight="0" maxHeight={['50%', '50%', '100%']} maxWidth={['100%', '100%', 'calc(100% - 350px)']} flexGrow={1} border={0} node={node} />
<LinkBlockItem minWidth="0" minHeight="0" height={['50%', '50%', '100%']} width={['100%', '100%', 'calc(100% - 350px)']} flexGrow={0} border={0} node={node} />
<Col
minHeight="0"
flexShrink={0}

View File

@ -184,6 +184,7 @@ const EmbedBox = styled.div<{ aspect?: number; iHeight?: number; iWidth?: number
width: max-content;
height: max-content;
max-height: 100%;
max-width: 100%;
flex-grow: 1;
` : `

View File

@ -1,6 +1,8 @@
import { Box, Row, Text } from '@tlon/indigo-react';
import { omit } from 'lodash';
import Mousetrap from 'mousetrap';
import _ from 'lodash';
import f from 'lodash/fp';
import React, {
ReactElement, useCallback,
useEffect, useMemo,
@ -11,7 +13,7 @@ import React, {
import { useHistory, useLocation } from 'react-router-dom';
import * as ob from 'urbit-ob';
import defaultApps from '~/logic/lib/default-apps';
import makeIndex from '~/logic/lib/omnibox';
import makeIndex, { OmniboxItem } from '~/logic/lib/omnibox';
import { useOutsideClick } from '~/logic/lib/useOutsideClick';
import { deSig } from '~/logic/lib/util';
import useContactState from '~/logic/state/contact';
@ -41,6 +43,7 @@ const SEARCHED_CATEGORIES = [
'apps'
];
const settingsSel = (s: SettingsState) => 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<string, OmniboxItem[]>(
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<string, OmniboxItem[]>();
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 (
<Box
maxHeight={['200px', '400px']}
overflowY='auto'
overflowX='hidden'
overflow='hidden'
borderBottomLeftRadius={2}
borderBottomRightRadius={2}
>
{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 (
<Box key={i} width='max(50vw, 300px)' maxWidth='700px'>
{categoryTitle}
{categoryResults.sort(sortResults).map((result, i2) => (
{categoryResults.map((result, i2) => (
<OmniboxResult
key={i2}
// @ts-ignore withHovering doesn't pass props