Merge branch 'release/next-js' into release/hotfix/2021-1-21

This commit is contained in:
Matilde Park 2021-01-21 19:04:47 -05:00
commit 604b338dcd
54 changed files with 3750 additions and 3718 deletions

View File

@ -96,7 +96,7 @@ module.exports = {
] ]
} }
}, },
exclude: /node_modules/ exclude: /node_modules\/(?!(@tlon\/indigo-dark|@tlon\/indigo-light)\/).*/
}, },
{ {
test: /\.css$/i, test: /\.css$/i,

View File

@ -26,7 +26,7 @@ module.exports = {
] ]
} }
}, },
exclude: /node_modules/ exclude: /node_modules\/(?!(@tlon\/indigo-dark|@tlon\/indigo-light)\/).*/
}, },
{ {
test: /\.css$/i, test: /\.css$/i,

File diff suppressed because it is too large Load Diff

View File

@ -4,83 +4,85 @@
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.10.5", "@babel/runtime": "^7.12.5",
"@reach/disclosure": "^0.10.5", "@reach/disclosure": "^0.10.5",
"@reach/menu-button": "^0.10.5", "@reach/menu-button": "^0.10.5",
"@reach/tabs": "^0.10.5", "@reach/tabs": "^0.10.5",
"@tlon/indigo-light": "^1.0.3", "@tlon/indigo-dark": "^1.0.6",
"@tlon/indigo-react": "1.2.15", "@tlon/indigo-light": "^1.0.6",
"@tlon/sigil-js": "^1.4.2", "@tlon/indigo-react": "1.2.17",
"aws-sdk": "^2.726.0", "@tlon/sigil-js": "^1.4.3",
"aws-sdk": "^2.830.0",
"big-integer": "^1.6.48", "big-integer": "^1.6.48",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"codemirror": "^5.55.0", "codemirror": "^5.59.2",
"css-loader": "^3.5.3", "css-loader": "^3.6.0",
"file-saver": "^2.0.2", "file-saver": "^2.0.5",
"formik": "^2.1.4", "formik": "^2.2.6",
"immer": "^8.0.0", "immer": "^8.0.1",
"lodash": "^4.17.15", "lodash": "^4.17.20",
"markdown-to-jsx": "^6.11.4", "markdown-to-jsx": "^6.11.4",
"moment": "^2.20.1", "moment": "^2.29.1",
"mousetrap": "^1.6.5", "mousetrap": "^1.6.5",
"mousetrap-global-bind": "^1.1.0", "mousetrap-global-bind": "^1.1.0",
"normalize-wheel": "1.0.1", "normalize-wheel": "1.0.1",
"oembed-parser": "^1.4.1", "oembed-parser": "^1.4.5",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"react": "^16.5.2", "react": "^16.14.0",
"react-codemirror2": "^6.0.1", "react-codemirror2": "^6.0.1",
"react-dom": "^16.8.6", "react-dom": "^16.14.0",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
"react-markdown": "^4.3.1", "react-markdown": "^4.3.1",
"react-oembed-container": "^1.0.0", "react-oembed-container": "^1.0.0",
"react-router-dom": "^5.0.0", "react-router-dom": "^5.2.0",
"react-virtuoso": "^0.20.0", "react-virtuoso": "^0.20.3",
"react-visibility-sensor": "^5.1.1", "react-visibility-sensor": "^5.1.1",
"remark-breaks": "^2.0.1",
"remark-disable-tokenizers": "^1.0.24", "remark-disable-tokenizers": "^1.0.24",
"style-loader": "^1.2.1", "style-loader": "^1.3.0",
"styled-components": "^5.1.0", "styled-components": "^5.1.1",
"styled-system": "^5.1.5", "styled-system": "^5.1.5",
"suncalc": "^1.8.0", "suncalc": "^1.8.0",
"urbit-ob": "^5.0.0", "urbit-ob": "^5.0.1",
"yup": "^0.29.3", "yup": "^0.29.3",
"zustand": "^3.2.0" "zustand": "^3.3.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.9.0", "@babel/core": "^7.12.10",
"@babel/plugin-proposal-class-properties": "^7.8.3", "@babel/plugin-proposal-class-properties": "^7.12.1",
"@babel/plugin-proposal-object-rest-spread": "^7.9.5", "@babel/plugin-proposal-object-rest-spread": "^7.12.1",
"@babel/plugin-proposal-optional-chaining": "^7.9.0", "@babel/plugin-proposal-optional-chaining": "^7.12.7",
"@babel/plugin-transform-runtime": "^7.10.5", "@babel/plugin-transform-runtime": "^7.12.10",
"@babel/preset-env": "^7.9.5", "@babel/preset-env": "^7.12.11",
"@babel/preset-react": "^7.9.4", "@babel/preset-react": "^7.12.10",
"@babel/preset-typescript": "^7.10.1", "@babel/preset-typescript": "^7.12.7",
"@types/lodash": "^4.14.155", "@types/lodash": "^4.14.168",
"@types/react": "^16.9.38", "@types/react": "^16.14.2",
"@types/react-dom": "^16.9.8", "@types/react-dom": "^16.9.10",
"@types/react-router-dom": "^5.1.5", "@types/react-router-dom": "^5.1.7",
"@types/styled-components": "^5.1.2", "@types/styled-components": "^5.1.7",
"@types/styled-system": "^5.1.10", "@types/styled-system": "^5.1.10",
"@types/yup": "^0.29.7", "@types/yup": "^0.29.11",
"@typescript-eslint/eslint-plugin": "^3.8.0", "@typescript-eslint/eslint-plugin": "^3.10.1",
"@typescript-eslint/parser": "^3.8.0", "@typescript-eslint/parser": "^3.10.1",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"babel-loader": "^8.1.0", "babel-loader": "^8.2.2",
"babel-plugin-lodash": "^3.3.4", "babel-plugin-lodash": "^3.3.4",
"babel-plugin-root-import": "^6.5.0", "babel-plugin-root-import": "^6.6.0",
"clean-webpack-plugin": "^3.0.0", "clean-webpack-plugin": "^3.0.0",
"cross-env": "^7.0.2", "cross-env": "^7.0.3",
"eslint": "^6.8.0", "eslint": "^6.8.0",
"eslint-plugin-react": "^7.19.0", "eslint-plugin-react": "^7.22.0",
"file-loader": "^6.0.0", "file-loader": "^6.2.0",
"html-webpack-plugin": "^4.2.0", "html-webpack-plugin": "^4.5.1",
"moment-locales-webpack-plugin": "^1.2.0", "moment-locales-webpack-plugin": "^1.2.0",
"react-hot-loader": "^4.12.21", "react-hot-loader": "^4.13.0",
"sass": "^1.26.5", "sass": "^1.32.5",
"sass-loader": "^8.0.2", "sass-loader": "^8.0.2",
"typescript": "^3.9.7", "typescript": "^3.9.7",
"webpack": "^4.43.0", "webpack": "^4.46.0",
"webpack-cli": "^3.3.11", "webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.10.3" "webpack-dev-server": "^3.11.2"
}, },
"scripts": { "scripts": {
"lint": "eslint ./src/**/*.{js,ts,tsx}", "lint": "eslint ./src/**/*.{js,ts,tsx}",

View File

@ -1,4 +1,4 @@
import { useEffect } from 'react'; import { useEffect, useState } from 'react';
import _ from "lodash"; import _ from "lodash";
import f, { memoize } from "lodash/fp"; import f, { memoize } from "lodash/fp";
import bigInt, { BigInteger } from "big-integer"; import bigInt, { BigInteger } from "big-integer";
@ -13,7 +13,7 @@ export const MOMENT_CALENDAR_DATE = {
nextWeek: "dddd", nextWeek: "dddd",
lastDay: "[Yesterday]", lastDay: "[Yesterday]",
lastWeek: "[Last] dddd", lastWeek: "[Last] dddd",
sameElse: "DD/MM/YYYY", sameElse: "~YYYY.M.D",
}; };
export function appIsGraph(app: string) { export function appIsGraph(app: string) {
@ -361,4 +361,13 @@ export function pluralize(text: string, isPlural = false, vowel = false) {
export function useShowNickname(contact: Contact | null, hide?: boolean): boolean { export function useShowNickname(contact: Contact | null, hide?: boolean): boolean {
const hideNicknames = typeof hide !== 'undefined' ? hide : useLocalState(state => state.hideNicknames); const hideNicknames = typeof hide !== 'undefined' ? hide : useLocalState(state => state.hideNicknames);
return !!(contact && contact.nickname && !hideNicknames); return !!(contact && contact.nickname && !hideNicknames);
}
export function useHovering() {
const [hovering, setHovering] = useState(false);
const bind = {
onMouseEnter: () => setHovering(true),
onMouseLeave: () => setHovering(false)
};
return { hovering, bind };
} }

View File

@ -386,5 +386,7 @@ function archive(json: any, state: HarkState) {
notifIdxEqual(index, idxNotif.index) notifIdxEqual(index, idxNotif.index)
); );
state.notifications.set(time, unarchived); state.notifications.set(time, unarchived);
const newlyRead = archived.filter(x => !x.notification.read).length;
updateNotificationStats(state, index, 'notifications', (x) => x - newlyRead);
} }
} }

View File

