Merge branch 'master' into next/vere

This commit is contained in:
Jōshin 2022-03-18 16:49:12 -06:00
commit beb3ee8ec0
No known key found for this signature in database
GPG Key ID: A8BE5A9A521639D0
28 changed files with 35669 additions and 13884 deletions

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
16.14.0

View File

@ -159,6 +159,42 @@ so that I can type e.g. `git mu origin/foo 1337`.
If you're making a Vere release, just play it safe and update all the pills.
To produce multi pills, you will need to set up an environment with the
appropriate desks with the appropriate contents, doing something like the
following (where `> ` denotes an urbit command and `% ` denotes a unix shell
command):
```console
> |merge %garden our %base
> |merge %landscape our %base
> |merge %bitcoin our %base
> |merge %webterm our %base
> |mount %
> |mount %garden
> |mount %landscape
> |mount %bitcoin
> |mount %webterm
% rsync -avL --delete pkg/arvo/ zod/base/
% for desk in garden landscape bitcoin webterm; do \
rsync -avL --delete pkg/$desk/ zod/$desk/ \
done
> |commit %base
> |commit %garden
> |commit %landscape
> |commit %bitcoin
> |commit %webterm
> .multi/pill +solid %base %garden %landscape %bitcoin %webterm
> .brass-multi/pill +brass %base %garden %landscape %bitcoin %webterm
```
And then of course:
```console
> .solid/pill +solid
> .brass/pill +brass
> .ivory/pill +ivory
```
For an Urbit OS release, after all the merge commits, make a release with the
commit message "release: urbit-os-v1.0.xx". This commit should have up-to-date
artifacts from pkg/interface and a new version number in the desk.docket-0 of

View File

