Merge branch 'master' into release/next-vere

This commit is contained in:
pilfer-pandex 2021-01-29 14:46:51 -08:00
commit 269ec2d596
23 changed files with 287 additions and 167 deletions

View File

@ -1,11 +1,8 @@
blank_issues_enabled: true
contact_links:
- name: Landscape design issue
url: https://github.com/urbit/landscape/issues/new?assignees=&labels=design+issue&template=report-a-design-issue.md&title=
about: Submit non-functionality, design-specific issues to the Landscape team here.
- name: Landscape feature request
url: https://github.com/urbit/landscape/issues/new?assignees=&labels=feature+request&template=feature_request.md&title=
about: Landscape is comprised of Tlon's user applications and client for Urbit. Submit Landscape feature requests here.
- name: Submit a Landscape issue
url: https://github.com/urbit/landscape/issues/new/choose
about: Issues with Landscape (Tlon's flagship client) should be filed at urbit/landscape. This includes groups, chats, collections, notebooks, and more.
- name: urbit-dev mailing list
url: https://groups.google.com/a/urbit.org/g/dev
about: Developer questions and discussions also take place on the urbit-dev mailing list.

View File

@ -1,39 +0,0 @@
---
name: Landscape bug report
about: 'Use this template to file a bug for any Landscape app: Chat, Publish, Links, Groups,
Weather or Clock'
title: ''
labels: landscape
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem. If possible, please also screenshot your browser's dev console. Here are [Chrome's docs](https://developers.google.com/web/tools/chrome-devtools/open) for using this feature.
**Desktop (please complete the following information):**
- OS: [e.g. MacOS 10.15.3]
- Browser [e.g. chrome, safari]
- Base hash of your urbit ship. Run `+trouble` in Dojo to see this.
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Base hash of your urbit ship. Run `+trouble` in Dojo to see this.
**Additional context**
Add any other context about the problem here.

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:271d575a87373f4ed73b195780973ed41cb72be21b428a645c42a49ab5f786ee
size 8873583
oid sha256:6b4b198b552066fdee2a694a3134bf641b20591bebda21aa90920f4107f04f20
size 9065500

View File

@ -5,7 +5,7 @@
/- glob
/+ default-agent, verb, dbug
|%
++ hash 0v7.ttn7o.50403.rf6oh.63hnc.hgpc9
++ hash 0v1.39us5.oj5a9.9as9u.od9db.0dipj
+$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))]
+$ all-states
$% state-0

View File

@ -726,7 +726,8 @@
$: %0
p=time
$= q
$% [%add-nodes =resource:store nodes=(tree [index:store tree-node])]
$% [%add-graph =resource:store =tree-graph mark=(unit ^mark) ow=?]
[%add-nodes =resource:store nodes=(tree [index:store tree-node])]
[%remove-nodes =resource:store indices=(tree index:store)]
[%add-signatures =uid:store signatures=tree-signatures]
[%remove-signatures =uid:store signatures=tree-signatures]
@ -806,6 +807,14 @@
^- logged-update:store
:+ %0 p.t
?- -.q.t
%add-graph
:* %add-graph
resource.q.t
(remake-graph tree-graph.q.t)
mark.q.t
ow.q.t
==
::
%add-nodes
:- %add-nodes
:- resource.q.t

View File

@ -24,6 +24,6 @@
<div id="portal-root"></div>
<script src="/~landscape/js/channel.js"></script>
<script src="/~landscape/js/session.js"></script>
<script src="/~landscape/js/bundle/index.7d4248944fe1255cb74b.js"></script>
<script src="/~landscape/js/bundle/index.5fdbe84c6b57646a6a6b.js"></script>
</body>
</html>

View File

@ -29,8 +29,6 @@
%contact-store
%contact-hook
%invite-store
%chat-store
%chat-hook
%graph-store
==
|= app=@tas

Binary file not shown.

View File

@ -18,7 +18,7 @@
"codemirror": "^5.59.2",
"css-loader": "^3.6.0",
"file-saver": "^2.0.5",
"formik": "^2.2.6",
"formik": "^2.1.5",
"immer": "^8.0.1",
"lodash": "^4.17.20",
"markdown-to-jsx": "^6.11.4",

View File

