diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 0f1200711..ed0ab9842 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -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. diff --git a/.github/ISSUE_TEMPLATE/os1-bug-report.md b/.github/ISSUE_TEMPLATE/os1-bug-report.md deleted file mode 100644 index b6800a7a7..000000000 --- a/.github/ISSUE_TEMPLATE/os1-bug-report.md +++ /dev/null @@ -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. diff --git a/bin/solid.pill b/bin/solid.pill index 7e7658c6d..4b6217cc4 100644 --- a/bin/solid.pill +++ b/bin/solid.pill @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:271d575a87373f4ed73b195780973ed41cb72be21b428a645c42a49ab5f786ee -size 8873583 +oid sha256:6b4b198b552066fdee2a694a3134bf641b20591bebda21aa90920f4107f04f20 +size 9065500 diff --git a/pkg/arvo/app/glob.hoon b/pkg/arvo/app/glob.hoon index 293574f2a..09b64b587 100644 --- a/pkg/arvo/app/glob.hoon +++ b/pkg/arvo/app/glob.hoon @@ -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 diff --git a/pkg/arvo/app/graph-store.hoon b/pkg/arvo/app/graph-store.hoon index 12baed69c..db6daabd0 100644 --- a/pkg/arvo/app/graph-store.hoon +++ b/pkg/arvo/app/graph-store.hoon @@ -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 diff --git a/pkg/arvo/app/landscape/index.html b/pkg/arvo/app/landscape/index.html index a50a2c7fa..51923fbaa 100644 --- a/pkg/arvo/app/landscape/index.html +++ b/pkg/arvo/app/landscape/index.html @@ -24,6 +24,6 @@
- + diff --git a/pkg/arvo/app/lens.hoon b/pkg/arvo/app/lens.hoon index 64fb2737c..66165e19e 100644 --- a/pkg/arvo/app/lens.hoon +++ b/pkg/arvo/app/lens.hoon @@ -29,8 +29,6 @@ %contact-store %contact-hook %invite-store - %chat-store - %chat-hook %graph-store == |= app=@tas diff --git a/pkg/interface/package-lock.json b/pkg/interface/package-lock.json index cdadefa5f..d4a06d132 100644 Binary files a/pkg/interface/package-lock.json and b/pkg/interface/package-lock.json differ diff --git a/pkg/interface/package.json b/pkg/interface/package.json index b8c18d79d..a65578c43 100644 --- a/pkg/interface/package.json +++ b/pkg/interface/package.json @@ -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", diff --git a/pkg/interface/src/logic/api/hark.ts b/pkg/interface/src/logic/api/hark.ts index 498665bab..96111757c 100644 --- a/pkg/interface/src/logic/api/hark.ts +++ b/pkg/interface/src/logic/api/hark.ts @@ -196,10 +196,11 @@ export class HarkApi extends BaseApi { }); } - getMore() { + async getMore(): Promise { 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) { diff --git a/pkg/interface/src/logic/lib/useLazyScroll.ts b/pkg/interface/src/logic/lib/useLazyScroll.ts new file mode 100644 index 000000000..f9e8c10eb --- /dev/null +++ b/pkg/interface/src/logic/lib/useLazyScroll.ts @@ -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, + margin: number, + loadMore: () => Promise +) { + 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; +} diff --git a/pkg/interface/src/logic/lib/util.ts b/pkg/interface/src/logic/lib/util.ts index ba2bc67c8..da7ffa7fc 100644 --- a/pkg/interface/src/logic/lib/util.ts +++ b/pkg/interface/src/logic/lib/util.ts @@ -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 }; -} \ No newline at end of file +}; diff --git a/pkg/interface/src/logic/state/local.tsx b/pkg/interface/src/logic/state/local.tsx index 408bd2bdd..8b3260904 100644 --- a/pkg/interface/src/logic/state/local.tsx +++ b/pkg/interface/src/logic/state/local.tsx @@ -40,7 +40,8 @@ const useLocalState = create(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/logic/store/store.ts b/pkg/interface/src/logic/store/store.ts index 56720d4d9..5b5ecf171 100644 --- a/pkg/interface/src/logic/store/store.ts +++ b/pkg/interface/src/logic/store/store.ts @@ -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 { launchReducer = new LaunchReducer(); connReducer = new ConnectionReducer(); + pastActions: Record = {} + + 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 { } 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); 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/apps/chat/components/ChatMessage.tsx b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx index c1ff5f630..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) { @@ -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/content/text.js b/pkg/interface/src/views/apps/chat/components/content/text.js index 98094c31e..2fe98c6f8 100644 --- a/pkg/interface/src/views/apps/chat/components/content/text.js +++ b/pkg/interface/src/views/apps/chat/components/content/text.js @@ -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]} /> )); diff --git a/pkg/interface/src/views/apps/launch/components/ModalButton.tsx b/pkg/interface/src/views/apps/launch/components/ModalButton.tsx index aa17ae40c..1bc62dcb0 100644 --- a/pkg/interface/src/views/apps/launch/components/ModalButton.tsx +++ b/pkg/interface/src/views/apps/launch/components/ModalButton.tsx @@ -60,7 +60,7 @@ const ModalButton = (props) => { )} - setModalShown(true)} display="flex" alignItems="center" @@ -73,7 +73,7 @@ const ModalButton = (props) => { {...rest} > {props.text} - + ); } diff --git a/pkg/interface/src/views/apps/links/LinkWindow.tsx b/pkg/interface/src/views/apps/links/LinkWindow.tsx index abc85d672..71ab000d6 100644 --- a/pkg/interface/src/views/apps/links/LinkWindow.tsx +++ b/pkg/interface/src/views/apps/links/LinkWindow.tsx @@ -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 ( + + + + ); + } + return ( (virtualList.current = l ?? undefined)} @@ -82,7 +89,7 @@ export function LinkWindow(props: LinkWindowProps) { if(index.eq(first ?? bigInt.zero)) { return ( <> - + diff --git a/pkg/interface/src/views/apps/notifications/inbox.tsx b/pkg/interface/src/views/apps/notifications/inbox.tsx index bdf366e1b..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(() => { @@ -75,12 +73,12 @@ export default function Inbox(props: { }; let notificationsByDay = f.flow( - f.map(([date, nots]) => [ + f.map(([date, nots]) => [ date, nots.filter(filterNotification(associations, props.filter)), ]), - f.groupBy(([date]) => { - date = moment(daToUnix(date)); + f.groupBy(([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( + 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)} - - {[...notificationsByDay.keys()].map((day, index) => { - const timeboxes = notificationsByDay.get(day); + + + {[...notificationsByDayMap.keys()].sort().reverse().map((day, index) => { + const timeboxes = notificationsByDayMap.get(day)!; return timeboxes.length > 0 && ( ); })} + {loadedAll && ( +
+ No more notifications +
+ )} ); } @@ -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 ( <> - + {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 248ffc6e3..c9ca7455f 100644 --- a/pkg/interface/src/views/apps/notifications/notification.tsx +++ b/pkg/interface/src/views/apps/notifications/notification.tsx @@ -29,7 +29,6 @@ interface NotificationProps { contacts: Contacts; graphConfig: NotificationGraphConfig; groupConfig: GroupNotificationsConfig; - chatConfig: string[]; } function getMuted( diff --git a/pkg/interface/src/views/components/OverlaySigil.tsx b/pkg/interface/src/views/components/OverlaySigil.tsx index b05b9ebb1..b2567596d 100644 --- a/pkg/interface/src/views/components/OverlaySigil.tsx +++ b/pkg/interface/src/views/components/OverlaySigil.tsx @@ -90,6 +90,8 @@ class OverlaySigil extends PureComponent { api, sigilClass, hideAvatars, + pr = 0, + pl = 0, ...rest } = this.props; @@ -113,6 +115,8 @@ class OverlaySigil extends PureComponent { onClick={this.profileShow} ref={this.containerRef} className={className} + pr={pr} + pl={pl} > {state.clicked && (