mirror of
https://github.com/urbit/shrub.git
synced 2024-11-25 07:12:10 +03:00
Merge branch 'release/next-userspace' into mp/landscape/dm-restoration
This commit is contained in:
commit
3b39122984
20
pkg/arvo/gen/metadata-store/remove.hoon
Normal file
20
pkg/arvo/gen/metadata-store/remove.hoon
Normal file
@ -0,0 +1,20 @@
|
||||
:: :metadata-store|remove: remove resource from group
|
||||
:: Usage:
|
||||
:: :metadata-store|remove
|
||||
:: <group-name> <app-name> <channel-path>
|
||||
:: %urbit-community %chat /~darrux-landes/general-503
|
||||
::
|
||||
:: You can acquire the channel-path with
|
||||
:: :metadata-store +dbug [%state '(~(got by group-indices) <group-path>)'
|
||||
:: and looking for the entry with an app-path that is similar to the
|
||||
:: title of the channel
|
||||
::
|
||||
/- *metadata-store
|
||||
/+ resource
|
||||
:- %say
|
||||
|= $: [now=@da eny=@uvJ =beak]
|
||||
[[group=term app=term =path ~] ~]
|
||||
==
|
||||
:- %metadata-action
|
||||
^- metadata-action
|
||||
[%remove (en-path:resource [p.beak group]) app path]
|
@ -3,6 +3,10 @@ import f from 'lodash/fp';
|
||||
|
||||
export const MOBILE_BROWSER_REGEX = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i;
|
||||
|
||||
export function parentPath(path) {
|
||||
return _.dropRight(path.split('/'), 1).join('/');
|
||||
}
|
||||
|
||||
export function clamp(x,min,max) {
|
||||
return Math.max(min, Math.min(max, x));
|
||||
}
|
||||
|
@ -49,6 +49,7 @@ const Root = styled.div`
|
||||
/* Works on Chrome/Edge/Safari */
|
||||
*::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
*::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { Component, PureComponent } from "react";
|
||||
import moment from "moment";
|
||||
import _ from "lodash";
|
||||
import { Box, Row, Text } from "@tlon/indigo-react";
|
||||
import { Box, Row, Text, Rule } from "@tlon/indigo-react";
|
||||
|
||||
import { OverlaySigil } from './overlay-sigil';
|
||||
import { uxToHex, cite, writeText } from '~/logic/lib/util';
|
||||
@ -14,15 +14,15 @@ import RemoteContent from '~/views/components/RemoteContent';
|
||||
export const DATESTAMP_FORMAT = '[~]YYYY.M.D';
|
||||
|
||||
export const UnreadMarker = React.forwardRef(({ dayBreak, when }, ref) => (
|
||||
<div ref={ref} style={{ color: "#219dff" }} className="flex items-center f9 absolute w-100 left-0 pv0">
|
||||
<hr style={{ borderColor: "#219dff" }} className="dn-s ma0 w2 bt-0" />
|
||||
<p className="mh4 z-2" style={{ whiteSpace: 'normal' }}>New messages below</p>
|
||||
<hr style={{ borderColor: "#219dff" }} className="ma0 flex-grow-1 bt-0" />
|
||||
<Row ref={ref} color='blue' alignItems='center' fontSize='0' position='absolute' width='100%' py='2'>
|
||||
<Rule borderColor='blue' display={['none', 'block']} m='0' width='2rem' />
|
||||
<Text flexShrink='0' display='block' zIndex='2' mx='4' color='blue'>New messages below</Text>
|
||||
<Rule borderColor='blue' flexGrow='1' m='0'/>
|
||||
{dayBreak
|
||||
? <p className="gray2 mh4">{moment(when).calendar()}</p>
|
||||
? <Text display='block' gray mx='4'>{moment(when).calendar()}</Text>
|
||||
: null}
|
||||
<hr style={{ width: "calc(50% - 48px)" }} style={{ borderColor: "#219dff" }} className="ma0 bt-0" />
|
||||
</div>
|
||||
<Rule style={{ width: "calc(50% - 48px)" }} borderColor='blue' m='0' />
|
||||
</Row>
|
||||
));
|
||||
|
||||
export const DayBreak = ({ when }) => (
|
||||
@ -95,7 +95,7 @@ export default class ChatMessage extends Component<ChatMessageProps> {
|
||||
|
||||
const containerClass = `${renderSigil
|
||||
? `cf pt2 pl3 lh-copy`
|
||||
: `items-center cf hide-child`} ${isPending ? 'o-40' : ''} ${className}`
|
||||
: `items-top cf hide-child`} ${isPending ? 'o-40' : ''} ${className}`
|
||||
|
||||
const timestamp = moment.unix(msg.when / 1000).format(renderSigil ? 'hh:mm a' : 'hh:mm');
|
||||
|
||||
@ -122,7 +122,7 @@ export default class ChatMessage extends Component<ChatMessageProps> {
|
||||
};
|
||||
|
||||
const unreadContainerStyle = {
|
||||
height: isLastRead ? '1.66em' : '0',
|
||||
height: isLastRead ? '2rem' : '0',
|
||||
};
|
||||
|
||||
return (
|
||||
@ -249,7 +249,7 @@ export class MessageWithSigil extends PureComponent<MessageProps> {
|
||||
|
||||
export const MessageWithoutSigil = ({ timestamp, msg, remoteContentPolicy, measure }) => (
|
||||
<>
|
||||
<p className="child pr1 mono f9 gray2 dib">{timestamp}</p>
|
||||
<Text mono gray display='inline-block' pr='1' pt='2px' lineHeight='tall' className="child">{timestamp}</Text>
|
||||
<Box fontSize='14px' className="clamp-message" style={{ flexGrow: 1 }}>
|
||||
<MessageContent content={msg.letter} remoteContentPolicy={remoteContentPolicy} measure={measure}/>
|
||||
</Box>
|
||||
|
@ -3,7 +3,7 @@ import { UnControlled as CodeEditor } from 'react-codemirror2';
|
||||
import { MOBILE_BROWSER_REGEX } from "~/logic/lib/util";
|
||||
import CodeMirror from 'codemirror';
|
||||
|
||||
import { Row } from '@tlon/indigo-react';
|
||||
import { Row, BaseInput } from '@tlon/indigo-react';
|
||||
|
||||
import 'codemirror/mode/markdown/markdown';
|
||||
import 'codemirror/addon/display/placeholder';
|
||||
@ -36,6 +36,25 @@ const MARKDOWN_CONFIG = {
|
||||
}
|
||||
};
|
||||
|
||||
// Until CodeMirror supports options.inputStyle = 'textarea' on mobile,
|
||||
// we need to hack this into a regular input that has some funny behaviors
|
||||
const inputProxy = (input) => new Proxy(input, {
|
||||
get(target, property) {
|
||||
if (property in target) {
|
||||
return target[property];
|
||||
}
|
||||
if (property === 'setOption') {
|
||||
return () => {};
|
||||
}
|
||||
if (property === 'getValue') {
|
||||
return () => target.value;
|
||||
}
|
||||
if (property === 'setValue') {
|
||||
return (val) => target.value = val;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default class ChatEditor extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@ -129,7 +148,11 @@ export default class ChatEditor extends Component {
|
||||
'Esc': () => {
|
||||
this.editor?.getInputField().blur();
|
||||
}
|
||||
}
|
||||
},
|
||||
// The below will ony work once codemirror's bug is fixed
|
||||
spellcheck: !!MOBILE_BROWSER_REGEX.test(navigator.userAgent),
|
||||
autocorrect: !!MOBILE_BROWSER_REGEX.test(navigator.userAgent),
|
||||
autocapitalize: !!MOBILE_BROWSER_REGEX.test(navigator.userAgent)
|
||||
};
|
||||
|
||||
return (
|
||||
@ -142,19 +165,39 @@ export default class ChatEditor extends Component {
|
||||
paddingTop='8px'
|
||||
width='calc(100% - 88px)'
|
||||
className={inCodeMode ? 'chat code' : 'chat'}
|
||||
color="black"
|
||||
>
|
||||
<CodeEditor
|
||||
{MOBILE_BROWSER_REGEX.test(navigator.userAgent)
|
||||
? <BaseInput
|
||||
fontFamily={inCodeMode ? 'Source Code Pro' : 'Inter'}
|
||||
fontSize="14px"
|
||||
style={{ width: '100%', marginTop: '-8px', background: 'transparent', color: 'currentColor' }}
|
||||
placeholder={inCodeMode ? "Code..." : "Message..."}
|
||||
onKeyUp={event => {
|
||||
if (event.key === 'Enter') {
|
||||
this.submit();
|
||||
} else {
|
||||
this.messageChange(null, null, event.target.value);
|
||||
}
|
||||
}}
|
||||
ref={input => {
|
||||
if (!input) return;
|
||||
this.editor = inputProxy(input);
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
: <CodeEditor
|
||||
value={message}
|
||||
options={options}
|
||||
onChange={(e, d, v) => this.messageChange(e, d, v)}
|
||||
editorDidMount={(editor) => {
|
||||
this.editor = editor;
|
||||
if (!MOBILE_BROWSER_REGEX.test(navigator.userAgent)) {
|
||||
editor.focus();
|
||||
}
|
||||
editor.focus();
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ export default class CodeContent extends Component {
|
||||
overflow='auto'
|
||||
maxHeight='10em'
|
||||
maxWidth='100%'
|
||||
style={{ whiteSpace: 'pre' }}
|
||||
backgroundColor='scales.black10'
|
||||
>
|
||||
{content.code.output[0].join('\n')}
|
||||
@ -36,6 +37,7 @@ export default class CodeContent extends Component {
|
||||
overflow='auto'
|
||||
maxHeight='10em'
|
||||
maxWidth='100%'
|
||||
style={{ whiteSpace: 'pre' }}
|
||||
>
|
||||
{content.code.expression}
|
||||
</Text>
|
||||
|
@ -1,24 +1,21 @@
|
||||
import React from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { Box, Row, Icon, Text, Center } from '@tlon/indigo-react';
|
||||
import { uxToHex, adjustHex } from "~/logic/lib/util";
|
||||
import { uxToHex, adjustHex } from '~/logic/lib/util';
|
||||
|
||||
import './css/custom.css';
|
||||
|
||||
import { Sigil } from "~/logic/lib/sigil";
|
||||
import { Sigil } from '~/logic/lib/sigil';
|
||||
import Tiles from './components/tiles';
|
||||
import Tile from './components/tiles/tile';
|
||||
import Welcome from './components/welcome';
|
||||
import Groups from './components/Groups';
|
||||
|
||||
export default class LaunchApp extends React.Component {
|
||||
|
||||
componentDidMount() {
|
||||
// preload spinner asset
|
||||
new Image().src = '/~landscape/img/Spinner.png';
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -27,16 +24,15 @@ export default class LaunchApp extends React.Component {
|
||||
const sigilColor = contact?.color
|
||||
? `#${uxToHex(contact.color)}`
|
||||
: props.dark
|
||||
? "#FFFFFF"
|
||||
: "#000000";
|
||||
|
||||
? '#FFFFFF'
|
||||
: '#000000';
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>OS1 - Home</title>
|
||||
</Helmet>
|
||||
<div className="h-100 overflow-y-scroll">
|
||||
<Box height='100%' overflowY='scroll'>
|
||||
<Welcome firstTime={props.launch.firstTime} api={props.api} />
|
||||
<Box
|
||||
ml='2'
|
||||
@ -80,7 +76,7 @@ export default class LaunchApp extends React.Component {
|
||||
weather={props.weather}
|
||||
/>
|
||||
</Box>
|
||||
<Groups associations={props.associations} invites={props.invites} api={props.api}/>
|
||||
<Groups associations={props.associations} invites={props.invites} api={props.api} />
|
||||
<Box
|
||||
position="absolute"
|
||||
fontFamily="mono"
|
||||
@ -92,10 +88,11 @@ export default class LaunchApp extends React.Component {
|
||||
mb={3}
|
||||
borderRadius={2}
|
||||
fontSize={0}
|
||||
p={2}>
|
||||
p={2}
|
||||
>
|
||||
{props.baseHash}
|
||||
</Box>
|
||||
</div>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { Text, Icon } from '@tlon/indigo-react';
|
||||
|
||||
import Tile from './tile';
|
||||
@ -23,7 +22,7 @@ export default class BasicTile extends React.PureComponent {
|
||||
verticalAlign='top'
|
||||
pt='5px'
|
||||
pr='2px'
|
||||
/>
|
||||
/>
|
||||
: null
|
||||
}{props.title}
|
||||
</Text>
|
||||
|
@ -1,38 +1,28 @@
|
||||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import SunCalc from 'suncalc';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import Tile from './tile';
|
||||
|
||||
const innerSize = 124; // clock size
|
||||
const VIEWBOX_SIZE = 100;
|
||||
const CX = VIEWBOX_SIZE / 2;
|
||||
const CY = VIEWBOX_SIZE / 2;
|
||||
const RADIUS = VIEWBOX_SIZE / 2;
|
||||
const CELESTIAL_BODY_SIZE = 16;
|
||||
|
||||
// polar to cartesian
|
||||
// var ptc = function(r, theta) {
|
||||
// return {
|
||||
// x: r * Math.cos(theta),
|
||||
// y: r * Math.sin(theta)
|
||||
// }
|
||||
// }
|
||||
|
||||
let timeTextColor = '#000000', dateTextColor = '#333333', background = '#ffffff';
|
||||
|
||||
const dark = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
if (dark.matches) {
|
||||
timeTextColor = '#ffffff';
|
||||
dateTextColor = '#7f7f7f';
|
||||
background = '#333';
|
||||
}
|
||||
|
||||
function darkColors(dark) {
|
||||
if (dark.matches) {
|
||||
timeTextColor = '#ffffff';
|
||||
dateTextColor = '#7f7f7f';
|
||||
background = '#ffffff';
|
||||
const ApplyClockBg = styled.div`
|
||||
.background {
|
||||
fill: ${p => p.theme.colors.white};
|
||||
}
|
||||
}
|
||||
|
||||
dark.addListener(darkColors);
|
||||
.time {
|
||||
fill: ${p => p.theme.colors.black};
|
||||
color: ${p => p.theme.colors.black};
|
||||
}
|
||||
.date {
|
||||
fill: ${p => p.theme.colors.gray};
|
||||
}
|
||||
`;
|
||||
|
||||
const toRelativeTime = (date, referenceTime, unit) => moment(date)
|
||||
.diff(referenceTime, unit);
|
||||
@ -42,10 +32,6 @@ const minsToDegs = (mins) => {
|
||||
return (mins / 1440) * 360;
|
||||
};
|
||||
|
||||
const splitArc = (start, end) => end + ((start - end) * 0.5);
|
||||
|
||||
const isOdd = n => Math.abs(n % 2) == 1;
|
||||
|
||||
const radToDeg = rad => rad * (180 / Math.PI);
|
||||
|
||||
const degToRad = deg => deg * (Math.PI / 180);
|
||||
@ -54,58 +40,137 @@ const convert = (date, referenceTime) => {
|
||||
return minsToDegs(toRelativeTime(date, referenceTime, 'minutes'));
|
||||
};
|
||||
|
||||
const circle = (ctx, x, y, r, from, to, fill) => {
|
||||
ctx.beginPath();
|
||||
ctx.arc( x, y, r, from, to );
|
||||
ctx.strokeStyle = 'rgba(0,0,0,0)';
|
||||
ctx.fillStyle = fill || 'rgba(0,0,0,0)';
|
||||
ctx.fill();
|
||||
};
|
||||
|
||||
const circleClip = (ctx, x, y, r, from, to, fill) => {
|
||||
ctx.globalCompositeOperation = 'xor';
|
||||
circle(ctx, x, y, r, from, to, fill);
|
||||
ctx.globalCompositeOperation = 'source-over';
|
||||
};
|
||||
|
||||
const circleOutline = (ctx, x, y, r, from, to, stroke, lineWidth) => {
|
||||
ctx.beginPath();
|
||||
ctx.arc( x, y, r, from, to );
|
||||
ctx.fillStyle = 'rgba(0,0,0,0)';
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.strokeStyle = stroke || 'rgba(0,0,0,0)';
|
||||
if (lineWidth) {
|
||||
ctx.stroke();
|
||||
// https://github.com/tingletech/moon-phase
|
||||
export const dFromPhase = (moonPhase) => {
|
||||
let mag, sweep, d = "m50,0";
|
||||
if (moonPhase <= 0.25) {
|
||||
sweep = [ 1, 0 ];
|
||||
mag = 20 - 20 * moonPhase * 4;
|
||||
} else if (moonPhase <= 0.50) {
|
||||
sweep = [ 0, 0 ];
|
||||
mag = 20 * (moonPhase - 0.25) * 4;
|
||||
} else if (moonPhase <= 0.75) {
|
||||
sweep = [ 1, 1 ];
|
||||
mag = 20 - 20 * (moonPhase - 0.50) * 4;
|
||||
} else if (moonPhase <= 1) {
|
||||
sweep = [ 0, 1 ];
|
||||
mag = 20 * (moonPhase - 0.75) * 4;
|
||||
}
|
||||
};
|
||||
|
||||
const arc = (ctx, x, y, r, from, to, fill) => {
|
||||
ctx.beginPath();
|
||||
ctx.arc( x, y, r, from, to );
|
||||
ctx.fillStyle = 'rgba(0,0,0,0)';
|
||||
ctx.lineWidth = r * 2;
|
||||
ctx.strokeStyle = fill || 'rgba(0,0,0,0)';
|
||||
ctx.stroke();
|
||||
};
|
||||
d = d + "a" + mag + ",20 0 1," + sweep[0] + " 0,100 ";
|
||||
d = d + "a20,20 0 1," + sweep[1] + " 0,-100";
|
||||
return d;
|
||||
}
|
||||
|
||||
const degArc = (ctx, x, y, r, from, to, fill) => {
|
||||
ctx.beginPath();
|
||||
ctx.arc( x, y, r, degToRad(from), degToRad(to));
|
||||
ctx.fillStyle = 'rgba(0,0,0,0)';
|
||||
ctx.lineWidth = r * 2;
|
||||
ctx.strokeStyle = fill || 'rgba(0,0,0,0)';
|
||||
ctx.stroke();
|
||||
};
|
||||
const Moon = ({ angle, ...props }) => {
|
||||
const phase = SunCalc.getMoonIllumination(moment().toDate()).phase.toFixed(2);
|
||||
const cx = CX + (RADIUS - 12) * Math.cos(degToRad(angle)) - (CELESTIAL_BODY_SIZE / 2);
|
||||
const cy = CY + (RADIUS - 12) * Math.sin(degToRad(angle)) - (CELESTIAL_BODY_SIZE / 2);
|
||||
return (
|
||||
<g>
|
||||
<mask id="umbra">
|
||||
<rect x="-50" y="-50" height="200" width="200" fill="black" />
|
||||
<path d={dFromPhase(phase)} fill="white"/>
|
||||
</mask>
|
||||
<use
|
||||
width={CELESTIAL_BODY_SIZE}
|
||||
height={CELESTIAL_BODY_SIZE}
|
||||
xlinkHref="#Moon-symbol"
|
||||
x={cx}
|
||||
y={cy}
|
||||
transform={`rotate(${angle} ${cx + (CELESTIAL_BODY_SIZE / 2)} ${cy + (CELESTIAL_BODY_SIZE / 2)})`}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
class Clock extends React.Component {
|
||||
const Sun = ({ angle, ...props}) => (
|
||||
<circle
|
||||
id="sun"
|
||||
cx={CX + (RADIUS - 12) * Math.cos(degToRad(angle))}
|
||||
cy={CY + (RADIUS - 12) * Math.sin(degToRad(angle))}
|
||||
fill="#FCC440"
|
||||
stroke="rgba(0,0,0,0.1)"
|
||||
r={CELESTIAL_BODY_SIZE / 2}
|
||||
{...props}
|
||||
></circle>
|
||||
);
|
||||
|
||||
const SvgArc = ({ start, end, ...rest }) => {
|
||||
const x1 = CX + RADIUS * Math.cos(degToRad(start));
|
||||
const y1 = CY + RADIUS * Math.sin(degToRad(start));
|
||||
const x2 = CX + RADIUS * Math.cos(degToRad(end));
|
||||
const y2 = CY + RADIUS * Math.sin(degToRad(end));
|
||||
|
||||
const isLarge = Math.abs((start > 360 ? start - 360 : start) - end) > 180;
|
||||
|
||||
const d = [
|
||||
'M', CX, CY,
|
||||
'L', x1, y1,
|
||||
'A', RADIUS, RADIUS, '0', (isLarge ? '1' : '0'), '1', x2, y2, 'z'
|
||||
].join(' ');
|
||||
|
||||
return <path d={d} {...rest} />;
|
||||
}
|
||||
|
||||
class ClockText extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
time: Date.now()
|
||||
}
|
||||
}
|
||||
componentDidMount() {
|
||||
this.interval = setInterval(() => this.setState({ time: Date.now() }), 1000);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
|
||||
render() {
|
||||
const now = moment(this.state.time);
|
||||
return (
|
||||
<>
|
||||
<use xlinkHref="#clock-center" className="background" />
|
||||
<text
|
||||
textAnchor="middle"
|
||||
x={CX}
|
||||
y={CY - 2}
|
||||
fontSize="10"
|
||||
fontFamily="Inter"
|
||||
className="time"
|
||||
>
|
||||
<tspan>{now.format('h')}</tspan>
|
||||
<tspan>:<animate attributeName="fill"
|
||||
values="currentColor;transparent"
|
||||
begin="0s"
|
||||
dur="1s"
|
||||
calcMode="discrete"
|
||||
repeatCount="indefinite"/>
|
||||
</tspan>
|
||||
<tspan>{now.format('mm A')}</tspan>
|
||||
</text>
|
||||
<text
|
||||
textAnchor="middle"
|
||||
x={CX}
|
||||
y={CY + 11}
|
||||
fontSize="10"
|
||||
fontFamily="Inter"
|
||||
className="date"
|
||||
>{now.format('MMM D')}<tspan style={{ fontFeatureSettings: "'sups' 1" }}>{now.format('Do').replace(now.format('D'), '')}</tspan></text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Clock extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.animate = this.animate.bind(this);
|
||||
this.canvasRef = React.createRef();
|
||||
this.canvas = null;
|
||||
this.angle = 0;
|
||||
this.referenceTime = moment().startOf('day').subtract(6, 'hours');
|
||||
this.state = {
|
||||
time: Date.now(),
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
geolocationSuccess: false,
|
||||
@ -164,246 +229,84 @@ class Clock extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.canvas = initCanvas(
|
||||
this.canvasRef,
|
||||
{ x: innerSize, y: innerSize },
|
||||
4
|
||||
);
|
||||
|
||||
this.initGeolocation();
|
||||
this.animate();
|
||||
this.interval = setInterval(() => this.setState({ time: Date.now() }), 60000);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.animationTimer) {
|
||||
window.clearTimeout(this.animationTimer);
|
||||
}
|
||||
}
|
||||
|
||||
animate() {
|
||||
this.animationTimer =
|
||||
window.setTimeout(() => window.requestAnimationFrame(this.animate), 1000);
|
||||
|
||||
const { state } = this;
|
||||
const time = new Date();
|
||||
const ctx = this.canvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, ctx.width, ctx.height);
|
||||
ctx.save();
|
||||
|
||||
const ctr = innerSize / 2;
|
||||
|
||||
// Sun+moon calculations
|
||||
const cx = ctr;
|
||||
const cy = ctr;
|
||||
this.angle = degToRad(convert(time, this.referenceTime));
|
||||
const newX = cx + (ctr - 15) * Math.cos(this.angle);
|
||||
const newY = cy + (ctr - 15) * Math.sin(this.angle);
|
||||
|
||||
// Center white circle with time and date
|
||||
circle(
|
||||
ctx,
|
||||
ctr,
|
||||
ctr,
|
||||
ctr,
|
||||
-1,
|
||||
2 * Math.PI,
|
||||
background
|
||||
);
|
||||
|
||||
// Day
|
||||
degArc(
|
||||
ctx,
|
||||
ctr,
|
||||
ctr,
|
||||
ctr / 2,
|
||||
state.sunriseEnd,
|
||||
state.sunset,
|
||||
'rgba(33, 157, 255, .2)'
|
||||
);
|
||||
|
||||
// Sunrise
|
||||
degArc(
|
||||
ctx,
|
||||
ctr,
|
||||
ctr,
|
||||
ctr / 2,
|
||||
state.sunsetStart,
|
||||
state.sunriseEnd,
|
||||
'#FFC700'
|
||||
);
|
||||
|
||||
// Sunset
|
||||
degArc(
|
||||
ctx,
|
||||
ctr,
|
||||
ctr,
|
||||
ctr / 2,
|
||||
state.dusk,
|
||||
state.dawn,
|
||||
'rgba(255, 65, 54, .8)'
|
||||
);
|
||||
|
||||
// Night
|
||||
degArc(
|
||||
ctx,
|
||||
ctr,
|
||||
ctr,
|
||||
ctr / 2,
|
||||
state.night,
|
||||
state.nightEnd,
|
||||
'rgba(0, 0, 0, .8)'
|
||||
);
|
||||
|
||||
if (
|
||||
radToDeg(this.angle) > splitArc(state.sunriseEnd, state.nightEnd)
|
||||
&& radToDeg(this.angle) < splitArc(state.sunset, state.night)
|
||||
) {
|
||||
// Sun circle
|
||||
circle(
|
||||
ctx,
|
||||
newX-1/2,
|
||||
newY-1/2,
|
||||
8,
|
||||
0,
|
||||
2 * Math.PI,
|
||||
'#FCC440'
|
||||
);
|
||||
|
||||
// Sun circle border
|
||||
circleOutline(
|
||||
ctx,
|
||||
newX-1/2,
|
||||
newY-1/2,
|
||||
8,
|
||||
0,
|
||||
2 * Math.PI,
|
||||
'rgba(0,0,0,0.1)',
|
||||
1
|
||||
);
|
||||
} else {
|
||||
// Moon circle
|
||||
circle(
|
||||
ctx,
|
||||
newX-1/2,
|
||||
newY-1/2,
|
||||
8,
|
||||
0,
|
||||
2 * Math.PI,
|
||||
'#FFFFFF'
|
||||
);
|
||||
// Moon circle outline
|
||||
circleOutline(
|
||||
ctx,
|
||||
newX-1/2,
|
||||
newY-1/2,
|
||||
8,
|
||||
0,
|
||||
2 * Math.PI,
|
||||
'#000000',
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Outer borders
|
||||
circleOutline(
|
||||
ctx,
|
||||
ctr,
|
||||
ctr,
|
||||
ctr-1,
|
||||
-1,
|
||||
2 * Math.PI,
|
||||
'none',
|
||||
0
|
||||
);
|
||||
|
||||
// Center white circle border
|
||||
circleOutline(
|
||||
ctx,
|
||||
ctr,
|
||||
ctr,
|
||||
ctr/1.85,
|
||||
-1,
|
||||
2 * Math.PI,
|
||||
'none',
|
||||
0
|
||||
);
|
||||
|
||||
// Inner hole
|
||||
circle(
|
||||
ctx,
|
||||
ctr,
|
||||
ctr,
|
||||
ctr/1.85,
|
||||
-1,
|
||||
2 * Math.PI,
|
||||
background
|
||||
);
|
||||
|
||||
// Text for time and date
|
||||
const timeText = isOdd(time.getSeconds())
|
||||
? moment().format('h mm A')
|
||||
: moment().format('h:mm A');
|
||||
const dateText = moment().format('MMM Do');
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillStyle = timeTextColor;
|
||||
ctx.font = '12px Inter';
|
||||
ctx.fillText(timeText, ctr, ctr + 6 - 7);
|
||||
ctx.fillStyle = dateTextColor;
|
||||
ctx.font = '12px Inter';
|
||||
ctx.fillText(dateText, ctr, ctr + 6 + 7);
|
||||
|
||||
ctx.restore();
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
|
||||
render() {
|
||||
const now = moment(this.state.time);
|
||||
const angle = convert(now, this.referenceTime);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
style={{ height: '100%', width: '100%'}}
|
||||
ref={ canvasRef => this.canvasRef = canvasRef }
|
||||
id="clock-canvas"
|
||||
/>
|
||||
<ApplyClockBg>
|
||||
<svg
|
||||
style={{ height: '100%', width: '100%'}}
|
||||
viewBox={`0 0 ${VIEWBOX_SIZE} ${VIEWBOX_SIZE}`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
>
|
||||
<defs>
|
||||
<symbol id="clock-center">
|
||||
<circle r={VIEWBOX_SIZE / 1.85 / 2} cx={CX} cy={CY} />
|
||||
</symbol>
|
||||
<mask id="center-mask">
|
||||
<rect x="0" y="0" width={VIEWBOX_SIZE} height={VIEWBOX_SIZE} fill="white" />
|
||||
<use xlinkHref="#clock-center" fill="black"/>
|
||||
</mask>
|
||||
<symbol id="Moon-symbol" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
|
||||
<g>
|
||||
<path mask="url(#umbra)" d="m50,0 a20,20 0 1,1 0,100 a20,20 0 1,1 0,-100" fill="#fff" stroke="#000"/>
|
||||
</g>
|
||||
</symbol>
|
||||
</defs>
|
||||
<g mask="url(#center-mask)">
|
||||
<circle cx={CY} cy={CY} r={RADIUS} className="background" />
|
||||
<SvgArc
|
||||
id="day"
|
||||
start={this.state.sunriseEnd}
|
||||
end={this.state.sunset}
|
||||
fill="rgba(33, 157, 255, .2)"
|
||||
/>
|
||||
<SvgArc
|
||||
id="sunrise"
|
||||
start={this.state.sunsetStart}
|
||||
end={this.state.sunriseEnd}
|
||||
fill="#FFC700"
|
||||
/>
|
||||
<SvgArc
|
||||
id="sunset"
|
||||
start={this.state.dusk}
|
||||
end={this.state.dawn}
|
||||
fill="rgba(255, 65, 54, .8)"
|
||||
/>
|
||||
<SvgArc
|
||||
id="night"
|
||||
start={this.state.night}
|
||||
end={this.state.nightEnd}
|
||||
fill="rgba(0, 0, 0, .8)"
|
||||
/>
|
||||
{angle > this.state.nightEnd && angle < this.state.sunset
|
||||
? <Sun angle={angle} />
|
||||
: <Moon angle={angle} />
|
||||
}
|
||||
</g>
|
||||
<ClockText />
|
||||
</svg>
|
||||
</ApplyClockBg>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default class ClockTile extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
renderWrapper(child) {
|
||||
return (
|
||||
<Tile p={0} border={0} bg='transparent' boxShadow='none'>
|
||||
{child}
|
||||
</Tile>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const data = this.props.location ? this.props.location : {};
|
||||
return this.renderWrapper((
|
||||
<Clock data={data} />
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
const initCanvas = (canvas, size, ratio) => {
|
||||
const { x, y } = size;
|
||||
// let ratio = ctx.webkitBackingStorePixelRatio < 2
|
||||
// ? window.devicePixelRatio
|
||||
// : 1;
|
||||
|
||||
// default for high print resolution.
|
||||
// ratio = ratio * resMult;
|
||||
|
||||
canvas.width = x * ratio;
|
||||
canvas.height = y * ratio;
|
||||
canvas.style.width = x + 'px';
|
||||
canvas.style.height = y + 'px';
|
||||
|
||||
canvas.getContext('2d').scale(ratio, ratio);
|
||||
|
||||
return canvas;
|
||||
};
|
||||
const ClockTile = ({ location = {} }) => (
|
||||
<Tile p={0} border={0} bg='transparent' boxShadow='none'>
|
||||
<Clock data={location} />
|
||||
</Tile>
|
||||
);
|
||||
|
||||
export default ClockTile;
|
@ -1,26 +1,30 @@
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { Box, BaseImage } from '@tlon/indigo-react';
|
||||
import Tile from './tile';
|
||||
|
||||
export default class CustomTile extends React.PureComponent {
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
|
||||
return (
|
||||
<Tile>
|
||||
<div className={"w-100 h-100 relative bg-white bg-gray0-d ba " +
|
||||
"b--black br2 b--gray1-d"}>
|
||||
<img
|
||||
className="absolute invert-d"
|
||||
<Box
|
||||
width='100%'
|
||||
height='100%'
|
||||
position='relative'
|
||||
backgroundColor='white'
|
||||
border='1px solid'
|
||||
borderColor='washedGray'
|
||||
borderRadius='2'
|
||||
>
|
||||
<BaseImage
|
||||
position='absolute'
|
||||
className="invert-d"
|
||||
style={{ left: 38, top: 38 }}
|
||||
src={'/~launch/img/UnknownCustomTile.png'}
|
||||
width={48}
|
||||
height={48} />
|
||||
</div>
|
||||
src='/~launch/img/UnknownCustomTile.png'
|
||||
width='48px'
|
||||
height='48px'
|
||||
/>
|
||||
</Box>
|
||||
</Tile>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import { Box, Icon, Text } from '@tlon/indigo-react';
|
||||
import { Box, Icon, Text, BaseAnchor, BaseInput } from '@tlon/indigo-react';
|
||||
|
||||
import Tile from './tile';
|
||||
|
||||
@ -103,14 +103,15 @@ export default class WeatherTile extends React.Component {
|
||||
let secureCheck;
|
||||
let error;
|
||||
if (this.state.error === true) {
|
||||
error = <p className="f9 red2 pt1">Please try again.</p>;
|
||||
error = <Text display='block' color='red' pt='1'>Please try again.</Text>;
|
||||
}
|
||||
if (location.protocol === 'https:') {
|
||||
secureCheck = (
|
||||
<a className="black white-d f9 pointer"
|
||||
onClick={() => this.locationSubmit()}>
|
||||
<Text color='black' cursor='pointer'
|
||||
onClick={() => this.locationSubmit()}
|
||||
>
|
||||
Detect ->
|
||||
</a>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
return this.renderWrapper(
|
||||
@ -120,32 +121,38 @@ export default class WeatherTile extends React.Component {
|
||||
justifyContent='space-between'
|
||||
height='100%'
|
||||
>
|
||||
<a
|
||||
className="f9 black white-d pointer"
|
||||
<Text
|
||||
color='black'
|
||||
cursor='pointer'
|
||||
onClick={() =>
|
||||
this.setState({ manualEntry: !this.state.manualEntry })
|
||||
}
|
||||
>
|
||||
<-
|
||||
</a>
|
||||
</Text>
|
||||
{secureCheck}
|
||||
<Text pb={1} mb='auto'>
|
||||
Please enter your{' '}
|
||||
<a
|
||||
className="bb"
|
||||
<BaseAnchor
|
||||
borderBottom='1px solid'
|
||||
color='black'
|
||||
href="https://latitudeandlongitude.org/"
|
||||
target="_blank"
|
||||
>
|
||||
latitude and longitude
|
||||
</a>
|
||||
</BaseAnchor>
|
||||
.
|
||||
</Text>
|
||||
{error}
|
||||
<form mt='auto' className="flex" style={{ marginBlockEnd: 0}}>
|
||||
<input
|
||||
<Box mt='auto' display='flex' marginBlockEnd='0'>
|
||||
<BaseInput
|
||||
id="gps"
|
||||
size="10"
|
||||
className="w-100 black white-d bg-transparent bn f9"
|
||||
width='100%'
|
||||
color='black'
|
||||
fontSize='0'
|
||||
backgroundColor='transparent'
|
||||
border='0'
|
||||
type="text"
|
||||
placeholder="29.55, -95.08"
|
||||
onKeyDown={(e) => {
|
||||
@ -153,15 +160,21 @@ export default class WeatherTile extends React.Component {
|
||||
e.preventDefault();
|
||||
this.manualLocationSubmit(e.target.value);
|
||||
}
|
||||
}} />
|
||||
<input
|
||||
className={'bg-transparent black white-d bn pointer ' +
|
||||
'f9 flex-shrink-0 pr1'}
|
||||
}}
|
||||
/>
|
||||
<BaseInput
|
||||
backgroundColor='transparent'
|
||||
color='black'
|
||||
cursor='pointer'
|
||||
flexShrink='0'
|
||||
pl='1'
|
||||
fontSize='0'
|
||||
border='0'
|
||||
type="submit"
|
||||
onClick={() => this.manualLocationSubmit()}
|
||||
value="->"
|
||||
/>
|
||||
</form>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@ -216,17 +229,17 @@ export default class WeatherTile extends React.Component {
|
||||
alignItems='space-between'
|
||||
>
|
||||
<Text color={weatherStyle.text}>
|
||||
<Icon icon='Weather' color={weatherStyle.text} display='inline' style={{ position: 'relative', top: '.3em'}} />
|
||||
<Icon icon='Weather' color={weatherStyle.text} display='inline' style={{ position: 'relative', top: '.3em' }} />
|
||||
Weather
|
||||
<a
|
||||
style={{ color: weatherStyle.text }}
|
||||
className='pointer'
|
||||
onClick={() =>
|
||||
this.setState({ manualEntry: !this.state.manualEntry })
|
||||
}
|
||||
>
|
||||
->
|
||||
</a>
|
||||
<Text
|
||||
color={weatherStyle.text}
|
||||
cursor='pointer'
|
||||
onClick={() =>
|
||||
this.setState({ manualEntry: !this.state.manualEntry })
|
||||
}
|
||||
>
|
||||
->
|
||||
</Text>
|
||||
</Text>
|
||||
|
||||
<Box
|
||||
@ -257,15 +270,19 @@ export default class WeatherTile extends React.Component {
|
||||
|
||||
if (this.props.location) {
|
||||
return this.renderWrapper((
|
||||
<div
|
||||
className={'pa2 w-100 h-100 ' +
|
||||
'bg-white bg-gray0-d black white-d'}>
|
||||
<Box
|
||||
p='2'
|
||||
width='100%'
|
||||
height='100%'
|
||||
backgroundColor='white'
|
||||
color='black'
|
||||
>
|
||||
<Icon icon='Weather' color='black' display='inline' style={{ position: 'relative', top: '.3em' }} />
|
||||
<Text>Weather</Text>
|
||||
<p className="w-100 flex-col f9">
|
||||
<Text pt='2' width='100%' display='flex' flexDirection='column'>
|
||||
Loading, please check again later...
|
||||
</p>
|
||||
</div>
|
||||
</Text>
|
||||
</Box>
|
||||
));
|
||||
}
|
||||
return this.renderNoData();
|
||||
|
@ -60,6 +60,7 @@ export function MarkdownEditor(
|
||||
border={1}
|
||||
borderColor="lightGray"
|
||||
borderRadius={2}
|
||||
height={['calc(100% - 22vh)', '100%']}
|
||||
{...boxProps}
|
||||
>
|
||||
<CodeEditor
|
||||
|
@ -24,6 +24,7 @@ export const MarkdownField = ({
|
||||
return (
|
||||
<Box
|
||||
overflowY="hidden"
|
||||
height='100%'
|
||||
width="100%"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
|
@ -41,13 +41,13 @@ export function PostForm(props: PostFormProps) {
|
||||
onSubmit={onSubmit}
|
||||
validateOnBlur
|
||||
>
|
||||
<Form style={{ display: "contents" }}>
|
||||
<Row flexDirection={["column-reverse", "row"]} mb={4} gapX={4} justifyContent='space-between'>
|
||||
<Input maxWidth='40rem' flexGrow={1} placeholder="Post Title" id="title" />
|
||||
<Form style={{ display: "contents"}}>
|
||||
<Row flexShrink='0' flexDirection={["column-reverse", "row"]} mb={4} gapX={4} justifyContent='space-between'>
|
||||
<Input maxWidth='40rem' width='100%' flexShrink={[0, 1]} placeholder="Post Title" id="title" />
|
||||
<AsyncButton
|
||||
ml={[0,2]}
|
||||
mb={[4,0]}
|
||||
flexShrink={1}
|
||||
flexShrink={0}
|
||||
primary
|
||||
loadingText={loadingText}
|
||||
>
|
||||
|
@ -168,7 +168,7 @@ export function GroupsPane(props: GroupsPaneProps) {
|
||||
render={(routeProps) => {
|
||||
const newUrl = `${baseUrl}/new`;
|
||||
return (
|
||||
<Skeleton recentGroups={recentGroups} {...props} baseUrl={baseUrl}>
|
||||
<Skeleton mobileHide recentGroups={recentGroups} {...props} baseUrl={baseUrl}>
|
||||
<NewChannel
|
||||
{...routeProps}
|
||||
api={api}
|
||||
|
@ -4,26 +4,27 @@ import {
|
||||
ManagedTextInputField as Input,
|
||||
Col,
|
||||
ManagedRadioButtonField as Radio,
|
||||
Text
|
||||
} from '@tlon/indigo-react';
|
||||
import { Formik, Form } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import GlobalApi from '~/logic/api/global';
|
||||
import { AsyncButton } from '~/views/components/AsyncButton';
|
||||
import { FormError } from '~/views/components/FormError';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import { stringToSymbol } from '~/logic/lib/util';
|
||||
import { Associations } from '~/types/metadata-update';
|
||||
import { useWaitForProps } from '~/logic/lib/useWaitForProps';
|
||||
import { Groups } from '~/types/group-update';
|
||||
import { ShipSearch } from '~/views/components/ShipSearch';
|
||||
import { Rolodex } from '~/types';
|
||||
Text,
|
||||
} from "@tlon/indigo-react";
|
||||
import { Formik, Form } from "formik";
|
||||
import * as Yup from "yup";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { AsyncButton } from "~/views/components/AsyncButton";
|
||||
import { FormError } from "~/views/components/FormError";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import { stringToSymbol, parentPath } from "~/logic/lib/util";
|
||||
import GroupSearch from "~/views/components/GroupSearch";
|
||||
import { Associations } from "~/types/metadata-update";
|
||||
import { useWaitForProps } from "~/logic/lib/useWaitForProps";
|
||||
import { Groups } from "~/types/group-update";
|
||||
import { ShipSearch } from "~/views/components/ShipSearch";
|
||||
import { Rolodex, Workspace } from "~/types";
|
||||
|
||||
interface FormSchema {
|
||||
name: string;
|
||||
description: string;
|
||||
ships: string[];
|
||||
type: 'chat' | 'publish' | 'links';
|
||||
type: "chat" | "publish" | "link";
|
||||
}
|
||||
|
||||
const formSchema = Yup.object({
|
||||
@ -39,6 +40,7 @@ interface NewChannelProps {
|
||||
contacts: Rolodex;
|
||||
groups: Groups;
|
||||
group?: string;
|
||||
workspace: Workspace;
|
||||
}
|
||||
|
||||
export function NewChannel(props: NewChannelProps & RouteComponentProps) {
|
||||
@ -69,7 +71,7 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps) {
|
||||
case 'publish':
|
||||
await props.api.publish.newBook(resId, name, description, group);
|
||||
break;
|
||||
case 'links':
|
||||
case "link":
|
||||
if (group) {
|
||||
await api.graph.createManagedGraph(
|
||||
resId,
|
||||
@ -97,6 +99,8 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps) {
|
||||
await waiter(p => Boolean(p?.groups?.[`/ship/~${window.ship}/${resId}`]));
|
||||
}
|
||||
actions.setStatus({ success: null });
|
||||
const resourceUrl = parentPath(location.pathname);
|
||||
history.push(`${resourceUrl}/resource/${type}${type === 'link' ? '/ship' : ''}/~${window.ship}/${resId}`);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
actions.setStatus({ error: 'Channel creation failed' });
|
||||
@ -129,7 +133,7 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps) {
|
||||
<Box color="black" mb={2}>Channel Type</Box>
|
||||
<Radio label="Chat" id="chat" name="type" />
|
||||
<Radio label="Notebook" id="publish" name="type" />
|
||||
<Radio label="Collection" id="links" name="type" />
|
||||
<Radio label="Collection" id="link" name="type" />
|
||||
</Col>
|
||||
<Input
|
||||
id="name"
|
||||
|
Loading…
Reference in New Issue
Block a user