mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-12-01 11:33:41 +03:00
Merge branch 'next/groups'
This commit is contained in:
commit
c07bcd6e03
@ -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'
|
||||
==
|
||||
|
@ -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': '/'
|
||||
// },
|
||||
|
33262
pkg/interface/package-lock.json
generated
33262
pkg/interface/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
@ -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))}` };
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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,
|
||||
|
@ -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}');
|
||||
|
@ -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]
|
||||
);
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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' }}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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}
|
||||
|
@ -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%"
|
||||
|
@ -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} />);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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');
|
||||
|
||||
|
@ -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])
|
||||
|
@ -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)]
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
==
|
||||
|
@ -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)))
|
||||
|
@ -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)
|
||||
|
@ -21,7 +21,7 @@
|
||||
|= vip=vip-metadata:met
|
||||
^- permissions:graph
|
||||
?+ index.p.i !!
|
||||
[@ ~] [%self %self %no]
|
||||
[@ ~] [%yes %self %no]
|
||||
==
|
||||
::
|
||||
++ notification-kind
|
||||
|
@ -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 !>(~))
|
||||
|
Loading…
Reference in New Issue
Block a user