Merge branch 'release/next-js' into release/next-userspace

This commit is contained in:
Matilde Park 2021-03-15 13:18:04 -04:00
commit dfe186b96e
38 changed files with 522 additions and 365 deletions

View File

@ -94,7 +94,11 @@ module.exports = {
use: { use: {
loader: 'babel-loader', loader: 'babel-loader',
options: { options: {
presets: ['@babel/preset-env', '@babel/typescript', '@babel/preset-react'], presets: ['@babel/preset-env', '@babel/typescript', ['@babel/preset-react', {
runtime: 'automatic',
development: true,
importSource: '@welldone-software/why-did-you-render',
}]],
plugins: [ plugins: [
'@babel/transform-runtime', '@babel/transform-runtime',
'@babel/plugin-proposal-object-rest-spread', '@babel/plugin-proposal-object-rest-spread',

View File

@ -1995,6 +1995,15 @@
"@xtuc/long": "4.2.2" "@xtuc/long": "4.2.2"
} }
}, },
"@welldone-software/why-did-you-render": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@welldone-software/why-did-you-render/-/why-did-you-render-6.1.0.tgz",
"integrity": "sha512-0s+PuKQ4v9VV1SZSM6iS7d2T7X288T3DF+K8yfkFAhI31HhJGGH1SY1ssVm+LqjSMyrVWT60ZF5r0qUsO0Z9Lw==",
"dev": true,
"requires": {
"lodash": "^4"
}
},
"@xtuc/ieee754": { "@xtuc/ieee754": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",

View File

@ -71,6 +71,7 @@
"@types/yup": "^0.29.11", "@types/yup": "^0.29.11",
"@typescript-eslint/eslint-plugin": "^4.15.0", "@typescript-eslint/eslint-plugin": "^4.15.0",
"@urbit/eslint-config": "file:../npm/eslint-config", "@urbit/eslint-config": "file:../npm/eslint-config",
"@welldone-software/why-did-you-render": "^6.1.0",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"babel-loader": "^8.2.2", "babel-loader": "^8.2.2",
"babel-plugin-lodash": "^3.3.4", "babel-plugin-lodash": "^3.3.4",

View File

@ -1,3 +1,4 @@
import './wdyr';
import * as React from 'react'; import * as React from 'react';
import * as ReactDOM from 'react-dom'; import * as ReactDOM from 'react-dom';

View File

@ -77,7 +77,6 @@ export default class MetadataApi extends BaseApi<StoreState> {
tempChannel.delete(); tempChannel.delete();
}, },
(ev: any) => { (ev: any) => {
console.log(ev);
if ('metadata-hook-update' in ev) { if ('metadata-hook-update' in ev) {
done = true; done = true;
tempChannel.delete(); tempChannel.delete();

View File

@ -23,9 +23,10 @@ export const Sigil = memo(
size, size,
svgClass = '', svgClass = '',
icon = false, icon = false,
padding = 0 padding = 0,
display = 'inline-block'
}) => { }) => {
const innerSize = Number(size) - 2*padding; const innerSize = Number(size) - 2 * padding;
const paddingPx = `${padding}px`; const paddingPx = `${padding}px`;
const foregroundColor = foreground const foregroundColor = foreground
? foreground ? foreground
@ -34,14 +35,14 @@ export const Sigil = memo(
<Box <Box
backgroundColor={color} backgroundColor={color}
borderRadius={icon ? '1' : '0'} borderRadius={icon ? '1' : '0'}
display='inline-block' display={display}
height={size} height={size}
width={size} width={size}
className={classes} className={classes}
/> />
) : ( ) : (
<Box <Box
display='inline-block' display={display}
borderRadius={icon ? '1' : '0'} borderRadius={icon ? '1' : '0'}
flexBasis={size} flexBasis={size}
backgroundColor={color} backgroundColor={color}

View File

@ -133,7 +133,7 @@ function graphWatchSelf(json: any, state: HarkState): HarkState {
function readAll(json: any, state: HarkState): HarkState { function readAll(json: any, state: HarkState): HarkState {
const data = _.get(json, 'read-all'); const data = _.get(json, 'read-all');
if(data) { if(data) {
clearState(state); state = clearState(state);
} }
return state; return state;
} }
@ -149,15 +149,15 @@ function removeGraph(json: any, state: HarkState): HarkState {
function seenIndex(json: any, state: HarkState): HarkState { function seenIndex(json: any, state: HarkState): HarkState {
const data = _.get(json, 'seen-index'); const data = _.get(json, 'seen-index');
if(data) { if(data) {
updateNotificationStats(state, data.index, 'last', () => data.time); state = updateNotificationStats(state, data.index, 'last', () => data.time);
} }
return state; return state;
} }
function readEach(json: any, state: HarkState): HarkState { function readEach(json: any, state: HarkState): HarkState {
const data = _.get(json, 'read-each'); const data = _.get(json, 'read-each');
if(data) { if (data) {
updateUnreads(state, data.index, u => u.delete(data.target)); state = updateUnreads(state, data.index, u => u.delete(data.target));
} }
return state; return state;
} }
@ -165,7 +165,7 @@ function readEach(json: any, state: HarkState): HarkState {
function readSince(json: any, state: HarkState): HarkState { function readSince(json: any, state: HarkState): HarkState {
const data = _.get(json, 'read-count'); const data = _.get(json, 'read-count');
if(data) { if(data) {
updateUnreadCount(state, data, () => 0); state = updateUnreadCount(state, data, () => 0);
} }
return state; return state;
} }
@ -173,7 +173,7 @@ function readSince(json: any, state: HarkState): HarkState {
function unreadSince(json: any, state: HarkState): HarkState { function unreadSince(json: any, state: HarkState): HarkState {
const data = _.get(json, 'unread-count'); const data = _.get(json, 'unread-count');
if(data) { if(data) {
updateUnreadCount(state, data.index, u => u + 1); state = updateUnreadCount(state, data.index, u => u + 1);
} }
return state; return state;
} }
@ -181,7 +181,7 @@ function unreadSince(json: any, state: HarkState): HarkState {
function unreadEach(json: any, state: HarkState): HarkState { function unreadEach(json: any, state: HarkState): HarkState {
const data = _.get(json, 'unread-each'); const data = _.get(json, 'unread-each');
if(data) { if(data) {
updateUnreads(state, data.index, us => us.add(data.target)); state = updateUnreads(state, data.index, us => us.add(data.target));
} }
return state; return state;
} }
@ -191,13 +191,14 @@ function unreads(json: any, state: HarkState): HarkState {
if(data) { if(data) {
data.forEach(({ index, stats }) => { data.forEach(({ index, stats }) => {
const { unreads, notifications, last } = stats; const { unreads, notifications, last } = stats;
updateNotificationStats(state, index, 'notifications', x => x + notifications); state = updateNotificationStats(state, index, 'notifications', x => x + notifications);
updateNotificationStats(state, index, 'last', () => last); state = updateNotificationStats(state, index, 'last', () => last);
if('count' in unreads) { if('count' in unreads) {
updateUnreadCount(state, index, (u = 0) => u + unreads.count); state = updateUnreadCount(state, index, (u = 0) => u + unreads.count);
} else { } else {
state = updateUnreads(state, index, s => new Set());
unreads.each.forEach((u: string) => { unreads.each.forEach((u: string) => {
updateUnreads(state, index, s => s.add(u)); state = updateUnreads(state, index, s => s.add(u));
}); });
} }
}); });
@ -205,7 +206,7 @@ function unreads(json: any, state: HarkState): HarkState {
return state; return state;
} }
function clearState(state: HarkState) { function clearState(state: HarkState): HarkState {
const initialState = { const initialState = {
notifications: new BigIntOrderedMap<Timebox>(), notifications: new BigIntOrderedMap<Timebox>(),
archivedNotifications: new BigIntOrderedMap<Timebox>(), archivedNotifications: new BigIntOrderedMap<Timebox>(),
@ -225,6 +226,7 @@ function clearState(state: HarkState) {
Object.keys(initialState).forEach((key) => { Object.keys(initialState).forEach((key) => {
state[key] = initialState[key]; state[key] = initialState[key];
}); });
return state;
} }
function updateUnreadCount(state: HarkState, index: NotifIndex, count: (c: number) => number): HarkState { function updateUnreadCount(state: HarkState, index: NotifIndex, count: (c: number) => number): HarkState {
@ -242,10 +244,9 @@ function updateUnreads(state: HarkState, index: NotifIndex, f: (us: Set<string>)
if(!('graph' in index)) { if(!('graph' in index)) {
return state; return state;
} }
const unreads = _.get(state.unreads.graph, [index.graph.graph, index.graph.index, 'unreads'], new Set<string>()); let unreads = _.get(state.unreads.graph, [index.graph.graph, index.graph.index, 'unreads'], new Set<string>());
const oldSize = unreads.size;
f(unreads); f(unreads);
const newSize = unreads.size;
_.set(state.unreads.graph, [index.graph.graph, index.graph.index, 'unreads'], unreads); _.set(state.unreads.graph, [index.graph.graph, index.graph.index, 'unreads'], unreads);
return state; return state;
} }
@ -254,7 +255,7 @@ function updateNotificationStats(state: HarkState, index: NotifIndex, statField:
if(statField === 'notifications') { if(statField === 'notifications') {
state.notificationsCount = f(state.notificationsCount); state.notificationsCount = f(state.notificationsCount);
} }
if('graph' in index) { if ('graph' in index) {
const curr = _.get(state.unreads.graph, [index.graph.graph, index.graph.index, statField], 0); const curr = _.get(state.unreads.graph, [index.graph.graph, index.graph.index, statField], 0);
_.set(state.unreads.graph, [index.graph.graph, index.graph.index, statField], f(curr)); _.set(state.unreads.graph, [index.graph.graph, index.graph.index, statField], f(curr));
} else if('group' in index) { } else if('group' in index) {
@ -276,7 +277,6 @@ function added(json: any, state: HarkState): HarkState {
); );
if (arrIdx !== -1) { if (arrIdx !== -1) {
if (timebox[arrIdx]?.notification?.read) { if (timebox[arrIdx]?.notification?.read) {
// TODO this is additive, and with a persistent state it keeps incrementing
state = updateNotificationStats(state, index, 'notifications', x => x+1); state = updateNotificationStats(state, index, 'notifications', x => x+1);
} }
timebox[arrIdx] = { index, notification }; timebox[arrIdx] = { index, notification };
@ -361,7 +361,7 @@ function read(json: any, state: HarkState): HarkState {
const data = _.get(json, 'read-note', false); const data = _.get(json, 'read-note', false);
if (data) { if (data) {
const { time, index } = data; const { time, index } = data;
updateNotificationStats(state, index, 'notifications', x => x-1); state = updateNotificationStats(state, index, 'notifications', x => x-1);
setRead(time, index, true, state); setRead(time, index, true, state);
} }
return state; return state;
@ -371,7 +371,7 @@ function unread(json: any, state: HarkState): HarkState {
const data = _.get(json, 'unread-note', false); const data = _.get(json, 'unread-note', false);
if (data) { if (data) {
const { time, index } = data; const { time, index } = data;
updateNotificationStats(state, index, 'notifications', x => x+1); state = updateNotificationStats(state, index, 'notifications', x => x+1);
setRead(time, index, false, state); setRead(time, index, false, state);
} }
return state; return state;
@ -397,7 +397,7 @@ function archive(json: any, state: HarkState): HarkState {
state.notifications.set(time, unarchived); state.notifications.set(time, unarchived);
} }
const newlyRead = archived.filter(x => !x.notification.read).length; const newlyRead = archived.filter(x => !x.notification.read).length;
updateNotificationStats(state, index, 'notifications', x => x - newlyRead); state = updateNotificationStats(state, index, 'notifications', x => x - newlyRead);
} }
return state; return state;
} }

View File

@ -34,7 +34,7 @@ export const reduceState = <
export let stateStorageKeys: string[] = []; export let stateStorageKeys: string[] = [];
export const stateStorageKey = (stateName: string) => { export const stateStorageKey = (stateName: string) => {
stateName = `Landcape${stateName}State`; stateName = `Landscape${stateName}State`;
stateStorageKeys = [...new Set([...stateStorageKeys, stateName])]; stateStorageKeys = [...new Set([...stateStorageKeys, stateName])];
return stateName; return stateName;
}; };

View File

@ -24,7 +24,7 @@ type ChatInputProps = IuseStorage & {
message: string; message: string;
deleteMessage(): void; deleteMessage(): void;
hideAvatars: boolean; hideAvatars: boolean;
} };
interface ChatInputState { interface ChatInputState {
inCodeMode: boolean; inCodeMode: boolean;
@ -60,20 +60,23 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
submit(text) { submit(text) {
const { props, state } = this; const { props, state } = this;
const [,,ship,name] = props.station.split('/'); const [, , ship, name] = props.station.split('/');
if (state.inCodeMode) { if (state.inCodeMode) {
this.setState({ this.setState(
inCodeMode: false {
}, async () => { inCodeMode: false
const output = await props.api.graph.eval(text); },
const contents: Content[] = [{ code: { output, expression: text } }]; async () => {
const post = createPost(contents); const output = await props.api.graph.eval(text);
props.api.graph.addPost(ship, name, post); const contents: Content[] = [{ code: { output, expression: text } }];
}); const post = createPost(contents);
props.api.graph.addPost(ship, name, post);
}
);
return; return;
} }
const post = createPost(tokenizeMessage((text))); const post = createPost(tokenizeMessage(text));
props.deleteMessage(); props.deleteMessage();
@ -86,8 +89,8 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
this.chatEditor.current.editor.setValue(url); this.chatEditor.current.editor.setValue(url);
this.setState({ uploadingPaste: false }); this.setState({ uploadingPaste: false });
} else { } else {
const [,,ship,name] = props.station.split('/'); const [, , ship, name] = props.station.split('/');
props.api.graph.addPost(ship,name, createPost([{ url }])); props.api.graph.addPost(ship, name, createPost([{ url }]));
} }
} }
@ -110,7 +113,8 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
return; return;
} }
Array.from(files).forEach((file) => { Array.from(files).forEach((file) => {
this.props.uploadDefault(file) this.props
.uploadDefault(file)
.then(this.uploadSuccess) .then(this.uploadSuccess)
.catch(this.uploadError); .catch(this.uploadError);
}); });
@ -119,32 +123,40 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
render() { render() {
const { props, state } = this; const { props, state } = this;
const color = props.ourContact const color = props.ourContact ? uxToHex(props.ourContact.color) : '000000';
? uxToHex(props.ourContact.color) : '000000';
const sigilClass = props.ourContact const sigilClass = props.ourContact ? '' : 'mix-blend-diff';
? '' : 'mix-blend-diff';
const avatar = ( const avatar =
props.ourContact && props.ourContact && props.ourContact?.avatar && !props.hideAvatars ? (
((props.ourContact?.avatar) && !props.hideAvatars) <BaseImage
)
? <BaseImage
src={props.ourContact.avatar} src={props.ourContact.avatar}
height={16} height={24}
width={16} width={24}
style={{ objectFit: 'cover' }} style={{ objectFit: 'cover' }}
borderRadius={1} borderRadius={1}
display='inline-block' display='inline-block'
/> />
: <Sigil ) : (
ship={window.ship} <Box
size={16} width={24}
color={`#${color}`} height={24}
classes={sigilClass} display='flex'
icon justifyContent='center'
padding={2} alignItems='center'
/>; backgroundColor={`#${color}`}
borderRadius={1}
>
<Sigil
ship={window.ship}
size={16}
color={`#${color}`}
classes={sigilClass}
icon
padding={2}
/>
</Box>
);
return ( return (
<Row <Row
@ -158,7 +170,7 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
className='cf' className='cf'
zIndex={0} zIndex={0}
> >
<Row p='2' alignItems='center'> <Row p='12px 4px 12px 12px' alignItems='center'>
{avatar} {avatar}
</Row> </Row>
<ChatEditor <ChatEditor
@ -170,31 +182,23 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
onPaste={this.onPaste.bind(this)} onPaste={this.onPaste.bind(this)}
placeholder='Message...' placeholder='Message...'
/> />
<Box <Box mx={2} flexShrink={0} height='16px' width='16px' flexBasis='16px'>
mx={2} {this.props.canUpload ? (
flexShrink={0} this.props.uploading ? (
height='16px' <LoadingSpinner />
width='16px' ) : (
flexBasis='16px' <Icon
> icon='Links'
{this.props.canUpload width='16'
? this.props.uploading height='16'
? <LoadingSpinner /> onClick={() =>
: <Icon icon='Links' this.props.promptUpload().then(this.uploadSuccess)
width="16" }
height="16" />
onClick={() => this.props.promptUpload().then(this.uploadSuccess)} )
/> ) : null}
: null
}
</Box> </Box>
<Box <Box mr={2} flexShrink={0} height='16px' width='16px' flexBasis='16px'>
mr={2}
flexShrink={0}
height='16px'
width='16px'
flexBasis='16px'
>
<Icon <Icon
icon='Dojo' icon='Dojo'
onClick={this.toggleCode} onClick={this.toggleCode}
@ -206,4 +210,6 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
} }
} }
export default withLocalState(withStorage(ChatInput, { accept: 'image/*' }), ['hideAvatars']); export default withLocalState(withStorage(ChatInput, { accept: 'image/*' }), [
'hideAvatars'
]);

View File

@ -10,7 +10,7 @@ import React, {
import moment from 'moment'; import moment from 'moment';
import _ from 'lodash'; import _ from 'lodash';
import VisibilitySensor from 'react-visibility-sensor'; import VisibilitySensor from 'react-visibility-sensor';
import { Box, Row, Text, Rule, BaseImage } from '@tlon/indigo-react'; import { Box, Row, Text, Rule, BaseImage, Icon, Col } from '@tlon/indigo-react';
import { Sigil } from '~/logic/lib/sigil'; import { Sigil } from '~/logic/lib/sigil';
import OverlaySigil from '~/views/components/OverlaySigil'; import OverlaySigil from '~/views/components/OverlaySigil';
import { import {
@ -33,12 +33,13 @@ import TextContent from './content/text';
import CodeContent from './content/code'; import CodeContent from './content/code';
import RemoteContent from '~/views/components/RemoteContent'; import RemoteContent from '~/views/components/RemoteContent';
import { Mention } from '~/views/components/MentionText'; import { Mention } from '~/views/components/MentionText';
import { Dropdown } from '~/views/components/Dropdown';
import styled from 'styled-components'; import styled from 'styled-components';
import useLocalState from '~/logic/state/local'; import useLocalState from '~/logic/state/local';
import useSettingsState, {selectCalmState} from "~/logic/state/settings"; import useSettingsState, { selectCalmState } from '~/logic/state/settings';
import Timestamp from '~/views/components/Timestamp'; import Timestamp from '~/views/components/Timestamp';
import useContactState from '~/logic/state/contact'; import useContactState from '~/logic/state/contact';
import {useIdlingState} from '~/logic/lib/idling'; import { useIdlingState } from '~/logic/lib/idling';
export const DATESTAMP_FORMAT = '[~]YYYY.M.D'; export const DATESTAMP_FORMAT = '[~]YYYY.M.D';
@ -64,39 +65,156 @@ export const DayBreak = ({ when, shimTop = false }: DayBreakProps) => (
</Row> </Row>
); );
export const UnreadMarker = React.forwardRef(({ dayBreak, when, api, association }, ref) => { export const UnreadMarker = React.forwardRef(
const [visible, setVisible] = useState(false); ({ dayBreak, when, api, association }, ref) => {
const idling = useIdlingState(); const [visible, setVisible] = useState(false);
const dismiss = useCallback(() => { const idling = useIdlingState();
api.hark.markCountAsRead(association, '/', 'message'); const dismiss = useCallback(() => {
}, [api, association]); api.hark.markCountAsRead(association, '/', 'message');
}, [api, association]);
useEffect(() => { useEffect(() => {
if(visible && !idling) { if (visible && !idling) {
dismiss(); dismiss();
} }
}, [visible, idling]); }, [visible, idling]);
return (
<Row
position='absolute'
ref={ref}
px={2}
mt={2}
height={5}
justifyContent='center'
alignItems='center'
width='100%'
>
<Rule borderColor='lightBlue' />
<VisibilitySensor onChange={setVisible}>
<Text color='blue' fontSize={0} flexShrink='0' px={2}>
New messages below
</Text>
</VisibilitySensor>
<Rule borderColor='lightBlue' />
</Row>
);
}
);
const MessageActionItem = (props) => {
return ( return (
<Row <Row
position='absolute' color='black'
ref={ref} cursor='pointer'
px={2} fontSize={1}
mt={2} fontWeight='500'
height={5} px={3}
justifyContent='center' py={2}
alignItems='center' onClick={props.onClick}
width='100%' >
> <Text fontWeight='500' color={props.color}>
<Rule borderColor='lightBlue' /> {props.children}
<VisibilitySensor onChange={setVisible}> </Text>
<Text color='blue' fontSize={0} flexShrink='0' px={2}> </Row>
New messages below );
</Text> };
</VisibilitySensor>
<Rule borderColor='lightBlue' /> const MessageActions = ({ api, history, msg, group }) => {
</Row> const isAdmin = () => group.tags.role.admin.has(window.ship);
)}); const isOwn = () => msg.author === window.ship;
return (
<Box
borderRadius={1}
background='white'
border='1px solid'
borderColor='lightGray'
position='absolute'
top='-12px'
right={2}
>
<Row>
{isOwn() ? (
<Box
padding={1}
size={'24px'}
cursor='pointer'
onClick={(e) => console.log(e)}
>
<Icon icon='NullIcon' size={3} />
</Box>
) : null}
<Box
padding={1}
size={'24px'}
cursor='pointer'
onClick={(e) => console.log(e)}
>
<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'
>
{isOwn() ? (
<MessageActionItem onClick={(e) => console.log(e)}>
Edit Message
</MessageActionItem>
) : null}
<MessageActionItem onClick={(e) => console.log(e)}>
Reply
</MessageActionItem>
<MessageActionItem onClick={(e) => console.log(e)}>
Copy Message Link
</MessageActionItem>
{isAdmin() || isOwn() ? (
<MessageActionItem onClick={(e) => console.log(e)} color='red'>
Delete Message
</MessageActionItem>
) : null}
<MessageActionItem onClick={(e) => console.log(e)}>
View Signature
</MessageActionItem>
</Col>
}
>
<Box padding={1} size={'24px'} cursor='pointer'>
<Icon icon='Menu' size={3} />
</Box>
</Dropdown>
</Row>
</Box>
);
};
const MessageWrapper = (props) => {
const { hovering, bind } = useHovering();
return (
<Box
py='1'
backgroundColor={hovering ? 'washedGray' : 'transparent'}
position='relative'
{...bind}
>
{props.children}
{/* {hovering ? <MessageActions {...props} /> : null} */}
</Box>
);
};
interface ChatMessageProps { interface ChatMessageProps {
msg: Post; msg: Post;
@ -126,8 +244,7 @@ class ChatMessage extends Component<ChatMessageProps> {
this.divRef = React.createRef(); this.divRef = React.createRef();
} }
componentDidMount() { componentDidMount() {}
}
render() { render() {
const { const {
@ -146,7 +263,7 @@ class ChatMessage extends Component<ChatMessageProps> {
history, history,
api, api,
highlighted, highlighted,
fontSize, fontSize
} = this.props; } = this.props;
let { renderSigil } = this.props; let { renderSigil } = this.props;
@ -170,7 +287,6 @@ class ChatMessage extends Component<ChatMessageProps> {
.unix(msg['time-sent'] / 1000) .unix(msg['time-sent'] / 1000)
.format(renderSigil ? 'h:mm A' : 'h:mm'); .format(renderSigil ? 'h:mm A' : 'h:mm');
const messageProps = { const messageProps = {
msg, msg,
timestamp, timestamp,
@ -183,7 +299,7 @@ class ChatMessage extends Component<ChatMessageProps> {
api, api,
scrollWindow, scrollWindow,
highlighted, highlighted,
fontSize, fontSize
}; };
const unreadContainerStyle = { const unreadContainerStyle = {
@ -194,7 +310,7 @@ class ChatMessage extends Component<ChatMessageProps> {
<Box <Box
ref={this.props.innerRef} ref={this.props.innerRef}
pt={renderSigil ? 2 : 0} pt={renderSigil ? 2 : 0}
pb={isLastMessage ? 4 : 2} pb={isLastMessage ? '20px' : 0}
className={containerClass} className={containerClass}
backgroundColor={highlighted ? 'blue' : 'white'} backgroundColor={highlighted ? 'blue' : 'white'}
style={style} style={style}
@ -203,12 +319,14 @@ class ChatMessage extends Component<ChatMessageProps> {
<DayBreak when={msg['time-sent']} shimTop={renderSigil} /> <DayBreak when={msg['time-sent']} shimTop={renderSigil} />
) : null} ) : null}
{renderSigil ? ( {renderSigil ? (
<> <MessageWrapper {...messageProps}>
<MessageAuthor pb={'2px'} {...messageProps} /> <MessageAuthor pb={1} {...messageProps} />
<Message pl={5} pr={4} {...messageProps} /> <Message pl={'44px'} pr={4} {...messageProps} />
</> </MessageWrapper>
) : ( ) : (
<Message pl={5} pr={4} timestampHover {...messageProps} /> <MessageWrapper {...messageProps}>
<Message pl={'44px'} pr={4} timestampHover {...messageProps} />
</MessageWrapper>
)} )}
<Box style={unreadContainerStyle}> <Box style={unreadContainerStyle}>
{isLastRead ? ( {isLastRead ? (
@ -226,7 +344,9 @@ class ChatMessage extends Component<ChatMessageProps> {
} }
} }
export default React.forwardRef((props, ref) => <ChatMessage {...props} innerRef={ref} />); export default React.forwardRef((props, ref) => (
<ChatMessage {...props} innerRef={ref} />
));
export const MessageAuthor = ({ export const MessageAuthor = ({
timestamp, timestamp,
@ -239,9 +359,9 @@ export const MessageAuthor = ({
}) => { }) => {
const osDark = useLocalState((state) => state.dark); const osDark = useLocalState((state) => state.dark);
const theme = useSettingsState(s => s.display.theme); const theme = useSettingsState((s) => s.display.theme);
const dark = theme === 'dark' || (theme === 'auto' && osDark); const dark = theme === 'dark' || (theme === 'auto' && osDark);
const contacts = useContactState(state => state.contacts); const contacts = useContactState((state) => state.contacts);
const datestamp = moment const datestamp = moment
.unix(msg['time-sent'] / 1000) .unix(msg['time-sent'] / 1000)
@ -291,19 +411,30 @@ export const MessageAuthor = ({
display='inline-block' display='inline-block'
style={{ objectFit: 'cover' }} style={{ objectFit: 'cover' }}
src={contact.avatar} src={contact.avatar}
height={16} height={24}
width={16} width={24}
borderRadius={1} borderRadius={1}
/> />
) : ( ) : (
<Sigil <Box
ship={msg.author} width={24}
size={16} height={24}
color={color} display='flex'
classes={sigilClass} justifyContent='center'
icon alignItems='center'
padding={2} backgroundColor={color}
/> borderRadius={1}
>
<Sigil
ship={msg.author}
size={12}
display='block'
color={color}
classes={sigilClass}
icon
padding={0}
/>
</Box>
); );
return ( return (
<Box display='flex' alignItems='center' {...rest}> <Box display='flex' alignItems='center' {...rest}>
@ -311,9 +442,9 @@ export const MessageAuthor = ({
onClick={() => { onClick={() => {
setShowOverlay(true); setShowOverlay(true);
}} }}
height={16} height={24}
pr={2} pr={2}
pl={2} pl={'12px'}
cursor='pointer' cursor='pointer'
position='relative' position='relative'
> >
@ -340,10 +471,10 @@ export const MessageAuthor = ({
pt={1} pt={1}
pb={1} pb={1}
display='flex' display='flex'
alignItems='center' alignItems='baseline'
> >
<Text <Text
fontSize={0} fontSize={1}
mr={2} mr={2}
flexShrink={0} flexShrink={0}
mono={nameMono} mono={nameMono}
@ -385,13 +516,15 @@ export const Message = ({
...rest ...rest
}) => { }) => {
const { hovering, bind } = useHovering(); const { hovering, bind } = useHovering();
const contacts = useContactState(state => state.contacts); const contacts = useContactState((state) => state.contacts);
return ( return (
<Box position='relative' {...rest}> <Box position='relative' {...rest}>
{timestampHover ? ( {timestampHover ? (
<Text <Text
display={hovering ? 'block' : 'none'} display={hovering ? 'block' : 'none'}
position='absolute' position='absolute'
width='36px'
textAlign='right'
left='0' left='0'
top='3px' top='3px'
fontSize={0} fontSize={0}
@ -408,6 +541,7 @@ export const Message = ({
case 'text': case 'text':
return ( return (
<TextContent <TextContent
key={i}
api={api} api={api}
fontSize={1} fontSize={1}
lineHeight={'20px'} lineHeight={'20px'}
@ -415,10 +549,11 @@ export const Message = ({
/> />
); );
case 'code': case 'code':
return <CodeContent content={content} />; return <CodeContent key={i} content={content} />;
case 'url': case 'url':
return ( return (
<Box <Box
key={i}
flexShrink={0} flexShrink={0}
fontSize={1} fontSize={1}
lineHeight='20px' lineHeight='20px'
@ -452,9 +587,10 @@ export const Message = ({
</Box> </Box>
); );
case 'mention': case 'mention':
const first = (i) => (i === 0); const first = (i) => i === 0;
return ( return (
<Mention <Mention
key={i}
first={first(i)} first={first(i)}
group={group} group={group}
scrollWindow={scrollWindow} scrollWindow={scrollWindow}

View File

@ -91,7 +91,7 @@ const MessageMarkdown = React.memo((props) => {
}, []); }, []);
return lines.map((line, i) => ( return lines.map((line, i) => (
<> <React.Fragment key={i}>
{i !== 0 && <Row height={2} />} {i !== 0 && <Row height={2} />}
<ReactMarkdown <ReactMarkdown
{...rest} {...rest}
@ -123,7 +123,7 @@ const MessageMarkdown = React.memo((props) => {
] ]
]} ]}
/> />
</> </React.Fragment>
)); ));
}); });

View File

@ -32,6 +32,7 @@ import {
} from '~/logic/lib/tutorialModal'; } from '~/logic/lib/tutorialModal';
import useLaunchState from '~/logic/state/launch'; import useLaunchState from '~/logic/state/launch';
import useSettingsState, { selectCalmState } from '~/logic/state/settings'; import useSettingsState, { selectCalmState } from '~/logic/state/settings';
import useMetadataState from '~/logic/state/metadata';
const ScrollbarLessBox = styled(Box)` const ScrollbarLessBox = styled(Box)`
@ -48,6 +49,7 @@ export default function LaunchApp(props) {
const baseHash = useLaunchState(state => state.baseHash); const baseHash = useLaunchState(state => state.baseHash);
const [hashText, setHashText] = useState(baseHash); const [hashText, setHashText] = useState(baseHash);
const [exitingTut, setExitingTut] = useState(false); const [exitingTut, setExitingTut] = useState(false);
const associations = useMetadataState(s => s.associations);
const hashBox = ( const hashBox = (
<Box <Box
position={["relative", "absolute"]} position={["relative", "absolute"]}
@ -78,7 +80,7 @@ export default function LaunchApp(props) {
useEffect(() => { useEffect(() => {
if(query.get('tutorial')) { if(query.get('tutorial')) {
if(hasTutorialGroup(props)) { if(hasTutorialGroup({ associations })) {
nextTutStep(); nextTutStep();
} else { } else {
showModal(); showModal();
@ -92,7 +94,7 @@ export default function LaunchApp(props) {
let { hideGroups } = useLocalState(tutSelector); let { hideGroups } = useLocalState(tutSelector);
!hideGroups ? { hideGroups } = calmState : null; !hideGroups ? { hideGroups } = calmState : null;
const waiter = useWaitForProps(props); const waiter = useWaitForProps({ ...props, associations });
const { modal, showModal } = useModal({ const { modal, showModal } = useModal({
position: 'relative', position: 'relative',
@ -105,7 +107,7 @@ export default function LaunchApp(props) {
}; };
const onContinue = async (e) => { const onContinue = async (e) => {
e.stopPropagation(); e.stopPropagation();
if(!hasTutorialGroup(props)) { if(!hasTutorialGroup({ associations })) {
await props.api.groups.join(TUTORIAL_HOST, TUTORIAL_GROUP); await props.api.groups.join(TUTORIAL_HOST, TUTORIAL_GROUP);
await props.api.settings.putEntry('tutorial', 'joined', Date.now()); await props.api.settings.putEntry('tutorial', 'joined', Date.now());
await waiter(hasTutorialGroup); await waiter(hasTutorialGroup);

View File

@ -148,17 +148,13 @@ export const LinkItem = (props: LinkItemProps): ReactElement => {
</Anchor> </Anchor>
</Text> </Text>
</Box> </Box>
<Row minWidth='0' flexShrink={0} width="100%" justifyContent="space-between" py={3} bg="white"> <Row minWidth='0' flexShrink={0} width="100%" justifyContent="space-between" py={3} bg="white">
<Author <Author
showImage showImage
ship={author} ship={author}
date={node.post['time-sent']} date={node.post['time-sent']}
group={group} group={group}
api={api} />
></Author>
<Box ml="auto"> <Box ml="auto">
<Link <Link
to={node.post.pending ? '#' : `${baseUrl}/${index}`} to={node.post.pending ? '#' : `${baseUrl}/${index}`}

View File

@ -21,7 +21,7 @@ function Author(props: { patp: string; last?: boolean }): ReactElement {
const contact: Contact | undefined = contacts?.[`~${props.patp}`]; const contact: Contact | undefined = contacts?.[`~${props.patp}`];
const showNickname = useShowNickname(contact); const showNickname = useShowNickname(contact);
const name = contact?.nickname || `~${props.patp}`; const name = showNickname ? contact.nickname : `~${props.patp}`;
return ( return (
<Text mono={!showNickname}> <Text mono={!showNickname}>

View File

@ -85,7 +85,7 @@ export function ProfileHeaderImageEdit(props: any): ReactElement {
export function EditProfile(props: any): ReactElement { export function EditProfile(props: any): ReactElement {
const { contact, ship, api } = props; const { contact, ship, api } = props;
const isPublic = useContactState(state => state.isContactPublic); const isPublic = useContactState((state) => state.isContactPublic);
const [hideCover, setHideCover] = useState(false); const [hideCover, setHideCover] = useState(false);
const handleHideCover = (value) => { const handleHideCover = (value) => {
@ -150,7 +150,7 @@ export function EditProfile(props: any): ReactElement {
<Form width='100%' height='100%'> <Form width='100%' height='100%'>
<ProfileHeader> <ProfileHeader>
<ProfileControls> <ProfileControls>
<Row> <Row alignItems='baseline'>
<Button <Button
type='submit' type='submit'
display='inline' display='inline'
@ -178,7 +178,11 @@ export function EditProfile(props: any): ReactElement {
</Row> </Row>
<ProfileStatus contact={contact} /> <ProfileStatus contact={contact} />
</ProfileControls> </ProfileControls>
<ProfileImages hideCover={hideCover} contact={contact} ship={ship}> <ProfileImages
hideCover={hideCover}
contact={contact}
ship={ship}
>
<ProfileHeaderImageEdit <ProfileHeaderImageEdit
contact={contact} contact={contact}
setFieldValue={setFieldValue} setFieldValue={setFieldValue}
@ -203,11 +207,7 @@ export function EditProfile(props: any): ReactElement {
<MarkdownField id='bio' mb={3} /> <MarkdownField id='bio' mb={3} />
</Col> </Col>
<Checkbox mb={3} id='isPublic' label='Public Profile' /> <Checkbox mb={3} id='isPublic' label='Public Profile' />
<GroupSearch <GroupSearch label='Pinned Groups' id='groups' publicOnly />
label='Pinned Groups'
id='groups'
publicOnly
/>
<AsyncButton primary loadingText='Updating...' border mt={3}> <AsyncButton primary loadingText='Updating...' border mt={3}>
Submit Submit
</AsyncButton> </AsyncButton>

View File

@ -15,8 +15,8 @@ export function ProfileHeader(props: any): ReactElement {
return ( return (
<Box <Box
border='1px solid' border='1px solid'
borderColor='lightGray' borderColor='washedGray'
borderRadius='2' borderRadius='3'
overflow='hidden' overflow='hidden'
marginBottom='calc(64px + 2rem)' marginBottom='calc(64px + 2rem)'
> >
@ -65,7 +65,7 @@ export function ProfileImages(props: any): ReactElement {
return ( return (
<> <>
<Row ref={anchorRef} width='100%' height='300px' position='relative'> <Row ref={anchorRef} width='100%' height='400px' position='relative'>
{cover} {cover}
<Center position='absolute' width='100%' height='100%'> <Center position='absolute' width='100%' height='100%'>
{props.children} {props.children}
@ -74,7 +74,7 @@ export function ProfileImages(props: any): ReactElement {
<Box <Box
height='128px' height='128px'
width='128px' width='128px'
borderRadius='2' borderRadius='3'
overflow='hidden' overflow='hidden'
position='absolute' position='absolute'
left='50%' left='50%'

View File

@ -39,7 +39,7 @@ export function ViewProfile(props: any): ReactElement {
</ProfileHeader> </ProfileHeader>
<Row pb={2} alignItems='center' width='100%'> <Row pb={2} alignItems='center' width='100%'>
<Center width='100%'> <Center width='100%'>
<Text> <Text fontWeight='500'>
{!hideNicknames && contact?.nickname ? contact.nickname : ''} {!hideNicknames && contact?.nickname ? contact.nickname : ''}
</Text> </Text>
</Center> </Center>
@ -51,7 +51,7 @@ export function ViewProfile(props: any): ReactElement {
</Text> </Text>
</Center> </Center>
</Row> </Row>
<Col pb={2} alignItems='center' justifyContent='center' width='100%'> <Col pb={2} mt='3' alignItems='center' justifyContent='center' width='100%'>
<Center flexDirection='column' maxWidth='32rem'> <Center flexDirection='column' maxWidth='32rem'>
<RichText width='100%' disableRemoteContent> <RichText width='100%' disableRemoteContent>
{contact?.bio ? contact.bio : ''} {contact?.bio ? contact.bio : ''}

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Box, Text, Col, Anchor } from '@tlon/indigo-react'; import { Box, Text, Col, Anchor, Row } from '@tlon/indigo-react';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import bigInt from 'big-integer'; import bigInt from 'big-integer';
@ -9,6 +9,7 @@ import { Comments } from '~/views/components/Comments';
import { NoteNavigation } from './NoteNavigation'; import { NoteNavigation } from './NoteNavigation';
import GlobalApi from '~/logic/api/global'; import GlobalApi from '~/logic/api/global';
import { getLatestRevision, getComments } from '~/logic/lib/publish'; import { getLatestRevision, getComments } from '~/logic/lib/publish';
import { roleForShip } from '~/logic/lib/group';
import Author from '~/views/components/Author'; import Author from '~/views/components/Author';
import { Contacts, GraphNode, Graph, Association, Unreads, Group } from '@urbit/api'; import { Contacts, GraphNode, Graph, Association, Unreads, Group } from '@urbit/api';
@ -54,29 +55,37 @@ export function Note(props: NoteProps & RouteComponentProps) {
api.hark.markEachAsRead(props.association, '/',`/${index[1]}/1/1`, 'note', 'publish'); api.hark.markEachAsRead(props.association, '/',`/${index[1]}/1/1`, 'note', 'publish');
}, [props.association, props.note]); }, [props.association, props.note]);
let adminLinks: JSX.Element | null = null; let adminLinks: JSX.Element[] = [];
const ourRole = roleForShip(group, window.ship);
if (window.ship === note?.post?.author) { if (window.ship === note?.post?.author) {
adminLinks = ( adminLinks.push(
<Box display="inline-block" verticalAlign="middle"> <Link
<Link to={`${baseUrl}/edit`}> style={{ 'display': 'inline-block' }}
<Text to={`${baseUrl}/edit`}
color="green"
ml={2}
> >
Update <Text
</Text> color="blue"
ml={2}
>
Update
</Text>
</Link> </Link>
<Text )
color="red" };
ml={2}
onClick={deletePost} if (window.ship === note?.post?.author || ourRole === "admin") {
style={{ cursor: 'pointer' }} adminLinks.push(
> <Text
Delete color="red"
</Text> display='inline-block'
</Box> ml={2}
); onClick={deletePost}
} style={{ cursor: 'pointer' }}
>
Delete
</Text>
)
};
const windowRef = React.useRef(null); const windowRef = React.useRef(null);
useEffect(() => { useEffect(() => {
@ -103,13 +112,15 @@ export function Note(props: NoteProps & RouteComponentProps) {
</Link> </Link>
<Col> <Col>
<Text display="block" mb={2}>{title || ''}</Text> <Text display="block" mb={2}>{title || ''}</Text>
<Box display="flex"> <Row alignItems="center">
<Author <Author
showImage
ship={post?.author} ship={post?.author}
date={post?.['time-sent']} date={post?.['time-sent']}
group={group}
/> />
<Text ml={2}>{adminLinks}</Text> <Text ml={1}>{adminLinks}</Text>
</Box> </Row>
</Col> </Col>
<Box color="black" className="md" style={{ overflowWrap: 'break-word', overflow: 'hidden' }}> <Box color="black" className="md" style={{ overflowWrap: 'break-word', overflow: 'hidden' }}>
<ReactMarkdown source={body} linkTarget={'_blank'} renderers={renderers} /> <ReactMarkdown source={body} linkTarget={'_blank'} renderers={renderers} />

View File

@ -12,7 +12,6 @@ import {
getSnippet getSnippet
} from '~/logic/lib/publish'; } from '~/logic/lib/publish';
import { Unreads } from '@urbit/api'; import { Unreads } from '@urbit/api';
import GlobalApi from '~/logic/api/global';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import useHarkState from '~/logic/state/hark'; import useHarkState from '~/logic/state/hark';
@ -21,7 +20,6 @@ interface NotePreviewProps {
book: string; book: string;
node: GraphNode; node: GraphNode;
baseUrl: string; baseUrl: string;
api: GlobalApi;
group: Group; group: Group;
} }
@ -96,7 +94,6 @@ export function NotePreview(props: NotePreviewProps) {
date={post?.['time-sent']} date={post?.['time-sent']}
group={group} group={group}
unread={isUnread} unread={isUnread}
api={props.api}
/> />
<Box ml="auto" mr={1}> <Box ml="auto" mr={1}>
<Link to={url}> <Link to={url}>

View File

@ -5,13 +5,11 @@ import { Col, Box, Text, Row } from '@tlon/indigo-react';
import { Contacts, Rolodex, Groups, Associations, Graph, Association, Unreads } from '@urbit/api'; import { Contacts, Rolodex, Groups, Associations, Graph, Association, Unreads } from '@urbit/api';
import { NotebookPosts } from './NotebookPosts'; import { NotebookPosts } from './NotebookPosts';
import GlobalApi from '~/logic/api/global';
import { useShowNickname } from '~/logic/lib/util'; import { useShowNickname } from '~/logic/lib/util';
import useContactState from '~/logic/state/contact'; import useContactState from '~/logic/state/contact';
import useGroupState from '~/logic/state/group'; import useGroupState from '~/logic/state/group';
interface NotebookProps { interface NotebookProps {
api: GlobalApi;
ship: string; ship: string;
book: string; book: string;
graph: Graph; graph: Graph;
@ -40,7 +38,6 @@ export function Notebook(props: NotebookProps & RouteComponentProps): ReactEleme
const contacts = useContactState(state => state.contacts); const contacts = useContactState(state => state.contacts);
const contact = contacts?.[`~${ship}`]; const contact = contacts?.[`~${ship}`];
console.log(association.resource);
const showNickname = useShowNickname(contact); const showNickname = useShowNickname(contact);
@ -61,7 +58,6 @@ export function Notebook(props: NotebookProps & RouteComponentProps): ReactEleme
host={ship} host={ship}
book={book} book={book}
baseUrl={props.baseUrl} baseUrl={props.baseUrl}
api={props.api}
group={group} group={group}
/> />
</Col> </Col>

View File

@ -11,7 +11,6 @@ interface NotebookPostsProps {
baseUrl: string; baseUrl: string;
hideAvatars?: boolean; hideAvatars?: boolean;
hideNicknames?: boolean; hideNicknames?: boolean;
api: GlobalApi;
group: Group; group: Group;
} }
@ -29,7 +28,6 @@ export function NotebookPosts(props: NotebookPostsProps) {
contact={contacts[`~${node.post.author}`]} contact={contacts[`~${node.post.author}`]}
node={node} node={node}
baseUrl={props.baseUrl} baseUrl={props.baseUrl}
api={props.api}
group={props.group} group={props.group}
/> />
) )

View File

@ -4,8 +4,16 @@ import { Text } from '@tlon/indigo-react';
export function BackButton(props: {}) { export function BackButton(props: {}) {
return ( return (
<Link to="/~settings"> <Link to='/~settings'>
<Text display={["block", "none"]} fontSize="2" fontWeight="medium">{"<- Back to System Preferences"}</Text> <Text
display={['block', 'none']}
fontSize='2'
fontWeight='medium'
p={4}
pb={0}
>
{'<- Back to System Preferences'}
</Text>
</Link> </Link>
); );
} }

View File

@ -11,12 +11,12 @@ import {
Anchor Anchor
} from '@tlon/indigo-react'; } from '@tlon/indigo-react';
import GlobalApi from "~/logic/api/global"; import GlobalApi from '~/logic/api/global';
import { BucketList } from "./BucketList"; import { BucketList } from './BucketList';
import { S3State } from '~/types/s3-update'; import { S3State } from '~/types/s3-update';
import useS3State from '~/logic/state/storage'; import useS3State from '~/logic/state/storage';
import { BackButton } from './BackButton'; import { BackButton } from './BackButton';
import {StorageState} from '~/types'; import { StorageState } from '~/types';
import useStorageState from '~/logic/state/storage'; import useStorageState from '~/logic/state/storage';
interface FormSchema { interface FormSchema {
@ -33,7 +33,7 @@ interface S3FormProps {
export default function S3Form(props: S3FormProps): ReactElement { export default function S3Form(props: S3FormProps): ReactElement {
const { api } = props; const { api } = props;
const s3 = useStorageState(state => state.s3); const s3 = useStorageState((state) => state.s3);
const onSubmit = useCallback( const onSubmit = useCallback(
(values: FormSchema) => { (values: FormSchema) => {
@ -54,7 +54,8 @@ export default function S3Form(props: S3FormProps): ReactElement {
return ( return (
<> <>
<Col p="5" pt="4" borderBottom="1" borderBottomColor="washedGray"> <BackButton />
<Col p='5' pt='4' borderBottom='1' borderBottomColor='washedGray'>
<Formik <Formik
initialValues={ initialValues={
{ {
@ -68,42 +69,42 @@ export default function S3Form(props: S3FormProps): ReactElement {
onSubmit={onSubmit} onSubmit={onSubmit}
> >
<Form> <Form>
<BackButton/> <Col maxWidth='600px' gapY='5'>
<Col maxWidth="600px" gapY="5"> <Col gapY='1' mt='0'>
<Col gapY="1" mt="0"> <Text color='black' fontSize={2} fontWeight='medium'>
<Text color="black" fontSize={2} fontWeight="medium">
S3 Storage Setup S3 Storage Setup
</Text> </Text>
<Text gray> <Text gray>
Store credentials for your S3 object storage buckets on your Store credentials for your S3 object storage buckets on your
Urbit ship, and upload media freely to various modules. Urbit ship, and upload media freely to various modules.
<Anchor <Anchor
target="_blank" target='_blank'
style={{ textDecoration: 'none' }} style={{ textDecoration: 'none' }}
borderBottom="1" borderBottom='1'
ml="1" ml='1'
href="https://urbit.org/using/operations/using-your-ship/#bucket-setup"> href='https://urbit.org/using/operations/using-your-ship/#bucket-setup'
>
Learn more Learn more
</Anchor> </Anchor>
</Text> </Text>
</Col> </Col>
<Input label="Endpoint" id="s3endpoint" /> <Input label='Endpoint' id='s3endpoint' />
<Input label="Access Key ID" id="s3accessKeyId" /> <Input label='Access Key ID' id='s3accessKeyId' />
<Input <Input
type="password" type='password'
label="Secret Access Key" label='Secret Access Key'
id="s3secretAccessKey" id='s3secretAccessKey'
/> />
<Button style={{ cursor: "pointer" }} type="submit"> <Button style={{ cursor: 'pointer' }} type='submit'>
Submit Submit
</Button> </Button>
</Col> </Col>
</Form> </Form>
</Formik> </Formik>
</Col> </Col>
<Col maxWidth="600px" p="5" gapY="4"> <Col maxWidth='600px' p='5' gapY='4'>
<Col gapY="1"> <Col gapY='1'>
<Text color="black" mb={4} fontSize={2} fontWeight="medium"> <Text color='black' mb={4} fontSize={2} fontWeight='medium'>
S3 Buckets S3 Buckets
</Text> </Text>
<Text gray> <Text gray>

View File

@ -1,35 +1,42 @@
import React, { ReactNode } from "react"; import React, { ReactNode } from 'react';
import { useLocation } from "react-router-dom"; import { useLocation } from 'react-router-dom';
import Helmet from "react-helmet"; import Helmet from 'react-helmet';
import { Text, Box, Col, Row } from '@tlon/indigo-react'; import { Text, Box, Col, Row } from '@tlon/indigo-react';
import { NotificationPreferences } from "./components/lib/NotificationPref"; import { NotificationPreferences } from './components/lib/NotificationPref';
import DisplayForm from "./components/lib/DisplayForm"; import DisplayForm from './components/lib/DisplayForm';
import S3Form from "./components/lib/S3Form"; import S3Form from './components/lib/S3Form';
import { CalmPrefs } from "./components/lib/CalmPref"; import { CalmPrefs } from './components/lib/CalmPref';
import SecuritySettings from "./components/lib/Security"; import SecuritySettings from './components/lib/Security';
import { LeapSettings } from "./components/lib/LeapSettings"; import { LeapSettings } from './components/lib/LeapSettings';
import { useHashLink } from "~/logic/lib/useHashLink"; import { useHashLink } from '~/logic/lib/useHashLink';
import { SidebarItem as BaseSidebarItem } from "~/views/landscape/components/SidebarItem"; import { SidebarItem as BaseSidebarItem } from '~/views/landscape/components/SidebarItem';
import { PropFunc } from "~/types"; import { PropFunc } from '~/types';
export const Skeleton = (props: { children: ReactNode }) => ( export const Skeleton = (props: { children: ReactNode }) => (
<Box height="100%" width="100%" px={[0, 3]} pb={[0, 3]} borderRadius={1}> <Box height='100%' width='100%' px={[0, 3]} pb={[0, 3]} borderRadius={1}>
<Box <Box
height="100%" display='grid'
width="100%" gridTemplateColumns={[
borderRadius={1} '100%',
bg="white" 'minmax(150px, 1fr) 3fr',
'minmax(250px, 1fr) 4fr'
]}
gridTemplateRows='100%'
height='100%'
width='100%'
borderRadius={2}
bg='white'
border={1} border={1}
borderColor="washedGray" borderColor='washedGray'
> >
{props.children} {props.children}
</Box> </Box>
</Box> </Box>
); );
type ProvSideProps = "to" | "selected"; type ProvSideProps = 'to' | 'selected';
type BaseProps = PropFunc<typeof BaseSidebarItem>; type BaseProps = PropFunc<typeof BaseSidebarItem>;
function SidebarItem(props: { hash: string } & Omit<BaseProps, ProvSideProps>) { function SidebarItem(props: { hash: string } & Omit<BaseProps, ProvSideProps>) {
const { hash, icon, text, ...rest } = props; const { hash, icon, text, ...rest } = props;
@ -54,16 +61,15 @@ function SettingsItem(props: { children: ReactNode }) {
const { children } = props; const { children } = props;
return ( return (
<Box borderBottom="1" borderBottomColor="washedGray"> <Box borderBottom='1' borderBottomColor='washedGray'>
{children} {children}
</Box> </Box>
); );
} }
export default function SettingsScreen(props: any) { export default function SettingsScreen(props: any) {
const location = useLocation(); const location = useLocation();
const hash = location.hash.slice(1) const hash = location.hash.slice(1);
return ( return (
<> <>
@ -71,68 +77,49 @@ export default function SettingsScreen(props: any) {
<title>Landscape - Settings</title> <title>Landscape - Settings</title>
</Helmet> </Helmet>
<Skeleton> <Skeleton>
<Row height="100%" overflow="hidden"> <Col
<Col height='100%'
height="100%" borderRight='1'
borderRight="1" borderRightColor='washedGray'
borderRightColor="washedGray" display={hash === '' ? 'flex' : ['none', 'flex']}
display={hash === "" ? "flex" : ["none", "flex"]} width='100%'
minWidth="250px" overflowY='auto'
width="100%" >
maxWidth={["100vw", "350px"]} <Text display='block' mt='4' mb='3' mx='3' fontSize='2' fontWeight='700'>
> System Preferences
<Text </Text>
display="block" <Col>
my="4" <SidebarItem
mx="3" icon='Inbox'
fontSize="2" text='Notifications'
fontWeight="medium" hash='notifications'
> />
System Preferences <SidebarItem icon='Image' text='Display' hash='display' />
</Text> <SidebarItem icon='Upload' text='Remote Storage' hash='s3' />
<Col gapY="1"> <SidebarItem icon='LeapArrow' text='Leap' hash='leap' />
<SidebarItem <SidebarItem icon='Node' text='CalmEngine' hash='calm' />
icon="Inbox" <SidebarItem
text="Notifications" icon='Locked'
hash="notifications" text='Devices + Security'
/> hash='security'
<SidebarItem icon="Image" text="Display" hash="display" /> />
<SidebarItem icon="Upload" text="Remote Storage" hash="s3" />
<SidebarItem icon="LeapArrow" text="Leap" hash="leap" />
<SidebarItem icon="Node" text="CalmEngine" hash="calm" />
<SidebarItem
icon="Locked"
text="Devices + Security"
hash="security"
/>
</Col>
</Col> </Col>
<Col flexGrow={1} overflowY="auto"> </Col>
<SettingsItem> <Col flexGrow={1} overflowY='auto'>
{hash === "notifications" && ( <SettingsItem>
<NotificationPreferences {hash === 'notifications' && (
{...props} <NotificationPreferences
graphConfig={props.notificationsGraphConfig} {...props}
/> graphConfig={props.notificationsGraphConfig}
)} />
{hash === "display" && ( )}
<DisplayForm api={props.api} /> {hash === 'display' && <DisplayForm api={props.api} />}
)} {hash === 's3' && <S3Form api={props.api} />}
{hash === "s3" && ( {hash === 'leap' && <LeapSettings api={props.api} />}
<S3Form api={props.api} /> {hash === 'calm' && <CalmPrefs api={props.api} />}
)} {hash === 'security' && <SecuritySettings api={props.api} />}
{hash === "leap" && ( </SettingsItem>
<LeapSettings api={props.api} /> </Col>
)}
{hash === "calm" && (
<CalmPrefs api={props.api} />
)}
{hash === "security" && (
<SecuritySettings api={props.api} />
)}
</SettingsItem>
</Col>
</Row>
</Skeleton> </Skeleton>
</> </>
); );

View File

@ -11,7 +11,6 @@ import useSettingsState, {selectCalmState} from "~/logic/state/settings";
import useLocalState from "~/logic/state/local"; import useLocalState from "~/logic/state/local";
import OverlaySigil from './OverlaySigil'; import OverlaySigil from './OverlaySigil';
import { Sigil } from '~/logic/lib/sigil'; import { Sigil } from '~/logic/lib/sigil';
import GlobalApi from '~/logic/api/global';
import Timestamp from './Timestamp'; import Timestamp from './Timestamp';
import useContactState from '~/logic/state/contact'; import useContactState from '~/logic/state/contact';
@ -22,7 +21,6 @@ interface AuthorProps {
children?: ReactNode; children?: ReactNode;
unread?: boolean; unread?: boolean;
group: Group; group: Group;
api?: GlobalApi;
} }
// eslint-disable-next-line max-lines-per-function // eslint-disable-next-line max-lines-per-function

View File

@ -10,6 +10,7 @@ import { Group } from '@urbit/api';
import GlobalApi from '~/logic/api/global'; import GlobalApi from '~/logic/api/global';
import Author from '~/views/components/Author'; import Author from '~/views/components/Author';
import { MentionText } from '~/views/components/MentionText'; import { MentionText } from '~/views/components/MentionText';
import { roleForShip } from '~/logic/lib/group';
import { getLatestCommentRevision } from '~/logic/lib/publish'; import { getLatestCommentRevision } from '~/logic/lib/publish';
const ClickBox = styled(Box)` const ClickBox = styled(Box)`
@ -31,7 +32,7 @@ interface CommentItemProps {
export function CommentItem(props: CommentItemProps): ReactElement { export function CommentItem(props: CommentItemProps): ReactElement {
const { ship, name, api, comment, group } = props; const { ship, name, api, comment, group } = props;
const [, post] = getLatestCommentRevision(comment); const [, post] = getLatestCommentRevision(comment);
const disabled = props.pending || window.ship !== post?.author; const disabled = props.pending;
const onDelete = async () => { const onDelete = async () => {
await api.graph.removeNodes(ship, name, [comment.post?.index]); await api.graph.removeNodes(ship, name, [comment.post?.index]);
@ -41,6 +42,29 @@ export function CommentItem(props: CommentItemProps): ReactElement {
const commentIndex = commentIndexArray[commentIndexArray.length - 1]; const commentIndex = commentIndexArray[commentIndexArray.length - 1];
const updateUrl = `${props.baseUrl}/${commentIndex}`; const updateUrl = `${props.baseUrl}/${commentIndex}`;
const adminLinks: JSX.Element[] = [];
const ourRole = roleForShip(group, window.ship);
if (window.ship == post?.author && !disabled) {
adminLinks.push(
<Link to={updateUrl}>
<Text
color="blue"
ml={2}
>
Update
</Text>
</Link>
)
};
if ((window.ship == post?.author || ourRole == "admin") && !disabled) {
adminLinks.push(
<ClickBox display="inline-block" color="red" onClick={onDelete}>
<Text color='red'>Delete</Text>
</ClickBox>
)
};
return ( return (
<Box mb={4} opacity={post?.pending ? '60%' : '100%'}> <Box mb={4} opacity={post?.pending ? '60%' : '100%'}>
<Row bg="white" my={3}> <Row bg="white" my={3}>
@ -50,23 +74,10 @@ export function CommentItem(props: CommentItemProps): ReactElement {
date={post?.['time-sent']} date={post?.['time-sent']}
unread={props.unread} unread={props.unread}
group={group} group={group}
api={api}
> >
{!disabled && ( <Row alignItems="center">
<Box display="inline-block" verticalAlign="middle"> {adminLinks}
<Link to={updateUrl}> </Row>
<Text
color="green"
ml={2}
>
Update
</Text>
</Link>
<ClickBox display="inline-block" color="red" onClick={onDelete}>
<Text color='red'>Delete</Text>
</ClickBox>
</Box>
)}
</Author> </Author>
</Row> </Row>
<Box mb={2}> <Box mb={2}>

View File

@ -51,7 +51,6 @@ class RemoteContent extends Component<RemoteContentProps, RemoteContentState> {
} }
save = () => { save = () => {
console.log(`saving for: ${this.props.url}`);
if(this.saving) { if(this.saving) {
return; return;
} }
@ -60,7 +59,6 @@ class RemoteContent extends Component<RemoteContentProps, RemoteContentState> {
}; };
restore = () => { restore = () => {
console.log(`restoring for: ${this.props.url}`);
this.saving = false; this.saving = false;
this.props.restore(); this.props.restore();
} }

View File

@ -9,6 +9,7 @@ import {
Button, Button,
BaseImage BaseImage
} from '@tlon/indigo-react'; } from '@tlon/indigo-react';
import ReconnectButton from './ReconnectButton'; import ReconnectButton from './ReconnectButton';
import { Dropdown } from './Dropdown'; import { Dropdown } from './Dropdown';
import { StatusBarItem } from './StatusBarItem'; import { StatusBarItem } from './StatusBarItem';
@ -19,6 +20,7 @@ import { useTutorialModal } from './useTutorialModal';
import useHarkState from '~/logic/state/hark'; import useHarkState from '~/logic/state/hark';
import useInviteState from '~/logic/state/invite'; import useInviteState from '~/logic/state/invite';
import useContactState from '~/logic/state/contact';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import useLocalState, { selectLocalState } from '~/logic/state/local'; import useLocalState, { selectLocalState } from '~/logic/state/local';
import useSettingsState, { selectCalmState } from '~/logic/state/settings'; import useSettingsState, { selectCalmState } from '~/logic/state/settings';
@ -26,8 +28,9 @@ import useSettingsState, { selectCalmState } from '~/logic/state/settings';
const localSel = selectLocalState(['toggleOmnibox']); const localSel = selectLocalState(['toggleOmnibox']);
const StatusBar = (props) => { const StatusBar = (props) => {
const { ourContact, api, ship } = props; const { api, ship } = props;
const history = useHistory(); const history = useHistory();
const ourContact = useContactState((state) => state.contacts[`~${ship}`]);
const notificationsCount = useHarkState((state) => state.notificationsCount); const notificationsCount = useHarkState((state) => state.notificationsCount);
const doNotDisturb = useHarkState((state) => state.doNotDisturb); const doNotDisturb = useHarkState((state) => state.doNotDisturb);
const inviteState = useInviteState((state) => state.invites); const inviteState = useInviteState((state) => state.invites);
@ -38,7 +41,7 @@ const StatusBar = (props) => {
const { toggleOmnibox } = useLocalState(localSel); const { toggleOmnibox } = useLocalState(localSel);
const { hideAvatars } = useSettingsState(selectCalmState); const { hideAvatars } = useSettingsState(selectCalmState);
const color = !!ourContact ? `#${uxToHex(props.ourContact.color)}` : '#000'; const color = !!ourContact ? `#${uxToHex(ourContact.color)}` : '#000';
const xPadding = !hideAvatars && ourContact?.avatar ? '0' : '2'; const xPadding = !hideAvatars && ourContact?.avatar ? '0' : '2';
const bgColor = !hideAvatars && ourContact?.avatar ? '' : color; const bgColor = !hideAvatars && ourContact?.avatar ? '' : color;
const profileImage = const profileImage =

View File

@ -9,15 +9,13 @@ import {
StatelessAsyncButton as AsyncButton, StatelessAsyncButton as AsyncButton,
StatelessAsyncButton StatelessAsyncButton
} from './StatelessAsyncButton'; } from './StatelessAsyncButton';
import { Notebooks, Graphs, Inbox } from '@urbit/api'; import { Graphs } from '@urbit/api';
import useGraphState from '~/logic/state/graph'; import useGraphState from '~/logic/state/graph';
interface UnjoinedResourceProps { interface UnjoinedResourceProps {
association: Association; association: Association;
api: GlobalApi; api: GlobalApi;
baseUrl: string; baseUrl: string;
notebooks: Notebooks;
inbox: Inbox;
} }
function isJoined(path: string) { function isJoined(path: string) {
@ -31,7 +29,7 @@ function isJoined(path: string) {
} }
export function UnjoinedResource(props: UnjoinedResourceProps) { export function UnjoinedResource(props: UnjoinedResourceProps) {
const { api, notebooks, inbox } = props; const { api } = props;
const history = useHistory(); const history = useHistory();
const rid = props.association.resource; const rid = props.association.resource;
const appName = props.association['app-name']; const appName = props.association['app-name'];
@ -52,7 +50,7 @@ export function UnjoinedResource(props: UnjoinedResourceProps) {
if (isJoined(rid)({ graphKeys })) { if (isJoined(rid)({ graphKeys })) {
history.push(`${props.baseUrl}/resource/${app}${rid}`); history.push(`${props.baseUrl}/resource/${app}${rid}`);
} }
}, [props.association, inbox, graphKeys, notebooks]); }, [props.association, graphKeys]);
return ( return (
<Center p={6}> <Center p={6}>

View File

@ -40,7 +40,7 @@ interface VirtualScrollerProps<T> {
data: BigIntOrderedMap<T>; data: BigIntOrderedMap<T>;
/** /**
* The component to render the items * The component to render the items
* *
* @remarks * @remarks
* *
* This component must be referentially stable, so either use `useCallback` or * This component must be referentially stable, so either use `useCallback` or
@ -157,7 +157,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
this.loaded.top = true; this.loaded.top = true;
this.loaded.bottom = true; this.loaded.bottom = true;
} }
this.updateVisible(0); this.updateVisible(0);
this.resetScroll(); this.resetScroll();
this.loadRows(false); this.loadRows(false);
@ -493,7 +493,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
<> <>
{!IS_IOS && (<Box borderRadius="3" top ={isTop ? "0" : undefined} bottom={!isTop ? "0" : undefined} ref={el => { this.scrollRef = el; }} right="0" height="50px" position="absolute" width="4px" backgroundColor="lightGray" />)} {!IS_IOS && (<Box borderRadius="3" top ={isTop ? "0" : undefined} bottom={!isTop ? "0" : undefined} ref={el => { this.scrollRef = el; }} right="0" height="50px" position="absolute" width="4px" backgroundColor="lightGray" />)}
<ScrollbarLessBox overflowY='scroll' ref={this.setWindow} onScroll={this.onScroll} style={{ ...style, ...{ transform }, "-webkit-overflow-scrolling": "auto" }}> <ScrollbarLessBox overflowY='scroll' ref={this.setWindow} onScroll={this.onScroll} style={{ ...style, ...{ transform }, "WebkitOverflowScrolling": "auto" }}>
<Box style={{ transform, width: 'calc(100% - 4px)' }}> <Box style={{ transform, width: 'calc(100% - 4px)' }}>
{(isTop ? !atStart : !atEnd) && (<Center height="5"> {(isTop ? !atStart : !atEnd) && (<Center height="5">
<LoadingSpinner /> <LoadingSpinner />

View File

@ -16,9 +16,7 @@ export function useTutorialModal(
setTutorialRef(anchorRef.current); setTutorialRef(anchorRef.current);
} }
return () => { return () => {}
console.log(tutorialProgress);
}
}, [tutorialProgress, show, anchorRef]); }, [tutorialProgress, show, anchorRef]);
return show && onProgress === tutorialProgress; return show && onProgress === tutorialProgress;

View File

@ -10,16 +10,14 @@
font-family: 'Inter'; font-family: 'Inter';
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
src: url("/~landscape/fonts/inter-medium.woff2") format("woff2"), src: url("https://media.urbit.org/fonts/Inter-Medium.woff2") format("woff2");
url("https://media.urbit.org/fonts/Inter-Medium.woff2") format("woff2");
} }
@font-face { @font-face {
font-family: 'Inter'; font-family: 'Inter';
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
src: url("/~landscape/fonts/inter-semibold.woff2") format("woff2"), src: url("https://media.urbit.org/fonts/Inter-SemiBold.woff2") format("woff2");
url("https://media.urbit.org/fonts/Inter-SemiBold.woff2") format("woff2");
} }
@font-face { @font-face {

View File

@ -158,8 +158,6 @@ export function GroupsPane(props: GroupsPaneProps) {
baseUrl={baseUrl} baseUrl={baseUrl}
> >
<UnjoinedResource <UnjoinedResource
notebooks={props.notebooks}
inbox={props.inbox}
baseUrl={baseUrl} baseUrl={baseUrl}
api={api} api={api}
association={association} association={association}
@ -191,9 +189,8 @@ export function GroupsPane(props: GroupsPaneProps) {
<Route <Route
path={relativePath('')} path={relativePath('')}
render={(routeProps) => { render={(routeProps) => {
const hasDescription = groupAssociation?.metadata?.description; const channelCount = Object.keys(associations?.graph ?? {}).filter((e) => {
const channelCount = Object.keys(props?.associations?.graph ?? {}).filter((e) => { return associations?.graph?.[e]?.['group'] === groupPath;
return props?.associations?.graph?.[e]?.['group'] === groupPath;
}).length; }).length;
let summary: ReactNode; let summary: ReactNode;
if(groupAssociation?.group) { if(groupAssociation?.group) {

View File

@ -15,7 +15,7 @@ export function MetadataIcon(props: MetadataIconProps) {
const bgColor = metadata.picture ? {} : { bg: `#${uxToHex(metadata.color)}` }; const bgColor = metadata.picture ? {} : { bg: `#${uxToHex(metadata.color)}` };
return ( return (
<Box {...bgColor} {...rest} borderRadius={2} boxShadow="inset 0 0 0 1px" color="lightGray" overflow="hidden"> <Box {...bgColor} {...rest} borderRadius={1} boxShadow="inset 0 0 0 1px" color="lightGray" overflow="hidden">
{metadata.picture && <Image height="100%" src={metadata.picture} />} {metadata.picture && <Image height="100%" src={metadata.picture} />}
</Box> </Box>
); );

View File

@ -37,7 +37,6 @@ interface SidebarProps {
api: GlobalApi; api: GlobalApi;
selected?: string; selected?: string;
selectedGroup?: string; selectedGroup?: string;
includeUnmanaged?: boolean;
apps: SidebarAppConfigs; apps: SidebarAppConfigs;
baseUrl: string; baseUrl: string;
mobileHide?: boolean; mobileHide?: boolean;

View File

@ -30,7 +30,7 @@ export const SidebarItem = ({
bgActive="washedGray" bgActive="washedGray"
display="flex" display="flex"
px="3" px="3"
py="1" py="2"
justifyContent="space-between" justifyContent="space-between"
{...rest} {...rest}
> >

View File

@ -5,7 +5,6 @@ import { Associations } from '@urbit/api/metadata';
import { Sidebar } from './Sidebar/Sidebar'; import { Sidebar } from './Sidebar/Sidebar';
import GlobalApi from '~/logic/api/global'; import GlobalApi from '~/logic/api/global';
import GlobalSubscription from '~/logic/subscription/global';
import { useGraphModule } from './Sidebar/Apps'; import { useGraphModule } from './Sidebar/Apps';
import { Body } from '~/views/components/Body'; import { Body } from '~/views/components/Body';
import { Workspace } from '~/types/workspace'; import { Workspace } from '~/types/workspace';
@ -16,14 +15,11 @@ import ErrorBoundary from '~/views/components/ErrorBoundary';
interface SkeletonProps { interface SkeletonProps {
children: ReactNode; children: ReactNode;
recentGroups: string[]; recentGroups: string[];
linkListening: Set<Path>;
selected?: string; selected?: string;
selectedApp?: AppName; selectedApp?: AppName;
baseUrl: string; baseUrl: string;
mobileHide?: boolean; mobileHide?: boolean;
api: GlobalApi; api: GlobalApi;
subscription: GlobalSubscription;
includeUnmanaged: boolean;
workspace: Workspace; workspace: Workspace;
} }

View File

@ -0,0 +1,8 @@
import React from 'react';
if (process.env.NODE_ENV === 'development') {
const whyDidYouRender = require('@welldone-software/why-did-you-render');
whyDidYouRender(React, {
trackAllPureComponents: true,
});
}