@ -1,6 +1,9 @@
{
"name": "root",
"private": true,
"engines": {
"node": "16.14.0"
},
"devDependencies": {
"eslint": "^7.29.0",
"husky": "^6.0.0",

View File

@ -1,10 +1,10 @@
:~ title+'System'
info+'An app launcher for Urbit.'
color+0xee.5432
glob-http+['https://bootstrap.urbit.org/glob-0v5.1o2c9.g1btf.nandl.703oh.40up1.glob' 0v5.1o2c9.g1btf.nandl.703oh.40up1]
glob-http+['https://bootstrap.urbit.org/glob-0vcggb9.v4sgp.jbo30.t34mk.58i52.glob' 0vcggb9.v4sgp.jbo30.t34mk.58i52]
::glob-ames+~zod^0v0
base+'grid'
version+[1 0 3]
version+[1 1 0]
website+'https://tlon.io'
license+'MIT'
==

48997
pkg/grid/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -15,8 +15,10 @@
"tsc": "tsc --noEmit"
},
"dependencies": {
"@radix-ui/react-checkbox": "^0.1.5",
"@radix-ui/react-dialog": "^0.0.20",
"@radix-ui/react-dropdown-menu": "^0.0.23",
"@radix-ui/react-icons": "^1.1.0",
"@radix-ui/react-polymorphic": "^0.0.13",
"@radix-ui/react-portal": "^0.0.15",
"@radix-ui/react-toggle": "^0.0.10",
@ -36,6 +38,9 @@
"postcss-import": "^14.0.2",
"query-string": "^7.0.1",
"react": "^17.0.2",
"react-dnd": "^15.1.1",
"react-dnd-html5-backend": "^15.1.2",
"react-dnd-touch-backend": "^15.1.1",
"react-dom": "^17.0.2",
"react-error-boundary": "^3.1.3",
"react-router-dom": "^5.2.0",

View File

@ -0,0 +1,35 @@
import React, { useState } from 'react';
import classNames from 'classnames';
import * as RadixCheckbox from '@radix-ui/react-checkbox';
import { CheckIcon } from '@radix-ui/react-icons';
export const Checkbox: React.FC<RadixCheckbox.CheckboxProps> = ({
defaultChecked,
checked,
onCheckedChange,
disabled,
className,
children
}) => {
const [on, setOn] = useState(defaultChecked);
const isControlled = !!onCheckedChange;
const proxyChecked = isControlled ? checked : on;
const proxyOnCheckedChange = isControlled ? onCheckedChange : setOn;
return (
<div className="flex content-center space-x-2">
<RadixCheckbox.Root
className={classNames('default-ring rounded-lg bg-white h-7 w-7', className)}
checked={proxyChecked}
onCheckedChange={proxyOnCheckedChange}
disabled={disabled}
id="checkbox"
>
<RadixCheckbox.Indicator className="flex justify-center">
<CheckIcon className="text-black" />
</RadixCheckbox.Indicator>
</RadixCheckbox.Root>
<label htmlFor="checkbox">{children}</label>
</div>
);
};

View File

@ -0,0 +1,20 @@
import React from 'react';
export const Lock = (props: React.SVGProps<SVGSVGElement>) => (
<svg
width="10"
height="12"
viewBox="-11 -8 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8 5H9C9.55228 5 10 5.44772 10 6V11C10 11.5523 9.55229 12 9 12H1C0.447716 12 0 11.5523 0 11V6C0 5.44772 0.447715 5 1 5H2V3C2 1.34315 3.34315 0 5 0C6.65685 0 8 1.34315 8 3V5ZM7 5V3C7 1.89543 6.10457 1 5 1C3.89543 1 3 1.89543 3 3V5H7ZM3 6H9V11H1V6H2H3Z"
className="fill-current"
strokeMiterlimit="10"
/>
</svg>
);

View File

@ -5,6 +5,7 @@ import classNames from 'classnames';
import { NotificationPrefs } from './preferences/NotificationPrefs';
import { SystemUpdatePrefs } from './preferences/SystemUpdatePrefs';
import { InterfacePrefs } from './preferences/InterfacePrefs';
import { SecurityPrefs } from './preferences/SecurityPrefs';
import { useCharges } from '../state/docket';
import { AppPrefs } from './preferences/AppPrefs';
import { DocketImage } from '../components/DocketImage';
@ -14,6 +15,7 @@ import { LeftArrow } from '../components/icons/LeftArrow';
import { System } from '../components/icons/System';
import { Interface } from '../components/icons/Interface';
import { Notifications } from '../components/icons/Notifications';
import { Lock } from '../components/icons/Lock';
import { getAppName } from '../state/util';
interface SystemPreferencesSectionProps {
@ -77,11 +79,11 @@ export const SystemPreferences = (props: RouteComponentProps<{ submenu: string }
FallbackComponent={ErrorAlert}
onReset={() => history.push('/leap/system-preferences')}
>
<div className="sm:flex h-full overflow-y-auto">
<div className="h-full overflow-y-auto sm:flex">
<Route exact={isMobile} path={match.url}>
<aside className="flex-none self-start w-full sm:w-auto min-w-60 py-4 sm:py-8 font-semibold text-black sm:text-gray-600 border-r-2 border-gray-50">
<aside className="self-start flex-none w-full py-4 font-semibold text-black border-r-2 sm:w-auto min-w-60 sm:py-8 sm:text-gray-600 border-gray-50">
<nav className="px-2 sm:px-6">
<h2 className="sm:hidden h3 mb-4 px-2">System Preferences</h2>
<h2 className="px-2 mb-4 sm:hidden h3">System Preferences</h2>
<ul className="space-y-1">
<SystemPreferencesSection
url={subUrl('notifications')}
@ -101,6 +103,10 @@ export const SystemPreferences = (props: RouteComponentProps<{ submenu: string }
<Interface className="w-8 h-8 mr-3 bg-gray-100 rounded-md" />
Interface Settings
</SystemPreferencesSection>
<SystemPreferencesSection url={subUrl('security')} active={matchSub('security')}>
<Lock className="w-8 h-8 mr-3 bg-gray-100 rounded-md" />
Security
</SystemPreferencesSection>
</ul>
</nav>
<hr className="my-4 border-t-2 border-gray-50" />
@ -126,6 +132,7 @@ export const SystemPreferences = (props: RouteComponentProps<{ submenu: string }
<Route path={`${match.url}/apps/:desk`} component={AppPrefs} />
<Route path={`${match.url}/system-updates`} component={SystemUpdatePrefs} />
<Route path={`${match.url}/interface`} component={InterfacePrefs} />
<Route path={`${match.url}/security`} component={SecurityPrefs} />
<Route
path={[`${match.url}/notifications`, match.url]}
component={NotificationPrefs}
@ -133,7 +140,7 @@ export const SystemPreferences = (props: RouteComponentProps<{ submenu: string }
</Switch>
<Link
to={match.url}
className="inline-flex sm:hidden items-center sm:none mt-auto pt-4 h4 text-gray-400"
className="inline-flex items-center pt-4 mt-auto text-gray-400 sm:hidden sm:none h4"
>
<LeftArrow className="w-3 h-3 mr-2" /> Back
</Link>

View File

@ -0,0 +1,32 @@
import React, { useState } from 'react';
import classNames from 'classnames';
import { Button } from '../../components/Button';
import { Checkbox } from '../../components/Checkbox';
export const SecurityPrefs = () => {
const [allSessions, setAllSessions] = useState(false);
return (
<>
<h2 className="h3 mb-7">Security</h2>
<div className="space-y-3">
<section className={classNames('inner-section')}>
<h3 className="flex items-center mb-2 h4">Logout</h3>
<div className="flex flex-col justify-center flex-1 space-y-6">
<Checkbox
defaultChecked={false}
checked={allSessions}
onCheckedChange={() => setAllSessions((prev) => !prev)}
>
Log out of all sessions.
</Checkbox>
<form method="post" action="/~/logout">
{allSessions && <input type="hidden" name="all" />}
<Button>Logout</Button>
</form>
</div>
</section>
</div>
</>
);
};

View File

@ -1,46 +1,43 @@
import { map, omit } from 'lodash';
import React, { FunctionComponent, useEffect } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { Route, RouteComponentProps, useHistory, useParams } from 'react-router-dom';
import { Route, useHistory, useParams } from 'react-router-dom';
import { ErrorAlert } from '../components/ErrorAlert';
import { MenuState, Nav } from '../nav/Nav';
import { useCharges } from '../state/docket';
import useKilnState from '../state/kiln';
import { RemoveApp } from '../tiles/RemoveApp';
import { SuspendApp } from '../tiles/SuspendApp';
import { Tile } from '../tiles/Tile';
import { TileGrid } from '../tiles/TileGrid';
import { TileInfo } from '../tiles/TileInfo';
interface RouteProps {
menu?: MenuState;
}
export const Grid: FunctionComponent<{}> = () => {
const charges = useCharges();
export const Grid: FunctionComponent = () => {
const { push } = useHistory();
const { menu } = useParams<RouteProps>();
const chargesLoaded = Object.keys(charges).length > 0;
useEffect(() => {
// TOOD: rework
// Heuristically detect reload completion and redirect
async function attempt(count = 0) {
if(count > 5) {
if (count > 5) {
window.location.reload();
}
const start = performance.now();
await useKilnState.getState().fetchVats();
await useKilnState.getState().fetchVats();
if((performance.now() - start) > 5000) {
attempt(count+1);
if (performance.now() - start > 5000) {
attempt(count + 1);
} else {
push('/');
}
}
if(menu === 'upgrading') {
if (menu === 'upgrading') {
attempt();
}
}, [menu])
}, [menu]);
return (
<div className="flex flex-col">
@ -49,15 +46,7 @@ export const Grid: FunctionComponent<{}> = () => {
</header>
<main className="h-full w-full flex justify-center pt-4 md:pt-16 pb-32 relative z-0">
{!chargesLoaded && <span>Loading...</span>}
{chargesLoaded && (
<div className="grid justify-center grid-cols-2 sm:grid-cols-[repeat(auto-fit,minmax(auto,250px))] gap-4 px-4 md:px-8 w-full max-w-6xl">
{charges &&
map(omit(charges, window.desk), (charge, desk) => (
<Tile key={desk} charge={charge} desk={desk} disabled={menu === 'upgrading'} />
))}
</div>
)}
<TileGrid menu={menu} />
<ErrorBoundary FallbackComponent={ErrorAlert} onReset={() => push('/')}>
<Route exact path="/app/:desk">
<TileInfo />

View File

@ -21,6 +21,9 @@ interface BaseSettingsState {
theme: 'light' | 'dark' | 'auto';
doNotDisturb: boolean;
};
tiles: {
order: string[];
};
putEntry: (bucket: string, key: string, value: Value) => Promise<void>;
[ref: string]: unknown;
}
@ -71,6 +74,9 @@ export const useSettingsState = createState<BaseSettingsState>(
theme: 'auto',
doNotDisturb: true
},
tiles: {
order: []
},
loaded: false,
putEntry: async (bucket, key, val) => {
const poke = doPutEntry(window.desk, bucket, key, val);

View File

@ -1,5 +1,6 @@
import classNames from 'classnames';
import React, { FunctionComponent } from 'react';
import { useDrag } from 'react-dnd';
import { chadIsRunning } from '@urbit/api';
import { TileMenu } from './TileMenu';
import { Spinner } from '../components/Spinner';
@ -9,6 +10,7 @@ import { ChargeWithDesk } from '../state/docket';
import { useTileColor } from './useTileColor';
import { useVat } from '../state/kiln';
import { Bullet } from '../components/icons/Bullet';
import { dragTypes } from './TileGrid';
type TileProps = {
charge: ChargeWithDesk;
@ -28,13 +30,23 @@ export const Tile: FunctionComponent<TileProps> = ({ charge, desk, disabled = fa
const link = getAppHref(href);
const backgroundColor = suspended ? suspendColor : active ? tileColor || 'purple' : suspendColor;
const [{ isDragging }, drag] = useDrag(() => ({
type: dragTypes.TILE,
item: { desk },
collect: (monitor) => ({
isDragging: !!monitor.isDragging()
})
}));
return (
<a
ref={drag}
href={active ? link : undefined}
target="_blank"
rel="noreferrer"
className={classNames(
'group relative font-semibold aspect-w-1 aspect-h-1 rounded-3xl default-ring focus-visible:ring-4 overflow-hidden',
'group absolute font-semibold w-full h-full rounded-3xl default-ring focus-visible:ring-4 overflow-hidden',
isDragging && 'opacity-0',
lightText && active && !loading ? 'text-gray-200' : 'text-gray-800',
!active && 'cursor-default'
)}
@ -48,7 +60,7 @@ export const Tile: FunctionComponent<TileProps> = ({ charge, desk, disabled = fa
<>
{loading && <Spinner className="h-6 w-6 mr-2" />}
<span className="text-gray-500">
{suspended ? 'Suspended' : loading ? 'Installing' : hung ? 'Errored' : null }
{suspended ? 'Suspended' : loading ? 'Installing' : hung ? 'Errored' : null}
</span>
</>
)}

View File

@ -0,0 +1,56 @@
import classNames from 'classnames';
import { uniq, without } from 'lodash';
import React, { FunctionComponent } from 'react';
import { useDrop } from 'react-dnd';
import { useSettingsState } from '../state/settings';
import { dragTypes, selTiles } from './TileGrid';
interface TileContainerProps {
desk: string;
}
export const TileContainer: FunctionComponent<TileContainerProps> = ({ desk, children }) => {
const { order } = useSettingsState(selTiles);
const [{ isOver }, drop] = useDrop<{ desk: string }, undefined, { isOver: boolean }>(
() => ({
accept: dragTypes.TILE,
drop: ({ desk: itemDesk }) => {
if (!itemDesk || itemDesk === desk) {
return undefined;
}
// [1, 2, 3, 4] 1 -> 3
// [2, 3, 4]
const beforeSlot = order.indexOf(itemDesk) < order.indexOf(desk);
const orderWithoutOriginal = without(order, itemDesk);
const slicePoint = orderWithoutOriginal.indexOf(desk);
// [2, 3] [4]
const left = orderWithoutOriginal.slice(0, beforeSlot ? slicePoint + 1 : slicePoint);
const right = orderWithoutOriginal.slice(slicePoint);
// concat([2, 3], [1], [4])
const newOrder = uniq(left.concat([itemDesk], right));
// [2, 3, 1, 4]
console.log({ order, left, right, slicePoint, newOrder });
useSettingsState.getState().putEntry('tiles', 'order', newOrder);
return undefined;
},
collect: (monitor) => ({
isOver: !!monitor.isOver()
})
}),
[desk, order]
);
return (
<div
ref={drop}
className={classNames(
'relative aspect-w-1 aspect-h-1 rounded-3xl ring-4',
isOver && 'ring-blue-500',
!isOver && 'ring-transparent'
)}
>
{children}
</div>
);
};

View File

@ -0,0 +1,77 @@
import React, { useEffect } from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { TouchBackend } from 'react-dnd-touch-backend';
import { uniq } from 'lodash';
import { ChargeWithDesk, useCharges } from '../state/docket';
import { Tile } from './Tile';
import { MenuState } from '../nav/Nav';
import { SettingsState, useSettingsState } from '../state/settings';
import { TileContainer } from './TileContainer';
import { useMedia } from '../logic/useMedia';
export interface TileData {
desk: string;
charge: ChargeWithDesk;
position: number;
dragging: boolean;
}
interface TileGridProps {
menu?: MenuState;
}
export const dragTypes = {
TILE: 'tile'
};
export const selTiles = (s: SettingsState) => s.tiles;
export const TileGrid = ({ menu }: TileGridProps) => {
const charges = useCharges();
const chargesLoaded = Object.keys(charges).length > 0;
const { order } = useSettingsState(selTiles);
const isMobile = useMedia('(pointer: coarse)');
useEffect(() => {
const hasKeys = order && !!order.length;
const chargeKeys = Object.keys(charges);
if (!hasKeys) {
useSettingsState.getState().putEntry('tiles', 'order', chargeKeys);
} else if (order.length < chargeKeys.length) {
useSettingsState.getState().putEntry('tiles', 'order', uniq(order.concat(chargeKeys)));
}
}, [charges, order]);
if (!chargesLoaded) {
return <span>Loading...</span>;
}
return (
<DndProvider
backend={isMobile ? TouchBackend : HTML5Backend}
options={
isMobile
? {
delay: 50,
scrollAngleRanges: [
{ start: 30, end: 150 },
{ start: 210, end: 330 }
]
}
: undefined
}
>
<div className="grid justify-center grid-cols-2 sm:grid-cols-[repeat(auto-fit,minmax(auto,250px))] gap-4 px-4 md:px-8 w-full max-w-6xl">
{order
.filter((d) => d !== window.desk)
.map((desk) => (
<TileContainer desk={desk}>
<Tile key={desk} charge={charges[desk]} desk={desk} disabled={menu === 'upgrading'} />
</TileContainer>
))}
</div>
</DndProvider>
);
};

1
pkg/interface/.nvmrc Normal file
View File

@ -0,0 +1 @@
16.14.0

View File

@ -4,6 +4,9 @@
"description": "",
"main": "index.js",
"private": true,
"engines": {
"node": "16.14.0"
},
"dependencies": {
"@babel/runtime": "^7.12.5",
"@radix-ui/react-dialog": "^0.1.0",

View File

@ -229,6 +229,18 @@ export function deSig(ship: string): string {
return ship.replace('~', '');
}
export function preSig(ship: string): string {
if (!ship) {
return '';
}
if (ship.trim().startsWith('~')) {
return ship.trim();
}
return '~'.concat(ship.trim());
}
export function uxToHex(ux: string) {
if (ux.length > 2 && ux.substr(0, 2) === '0x') {
const value = ux.substr(2).replace('.', '').padStart(6, '0');

View File

@ -1,54 +0,0 @@
import {
Button,
Col,
StatelessCheckboxField, Text
} from '@tlon/indigo-react';
import React, { useState } from 'react';
import { BackButton } from './BackButton';
export default function SecuritySettings() {
const [allSessions, setAllSessions] = useState(false);
return (
<>
<BackButton />
<Col gapY={5} p={5} pt={4}>
<Col gapY={1} mt={0}>
<Text fontSize={2} fontWeight="medium">
Security Preferences
</Text>
<Text gray>
Manage sessions, login credentials and web access
</Text>
</Col>
<Col gapY={1}>
<Text color="black">
Log out of this session
</Text>
<Text mb={3} gray>
{allSessions
? 'You will be logged out of all browsers that have currently logged into your Urbit.'
: 'You will be logged out of your Urbit on this browser.'}
</Text>
<StatelessCheckboxField
mb={3}
selected={allSessions}
onChange={() => setAllSessions(s => !s)}
>
<Text>Log out of all sessions</Text>
</StatelessCheckboxField>
<form method="post" action="/~/logout">
{allSessions && <input type="hidden" name="all" />}
<Button
primary
destructive
border={1}
style={{ cursor: 'pointer' }}
>
Logout
</Button>
</form>
</Col>
</Col>
</>
);
}

View File

@ -11,7 +11,6 @@ import DisplayForm from './components/lib/DisplayForm';
import { LeapSettings } from './components/lib/LeapSettings';
import { NotificationPreferences } from './components/lib/NotificationPref';
import S3Form from './components/lib/S3Form';
import SecuritySettings from './components/lib/Security';
import { DmSettings } from './components/lib/DmSettings';
import ShortcutSettings from './components/lib/ShortcutSettings';
@ -117,11 +116,6 @@ return;
<SidebarItem icon='Messages' text='Direct Messages' hash='dm' />
<SidebarItem icon='Node' text='CalmEngine' hash='calm' />
<SidebarItem icon='EastCarat' text='Shortcuts' hash='shortcuts' />
<SidebarItem
icon='Locked'
text='Devices + Security'
hash='security'
/>
</Col>
</Col>
<Col flexGrow={1} overflowY='auto'>
@ -138,7 +132,6 @@ return;
{hash === 's3' && <S3Form />}
{hash === 'leap' && <LeapSettings />}
{hash === 'calm' && <CalmPrefs />}
{hash === 'security' && <SecuritySettings />}
{hash === 'debug' && <DebugPane />}
</SettingsItem>
</Col>

View File

@ -40,8 +40,9 @@ export function MentionText(props: MentionTextProps) {
export function Mention(props: {
ship: string;
first?: boolean;
emphasis?: 'bold' | 'italic';
} & PropFunc<typeof Text>) {
const { ship, first = false, ...rest } = props;
const { ship, first = false, emphasis, ...rest } = props;
const contact = useContact(`~${deSig(ship)}`);
const showNickname = useShowNickname(contact);
const name = showNickname ? contact?.nickname : cite(ship);
@ -51,8 +52,10 @@ export function Mention(props: {
marginLeft={first? 0 : 1}
marginRight={1}
px={1}
bold={emphasis === 'bold' ? true : false}
bg='washedBlue'
color='blue'
fontStyle={emphasis === 'italic' ? 'italic' : undefined}
fontSize={showNickname ? 1 : 0}
mono={!showNickname}
title={showNickname ? cite(ship) : contact?.nickname}

View File

@ -167,9 +167,11 @@ export function ShipSearch<I extends string, V extends Value<I>>(
name={id}
render={(arrayHelpers) => {
const onAdd = (ship: string) => {
setFieldValue(name(), ship);
inputIdx.current += 1;
arrayHelpers.push('');
if (!pills.includes(ship)) {
setFieldValue(name(), ship);
inputIdx.current += 1;
arrayHelpers.push('');
}
};
const onRemove = (idx: number) => {

View File

@ -34,6 +34,72 @@ interface GraphMentionNode {
ship: string;
}
const addEmphasisToMention = (contents: Content[], content: Content, index: number) => {
const prevContent = contents[index - 1];
const nextContent = contents[index + 1];
if (
'text' in content &&
(content.text.trim() === '**' || content.text.trim() === '*' )
) {
return {
text: ''
};
}
if(
'text' in content &&
content.text.endsWith('*') &&
!content.text.startsWith('*') &&
nextContent !== undefined &&
'mention' in nextContent
) {
if (content.text.charAt((content.text.length - 2)) === '*') {
return { text: content.text.slice(0, content.text.length - 2) };
}
return { text: content.text.slice(0, content.text.length - 1) };
}
if (
'text' in content &&
content.text.startsWith('*') &&
!content.text.endsWith('*') &&
prevContent !== undefined &&
'mention' in contents[index - 1]
) {
if (content.text.charAt(1) === '*') {
return { text: content.text.slice(2, content.text.length) };
}
return { text: content.text.slice(1, content.text.length) };
}
if (
'mention' in content &&
prevContent !== undefined &&
'text' in prevContent &&
// @ts-ignore type guard above covers this.
prevContent.text.endsWith('*') &&
nextContent !== undefined &&
'text' in contents[index + 1] &&
// @ts-ignore type guard above covers this.
nextContent.text.startsWith('*')
) {
if (
// @ts-ignore covered by typeguard in conditions
prevContent.text.charAt(prevContent.text.length - 2) === '*' &&
// @ts-ignore covered by typeguard in conditions
nextContent.text.charAt(nextContent.text[1]) === '*'
) {
return {
mention: content.mention,
emphasis: 'bold'
};
}
return {
mention: content.mention,
emphasis: 'italic'
};
}
return content;
};
const codeToMdAst = (content: CodeContent) => {
return {
type: 'root',
@ -100,7 +166,8 @@ const contentToMdAst = (tall: boolean) => (
children: [
{
type: 'graph-mention',
ship: content.mention
ship: content.mention,
emphasis: content.emphasis
}
]
}
@ -343,7 +410,9 @@ const renderers = {
list: ({ depth, ordered, children }) => {
return ordered ? <Ol>{children}</Ol> : <Ul>{children}</Ul>;
},
'graph-mention': ({ ship }) => <Mention ship={ship} />,
'graph-mention': (obj) => {
return <Mention ship={obj.ship} emphasis={obj.emphasis} />;
},
image: ({ url, tall }) => (
<Box mt="1" mb="2" flexShrink={0}>
<RemoteContent key={url} url={url} tall={tall} />
@ -439,7 +508,10 @@ export const GraphContent = React.memo((
transcluded = 0,
...rest
} = props;
const [, ast] = stitchAsts(contents.map(contentToMdAst(tall)));
const [, ast] = stitchAsts(
contents
.map((content, index) => addEmphasisToMention(contents, content, index))
.map(contentToMdAst(tall)));
return (
<Box {...rest}>
<Graphdown transcluded={transcluded} ast={ast} tall={tall} />

View File

@ -6,7 +6,7 @@ import {
Button,
ManagedTextInputField,
ManagedCheckboxField,
ContinuousProgressBar,
ContinuousProgressBar
} from '@tlon/indigo-react';
import { Formik, Form } from 'formik';
import React, { useEffect, useState } from 'react';
@ -20,6 +20,7 @@ import airlock from '~/logic/api';
import { joinError, joinLoad, JoinProgress } from '@urbit/api';
import { useQuery } from '~/logic/lib/useQuery';
import { JoinKind, JoinDesc, JoinSkeleton } from './Skeleton';
import { preSig } from '~/logic/lib/util';
interface InviteWithUid extends Invite {
uid: string;
@ -32,7 +33,7 @@ interface FormSchema {
const initialValues = {
autojoin: false,
shareContact: false,
shareContact: false
};
function JoinForm(props: {
@ -173,7 +174,6 @@ function JoinError(props: {
useGroupState.getState().abortJoin(desc.group);
dismiss();
};
return (
<JoinSkeleton modal={modal} title={title} desc={desc}>
@ -272,7 +272,7 @@ export function JoinPrompt(props: JoinPromptProps) {
};
const onSubmit = async ({ link }: PromptFormSchema) => {
const path = `/ship/${link}`;
const path = `/ship/${preSig(link)}`;
history.push({
search: appendQuery({ 'join-path': path })
});

View File

@ -622,6 +622,9 @@
[%x %keys ~]
:- ~ :- ~ :- mar
!>(`update:store`[now.bowl [%keys ~(key by graphs)]])
[%x %archived-keys ~]
:- ~ :- ~ :- mar
!>(`update:store`[now.bowl [%keys ~(key by archive)]])
::
[%x %tag-queries *]
:- ~ :- ~ :- mar
@ -636,7 +639,7 @@
=/ =ship (slav %p i.t.t.path)
=/ =term i.t.t.t.path
=/ marked-graph=(unit marked-graph:store)
(~(get by graphs) [ship term])
(~(get by archive) [ship term])
?~ marked-graph [~ ~]
=* graph p.u.marked-graph
=* mark q.u.marked-graph

View File

@ -127,7 +127,7 @@
++ hark-graph-migrate
|= old=state-7:hist
=| cards=(list card)
|^
|^
[(flop get-places) state]
::
++ hark
@ -225,7 +225,7 @@
?+ -.q.update `state
%add-graph (add-graph resource.q.update)
::
?(%remove-graph %archive-graph)
?(%remove-graph %archive-graph)
(remove-graph resource.q.update)
::
%remove-posts
@ -258,20 +258,20 @@
%+ skim ~(tap in watching)
|= [r=resource idx=index:graph-store]
=(r rid)
:_
:_
%_ state
watching (~(dif in watching) unwatched)
places (~(del by places) rid)
==
%+ turn ~(tap in (~(get ju places) rid))
|= =place:store
(poke-hark %del-place place)
(poke-hark %del-place place)
:: XX: fix
::
++ add-graph
|= rid=resource
^- (quip card _state)
=/ graph=graph:graph-store :: graph in subscription is bunted
=/ graph=graph:graph-store :: graph in subscription is bunted
(get-graph-mop:gra rid)
=/ node=(unit node:graph-store)
(bind (pry:orm:graph-store graph) |=([@ =node:graph-store] node))
@ -294,7 +294,7 @@
++ on-peek on-peek:def
::
++ on-leave on-leave:def
++ on-arvo
++ on-arvo
|= [=wire =sign-arvo]
^- (quip card _this)
?+ wire (on-arvo:def wire sign-arvo)
@ -317,7 +317,7 @@
::
++ get-place
|= [rid=resource =index:graph-store]
:- q.byk.bowl
:- q.byk.bowl
%+ welp /graph/(scot %p entity.rid)/[name.rid]
(graph-index-to-path index)
::
@ -372,7 +372,7 @@
^- (unit _update-core)
=/ m=(unit ^mark)
(get-mark:gra r)
?~ m ~
?~ m ~
:- ~
%_ update-core
rid r
@ -394,7 +394,7 @@
^- (list card)
%+ welp (turn (flop hark-pokes) poke-hark)
%- zing
%+ turn (flop new-watches)
%+ turn (flop new-watches)
|=(=index:graph-store (give ~[/updates] [%listen rid index]))
::
++ hark
@ -409,7 +409,7 @@
?~ updates update-core
=/ cor=(unit _post-core)
(abed:post-core i.updates)
?~ cor $(updates t.updates)
?~ cor $(updates t.updates)
$(updates t.updates, update-core abet:added:u.cor)
::
++ remove-posts
@ -428,7 +428,7 @@
++ post-core
|_ [kind=notif-kind:hook =post:graph-store]
++ post-core .
++ abet
++ abet
=. places (~(put ju places) rid place)
update-core
++ abed
@ -471,6 +471,7 @@
^+ post-core
?. should-notify post-core
=/ title=(list content:store)
?: =(title (crip "{(scow %p our.bowl)}/dm-inbox")) title.kind
?. is-mention title.kind
~[text/(rap 3 'You were mentioned in ' title ~)]
=/ link=path
@ -484,7 +485,7 @@
^+ post-core
%_ post-core
update-core
?- mode.kind
?- mode.kind
%count (hark %unread-count place %.y 1)
%each (hark %unread-each place /(rsh 4 (scot %ui (rear self-idx))))
%none update-core
@ -495,7 +496,7 @@
^+ post-core
%_ post-core
update-core
?- mode.kind
?- mode.kind
%count (hark %unread-count place %.n 1)
%each (hark %read-each place /(rsh 4 (scot %ui (rear self-idx))))
%none update-core
@ -535,7 +536,7 @@
++ notif-kind
|= p=post:graph-store
^- (unit notif-kind:hook)
|^
|^
?+ mark ~
%graph-validator-chat chat
%graph-validator-publish publish
@ -572,7 +573,7 @@
++ link
^- (unit notif-kind:hook)
?+ index.p ~
[@ ~]
[@ ~]
:- ~
:* [text+(rap 3 'New links in ' title ~)]~
[ship+author.p text+': ' (hark-contents:graph-store contents.p)]
@ -599,7 +600,7 @@
::
++ dm
?+ index.p ~
[@ @ ~]
[@ @ ~]
:- ~
:* ~[text+'New messages from ' ship+author.p]
(hark-contents:graph-store contents.p)

View File

@ -1,10 +1,10 @@
:~ title+'Groups'
info+'A suite of applications to communicate on Urbit'
color+0xee.5432
glob-http+['https://bootstrap.urbit.org/glob-0v1.5sbiv.4flu3.qfv1i.k2an0.65r45.glob' 0v1.5sbiv.4flu3.qfv1i.k2an0.65r45]
glob-http+['https://bootstrap.urbit.org/glob-0v1r2v6.v94vo.0v3ei.0ukff.upuui.glob' 0v1r2v6.v94vo.0v3ei.0ukff.upuui]
base+'landscape'
version+[1 0 8]
version+[1 0 9]
website+'https://tlon.io'
license+'MIT'
==

View File

@ -41,6 +41,7 @@ export interface AppReference {
export interface MentionContent {
mention: string;
emphasis?: 'bold' | 'italic';
}
export type Content =
| TextContent