(persist((set, get) => ({
}
})),
set: fn => set(produce(fn))
-}), {
+ }), {
+ blacklist: ['suspendedFocus', 'toggleOmnibox', 'omniboxShown'],
name: 'localReducer'
}));
@@ -55,4 +56,4 @@ function withLocalState(Component: any, stateMemb
});
}
-export { useLocalState as default, withLocalState };
\ No newline at end of file
+export { useLocalState as default, withLocalState };
diff --git a/pkg/interface/src/types/invite-update.ts b/pkg/interface/src/types/invite-update.ts
index a1eb2ed91..b897687a4 100644
--- a/pkg/interface/src/types/invite-update.ts
+++ b/pkg/interface/src/types/invite-update.ts
@@ -1,4 +1,5 @@
import { Serial, PatpNoSig, Path } from './noun';
+import {Resource} from './group-update';
export type InviteUpdate =
InviteUpdateInitial
@@ -60,8 +61,8 @@ export type AppInvites = {
export interface Invite {
app: string;
- path: Path;
- recipeint: PatpNoSig;
+ recipient: PatpNoSig;
+ resource: Resource;
ship: PatpNoSig;
text: string;
}
diff --git a/pkg/interface/src/views/App.js b/pkg/interface/src/views/App.js
index 19f4ffc8c..0e70bce4f 100644
--- a/pkg/interface/src/views/App.js
+++ b/pkg/interface/src/views/App.js
@@ -11,8 +11,8 @@ import 'mousetrap-global-bind';
import './css/indigo-static.css';
import './css/fonts.css';
-import light from './themes/light';
-import dark from './themes/old-dark';
+import light from '@tlon/indigo-light';
+import dark from '@tlon/indigo-dark';
import { Text, Anchor, Row } from '@tlon/indigo-react';
@@ -40,7 +40,7 @@ const Root = styled.div`
background-size: cover;
` : p.background?.type === 'color' ? `
background-color: ${p.background.color};
- ` : ''
+ ` : `background-color: ${p.theme.colors.white};`
}
display: flex;
flex-flow: column nowrap;
@@ -90,7 +90,7 @@ class App extends React.Component {
this.themeWatcher.onchange = this.updateTheme;
setTimeout(() => {
// 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);
}, 500);
this.api.local.getBaseHash();
@@ -139,9 +139,6 @@ class App extends React.Component {
const doNotDisturb = state.doNotDisturb || false;
const ourContact = this.state.contacts[this.ship] || null;
- const showBanner = localStorage.getItem("2020BreachBanner") || "flex";
- let banner = null;
-
return (
@@ -170,6 +167,7 @@ class App extends React.Component {
associations={state.associations}
apps={state.launch}
api={this.api}
+ contacts={state.contacts}
notifications={state.notificationsCount}
invites={state.invites}
groups={state.groups}
diff --git a/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx
index 7203d6cfe..30ed1c2c1 100644
--- a/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx
+++ b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx
@@ -4,7 +4,7 @@ import _ from "lodash";
import { Box, Row, Text, Rule } from "@tlon/indigo-react";
import OverlaySigil from '~/views/components/OverlaySigil';
-import { uxToHex, cite, writeText, useShowNickname } from '~/logic/lib/util';
+import { uxToHex, cite, writeText, useShowNickname, useHovering } from '~/logic/lib/util';
import { Group, Association, Contacts, Post } from "~/types";
import TextContent from './content/text';
import CodeContent from './content/code';
@@ -134,6 +134,7 @@ export default class ChatMessage extends Component {
className={containerClass}
style={style}
mb={1}
+ position="relative"
>
{dayBreak && !isLastRead ? : null}
{renderSigil
@@ -194,6 +195,8 @@ export const MessageWithSigil = (props) => {
}
};
+ const { hovering, bind } = useHovering();
+
return (
<>
{
history={history}
api={api}
bg="white"
- className="fl pr3 v-top pt1"
+ className="fl v-top pt1"
+ pr={3}
+ pl={2}
/>
-
+
{
}}
title={`~${msg.author}`}
>{name}
- {timestamp}
- {datestamp}
+ {timestamp}
+ {datestamp}
{msg.contents.map(c =>
@@ -257,20 +269,40 @@ const ContentBox = styled(Box)`
`;
-export const MessageWithoutSigil = ({ timestamp, contacts, msg, measure, group }) => (
- <>
- {timestamp}
-
- {msg.contents.map((c, i) => (
- ))}
-
- >
-);
+export const MessageWithoutSigil = ({ timestamp, contacts, msg, measure, group }) => {
+ const { hovering, bind } = useHovering();
+ return (
+ <>
+ {timestamp}
+
+ {msg.contents.map((c, i) => (
+ ))}
+
+ >
+ )
+};
export const MessageContent = ({ content, contacts, measure, fontSize, group }) => {
if ('code' in content) {
@@ -282,7 +314,7 @@ export const MessageContent = ({ content, contacts, measure, fontSize, group })
url={content.url}
onLoad={measure}
imageProps={{style: {
- maxWidth: '18rem',
+ maxWidth: 'min(100%,18rem)',
display: 'block'
}}}
videoProps={{style: {
@@ -292,7 +324,8 @@ export const MessageContent = ({ content, contacts, measure, fontSize, group })
}}
textProps={{style: {
fontSize: 'inherit',
- textDecoration: 'underline'
+ borderBottom: '1px solid',
+ textDecoration: 'none'
}}}
/>
diff --git a/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx b/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx
index 92d2c48fa..5073b8549 100644
--- a/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx
+++ b/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx
@@ -258,7 +258,7 @@ export default class ChatWindow extends Component new Proxy(input, {
if (property === 'setValue') {
return (val) => target.value = val;
}
+ if (property === 'element') {
+ return input;
+ }
}
});
+const MobileBox = styled(Box)`
+ display: inline-grid;
+ vertical-align: center;
+ align-items: stretch;
+ position: relative;
+ justify-content: flex-start;
+ width: 100%;
+
+ &:after,
+ textarea {
+ grid-area: 2 / 1;
+ width: auto;
+ min-width: 1em;
+ font: inherit;
+ padding: 0.25em;
+ margin: 0;
+ resize: none;
+ background: none;
+ appearance: none;
+ border: none;
+ }
+ &::after {
+ content: attr(data-value) ' ';
+ visibility: hidden;
+ white-space: pre-wrap;
+ }
+`;
+
export default class ChatEditor extends Component {
constructor(props) {
super(props);
@@ -161,31 +193,49 @@ export default class ChatEditor extends Component {
alignItems='center'
flexGrow='1'
height='100%'
+ paddingTop={MOBILE_BROWSER_REGEX.test(navigator.userAgent) ? '16px' : '0'}
+ paddingBottom={MOBILE_BROWSER_REGEX.test(navigator.userAgent) ? '16px' : '0'}
maxHeight='224px'
width='calc(100% - 88px)'
className={inCodeMode ? 'chat code' : 'chat'}
color="black"
>
{MOBILE_BROWSER_REGEX.test(navigator.userAgent)
- ? {
- if (event.key === 'Enter') {
- this.submit();
- } else {
+ ? {
+ if (this.editor) {
+ this.editor.element.focus();
+ }
+ }}
+ >
+ {
this.messageChange(null, null, event.target.value);
- }
- }}
- ref={input => {
- if (!input) return;
- this.editor = inputProxy(input);
- }}
- {...props}
- />
+ }}
+ onKeyDown={event => {
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ this.submit();
+ } else {
+ this.messageChange(null, null, event.target.value);
+ }
+ }}
+ ref={input => {
+ if (!input) return;
+ this.editor = inputProxy(input);
+ }}
+ {...props}
+ />
+
:
{content.code.expression}
diff --git a/pkg/interface/src/views/apps/chat/components/content/text.js b/pkg/interface/src/views/apps/chat/components/content/text.js
index d5c5c58af..2fe98c6f8 100644
--- a/pkg/interface/src/views/apps/chat/components/content/text.js
+++ b/pkg/interface/src/views/apps/chat/components/content/text.js
@@ -2,6 +2,7 @@ import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import ReactMarkdown from 'react-markdown';
import RemarkDisableTokenizers from 'remark-disable-tokenizers';
+import RemarkBreaks from 'remark-breaks';
import urbitOb from 'urbit-ob';
import { Text } from '@tlon/indigo-react';
@@ -26,10 +27,10 @@ const DISABLED_INLINE_TOKENS = [
const renderers = {
inlineCode: ({language, value}) => {
- return {value}
+ return {value}
},
paragraph: ({ children }) => {
- return ({children});
+ return ({children});
},
code: ({language, value}) => {
return
@@ -51,6 +53,9 @@ const MessageMarkdown = React.memo(props => (
{...props}
unwrapDisallowed={true}
renderers={renderers}
+ // shim until we uncover why RemarkBreaks and
+ // RemarkDisableTokenizers can't be loaded simultaneously
+ disallowedTypes={['heading', 'list', 'listItem', 'link']}
allowNode={(node, index, parent) => {
if (
node.type === 'blockquote'
@@ -65,10 +70,7 @@ const MessageMarkdown = React.memo(props => (
return true;
}}
- plugins={[[
- RemarkDisableTokenizers,
- { block: DISABLED_BLOCK_TOKENS, inline: DISABLED_INLINE_TOKENS }
- ]]} />
+ plugins={[RemarkBreaks]} />
));
diff --git a/pkg/interface/src/views/apps/chat/css/custom.css b/pkg/interface/src/views/apps/chat/css/custom.css
index 898422b79..702106082 100644
--- a/pkg/interface/src/views/apps/chat/css/custom.css
+++ b/pkg/interface/src/views/apps/chat/css/custom.css
@@ -277,9 +277,6 @@ pre.CodeMirror-placeholder.CodeMirror-line-like { color: var(--gray); }
/* dark */
@media (prefers-color-scheme: dark) {
- body {
- background-color: #333;
- }
.bg-black-d {
background-color: black;
}
diff --git a/pkg/interface/src/views/apps/launch/app.js b/pkg/interface/src/views/apps/launch/app.js
index bd1877a1c..7861d905f 100644
--- a/pkg/interface/src/views/apps/launch/app.js
+++ b/pkg/interface/src/views/apps/launch/app.js
@@ -1,5 +1,4 @@
import React, { useState } from 'react';
-import Helmet from 'react-helmet';
import styled from 'styled-components';
import { Box, Row, Icon, Text } from '@tlon/indigo-react';
@@ -14,6 +13,7 @@ import ModalButton from './components/ModalButton';
import { writeText } from '~/logic/lib/util';
import { NewGroup } from "~/views/landscape/components/NewGroup";
import { JoinGroup } from "~/views/landscape/components/JoinGroup";
+import { Helmet } from 'react-helmet';
const ScrollbarLessBox = styled(Box)`
scrollbar-width: none !important;
@@ -25,13 +25,38 @@ const ScrollbarLessBox = styled(Box)`
export default function LaunchApp(props) {
const [hashText, setHashText] = useState(props.baseHash);
-
+ const hashBox = (
+ {
+ writeText(props.baseHash);
+ setHashText('copied');
+ setTimeout(() => {
+ setHashText(props.baseHash);
+ }, 2000);
+ }}
+ >
+ {hashText || props.baseHash}
+
+ );
return (
<>
-
- OS1 - Home
+
+ { props.notificationsCount ? `(${String(props.notificationsCount) }) `: '' }Landscape
-
+
- DMs + Drafts
+ DMs + Drafts
@@ -77,36 +102,15 @@ export default function LaunchApp(props) {
icon="CreateGroup"
bg="green"
color="#fff"
- text="Create a Group"
+ text="Create Group"
>
+ {hashBox}
- {
- writeText(props.baseHash);
- setHashText('copied');
- setTimeout(() => {
- setHashText(props.baseHash);
- }, 2000);
- }}
- >
- {hashText || props.baseHash}
-
+ {hashBox}
>
);
}
diff --git a/pkg/interface/src/views/apps/launch/components/tiles/basic.js b/pkg/interface/src/views/apps/launch/components/tiles/basic.js
index 6206dd78b..d90dc29b0 100644
--- a/pkg/interface/src/views/apps/launch/components/tiles/basic.js
+++ b/pkg/interface/src/views/apps/launch/components/tiles/basic.js
@@ -20,8 +20,8 @@ export default class BasicTile extends React.PureComponent {
size='12px'
display='inline-block'
verticalAlign='top'
- pt='5px'
- pr='2px'
+ mt='5px'
+ mr='2'
/>
: null
}{props.title}
diff --git a/pkg/interface/src/views/apps/launch/components/tiles/weather.js b/pkg/interface/src/views/apps/launch/components/tiles/weather.js
index 68e91f815..7e409edd8 100644
--- a/pkg/interface/src/views/apps/launch/components/tiles/weather.js
+++ b/pkg/interface/src/views/apps/launch/components/tiles/weather.js
@@ -171,7 +171,7 @@ export default class WeatherTile extends React.Component {
onClick={() => this.setState({ manualEntry: !this.state.manualEntry })}
>
-
+
Weather
@@ -217,15 +217,14 @@ export default class WeatherTile extends React.Component {
title={`${locationName} Weather`}
>
-
- Weather
+
this.setState({ manualEntry: !this.state.manualEntry })
}
>
- ->
+ Weather ->
@@ -268,7 +267,7 @@ export default class WeatherTile extends React.Component {
flexDirection="column"
justifyContent="flex-start"
>
- Weather
+ Weather
Loading, please check again later...
diff --git a/pkg/interface/src/views/apps/launch/css/custom.css b/pkg/interface/src/views/apps/launch/css/custom.css
index 086247060..8df17cad1 100644
--- a/pkg/interface/src/views/apps/launch/css/custom.css
+++ b/pkg/interface/src/views/apps/launch/css/custom.css
@@ -40,12 +40,12 @@ button {
/* stolen from indigo-react reset.css
* TODO: remove and add reset.css properly
*/
-
+
@keyframes loadingSpinnerRotation {
from {
transform: rotate(0deg);
}
-
+
to {
transform: rotate(360deg);
}
@@ -53,9 +53,6 @@ button {
/* dark */
@media all and (prefers-color-scheme: dark) {
- body {
- background-color: #333;
- }
.bg-gray0-d {
background-color: #333;
}
diff --git a/pkg/interface/src/views/apps/links/LinkResource.tsx b/pkg/interface/src/views/apps/links/LinkResource.tsx
index 962871f03..0bf62c845 100644
--- a/pkg/interface/src/views/apps/links/LinkResource.tsx
+++ b/pkg/interface/src/views/apps/links/LinkResource.tsx
@@ -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 { Switch, Route, Link } from "react-router-dom";
import bigInt from 'big-integer';
@@ -10,10 +10,14 @@ import { RouteComponentProps } from "react-router-dom";
import { LinkItem } from "./components/LinkItem";
import LinkSubmit from "./components/LinkSubmit";
+import { LinkPreview } from "./components/link-preview";
+import { LinkWindow } from "./LinkWindow";
import { Comments } from "~/views/components/Comments";
import "./css/custom.css";
+const emptyMeasure = () => {};
+
type LinkResourceProps = StoreState & {
association: Association;
api: GlobalApi;
@@ -57,39 +61,28 @@ export function LinkResource(props: LinkResourceProps) {
return ;
}
+
return (
-
+
{
return (
-
-
-
-
- {Array.from(graph).map(([date, node]) => {
- const contact = contactDetails[node.post.author];
- return (
-
- );
- })}
-
+
);
}}
/>
@@ -112,6 +105,7 @@ export function LinkResource(props: LinkResourceProps) {
const contact = contactDetails[node.post.author];
return (
+
{"<- Back"}
+
);
}}
/>
diff --git a/pkg/interface/src/views/apps/links/LinkWindow.tsx b/pkg/interface/src/views/apps/links/LinkWindow.tsx
new file mode 100644
index 000000000..71ab000d6
--- /dev/null
+++ b/pkg/interface/src/views/apps/links/LinkWindow.tsx
@@ -0,0 +1,104 @@
+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();
+ 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'
+ }), []);
+
+ if (!first) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+ (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 (
+ <>
+
+
+
+
+ >
+ )
+ }
+ return ;
+ }}
+ loadRows={fetchLinks}
+ />
+ );
+}
diff --git a/pkg/interface/src/views/apps/links/components/LinkItem.tsx b/pkg/interface/src/views/apps/links/components/LinkItem.tsx
index e27e45365..83bfc2dc8 100644
--- a/pkg/interface/src/views/apps/links/components/LinkItem.tsx
+++ b/pkg/interface/src/views/apps/links/components/LinkItem.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useState, useEffect, useRef, useCallback } from 'react';
import { Link } from 'react-router-dom';
import { Row, Col, Anchor, Box, Text, Icon, Action } from '@tlon/indigo-react';
@@ -17,8 +17,9 @@ interface LinkItemProps {
api: GlobalApi;
group: Group;
path: string;
- contacts: Rolodex[];
+ contacts: Rolodex;
unreads: Unreads;
+ measure: (el: any) => void;
}
export const LinkItem = (props: LinkItemProps) => {
@@ -29,9 +30,12 @@ export const LinkItem = (props: LinkItemProps) => {
group,
path,
contacts,
+ measure,
...rest
} = props;
+ const ref = useRef(null);
+
const URLparser = new RegExp(
/((?:([\w\d\.-]+)\:\/\/?){1}(?:(www)\.?){0,1}(((?:[\w\d-]+\.)*)([\w\d-]+\.[\w\d]+))){1}(?:\:(\d+)){0,1}((\/(?:(?:[^\/\s\?]+\/)*))(?:([^\?\/\s#]+?(?:.[^\?\s]+){0,1}){0,1}(?:\?([^\s#]+)){0,1})){0,1}(?:#([^#\s]+)){0,1}/
);
@@ -70,9 +74,18 @@ export const LinkItem = (props: LinkItemProps) => {
const markRead = () => {
api.hark.markEachAsRead(props.association, '/', `/${index}`, 'link', 'link');
}
+
+
+ const onMeasure = useCallback(() => {
+ ref.current && measure(ref.current);
+ }, [ref.current, measure])
+
+ useEffect(() => {
+ onMeasure();
+ }, [onMeasure]);
+
return (
-
-
+
{
url={contents[1].url}
text={contents[0].text}
unfold={true}
+ onLoad={onMeasure}
style={{ alignSelf: 'center' }}
oembedProps={{
p: 2,
@@ -116,9 +130,9 @@ export const LinkItem = (props: LinkItemProps) => {
-
+
-
+
{
-
+
{
>
-
+
);
};
diff --git a/pkg/interface/src/views/apps/links/components/LinkSubmit.tsx b/pkg/interface/src/views/apps/links/components/LinkSubmit.tsx
index ab9a78b0f..2eaba4555 100644
--- a/pkg/interface/src/views/apps/links/components/LinkSubmit.tsx
+++ b/pkg/interface/src/views/apps/links/components/LinkSubmit.tsx
@@ -132,7 +132,6 @@ const LinkSubmit = (props: LinkSubmitProps) => {
position="absolute"
px={2}
pt={2}
- fontSize={0}
style={{ pointerEvents: 'none' }}
>{canUpload
? <>
@@ -180,7 +179,6 @@ const LinkSubmit = (props: LinkSubmitProps) => {
type="url"
pl={2}
width="100%"
- fontSize={0}
py={2}
color="black"
backgroundColor="transparent"
@@ -198,8 +196,8 @@ const LinkSubmit = (props: LinkSubmitProps) => {
pl={2}
backgroundColor="transparent"
width="100%"
- fontSize={0}
color="black"
+ fontSize={1}
style={{
resize: 'none',
height: 40
diff --git a/pkg/interface/src/views/apps/notifications/chat.tsx b/pkg/interface/src/views/apps/notifications/chat.tsx
deleted file mode 100644
index 87e870fcc..000000000
--- a/pkg/interface/src/views/apps/notifications/chat.tsx
+++ /dev/null
@@ -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 (
-
-
-
- {_.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 (
-
- {}}
- msg={content}
- isLastRead={false}
- group={group}
- contacts={groupContacts}
- fontSize='0'
- pt='2'
- />
-
- );
- })}
- {contents.length > 5 && (
-
- and {contents.length - 5} other message
- {contents.length > 6 ? "s" : ""}
-
- )}
-
-
- );
-}
diff --git a/pkg/interface/src/views/apps/notifications/graph.tsx b/pkg/interface/src/views/apps/notifications/graph.tsx
index 829f96bbc..8358be149 100644
--- a/pkg/interface/src/views/apps/notifications/graph.tsx
+++ b/pkg/interface/src/views/apps/notifications/graph.tsx
@@ -25,7 +25,7 @@ import ChatMessage, {MessageWithoutSigil} from "../chat/components/ChatMessage";
function getGraphModuleIcon(module: string) {
if (module === "link") {
- return "Links";
+ return "Collection";
}
return _.capitalize(module);
}
@@ -90,6 +90,8 @@ const GraphNodeContent = ({ group, post, contacts, mod, description, index, remo
content={contents}
group={group}
contacts={contacts}
+ fontSize='14px'
+ lineHeight="tall"
/>
} else if (idx[1] === "1") {
const [{ text: header }, { text: body }] = contents;
@@ -164,13 +166,14 @@ const GraphNode = ({
group,
read,
onRead,
+ showContact = false,
remoteContentPolicy
}) => {
const { contents } = post;
author = deSig(author);
const history = useHistory();
- const img = (
+ const img = showContact ? (
- );
+ ) : ;
const groupContacts = contacts[groupPath] ?? {};
@@ -192,10 +195,10 @@ const GraphNode = ({
}, [read, onRead]);
return (
-
+
{img}
-
{moment(time).format("HH:mm")}
-
-
+
}
+
{
- if (props.archived) {
+ if (props.archived || read) {
return;
}
- const func = read ? "unread" : "read";
- return api.hark[func](timebox, { graph: index });
+ return api.hark["read"](timebox, { graph: index });
}, [api, timebox, index, read]);
return (
-
+ <>
-
+
{_.map(contents, (content, idx) => (
))}
-
-
+
+ >
);
}
diff --git a/pkg/interface/src/views/apps/notifications/header.tsx b/pkg/interface/src/views/apps/notifications/header.tsx
index e930d9b19..b987a0693 100644
--- a/pkg/interface/src/views/apps/notifications/header.tsx
+++ b/pkg/interface/src/views/apps/notifications/header.tsx
@@ -1,5 +1,5 @@
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 _ from "lodash";
import moment from "moment";
@@ -71,12 +71,13 @@ export function Header(props: {
channel;
return (
-
+
{!props.archived && (
)}
@@ -84,13 +85,13 @@ export function Header(props: {
{authorDesc}
{description}
- {!!moduleIcon && }
- {!!channel && {channelTitle}}
-
+ {!!moduleIcon && }
+ {!!channel && {channelTitle}}
+
{groupTitle &&
<>
- {groupTitle}
-
+ {groupTitle}
+
>
}
diff --git a/pkg/interface/src/views/apps/notifications/inbox.tsx b/pkg/interface/src/views/apps/notifications/inbox.tsx
index 4b55a7e49..3bc95e7ec 100644
--- a/pkg/interface/src/views/apps/notifications/inbox.tsx
+++ b/pkg/interface/src/views/apps/notifications/inbox.tsx
@@ -1,18 +1,16 @@
-import React, { useEffect, useCallback } from "react";
+import React, { useEffect, useCallback, useRef, useState } from "react";
import f from "lodash/fp";
import _ from "lodash";
-import { Icon, Col, Row, Box, Text, Anchor, Rule } from "@tlon/indigo-react";
+import { Icon, Col, Row, Box, Text, Anchor, Rule, Center } from "@tlon/indigo-react";
import moment from "moment";
-import { Notifications, Rolodex, Timebox, IndexedNotification, Groups } from "~/types";
+import { Notifications, Rolodex, Timebox, IndexedNotification, Groups, GroupNotificationsConfig, NotificationGraphConfig } from "~/types";
import { MOMENT_CALENDAR_DATE, daToUnix, resourceAsPath } from "~/logic/lib/util";
import { BigInteger } from "big-integer";
import GlobalApi from "~/logic/api/global";
import { Notification } from "./notification";
import { Associations } from "~/types";
-import { cite } from '~/logic/lib/util';
-import { InviteItem } from '~/views/components/Invite';
-import { useWaitForProps } from "~/logic/lib/useWaitForProps";
-import { useHistory } from "react-router-dom";
+import {Invites} from "./invites";
+import {useLazyScroll} from "~/logic/lib/useLazyScroll";
type DatedTimebox = [BigInteger, Timebox];
@@ -45,10 +43,10 @@ export default function Inbox(props: {
contacts: Rolodex;
filter: string[];
invites: any;
+ notificationsGroupConfig: GroupNotificationsConfig;
+ notificationsGraphConfig: NotificationGraphConfig;
}) {
const { api, associations, invites } = props;
- const waiter = useWaitForProps(props)
- const history = useHistory();
useEffect(() => {
let seen = false;
setTimeout(() => {
@@ -61,109 +59,73 @@ export default function Inbox(props: {
};
}, []);
- const [newNotifications, ...notifications] =
+ const 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(
- f.map(([date, nots]) => [
+ let notificationsByDay = f.flow(
+ f.map(([date, nots]) => [
date,
nots.filter(filterNotification(associations, props.filter)),
]),
- f.groupBy(([date]) =>
- moment(daToUnix(date)).format("DDMMYYYY")
- ),
- f.values,
- f.reverse
+ f.groupBy(([d]) => {
+ const date = moment(daToUnix(d));
+ if (moment().subtract(6, 'hours').isBefore(date)) {
+ return 'latest';
+ } else {
+ return date.format("YYYYMMDD");
+ }
+ }),
)(notifications);
- useEffect(() => {
- api.hark.getMore(props.showArchive);
- }, [props.showArchive]);
+ const notificationsByDayMap = new Map(
+ Object.keys(notificationsByDay).map(timebox => {
+ return [timebox, notificationsByDay[timebox]];
+ })
+ );
- const onScroll = useCallback((e) => {
- let container = e.target;
- const { scrollHeight, scrollTop, clientHeight } = container;
- if((scrollHeight - scrollTop) < 1.5 * clientHeight) {
- api.hark.getMore(props.showArchive);
- }
- }, [props.showArchive]);
+ const scrollRef = useRef(null);
- const acceptInvite = (app: string, uid: string) => async (invite) => {
- const resource = {
- ship: `~${invite.resource.ship}`,
- name: invite.resource.name
- };
+ const loadMore = useCallback(async () => {
+ return api.hark.getMore();
+ }, [api]);
- const resourcePath = resourceAsPath(invite.resource);
- if(app === 'contacts') {
- await api.contacts.join(resource);
- await waiter(p => resourcePath in p.associations?.contacts);
- await api.invite.accept(app, uid);
- history.push(`/~landscape${resourcePath}`);
- } else if ( app === 'chat') {
- await api.invite.accept(app, uid);
- history.push(`/~landscape/home/resource/chat${resourcePath.slice(5)}`);
- } else if ( app === 'graph') {
- await api.invite.accept(app, uid);
- history.push(`/~graph/join${resourcePath}`);
- }
- };
+ const loadedAll = useLazyScroll(scrollRef, 0.2, loadMore);
- const inviteItems = (invites, api) => {
- const returned = [];
- Object.keys(invites).map((appKey) => {
- const app = invites[appKey];
- Object.keys(app).map((uid) => {
- const invite = app[uid];
- const inviteItem =
- api.invite.decline(appKey, uid)}
- />;
- returned.push(inviteItem);
- });
- });
- return returned;
- };
return (
-
-
- {inviteItems(invites, api)}
-
- {newNotifications && (
-
- )}
- {_.map(
- notificationsByDay,
- (timeboxes, idx) =>
- timeboxes.length > 0 && (
-
- )
+
+
+ {[...notificationsByDayMap.keys()].sort().reverse().map((day, index) => {
+ const timeboxes = notificationsByDayMap.get(day)!;
+ return timeboxes.length > 0 && (
+
+ );
+ })}
+ {loadedAll && (
+
+ No more notifications
+
)}
);
@@ -181,21 +143,17 @@ function sortIndexedNotification(
}
function DaySection({
+ label,
contacts,
groups,
archive,
timeboxes,
- latest = false,
associations,
api,
groupConfig,
graphConfig,
- 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);
if (lent === 0 || timeboxes.length === 0) {
return null;
@@ -203,23 +161,22 @@ function DaySection({
return (
<>
-
+
- {moment(daToUnix(timeboxes[0][0])).calendar(null, calendar)}
+ {label}
- {_.map(timeboxes.sort(sortTimeboxes), ([date, nots], i) =>
+ {_.map(timeboxes.sort(sortTimeboxes), ([date, nots], i: number) =>
_.map(nots.sort(sortIndexedNotification), (not, j: number) => (
{(i !== 0 || j !== 0) && (
-
+
)}
async () => {
+ const resource = {
+ ship: `~${invite.resource.ship}`,
+ name: invite.resource.name,
+ };
+
+ const resourcePath = resourceAsPath(invite.resource);
+ if (app === "contacts") {
+ await api.contacts.join(resource);
+ await waiter((p) => resourcePath in p.associations?.contacts);
+ await api.invite.accept(app, uid);
+ history.push(`/~landscape${resourcePath}`);
+ } else if (app === "graph") {
+ await api.invite.accept(app, uid);
+ history.push(`/~graph/join${resourcePath}`);
+ }
+ };
+
+ const declineInvite = useCallback(
+ (app: string, uid: string) => () => api.invite.decline(app, uid),
+ [api]
+ );
+
+ return (
+
+ {Object.keys(invites).reduce((items, appKey) => {
+ const app = invites[appKey];
+ let appItems = Object.keys(app).map((uid) => {
+ const invite = app[uid];
+ return (
+
+ );
+ });
+ return [...items, ...appItems];
+ }, [] as JSX.Element[])}
+
+ );
+}
diff --git a/pkg/interface/src/views/apps/notifications/notification.tsx b/pkg/interface/src/views/apps/notifications/notification.tsx
index 6f2878b2b..c9ca7455f 100644
--- a/pkg/interface/src/views/apps/notifications/notification.tsx
+++ b/pkg/interface/src/views/apps/notifications/notification.tsx
@@ -1,5 +1,5 @@
-import React, { ReactNode, useCallback, useMemo } from "react";
-import { Row, Box, Col, Text, Anchor, Icon, Action } from "@tlon/indigo-react";
+import React, { ReactNode, useCallback, useMemo, useState } from "react";
+import { Row, Box } from "@tlon/indigo-react";
import _ from "lodash";
import {
GraphNotificationContents,
@@ -7,7 +7,6 @@ import {
GroupNotificationContents,
NotificationGraphConfig,
GroupNotificationsConfig,
- NotifIndex,
Groups,
Associations,
Contacts,
@@ -17,8 +16,8 @@ import { getParentIndex } from "~/logic/lib/notification";
import { StatelessAsyncAction } from "~/views/components/StatelessAsyncAction";
import { GroupNotification } from "./group";
import { GraphNotification } from "./graph";
-import { ChatNotification } from "./chat";
import { BigInteger } from "big-integer";
+import { useHovering } from "~/logic/lib/util";
interface NotificationProps {
notification: IndexedNotification;
@@ -30,7 +29,6 @@ interface NotificationProps {
contacts: Contacts;
graphConfig: NotificationGraphConfig;
groupConfig: GroupNotificationsConfig;
- chatConfig: string[];
}
function getMuted(
@@ -55,9 +53,6 @@ function getMuted(
if ("group" in index) {
return _.findIndex(groups || [], (g) => g === index.group.group) === -1;
}
- if ("chat" in index) {
- return _.findIndex(chat || [], (c) => c === index.chat.chat) === -1;
- }
return false;
}
@@ -89,11 +84,21 @@ function NotificationWrapper(props: {
return api.hark[func](notif);
}, [notif, api, isMuted]);
+ const { hovering, bind } = useHovering();
+
const changeMuteDesc = isMuted ? "Unmute" : "Mute";
return (
-
+
{children}
-
+
{changeMuteDesc}
@@ -103,7 +108,7 @@ function NotificationWrapper(props: {
)}
-
+
);
}
@@ -166,26 +171,6 @@ export function Notification(props: NotificationProps) {
);
}
- if ("chat" in notification.index) {
- const index = notification.index.chat;
- const c: ChatNotificationContents = (contents as any).chat;
- return (
-
-
-
- );
- }
return null;
}
diff --git a/pkg/interface/src/views/apps/notifications/notifications.tsx b/pkg/interface/src/views/apps/notifications/notifications.tsx
index cac2ff16a..c4567cc09 100644
--- a/pkg/interface/src/views/apps/notifications/notifications.tsx
+++ b/pkg/interface/src/views/apps/notifications/notifications.tsx
@@ -2,6 +2,7 @@ import React, { useCallback, useState } from "react";
import _ from 'lodash';
import { Box, Col, Text, Row } from "@tlon/indigo-react";
import { Link, Switch, Route } from "react-router-dom";
+import Helmet from "react-helmet";
import { Body } from "~/views/components/Body";
import { PropFunc } from "~/types/util";
@@ -52,74 +53,79 @@ export default function NotificationsScreen(props: any) {
render={(routeProps) => {
const { view } = routeProps.match.params;
return (
-
-
-
- Updates
-
-
-
- Inbox
-
-
-
-
- Preferences
-
-
-
-
-
-
-
-
- }
+ <>
+
+ { props.notificationsCount ? `(${String(props.notificationsCount) }) `: '' }Landscape - Notifications
+
+
+
+
-
-
- Filter:
-
- {groupFilterDesc}
-
-
-
- {view === "preferences" && (
-
- )}
- {!view && }
-
-
+ Updates
+
+
+
+ Inbox
+
+
+
+
+ Preferences
+
+
+
+
+
+
+
+
+ }
+ >
+
+
+ Filter:
+
+ {groupFilterDesc}
+
+
+
+ {view === "preferences" && (
+
+ )}
+ {!view && }
+
+
+ >
);
}}
/>
diff --git a/pkg/interface/src/views/apps/profile/components/Profile.tsx b/pkg/interface/src/views/apps/profile/components/Profile.tsx
index 9cac884b2..cb46b3514 100644
--- a/pkg/interface/src/views/apps/profile/components/Profile.tsx
+++ b/pkg/interface/src/views/apps/profile/components/Profile.tsx
@@ -29,7 +29,7 @@ export function Profile(props: any) {
const image = (!hideAvatars && contact?.avatar)
?
- : ;
+ : ;
return (
- {`~${ship}`}
+ {`${ship}`}
@@ -63,7 +63,7 @@ export function ViewProfile(props: any) {
- ) : null
+ ) : null
}
- {`~${ship} `}
+ {`${ship} `}
remains private
diff --git a/pkg/interface/src/views/apps/profile/profile.tsx b/pkg/interface/src/views/apps/profile/profile.tsx
index d271f9f9f..7baa1724d 100644
--- a/pkg/interface/src/views/apps/profile/profile.tsx
+++ b/pkg/interface/src/views/apps/profile/profile.tsx
@@ -15,7 +15,7 @@ export default function ProfileScreen(props: any) {
return (
<>
- OS1 - Profile
+ { props.notificationsCount ? `(${String(props.notificationsCount) }) `: '' }Landscape - Profile
= useRef();
diff --git a/pkg/interface/src/views/apps/publish/components/Note.tsx b/pkg/interface/src/views/apps/publish/components/Note.tsx
index 7035fa26c..3c71ecd9e 100644
--- a/pkg/interface/src/views/apps/publish/components/Note.tsx
+++ b/pkg/interface/src/views/apps/publish/components/Note.tsx
@@ -47,7 +47,7 @@ export function Note(props: NoteProps & RouteComponentProps) {
const noteId = bigInt(index[1]);
useEffect(() => {
api.hark.markEachAsRead(props.association, '/',`/${index[1]}/1/1`, 'note', 'publish');
- }, [props.association]);
+ }, [props.association, props.note]);
@@ -67,7 +67,7 @@ export function Note(props: NoteProps & RouteComponentProps) {
color="red"
ml={2}
onClick={deletePost}
- css={{ cursor: "pointer" }}
+ style={{ cursor: "pointer" }}
>
Delete
@@ -75,6 +75,13 @@ export function Note(props: NoteProps & RouteComponentProps) {
);
}
+ const windowRef = React.useRef(null);
+ useEffect(() => {
+ if (windowRef.current) {
+ windowRef.current.parentElement.scrollTop = 0;
+ }
+ }, [windowRef, note]);
+
return (
{"<- Notebook Index"}
diff --git a/pkg/interface/src/views/apps/publish/components/NotePreview.tsx b/pkg/interface/src/views/apps/publish/components/NotePreview.tsx
index 163556e9f..1820188c4 100644
--- a/pkg/interface/src/views/apps/publish/components/NotePreview.tsx
+++ b/pkg/interface/src/views/apps/publish/components/NotePreview.tsx
@@ -61,9 +61,9 @@ export function NotePreview(props: NotePreviewProps) {
overflow='hidden'
p='2'
>
- {title}
+ {title}
-
+
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 (
-
-
- {metadata?.title}
- by
-
- {showNickname ? contact?.nickname : ship}
-
-
- {isWriter && (
-
-
-
- )}
-
-
-
- OS1 - Terminal
+
+ { this.props.notificationsCount ? `(${String(this.props.notificationsCount) }) `: '' }Landscape
+