Merge branch 'next/groups'

This commit is contained in:
Hunter Miller 2022-01-27 10:25:14 -06:00
commit c07bcd6e03
33 changed files with 8609 additions and 25383 deletions

View File

@ -1,10 +1,10 @@
:~ title+'System'
info+'An app launcher for Urbit.'
color+0xee.5432
glob-http+['https://bootstrap.urbit.org/glob-0v4.64ana.19ug9.ik7l6.og080.68ce4.glob' 0v4.64ana.19ug9.ik7l6.og080.68ce4]
glob-http+['https://bootstrap.urbit.org/glob-0v5.1o2c9.g1btf.nandl.703oh.40up1.glob' 0v5.1o2c9.g1btf.nandl.703oh.40up1]
::glob-ames+~zod^0v0
base+'grid'
version+[1 0 2]
version+[1 0 3]
website+'https://tlon.io'
license+'MIT'
==

View File

@ -9,15 +9,14 @@ const { execSync } = require('child_process');
const GIT_DESC = execSync('git describe --always', { encoding: 'utf8' }).trim();
let devServer = {
contentBase: path.join(__dirname, '../public'),
hot: true,
port: 9000,
host: '0.0.0.0',
disableHostCheck: true,
historyApiFallback: {
index: '/apps/landscape/index.html',
disableDotRule: true
},
publicPath: '/apps/landscape/'
}
};
const router = _.mapKeys(urbitrc.FLEET || {}, (value, key) => `${key}.localhost:9000`);
@ -25,7 +24,6 @@ const router = _.mapKeys(urbitrc.FLEET || {}, (value, key) => `${key}.localhost
if(urbitrc.URL) {
devServer = {
...devServer,
index: 'index.html',
// headers: {
// 'Service-Worker-Allowed': '/'
// },

File diff suppressed because it is too large Load Diff

View File

@ -25,6 +25,7 @@
"css-loader": "^3.6.0",
"file-saver": "^2.0.5",
"formik": "^2.1.5",
"fuzzy": "^0.1.3",
"immer": "^9.0.2",
"lodash": "^4.17.21",
"moment": "^2.29.1",

View File

@ -1,17 +1,17 @@
import useMetadataState from '../state/metadata';
import ob from 'urbit-ob';
import useInviteState from '../state/invite';
import {resourceAsPath} from '../../../../npm/api/dist';
import { deSig, resourceAsPath } from '@urbit/api';
function getGroupResourceRedirect(key: string) {
const association = useMetadataState.getState().associations.graph[`/ship/${key}`];
const { metadata } = association;
if(!association || !('graph' in metadata.config)) {
const graphs = useMetadataState.getState().associations.graph;
const association = graphs[`/ship/${key}`];
if(!association || !('graph' in association.metadata.config)) {
return '';
}
const section = association.group === association.resource ? '/messages' : association.group;
return `/~landscape${section}/resource/${metadata.config.graph}${association.resource}`;
return `/~landscape${section}/resource/${association.metadata.config.graph}${association.resource}`;
}
function getPostRedirect(key: string, segs: string[]) {
@ -70,7 +70,17 @@ function getGraphRedirect(link: string) {
function getInviteRedirect(link: string) {
const [,,app,uid] = link.split('/');
const invite = useInviteState.getState().invites[app][uid];
if(!invite) { return ''; }
if(!invite) {
return '';
}
const { ship, name } = invite.resource;
const alreadyJoined = getGroupResourceRedirect(`~${deSig(ship)}/${name}`);
if (alreadyJoined) {
return alreadyJoined;
}
return { search: `?join-kind=${app}&join-path=${encodeURIComponent(resourceAsPath(invite.resource))}` };
}

View File

@ -6,7 +6,6 @@ const makeIndexes = () => new Map([
['commands', []],
['subscriptions', []],
['groups', []],
['apps', []],
['other', []]
]);
@ -61,29 +60,6 @@ const commandIndex = function (currentGroup, groups, associations) {
return commands;
};
const appIndex = function (apps) {
// all apps are indexed from launch data
// indexed into 'apps'
const applications = [];
Object.keys(apps)
.filter((e) => {
return !['weather','clock'].includes(e);
})
.sort((a, b) => {
return a.localeCompare(b);
})
.map((e) => {
const obj = result(
apps[e].type?.basic?.title || apps[e].type.custom?.tile || e,
apps[e]?.type.basic?.linkedUrl || apps[e]?.type.custom?.linkedUrl || '',
apps[e]?.type?.basic?.title || apps[e].type.custom?.tile || e,
null
);
applications.push(obj);
});
return applications;
};
const otherIndex = function(config) {
const other = [];
const idx = {
@ -102,7 +78,7 @@ const otherIndex = function(config) {
return other;
};
export default function index(contacts, associations, apps, currentGroup, groups, hide): Map<string, OmniboxItem[]> {
export default function index(contacts, associations, currentGroup, groups, hide): Map<string, OmniboxItem[]> {
const indexes = makeIndexes();
indexes.set('ships', shipIndex(contacts));
// all metadata from all apps is indexed
@ -164,7 +140,6 @@ export default function index(contacts, associations, apps, currentGroup, groups
indexes.set('commands', commandIndex(currentGroup, groups, associations));
indexes.set('subscriptions', subscriptions);
indexes.set('groups', landscape);
indexes.set('apps', appIndex(apps));
indexes.set('other', otherIndex(hide));
return indexes;

View File

@ -210,6 +210,9 @@ function more(json: any, state: HarkState): HarkState {
function added(json: any, state: HarkState): HarkState {
if('added' in json) {
const { bin } = json.added;
if(bin.place.desk !== window.desk) {
return state;
}
const binId = harkBinToId(bin);
state.unseen[binId] = json.added;
}
@ -239,6 +242,9 @@ function timebox(json: any, state: HarkState): HarkState {
const time = makePatDa(lid.archive);
const old = state.archive.get(time) || {};
notifications.forEach((note: any) => {
if(note.bin.place.desk !== window.desk) {
return;
}
const binId = harkBinToId(note.bin);
old[binId] = note;
});
@ -246,6 +252,9 @@ function timebox(json: any, state: HarkState): HarkState {
} else {
const seen = 'seen' in lid ? 'seen' : 'unseen';
notifications.forEach((note: any) => {
if(note.bin.place.desk !== window.desk) {
return;
}
const binId = harkBinToId(note.bin);
state[seen][binId] = note;
});

View File

@ -40,6 +40,7 @@ export interface SettingsState {
hideUnreads: boolean;
hideGroups: boolean;
hideUtilities: boolean;
disableSpellcheck: boolean;
};
keyboard: ShortcutMapping;
remoteContentPolicy: RemoteContentPolicy;
@ -72,7 +73,8 @@ const useSettingsState = createState<SettingsState>(
hideAvatars: false,
hideUnreads: false,
hideGroups: false,
hideUtilities: false
hideUtilities: false,
disableSpellcheck: false
},
remoteContentPolicy: {
imageShown: true,

View File

@ -27,7 +27,7 @@ import './css/indigo-static.css';
import { Content } from './landscape/components/Content';
import './landscape/css/custom.css';
import { bootstrapApi } from '~/logic/api/bootstrap';
import { uxToHex } from '@urbit/api/dist';
import { uxToHex } from '@urbit/api';
function ensureValidHex(color) {
if (!color)
@ -43,7 +43,11 @@ const Root = withState(styled.div`
font-family: ${p => p.theme.fonts.sans};
height: 100%;
width: 100%;
padding: 0;
padding-left: env(safe-area-inset-left, 0px);
padding-right: env(safe-area-inset-right, 0px);
padding-top: env(safe-area-inset-top, 0px);
padding-bottom: env(safe-area-inset-bottom, 0px);
margin: 0;
${p => p.display.backgroundType === 'url' ? `
background-image: url('${p.display.background}');

View File

@ -89,7 +89,7 @@ const ChatResource = (props: ChatResourceProps): ReactElement => {
);
const isAdmin = useMemo(
() => (group ? group.tags.role.admin.has(`~${window.ship}`) : false),
() => group ? group.tags.role.admin.has(deSig(window.ship)) : false,
[group]
);

View File

@ -1,4 +1,4 @@
import { acceptDm, cite, Content, declineDm, deSig, Post, removeDmMessage } from '@urbit/api';
import { acceptDm, cite, Content, declineDm, deSig, Post } from '@urbit/api';
import React, { useCallback, useEffect } from 'react';
import _ from 'lodash';
import bigInt from 'big-integer';
@ -77,8 +77,10 @@ export function DmResource(props: DmResourceProps) {
);
useEffect(() => {
getNewest(`~${window.ship}`, 'dm-inbox', 100, `/${patp2dec(ship)}`);
}, [ship]);
if(dm.size === 0 && !pending) {
getNewest(`~${window.ship}`, 'dm-inbox', 100, `/${patp2dec(ship)}`);
}
}, [ship, dm]);
const fetchMessages = useCallback(
async (newer: boolean) => {
@ -125,10 +127,6 @@ export function DmResource(props: DmResourceProps) {
[ship, addDmMessage]
);
const onDelete = useCallback((msg: Post) => {
airlock.poke(removeDmMessage(`~${window.ship}`, msg.index));
}, []);
const onAccept = async () => {
await airlock.poke(acceptDm(ship));
};
@ -136,6 +134,7 @@ export function DmResource(props: DmResourceProps) {
history.push('/~landscape/messages');
await airlock.poke(declineDm(ship));
};
return (
<Col width="100%" height="100%" overflow="hidden">
<Row
@ -206,7 +205,6 @@ export function DmResource(props: DmResourceProps) {
onReply={quoteReply}
fetchMessages={fetchMessages}
dismissUnread={dismissUnread}
onDelete={onDelete}
getPermalink={() => undefined}
isAdmin={false}
onSubmit={onSubmit}

View File

@ -8,6 +8,7 @@ import React, { useRef, ClipboardEvent, useEffect, useImperativeHandle } from 'r
import { Controlled as CodeEditor } from 'react-codemirror2';
import styled from 'styled-components';
import { MOBILE_BROWSER_REGEX } from '~/logic/lib/util';
import useSettingsState from '~/logic/state/settings';
import '../css/custom.css';
import { useChatStore } from './ChatPane';
@ -131,6 +132,8 @@ const ChatEditor = React.forwardRef<CodeMirrorShim, ChatEditorProps>(({ inCodeMo
useImperativeHandle(ref, () => editorRef.current);
const editor = editorRef.current;
const disableSpellcheck = useSettingsState(s => s.calm.disableSpellcheck);
const {
message,
setMessage
@ -234,6 +237,7 @@ const ChatEditor = React.forwardRef<CodeMirrorShim, ChatEditorProps>(({ inCodeMo
fontFamily={inCodeMode ? 'Source Code Pro' : 'Inter'}
fontSize={1}
lineHeight="tall"
spellCheck={!disableSpellcheck}
value={message}
rows={1}
style={{ width: '100%', background: 'transparent', color: 'currentColor' }}

View File

@ -284,6 +284,9 @@ const MessageActionItem = (props) => {
const MessageActions = ({ onReply, onDelete, msg, isAdmin, permalink }) => {
const isOwn = () => msg.author === window.ship;
const { doCopy, copyDisplay } = useCopy(permalink, 'Copy Message Link');
const showCopyMessageLink = Boolean(permalink);
const showDelete = (isAdmin || isOwn()) && onDelete;
const showDropdown = showCopyMessageLink || showDelete;
return (
<Box
@ -304,49 +307,46 @@ const MessageActions = ({ onReply, onDelete, msg, isAdmin, permalink }) => {
>
<Icon icon='Chat' size={3} />
</Box>
<Dropdown
dropWidth='250px'
width='auto'
alignY='top'
alignX='right'
flexShrink={0}
offsetY={8}
offsetX={-24}
options={
<Col
py={2}
backgroundColor='white'
color='washedGray'
border={1}
borderRadius={2}
borderColor='lightGray'
boxShadow='0px 0px 0px 3px'
>
<MessageActionItem onClick={() => onReply(msg)}>
Reply
</MessageActionItem>
{permalink ? (
<MessageActionItem onClick={doCopy}>
{copyDisplay}
{showDropdown && (
<Dropdown
dropWidth='250px'
width='auto'
alignY='top'
alignX='right'
flexShrink={0}
offsetY={8}
offsetX={-24}
options={
<Col
py={2}
backgroundColor='white'
color='washedGray'
border={1}
borderRadius={2}
borderColor='lightGray'
boxShadow='0px 0px 0px 3px'
>
<MessageActionItem onClick={() => onReply(msg)}>
Reply
</MessageActionItem>
) : null }
{(isAdmin || isOwn()) ? (
<MessageActionItem onClick={e => onDelete(msg)} color='red'>
Delete Message
</MessageActionItem>
) : null}
{false && (
<MessageActionItem onClick={e => console.log(e)}>
View Signature
</MessageActionItem>
)}
</Col>
}
>
<Box padding={1} size={'24px'} cursor='pointer'>
<Icon icon='Menu' size={3} />
</Box>
</Dropdown>
{showCopyMessageLink && (
<MessageActionItem onClick={doCopy}>
{copyDisplay}
</MessageActionItem>
)}
{showDelete && (
<MessageActionItem onClick={e => onDelete(msg)} color='red'>
Delete Message
</MessageActionItem>
)}
</Col>
}
>
<Box padding={1} size={'24px'} cursor='pointer'>
<Icon icon='Menu' size={3} />
</Box>
</Dropdown>
)}
</Row>
</Box>
);
@ -418,7 +418,7 @@ function ChatMessage(props: ChatMessageProps) {
}
const onReply = props?.onReply || emptyCallback;
const onDelete = props?.onDelete || emptyCallback;
const onDelete = props?.onDelete; // If missing hide delete action
const transcluded = props?.transcluded || 0;
const renderSigil = props.renderSigil || (Boolean(nextMsg && msg.author !== nextMsg.author) ||
!nextMsg
@ -513,111 +513,3 @@ function ChatMessage(props: ChatMessageProps) {
export default React.memo(React.forwardRef((props: Omit<ChatMessageProps, 'innerRef'>, ref: any) => (
<ChatMessage {...props} innerRef={ref} />
)));
export const MessagePlaceholder = ({
height,
index,
className = '',
style = {},
...props
}) => (
<Box
width='100%'
fontSize={2}
pl={3}
pt={4}
pr={3}
display='flex'
lineHeight='tall'
className={className}
style={{ height, ...style }}
{...props}
>
<Box
pr={3}
verticalAlign='top'
backgroundColor='white'
style={{ float: 'left' }}
>
<Text
display='block'
background='washedGray'
width='24px'
height='24px'
borderRadius='50%'
style={{
visibility: index % 5 == 0 ? 'initial' : 'hidden'
}}
></Text>
</Box>
<Box
style={{ float: 'right', flexGrow: 1 }}
color='black'
className='clamp-message'
>
<Box
className='hide-child'
paddingTop={4}
style={{ visibility: index % 5 == 0 ? 'initial' : 'hidden' }}
>
<Text
display='inline-block'
verticalAlign='middle'
fontSize={0}
color='washedGray'
cursor='default'
>
<Text maxWidth='32rem' display='block'>
<Text
backgroundColor='washedGray'
borderRadius={2}
display='block'
width='100%'
height='100%'
></Text>
</Text>
</Text>
<Text
display='inline-block'
mono
verticalAlign='middle'
fontSize={0}
color='washedGray'
>
<Text
background='washedGray'
borderRadius={2}
display='block'
height='1em'
style={{ width: `${((index % 3) + 1) * 3}em` }}
></Text>
</Text>
<Text
mono
verticalAlign='middle'
fontSize={0}
ml={2}
color='washedGray'
borderRadius={2}
display={['none', 'inline-block']}
className='child'
>
<Text
backgroundColor='washedGray'
borderRadius={2}
display='block'
width='100%'
height='100%'
></Text>
</Text>
</Box>
<Text
display='block'
backgroundColor='washedGray'
borderRadius={2}
height='1em'
style={{ width: `${(index % 5) * 20}%` }}
></Text>
</Box>
</Box>
);

View File

@ -161,6 +161,13 @@ class ChatWindow extends Component<
}
}
onTopLoaded = () => {
const { graphSize, unreadCount } = this.props;
if(graphSize >= unreadCount) {
this.props.dismissUnread();
}
};
onBottomLoaded = () => {
if(this.state.unreadIndex.eq(bigInt.zero)) {
this.calculateUnreadIndex();
@ -274,6 +281,7 @@ class ChatWindow extends Component<
origin='bottom'
style={virtScrollerStyle}
onBottomLoaded={this.onBottomLoaded}
onTopLoaded={this.onTopLoaded}
// @ts-ignore paging @liam-fitzgerald on virtualscroller props
onScroll={this.onScroll}
data={graph}

View File

@ -40,7 +40,7 @@ export const LaunchApp = (props: LaunchAppProps): ReactElement | null => {
</title>
</Helmet>
<Route path="/join/:ship/:name">
<JoinRoute modal />
<JoinRoute />
</Route>
<ScrollbarLessBox
height="100%"

View File

@ -46,13 +46,6 @@ export function Note(props: NoteProps & RouteComponentProps) {
props.history.push(rootUrl);
};
if (typeof note.post === 'string' || !note.post) {
return (
<Box width="100%" pt="2" textAlign="center">
<Text gray>This note has been deleted.</Text>
</Box>
);
}
const comments = getComments(note);
const [, title, , post] = getLatestRevision(note);
@ -148,4 +141,16 @@ export function Note(props: NoteProps & RouteComponentProps) {
);
}
export default Note;
export default function(props: NoteProps & RouteComponentProps) {
const { note } = props;
if (typeof note.post === 'string' || !note.post) {
return (
<Box width="100%" pt="2" textAlign="center">
<Text gray>This note has been deleted.</Text>
</Box>
);
}
return (<Note {...props} />);
}

View File

@ -20,6 +20,7 @@ interface FormSchema {
audioShown: boolean;
oembedShown: boolean;
videoShown: boolean;
disableSpellcheck: boolean;
}
const settingsSel = (s: SettingsState): FormSchema => ({
@ -28,10 +29,11 @@ const settingsSel = (s: SettingsState): FormSchema => ({
hideUnreads: s.calm.hideUnreads,
hideGroups: s.calm.hideGroups,
hideUtilities: s.calm.hideUtilities,
disableSpellcheck: s.calm.disableSpellcheck,
imageShown: !s.remoteContentPolicy.imageShown,
videoShown: !s.remoteContentPolicy.videoShown,
oembedShown: !s.remoteContentPolicy.oembedShown,
audioShown: !s.remoteContentPolicy.audioShown
audioShown: !s.remoteContentPolicy.audioShown,
});
export function CalmPrefs() {
@ -108,6 +110,12 @@ export function CalmPrefs() {
id="oembedShown"
caption="Embedded content may contain scripts that can track you"
/>
<Text fontWeight="medium">Input settings</Text>
<Toggle
label="Disable spellcheck"
id="disableSpellcheck"
caption="Disable browser spellcheck"
/>
</Col>
</Form>
</FormikOnBlur>

View File

@ -13,6 +13,7 @@ import {
} from 'formik';
import React, { useEffect, useMemo } from 'react';
import * as Yup from 'yup';
import useSettingsState from '~/logic/state/settings';
import { ShipImage } from './ShipImage';
interface FormSchema {
@ -35,6 +36,7 @@ interface CommentInputProps {
const SubmitTextArea = (props) => {
const { submitForm } = useFormikContext<FormSchema>();
const [field] = useField(props.id);
const disableSpellcheck = useSettingsState(s => s.calm.disableSpellcheck);
const onKeyDown = (e: KeyboardEvent) => {
if ((e.getModifierState('Control') || e.metaKey) && e.key === 'Enter') {
submitForm();
@ -50,6 +52,7 @@ const SubmitTextArea = (props) => {
fontWeight="500"
fontSize="1"
flexGrow={1}
spellCheck={!disableSpellcheck}
style={{ resize: 'vertical' }}
{...field}
onKeyDown={onKeyDown}

View File

@ -90,6 +90,11 @@ export interface VirtualScrollerProps<K,V> {
* Callback to execute when finished loading from start
*/
onBottomLoaded?: () => void;
/*
* Callback to execute when finished loading from end
*/
onTopLoaded?: () => void;
/*
* equality function for the key type
*/
@ -413,6 +418,9 @@ export default class VirtualScroller<K,V> extends Component<VirtualScrollerProps
if(newer && this.props.onBottomLoaded) {
this.props.onBottomLoaded();
}
if(!newer && this.props.onTopLoaded) {
this.props.onTopLoaded();
}
}
};

View File

@ -1,6 +1,7 @@
import { Box, Row, Text } from '@tlon/indigo-react';
import { omit } from 'lodash';
import Mousetrap from 'mousetrap';
import fuzzy from 'fuzzy';
import _ from 'lodash';
import f from 'lodash/fp';
import React, {
@ -40,11 +41,23 @@ const SEARCHED_CATEGORIES = [
'other',
'groups',
'subscriptions',
'apps'
];
const settingsSel = (s: SettingsState) => s.leap;
const CAT_LIMIT = 6;
/**
* Flatten `catMap` according to ordering in `cats`
*/
function flattenCattegoryMap(cats: string[], catMap: Map<string, OmniboxItem[]>) {
let res = [] as OmniboxItem[];
cats.forEach(cat => {
res = res.concat(_.take(catMap.get(cat), CAT_LIMIT));
});
return res;
}
export function Omnibox(props: OmniboxProps): ReactElement {
const location = useLocation();
const history = useHistory();
@ -57,7 +70,6 @@ export function Omnibox(props: OmniboxProps): ReactElement {
const contactState = useContactState(state => state.contacts);
const notificationCount = useHarkState(state => state.notificationsCount);
const invites = useInviteState(state => state.invites);
const tiles = useLaunchState(state => state.tiles);
const [leapCursor, setLeapCursor] = useState('pointer');
const contacts = useMemo(() => {
@ -83,12 +95,11 @@ export function Omnibox(props: OmniboxProps): ReactElement {
return makeIndex(
contacts,
associations,
tiles,
selectedGroup,
groups,
leapConfig
);
}, [selectedGroup, leapConfig, contacts, associations, groups, tiles]);
}, [selectedGroup, leapConfig, contacts, associations, groups]);
const onOutsideClick = useCallback(() => {
props.show && props.toggle();
@ -127,29 +138,28 @@ export function Omnibox(props: OmniboxProps): ReactElement {
);
}, [index]);
const results = useMemo(() => {
const [results, categoryOrder] = useMemo(
(): [Map<string, OmniboxItem[]>, string[]] => {
if (query.length <= 1) {
return initialResults;
return [initialResults, ['other']];
}
const q = query.toLowerCase();
const resultsMap = new Map<string, OmniboxItem[]>();
let categoryMaxes: Record<string, number> = {};
SEARCHED_CATEGORIES.map((category) => {
const categoryIndex = index.get(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)
);
})
);
const fuzzied = fuzzy
.filter(q, categoryIndex, { extract: res => res.title });
categoryMaxes[category] = fuzzied
.map(a => a.score)
.reduce((a,b) => Math.max(a,b), 0);
resultsMap.set(category, fuzzied.map(a => a.original));
});
return resultsMap;
let order = Object.entries(categoryMaxes)
.sort(([,a],[,b]) => b - a)
.map(([id]) => id);
return [resultsMap, order];
}, [query, index]);
const navigate = useCallback(
@ -184,7 +194,7 @@ export function Omnibox(props: OmniboxProps): ReactElement {
);
const setPreviousSelected = useCallback(() => {
const flattenedResults = Array.from(results.values()).map(f.take(CAT_LIMIT)).flat();
const flattenedResults = flattenCattegoryMap(categoryOrder, results);
const totalLength = flattenedResults.length;
if (selected.length) {
const currentIndex = flattenedResults.indexOf(
@ -204,10 +214,10 @@ export function Omnibox(props: OmniboxProps): ReactElement {
const { app, link } = flattenedResults[totalLength - 1];
setSelected([app, link]);
}
}, [results, selected]);
}, [results, categoryOrder, selected]);
const setNextSelected = useCallback(() => {
const flattenedResults = Array.from(results.values()).map(f.take(CAT_LIMIT)).flat();
const flattenedResults = flattenCattegoryMap(categoryOrder, results);
if (selected.length) {
const currentIndex = flattenedResults.indexOf(
// @ts-ignore unclear how to give this spread a return signature
@ -226,7 +236,7 @@ export function Omnibox(props: OmniboxProps): ReactElement {
const { app, link } = flattenedResults[0];
setSelected([app, link]);
}
}, [selected, results]);
}, [results, categoryOrder, selected]);
const setSelection = (app, link) => {
setLeapCursor('pointer');
@ -258,14 +268,15 @@ export function Omnibox(props: OmniboxProps): ReactElement {
}
if (evt.key === 'Enter') {
evt.preventDefault();
let values = flattenCattegoryMap(categoryOrder, results);
if (selected.length) {
navigate(selected[0], selected[1], evt.shiftKey);
} else if (Array.from(results.values()).flat().length === 0) {
} else if (values.length === 0) {
return;
} else {
navigate(
Array.from(results.values()).flat()[0].app,
Array.from(results.values()).flat()[0].link,
values[0].app,
values[0].link,
evt.shiftKey
);
}
@ -278,15 +289,16 @@ export function Omnibox(props: OmniboxProps): ReactElement {
query,
props.show,
results,
categoryOrder,
setPreviousSelected,
setNextSelected
]
);
useEffect(() => {
const flattenedResultLinks: [string, string][] = Array.from(results.values())
.flat()
.map(result => [result.app, result.link]);
const flattenedResultLinks: [string, string][] =
flattenCattegoryMap(categoryOrder, results)
.map(result => [result.app, result.link]);
if (!flattenedResultLinks.includes(selected as [string, string])) {
setSelected(flattenedResultLinks[0] || []);
}
@ -322,10 +334,10 @@ export function Omnibox(props: OmniboxProps): ReactElement {
borderBottomLeftRadius={2}
borderBottomRightRadius={2}
>
{SEARCHED_CATEGORIES.map(category =>
{categoryOrder.map(category =>
({
category,
categoryResults: _.take(results.get(category).sort(sortResults), CAT_LIMIT)
categoryResults: _.take(results.get(category), CAT_LIMIT)
})
)
.filter(category => category.categoryResults.length > 0)

View File

@ -114,7 +114,6 @@ export function GroupSwitcher(props: {
width="100%"
alignItems="stretch"
>
{(props.baseUrl === '/~landscape/home') ?
<GroupSwitcherItem to="">
<Icon
mr={2}
@ -124,16 +123,6 @@ export function GroupSwitcher(props: {
/>
<Text>All Groups</Text>
</GroupSwitcherItem>
:
<GroupSwitcherItem to="/~landscape/home">
<Icon
mr={2}
color="gray"
display="block"
icon="Home"
/>
<Text>My Channels</Text>
</GroupSwitcherItem>}
<RecentGroups
recent={props.recentGroups}
/>

View File

@ -7,19 +7,19 @@ import {
ManagedTextInputField,
ManagedCheckboxField,
ContinuousProgressBar,
} from "@tlon/indigo-react";
import { Formik, Form } from "formik";
import React, { useEffect } from "react";
import { useHistory, useLocation, useParams } from "react-router-dom";
import useGroupState from "~/logic/state/group";
import useInviteState, { useInviteForResource } from "~/logic/state/invite";
import useMetadataState, { usePreview } from "~/logic/state/metadata";
import { decline, Invite } from "@urbit/api";
import { join, JoinRequest } from "@urbit/api/groups";
import airlock from "~/logic/api";
import { joinError, joinResult, joinLoad, JoinProgress } from "@urbit/api";
import { useQuery } from "~/logic/lib/useQuery";
import { JoinKind, JoinDesc, JoinSkeleton } from "./Skeleton";
} from '@tlon/indigo-react';
import { Formik, Form } from 'formik';
import React, { useEffect, useState } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import useGroupState from '~/logic/state/group';
import { useInviteForResource } from '~/logic/state/invite';
import useMetadataState, { usePreview } from '~/logic/state/metadata';
import { decline, Invite } from '@urbit/api';
import { join, JoinRequest } from '@urbit/api/groups';
import airlock from '~/logic/api';
import { joinError, joinLoad, JoinProgress } from '@urbit/api';
import { useQuery } from '~/logic/lib/useQuery';
import { JoinKind, JoinDesc, JoinSkeleton } from './Skeleton';
interface InviteWithUid extends Invite {
uid: string;
@ -42,7 +42,7 @@ function JoinForm(props: {
}) {
const { desc, dismiss, invite } = props;
const onSubmit = (values: FormSchema) => {
const [, , ship, name] = desc.group.split("/");
const [, , ship, name] = desc.group.split('/');
airlock.poke(
join(ship, name, desc.kind, values.autojoin, values.shareContact)
);
@ -52,26 +52,26 @@ function JoinForm(props: {
airlock.poke(decline(desc.kind, invite.uid));
dismiss();
};
const isGroups = desc.kind === "groups";
const isGroups = desc.kind === 'groups';
return (
<Formik initialValues={initialValues} onSubmit={onSubmit}>
<Form>
<Col p="4" gapY="4">
<Col p='4' gapY='4'>
{isGroups ? (
<ManagedCheckboxField id="autojoin" label="Join all channels" />
<ManagedCheckboxField id='autojoin' label='Join all channels' />
) : null}
<ManagedCheckboxField id="shareContact" label="Share identity" />
<Row justifyContent="space-between" width="100%">
<ManagedCheckboxField id='shareContact' label='Share identity' />
<Row justifyContent='space-between' width='100%'>
<Button onClick={dismiss}>Dismiss</Button>
<Row gapX="2">
<Row gapX='2'>
{!invite ? null : (
<Button onClick={onDecline} destructive type="button">
<Button onClick={onDecline} destructive type='button'>
Decline
</Button>
)}
<Button primary type="submit">
{!invite ? "Join Group" : "Accept"}
<Button primary type='submit'>
{!invite ? 'Join Group' : 'Accept'}
</Button>
</Row>
</Row>
@ -80,10 +80,6 @@ function JoinForm(props: {
</Formik>
);
}
const REQUEST: JoinDesc = {
group: "/ship/~bitbet-bolbel/urbit-community",
kind: "groups",
};
export function JoinInitial(props: {
invite?: InviteWithUid;
@ -93,7 +89,7 @@ export function JoinInitial(props: {
}) {
const { desc, dismiss, modal, invite } = props;
const title = (() => {
const name = desc.kind === "graph" ? "Group Chat" : "Group";
const name = desc.kind === 'graph' ? 'Group Chat' : 'Group';
if (invite) {
return `You've been invited to a ${name}`;
} else {
@ -117,11 +113,11 @@ function JoinLoading(props: {
const { desc, request, dismiss, modal, finished } = props;
const history = useHistory();
useEffect(() => {
if (desc.kind === "graph" && request.progress === "done") {
if (desc.kind === 'graph' && request.progress === 'done') {
history.push(finished);
}
}, [request]);
const name = desc.kind === "graph" ? "Group Chat" : "Group";
const name = desc.kind === 'graph' ? 'Group Chat' : 'Group';
const title = `Joining ${name}, please wait`;
const onCancel = () => {
useGroupState.getState().abortJoin(desc.group);
@ -129,7 +125,7 @@ function JoinLoading(props: {
};
return (
<JoinSkeleton modal={modal} desc={desc} title={title}>
<Col maxWidth="512px" p="4" gapY="4">
<Col maxWidth='512px' p='4' gapY='4'>
{joinLoad.indexOf(request.progress as any) !== -1 ? (
<JoinProgressIndicator progress={request.progress} />
) : null}
@ -139,7 +135,7 @@ function JoinLoading(props: {
offline, or the connection between you both may be unstable.
</Text>
</Box>
<Row gapX="2">
<Row gapX='2'>
<Button onClick={dismiss}>Dismiss</Button>
<Button destructive onClick={onCancel}>
Cancel Join
@ -160,14 +156,14 @@ function JoinError(props: {
const group = preview?.metadata?.title ?? desc.group;
const title = `Joining ${group} failed`;
const explanation =
request.progress === "no-perms"
? "You do not have the correct permissions"
: "An unexpected error occurred";
request.progress === 'no-perms'
? 'You do not have the correct permissions'
: 'An unexpected error occurred';
return (
<JoinSkeleton modal={modal} title={title} desc={desc}>
<Col p="4" gapY="4">
<Text fontWeight="medium">{explanation}</Text>
<Col p='4' gapY='4'>
<Text fontWeight='medium'>{explanation}</Text>
<Row>
<Button>Dismiss</Button>
</Row>
@ -186,26 +182,37 @@ export interface JoinProps {
export function Join(props: JoinProps) {
const { desc, modal, dismiss, redir } = props;
const { group, kind } = desc;
const [, , ship, name] = group.split("/");
const graph = kind === "graph";
const finishedPath = !!redir
const [, , ship, name] = group.split('/');
const graph = kind === 'graph';
const associations = useMetadataState(s => s.associations);
const joined = graph ? associations.graph[group] : associations.groups[group];
const finishedPath = redir
? redir
: graph
? `/~landscape/messages/resource/chat/${ship}/${name}`
: `/~landscape/ship/${ship}/${name}`;
const history = useHistory();
const joinRequest = useGroupState((s) => s.pendingJoin[group]);
const joinRequest = useGroupState(s => s.pendingJoin[group]);
const [openedRequest, setOpenedRequest] = useState<JoinRequest>();
const invite = useInviteForResource(kind, ship, name);
const isDone = joinRequest && joinRequest.progress === "done";
const isDone = openedRequest && openedRequest.progress === 'done' && joined;
const isErrored =
joinRequest && joinError.includes(joinRequest.progress as any);
openedRequest && joinError.includes(openedRequest.progress as any);
const isLoading =
joinRequest && joinLoad.includes(joinRequest.progress as any);
openedRequest && joinLoad.includes(openedRequest.progress as any);
// If we opened this modal from a join request,
// don't let the request getting deleted move us to the wrong state
useEffect(() => {
if (joinRequest) {
setOpenedRequest(joinRequest);
}
}, [joinRequest]);
useEffect(() => {
if (isDone && desc.kind == "graph") {
if (isDone && desc.kind == 'graph') {
history.push(finishedPath);
}
}, [isDone, desc]);
@ -222,20 +229,16 @@ export function Join(props: JoinProps) {
modal={modal}
dismiss={dismiss}
desc={desc}
request={joinRequest}
request={openedRequest}
finished={finishedPath}
/>
) : isErrored ? (
<JoinError modal={modal} desc={desc} request={joinRequest} />
<JoinError modal={modal} desc={desc} request={openedRequest} />
) : (
<JoinInitial modal={modal} dismiss={dismiss} desc={desc} invite={invite} />
);
}
interface PromptFormProps {
kind: string;
}
interface PromptFormSchema {
link: string;
}
@ -245,37 +248,37 @@ export interface JoinPromptProps {
}
export function JoinPrompt(props: JoinPromptProps) {
const { kind, dismiss } = props;
const { query, appendQuery } = useQuery();
const { dismiss } = props;
const { appendQuery } = useQuery();
const history = useHistory();
const initialValues = {
link: "",
link: ''
};
const onSubmit = async ({ link }: PromptFormSchema) => {
const path = `/ship/${link}`;
history.push({
search: appendQuery({ "join-path": path }),
search: appendQuery({ 'join-path': path })
});
};
return (
<JoinSkeleton modal body={<Text>a</Text>} title="Join a Group">
<JoinSkeleton modal body={<Text>a</Text>} title='Join a Group'>
<Formik initialValues={initialValues} onSubmit={onSubmit}>
<Form>
<Col p="4" gapY="4">
<Col p='4' gapY='4'>
<ManagedTextInputField
label="Invite Link"
id="link"
caption="Enter either a web+urbitgraph:// link or an identifier in the form ~sampel-palnet/group"
label='Invite Link'
id='link'
caption='Enter either a web+urbitgraph:// link or an identifier in the form ~sampel-palnet/group'
/>
<Row gapX="2">
{!!dismiss ? (
<Button type="button" onClick={dismiss}>
<Row gapX='2'>
{dismiss ? (
<Button type='button' onClick={dismiss}>
Dismiss
</Button>
) : null}
<Button type="submit" primary>
<Button type='submit' primary>
Join
</Button>
</Row>
@ -289,26 +292,26 @@ export function JoinPrompt(props: JoinPromptProps) {
function JoinProgressIndicator(props: { progress: JoinProgress }) {
const { progress } = props;
const percentage =
progress === "done" ? 100 : (joinLoad.indexOf(progress as any) + 1) * 25;
progress === 'done' ? 100 : (joinLoad.indexOf(progress as any) + 1) * 25;
const description = (() => {
switch (progress) {
case "start":
return "Connecting to host";
case "added":
return "Retrieving members";
case "metadata":
return "Retrieving channels";
case "done":
return "Finished";
case 'start':
return 'Connecting to host';
case 'added':
return 'Retrieving members';
case 'metadata':
return 'Retrieving channels';
case 'done':
return 'Finished';
default:
return "";
return '';
}
})();
return (
<Col gapY="2">
<Text color="lightGray">{description}</Text>
<Col gapY='2'>
<Text color='lightGray'>{description}</Text>
<ContinuousProgressBar percentage={percentage} />
</Col>
);
@ -323,8 +326,7 @@ export interface JoinDoneProps {
export function JoinDone(props: JoinDoneProps) {
const { desc, modal, finished, dismiss } = props;
const { preview, error } = usePreview(desc.group);
const name = desc.kind === "groups" ? "Group" : "Group Chat";
const name = desc.kind === 'groups' ? 'Group' : 'Group Chat';
const title = `Joined ${name} successfully`;
const history = useHistory();
@ -334,9 +336,9 @@ export function JoinDone(props: JoinDoneProps) {
return (
<JoinSkeleton title={title} modal={modal} desc={desc}>
<Col p="4" gapY="4">
<JoinProgressIndicator progress="done" />
<Row gapX="2">
<Col p='4' gapY='4'>
<JoinProgressIndicator progress='done' />
<Row gapX='2'>
<Button onClick={dismiss}>Dismiss</Button>
<Button onClick={onView} primary>
View Group
@ -347,21 +349,20 @@ export function JoinDone(props: JoinDoneProps) {
);
}
export function JoinRoute(props: { graph?: boolean; modal?: boolean }) {
const { modal = false, graph = false } = props;
export function JoinRoute() {
const { query } = useQuery();
const history = useHistory();
const { pathname } = useLocation();
const kind = query.get("join-kind");
const path = query.get("join-path");
const redir = query.get("redir");
const kind = query.get('join-kind');
const path = query.get('join-path');
const redir = query.get('redir');
if (!kind) {
return null;
}
const desc: JoinDesc = path
? {
group: path,
kind: graph ? "graph" : "groups",
kind: kind as JoinKind
}
: undefined;

View File

@ -221,7 +221,6 @@ export const SidebarAssociationItem = React.memo(
mod = association.metadata.config.graph;
}
const pending = useGroupState(s => association.group in s.pendingJoin);
console.log(pending);
const rid = association?.resource;
const { hideNicknames } = useSettingsState((s) => s.calm);
const contacts = useContactState((s) => s.contacts);

View File

@ -45,7 +45,7 @@ export function SidebarListHeader(props: {
const metadata = associations?.groups?.[groupPath]?.metadata;
const memberMetadata =
groupPath ? metadata.vip === 'member-metadata' : false;
groupPath && metadata ? metadata.vip === 'member-metadata' : false;
const isAdmin = memberMetadata || (role === 'admin') || (props.workspace?.type === 'home') || (props.workspace?.type === 'messages');

View File

@ -203,7 +203,8 @@
?> =(1 ~(wyt by nodes))
=/ ship-screen (~(get ju screened) src.bowl)
=. ship-screen (~(uni in ship-screen) (normalize-incoming nodes))
:_ state(screened (~(put by screened) src.bowl ship-screen))
=. screened (~(put by screened) src.bowl ship-screen)
:_ state
=/ =action:hook
[%pendings ~(key by screened)]
:- (fact:io dm-hook-action+!>(action) ~[/updates])

View File

@ -67,7 +67,7 @@
::
++ on-init on-init:def
++ on-save !>(-.state)
++ on-load
++ on-load
|= =vase
=+ !<(old=versioned-state vase)
=? old ?=(~ old)
@ -95,7 +95,7 @@
++ on-watch on-watch:def
++ on-leave on-leave:def
++ on-peek on-peek:def
++ on-arvo
++ on-arvo
|= [=wire =sign-arvo]
^- (quip card _this)
?+ wire (on-arvo:def wire sign-arvo)
@ -219,11 +219,12 @@
?. allowed
~
`vas
::
%add-signatures ``vas
%remove-signatures ``vas
::
%add-graph [~ ~]
%remove-graph [~ ~]
%add-signatures [~ ~]
%remove-signatures [~ ~]
%archive-graph [~ ~]
%unarchive-graph [~ ~]
%add-tag [~ ~]
@ -362,14 +363,14 @@
::
++ is-allowed-add
~/ %is-allowed-add
|= [=resource:res nodes=(map index:store node:store)]
|= [=resource:res nodes=(map index:store node:store)]
^- [? (list card)]
|^
%- (bond |.([%.n ~]))
%+ biff (get-roles-writers-variation resource)
|= [is-admin=? writers=(set ship) vip=vip-metadata:metadata]
^- (unit [? (list card)])
%- some
%- some
=/ a ~(tap by nodes)
=| cards=(list card)
|- ^- [? (list card)]

View File

@ -326,7 +326,7 @@
:_ this
%+ turn ~(tap by associations)
|= [=md-resource:metadata =association:metadata]
%+ poke-our:pass:io %metadata-store
%+ poke-our:pass:io:hc %metadata-store
:- %metadata-update-2
!> ^- update:metadata
[%remove resource md-resource]

View File

@ -1,6 +1,6 @@
:: metadata-push-hook [landscape]:
::
/- *group, *invite-store, store=metadata-store
/- *group, *invite-store, store=metadata-store, group-store
/+ default-agent, verb, dbug, grpl=group, push-hook,
resource, mdl=metadata, gral=graph, agentio
~% %group-hook-top ..part ~
@ -29,6 +29,14 @@
--
::
::
=+
^= hook-core
|_ =bowl:gall
+* io ~(. agentio bowl)
pass pass:io
++ watch-groups (~(watch-our pass /groups) %group-store /groups)
--
::
=| state-zero
=* state -
%- agent:dbug
@ -43,11 +51,20 @@
met ~(. mdl bowl)
gra ~(. gral bowl)
io ~(. agentio bowl)
hc ~(. hook-core bowl)
pass pass:io
::
++ on-init on-init:def
++ on-save !>(~)
++ on-load on-load:def
++ on-init
:_ this
~[watch-groups:hc]
::
++ on-save !>(state)
++ on-load
|= =vase
=+ !<(old=versioned-state vase)
?: ?=([%0 ~] old) `this
:_ this
~[watch-groups:hc]
::
++ on-poke
|= [=mark =vase]
@ -82,7 +99,44 @@
==
--
::
++ on-agent on-agent:def
++ on-agent
|= [=wire =sign:agent:gall]
?. ?=([%groups ~] wire)
(on-agent:def wire sign)
?+ -.sign (on-agent:def wire sign)
%kick :_(this ~[watch-groups:hc])
::
%fact
?. =(p.cage.sign %group-update-0) `this
=+ !<(=update:group-store q.cage.sign)
?. ?=(%remove-members -.update) `this
|^
=/ graphs=(set resource)
(hosting-graphs resource.update)
:_ this
%+ weld
(turn ~(tap in graphs) (cury revoke %graph-push-hook))
?. =(entity.resource.update our.bowl) ~
(revoke %metadata-push-hook resource.update)^~
::
++ revoke
|= [=dude:gall rid=resource]
=/ =action:push-hook [%revoke ships.update rid]
=/ =cage push-hook-action+!>(action)
(poke-our:pass dude cage)
::
++ hosting-graphs
|= rid=resource
^- (set resource)
=/ graphs=associations:store
(app-metadata-for-group:met resource.update %graph)
%- ~(gas in *(set resource))
%+ murn ~(tap in ~(key by graphs))
|= [app=term graph=resource]
?. =(our.bowl entity.graph) ~
`graph
--
==
++ on-watch on-watch:def
++ on-leave on-leave:def
++ on-peek on-peek:def

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-0v3.m2nd4.9tg9d.vs9ls.9rj6u.7lqhg.glob' 0v3.m2nd4.9tg9d.vs9ls.9rj6u.7lqhg]
glob-http+['https://bootstrap.urbit.org/glob-0v7.i3htk.kflos.l8nic.q5u2o.v8oir.glob' 0v7.i3htk.kflos.l8nic.q5u2o.v8oir]
base+'landscape'
version+[1 0 4]
version+[1 0 5]
website+'https://tlon.io'
license+'MIT'
==

View File

@ -432,7 +432,7 @@
::
++ tr-emis
|= caz=(list card)
tr-core(cards (welp (flop cards) cards))
tr-core(cards (welp (flop caz) cards))
::
++ tr-ap-og
|= ap=_^?(|.(*(quip card _pull-hook)))

View File

@ -423,11 +423,14 @@
::
++ revoke
|= [ships=(set ship) rid=resource]
=/ pax=path
=/ ver-pax=path
[%resource %ver (en-path:resource rid)]
=/ unver-pax=path
[%resource (en-path:resource rid)]
:_ state
%+ murn
(incoming-subscriptions pax)
%+ welp (incoming-subscriptions unver-pax)
(incoming-subscriptions ver-pax)
|= [her=ship =path]
^- (unit card)
?. (~(has in ships) her)

View File

@ -21,7 +21,7 @@
|= vip=vip-metadata:met
^- permissions:graph
?+ index.p.i !!
[@ ~] [%self %self %no]
[@ ~] [%yes %self %no]
==
::
++ notification-kind

View File

@ -22,9 +22,20 @@
:- %pull-hook-action
!> ^- action:pull-hook
[%remove rid]
;< ~ bind:m (raw-poke-our %contact-pull-hook pull-hook-act)
;< ~ bind:m (raw-poke-our %metadata-pull-hook pull-hook-act)
;< ~ bind:m (raw-poke-our %group-pull-hook pull-hook-act)
;< ~ bind:m (raw-poke-our %group-store %group-update-0 !>([%remove-group rid ~]))
;< ~ bind:m (cleanup-md:view rid)
=/ leave=cage
:- %group-update-0
!> ^- update:store
[%remove-members rid (silt our.bowl ~)]
=/ remove=cage
:- %group-update-0
!> ^- update:store
[%remove-group rid ~]
;< ~ bind:m
(raw-poke-our %group-push-hook leave)
;< ~ bind:m
(raw-poke-our %group-pull-hook pull-hook-act)
;< ~ bind:m
(raw-poke-our %contact-pull-hook pull-hook-act)
;< ~ bind:m
(raw-poke-our %group-store remove)
(pure:m !>(~))