@ -196,10 +196,11 @@ export class HarkApi extends BaseApi<StoreState> {
});
}
getMore() {
async getMore(): Promise<boolean> {
const offset = this.store.state['notifications']?.size || 0;
const count = 3;
return this.getSubset(offset, count, false);
await this.getSubset(offset, count, false);
return offset === (this.store.state.notifications?.size || 0);
}
async getSubset(offset:number, count:number, isArchive: boolean) {

View File

@ -0,0 +1,51 @@
import { useEffect, RefObject, useRef, useState } from "react";
import _ from "lodash";
export function distanceToBottom(el: HTMLElement) {
const { scrollTop, scrollHeight, clientHeight } = el;
const scrolledPercent =
(scrollHeight - scrollTop - clientHeight) / scrollHeight;
return _.isNaN(scrolledPercent) ? 0 : scrolledPercent;
}
export function useLazyScroll(
ref: RefObject<HTMLElement>,
margin: number,
loadMore: () => Promise<boolean>
) {
const [isDone, setIsDone] = useState(false);
useEffect(() => {
if (!ref.current) {
return;
}
setIsDone(false);
const scroll = ref.current;
const loadUntil = (el: HTMLElement) => {
if (!isDone && distanceToBottom(el) < margin) {
return loadMore().then((done) => {
if (done) {
setIsDone(true);
return Promise.resolve();
}
return loadUntil(el);
});
}
return Promise.resolve();
};
loadUntil(scroll);
const onScroll = (e: Event) => {
const el = e.currentTarget! as HTMLElement;
loadUntil(el);
};
ref.current.addEventListener("scroll", onScroll);
return () => {
ref.current?.removeEventListener("scroll", onScroll);
};
}, [ref?.current]);
return isDone;
}

View File

@ -363,11 +363,19 @@ export function useShowNickname(contact: Contact | null, hide?: boolean): boolea
return !!(contact && contact.nickname && !hideNicknames);
}
export function useHovering() {
interface useHoveringInterface {
hovering: boolean;
bind: {
onMouseOver: () => void,
onMouseLeave: () => void
}
}
export const useHovering = (): useHoveringInterface => {
const [hovering, setHovering] = useState(false);
const bind = {
onMouseEnter: () => setHovering(true),
onMouseOver: () => setHovering(true),
onMouseLeave: () => setHovering(false)
};
return { hovering, bind };
}
};

View File

@ -40,7 +40,8 @@ const useLocalState = create<LocalState>(persist((set, get) => ({
}
})),
set: fn => set(produce(fn))
}), {
}), {
blacklist: ['suspendedFocus', 'toggleOmnibox', 'omniboxShown'],
name: 'localReducer'
}));
@ -55,4 +56,4 @@ function withLocalState<P, S extends keyof LocalState>(Component: any, stateMemb
});
}
export { useLocalState as default, withLocalState };
export { useLocalState as default, withLocalState };

View File

@ -1,3 +1,5 @@
import _ from 'lodash';
import BaseStore from './base';
import InviteReducer from '../reducers/invite-update';
import MetadataReducer from '../reducers/metadata-update';
@ -40,6 +42,18 @@ export default class GlobalStore extends BaseStore<StoreState> {
launchReducer = new LaunchReducer();
connReducer = new ConnectionReducer();
pastActions: Record<string, any> = {}
constructor() {
super();
(window as any).debugStore = this.debugStore.bind(this);
}
debugStore(tag: string, ...stateKeys: string[]) {
console.log(this.pastActions[tag]);
console.log(_.pick(this.state, stateKeys));
}
rehydrate() {
this.localReducer.rehydrate(this.state);
}
@ -94,6 +108,11 @@ export default class GlobalStore extends BaseStore<StoreState> {
}
reduce(data: Cage, state: StoreState) {
// debug shim
const tag = Object.keys(data)[0];
const oldActions = this.pastActions[tag] || [];
this.pastActions[tag] = [data[tag], ...oldActions.slice(0,14)];
this.inviteReducer.reduce(data, this.state);
this.metadataReducer.reduce(data, this.state);
this.localReducer.reduce(data, this.state);

View File

@ -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;
}

View File

@ -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<ChatMessageProps> {
className={containerClass}
style={style}
mb={1}
position="relative"
>
{dayBreak && !isLastRead ? <DayBreak when={msg['time-sent']} /> : null}
{renderSigil
@ -194,6 +195,8 @@ export const MessageWithSigil = (props) => {
}
};
const { hovering, bind } = useHovering();
return (
<>
<OverlaySigil
@ -206,9 +209,11 @@ export const MessageWithSigil = (props) => {
history={history}
api={api}
bg="white"
className="fl pr3 v-top pt1"
className="fl v-top pt1"
pr={3}
pl={2}
/>
<Box flexGrow={1} display='block' className="clamp-message">
<Box flexGrow={1} display='block' className="clamp-message" {...bind}>
<Box
flexShrink={0}
className="hide-child"
@ -231,8 +236,15 @@ export const MessageWithSigil = (props) => {
}}
title={`~${msg.author}`}
>{name}</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} fontSize={0} gray mono>{timestamp}</Text>
<Text
flexShrink={0}
fontSize={0}
gray
mono
ml={2}
display={['none', hovering ? 'block' : 'none']}
>{datestamp}</Text>
</Box>
<ContentBox flexShrink={0} fontSize={fontSize ? fontSize : '14px'}>
{msg.contents.map(c =>
@ -257,20 +269,40 @@ const ContentBox = styled(Box)`
`;
export const MessageWithoutSigil = ({ timestamp, contacts, msg, measure, group }) => (
<>
<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 }}>
{msg.contents.map((c, i) => (
<MessageContent
key={i}
contacts={contacts}
content={c}
group={group}
measure={measure}/>))}
</ContentBox>
</>
);
export const MessageWithoutSigil = ({ timestamp, contacts, msg, measure, group }) => {
const { hovering, bind } = useHovering();
return (
<>
<Text
flexShrink={0}
mono
gray
display={hovering ? 'block': 'none'}
pt='2px'
lineHeight='tall'
fontSize={0}
position="absolute"
left={1}
>{timestamp}</Text>
<ContentBox
flexShrink={0}
fontSize='14px'
className="clamp-message"
style={{ flexGrow: 1 }}
{...bind}
pl={6}
>
{msg.contents.map((c, i) => (
<MessageContent
key={i}
contacts={contacts}
content={c}
group={group}
measure={measure}/>))}
</ContentBox>
</>
)
};
export const MessageContent = ({ content, contacts, measure, fontSize, group }) => {
if ('code' in content) {
@ -292,7 +324,8 @@ export const MessageContent = ({ content, contacts, measure, fontSize, group })
}}
textProps={{style: {
fontSize: 'inherit',
textDecoration: 'underline'
borderBottom: '1px solid',
textDecoration: 'none'
}}}
/>
</Box>

View File

@ -53,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'
@ -67,11 +70,7 @@ const MessageMarkdown = React.memo(props => (
return true;
}}
plugins={[[
RemarkBreaks,
RemarkDisableTokenizers,
{ block: DISABLED_BLOCK_TOKENS, inline: DISABLED_INLINE_TOKENS }
]]} />
plugins={[RemarkBreaks]} />
));