@ -31,7 +31,7 @@ const useLocalState = create<LocalState>(persist((set, get) => ({
suspendedFocus: undefined, suspendedFocus: undefined,
toggleOmnibox: () => set(produce(state => { toggleOmnibox: () => set(produce(state => {
state.omniboxShown = !state.omniboxShown; state.omniboxShown = !state.omniboxShown;
if (state.suspendedFocus) { if (typeof state.suspendedFocus?.focus === 'function') {
state.suspendedFocus.focus(); state.suspendedFocus.focus();
state.suspendedFocus = undefined; state.suspendedFocus = undefined;
} else { } else {

View File

@ -11,8 +11,8 @@ import 'mousetrap-global-bind';
import './css/indigo-static.css'; import './css/indigo-static.css';
import './css/fonts.css'; import './css/fonts.css';
import light from './themes/light'; import light from '@tlon/indigo-light';
import dark from './themes/old-dark'; import dark from '@tlon/indigo-dark';
import { Text, Anchor, Row } from '@tlon/indigo-react'; import { Text, Anchor, Row } from '@tlon/indigo-react';
@ -40,7 +40,7 @@ const Root = styled.div`
background-size: cover; background-size: cover;
` : p.background?.type === 'color' ? ` ` : p.background?.type === 'color' ? `
background-color: ${p.background.color}; background-color: ${p.background.color};
` : '' ` : `background-color: ${p.theme.colors.white};`
} }
display: flex; display: flex;
flex-flow: column nowrap; flex-flow: column nowrap;
@ -90,7 +90,7 @@ class App extends React.Component {
this.themeWatcher.onchange = this.updateTheme; this.themeWatcher.onchange = this.updateTheme;
setTimeout(() => { setTimeout(() => {
// Something about how the store works doesn't like changing it // Something about how the store works doesn't like changing it
// before the app has actually rendered, hence the timeout // before the app has actually rendered, hence the timeout.
this.updateTheme(this.themeWatcher); this.updateTheme(this.themeWatcher);
}, 500); }, 500);
this.api.local.getBaseHash(); this.api.local.getBaseHash();

View File

@ -231,8 +231,8 @@ export const MessageWithSigil = (props) => {
}} }}
title={`~${msg.author}`} title={`~${msg.author}`}
>{name}</Text> >{name}</Text>
<Text flexShrink='0' gray mono className="v-mid">{timestamp}</Text> <Text flexShrink='0' fontSize='0' gray mono className="v-mid">{timestamp}</Text>
<Text flexShrink={0} gray mono ml={2} className="v-mid child dn-s">{datestamp}</Text> <Text flexShrink={0} gray mono ml={2} className="v-mid child dn-s">{datestamp}</Text>
</Box> </Box>
<ContentBox flexShrink={0} fontSize={fontSize ? fontSize : '14px'}> <ContentBox flexShrink={0} fontSize={fontSize ? fontSize : '14px'}>
{msg.contents.map(c => {msg.contents.map(c =>
@ -259,7 +259,7 @@ const ContentBox = styled(Box)`
export const MessageWithoutSigil = ({ timestamp, contacts, msg, measure, group }) => ( export const MessageWithoutSigil = ({ timestamp, contacts, msg, measure, group }) => (
<> <>
<Text flexShrink={0} mono gray display='inline-block' pt='2px' lineHeight='tall' className="child">{timestamp}</Text> <Text flexShrink={0} mono gray display='inline-block' pt='2px' lineHeight='tall' className="child" fontSize='0'>{timestamp}</Text>
<ContentBox flexShrink={0} fontSize='14px' className="clamp-message" style={{ flexGrow: 1 }}> <ContentBox flexShrink={0} fontSize='14px' className="clamp-message" style={{ flexGrow: 1 }}>
{msg.contents.map((c, i) => ( {msg.contents.map((c, i) => (
<MessageContent <MessageContent
@ -282,7 +282,7 @@ export const MessageContent = ({ content, contacts, measure, fontSize, group })
url={content.url} url={content.url}
onLoad={measure} onLoad={measure}
imageProps={{style: { imageProps={{style: {
maxWidth: '18rem', maxWidth: 'min(100%,18rem)',
display: 'block' display: 'block'
}}} }}}
videoProps={{style: { videoProps={{style: {

View File

@ -193,6 +193,8 @@ export default class ChatEditor extends Component {
alignItems='center' alignItems='center'
flexGrow='1' flexGrow='1'
height='100%' height='100%'
paddingTop={MOBILE_BROWSER_REGEX.test(navigator.userAgent) ? '16px' : '0'}
paddingBottom={MOBILE_BROWSER_REGEX.test(navigator.userAgent) ? '16px' : '0'}
maxHeight='224px' maxHeight='224px'
width='calc(100% - 88px)' width='calc(100% - 88px)'
className={inCodeMode ? 'chat code' : 'chat'} className={inCodeMode ? 'chat code' : 'chat'}
@ -201,7 +203,7 @@ export default class ChatEditor extends Component {
{MOBILE_BROWSER_REGEX.test(navigator.userAgent) {MOBILE_BROWSER_REGEX.test(navigator.userAgent)
? <MobileBox ? <MobileBox
data-value={this.state.message} data-value={this.state.message}
fontSize="14px" fontSize="1"
lineHeight="tall" lineHeight="tall"
onClick={event => { onClick={event => {
if (this.editor) { if (this.editor) {
@ -211,7 +213,7 @@ export default class ChatEditor extends Component {
> >
<BaseTextArea <BaseTextArea
fontFamily={inCodeMode ? 'Source Code Pro' : 'Inter'} fontFamily={inCodeMode ? 'Source Code Pro' : 'Inter'}
fontSize="14px" fontSize="1"
lineHeight="tall" lineHeight="tall"
rows="1" rows="1"
style={{ width: '100%', background: 'transparent', color: 'currentColor' }} style={{ width: '100%', background: 'transparent', color: 'currentColor' }}

View File

@ -12,6 +12,7 @@ export default class CodeContent extends Component {
( (
<Text <Text
display='block' display='block'
fontSize='0'
mono mono
p='1' p='1'
my='0' my='0'
@ -37,6 +38,7 @@ export default class CodeContent extends Component {
overflow='auto' overflow='auto'
maxHeight='10em' maxHeight='10em'
maxWidth='100%' maxWidth='100%'
fontSize='0'
style={{ whiteSpace: 'pre' }} style={{ whiteSpace: 'pre' }}
> >
{content.code.expression} {content.code.expression}

View File

@ -2,6 +2,7 @@ import React, { Component } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import RemarkDisableTokenizers from 'remark-disable-tokenizers'; import RemarkDisableTokenizers from 'remark-disable-tokenizers';
import RemarkBreaks from 'remark-breaks';
import urbitOb from 'urbit-ob'; import urbitOb from 'urbit-ob';
import { Text } from '@tlon/indigo-react'; import { Text } from '@tlon/indigo-react';
@ -26,10 +27,10 @@ const DISABLED_INLINE_TOKENS = [
const renderers = { const renderers = {
inlineCode: ({language, value}) => { inlineCode: ({language, value}) => {
return <Text mono p='1' backgroundColor='washedGray' style={{ whiteSpace: 'preWrap'}}>{value}</Text> return <Text mono p='1' backgroundColor='washedGray' fontSize='0' style={{ whiteSpace: 'preWrap'}}>{value}</Text>
}, },
paragraph: ({ children }) => { paragraph: ({ children }) => {
return (<Text fontSize="14px">{children}</Text>); return (<Text fontSize="1">{children}</Text>);
}, },
code: ({language, value}) => { code: ({language, value}) => {
return <Text return <Text
@ -38,6 +39,7 @@ const renderers = {
display='block' display='block'
borderRadius='1' borderRadius='1'
mono mono
fontSize='0'
backgroundColor='washedGray' backgroundColor='washedGray'
overflowX='auto' overflowX='auto'
style={{ whiteSpace: 'pre'}}> style={{ whiteSpace: 'pre'}}>
@ -66,6 +68,7 @@ const MessageMarkdown = React.memo(props => (
return true; return true;
}} }}
plugins={[[ plugins={[[
RemarkBreaks,
RemarkDisableTokenizers, RemarkDisableTokenizers,
{ block: DISABLED_BLOCK_TOKENS, inline: DISABLED_INLINE_TOKENS } { block: DISABLED_BLOCK_TOKENS, inline: DISABLED_INLINE_TOKENS }
]]} /> ]]} />

View File

@ -277,9 +277,6 @@ pre.CodeMirror-placeholder.CodeMirror-line-like { color: var(--gray); }
/* dark */ /* dark */
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
body {
background-color: #333;
}
.bg-black-d { .bg-black-d {
background-color: black; background-color: black;
} }

View File

@ -78,7 +78,7 @@ export default function LaunchApp(props) {
color="black" color="black"
icon="Mail" icon="Mail"
/> />
<Text ml="1" mt='1px' color="black">DMs + Drafts</Text> <Text ml="2" mt='1px' color="black">DMs + Drafts</Text>
</Row> </Row>
</Box> </Box>
</Tile> </Tile>
@ -102,7 +102,7 @@ export default function LaunchApp(props) {
icon="CreateGroup" icon="CreateGroup"
bg="green" bg="green"
color="#fff" color="#fff"
text="Create a Group" text="Create Group"
> >
<NewGroup {...props} /> <NewGroup {...props} />
</ModalButton> </ModalButton>

View File

@ -20,8 +20,8 @@ export default class BasicTile extends React.PureComponent {
size='12px' size='12px'
display='inline-block' display='inline-block'
verticalAlign='top' verticalAlign='top'
pt='5px' mt='5px'
pr='2px' mr='2'
/> />
: null : null
}{props.title} }{props.title}

View File

@ -171,7 +171,7 @@ export default class WeatherTile extends React.Component {
onClick={() => this.setState({ manualEntry: !this.state.manualEntry })} onClick={() => this.setState({ manualEntry: !this.state.manualEntry })}
> >
<Box> <Box>
<Icon icon='Weather' display='inline-block' verticalAlign='top' pt='3px' pr='2px' /> <Icon icon='Weather' display='inline-block' verticalAlign='top' mt='3px' mr='2' />
<Text>Weather</Text> <Text>Weather</Text>
</Box> </Box>
<Text style={{ cursor: 'pointer' }}> <Text style={{ cursor: 'pointer' }}>
@ -217,15 +217,14 @@ export default class WeatherTile extends React.Component {
title={`${locationName} Weather`} title={`${locationName} Weather`}
> >
<Text> <Text>
<Icon icon='Weather' display='inline' style={{ position: 'relative', top: '.3em' }} /> <Icon icon='Weather' display='inline' mr='2' style={{ position: 'relative', top: '.3em' }} />
Weather
<Text <Text
cursor='pointer' cursor='pointer'
onClick={() => onClick={() =>
this.setState({ manualEntry: !this.state.manualEntry }) this.setState({ manualEntry: !this.state.manualEntry })
} }
> >
-> Weather ->
</Text> </Text>
</Text> </Text>
@ -268,7 +267,7 @@ export default class WeatherTile extends React.Component {
flexDirection="column" flexDirection="column"
justifyContent="flex-start" justifyContent="flex-start"
> >
<Text><Icon icon='Weather' color='black' display='inline' style={{ position: 'relative', top: '.3em' }} /> Weather</Text> <Text><Icon icon='Weather' color='black' display='inline' mr='2' style={{ position: 'relative', top: '.3em' }} /> Weather</Text>
<Text width='100%' display='flex' flexDirection='column' mt={1}> <Text width='100%' display='flex' flexDirection='column' mt={1}>
Loading, please check again later... Loading, please check again later...
</Text> </Text>

View File

@ -40,12 +40,12 @@ button {
/* stolen from indigo-react reset.css /* stolen from indigo-react reset.css
* TODO: remove and add reset.css properly * TODO: remove and add reset.css properly
*/ */
@keyframes loadingSpinnerRotation { @keyframes loadingSpinnerRotation {
from { from {
transform: rotate(0deg); transform: rotate(0deg);
} }
to { to {
transform: rotate(360deg); transform: rotate(360deg);
} }
@ -53,9 +53,6 @@ button {
/* dark */ /* dark */
@media all and (prefers-color-scheme: dark) { @media all and (prefers-color-scheme: dark) {
body {
background-color: #333;
}
.bg-gray0-d { .bg-gray0-d {
background-color: #333; background-color: #333;
} }

View File

@ -1,4 +1,4 @@
import React, { useEffect } from "react"; import React, { useEffect, useCallback } from "react";
import { Box, Row, Col, Center, LoadingSpinner, Text } from "@tlon/indigo-react"; import { Box, Row, Col, Center, LoadingSpinner, Text } from "@tlon/indigo-react";
import { Switch, Route, Link } from "react-router-dom"; import { Switch, Route, Link } from "react-router-dom";
import bigInt from 'big-integer'; import bigInt from 'big-integer';
@ -10,10 +10,14 @@ import { RouteComponentProps } from "react-router-dom";
import { LinkItem } from "./components/LinkItem"; import { LinkItem } from "./components/LinkItem";
import LinkSubmit from "./components/LinkSubmit"; import LinkSubmit from "./components/LinkSubmit";
import { LinkPreview } from "./components/link-preview";
import { LinkWindow } from "./LinkWindow";
import { Comments } from "~/views/components/Comments"; import { Comments } from "~/views/components/Comments";
import "./css/custom.css"; import "./css/custom.css";
const emptyMeasure = () => {};
type LinkResourceProps = StoreState & { type LinkResourceProps = StoreState & {
association: Association; association: Association;
api: GlobalApi; api: GlobalApi;
@ -57,39 +61,28 @@ export function LinkResource(props: LinkResourceProps) {
return <Center width='100%' height='100%'><LoadingSpinner/></Center>; return <Center width='100%' height='100%'><LoadingSpinner/></Center>;
} }
return ( return (
<Col alignItems="center" height="100%" width="100%" overflowY="auto"> <Col alignItems="center" height="100%" width="100%" overflowY="hidden">
<Switch> <Switch>
<Route <Route
exact exact
path={relativePath("")} path={relativePath("")}
render={(props) => { render={(props) => {
return ( return (
<Col width="100%" p={4} alignItems="center" maxWidth="768px"> <LinkWindow
<Col width="100%" flexShrink='0'> s3={s3}
<LinkSubmit s3={s3} name={name} ship={ship.slice(1)} api={api} /> association={resource}
</Col> contacts={contacts}
{Array.from(graph).map(([date, node]) => { resource={resourcePath}
const contact = contactDetails[node.post.author]; graph={graph}
return ( unreads={unreads}
<LinkItem baseUrl={resourceUrl}
association={resource} group={group}
contacts={contacts} path={resource["group-path"]}
key={date.toString()} api={api}
resource={resourcePath} mb={3}
node={node} />
contacts={contactDetails}
unreads={unreads}
nickname={contact?.nickname}
baseUrl={resourceUrl}
group={group}
path={resource["group-path"]}
api={api}
mb={3}
/>
);
})}
</Col>
); );
}} }}
/> />
@ -112,6 +105,7 @@ export function LinkResource(props: LinkResourceProps) {
const contact = contactDetails[node.post.author]; const contact = contactDetails[node.post.author];
return ( return (
<Col alignItems="center" overflowY="auto" width="100%">
<Col width="100%" p={3} maxWidth="768px"> <Col width="100%" p={3} maxWidth="768px">
<Link to={resourceUrl}><Text bold>{"<- Back"}</Text></Link> <Link to={resourceUrl}><Text bold>{"<- Back"}</Text></Link>
<LinkItem <LinkItem
@ -125,6 +119,7 @@ export function LinkResource(props: LinkResourceProps) {
path={resource["group-path"]} path={resource["group-path"]}
api={api} api={api}
mt={3} mt={3}
measure={emptyMeasure}
/> />
<Comments <Comments
ship={ship} ship={ship}
@ -141,6 +136,7 @@ export function LinkResource(props: LinkResourceProps) {
group={group} group={group}
/> />
</Col> </Col>
</Col>
); );
}} }}
/> />

View File

@ -0,0 +1,97 @@
import React, { useRef, useCallback, useEffect, useMemo } from "react";
import { Col } from "@tlon/indigo-react";
import bigInt from 'big-integer';
import {
Association,
Graph,
Contacts,
Unreads,
LocalUpdateRemoteContentPolicy,
Group,
Rolodex,
S3State,
} from "~/types";
import GlobalApi from "~/logic/api/global";
import VirtualScroller from "~/views/components/VirtualScroller";
import { LinkItem } from "./components/LinkItem";
import LinkSubmit from "./components/LinkSubmit";
interface LinkWindowProps {
association: Association;
contacts: Rolodex;
resource: string;
graph: Graph;
unreads: Unreads;
hideNicknames: boolean;
hideAvatars: boolean;
baseUrl: string;
group: Group;
path: string;
api: GlobalApi;
s3: S3State;
}
export function LinkWindow(props: LinkWindowProps) {
const { graph, api, association } = props;
const loadedNewest = useRef(true);
const loadedOldest = useRef(false);
const virtualList = useRef<VirtualScroller>();
const fetchLinks = useCallback(
async (newer: boolean) => {
/* stubbed, should we generalize the display of graphs in virtualscroller? */
}, []
);
useEffect(() => {
const list = virtualList?.current;
if(!list) return;
list.calculateVisibleItems();
}, [graph.size]);
const first = graph.peekLargest()?.[0];
const [,,ship, name] = association['app-path'].split('/');
const style = useMemo(() =>
({
height: "100%",
width: "100%",
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}), []);
return (
<VirtualScroller
ref={(l) => (virtualList.current = l ?? undefined)}
origin="top"
style={style}
onStartReached={() => {}}
onScroll={() => {}}
data={graph}
size={graph.size}
renderer={({ index, measure, scrollWindow }) => {
const node = graph.get(index);
const post = node?.post;
if (!node || !post) return null;
const linkProps = {
...props,
node,
measure,
key: index.toString()
};
if(index.eq(first ?? bigInt.zero)) {
return (
<>
<Col key={index.toString()} mx="auto" mt="4" maxWidth="768px" width="100%" flexShrink='0' px={3}>
<LinkSubmit s3={props.s3} name={name} ship={ship.slice(1)} api={api} />
</Col>
<LinkItem {...linkProps} />
</>
)
}
return <LinkItem {...linkProps} />;
}}
loadRows={fetchLinks}
/>
);
}

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React, { useState, useEffect, useRef, useCallback } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Row, Col, Anchor, Box, Text, Icon, Action } from '@tlon/indigo-react'; import { Row, Col, Anchor, Box, Text, Icon, Action } from '@tlon/indigo-react';
@ -17,8 +17,9 @@ interface LinkItemProps {
api: GlobalApi; api: GlobalApi;
group: Group; group: Group;
path: string; path: string;
contacts: Rolodex[]; contacts: Rolodex;
unreads: Unreads; unreads: Unreads;
measure: (el: any) => void;
} }
export const LinkItem = (props: LinkItemProps) => { export const LinkItem = (props: LinkItemProps) => {
@ -29,9 +30,12 @@ export const LinkItem = (props: LinkItemProps) => {
group, group,
path, path,
contacts, contacts,
measure,
...rest ...rest
} = props; } = props;
const ref = useRef<HTMLDivElement | null>(null);
const URLparser = new RegExp( const URLparser = new RegExp(
/((?:([\w\d\.-]+)\:\/\/?){1}(?:(www)\.?){0,1}(((?:[\w\d-]+\.)*)([\w\d-]+\.[\w\d]+))){1}(?:\:(\d+)){0,1}((\/(?:(?:[^\/\s\?]+\/)*))(?:([^\?\/\s#]+?(?:.[^\?\s]+){0,1}){0,1}(?:\?([^\s#]+)){0,1})){0,1}(?:#([^#\s]+)){0,1}/ /((?:([\w\d\.-]+)\:\/\/?){1}(?:(www)\.?){0,1}(((?:[\w\d-]+\.)*)([\w\d-]+\.[\w\d]+))){1}(?:\:(\d+)){0,1}((\/(?:(?:[^\/\s\?]+\/)*))(?:([^\?\/\s#]+?(?:.[^\?\s]+){0,1}){0,1}(?:\?([^\s#]+)){0,1})){0,1}(?:#([^#\s]+)){0,1}/
); );
@ -70,9 +74,18 @@ export const LinkItem = (props: LinkItemProps) => {
const markRead = () => { const markRead = () => {
api.hark.markEachAsRead(props.association, '/', `/${index}`, 'link', 'link'); api.hark.markEachAsRead(props.association, '/', `/${index}`, 'link', 'link');
} }
const onMeasure = useCallback(() => {
ref.current && measure(ref.current);
}, [ref.current, measure])
useEffect(() => {
onMeasure();
}, [onMeasure]);
return ( return (
<Box width="100%" {...rest}> <Box mx="auto" px={3} maxWidth="768px" ref={ref} width="100%" {...rest}>
<Box <Box
lineHeight="tall" lineHeight="tall"
display='flex' display='flex'
@ -90,6 +103,7 @@ export const LinkItem = (props: LinkItemProps) => {
url={contents[1].url} url={contents[1].url}
text={contents[0].text} text={contents[0].text}
unfold={true} unfold={true}
onLoad={onMeasure}
style={{ alignSelf: 'center' }} style={{ alignSelf: 'center' }}
oembedProps={{ oembedProps={{
p: 2, p: 2,
@ -116,9 +130,9 @@ export const LinkItem = (props: LinkItemProps) => {
</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
contacts={contacts} contacts={contacts}
@ -136,9 +150,9 @@ export const LinkItem = (props: LinkItemProps) => {
</Box> </Box>
</Link> </Link>
</Box> </Box>
<Dropdown <Dropdown
width="200px" dropWidth="200px"
alignX="right" alignX="right"
alignY="top" alignY="top"
options={ options={
@ -156,7 +170,7 @@ export const LinkItem = (props: LinkItemProps) => {
> >
<Icon ml="2" display="block" icon="Ellipsis" color="gray" /> <Icon ml="2" display="block" icon="Ellipsis" color="gray" />
</Dropdown> </Dropdown>
</Row> </Row>
</Box>); </Box>);
}; };

View File

@ -132,7 +132,6 @@ const LinkSubmit = (props: LinkSubmitProps) => {
position="absolute" position="absolute"
px={2} px={2}
pt={2} pt={2}
fontSize={0}
style={{ pointerEvents: 'none' }} style={{ pointerEvents: 'none' }}
>{canUpload >{canUpload
? <> ? <>
@ -180,7 +179,6 @@ const LinkSubmit = (props: LinkSubmitProps) => {
type="url" type="url"
pl={2} pl={2}
width="100%" width="100%"
fontSize={0}
py={2} py={2}
color="black" color="black"
backgroundColor="transparent" backgroundColor="transparent"
@ -198,8 +196,8 @@ const LinkSubmit = (props: LinkSubmitProps) => {
pl={2} pl={2}
backgroundColor="transparent" backgroundColor="transparent"
width="100%" width="100%"
fontSize={0}
color="black" color="black"
fontSize={1}
style={{ style={{
resize: 'none', resize: 'none',
height: 40 height: 40

View File

@ -1,105 +0,0 @@
import React, { useCallback } from "react";
import _ from "lodash";
import { Link } from "react-router-dom";
import GlobalApi from "~/logic/api/global";
import {
Rolodex,
Associations,
ChatNotifIndex,
ChatNotificationContents,
Groups,
} from "~/types";
import { BigInteger } from "big-integer";
import { Box, Col } from "@tlon/indigo-react";
import { Header } from "./header";
import { pluralize } from "~/logic/lib/util";
import ChatMessage from "../chat/components/ChatMessage";
function describeNotification(mention: boolean, lent: number) {
const msg = pluralize("message", lent !== 1);
if (mention) {
return `mentioned you in ${msg} in`;
}
return `sent ${msg} in`;
}
export function ChatNotification(props: {
index: ChatNotifIndex;
contents: ChatNotificationContents;
archived: boolean;
read: boolean;
time: number;
timebox: BigInteger;
associations: Associations;
contacts: Rolodex;
groups: Groups;
api: GlobalApi;
}) {
const { index, contents, read, time, api, timebox } = props;
const authors = _.map(contents, "author");
const { chat, mention } = index;
const association = props?.associations?.chat?.[chat];
const groupPath = association?.["group-path"];
const appPath = index?.chat;
const group = props?.groups?.[groupPath];
const desc = describeNotification(mention, contents.length);
const groupContacts = props.contacts[groupPath] || {};
const onClick = useCallback(() => {
if (props.archived) {
return;
}
const func = read ? "unread" : "read";
return api.hark[func](timebox, { chat: index });
}, [api, timebox, index, read]);
return (
<Col onClick={onClick} flexGrow="1" p="2">
<Header
chat
associations={props.associations}
read={read}
archived={props.archived}
time={time}
authors={authors}
moduleIcon="Chat"
channel={chat}
contacts={props.contacts}
group={groupPath}
description={desc}
/>
<Col pb="3" pl="5">
{_.map(_.take(contents, 5), (content, idx) => {
let workspace = groupPath;
if (workspace === undefined || group?.hidden) {
workspace = '/home';
}
const to = `/~landscape${workspace}/resource/chat${appPath}?msg=${content.number}`;
return (
<Link key={idx} to={to}>
<ChatMessage
measure={() => {}}
msg={content}
isLastRead={false}
group={group}
contacts={groupContacts}
fontSize='0'
pt='2'
/>
</Link>
);
})}
{contents.length > 5 && (
<Box ml="4" mt="3" mb="2" color="gray" fontSize="14px">
and {contents.length - 5} other message
{contents.length > 6 ? "s" : ""}
</Box>
)}
</Col>
</Col>
);
}

View File

@ -90,6 +90,8 @@ const GraphNodeContent = ({ group, post, contacts, mod, description, index, remo
content={contents} content={contents}
group={group} group={group}
contacts={contacts} contacts={contacts}
fontSize='14px'
lineHeight="tall"
/> />
} else if (idx[1] === "1") { } else if (idx[1] === "1") {
const [{ text: header }, { text: body }] = contents; const [{ text: header }, { text: body }] = contents;
@ -164,13 +166,14 @@ const GraphNode = ({
group, group,
read, read,
onRead, onRead,
showContact = false,
remoteContentPolicy remoteContentPolicy
}) => { }) => {
const { contents } = post; const { contents } = post;
author = deSig(author); author = deSig(author);
const history = useHistory(); const history = useHistory();
const img = ( const img = showContact ? (
<Sigil <Sigil
ship={`~${author}`} ship={`~${author}`}
size={16} size={16}
@ -178,7 +181,7 @@ const GraphNode = ({
color={`#000000`} color={`#000000`}
classes="mix-blend-diff" classes="mix-blend-diff"
/> />
); ) : <Box style={{ width: '16px' }}></Box>;
const groupContacts = contacts[groupPath] ?? {}; const groupContacts = contacts[groupPath] ?? {};
@ -192,10 +195,10 @@ const GraphNode = ({
}, [read, onRead]); }, [read, onRead]);
return ( return (
<Row onClick={onClick} gapX="2" pt="2"> <Row onClick={onClick} gapX="2" pt={showContact ? 2 : 0}>
<Col>{img}</Col> <Col>{img}</Col>
<Col flexGrow={1} alignItems="flex-start"> <Col flexGrow={1} alignItems="flex-start">
<Row {showContact && <Row
mb="2" mb="2"
height="16px" height="16px"
alignItems="center" alignItems="center"
@ -208,8 +211,8 @@ const GraphNode = ({
<Text ml="2" gray> <Text ml="2" gray>
{moment(time).format("HH:mm")} {moment(time).format("HH:mm")}
</Text> </Text>
</Row> </Row>}
<Row width="100%" p="1"> <Row width="100%" p="1" flexDirection="column">
<GraphNodeContent <GraphNodeContent
contacts={groupContacts} contacts={groupContacts}
post={post} post={post}
@ -244,16 +247,15 @@ export function GraphNotification(props: {
const desc = describeNotification(index.description, contents.length !== 1); const desc = describeNotification(index.description, contents.length !== 1);
const onClick = useCallback(() => { const onClick = useCallback(() => {
if (props.archived) { if (props.archived || read) {
return; return;
} }
const func = read ? "unread" : "read"; return api.hark["read"](timebox, { graph: index });
return api.hark[func](timebox, { graph: index });
}, [api, timebox, index, read]); }, [api, timebox, index, read]);
return ( return (
<Col flexGrow={1} width="100%" p="2"> <>
<Header <Header
onClick={onClick} onClick={onClick}
archived={props.archived} archived={props.archived}
@ -267,7 +269,7 @@ return (
description={desc} description={desc}
associations={props.associations} associations={props.associations}
/> />
<Col flexGrow={1} width="100%" pl="5"> <Box flexGrow={1} width="100%" pl={5} gridArea="main">
{_.map(contents, (content, idx) => ( {_.map(contents, (content, idx) => (
<GraphNode <GraphNode
post={content} post={content}
@ -282,9 +284,10 @@ return (
groupPath={group} groupPath={group}
read={read} read={read}
onRead={onClick} onRead={onClick}
showContact={idx === 0}
/> />
))} ))}
</Col> </Box>
</Col> </>
); );
} }

View File

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { Text as NormalText, Row, Icon, Rule } from "@tlon/indigo-react"; import { Text as NormalText, Row, Icon, Rule, Box } from "@tlon/indigo-react";
import f from "lodash/fp"; import f from "lodash/fp";
import _ from "lodash"; import _ from "lodash";
import moment from "moment"; import moment from "moment";
@ -71,12 +71,13 @@ export function Header(props: {
channel; channel;
return ( return (
<Row onClick={props.onClick} p="2" flexWrap="wrap" gapX="1" alignItems="center"> <Row onClick={props.onClick} p="2" flexWrap="wrap" alignItems="center" gridArea="header">
{!props.archived && ( {!props.archived && (
<Icon <Icon
display="block" display="block"
mr="1" opacity={read ? 0 : 1}
icon={read ? "Circle" : "Bullet"} mr={2}
icon="Bullet"
color="blue" color="blue"
/> />
)} )}
@ -84,13 +85,13 @@ export function Header(props: {
{authorDesc} {authorDesc}
</Text> </Text>
<Text mr="1">{description}</Text> <Text mr="1">{description}</Text>
{!!moduleIcon && <Icon icon={moduleIcon as any} />} {!!moduleIcon && <Icon icon={moduleIcon as any} mr={1} />}
{!!channel && <Text fontWeight="500">{channelTitle}</Text>} {!!channel && <Text fontWeight="500" mr={1}>{channelTitle}</Text>}
<Rule vertical height="12px" /> <Rule vertical height="12px" mr={1} />
{groupTitle && {groupTitle &&
<> <>
<Text fontWeight="500">{groupTitle}</Text> <Text fontWeight="500" mr={1}>{groupTitle}</Text>
<Rule vertical height="12px"/> <Rule vertical height="12px" mr={1} />
</> </>
} }
<Text fontWeight="regular" color="lightGray"> <Text fontWeight="regular" color="lightGray">

View File

@ -61,20 +61,36 @@ export default function Inbox(props: {
}; };
}, []); }, []);
const [newNotifications, ...notifications] = const notifications =
Array.from(props.showArchive ? props.archive : props.notifications) || []; Array.from(props.showArchive ? props.archive : props.notifications) || [];
const calendar = {
...MOMENT_CALENDAR_DATE, sameDay: function (now) {
if (this.subtract(6, 'hours').isBefore(now)) {
return "[Earlier Today]";
} else {
return MOMENT_CALENDAR_DATE.sameDay;
}
}
};
const notificationsByDay = f.flow( let notificationsByDay = f.flow(
f.map<DatedTimebox>(([date, nots]) => [ f.map<DatedTimebox>(([date, nots]) => [
date, date,
nots.filter(filterNotification(associations, props.filter)), nots.filter(filterNotification(associations, props.filter)),
]), ]),
f.groupBy<DatedTimebox>(([date]) => f.groupBy<DatedTimebox>(([date]) => {
moment(daToUnix(date)).format("DDMMYYYY") date = moment(daToUnix(date));
), if (moment().subtract(6, 'hours').isBefore(date)) {
f.values, return 'latest';
f.reverse } else {
return date.format("YYYYMMDD");
}
}),
)(notifications); )(notifications);
notificationsByDay = new Map(Object.keys(notificationsByDay).sort().reverse().map(timebox => {
return [timebox, notificationsByDay[timebox]];
}));
useEffect(() => { useEffect(() => {
api.hark.getMore(props.showArchive); api.hark.getMore(props.showArchive);
@ -130,41 +146,27 @@ export default function Inbox(props: {
return ( return (
<Col position="relative" height="100%" overflowY="auto" onScroll={onScroll} > <Col position="relative" height="100%" overflowY="auto" onScroll={onScroll} >
<Col zIndex={4} gapY={2} bg="white" top="0px" position="sticky"> <Col zIndex={4} gapY={2} bg="white" top="0px" position="sticky" flexShrink={0}>
{inviteItems(invites, api)} {inviteItems(invites, api)}
</Col> </Col>
{newNotifications && ( {[...notificationsByDay.keys()].map((day, index) => {
<DaySection const timeboxes = notificationsByDay.get(day);
latest return timeboxes.length > 0 && (
timeboxes={[newNotifications]} <DaySection
contacts={props.contacts} key={day}
archive={!!props.showArchive} label={day === 'latest' ? 'Today' : moment(day).calendar(null, calendar)}
associations={props.associations} timeboxes={timeboxes}
groups={props.groups} contacts={props.contacts}
graphConfig={props.notificationsGraphConfig} archive={!!props.showArchive}
groupConfig={props.notificationsGroupConfig} associations={props.associations}
chatConfig={props.notificationsChatConfig} api={api}
api={api} groups={props.groups}
/> graphConfig={props.notificationsGraphConfig}
)} groupConfig={props.notificationsGroupConfig}
{_.map( chatConfig={props.notificationsChatConfig}
notificationsByDay, />
(timeboxes, idx) => );
timeboxes.length > 0 && ( })}
<DaySection
key={idx}
timeboxes={timeboxes}
contacts={props.contacts}
archive={!!props.showArchive}
associations={props.associations}
api={api}
groups={props.groups}
graphConfig={props.notificationsGraphConfig}
groupConfig={props.notificationsGroupConfig}
chatConfig={props.notificationsChatConfig}
/>
)
)}
</Col> </Col>
); );
} }
@ -181,21 +183,18 @@ function sortIndexedNotification(
} }
function DaySection({ function DaySection({
label,
contacts, contacts,
groups, groups,
archive, archive,
timeboxes, timeboxes,
latest = false,
associations, associations,
api, api,
groupConfig, groupConfig,
graphConfig, graphConfig,
chatConfig, chatConfig,
remoteContentPolicy
}) { }) {
const calendar = latest
? MOMENT_CALENDAR_DATE
: { ...MOMENT_CALENDAR_DATE, sameDay: "[Earlier Today]" };
const lent = timeboxes.map(([,nots]) => nots.length).reduce(f.add, 0); const lent = timeboxes.map(([,nots]) => nots.length).reduce(f.add, 0);
if (lent === 0 || timeboxes.length === 0) { if (lent === 0 || timeboxes.length === 0) {
return null; return null;
@ -206,7 +205,7 @@ function DaySection({
<Box position="sticky" zIndex="3" top="-1px" bg="white"> <Box position="sticky" zIndex="3" top="-1px" bg="white">
<Box p="2" bg="scales.black05"> <Box p="2" bg="scales.black05">
<Text> <Text>
{moment(daToUnix(timeboxes[0][0])).calendar(null, calendar)} {label}
</Text> </Text>
</Box> </Box>
</Box> </Box>

View File

@ -1,5 +1,5 @@
import React, { ReactNode, useCallback, useMemo } from "react"; import React, { ReactNode, useCallback, useMemo, useState } from "react";
import { Row, Box, Col, Text, Anchor, Icon, Action } from "@tlon/indigo-react"; import { Row, Box } from "@tlon/indigo-react";
import _ from "lodash"; import _ from "lodash";
import { import {
GraphNotificationContents, GraphNotificationContents,
@ -7,7 +7,6 @@ import {
GroupNotificationContents, GroupNotificationContents,
NotificationGraphConfig, NotificationGraphConfig,
GroupNotificationsConfig, GroupNotificationsConfig,
NotifIndex,
Groups, Groups,
Associations, Associations,
Contacts, Contacts,
@ -17,8 +16,8 @@ import { getParentIndex } from "~/logic/lib/notification";
import { StatelessAsyncAction } from "~/views/components/StatelessAsyncAction"; import { StatelessAsyncAction } from "~/views/components/StatelessAsyncAction";
import { GroupNotification } from "./group"; import { GroupNotification } from "./group";
import { GraphNotification } from "./graph"; import { GraphNotification } from "./graph";
import { ChatNotification } from "./chat";
import { BigInteger } from "big-integer"; import { BigInteger } from "big-integer";
import { useHovering } from "~/logic/lib/util";
interface NotificationProps { interface NotificationProps {
notification: IndexedNotification; notification: IndexedNotification;
@ -55,9 +54,6 @@ function getMuted(
if ("group" in index) { if ("group" in index) {
return _.findIndex(groups || [], (g) => g === index.group.group) === -1; return _.findIndex(groups || [], (g) => g === index.group.group) === -1;
} }
if ("chat" in index) {
return _.findIndex(chat || [], (c) => c === index.chat.chat) === -1;
}
return false; return false;
} }
@ -89,11 +85,21 @@ function NotificationWrapper(props: {
return api.hark[func](notif); return api.hark[func](notif);
}, [notif, api, isMuted]); }, [notif, api, isMuted]);
const { hovering, bind } = useHovering();
const changeMuteDesc = isMuted ? "Unmute" : "Mute"; const changeMuteDesc = isMuted ? "Unmute" : "Mute";
return ( return (
<Row width="100%" flexShrink={0} alignItems="top" justifyContent="space-between"> <Box
width="100%"
display="grid"
gridTemplateColumns="1fr 200px"
gridTemplateRows="auto"
gridTemplateAreas="'header actions' 'main main'"
pb={2}
{...bind}
>
{children} {children}
<Row gapX="2" p="2" pt='3' alignItems="top"> <Row gapX="2" p="2" pt='3' gridArea="actions" justifyContent="flex-end" opacity={[1, hovering ? 1 : 0]}>
<StatelessAsyncAction name={changeMuteDesc} onClick={onChangeMute} backgroundColor="transparent"> <StatelessAsyncAction name={changeMuteDesc} onClick={onChangeMute} backgroundColor="transparent">
{changeMuteDesc} {changeMuteDesc}
</StatelessAsyncAction> </StatelessAsyncAction>
@ -103,7 +109,7 @@ function NotificationWrapper(props: {
</StatelessAsyncAction> </StatelessAsyncAction>
)} )}
</Row> </Row>
</Row> </Box>
); );
} }
@ -166,26 +172,6 @@ export function Notification(props: NotificationProps) {
</Wrapper> </Wrapper>
); );
} }
if ("chat" in notification.index) {
const index = notification.index.chat;
const c: ChatNotificationContents = (contents as any).chat;
return (
<Wrapper>
<ChatNotification
api={props.api}
index={index}
contents={c}
contacts={props.contacts}
read={read}
archived={archived}
groups={props.groups}
timebox={props.time}
time={time}
associations={associations}
/>
</Wrapper>
);
}
return null; return null;
} }

View File

@ -33,7 +33,7 @@ const SidebarItem = ({ children, view, current }) => {
backgroundColor={selected ? "washedGray" : "white"} backgroundColor={selected ? "washedGray" : "white"}
> >
<Icon mr={2} display="inline-block" icon={icon(view)} color='black' /> <Icon mr={2} display="inline-block" icon={icon(view)} color='black' />
<Text color='black' fontSize={0}> <Text color='black'>
{children} {children}
</Text> </Text>
</Row> </Row>
@ -76,7 +76,7 @@ export default function ProfileScreen(props: any) {
height="100%" height="100%"
width="100%" width="100%"
display="grid" display="grid"
gridTemplateColumns={["100%", "200px 1fr"]} gridTemplateColumns={["100%", "250px 1fr"]}
gridTemplateRows={["48px 1fr", "1fr"]} gridTemplateRows={["48px 1fr", "1fr"]}
borderRadius={1} borderRadius={1}
bg="white" bg="white"
@ -95,8 +95,8 @@ export default function ProfileScreen(props: any) {
bg={sigilColor} bg={sigilColor}
borderRadius={8} borderRadius={8}
my={4} my={4}
height={128} height={160}
width={128} width={160}
display="flex" display="flex"
justifyContent="center" justifyContent="center"
alignItems="center" alignItems="center"

View File

@ -10,6 +10,7 @@ import CodeMirror from "codemirror";
import "codemirror/mode/markdown/markdown"; import "codemirror/mode/markdown/markdown";
import "codemirror/addon/display/placeholder"; import "codemirror/addon/display/placeholder";
import "codemirror/addon/edit/continuelist";
import "codemirror/lib/codemirror.css"; import "codemirror/lib/codemirror.css";
import { Box } from "@tlon/indigo-react"; import { Box } from "@tlon/indigo-react";
@ -54,6 +55,7 @@ export function MarkdownEditor(
scrollbarStyle: "native", scrollbarStyle: "native",
// cursorHeight: 0.85, // cursorHeight: 0.85,
placeholder: placeholder || "", placeholder: placeholder || "",
extraKeys: { 'Enter': 'newlineAndIndentContinueMarkdownList' }
}; };
const editor: React.RefObject<any> = useRef(); const editor: React.RefObject<any> = useRef();

View File

@ -67,7 +67,7 @@ export function Note(props: NoteProps & RouteComponentProps) {
color="red" color="red"
ml={2} ml={2}
onClick={deletePost} onClick={deletePost}
css={{ cursor: "pointer" }} style={{ cursor: "pointer" }}
> >
Delete Delete
</Text> </Text>

View File

@ -61,9 +61,9 @@ export function NotePreview(props: NotePreviewProps) {
overflow='hidden' overflow='hidden'
p='2' p='2'
> >
<WrappedBox mb={2}><Text bold fontSize='0'>{title}</Text></WrappedBox> <WrappedBox mb={2}><Text bold>{title}</Text></WrappedBox>
<WrappedBox> <WrappedBox>
<Text fontSize='14px'> <Text fontSize='14px' lineHeight='tall'>
<ReactMarkdown <ReactMarkdown
unwrapDisallowed unwrapDisallowed
allowedTypes={['text', 'root', 'break', 'paragraph', 'image']} allowedTypes={['text', 'root', 'break', 'paragraph', 'image']}

View File

@ -1,10 +1,9 @@
import React from "react"; import React from "react";
import { Link, RouteComponentProps } from "react-router-dom"; import { RouteComponentProps } from "react-router-dom";
import { NotebookPosts } from "./NotebookPosts"; import { NotebookPosts } from "./NotebookPosts";
import { Box, Button, Text, Row, Col } from "@tlon/indigo-react"; import { Col } from "@tlon/indigo-react";
import GlobalApi from "~/logic/api/global"; import GlobalApi from "~/logic/api/global";
import { Contacts, Rolodex, Groups, Associations, Graph, Association, Unreads } from "~/types"; import { Contacts, Rolodex, Groups, Associations, Graph, Association, Unreads } from "~/types";
import { useShowNickname } from "~/logic/lib/util";
interface NotebookProps { interface NotebookProps {
api: GlobalApi; api: GlobalApi;
@ -30,44 +29,14 @@ export function Notebook(props: NotebookProps & RouteComponentProps) {
association, association,
graph graph
} = props; } = props;
const { metadata } = association;
const group = groups[association?.['group-path']]; const group = groups[association?.['group-path']];
if (!group) { if (!group) {
return null; // Waiting on groups to populate return null; // Waiting on groups to populate
} }
const relativePath = (p: string) => props.baseUrl + p;
const contact = notebookContacts?.[ship];
const isOwn = `~${window.ship}` === ship;
let isWriter = true;
if (group.tags?.publish?.[`writers-${book}`]) {
isWriter = isOwn || group.tags?.publish?.[`writers-${book}`]?.has(window.ship);
}
const showNickname = useShowNickname(contact);
return ( return (
<Col gapY="4" pt={4} mx="auto" px={3} maxWidth="768px"> <Col gapY="4" pt={4} mx="auto" px={3} maxWidth="768px">
<Row justifyContent="space-between">
<Box>
<Text display='block'>{metadata?.title}</Text>
<Text color="lightGray">by </Text>
<Text fontFamily={showNickname ? 'sans' : 'mono'}>
{showNickname ? contact?.nickname : ship}
</Text>
</Box>
{isWriter && (
<Link to={relativePath('/new')}>
<Button primary style={{ cursor: 'pointer' }}>
New Post
</Button>
</Link>
)}
</Row>
<Box borderBottom="1" borderBottomColor="washedGray" />
<NotebookPosts <NotebookPosts
graph={graph} graph={graph}
host={ship} host={ship}

View File

@ -206,9 +206,6 @@
} }
@media all and (prefers-color-scheme: dark) { @media all and (prefers-color-scheme: dark) {
body {
background-color: #333;
}
.bg-black-d { .bg-black-d {
background-color: black; background-color: black;
} }
@ -267,7 +264,7 @@
background-color: #333; background-color: #333;
} }
.publish .cm-s-tlon.CodeMirror { .publish .cm-s-tlon.CodeMirror {
background: #333; background: unset;
color: #fff; color: #fff;
} }

View File

@ -29,10 +29,15 @@ export function AsyncButton({
}, [status]); }, [status]);
return ( return (
<Button disabled={!isValid} type="submit" {...rest}> <Button
hideDisabled={isSubmitting}
disabled={!isValid || isSubmitting}
type="submit"
{...rest}
>
{isSubmitting ? ( {isSubmitting ? (
<LoadingSpinner <LoadingSpinner
foreground={rest.primary ? "white" : 'black'} foreground={rest.primary ? "white" : "black"}
background="gray" background="gray"
/> />
) : success === true ? ( ) : success === true ? (

View File

@ -52,13 +52,14 @@ export default function Author(props: AuthorProps) {
<Box <Box
ml={showImage ? 2 : 0} ml={showImage ? 2 : 0}
color="black" color="black"
fontSize='1'
lineHeight='tall' lineHeight='tall'
fontFamily={showNickname ? "sans" : "mono"} fontFamily={showNickname ? "sans" : "mono"}
fontWeight={showNickname ? '500' : '400'} fontWeight={showNickname ? '500' : '400'}
> >
{name} {name}
</Box> </Box>
<Box ml={2} color={props.unread ? "blue" : "gray"}> <Box fontSize='1' ml={2} color={props.unread ? "blue" : "gray"}>
{dateFmt} {dateFmt}
</Box> </Box>
{props.children} {props.children}

View File

@ -21,6 +21,7 @@ interface DropdownProps {
alignY: AlignY | AlignY[]; alignY: AlignY | AlignY[];
alignX: AlignX | AlignX[]; alignX: AlignX | AlignX[];
width?: string; width?: string;
dropWidth?: string;
} }
const ClickBox = styled(Box)` const ClickBox = styled(Box)`
@ -111,14 +112,14 @@ export function Dropdown(props: DropdownProps) {
}); });
return ( return (
<Box flexShrink={1} position={open ? "relative" : "static"} minWidth='0'> <Box flexShrink={1} position={open ? "relative" : "static"} minWidth='0' width={props?.width ? props.width : 'auto'}>
<ClickBox width='100%' ref={anchorRef} onClick={onOpen}> <ClickBox width='100%' ref={anchorRef} onClick={onOpen}>
{children} {children}
</ClickBox> </ClickBox>
{open && ( {open && (
<Portal> <Portal>
<DropdownOptions <DropdownOptions
width={props.width || "max-content"} width={props?.dropWidth || "max-content"}
{...coords} {...coords}
ref={dropdownRef} ref={dropdownRef}
> >

View File

@ -19,10 +19,10 @@ interface MentionTextProps {
group: Group; group: Group;
} }
export function MentionText(props: MentionTextProps) { export function MentionText(props: MentionTextProps) {
const { content, contacts, contact, group } = props; const { content, contacts, contact, group, ...rest } = props;
return ( return (
<RichText contacts={contacts} contact={contact} group={group}> <RichText contacts={contacts} contact={contact} group={group} {...rest}>
{content.reduce((accum, c) => { {content.reduce((accum, c) => {
if ("text" in c) { if ("text" in c) {
return accum + c.text; return accum + c.text;
@ -44,7 +44,7 @@ export function Mention(props: {
const { contacts, ship } = props; const { contacts, ship } = props;
let { contact } = props; let { contact } = props;
contact = (contact?.nickname) ? contact : contacts?.[ship]; contact = (contact?.color) ? contact : contacts?.[ship];
const showNickname = useShowNickname(contact); const showNickname = useShowNickname(contact);

View File

@ -129,7 +129,7 @@ class ProfileOverlay extends PureComponent<ProfileOverlayProps, {}> {
)} )}
<Text mono gray>{cite(`~${ship}`)}</Text> <Text mono gray>{cite(`~${ship}`)}</Text>
{!isOwn && ( {!isOwn && (
<Button mt={2} width="100%" style={{ cursor: 'pointer' }} onClick={() => history.push(`/~landscape/dm/${ship}`)}> <Button mt={2} fontSize='0' width="100%" style={{ cursor: 'pointer' }} onClick={() => history.push(`/~landscape/dm/${ship}`)}>
Send Message Send Message
</Button> </Button>
)} )}

View File

@ -27,19 +27,17 @@ const RichText = React.memo(({ disableRemoteContent, ...props }) => (
{...props} {...props}
renderers={{ renderers={{
link: (linkProps) => { link: (linkProps) => {
if (disableRemoteContent) { const remoteContentPolicy = disableRemoteContent ? {
linkProps.remoteContentPolicy = { imageShown: false,
imageShown: false, audioShown: false,
audioShown: false, videoShown: false,
videoShown: false, oembedShown: false
oembedShown: false } : null;
};
}
if (hasProvider(linkProps.href)) { if (hasProvider(linkProps.href)) {
return <RemoteContent className="mw-100" url={linkProps.href} />; return <RemoteContent className="mw-100" url={linkProps.href} />;
} }
return <BaseAnchor target='_blank' rel='noreferrer noopener' borderBottom='1px solid' {...linkProps}>{linkProps.children}</BaseAnchor>; return <BaseAnchor target='_blank' rel='noreferrer noopener' borderBottom='1px solid' remoteContentPolicy={remoteContentPolicy} {...linkProps}>{linkProps.children}</BaseAnchor>;
}, },
linkReference: (linkProps) => { linkReference: (linkProps) => {
const linkText = String(linkProps.children[0].props.children); const linkText = String(linkProps.children[0].props.children);

View File

@ -8,6 +8,7 @@ import { useFormikContext } from "formik";
interface AsyncActionProps { interface AsyncActionProps {
children: ReactNode; children: ReactNode;
name: string; name: string;
disabled?: boolean;
onClick: (e: React.MouseEvent) => Promise<void>; onClick: (e: React.MouseEvent) => Promise<void>;
} }
@ -15,6 +16,7 @@ export function StatelessAsyncAction({
children, children,
onClick, onClick,
name = '', name = '',
disabled = false,
...rest ...rest
}: AsyncActionProps & Parameters<typeof Action>[0]) { }: AsyncActionProps & Parameters<typeof Action>[0]) {
const { const {
@ -23,7 +25,10 @@ export function StatelessAsyncAction({
} = useStatelessAsyncClickable(onClick, name); } = useStatelessAsyncClickable(onClick, name);
return ( return (
<Action onClick={handleClick} {...rest}> <Action
hideDisabled={!disabled}
disabled={disabled || state === 'loading'}
onClick={handleClick} {...rest}>
{state === "error" ? ( {state === "error" ? (
"Error" "Error"
) : state === "loading" ? ( ) : state === "loading" ? (

View File

@ -7,7 +7,7 @@ import { useStatelessAsyncClickable } from "~/logic/lib/useStatelessAsyncClickab
interface AsyncButtonProps { interface AsyncButtonProps {
children: ReactNode; children: ReactNode;
name: string; name?: string;
onClick: (e: React.MouseEvent) => Promise<void>; onClick: (e: React.MouseEvent) => Promise<void>;
} }
@ -15,6 +15,7 @@ export function StatelessAsyncButton({
children, children,
onClick, onClick,
name = "", name = "",
disabled = false,
...rest ...rest
}: AsyncButtonProps & Parameters<typeof Button>[0]) { }: AsyncButtonProps & Parameters<typeof Button>[0]) {
const { const {
@ -23,7 +24,12 @@ export function StatelessAsyncButton({
} = useStatelessAsyncClickable(onClick, name); } = useStatelessAsyncClickable(onClick, name);
return ( return (
<Button onClick={handleClick} {...rest}> <Button
hideDisabled={!disabled}
disabled={disabled || state === 'loading'}
onClick={handleClick}
{...rest}
>
{state === "error" ? ( {state === "error" ? (
"Error" "Error"
) : state === "loading" ? ( ) : state === "loading" ? (

View File

@ -5,6 +5,7 @@ import ReconnectButton from './ReconnectButton';
import { StatusBarItem } from './StatusBarItem'; import { StatusBarItem } from './StatusBarItem';
import { Sigil } from '~/logic/lib/sigil'; import { Sigil } from '~/logic/lib/sigil';
import useLocalState from '~/logic/state/local'; import useLocalState from '~/logic/state/local';
import { cite } from '~/logic/lib/util';
const StatusBar = (props) => { const StatusBar = (props) => {
const invites = [].concat(...Object.values(props.invites).map(obj => Object.values(obj))); const invites = [].concat(...Object.values(props.invites).map(obj => Object.values(obj)));
@ -21,7 +22,7 @@ const StatusBar = (props) => {
pb='3' pb='3'
> >
<Row collapse> <Row collapse>
<Button borderColor='washedGray' mr='2' px='2' onClick={() => props.history.push('/')} {...props}> <Button width="32px" borderColor='washedGray' mr='2' px='2' onClick={() => props.history.push('/')} {...props}>
<Icon icon='Spaces' color='black'/> <Icon icon='Spaces' color='black'/>
</Button> </Button>
@ -59,9 +60,8 @@ const StatusBar = (props) => {
> >
<Text color='#000000'>Submit <Text color='#000000' display={['none', 'inline']}>an</Text> issue</Text> <Text color='#000000'>Submit <Text color='#000000' display={['none', 'inline']}>an</Text> issue</Text>
</StatusBarItem> </StatusBarItem>
<StatusBarItem px={'2'} flexShrink='0' onClick={() => props.history.push('/~profile')}> <StatusBarItem width={['32px', 'auto']} px={'2'} flexShrink='0' onClick={() => props.history.push('/~profile')}>
<Sigil ship={props.ship} size={16} color='black' classes='mix-blend-diff' icon /> <Sigil ship={props.ship} size={16} color='black' classes='mix-blend-diff' icon />
<Text ml={2} display={["none", "inline"]} fontFamily="mono">~{props.ship}</Text>
</StatusBarItem> </StatusBarItem>
</Row> </Row>
</Box> </Box>

View File

@ -44,6 +44,8 @@ export default class VirtualScroller extends Component<VirtualScrollerProps, Vir
timeout: ReturnType<typeof setTimeout>; timeout: ReturnType<typeof setTimeout>;
} | undefined; } | undefined;
overscan = 150;
OVERSCAN_SIZE = 100; // Minimum number of messages on either side before loadRows is called OVERSCAN_SIZE = 100; // Minimum number of messages on either side before loadRows is called
constructor(props: VirtualScrollerProps) { constructor(props: VirtualScrollerProps) {
@ -53,7 +55,7 @@ export default class VirtualScroller extends Component<VirtualScrollerProps, Vir
visibleItems: new BigIntOrderedMap(), visibleItems: new BigIntOrderedMap(),
endgap: props.origin === 'bottom' ? 0 : undefined, endgap: props.origin === 'bottom' ? 0 : undefined,
totalHeight: 0, totalHeight: 0,
averageHeight: 64, averageHeight: 130,
scrollTop: props.origin === 'top' ? 0 : undefined scrollTop: props.origin === 'top' ? 0 : undefined
}; };
@ -61,8 +63,8 @@ export default class VirtualScroller extends Component<VirtualScrollerProps, Vir
this.window = null; this.window = null;
this.cache = new BigIntOrderedMap(); this.cache = new BigIntOrderedMap();
this.recalculateTotalHeight = this.recalculateTotalHeight.bind(this); this.recalculateTotalHeight = _.throttle(this.recalculateTotalHeight.bind(this), 200);
this.calculateVisibleItems = this.calculateVisibleItems.bind(this); this.calculateVisibleItems = _.throttle(this.calculateVisibleItems.bind(this), 200);
this.estimateIndexFromScrollTop = this.estimateIndexFromScrollTop.bind(this); this.estimateIndexFromScrollTop = this.estimateIndexFromScrollTop.bind(this);
this.invertedKeyHandler = this.invertedKeyHandler.bind(this); this.invertedKeyHandler = this.invertedKeyHandler.bind(this);
this.heightOf = this.heightOf.bind(this); this.heightOf = this.heightOf.bind(this);
@ -74,6 +76,8 @@ export default class VirtualScroller extends Component<VirtualScrollerProps, Vir
componentDidMount() { componentDidMount() {
this.calculateVisibleItems(); this.calculateVisibleItems();
this.recalculateTotalHeight();
} }
componentDidUpdate(prevProps: VirtualScrollerProps, prevState: VirtualScrollerState) { componentDidUpdate(prevProps: VirtualScrollerProps, prevState: VirtualScrollerState) {
@ -107,7 +111,7 @@ export default class VirtualScroller extends Component<VirtualScrollerProps, Vir
let { averageHeight } = this.state; let { averageHeight } = this.state;
let totalHeight = 0; let totalHeight = 0;
this.props.data.forEach((datum, index) => { this.props.data.forEach((datum, index) => {
totalHeight += this.heightOf(index); totalHeight += Math.max(this.heightOf(index), 0);
}); });
averageHeight = Number((totalHeight / this.props.data.size).toFixed()); averageHeight = Number((totalHeight / this.props.data.size).toFixed());
totalHeight += (this.props.size - this.props.data.size) * averageHeight; totalHeight += (this.props.size - this.props.data.size) * averageHeight;
@ -136,41 +140,23 @@ export default class VirtualScroller extends Component<VirtualScrollerProps, Vir
let startgap = 0, heightShown = 0, endgap = 0; let startgap = 0, heightShown = 0, endgap = 0;
let startGapFilled = false; let startGapFilled = false;
let visibleItems = new BigIntOrderedMap<any>(); let visibleItems = new BigIntOrderedMap<any>();
let startBuffer = new BigIntOrderedMap<any>();
let endBuffer = new BigIntOrderedMap<any>();
const { scrollTop, offsetHeight: windowHeight } = this.window; const { scrollTop, offsetHeight: windowHeight } = this.window;
const { averageHeight } = this.state; const { averageHeight, totalHeight } = this.state;
const { data, size: totalSize, onCalculateVisibleItems } = this.props; const { data, size: totalSize, onCalculateVisibleItems } = this.props;
console.log(windowHeight);
const overscan = Math.max(windowHeight / 2, 200);
[...data].forEach(([index, datum]) => { [...data].forEach(([index, datum]) => {
const height = this.heightOf(index); const height = this.heightOf(index);
if (startgap < (scrollTop - overscan) && !startGapFilled) { if (startgap < (scrollTop - this.overscan) && !startGapFilled) {
startBuffer.set(index, datum);
startgap += height; startgap += height;
} else if (heightShown < (windowHeight + overscan)) { } else if (heightShown < (windowHeight + this.overscan)) {
startGapFilled = true; startGapFilled = true;
visibleItems.set(index, datum); visibleItems.set(index, datum);
heightShown += height; heightShown += height;
} else if (endBuffer.size < visibleItems.size) { }
endBuffer.set(index, data.get(index));
} else {
endgap += height;
}
});
startBuffer = new BigIntOrderedMap(
[...startBuffer].reverse().slice(0, (visibleItems.size - visibleItems.size % 5))
);
startBuffer.forEach((_datum, index) => {
startgap -= this.heightOf(index);
}); });
endgap = totalHeight - heightShown - startgap;
const firstVisibleKey = visibleItems.peekSmallest()?.[0] ?? this.estimateIndexFromScrollTop(scrollTop)!; const firstVisibleKey = visibleItems.peekSmallest()?.[0] ?? this.estimateIndexFromScrollTop(scrollTop)!;
const smallest = data.peekSmallest(); const smallest = data.peekSmallest();
@ -189,7 +175,7 @@ export default class VirtualScroller extends Component<VirtualScrollerProps, Vir
onCalculateVisibleItems ? onCalculateVisibleItems(visibleItems) : null; onCalculateVisibleItems ? onCalculateVisibleItems(visibleItems) : null;
this.setState({ this.setState({
startgap: Number(startgap.toFixed()), startgap: Number(startgap.toFixed()),
visibleItems: new BigIntOrderedMap([...startBuffer, ...visibleItems, ...endBuffer]), visibleItems,
endgap: Number(endgap.toFixed()), endgap: Number(endgap.toFixed()),
}); });
} }
@ -238,6 +224,8 @@ export default class VirtualScroller extends Component<VirtualScrollerProps, Vir
} }
} }
this.overscan = Math.max(element.offsetHeight * 3, 500);
this.window = element; this.window = element;
if (this.props.origin === 'bottom') { if (this.props.origin === 'bottom') {
element.addEventListener('wheel', (event) => { element.addEventListener('wheel', (event) => {
@ -303,7 +291,7 @@ export default class VirtualScroller extends Component<VirtualScrollerProps, Vir
data data
} = this.props; } = this.props;
const indexesToRender = visibleItems.keys().reverse(); const indexesToRender = origin === 'top' ? visibleItems.keys() : visibleItems.keys().reverse();
const transform = origin === 'top' ? 'scale3d(1, 1, 1)' : 'scale3d(1, -1, 1)'; const transform = origin === 'top' ? 'scale3d(1, 1, 1)' : 'scale3d(1, -1, 1)';
@ -314,7 +302,7 @@ export default class VirtualScroller extends Component<VirtualScrollerProps, Vir
height: element.offsetHeight, height: element.offsetHeight,
element element
}); });
_.debounce(this.recalculateTotalHeight, 500)(); this.recalculateTotalHeight();
} }
}; };
return renderer({ index, measure, scrollWindow: this.window }); return renderer({ index, measure, scrollWindow: this.window });
@ -322,7 +310,7 @@ export default class VirtualScroller extends Component<VirtualScrollerProps, Vir
return ( return (
<Box overflowY='scroll' ref={this.setWindow.bind(this)} onScroll={this.onScroll.bind(this)} style={{ ...style, ...{ transform } }}> <Box overflowY='scroll' ref={this.setWindow.bind(this)} onScroll={this.onScroll.bind(this)} style={{ ...style, ...{ transform } }}>
<Box ref={this.scrollContainer} style={{ transform }}> <Box ref={this.scrollContainer} style={{ transform, width: '100%' }}>
<Box style={{ height: `${origin === 'top' ? startgap : endgap}px` }}></Box> <Box style={{ height: `${origin === 'top' ? startgap : endgap}px` }}></Box>
{indexesToRender.map(render)} {indexesToRender.map(render)}
<Box style={{ height: `${origin === 'top' ? endgap : startgap}px` }}></Box> <Box style={{ height: `${origin === 'top' ? endgap : startgap}px` }}></Box>

View File

@ -22,7 +22,7 @@ export class OmniboxInput extends Component {
border='1px solid transparent' border='1px solid transparent'
borderRadius='2' borderRadius='2'
maxWidth='calc(600px - 1.15rem)' maxWidth='calc(600px - 1.15rem)'
fontSize='0' fontSize='1'
style={{ boxSizing: 'border-box' }} style={{ boxSizing: 'border-box' }}
placeholder='Search...' placeholder='Search...'
onKeyDown={props.control} onKeyDown={props.control}

View File

@ -48,7 +48,7 @@ export function ChannelMenu(props: ChannelMenuProps) {
const isOurs = ship.slice(1) === window.ship; const isOurs = ship.slice(1) === window.ship;
const isMuted = const isMuted =
props.graphNotificationConfig.watching.findIndex( props.graphNotificationConfig.watching.findIndex(
(a) => a.graph === appPath && a.index === "/" (a) => a.graph === appPath && a.index === "/"
) === -1; ) === -1;
@ -102,7 +102,7 @@ export function ChannelMenu(props: ChannelMenuProps) {
</ChannelMenuItem> </ChannelMenuItem>
<ChannelMenuItem bottom icon="Gear" color="black"> <ChannelMenuItem bottom icon="Gear" color="black">
<Link to={`${baseUrl}/settings`}> <Link to={`${baseUrl}/settings`}>
<Box fontSize={0} p="2"> <Box fontSize={1} p="2">
Channel Settings Channel Settings
</Box> </Box>
</Link> </Link>
@ -119,9 +119,9 @@ export function ChannelMenu(props: ChannelMenuProps) {
} }
alignX="right" alignX="right"
alignY="top" alignY="top"
width="250px" dropWidth="250px"
> >
<Icon display="block" icon="Menu" color="gray" /> <Icon display="block" icon="Menu" color="gray" pr='2' />
</Dropdown> </Dropdown>
); );
} }

View File

@ -1,21 +1,18 @@
import React from "react"; import React from 'react';
import { import {
Center,
Box, Box,
Col, Col,
Row, Row,
Text, Text,
IconButton, Icon
Button, } from '@tlon/indigo-react';
Icon, import { uxToHex } from '~/logic/lib/util';
} from "@tlon/indigo-react"; import { Link } from 'react-router-dom';
import { uxToHex } from "~/logic/lib/util";
import { Link } from "react-router-dom";
import { Association, Associations } from "~/types/metadata-update"; import { Associations } from '~/types/metadata-update';
import { Dropdown } from "~/views/components/Dropdown"; import { Dropdown } from '~/views/components/Dropdown';
import { Workspace } from "~/types"; import { Workspace } from '~/types';
import { getTitleFromWorkspace } from "~/logic/lib/workspace"; import { getTitleFromWorkspace } from '~/logic/lib/workspace';
const GroupSwitcherItem = ({ to, children, bottom = false, ...rest }) => ( const GroupSwitcherItem = ({ to, children, bottom = false, ...rest }) => (
<Link to={to}> <Link to={to}>
@ -47,7 +44,7 @@ function RecentGroups(props: { recent: string[]; associations: Associations }) {
return (e in associations?.contacts); return (e in associations?.contacts);
}).slice(1, 5).map((g) => { }).slice(1, 5).map((g) => {
const assoc = associations.contacts[g]; const assoc = associations.contacts[g];
const color = uxToHex(assoc?.metadata?.color || "0x0"); const color = uxToHex(assoc?.metadata?.color || '0x0');
return ( return (
<Link key={g} style={{ minWidth: 0 }} to={`/~landscape${g}`}> <Link key={g} style={{ minWidth: 0 }} to={`/~landscape${g}`}>
<Row px={1} pb={2} alignItems="center"> <Row px={1} pb={2} alignItems="center">
@ -60,7 +57,7 @@ function RecentGroups(props: { recent: string[]; associations: Associations }) {
bg={`#${color}`} bg={`#${color}`}
mr={2} mr={2}
display="block" display="block"
flexShrink='0' flexShrink={0}
/> />
<Text verticalAlign='top' maxWidth='100%' overflow='hidden' display='inline-block' style={{ textOverflow: 'ellipsis', whiteSpace: 'pre' }}>{assoc?.metadata?.title}</Text> <Text verticalAlign='top' maxWidth='100%' overflow='hidden' display='inline-block' style={{ textOverflow: 'ellipsis', whiteSpace: 'pre' }}>{assoc?.metadata?.title}</Text>
</Row> </Row>
@ -76,22 +73,20 @@ export function GroupSwitcher(props: {
workspace: Workspace; workspace: Workspace;
baseUrl: string; baseUrl: string;
recentGroups: string[]; recentGroups: string[];
isAdmin: any;
}) { }) {
const { associations, workspace, isAdmin } = props; const { associations, workspace, isAdmin } = props;
const title = getTitleFromWorkspace(associations, workspace); const title = getTitleFromWorkspace(associations, workspace);
const navTo = (to: string) => `${props.baseUrl}${to}`; const navTo = (to: string) => `${props.baseUrl}${to}`;
return ( return (
<Box zIndex="2" position="sticky" top="0px" p={2}> <Box height='48px' backgroundColor="white" zIndex="2" position="sticky" top="0px" py={3} pl='3' borderBottom='1px solid' borderColor='washedGray'>
<Col <Col
justifyContent="center"
bg="white" bg="white"
borderRadius={1}
border={1}
borderColor="washedGray"
> >
<Row alignItems="center" justifyContent="space-between"> <Row justifyContent="space-between">
<Dropdown <Dropdown
width="231px" width="100%"
dropWidth="231px"
alignY="top" alignY="top"
options={ options={
<Col <Col
@ -134,9 +129,9 @@ export function GroupSwitcher(props: {
<Icon mr="2" color="gray" icon="Plus" /> <Icon mr="2" color="gray" icon="Plus" />
<Text> Join Group</Text> <Text> Join Group</Text>
</GroupSwitcherItem> </GroupSwitcherItem>
{workspace.type === "group" && ( {workspace.type === 'group' && (
<> <>
<GroupSwitcherItem to={navTo("/popover/participants")}> <GroupSwitcherItem to={navTo('/popover/participants')}>
<Icon <Icon
mr={2} mr={2}
color="gray" color="gray"
@ -144,7 +139,7 @@ export function GroupSwitcher(props: {
/> />
<Text> Participants</Text> <Text> Participants</Text>
</GroupSwitcherItem> </GroupSwitcherItem>
<GroupSwitcherItem to={navTo("/popover/settings")}> <GroupSwitcherItem to={navTo('/popover/settings')}>
<Icon <Icon
mr={2} mr={2}
color="gray" color="gray"
@ -152,7 +147,7 @@ export function GroupSwitcher(props: {
/> />
<Text> Group Settings</Text> <Text> Group Settings</Text>
</GroupSwitcherItem> </GroupSwitcherItem>
{isAdmin && (<GroupSwitcherItem bottom to={navTo("/invites")}> {isAdmin && (<GroupSwitcherItem bottom to={navTo('/invites')}>
<Icon <Icon
mr={2} mr={2}
color="blue" color="blue"
@ -165,25 +160,25 @@ export function GroupSwitcher(props: {
</Col> </Col>
} }
> >
<Row p={2} alignItems="center" width='100%' minWidth='0'> <Row width='100%' minWidth='0' flexShrink={0}>
<Row alignItems="center" mr={1} flex='1' width='100%' minWidth='0'> <Row justifyContent="space-between" mr={1} flexShrink={0} width='100%' minWidth='0'>
<Text overflow='hidden' display='inline-block' flexShrink='1' style={{ textOverflow: 'ellipsis', whiteSpace: 'pre'}}>{title}</Text> <Text lineHeight="1.1" fontSize='2' fontWeight="700" overflow='hidden' display='inline-block' flexShrink='1' style={{ textOverflow: 'ellipsis', whiteSpace: 'pre' }}>{title}</Text>
<Icon size='12px' ml='1' mt="0px" display="inline-block" icon="ChevronSouth" />
</Row> </Row>
</Row> </Row>
</Dropdown> </Dropdown>
<Row pr={1} justifyContent="flex-end" alignItems="center"> <Row pr='3' verticalAlign="middle">
{(workspace.type === "group") && ( {(workspace.type === 'group') && (
<> <>
{isAdmin && (<Link to={navTo("/invites")}> {isAdmin && (<Link to={navTo('/invites')}>
<Icon <Icon
display="block" display="inline-block"
color='blue' color='blue'
icon="Users" icon="Users"
ml='12px'
/> />
</Link>)} </Link>)}
<Link to={navTo("/popover/settings")}> <Link to={navTo('/popover/settings')}>
<Icon color='gray' display="block" m={1} icon="Gear" /> <Icon color='gray' display="inline-block" ml={'12px'} icon="Gear" />
</Link> </Link>
</> </>
)} )}

View File

@ -85,7 +85,7 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps) {
moduleType moduleType
); );
} }
if (!group) { if (!group) {
await waiter(p => Boolean(p?.groups?.[`/ship/~${window.ship}/${resId}`])); await waiter(p => Boolean(p?.groups?.[`/ship/~${window.ship}/${resId}`]));
} }
@ -99,11 +99,11 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps) {
actions.setStatus({ error: 'Channel creation failed' }); actions.setStatus({ error: 'Channel creation failed' });
} }
}; };
return ( return (
<Col overflowY="auto" p={3}> <Col overflowY="auto" p={3}>
<Box pb='3' display={['block', 'none']} onClick={() => history.push(props.baseUrl)}> <Box pb='3' display={['block', 'none']} onClick={() => history.push(props.baseUrl)}>
{'<- Back'} <Text fontSize='0' bold>{'<- Back'}</Text>
</Box> </Box>
<Box fontWeight="bold" mb={4} color="black"> <Box fontWeight="bold" mb={4} color="black">
New Channel New Channel

View File

@ -80,7 +80,7 @@ export function PopoverRoutes(
<Box <Box
display="grid" display="grid"
gridTemplateRows={["32px 1fr", "100%"]} gridTemplateRows={["32px 1fr", "100%"]}
gridTemplateColumns={["100%", "150px 1fr"]} gridTemplateColumns={["100%", "250px 1fr"]}
height="100%" height="100%"
width="100%" width="100%"
> >

View File

@ -52,6 +52,7 @@ export function Resource(props: ResourceProps) {
return ( return (
<ResourceSkeleton <ResourceSkeleton
baseUrl={props.baseUrl} baseUrl={props.baseUrl}
groupTags={props.groups?.[selectedGroup]?.tags}
{...skelProps} {...skelProps}
> >
<ChannelSettings <ChannelSettings
@ -72,6 +73,7 @@ export function Resource(props: ResourceProps) {
notificationsGraphConfig={props.notificationsGraphConfig} notificationsGraphConfig={props.notificationsGraphConfig}
notificationsChatConfig={props.notificationsChatConfig} notificationsChatConfig={props.notificationsChatConfig}
baseUrl={props.baseUrl} baseUrl={props.baseUrl}
groupTags={props.groups?.[selectedGroup]?.tags}
{...skelProps} {...skelProps}
atRoot atRoot
> >

View File

@ -16,7 +16,7 @@ import { ChannelMenu } from "./ChannelMenu";
import { NotificationGraphConfig } from "~/types"; import { NotificationGraphConfig } from "~/types";
const TruncatedBox = styled(Box)` const TruncatedBox = styled(Box)`
white-space: nowrap; white-space: pre;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
`; `;
@ -29,19 +29,33 @@ type ResourceSkeletonProps = {
children: ReactNode; children: ReactNode;
atRoot?: boolean; atRoot?: boolean;
title?: string; title?: string;
groupTags?: any;
}; };
export function ResourceSkeleton(props: ResourceSkeletonProps) { export function ResourceSkeleton(props: ResourceSkeletonProps) {
const { association, api, baseUrl, children, atRoot } = props; const { association, api, baseUrl, children, atRoot, groupTags } = props;
const app = association?.metadata?.module || association["app-name"]; const app = association?.metadata?.module || association["app-name"];
const appPath = association["app-path"]; const appPath = association["app-path"];
const workspace = const workspace =
baseUrl === "/~landscape/home" ? "/home" : association["group-path"]; baseUrl === "/~landscape/home" ? "/home" : association["group-path"];
const title = props.title || association?.metadata?.title; const title = props.title || association?.metadata?.title;
const [, , ship, resource] = appPath.split("/");
const resourcePath = (p: string) => baseUrl + `/resource/${app}/ship/${ship}/${resource}` + p;
const isOwn = `~${window.ship}` === ship;
let isWriter = (app === 'publish') ? true : false;
if (groupTags?.publish?.[`writers-${resource}`]) {
isWriter = isOwn || groupTags?.publish?.[`writers-${resource}`]?.has(window.ship);
}
return ( return (
<Col width="100%" height="100%" overflowY="hidden"> <Col width="100%" height="100%" overflowY="hidden">
<Box <Box
flexShrink="0" flexShrink="0"
height='48px'
py="2" py="2"
px="2" px="2"
display="flex" display="flex"
@ -54,9 +68,11 @@ export function ResourceSkeleton(props: ResourceSkeletonProps) {
borderRight={1} borderRight={1}
borderRightColor="gray" borderRightColor="gray"
pr={3} pr={3}
fontSize='1'
mr={3} mr={3}
my="1" my="1"
display={["block", "none"]} display={["block", "none"]}
flexShrink={0}
> >
<Link to={`/~landscape${workspace}`}> {"<- Back"}</Link> <Link to={`/~landscape${workspace}`}> {"<- Back"}</Link>
</Box> </Box>
@ -70,15 +86,15 @@ export function ResourceSkeleton(props: ResourceSkeletonProps) {
{atRoot && ( {atRoot && (
<> <>
<Box pr={1} mr={2}> <Box px={1} mr={2} minWidth={0} display="flex">
<Text display="inline-block" verticalAlign="middle"> <Text fontSize='2' fontWeight='700' display="inline-block" verticalAlign="middle" textOverflow="ellipsis" overflow="hidden" whiteSpace="pre" minWidth={0}>
{title} {title}
</Text> </Text>
</Box> </Box>
<TruncatedBox <TruncatedBox
display={["none", "block"]} display={["none", "block"]}
maxWidth="60%"
verticalAlign="middle" verticalAlign="middle"
maxWidth='60%'
flexShrink={1} flexShrink={1}
title={association?.metadata?.description} title={association?.metadata?.description}
color="gray" color="gray"
@ -93,6 +109,11 @@ export function ResourceSkeleton(props: ResourceSkeletonProps) {
</RichText> </RichText>
</TruncatedBox> </TruncatedBox>
<Box flexGrow={1} /> <Box flexGrow={1} />
{isWriter && (
<Link to={resourcePath('/new')} style={{ flexShrink: '0' }}>
<Text bold pr='3' color='blue'>+ New Post</Text>
</Link>
)}
<ChannelMenu <ChannelMenu
graphNotificationConfig={props.notificationsGraphConfig} graphNotificationConfig={props.notificationsGraphConfig}
chatNotificationConfig={props.notificationsChatConfig} chatNotificationConfig={props.notificationsChatConfig}

View File

@ -1,26 +1,24 @@
import React, { ReactNode } from "react"; import React, { ReactNode } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { import {
Box, Col
Col, } from '@tlon/indigo-react';
} from "@tlon/indigo-react";
import { Link } from "react-router-dom";
import GlobalApi from "~/logic/api/global"; import GlobalApi from '~/logic/api/global';
import { GroupSwitcher } from "../GroupSwitcher"; import { GroupSwitcher } from '../GroupSwitcher';
import { import {
Associations, Associations,
Workspace, Workspace,
Groups, Groups,
Invites, Invites,
Rolodex, Rolodex
} from "~/types"; } from '~/types';
import { SidebarListHeader } from "./SidebarListHeader"; import { SidebarListHeader } from './SidebarListHeader';
import { useLocalStorageState } from "~/logic/lib/useLocalStorageState"; import { useLocalStorageState } from '~/logic/lib/useLocalStorageState';
import { getGroupFromWorkspace } from "~/logic/lib/workspace"; import { getGroupFromWorkspace } from '~/logic/lib/workspace';
import { SidebarAppConfigs } from './types'; import { SidebarAppConfigs } from './types';
import { SidebarList } from "./SidebarList"; import { SidebarList } from './SidebarList';
import { roleForShip } from "~/logic/lib/group"; import { roleForShip } from '~/logic/lib/group';
const ScrollbarLessCol = styled(Col)` const ScrollbarLessCol = styled(Col)`
scrollbar-width: none !important; scrollbar-width: none !important;
@ -30,7 +28,6 @@ const ScrollbarLessCol = styled(Col)`
} }
`; `;
interface SidebarProps { interface SidebarProps {
contacts: Rolodex; contacts: Rolodex;
children: ReactNode; children: ReactNode;
@ -48,38 +45,24 @@ interface SidebarProps {
workspace: Workspace; workspace: Workspace;
} }
// Magic spacer that because firefox doesn't correctly calculate
// position: sticky on a flex child
// remove when https://bugzilla.mozilla.org/show_bug.cgi?id=1488080
// is fixed
const SidebarStickySpacer = styled(Box)`
height: 0px;
flex-grow: 1;
@-moz-document url-prefix() {
& {
height: ${p => p.theme.space[6] }px;
}
}
`;
export function Sidebar(props: SidebarProps) { export function Sidebar(props: SidebarProps) {
const { invites, api, associations, selected, apps, workspace } = props; const { associations, selected, workspace } = props;
const groupPath = getGroupFromWorkspace(workspace); const groupPath = getGroupFromWorkspace(workspace);
const display = props.mobileHide ? ["none", "flex"] : "flex"; const display = props.mobileHide ? ['none', 'flex'] : 'flex';
if (!associations) { if (!associations) {
return null; return null;
} }
const [config, setConfig] = useLocalStorageState<SidebarListConfig>( const [config, setConfig] = useLocalStorageState<SidebarListConfig>(
`group-config:${groupPath || "home"}`, `group-config:${groupPath || 'home'}`,
{ {
sortBy: "lastUpdated", sortBy: 'lastUpdated',
hideUnjoined: false, hideUnjoined: false
} }
); );
const role = props.groups?.[groupPath] ? roleForShip(props.groups[groupPath], window.ship) : undefined; const role = props.groups?.[groupPath] ? roleForShip(props.groups[groupPath], window.ship) : undefined;
const isAdmin = (role === "admin") || (workspace?.type === 'home'); const isAdmin = (role === 'admin') || (workspace?.type === 'home');
return ( return (
<ScrollbarLessCol <ScrollbarLessCol
@ -107,8 +90,9 @@ export function Sidebar(props: SidebarProps) {
groups={props.groups} groups={props.groups}
initialValues={config} initialValues={config}
handleSubmit={setConfig} handleSubmit={setConfig}
selected={selected || ""} selected={selected || ''}
workspace={workspace} /> workspace={workspace}
/>
<SidebarList <SidebarList
config={config} config={config}
associations={associations} associations={associations}
@ -118,31 +102,6 @@ export function Sidebar(props: SidebarProps) {
apps={props.apps} apps={props.apps}
baseUrl={props.baseUrl} baseUrl={props.baseUrl}
/> />
<SidebarStickySpacer flexShrink={0} />
<Box
flexShrink="0"
display={isAdmin ? "flex" : "none"}
justifyContent="center"
position="sticky"
bottom={"8px"}
width="100%"
height="fit-content"
py="2"
>
<Link
to={!!groupPath ? `/~landscape${groupPath}/new` : `/~landscape/home/new`}
>
<Box
bg="white"
p={2}
borderRadius={1}
border={1}
borderColor="lightGray"
>
+ New Channel
</Box>
</Link>
</Box>
</ScrollbarLessCol> </ScrollbarLessCol>
); );
} }

View File

@ -93,8 +93,8 @@ export function SidebarItem(props: {
justifyContent="space-between" justifyContent="space-between"
alignItems="center" alignItems="center"
py={1} py={1}
pl={2} pl={3}
pr={2} pr={3}
selected={selected} selected={selected}
> >
<Row width='100%' alignItems="center" flex='1 auto' minWidth='0'> <Row width='100%' alignItems="center" flex='1 auto' minWidth='0'>
@ -105,7 +105,7 @@ export function SidebarItem(props: {
/> />
<Box width='100%' flexShrink={2} ml={2} display='flex' overflow='hidden'> <Box width='100%' flexShrink={2} ml={2} display='flex' overflow='hidden'>
<Text <Text
lineHeight="short" lineHeight="tall"
display='inline-block' display='inline-block'
flex='1' flex='1'
overflow='hidden' overflow='hidden'

View File

@ -14,7 +14,8 @@ import { Dropdown } from "~/views/components/Dropdown";
import { FormikHelpers } from "formik"; import { FormikHelpers } from "formik";
import { SidebarListConfig, Workspace } from "./types"; import { SidebarListConfig, Workspace } from "./types";
import { Link, useHistory } from 'react-router-dom'; import { Link, useHistory } from 'react-router-dom';
import {ShipSearch} from "~/views/components/ShipSearch"; import { getGroupFromWorkspace } from "~/logic/lib/workspace";
import { roleForShip } from "~/logic/lib/group";
import {Groups, Rolodex} from "~/types"; import {Groups, Rolodex} from "~/types";
export function SidebarListHeader(props: { export function SidebarListHeader(props: {
@ -36,42 +37,52 @@ export function SidebarListHeader(props: {
[props.handleSubmit] [props.handleSubmit]
); );
const groupPath = getGroupFromWorkspace(props.workspace);
const role = props.groups?.[groupPath] ? roleForShip(props.groups[groupPath], window.ship) : undefined;
const isAdmin = (role === "admin") || (props.workspace?.type === 'home');
return ( return (
<Row <Row
flexShrink="0" flexShrink="0"
alignItems="center" alignItems="center"
justifyContent="space-between" justifyContent="space-between"
py={2} py={2}
pr={2} px={3}
pl={2} height='48px'
> >
<Box flexShrink='0'> <Box flexShrink='0'>
<Text bold> <Text>
{props.initialValues.hideUnjoined ? "Joined Channels" : "All Channels"} {props.initialValues.hideUnjoined ? "Joined Channels" : "All Channels"}
</Text> </Text>
</Box> </Box>
<Box <Box
width='100%'
textAlign='right' textAlign='right'
mr='2' display='flex'
display={(props.workspace?.type === 'home') ? 'inline-block' : 'none'} alignItems='center'
> >
<Link to={`${props.baseUrl}/invites`}> <Link
style={{
display: isAdmin ? "inline-block" : "none" }}
to={
!!groupPath ? `/~landscape${groupPath}/new` : `/~landscape/home/new`}>
<Icon icon="Plus" color="gray" pr='12px'/>
</Link>
<Link to={`${props.baseUrl}/invites`}
style={{ display: (props.workspace?.type === 'home') ? 'inline-block' : 'none'}}>
<Text <Text
display='inline-block' display='inline-block'
verticalAlign='middle'
py='1px' py='1px'
px='3px' px='3px'
mr='12px'
backgroundColor='washedBlue' backgroundColor='washedBlue'
color='blue' color='blue'
borderRadius='1'> borderRadius='1'>
+ DM + DM
</Text> </Text>
</Link> </Link>
</Box>
<Dropdown <Dropdown
flexShrink='0' flexShrink='0'
width="200px" width="auto"
alignY="top" alignY="top"
alignX={["right", "left"]} alignX={["right", "left"]}
options={ options={
@ -102,6 +113,7 @@ export function SidebarListHeader(props: {
> >
<Icon color="gray" icon="Adjust" /> <Icon color="gray" icon="Adjust" />
</Dropdown> </Dropdown>
</Box>
</Row> </Row>
); );
} }

View File

@ -1,171 +0,0 @@
import baseStyled, { ThemedStyledInterface } from "styled-components";
const base = {
white: "rgba(255,255,255,1)",
black: "rgba(0,0,0,1)",
red: "rgba(255,65,54,1)",
yellow: "rgba(255,199,0,1)",
green: "rgba(0,159,101,1)",
blue: "rgba(0,142,255,1)",
};
const scales = {
white10: "rgba(255,255,255,0.1)",
white20: "rgba(255,255,255,0.2)",
white30: "rgba(255,255,255,0.3)",
white40: "rgba(255,255,255,0.4)",
white50: "rgba(255,255,255,0.5)",
white60: "rgba(255,255,255,0.6)",
white70: "rgba(255,255,255,0.7)",
white80: "rgba(255,255,255,0.8)",
white90: "rgba(255,255,255,0.9)",
white100: "rgba(255,255,255,1)",
black05: "rgba(0,0,0,0.05)",
black10: "rgba(0,0,0,0.1)",
black20: "rgba(0,0,0,0.2)",
black30: "rgba(0,0,0,0.3)",
black40: "rgba(0,0,0,0.4)",
black50: "rgba(0,0,0,0.5)",
black60: "rgba(0,0,0,0.6)",
black70: "rgba(0,0,0,0.7)",
black80: "rgba(0,0,0,0.8)",
black90: "rgba(0,0,0,0.9)",
black100: "rgba(0,0,0,1)",
red10: "rgba(255,65,54,0.1)",
red20: "rgba(255,65,54,0.2)",
red30: "rgba(255,65,54,0.3)",
red40: "rgba(255,65,54,0.4)",
red50: "rgba(255,65,54,0.5)",
red60: "rgba(255,65,54,0.6)",
red70: "rgba(255,65,54,0.7)",
red80: "rgba(255,65,54,0.8)",
red90: "rgba(255,65,54,0.9)",
red100: "rgba(255,65,54,1)",
yellow10: "rgba(255,199,0,0.1)",
yellow20: "rgba(255,199,0,0.2)",
yellow30: "rgba(255,199,0,0.3)",
yellow40: "rgba(255,199,0,0.4)",
yellow50: "rgba(255,199,0,0.5)",
yellow60: "rgba(255,199,0,0.6)",
yellow70: "rgba(255,199,0,0.7)",
yellow80: "rgba(255,199,0,0.8)",
yellow90: "rgba(255,199,0,0.9)",
yellow100: "rgba(255,199,0,1)",
green10: "rgba(0,159,101,0.1)",
green20: "rgba(0,159,101,0.2)",
green30: "rgba(0,159,101,0.3)",
green40: "rgba(0,159,101,0.4)",
green50: "rgba(0,159,101,0.5)",
green60: "rgba(0,159,101,0.6)",
green70: "rgba(0,159,101,0.7)",
green80: "rgba(0,159,101,0.8)",
green90: "rgba(0,159,101,0.9)",
green100: "rgba(0,159,101,1)",
blue10: "rgba(0,142,255,0.1)",
blue20: "rgba(0,142,255,0.2)",
blue30: "rgba(0,142,255,0.3)",
blue40: "rgba(0,142,255,0.4)",
blue50: "rgba(0,142,255,0.5)",
blue60: "rgba(0,142,255,0.6)",
blue70: "rgba(0,142,255,0.7)",
blue80: "rgba(0,142,255,0.8)",
blue90: "rgba(0,142,255,0.9)",
blue100: "rgba(0,142,255,1)",
};
const theme = {
colors: {
white: base.white,
black: base.black,
darkGray: scales.black80,
gray: scales.black60,
lightGray: scales.black30,
washedGray: scales.black10,
red: base.red,
lightRed: scales.red30,
washedRed: scales.red10,
yellow: base.yellow,
lightYellow: scales.yellow30,
washedYellow: scales.yellow10,
green: base.green,
lightGreen: scales.green30,
washedGreen: scales.green10,
blue: base.blue,
lightBlue: scales.blue30,
washedBlue: scales.blue10,
none: "rgba(0,0,0,0)",
scales: scales,
},
fonts: {
sans: `"Inter", "Inter UI", -apple-system, BlinkMacSystemFont, 'San Francisco', 'Helvetica Neue', Arial, sans-serif`,
mono: `"Source Code Pro", "Roboto mono", "Courier New", monospace`,
},
// font-size
fontSizes: [
12, // 0
16, // 1
24, // 2
32, // 3
48, // 4
64, // 5
],
// font-weight
fontWeights: {
thin: 300,
regular: 400,
bold: 600,
},
// line-height
lineHeights: {
min: 1.2,
short: 1.333333,
regular: 1.5,
tall: 1.666666,
},
// border, border-top, border-right, border-bottom, border-left
borders: ["none", "1px solid"],
// margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, grid-gap, grid-column-gap, grid-row-gap
space: [
0, // 0
4, // 1
8, // 2
16, // 3
24, // 4
32, // 5
48, // 6
64, // 7
96, // 8
],
// border-radius
radii: [
0, // 0
2, // 1
4, // 2
8, // 3
16, // 4
],
// width, height, min-width, max-width, min-height, max-height
sizes: [
0, // 0
4, // 1
8, // 2
16, // 3
24, // 4
32, // 5
48, // 6
64, // 7
96, // 8
],
// z-index
zIndices: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
breakpoints: ["550px", "750px", "960px"],
};
export type Theme = typeof theme;
export const styled = baseStyled as ThemedStyledInterface<Theme>;
export default theme;

View File

@ -1,186 +0,0 @@
import baseStyled, { ThemedStyledInterface } from "styled-components";
const base = {
white: "rgba(255,255,255,1)",
black: "rgba(0,0,0,1)",
red: "rgba(255,65,54,1)",
yellow: "rgba(255,199,0,1)",
green: "rgba(0,159,101,1)",
blue: "rgba(0,142,255,1)",
};
const scales = {
white05: "rgba(255,255,255,0.05)",
white10: "rgba(255,255,255,0.1)",
white20: "rgba(255,255,255,0.2)",
white30: "rgba(255,255,255,0.3)",
white40: "rgba(255,255,255,0.4)",
white50: "rgba(255,255,255,0.5)",
white60: "rgba(255,255,255,0.6)",
white70: "rgba(255,255,255,0.7)",
white80: "rgba(255,255,255,0.8)",
white90: "rgba(255,255,255,0.9)",
white100: "rgba(255,255,255,1)",
black05: "rgba(0,0,0,0.05)",
black10: "rgba(0,0,0,0.1)",
black20: "rgba(0,0,0,0.2)",
black30: "rgba(0,0,0,0.3)",
black40: "rgba(0,0,0,0.4)",
black50: "rgba(0,0,0,0.5)",
black60: "rgba(0,0,0,0.6)",
black70: "rgba(0,0,0,0.7)",
black80: "rgba(0,0,0,0.8)",
black90: "rgba(0,0,0,0.9)",
black100: "rgba(0,0,0,1)",
red05: "rgba(255,65,54,0.05)",
red10: "rgba(255,65,54,0.1)",
red20: "rgba(255,65,54,0.2)",
red30: "rgba(255,65,54,0.3)",
red40: "rgba(255,65,54,0.4)",
red50: "rgba(255,65,54,0.5)",
red60: "rgba(255,65,54,0.6)",
red70: "rgba(255,65,54,0.7)",
red80: "rgba(255,65,54,0.8)",
red90: "rgba(255,65,54,0.9)",
red100: "rgba(255,65,54,1)",
yellow05: "rgba(255,199,0,0.05)",
yellow10: "rgba(255,199,0,0.1)",
yellow20: "rgba(255,199,0,0.2)",
yellow30: "rgba(255,199,0,0.3)",
yellow40: "rgba(255,199,0,0.4)",
yellow50: "rgba(255,199,0,0.5)",
yellow60: "rgba(255,199,0,0.6)",
yellow70: "rgba(255,199,0,0.7)",
yellow80: "rgba(255,199,0,0.8)",
yellow90: "rgba(255,199,0,0.9)",
yellow100: "rgba(255,199,0,1)",
green05: "rgba(0,159,101,0.05)",
green10: "rgba(0,159,101,0.1)",
green20: "rgba(0,159,101,0.2)",
green30: "rgba(0,159,101,0.3)",
green40: "rgba(0,159,101,0.4)",
green50: "rgba(0,159,101,0.5)",
green60: "rgba(0,159,101,0.6)",
green70: "rgba(0,159,101,0.7)",
green80: "rgba(0,159,101,0.8)",
green90: "rgba(0,159,101,0.9)",
green100: "rgba(0,159,101,1)",
blue05: "rgba(0,142,255,0.05)",
blue10: "rgba(0,142,255,0.1)",
blue20: "rgba(0,142,255,0.2)",
blue30: "rgba(0,142,255,0.3)",
blue40: "rgba(0,142,255,0.4)",
blue50: "rgba(0,142,255,0.5)",
blue60: "rgba(0,142,255,0.6)",
blue70: "rgba(0,142,255,0.7)",
blue80: "rgba(0,142,255,0.8)",
blue90: "rgba(0,142,255,0.9)",
blue100: "rgba(0,142,255,1)",
};
const util = {
cyan: "#00FFFF",
magenta: "#FF00FF",
yellow: "#FFFF00",
black: "#000000",
gray0: "#333333"
};
const theme = {
colors: {
white: util.gray0,
black: base.white,
darkGray: scales.white80,
gray: scales.white60,
lightGray: scales.white30,
washedGray: scales.white05,
red: base.red,
lightRed: scales.red30,
washedRed: scales.red05,
yellow: base.yellow,
lightYellow: scales.yellow30,
washedYellow: scales.yellow10,
green: base.green,
lightGreen: scales.green30,
washedGreen: scales.green10,
blue: base.blue,
lightBlue: scales.blue30,
washedBlue: scales.blue10,
none: "rgba(0,0,0,0)",
scales: scales,
util: util,
},
fonts: {
sans: `"Inter", "Inter UI", -apple-system, BlinkMacSystemFont, 'San Francisco', 'Helvetica Neue', Arial, sans-serif`,
mono: `"Source Code Pro", "Roboto mono", "Courier New", monospace`,
},
// font-size
fontSizes: [
12, // 0
16, // 1
24, // 2
32, // 3
48, // 4
64, // 5
],
// font-weight
fontWeights: {
thin: 300,
regular: 400,
semibold: 500,
bold: 600,
},
// line-height
lineHeights: {
min: 1.2,
short: 1.333333,
regular: 1.5,
tall: 1.666666,
},
// border, border-top, border-right, border-bottom, border-left
borders: ["none", "1px solid"],
// margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, grid-gap, grid-column-gap, grid-row-gap
space: [
0, // 0
4, // 1
8, // 2
16, // 3
24, // 4
32, // 5
48, // 6
64, // 7
96, // 8
],
// border-radius
radii: [
0, // 0
2, // 1
4, // 2
8, // 3
],
// width, height, min-width, max-width, min-height, max-height
sizes: [
0, // 0
4, // 1
8, // 2
16, // 3
24, // 4
32, // 5
48, // 6
64, // 7
96, // 8
],
// z-index
zIndices: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
breakpoints: ["550px", "750px", "960px"],
};
export type Theme = typeof theme;
export const styled = baseStyled as ThemedStyledInterface<Theme>;
export default theme;