mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-12-01 11:33:41 +03:00
Merge branch 'next/groups' into lf/landscape-floor
This commit is contained in:
commit
1d1b9ae863
@ -134,23 +134,32 @@
|
||||
==
|
||||
--
|
||||
::
|
||||
++ on-peek
|
||||
++ on-peek
|
||||
~/ %hark-store-peek
|
||||
|= =path
|
||||
^- (unit (unit cage))
|
||||
?+ path (on-peek:def path)
|
||||
::
|
||||
[%x %recent %inbox @ @ ~]
|
||||
=/ date=@da
|
||||
(slav %ud i.t.t.t.path)
|
||||
=/ length=@ud
|
||||
(slav %ud i.t.t.t.t.path)
|
||||
:^ ~ ~ %hark-update
|
||||
!> ^- update:store
|
||||
:- %more
|
||||
%+ turn (tab:orm archive `date length)
|
||||
|= [time=@da =timebox:store]
|
||||
^- update:store
|
||||
[%timebox archive+time ~(val by timebox)]
|
||||
[%x %recent %inbox @ @ ~]
|
||||
=/ date=@da
|
||||
(slav %ud i.t.t.t.path)
|
||||
=/ length=@ud
|
||||
(slav %ud i.t.t.t.t.path)
|
||||
:^ ~ ~ %hark-update
|
||||
!> ^- update:store
|
||||
:- %more
|
||||
%+ turn (tab:orm archive `date length)
|
||||
|= [time=@da =timebox:store]
|
||||
^- update:store
|
||||
[%timebox archive+time ~(val by timebox)]
|
||||
::
|
||||
[%x %all-stats ~]
|
||||
:^ ~ ~ %hark-update
|
||||
!> ^- update:store
|
||||
:- %more
|
||||
^- (list update:store)
|
||||
:~ [%all-stats places]
|
||||
==
|
||||
==
|
||||
::
|
||||
++ on-poke
|
||||
|
@ -10,7 +10,7 @@ const GIT_DESC = execSync('git describe --always', { encoding: 'utf8' }).trim();
|
||||
module.exports = {
|
||||
mode: 'production',
|
||||
entry: {
|
||||
app: './src/index.js',
|
||||
app: './src/index.tsx',
|
||||
serviceworker: './src/serviceworker.js'
|
||||
},
|
||||
module: {
|
||||
|
81182
pkg/interface/package-lock.json
generated
81182
pkg/interface/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -107,6 +107,7 @@
|
||||
"lint-staged": "^11.0.0",
|
||||
"loki": "^0.28.1",
|
||||
"moment-locales-webpack-plugin": "^1.2.0",
|
||||
"react-refresh": "^0.11.0",
|
||||
"sass": "^1.32.5",
|
||||
"sass-loader": "^8.0.2",
|
||||
"storybook-addon-designs": "^6.0.0",
|
||||
|
@ -1,7 +1,11 @@
|
||||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import { bootstrapApi } from './logic/api/bootstrap';
|
||||
import './register-sw';
|
||||
import './storage-wipe';
|
||||
import App from './views/App';
|
||||
|
||||
// Start subscriptions as soon as possible before rendering anything
|
||||
bootstrapApi();
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
|
@ -9,6 +9,7 @@ import useLaunchState from '../state/launch';
|
||||
import useSettingsState from '../state/settings';
|
||||
import useLocalState from '../state/local';
|
||||
import useStorageState from '../state/storage';
|
||||
import gcpManager from '../lib/gcpManager';
|
||||
|
||||
export async function bootstrapApi() {
|
||||
airlock.onError = (e) => {
|
||||
@ -31,20 +32,21 @@ export async function bootstrapApi() {
|
||||
useLocalState.setState({ subscription: 'connected' });
|
||||
};
|
||||
|
||||
[useGraphState].map(s => s.getState()?.clear?.());
|
||||
useGraphState.getState().getShallowChildren(`~${window.ship}`, 'dm-inbox');
|
||||
await useMetadataState.getState().initialize(airlock);
|
||||
|
||||
const promises = [
|
||||
useHarkState,
|
||||
useMetadataState,
|
||||
const subs = [
|
||||
useGroupState,
|
||||
useContactState,
|
||||
useHarkState,
|
||||
useSettingsState,
|
||||
useLaunchState,
|
||||
useInviteState,
|
||||
useGraphState,
|
||||
useStorageState
|
||||
useStorageState,
|
||||
useLaunchState,
|
||||
useGraphState
|
||||
].map(state => state.getState().initialize(airlock));
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
await Promise.all(subs);
|
||||
|
||||
useSettingsState.getState().getAll();
|
||||
gcpManager.start();
|
||||
}
|
||||
|
35
pkg/interface/src/logic/lib/S3Client.ts
Normal file
35
pkg/interface/src/logic/lib/S3Client.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { StorageClient, StorageUpload, UploadParams } from './StorageClient';
|
||||
import type S3 from 'aws-sdk/clients/s3';
|
||||
|
||||
export default class S3Client implements StorageClient {
|
||||
config: S3.ClientConfiguration;
|
||||
client: S3 | null = null;
|
||||
S3: typeof import('aws-sdk/clients/s3');
|
||||
|
||||
constructor(config: S3.ClientConfiguration) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
async initAndUpload(params: UploadParams) {
|
||||
if (!this.S3) {
|
||||
await this.loadLib();
|
||||
}
|
||||
|
||||
if (!this.client) {
|
||||
this.client = new this.S3(this.config);
|
||||
}
|
||||
|
||||
return this.client.upload(params).promise();
|
||||
}
|
||||
|
||||
upload(params: UploadParams): StorageUpload {
|
||||
const upload = this.initAndUpload.bind(this);
|
||||
return {
|
||||
promise: () => upload(params)
|
||||
};
|
||||
}
|
||||
|
||||
async loadLib() {
|
||||
this.S3 = (await import('aws-sdk/clients/s3')).default;
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import _ from 'lodash';
|
||||
import { useState, ClipboardEvent } from 'react';
|
||||
import { useState, ClipboardEvent, useCallback } from 'react';
|
||||
import { useFileDrag } from './useDrag';
|
||||
import useStorage, { IuseStorage } from './useStorage';
|
||||
|
||||
@ -32,20 +32,9 @@ export function useFileUpload({ multiple = true, ...params }: useFileUploadParam
|
||||
canUpload, uploadDefault
|
||||
} = storage;
|
||||
const [source, setSource] = useState<FileUploadSource>('paste');
|
||||
const drag = useFileDrag(f => uploadFiles(f, 'drag'));
|
||||
|
||||
function onPaste(event: ClipboardEvent) {
|
||||
if (!event.clipboardData || !event.clipboardData.files.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
uploadFiles(event.clipboardData.files, 'paste');
|
||||
}
|
||||
|
||||
function uploadFiles(files: FileList | File[], uploadSource: FileUploadSource) {
|
||||
const uploadFiles = useCallback((files: FileList | File[], uploadSource: FileUploadSource) => {
|
||||
if (isFileUploadHandler(params)) {
|
||||
return params.onFiles(files, storage, uploadSource);
|
||||
}
|
||||
@ -67,6 +56,19 @@ export function useFileUpload({ multiple = true, ...params }: useFileUploadParam
|
||||
onError && onError(err);
|
||||
});
|
||||
});
|
||||
}, [canUpload, storage, params]);
|
||||
|
||||
const drag = useFileDrag(f => uploadFiles(f, 'drag'));
|
||||
|
||||
function onPaste(event: ClipboardEvent) {
|
||||
if (!event.clipboardData || !event.clipboardData.files.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
uploadFiles(event.clipboardData.files, 'paste');
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import S3 from 'aws-sdk/clients/s3';
|
||||
import S3Client from './S3Client';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import useStorageState from '../state/storage';
|
||||
import GcpClient from './GcpClient';
|
||||
@ -10,7 +10,7 @@ export interface IuseStorage {
|
||||
upload: (file: File, bucket: string) => Promise<string>;
|
||||
uploadDefault: (file: File) => Promise<string>;
|
||||
uploading: boolean;
|
||||
promptUpload: () => Promise<string>;
|
||||
promptUpload: (onError?: (err: Error) => void) => Promise<string>;
|
||||
}
|
||||
|
||||
const useStorage = ({ accept = '*' } = { accept: '*' }): IuseStorage => {
|
||||
@ -32,7 +32,7 @@ const useStorage = ({ accept = '*' } = { accept: '*' }): IuseStorage => {
|
||||
!s3.credentials.secretAccessKey) {
|
||||
return;
|
||||
}
|
||||
client.current = new S3({
|
||||
client.current = new S3Client({
|
||||
credentials: s3.credentials,
|
||||
endpoint: s3.credentials.endpoint
|
||||
});
|
||||
@ -85,7 +85,7 @@ const useStorage = ({ accept = '*' } = { accept: '*' }): IuseStorage => {
|
||||
}, [s3, upload]);
|
||||
|
||||
const promptUpload = useCallback(
|
||||
(): Promise<string> => {
|
||||
(onError?: (err: Error) => void): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileSelector = document.createElement('input');
|
||||
fileSelector.setAttribute('type', 'file');
|
||||
@ -95,6 +95,9 @@ const useStorage = ({ accept = '*' } = { accept: '*' }): IuseStorage => {
|
||||
const files = fileSelector.files;
|
||||
if (!files || files.length <= 0) {
|
||||
reject();
|
||||
} else if (onError) {
|
||||
uploadDefault(files[0]).then(resolve).catch(err => onError(err));
|
||||
document.body.removeChild(fileSelector);
|
||||
} else {
|
||||
uploadDefault(files[0]).then(resolve);
|
||||
document.body.removeChild(fileSelector);
|
||||
|
@ -5,7 +5,6 @@ import f from 'lodash/fp';
|
||||
import { Association, Patp } from '@urbit/api';
|
||||
import { enableMapSet } from 'immer';
|
||||
/* eslint-disable max-lines */
|
||||
import anyAscii from 'any-ascii';
|
||||
import bigInt, { BigInteger } from 'big-integer';
|
||||
import { IconRef, Workspace } from '~/types';
|
||||
|
||||
@ -410,7 +409,8 @@ export function getContactDetails(contact: any) {
|
||||
return { nickname, color, member, avatar };
|
||||
}
|
||||
|
||||
export function stringToSymbol(str: string) {
|
||||
export async function stringToSymbol(str: string) {
|
||||
const anyAscii = (await import('any-ascii')).default;
|
||||
const ascii = anyAscii(str);
|
||||
let result = '';
|
||||
for (let i = 0; i < ascii.length; i++) {
|
||||
|
@ -2,7 +2,7 @@ import { patp2dec } from 'urbit-ob';
|
||||
import shallow from 'zustand/shallow';
|
||||
import {
|
||||
Association, BigIntOrderedMap, deSig, GraphNode, Graphs, FlatGraphs, resourceFromPath, ThreadGraphs, getGraph, getShallowChildren, setScreen,
|
||||
addDmMessage, addPost, Content, getDeepOlderThan, getFirstborn, getNewest, getNode, getOlderSiblings, getYoungerSiblings, markPending, Post, addNode, GraphNodePoke
|
||||
addDmMessage, addPost, Content, getDeepOlderThan, getFirstborn, getNewest, getNode, getOlderSiblings, getYoungerSiblings, markPending, Post, addNode, GraphNodePoke, getKeys
|
||||
} from '@urbit/api';
|
||||
import { useCallback } from 'react';
|
||||
import { createState, createSubscription, reduceStateN, pokeOptimisticallyN } from './base';
|
||||
@ -25,6 +25,7 @@ export interface GraphState {
|
||||
pendingDms: Set<string>;
|
||||
screening: boolean;
|
||||
graphTimesentMap: Record<number, string>;
|
||||
getKeys(): Promise<void>;
|
||||
getShallowChildren: (ship: string, name: string, index?: string) => Promise<void>;
|
||||
getDeepOlderThan: (ship: string, name: string, count: number, start?: string) => Promise<void>;
|
||||
getNewest: (ship: string, resource: string, count: number, index?: string) => Promise<void>;
|
||||
@ -149,15 +150,11 @@ const useGraphState = createState<GraphState>('Graph', (set, get) => ({
|
||||
setScreen: (screen: boolean) => {
|
||||
const poke = setScreen(screen);
|
||||
pokeOptimisticallyN(useGraphState, poke, reduceDm);
|
||||
},
|
||||
getKeys: async () => {
|
||||
const keys = await airlock.scry(getKeys());
|
||||
GraphReducer(keys);
|
||||
}
|
||||
// getKeys: async () => {
|
||||
// const api = useApi();
|
||||
// const keys = await api.scry({
|
||||
// app: 'graph-store',
|
||||
// path: '/keys'
|
||||
// });
|
||||
// graphReducer(keys);
|
||||
// },
|
||||
// getTags: async () => {
|
||||
// const api = useApi();
|
||||
// const tags = await api.scry({
|
||||
|
@ -35,6 +35,7 @@ export interface HarkState {
|
||||
doNotDisturb: boolean;
|
||||
poke: (poke: Poke<any>) => Promise<void>;
|
||||
getMore: () => Promise<boolean>;
|
||||
getUnreads: () => Promise<void>;
|
||||
opened: () => void;
|
||||
readCount: (path: string) => Promise<void>;
|
||||
// getTimeSubset: (start?: Date, end?: Date) => Promise<void>;
|
||||
@ -121,6 +122,13 @@ const useHarkState = createState<HarkState>(
|
||||
reduceStateN(useHarkState.getState(), update, [reduce]);
|
||||
return get().archive?.size === oldSize;
|
||||
},
|
||||
getUnreads: async (): Promise<void> => {
|
||||
const update = await api.scry({
|
||||
app: 'hark-store',
|
||||
path: '/all-stats'
|
||||
});
|
||||
reduceStateN(useHarkState.getState(), update, [reduce]);
|
||||
},
|
||||
unseen: {},
|
||||
seen: {},
|
||||
notificationsCount: 0,
|
||||
|
@ -1,18 +1,12 @@
|
||||
import Mousetrap from 'mousetrap';
|
||||
import shallow from 'zustand/shallow';
|
||||
import 'mousetrap-global-bind';
|
||||
import * as React from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import { Router, withRouter } from 'react-router-dom';
|
||||
import styled, { ThemeProvider } from 'styled-components';
|
||||
import FingerprintJS from '@fingerprintjs/fingerprintjs';
|
||||
import gcpManager from '~/logic/lib/gcpManager';
|
||||
import { svgDataURL } from '~/logic/lib/util';
|
||||
import history from '~/logic/lib/history';
|
||||
import { favicon } from '~/logic/state/contact';
|
||||
import useLocalState, { selectLocalState } from '~/logic/state/local';
|
||||
import useSettingsState, { selectSettingsState, SettingsState } from '~/logic/state/settings';
|
||||
import useGraphState, { GraphState } from '~/logic/state/graph';
|
||||
import { SettingsState } from '~/logic/state/settings';
|
||||
import { ShortcutContextProvider } from '~/logic/lib/shortcutContext';
|
||||
|
||||
import ErrorBoundary from '~/views/components/ErrorBoundary';
|
||||
@ -23,9 +17,9 @@ import './css/fonts.css';
|
||||
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';
|
||||
import { useThemeWatcher } from '~/logic/lib/useThemeWatcher';
|
||||
import useLocalState from '~/logic/state/local';
|
||||
|
||||
function ensureValidHex(color) {
|
||||
if (!color)
|
||||
@ -91,31 +85,13 @@ const Root = styled.div<RootProps>`
|
||||
|
||||
const StatusBarWithRouter = withRouter(StatusBar);
|
||||
|
||||
const selLocal = selectLocalState(['toggleOmnibox']);
|
||||
const selSettings = selectSettingsState(['display', 'getAll']);
|
||||
const selGraph = (s: GraphState) => s.getShallowChildren;
|
||||
|
||||
const App: React.FunctionComponent = () => {
|
||||
const { getAll } = useSettingsState(selSettings, shallow);
|
||||
const { toggleOmnibox } = useLocalState(selLocal);
|
||||
const getShallowChildren = useGraphState(selGraph);
|
||||
const { theme, display } = useThemeWatcher();
|
||||
|
||||
React.useEffect(() => {
|
||||
bootstrapApi();
|
||||
getShallowChildren(`~${window.ship}`, 'dm-inbox');
|
||||
|
||||
getId().then((value) => {
|
||||
useLocalState.setState({ browserId: value });
|
||||
});
|
||||
|
||||
getAll();
|
||||
gcpManager.start();
|
||||
Mousetrap.bindGlobal(['command+/', 'ctrl+/'], (e) => {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
toggleOmnibox();
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
@ -1,6 +1,13 @@
|
||||
import { Box, Icon, LoadingSpinner, Row } from '@tlon/indigo-react';
|
||||
import { Box, Col, Icon, LoadingSpinner, Row, Text } from '@tlon/indigo-react';
|
||||
import { Contact, Content, evalCord } from '@urbit/api';
|
||||
import React, { FC, PropsWithChildren, useRef, useState } from 'react';
|
||||
import VisibilitySensor from 'react-visibility-sensor';
|
||||
import React, {
|
||||
FC,
|
||||
PropsWithChildren,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState
|
||||
} from 'react';
|
||||
import tokenizeMessage from '~/logic/lib/tokenizeMessage';
|
||||
import { IuseStorage } from '~/logic/lib/useStorage';
|
||||
import { MOBILE_BROWSER_REGEX } from '~/logic/lib/util';
|
||||
@ -11,41 +18,57 @@ import { ChatAvatar } from './ChatAvatar';
|
||||
import { useChatStore } from './ChatPane';
|
||||
import { useImperativeHandle } from 'react';
|
||||
import { FileUploadSource, useFileUpload } from '~/logic/lib/useFileUpload';
|
||||
import { Portal } from '~/views/components/Portal';
|
||||
import styled from 'styled-components';
|
||||
import { useOutsideClick } from '~/logic/lib/useOutsideClick';
|
||||
|
||||
type ChatInputProps = PropsWithChildren<IuseStorage & {
|
||||
hideAvatars: boolean;
|
||||
ourContact?: Contact;
|
||||
placeholder: string;
|
||||
onSubmit: (contents: Content[]) => void;
|
||||
}>;
|
||||
const FixedOverlay = styled(Col)`
|
||||
position: fixed;
|
||||
-webkit-transition: all 0.1s ease-out;
|
||||
-moz-transition: all 0.1s ease-out;
|
||||
-o-transition: all 0.1s ease-out;
|
||||
transition: all 0.1s ease-out;
|
||||
`;
|
||||
|
||||
type ChatInputProps = PropsWithChildren<
|
||||
IuseStorage & {
|
||||
hideAvatars: boolean;
|
||||
ourContact?: Contact;
|
||||
placeholder: string;
|
||||
onSubmit: (contents: Content[]) => void;
|
||||
uploadError: string;
|
||||
setUploadError: (val: string) => void;
|
||||
handleUploadError: (err: Error) => void;
|
||||
}
|
||||
>;
|
||||
|
||||
const InputBox: FC = ({ children }) => (
|
||||
<Row
|
||||
alignItems='center'
|
||||
position='relative'
|
||||
alignItems="center"
|
||||
position="relative"
|
||||
flexGrow={1}
|
||||
flexShrink={0}
|
||||
borderTop={1}
|
||||
borderTopColor='lightGray'
|
||||
backgroundColor='white'
|
||||
className='cf'
|
||||
borderTopColor="lightGray"
|
||||
backgroundColor="white"
|
||||
className="cf"
|
||||
zIndex={0}
|
||||
>
|
||||
{ children }
|
||||
{children}
|
||||
</Row>
|
||||
);
|
||||
|
||||
const IconBox = ({ children, ...props }) => (
|
||||
<Box
|
||||
ml='12px'
|
||||
ml="12px"
|
||||
mr={3}
|
||||
flexShrink={0}
|
||||
height='16px'
|
||||
width='16px'
|
||||
flexBasis='16px'
|
||||
height="16px"
|
||||
width="16px"
|
||||
flexBasis="16px"
|
||||
{...props}
|
||||
>
|
||||
{ children }
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
|
||||
@ -68,99 +91,157 @@ const MobileSubmitButton = ({ enabled, onSubmit }) => (
|
||||
</Box>
|
||||
);
|
||||
|
||||
export const ChatInput = React.forwardRef(({ ourContact, hideAvatars, placeholder, onSubmit }: ChatInputProps, ref) => {
|
||||
const chatEditor = useRef<CodeMirrorShim>(null);
|
||||
useImperativeHandle(ref, () => chatEditor.current);
|
||||
const [inCodeMode, setInCodeMode] = useState(false);
|
||||
export const ChatInput = React.forwardRef(
|
||||
(
|
||||
{
|
||||
ourContact,
|
||||
hideAvatars,
|
||||
placeholder,
|
||||
onSubmit,
|
||||
uploadError,
|
||||
setUploadError,
|
||||
handleUploadError
|
||||
}: ChatInputProps,
|
||||
ref
|
||||
) => {
|
||||
const chatEditor = useRef<CodeMirrorShim>(null);
|
||||
useImperativeHandle(ref, () => chatEditor.current);
|
||||
const [inCodeMode, setInCodeMode] = useState(false);
|
||||
const [showPortal, setShowPortal] = useState(false);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const innerRef = useRef<HTMLDivElement>(null);
|
||||
const outerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const {
|
||||
message,
|
||||
setMessage
|
||||
} = useChatStore();
|
||||
const { canUpload, uploading, promptUpload, onPaste } = useFileUpload({
|
||||
onSuccess: uploadSuccess
|
||||
});
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
setShowPortal(false);
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
function uploadSuccess(url: string, source: FileUploadSource) {
|
||||
if (source === 'paste') {
|
||||
setMessage(url);
|
||||
} else {
|
||||
onSubmit([{ url }]);
|
||||
}
|
||||
}
|
||||
useOutsideClick(innerRef, () => setShowPortal(false));
|
||||
|
||||
function toggleCode() {
|
||||
setInCodeMode(!inCodeMode);
|
||||
}
|
||||
const { message, setMessage } = useChatStore();
|
||||
const { canUpload, uploading, promptUpload, onPaste } = useFileUpload({
|
||||
onSuccess: uploadSuccess,
|
||||
onError: handleUploadError
|
||||
});
|
||||
|
||||
async function submit() {
|
||||
const text = chatEditor.current?.getValue() || '';
|
||||
|
||||
if (text === '') {
|
||||
return;
|
||||
function uploadSuccess(url: string, source: FileUploadSource) {
|
||||
if (source === 'paste') {
|
||||
setMessage(url);
|
||||
} else {
|
||||
onSubmit([{ url }]);
|
||||
}
|
||||
setUploadError('');
|
||||
}
|
||||
|
||||
if (inCodeMode) {
|
||||
const output = await airlock.thread<string[]>(evalCord(text));
|
||||
onSubmit([{ code: { output, expression: text } }]);
|
||||
} else {
|
||||
onSubmit(tokenizeMessage(text));
|
||||
function toggleCode() {
|
||||
setInCodeMode(!inCodeMode);
|
||||
}
|
||||
|
||||
setInCodeMode(false);
|
||||
setMessage('');
|
||||
chatEditor.current.focus();
|
||||
}
|
||||
async function submit() {
|
||||
const text = chatEditor.current?.getValue() || '';
|
||||
|
||||
return (
|
||||
<InputBox>
|
||||
<Row p='12px 4px 12px 12px' flexShrink={0} alignItems='center'>
|
||||
<ChatAvatar contact={ourContact} hideAvatars={hideAvatars} />
|
||||
</Row>
|
||||
<ChatEditor
|
||||
ref={chatEditor}
|
||||
inCodeMode={inCodeMode}
|
||||
submit={submit}
|
||||
onPaste={(cm, e) => onPaste(e)}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
<IconBox mr={canUpload ? '12px' : 3}>
|
||||
<Icon
|
||||
icon='Dojo'
|
||||
cursor='pointer'
|
||||
onClick={toggleCode}
|
||||
color={inCodeMode ? 'blue' : 'black'}
|
||||
/>
|
||||
</IconBox>
|
||||
{canUpload && (
|
||||
<IconBox>
|
||||
{uploading ? (
|
||||
<LoadingSpinner />
|
||||
) : (
|
||||
<Icon
|
||||
icon='Attachment'
|
||||
cursor='pointer'
|
||||
width='16'
|
||||
height='16'
|
||||
onClick={() =>
|
||||
promptUpload().then(url => uploadSuccess(url, 'direct'))
|
||||
}
|
||||
if (text === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (inCodeMode) {
|
||||
const output = await airlock.thread<string[]>(evalCord(text));
|
||||
onSubmit([{ code: { output, expression: text } }]);
|
||||
} else {
|
||||
onSubmit(tokenizeMessage(text));
|
||||
}
|
||||
|
||||
setInCodeMode(false);
|
||||
setMessage('');
|
||||
chatEditor.current.focus();
|
||||
}
|
||||
|
||||
return (
|
||||
<Box ref={outerRef}>
|
||||
<VisibilitySensor active={showPortal} onChange={setVisible}>
|
||||
<InputBox>
|
||||
{showPortal && (
|
||||
<Portal>
|
||||
<FixedOverlay
|
||||
ref={innerRef}
|
||||
backgroundColor="white"
|
||||
color="washedGray"
|
||||
border={1}
|
||||
right={25}
|
||||
bottom={75}
|
||||
borderRadius={2}
|
||||
borderColor="lightGray"
|
||||
boxShadow="0px 0px 0px 3px"
|
||||
zIndex={3}
|
||||
fontSize={0}
|
||||
width="250px"
|
||||
padding={3}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<Text>{uploadError}</Text>
|
||||
<Text>Please check S3 settings.</Text>
|
||||
</FixedOverlay>
|
||||
</Portal>
|
||||
)}
|
||||
<Row p="12px 4px 12px 12px" flexShrink={0} alignItems="center">
|
||||
<ChatAvatar contact={ourContact} hideAvatars={hideAvatars} />
|
||||
</Row>
|
||||
<ChatEditor
|
||||
ref={chatEditor}
|
||||
inCodeMode={inCodeMode}
|
||||
submit={submit}
|
||||
onPaste={(cm, e) => onPaste(e)}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
)}
|
||||
</IconBox>
|
||||
)}
|
||||
{MOBILE_BROWSER_REGEX.test(navigator.userAgent) && (
|
||||
<MobileSubmitButton
|
||||
enabled={message !== ''}
|
||||
onSubmit={submit}
|
||||
/>
|
||||
)}
|
||||
</InputBox>
|
||||
);
|
||||
});
|
||||
<IconBox mr={canUpload ? '12px' : 3}>
|
||||
<Icon
|
||||
icon="Dojo"
|
||||
cursor="pointer"
|
||||
onClick={toggleCode}
|
||||
color={inCodeMode ? 'blue' : 'black'}
|
||||
/>
|
||||
</IconBox>
|
||||
{canUpload && (
|
||||
<IconBox>
|
||||
{uploadError == '' && uploading && <LoadingSpinner />}
|
||||
{uploadError !== '' && (
|
||||
<Icon
|
||||
icon="ExclaimationMark"
|
||||
cursor="pointer"
|
||||
onClick={() => setShowPortal(true)}
|
||||
/>
|
||||
)}
|
||||
{uploadError == '' && !uploading && (
|
||||
<Icon
|
||||
icon="Attachment"
|
||||
cursor="pointer"
|
||||
width="16"
|
||||
height="16"
|
||||
onClick={() =>
|
||||
promptUpload(handleUploadError).then(url =>
|
||||
uploadSuccess(url, 'direct')
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</IconBox>
|
||||
)}
|
||||
{MOBILE_BROWSER_REGEX.test(navigator.userAgent) && (
|
||||
<MobileSubmitButton enabled={message !== ''} onSubmit={submit} />
|
||||
)}
|
||||
</InputBox>
|
||||
</VisibilitySensor>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// @ts-ignore withLocalState prop passing weirdness
|
||||
export default withLocalState<Omit<ChatInputProps, keyof IuseStorage>, 'hideAvatars', ChatInput>(
|
||||
ChatInput,
|
||||
['hideAvatars']
|
||||
);
|
||||
export default withLocalState<
|
||||
Omit<ChatInputProps, keyof IuseStorage>,
|
||||
'hideAvatars',
|
||||
typeof ChatInput
|
||||
>(ChatInput, ['hideAvatars']);
|
||||
|
@ -114,8 +114,18 @@ export function ChatPane(props: ChatPaneProps): ReactElement {
|
||||
const graphTimesentMap = useGraphTimesent(id);
|
||||
const ourContact = useOurContact();
|
||||
const { restore, setMessage } = useChatStore(s => ({ setMessage: s.setMessage, restore: s.restore }));
|
||||
const [uploadError, setUploadError] = useState<string>('');
|
||||
|
||||
const handleUploadError = useCallback((err: Error) => {
|
||||
setUploadError(err.message);
|
||||
}, []);
|
||||
|
||||
const { canUpload, drag } = useFileUpload({
|
||||
onSuccess: url => onSubmit([{ url }])
|
||||
onSuccess: (url) => {
|
||||
onSubmit([{ url }]);
|
||||
setUploadError('');
|
||||
},
|
||||
onError: handleUploadError
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@ -171,6 +181,9 @@ export function ChatPane(props: ChatPaneProps): ReactElement {
|
||||
onSubmit={onSubmit}
|
||||
ourContact={(promptShare.length === 0 && ourContact) || undefined}
|
||||
placeholder="Message..."
|
||||
uploadError={uploadError}
|
||||
setUploadError={setUploadError}
|
||||
handleUploadError={handleUploadError}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { Box, LoadingSpinner, Action, Row } from '@tlon/indigo-react';
|
||||
import { Box, LoadingSpinner, Action, Row, Icon, Text } from '@tlon/indigo-react';
|
||||
|
||||
import { StatelessUrlInput } from '~/views/components/StatelessUrlInput';
|
||||
import SubmitDragger from '~/views/components/SubmitDragger';
|
||||
@ -21,6 +21,7 @@ export function LinkBlockInput(props: LinkBlockInputProps) {
|
||||
const [url, setUrl] = useState(props.url || '');
|
||||
const [valid, setValid] = useState(false);
|
||||
const [focussed, setFocussed] = useState(false);
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
const addPost = useGraphState(selGraph);
|
||||
|
||||
@ -39,10 +40,16 @@ export function LinkBlockInput(props: LinkBlockInputProps) {
|
||||
const handleChange = useCallback((val: string) => {
|
||||
setUrl(val);
|
||||
setValid(URLparser.test(val) || Boolean(parsePermalink(val)));
|
||||
setError('');
|
||||
}, []);
|
||||
|
||||
const handleError = useCallback((err: Error) => {
|
||||
setError(err.message);
|
||||
}, []);
|
||||
|
||||
const { uploading, canUpload, promptUpload, drag } = useFileUpload({
|
||||
onSuccess: handleChange
|
||||
onSuccess: handleChange,
|
||||
onError: handleError
|
||||
});
|
||||
|
||||
const doPost = () => {
|
||||
@ -64,7 +71,7 @@ export function LinkBlockInput(props: LinkBlockInputProps) {
|
||||
};
|
||||
|
||||
const onKeyPress = useCallback(
|
||||
(e) => {
|
||||
(e: any) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
doPost();
|
||||
@ -89,22 +96,43 @@ export function LinkBlockInput(props: LinkBlockInputProps) {
|
||||
backgroundColor="washedGray"
|
||||
{...drag.bind}
|
||||
>
|
||||
{drag.dragging && <SubmitDragger />}
|
||||
{drag.dragging && canUpload && <SubmitDragger />}
|
||||
{uploading ? (
|
||||
<Box
|
||||
display="flex"
|
||||
width="100%"
|
||||
height="100%"
|
||||
position="absolute"
|
||||
left={0}
|
||||
right={0}
|
||||
bg="white"
|
||||
zIndex={9}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<LoadingSpinner />
|
||||
</Box>
|
||||
error != '' ? (
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
width="100%"
|
||||
height="100%"
|
||||
padding={3}
|
||||
position="absolute"
|
||||
left={0}
|
||||
right={0}
|
||||
bg="white"
|
||||
zIndex={9}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Icon icon="ExclaimationMarkBold" size={32} />
|
||||
<Text bold>{error}</Text>
|
||||
<Text>Please check your S3 settings.</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<Box
|
||||
display="flex"
|
||||
width="100%"
|
||||
height="100%"
|
||||
position="absolute"
|
||||
left={0}
|
||||
right={0}
|
||||
bg="white"
|
||||
zIndex={9}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<LoadingSpinner />
|
||||
</Box>
|
||||
)
|
||||
) : (
|
||||
<StatelessUrlInput
|
||||
value={url}
|
||||
@ -114,6 +142,7 @@ export function LinkBlockInput(props: LinkBlockInputProps) {
|
||||
focussed={focussed}
|
||||
onBlur={onBlur}
|
||||
promptUpload={promptUpload}
|
||||
handleError={handleError}
|
||||
onKeyPress={onKeyPress}
|
||||
center
|
||||
/>
|
||||
@ -125,7 +154,11 @@ export function LinkBlockInput(props: LinkBlockInputProps) {
|
||||
p="2"
|
||||
justifyContent="row-end"
|
||||
>
|
||||
<Action onClick={doPost} disabled={!valid} backgroundColor="transparent">
|
||||
<Action
|
||||
onClick={doPost}
|
||||
disabled={!valid}
|
||||
backgroundColor="transparent"
|
||||
>
|
||||
Post
|
||||
</Action>
|
||||
</Row>
|
||||
|
@ -1,6 +1,13 @@
|
||||
import { BaseInput, Box, Button, LoadingSpinner } from '@tlon/indigo-react';
|
||||
import {
|
||||
BaseInput,
|
||||
Box,
|
||||
Button,
|
||||
Icon,
|
||||
LoadingSpinner,
|
||||
Text
|
||||
} from '@tlon/indigo-react';
|
||||
import { hasProvider } from 'oembed-parser';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { parsePermalink, permalinkToReference } from '~/logic/lib/permalinks';
|
||||
import { StatelessUrlInput } from '~/views/components/StatelessUrlInput';
|
||||
import SubmitDragger from '~/views/components/SubmitDragger';
|
||||
@ -22,15 +29,18 @@ const LinkSubmit = (props: LinkSubmitProps) => {
|
||||
const [linkTitle, setLinkTitle] = useState('');
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
const [linkValid, setLinkValid] = useState(false);
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
const {
|
||||
canUpload,
|
||||
uploading,
|
||||
promptUpload,
|
||||
drag,
|
||||
onPaste
|
||||
} = useFileUpload({
|
||||
onSuccess: setLinkValue,
|
||||
const handleError = useCallback((err: Error) => {
|
||||
setError(err.message);
|
||||
}, []);
|
||||
|
||||
const { canUpload, uploading, promptUpload, drag, onPaste } = useFileUpload({
|
||||
onSuccess: (url) => {
|
||||
setLinkValue(url);
|
||||
setError('');
|
||||
},
|
||||
onError: handleError,
|
||||
multiple: false
|
||||
});
|
||||
|
||||
@ -38,25 +48,21 @@ const LinkSubmit = (props: LinkSubmitProps) => {
|
||||
const url = linkValue;
|
||||
const text = linkTitle ? linkTitle : linkValue;
|
||||
const contents = url.startsWith('web+urbitgraph:/')
|
||||
? [{ text }, permalinkToReference(parsePermalink(url)!)]
|
||||
: [{ text }, { url }];
|
||||
? [{ text }, permalinkToReference(parsePermalink(url)!)]
|
||||
: [{ text }, { url }];
|
||||
|
||||
setDisabled(true);
|
||||
const parentIndex = props.parentIndex || '';
|
||||
const post = createPost(window.ship, contents, parentIndex);
|
||||
|
||||
addPost(
|
||||
`~${props.ship}`,
|
||||
props.name,
|
||||
post
|
||||
);
|
||||
addPost(`~${props.ship}`, props.name, post);
|
||||
setDisabled(false);
|
||||
setLinkValue('');
|
||||
setLinkTitle('');
|
||||
setLinkValid(false);
|
||||
};
|
||||
|
||||
const validateLink = (link) => {
|
||||
const validateLink = (link: any) => {
|
||||
const URLparser = new RegExp(
|
||||
/((?:([\w\d\.-]+)\:\/\/?){1}(?:(www)\.?){0,1}(((?:[\w\d-]+\.)*)([\w\d-]+\.[\w\d]+))){1}(?:\:(\d+)){0,1}((\/(?:(?:[^\/\s\?]+\/)*))(?:([^\?\/\s#]+?(?:.[^\?\s]+){0,1}){0,1}(?:\?([^\s#]+)){0,1})){0,1}(?:#([^#\s]+)){0,1}/
|
||||
);
|
||||
@ -70,9 +76,9 @@ const LinkSubmit = (props: LinkSubmitProps) => {
|
||||
setLinkValue(link);
|
||||
}
|
||||
}
|
||||
if(link.startsWith('web+urbitgraph://')) {
|
||||
if (link.startsWith('web+urbitgraph://')) {
|
||||
const permalink = parsePermalink(link);
|
||||
if(!permalink) {
|
||||
if (!permalink) {
|
||||
setLinkValid(false);
|
||||
return;
|
||||
}
|
||||
@ -86,17 +92,23 @@ const LinkSubmit = (props: LinkSubmitProps) => {
|
||||
if (result.title && !linkTitle) {
|
||||
setLinkTitle(result.title);
|
||||
}
|
||||
}).catch((error) => { /* noop*/ });
|
||||
})
|
||||
.catch((error) => {
|
||||
/* noop*/
|
||||
});
|
||||
} else if (!linkTitle) {
|
||||
setLinkTitle(decodeURIComponent(link
|
||||
.split('/')
|
||||
.pop()
|
||||
.split('.')
|
||||
.slice(0, -1)
|
||||
.join('.')
|
||||
.replace('_', ' ')
|
||||
.replace(/\d{4}\.\d{1,2}\.\d{2}\.\.\d{2}\.\d{2}\.\d{2}-/, '')
|
||||
));
|
||||
setLinkTitle(
|
||||
decodeURIComponent(
|
||||
link
|
||||
.split('/')
|
||||
.pop()
|
||||
.split('.')
|
||||
.slice(0, -1)
|
||||
.join('.')
|
||||
.replace('_', ' ')
|
||||
.replace(/\d{4}\.\d{1,2}\.\d{2}\.\.\d{2}\.\d{2}\.\d{2}-/, '')
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
return link;
|
||||
@ -113,7 +125,7 @@ const LinkSubmit = (props: LinkSubmitProps) => {
|
||||
|
||||
useEffect(onLinkChange, [linkValue]);
|
||||
|
||||
const onKeyPress = (e) => {
|
||||
const onKeyPress = (e: any) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
doPost();
|
||||
@ -122,60 +134,86 @@ const LinkSubmit = (props: LinkSubmitProps) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* @ts-ignore archaic event type mismatch */}
|
||||
{/* @ts-ignore archaic event type mismatch */}
|
||||
<Box
|
||||
flexShrink={0}
|
||||
position='relative'
|
||||
border='1px solid'
|
||||
position="relative"
|
||||
border="1px solid"
|
||||
borderColor={submitFocused ? 'black' : 'lightGray'}
|
||||
width='100%'
|
||||
width="100%"
|
||||
borderRadius={2}
|
||||
{...drag.bind}
|
||||
>
|
||||
{uploading && <Box
|
||||
display="flex"
|
||||
width="100%"
|
||||
height="100%"
|
||||
position="absolute"
|
||||
left={0}
|
||||
right={0}
|
||||
bg="white"
|
||||
zIndex={9}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<LoadingSpinner />
|
||||
</Box>}
|
||||
{drag.dragging && <SubmitDragger />}
|
||||
<StatelessUrlInput
|
||||
value={linkValue}
|
||||
promptUpload={promptUpload}
|
||||
canUpload={canUpload}
|
||||
onSubmit={doPost}
|
||||
onChange={setLinkValue}
|
||||
error={linkValid ? 'Invalid URL' : undefined}
|
||||
onKeyPress={onKeyPress}
|
||||
onPaste={onPaste}
|
||||
/>
|
||||
<BaseInput
|
||||
type="text"
|
||||
pl={2}
|
||||
backgroundColor="transparent"
|
||||
width="100%"
|
||||
color="black"
|
||||
fontSize={1}
|
||||
style={{
|
||||
resize: 'none',
|
||||
height: 40
|
||||
}}
|
||||
placeholder="Provide a title"
|
||||
onChange={e => setLinkTitle(e.target.value)}
|
||||
onBlur={() => setSubmitFocused(false)}
|
||||
onFocus={() => setSubmitFocused(true)}
|
||||
spellCheck="false"
|
||||
onKeyPress={onKeyPress}
|
||||
value={linkTitle}
|
||||
/>
|
||||
{uploading ? (
|
||||
error !== '' ? (
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
width="100%"
|
||||
height="100%"
|
||||
left={0}
|
||||
right={0}
|
||||
bg="white"
|
||||
zIndex={9}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
py={2}
|
||||
>
|
||||
<Icon icon="ExclaimationMarkBold" size={32} />
|
||||
<Text bold>{error}</Text>
|
||||
<Text>Please check your S3 settings.</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<Box
|
||||
display="flex"
|
||||
width="100%"
|
||||
height="100%"
|
||||
left={0}
|
||||
right={0}
|
||||
bg="white"
|
||||
zIndex={9}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
py={2}
|
||||
>
|
||||
<LoadingSpinner />
|
||||
</Box>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<StatelessUrlInput
|
||||
value={linkValue}
|
||||
promptUpload={promptUpload}
|
||||
canUpload={canUpload}
|
||||
onSubmit={doPost}
|
||||
onChange={setLinkValue}
|
||||
error={linkValid ? 'Invalid URL' : undefined}
|
||||
onKeyPress={onKeyPress}
|
||||
onPaste={onPaste}
|
||||
handleError={handleError}
|
||||
/>
|
||||
<BaseInput
|
||||
type="text"
|
||||
pl={2}
|
||||
backgroundColor="transparent"
|
||||
width="100%"
|
||||
color="black"
|
||||
fontSize={1}
|
||||
style={{
|
||||
resize: 'none',
|
||||
height: 40
|
||||
}}
|
||||
placeholder="Provide a title"
|
||||
onChange={e => setLinkTitle(e.target.value)}
|
||||
onBlur={() => setSubmitFocused(false)}
|
||||
onFocus={() => setSubmitFocused(true)}
|
||||
spellCheck="false"
|
||||
onKeyPress={onKeyPress}
|
||||
value={linkTitle}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{drag.dragging && <SubmitDragger />}
|
||||
</Box>
|
||||
<Box mt={2} mb={4}>
|
||||
<Button
|
||||
@ -183,7 +221,9 @@ const LinkSubmit = (props: LinkSubmitProps) => {
|
||||
flexShrink={0}
|
||||
disabled={!linkValid || disabled}
|
||||
onClick={doPost}
|
||||
>Post link</Button>
|
||||
>
|
||||
Post link
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
@ -66,7 +66,7 @@ export function Notification(props: {
|
||||
const key = `${harkLidToId(lid)}-${harkBinToId(notification.bin)}`;
|
||||
const history = useHistory();
|
||||
|
||||
const isMobile = useLocalState((s) => s.mobile);
|
||||
const isMobile = useLocalState(s => s.mobile);
|
||||
|
||||
const onArchive = useCallback(
|
||||
async (e) => {
|
||||
@ -80,8 +80,9 @@ export function Notification(props: {
|
||||
);
|
||||
|
||||
const { hovering, bind } = useHovering();
|
||||
const dedupedBody = uniqBy(notification.body, (item) => item.link);
|
||||
const contents = map(dedupedBody, 'content').filter((c) => c.length > 0);
|
||||
const dedupedBody = uniqBy(notification.body, item => item.link);
|
||||
const orderedByTime = dedupedBody.sort((a, b) => b.time - a.time);
|
||||
const contents = map(orderedByTime, 'content').filter(c => c.length > 0);
|
||||
const first = notification.body[0];
|
||||
if (!first) {
|
||||
// should be unreachable
|
||||
|
@ -9,8 +9,9 @@ type StatelessUrlInputProps = PropFunc<typeof BaseInput> & {
|
||||
focussed?: boolean;
|
||||
disabled?: boolean;
|
||||
onChange?: (value: string) => void;
|
||||
promptUpload: () => Promise<string>;
|
||||
promptUpload: (onError: (err: Error) => void) => Promise<string>;
|
||||
canUpload: boolean;
|
||||
handleError: (err: Error) => void;
|
||||
center?: boolean;
|
||||
};
|
||||
|
||||
@ -22,6 +23,7 @@ export function StatelessUrlInput(props: StatelessUrlInputProps) {
|
||||
onChange = () => {},
|
||||
promptUpload,
|
||||
canUpload,
|
||||
handleError,
|
||||
center = false,
|
||||
...rest
|
||||
} = props;
|
||||
@ -53,7 +55,7 @@ export function StatelessUrlInput(props: StatelessUrlInputProps) {
|
||||
cursor="pointer"
|
||||
color="blue"
|
||||
style={{ pointerEvents: 'all' }}
|
||||
onClick={() => promptUpload().then(onChange)}
|
||||
onClick={() => promptUpload(handleError).then(onChange)}
|
||||
>
|
||||
upload
|
||||
</Text>{' '}
|
||||
|
@ -7,7 +7,9 @@ import {
|
||||
Row,
|
||||
Text
|
||||
} from '@tlon/indigo-react';
|
||||
import React from 'react';
|
||||
import Mousetrap from 'mousetrap';
|
||||
import 'mousetrap-global-bind';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Sigil } from '~/logic/lib/sigil';
|
||||
import { uxToHex } from '~/logic/lib/util';
|
||||
@ -46,6 +48,14 @@ const StatusBar = () => {
|
||||
<Sigil ship={ship} size={16} color={color} icon />
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
Mousetrap.bindGlobal(['command+/', 'ctrl+/'], (e) => {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
toggleOmnibox();
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box
|
||||
display='grid'
|
||||
|
@ -6,6 +6,11 @@ import { PermalinkRoutes } from '~/views/apps/permalinks/app';
|
||||
import { useShortcut } from '~/logic/state/settings';
|
||||
import { Loading } from '~/views/components/Loading';
|
||||
import LaunchApp from '~/views/apps/launch/App';
|
||||
import Landscape from '~/views/landscape';
|
||||
import Settings from '~/views/apps/settings/settings';
|
||||
import Profile from '~/views/apps/profile/profile';
|
||||
import Notifications from '~/views/apps/notifications/notifications';
|
||||
import ErrorComponent from '~/views/components/Error';
|
||||
|
||||
import { getNotificationRedirectFromLink } from '~/logic/lib/notificationRedirects';
|
||||
import { JoinRoute } from './Join/Join';
|
||||
@ -19,12 +24,6 @@ export const Container = styled(Box)`
|
||||
height: calc(100% - 62px);
|
||||
`;
|
||||
|
||||
const Landscape = React.lazy(() => import('~/views/landscape/index'));
|
||||
const Settings = React.lazy(() => import('~/views/apps/settings/settings'));
|
||||
const Profile = React.lazy(() => import('~/views/apps/profile/profile'));
|
||||
const Notifications = React.lazy(() => import('~/views/apps/notifications/notifications'));
|
||||
const ErrorComponent = React.lazy(() => import('~/views/components/Error'));
|
||||
|
||||
export const Content = () => {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
|
@ -4,7 +4,7 @@ import Helmet from 'react-helmet';
|
||||
import { Box } from '@tlon/indigo-react';
|
||||
import {
|
||||
Route,
|
||||
RouteComponentProps, Switch
|
||||
RouteComponentProps, Switch, useLocation
|
||||
} from 'react-router-dom';
|
||||
import { useShortcut } from '~/logic/state/settings';
|
||||
import { useLocalStorageState } from '~/logic/lib/useLocalStorageState';
|
||||
@ -21,20 +21,21 @@ import { PopoverRoutes } from './PopoverRoutes';
|
||||
import { Skeleton } from './Skeleton';
|
||||
import { EmptyGroupHome } from './Home/EmptyGroupHome';
|
||||
import { Join } from './Join/Join';
|
||||
import { Resource } from './Resource';
|
||||
import { DmResource } from '~/views/apps/chat/DmResource';
|
||||
import { UnjoinedResource } from '~/views/components/UnjoinedResource';
|
||||
import { NewChannel } from './NewChannel';
|
||||
import { GroupHome } from './Home/GroupHome';
|
||||
import useGraphState from '~/logic/state/graph';
|
||||
|
||||
interface GroupsPaneProps {
|
||||
baseUrl: string;
|
||||
workspace: Workspace;
|
||||
}
|
||||
|
||||
const DmResource = React.lazy(() => import('~/views/apps/chat/DmResource').then(module => ({ default: module.DmResource })));
|
||||
const Resource = React.lazy(() => import('./Resource').then(module => ({ default: module.Resource })));
|
||||
const UnjoinedResource = React.lazy(() => import('~/views/components/UnjoinedResource').then(module => ({ default: module.UnjoinedResource })));
|
||||
const GroupHome = React.lazy(() => import('./Home/GroupHome').then(module => ({ default: module.GroupHome })));
|
||||
const NewChannel = React.lazy(() => import('./NewChannel').then(module => ({ default: module.NewChannel })));
|
||||
|
||||
export function GroupsPane(props: GroupsPaneProps) {
|
||||
const { baseUrl, workspace } = props;
|
||||
const location = useLocation();
|
||||
const associations = useMetadataState(state => state.associations);
|
||||
const notificationsCount = useHarkState(state => state.notificationsCount);
|
||||
|
||||
@ -56,6 +57,20 @@ export function GroupsPane(props: GroupsPaneProps) {
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const {
|
||||
getKeys,
|
||||
getShallowChildren
|
||||
} = useGraphState.getState();
|
||||
|
||||
useHarkState.getState().getUnreads();
|
||||
getKeys();
|
||||
|
||||
if (location.pathname.startsWith('/~landscape/messages')) {
|
||||
getShallowChildren(`~${window.ship}`, 'dm-inbox');
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace.type !== 'group') {
|
||||
return;
|
||||
@ -93,170 +108,170 @@ export function GroupsPane(props: GroupsPaneProps) {
|
||||
);
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route
|
||||
path={relativePath('/dm/:ship')}
|
||||
render={({ match }) => {
|
||||
const { ship } = match.params as Record<string, string>;
|
||||
<Switch>
|
||||
<Route
|
||||
path={relativePath('/dm/:ship')}
|
||||
render={({ match }) => {
|
||||
const { ship } = match.params as Record<string, string>;
|
||||
|
||||
return (
|
||||
<Skeleton
|
||||
mobileHide
|
||||
recentGroups={recentGroups}
|
||||
selected={ship}
|
||||
{...props}
|
||||
baseUrl={match.path}
|
||||
> <DmResource ship={ship} />
|
||||
|
||||
</Skeleton>
|
||||
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
path={[relativePath('/resource/:app/(ship)?/:host/:name')]}
|
||||
render={(routeProps) => {
|
||||
const { app, host, name } = routeProps.match.params as Record<
|
||||
string,
|
||||
string
|
||||
>;
|
||||
|
||||
const resource = `/ship/${host}/${name}`;
|
||||
const association = associations.graph[resource];
|
||||
const resourceUrl = `${baseUrl}/resource/${app}${resource}`;
|
||||
|
||||
if (!association) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Skeleton
|
||||
mobileHide
|
||||
recentGroups={recentGroups}
|
||||
selected={resource}
|
||||
{...props}
|
||||
baseUrl={resourceUrl}
|
||||
>
|
||||
<Resource
|
||||
workspace={props.workspace}
|
||||
association={association}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
{popovers(routeProps, resourceUrl)}
|
||||
</Skeleton>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
path={relativePath('/join/:app/(ship)?/:host/:name')}
|
||||
render={(routeProps) => {
|
||||
const { app, host, name } = routeProps.match.params;
|
||||
const appPath = `/ship/${host}/${name}`;
|
||||
const association = associations.graph[appPath];
|
||||
const resourceUrl = `${baseUrl}/join/${app}${appPath}`;
|
||||
let title = groupAssociation?.metadata?.title ?? 'Groups';
|
||||
|
||||
if (!association) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
title += ` - ${association.metadata.title}`;
|
||||
return (
|
||||
<>
|
||||
<Helmet defer={false}>
|
||||
<title>{notificationsCount ? `(${String(notificationsCount)}) ` : ''}{ title }</title>
|
||||
</Helmet>
|
||||
return (
|
||||
<Skeleton
|
||||
recentGroups={recentGroups}
|
||||
mobileHide
|
||||
selected={appPath}
|
||||
recentGroups={recentGroups}
|
||||
selected={ship}
|
||||
{...props}
|
||||
baseUrl={baseUrl}
|
||||
baseUrl={match.path}
|
||||
> <DmResource ship={ship} />
|
||||
|
||||
</Skeleton>
|
||||
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
path={[relativePath('/resource/:app/(ship)?/:host/:name')]}
|
||||
render={(routeProps) => {
|
||||
const { app, host, name } = routeProps.match.params as Record<
|
||||
string,
|
||||
string
|
||||
>;
|
||||
|
||||
const resource = `/ship/${host}/${name}`;
|
||||
const association = associations.graph[resource];
|
||||
const resourceUrl = `${baseUrl}/resource/${app}${resource}`;
|
||||
|
||||
if (!association) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Skeleton
|
||||
mobileHide
|
||||
recentGroups={recentGroups}
|
||||
selected={resource}
|
||||
{...props}
|
||||
baseUrl={resourceUrl}
|
||||
>
|
||||
<UnjoinedResource
|
||||
baseUrl={baseUrl}
|
||||
<Resource
|
||||
workspace={props.workspace}
|
||||
association={association}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
{popovers(routeProps, resourceUrl)}
|
||||
</Skeleton>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
path={relativePath('/pending/:ship/:name')}
|
||||
render={(routeProps) => {
|
||||
const { ship, name } = routeProps.match.params as Record<string, string>;
|
||||
const desc = {
|
||||
group: `/ship/${ship}/${name}`,
|
||||
kind: 'graph' as const
|
||||
};
|
||||
return (
|
||||
<Skeleton
|
||||
mobileHide
|
||||
recentGroups={recentGroups}
|
||||
{...props}
|
||||
baseUrl={baseUrl}
|
||||
>
|
||||
<Box overflow="auto">
|
||||
<Join desc={desc} />
|
||||
</Box>
|
||||
</Skeleton>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
path={relativePath('/new')}
|
||||
render={(routeProps) => {
|
||||
return (
|
||||
<Skeleton mobileHide recentGroups={recentGroups} {...props} baseUrl={baseUrl}>
|
||||
<NewChannel
|
||||
{...routeProps}
|
||||
baseUrl={baseUrl}
|
||||
group={groupPath}
|
||||
workspace={workspace}
|
||||
/>
|
||||
{popovers(routeProps, baseUrl)}
|
||||
</Skeleton>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
path={[relativePath('/'), relativePath('/feed+')]}
|
||||
render={(routeProps) => {
|
||||
const shouldHideSidebar =
|
||||
routeProps.location.pathname.includes('/feed');
|
||||
const title = groupAssociation?.metadata?.title ?? 'Groups';
|
||||
return (
|
||||
<>
|
||||
<Helmet defer={false}>
|
||||
<title>
|
||||
{notificationsCount ? `(${String(notificationsCount)}) ` : ''}
|
||||
{ title }
|
||||
</title>
|
||||
</Helmet>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
path={relativePath('/join/:app/(ship)?/:host/:name')}
|
||||
render={(routeProps) => {
|
||||
const { app, host, name } = routeProps.match.params;
|
||||
const appPath = `/ship/${host}/${name}`;
|
||||
const association = associations.graph[appPath];
|
||||
const resourceUrl = `${baseUrl}/join/${app}${appPath}`;
|
||||
let title = groupAssociation?.metadata?.title ?? 'Groups';
|
||||
|
||||
if (!association) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
title += ` - ${association.metadata.title}`;
|
||||
return (
|
||||
<>
|
||||
<Helmet defer={false}>
|
||||
<title>{notificationsCount ? `(${String(notificationsCount)}) ` : ''}{ title }</title>
|
||||
</Helmet>
|
||||
<Skeleton
|
||||
recentGroups={recentGroups}
|
||||
mobileHide
|
||||
selected={appPath}
|
||||
{...props}
|
||||
baseUrl={baseUrl}
|
||||
>
|
||||
<UnjoinedResource
|
||||
baseUrl={baseUrl}
|
||||
association={association}
|
||||
/>
|
||||
{popovers(routeProps, resourceUrl)}
|
||||
</Skeleton>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
path={relativePath('/pending/:ship/:name')}
|
||||
render={(routeProps) => {
|
||||
const { ship, name } = routeProps.match.params as Record<string, string>;
|
||||
const desc = {
|
||||
group: `/ship/${ship}/${name}`,
|
||||
kind: 'graph' as const
|
||||
};
|
||||
return (
|
||||
<Skeleton
|
||||
{...props}
|
||||
mobileHide={shouldHideSidebar}
|
||||
mobileHide
|
||||
recentGroups={recentGroups}
|
||||
{...props}
|
||||
baseUrl={baseUrl}
|
||||
>
|
||||
{ workspace.type === 'group' ? (
|
||||
<GroupHome
|
||||
baseUrl={baseUrl}
|
||||
groupPath={groupPath}
|
||||
/>
|
||||
) : (
|
||||
<EmptyGroupHome
|
||||
associations={associations}
|
||||
/>
|
||||
)}
|
||||
<Box overflow="auto">
|
||||
<Join desc={desc} />
|
||||
</Box>
|
||||
</Skeleton>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
path={relativePath('/new')}
|
||||
render={(routeProps) => {
|
||||
return (
|
||||
<Skeleton mobileHide recentGroups={recentGroups} {...props} baseUrl={baseUrl}>
|
||||
<NewChannel
|
||||
{...routeProps}
|
||||
baseUrl={baseUrl}
|
||||
group={groupPath}
|
||||
workspace={workspace}
|
||||
/>
|
||||
{popovers(routeProps, baseUrl)}
|
||||
</Skeleton>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Switch>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
path={[relativePath('/'), relativePath('/feed+')]}
|
||||
render={(routeProps) => {
|
||||
const shouldHideSidebar =
|
||||
routeProps.location.pathname.includes('/feed');
|
||||
const title = groupAssociation?.metadata?.title ?? 'Groups';
|
||||
return (
|
||||
<>
|
||||
<Helmet defer={false}>
|
||||
<title>
|
||||
{notificationsCount ? `(${String(notificationsCount)}) ` : ''}
|
||||
{ title }
|
||||
</title>
|
||||
</Helmet>
|
||||
<Skeleton
|
||||
{...props}
|
||||
mobileHide={shouldHideSidebar}
|
||||
recentGroups={recentGroups}
|
||||
baseUrl={baseUrl}
|
||||
>
|
||||
{ workspace.type === 'group' ? (
|
||||
<GroupHome
|
||||
baseUrl={baseUrl}
|
||||
groupPath={groupPath}
|
||||
/>
|
||||
) : (
|
||||
<EmptyGroupHome
|
||||
associations={associations}
|
||||
/>
|
||||
)}
|
||||
{popovers(routeProps, baseUrl)}
|
||||
</Skeleton>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
@ -70,7 +70,7 @@ export function NewChannel(props: NewChannelProps): ReactElement {
|
||||
const onSubmit = async (values: FormSchema, actions) => {
|
||||
const name = channelName(values);
|
||||
const resId: string =
|
||||
stringToSymbol(values.name) +
|
||||
await stringToSymbol(values.name) +
|
||||
(workspace?.type !== 'messages'
|
||||
? `-${Math.floor(Math.random() * 10000)}`
|
||||
: '');
|
||||
|
@ -45,7 +45,7 @@ export function NewGroup(): ReactElement {
|
||||
async (values: FormSchema, actions: FormikHelpers<FormSchema>) => {
|
||||
try {
|
||||
const { title, description, isPrivate } = values;
|
||||
const name = stringToSymbol(title.trim());
|
||||
const name = await stringToSymbol(title.trim());
|
||||
const policy: Enc<GroupPolicy> = isPrivate
|
||||
? {
|
||||
invite: {
|
||||
|
@ -385,6 +385,14 @@ export const addDmMessage = (our: PatpNoSig, ship: Patp, contents: Content[]): P
|
||||
|
||||
const encodeIndex = (idx: string) => idx.split('/').map(decToUd).join('/');
|
||||
|
||||
/**
|
||||
* Fetch all graph keys
|
||||
*/
|
||||
export const getKeys = (): Scry => ({
|
||||
app: 'graph-store',
|
||||
path: '/keys'
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetch newest (larger keys) nodes in a graph under some index
|
||||
*
|
||||
|
Loading…
Reference in New Issue
Block a user