View File

@ -60,7 +60,7 @@ const ModalButton = (props) => {
</Box>
</Box>
)}
<Box
<Button
onClick={() => setModalShown(true)}
display="flex"
alignItems="center"
@ -73,7 +73,7 @@ const ModalButton = (props) => {
{...rest}
>
<Icon icon={props.icon} mr={2} color={color}></Icon><Text color={color}>{props.text}</Text>
</Box>
</Button>
</>
);
}

View File

@ -48,10 +48,9 @@ export function LinkWindow(props: LinkWindowProps) {
}, [graph.size]);
const first = graph.peekLargest()?.[0];
const [,,ship, name] = association['app-path'].split('/');
const style = useMemo(() =>
const style = useMemo(() =>
({
height: "100%",
width: "100%",
@ -60,6 +59,14 @@ export function LinkWindow(props: LinkWindowProps) {
alignItems: 'center'
}), []);
if (!first) {
return (
<Col key={0} 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>
);
}
return (
<VirtualScroller
ref={(l) => (virtualList.current = l ?? undefined)}
@ -82,7 +89,7 @@ export function LinkWindow(props: LinkWindowProps) {
if(index.eq(first ?? bigInt.zero)) {
return (
<>
<Col key={index.toString()} mx="auto" mt="4" maxWidth="768px" width="100%" flexShrink='0' px={3}>
<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} />

View File

@ -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(() => {
@ -75,12 +73,12 @@ export default function Inbox(props: {
};
let notificationsByDay = f.flow(
f.map<DatedTimebox>(([date, nots]) => [
f.map<DatedTimebox, DatedTimebox>(([date, nots]) => [
date,
nots.filter(filterNotification(associations, props.filter)),
]),
f.groupBy<DatedTimebox>(([date]) => {
date = moment(daToUnix(date));
f.groupBy<DatedTimebox>(([d]) => {
const date = moment(daToUnix(d));
if (moment().subtract(6, 'hours').isBefore(date)) {
return 'latest';
} else {
@ -88,69 +86,27 @@ export default function Inbox(props: {
}
}),
)(notifications);
notificationsByDay = new Map(Object.keys(notificationsByDay).sort().reverse().map(timebox => {
return [timebox, notificationsByDay[timebox]];
}));
useEffect(() => {
api.hark.getMore(props.showArchive);
}, [props.showArchive]);
const notificationsByDayMap = new Map<string, DatedTimebox[]>(
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 =
<InviteItem
key={uid}
invite={invite}
onAccept={acceptInvite(appKey, uid)}
onDecline={() => api.invite.decline(appKey, uid)}
/>;
returned.push(inviteItem);
});
});
return returned;
};
return (
<Col position="relative" height="100%" overflowY="auto" onScroll={onScroll} >
<Col zIndex={4} gapY={2} bg="white" top="0px" position="sticky" flexShrink={0}>
{inviteItems(invites, api)}
</Col>
{[...notificationsByDay.keys()].map((day, index) => {
const timeboxes = notificationsByDay.get(day);
<Col ref={scrollRef} position="relative" height="100%" overflowY="auto">
<Invites invites={invites} api={api} associations={associations} />
{[...notificationsByDayMap.keys()].sort().reverse().map((day, index) => {
const timeboxes = notificationsByDayMap.get(day)!;
return timeboxes.length > 0 && (
<DaySection
key={day}
@ -163,10 +119,14 @@ export default function Inbox(props: {
groups={props.groups}
graphConfig={props.notificationsGraphConfig}
groupConfig={props.notificationsGroupConfig}
chatConfig={props.notificationsChatConfig}
/>
);
})}
{loadedAll && (
<Center mt="2" borderTop={notifications.length !== 0 ? 1 : 0} borderTopColor="washedGray" width="100%" height="96px">
<Text gray fontSize="1">No more notifications</Text>
</Center>
)}
</Col>
);
}
@ -192,7 +152,6 @@ function DaySection({
api,
groupConfig,
graphConfig,
chatConfig,
}) {
const lent = timeboxes.map(([,nots]) => nots.length).reduce(f.add, 0);
@ -202,23 +161,22 @@ function DaySection({
return (
<>
<Box position="sticky" zIndex="3" top="-1px" bg="white">
<Box position="sticky" zIndex={3} top="-1px" bg="white">
<Box p="2" bg="scales.black05">
<Text>
{label}
</Text>
</Box>
</Box>
{_.map(timeboxes.sort(sortTimeboxes), ([date, nots], i) =>
{_.map(timeboxes.sort(sortTimeboxes), ([date, nots], i: number) =>
_.map(nots.sort(sortIndexedNotification), (not, j: number) => (
<React.Fragment key={j}>
{(i !== 0 || j !== 0) && (
<Box flexShrink="0" height="4px" bg="scales.black05" />
<Box flexShrink={0} height="4px" bg="scales.black05" />
)}
<Notification
graphConfig={graphConfig}
groupConfig={groupConfig}
chatConfig={chatConfig}
api={api}
associations={associations}
notification={not}

View File

@ -0,0 +1,74 @@
import React, { useCallback } from "react";
import { Box, Row, Col } from "@tlon/indigo-react";
import GlobalApi from "~/logic/api/global";
import { Invites as IInvites, Associations, Invite } from "~/types";
import { resourceAsPath } from "~/logic/lib/util";
import { useHistory } from "react-router-dom";
import { useWaitForProps } from "~/logic/lib/useWaitForProps";
import InviteItem from "~/views/components/Invite";
interface InvitesProps {
api: GlobalApi;
invites: IInvites;
associations: Associations;
}
export function Invites(props: InvitesProps) {
const { api, invites } = props;
const history = useHistory();
const waiter = useWaitForProps(props);
const acceptInvite = (
app: string,
uid: string,
invite: Invite
) => 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 (
<Col
zIndex={4}
gapY={2}
bg="white"
top="0px"
position="sticky"
flexShrink={0}
>
{Object.keys(invites).reduce((items, appKey) => {
const app = invites[appKey];
let appItems = Object.keys(app).map((uid) => {
const invite = app[uid];
return (
<InviteItem
key={uid}
invite={invite}
onAccept={acceptInvite(appKey, uid, invite)}
onDecline={declineInvite(appKey, uid)}
/>
);
});
return [...items, ...appItems];
}, [] as JSX.Element[])}
</Col>
);
}

View File

@ -29,7 +29,6 @@ interface NotificationProps {
contacts: Contacts;
graphConfig: NotificationGraphConfig;
groupConfig: GroupNotificationsConfig;
chatConfig: string[];
}
function getMuted(

View File

@ -90,6 +90,8 @@ class OverlaySigil extends PureComponent<OverlaySigilProps, OverlaySigilState> {
api,
sigilClass,
hideAvatars,
pr = 0,
pl = 0,
...rest
} = this.props;
@ -113,6 +115,8 @@ class OverlaySigil extends PureComponent<OverlaySigilProps, OverlaySigilState> {
onClick={this.profileShow}
ref={this.containerRef}
className={className}
pr={pr}
pl={pl}
>
{state.clicked && (
<ProfileOverlay