Merge pull request #4936 from urbit/lf/read-graph-group

hark: read all in group, graph
This commit is contained in:
Liam Fitzgerald 2021-06-11 09:47:10 +10:00 committed by GitHub
commit 96daff6b7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 202 additions and 192 deletions

View File

@ -422,7 +422,11 @@
%read-note (read-note +.in)
::
%seen-index (seen-index +.in)
::
%remove-graph (remove-graph +.in)
%read-graph (read-graph +.in)
%read-group (read-group +.in)
::
%set-dnd (set-dnd +.in)
%seen seen
%read-all read-all
@ -566,10 +570,53 @@
(~(put by last-seen) stats-index new-time)
(give %seen-index new-time stats-index)
::
++ get-stats-indices
|= rid=resource
%- ~(gas ^in *(set stats-index:store))
%+ skim
;: weld
~(tap ^in ~(key by unreads-count))
~(tap ^in ~(key by last-seen))
~(tap ^in ~(key by unreads-each))
==
|= =stats-index:store
?. ?=(%graph -.stats-index) %.n
=(graph.stats-index rid)
::
++ read-all-each
|= =stats-index:store
=/ refs=(list index:graph-store)
~(tap ^in (~(get ju unreads-each) stats-index))
|-
?~ refs poke-core
$(refs t.refs, poke-core (read-each stats-index i.refs))
::
++ read-graph
|= rid=resource
=/ indices=(list stats-index:store)
~(tap ^in (get-stats-indices rid))
|-
?~ indices poke-core
=* index i.indices
=? poke-core (~(has by unreads-count) index)
(read-count i.indices)
=? poke-core (~(has by unreads-each) index)
(read-all-each i.indices)
$(indices t.indices)
::
++ read-group
|= rid=resource
=/ graphs=(list resource)
(graphs-of-group:met rid)
|-
?~ graphs poke-core
=/ core=_poke-core (read-graph i.graphs)
$(graphs t.graphs, poke-core core)
::
++ remove-graph
|= rid=resource
|^
=/ indices get-stats-indices
=/ indices (get-stats-indices rid)
=. poke-core
(give %remove-graph rid)
=. poke-core
@ -583,18 +630,6 @@
((dif-map-by-key ,@da) last-seen indices)
poke-core
::
++ get-stats-indices
%- ~(gas ^in *(set stats-index:store))
%+ skim
;: weld
~(tap ^in ~(key by unreads-count))
~(tap ^in ~(key by last-seen))
~(tap ^in ~(key by unreads-each))
==
|= =stats-index:store
?. ?=(%graph -.stats-index) %.n
=(graph.stats-index rid)
::
++ dif-map-by-key
|* value=mold
|= [=(map stats-index:store value) =(set stats-index:store)]

View File

@ -314,6 +314,8 @@
add-note+add
set-dnd+bo
read-count+stats-index
read-graph+dejs-path:resource
read-group+dejs-path:resource
read-each+read-graph-index
read-all+ul
==

View File

@ -100,4 +100,13 @@
^- (unit resource)
%+ bind (peek-association md-resource)
|=(association:store group)
::
++ graphs-of-group
|= group=resource
=/ =associations:store
(metadata-for-group group)
%+ murn ~(tap in ~(key by associations))
|= [=app-name:store rid=resource]
?.(=(%graph app-name) ~ `rid)
--

View File

@ -44,6 +44,9 @@
[%read-note =index]
::
[%seen-index time=@da =stats-index]
::
[%read-graph =resource]
[%read-group =resource]
[%remove-graph =resource]
::
[%read-all ~]
@ -281,6 +284,7 @@
[%unread-note time=@da index]
::
[%seen-index time=@da =stats-index]
::
[%remove-graph =resource]
::
[%read-all ~]

View File

@ -1,10 +1,9 @@
import { Association, GraphNotifDescription, IndexedNotification, NotifIndex } from '@urbit/api';
import BigIntOrderedMap from '@urbit/api/lib/BigIntOrderedMap';
import { BigInteger } from 'big-integer';
import { getParentIndex } from '../lib/notification';
import { dateToDa, decToUd } from '../lib/util';
import {reduce} from '../reducers/hark-update';
import {doOptimistically, optReduceState} from '../state/base';
import { reduce } from '../reducers/hark-update';
import { doOptimistically } from '../state/base';
import useHarkState from '../state/hark';
import { StoreState } from '../store/type';
import BaseApi from './base';
@ -62,7 +61,7 @@ export class HarkApi extends BaseApi<StoreState> {
index
}
};
await doOptimistically(useHarkState, action, this.harkAction.bind(this), [reduce])
await doOptimistically(useHarkState, action, this.harkAction.bind(this), [reduce]);
}
read(time: BigInteger, index: NotifIndex) {
@ -81,6 +80,18 @@ export class HarkApi extends BaseApi<StoreState> {
return this.actOnNotification('unread-note', time, index);
}
readGroup(group: string) {
return this.harkAction({
'read-group': group
});
}
readGraph(graph: string) {
return this.harkAction({
'read-graph': graph
});
}
dismissReadCount(graph: string, index: string) {
return this.harkAction({
'read-count': {

View File

@ -5,7 +5,7 @@ import React, {
useEffect,
useMemo,
useRef,
useState,
useState
} from 'react';
import _ from 'lodash';
import { getChord } from '~/logic/lib/util';
@ -13,10 +13,9 @@ import { getChord } from '~/logic/lib/util';
type Handler = (e: KeyboardEvent) => void;
const fallback: ShortcutContextProps = {
add: () => {},
remove: () => {},
remove: () => {}
};
export const ShortcutContext = createContext(fallback);
export interface ShortcutContextProps {
add: (cb: (e: KeyboardEvent) => void, key: string) => void;
@ -27,19 +26,19 @@ export function ShortcutContextProvider({ children }) {
const handlerRef = useRef<Handler>(() => {});
const add = useCallback((cb: Handler, key: string) => {
setShortcuts((s) => ({ ...s, [key]: cb }));
setShortcuts(s => ({ ...s, [key]: cb }));
}, []);
const remove = useCallback((cb: Handler, key: string) => {
setShortcuts((s) => (key in s ? _.omit(s, key) : s));
setShortcuts(s => (key in s ? _.omit(s, key) : s));
}, []);
useEffect(() => {
function onKeypress(e: KeyboardEvent) {
handlerRef.current(e);
}
document.addEventListener('keypress', onKeypress);
document.addEventListener('keydown', onKeypress);
return () => {
document.removeEventListener('keypress', onKeypress);
document.removeEventListener('keydown', onKeypress);
};
}, []);
@ -50,7 +49,7 @@ export function ShortcutContextProvider({ children }) {
};
}, [shortcuts]);
const value = useMemo(() => ({ add, remove }), [add, remove])
const value = useMemo(() => ({ add, remove }), [add, remove]);
return (
<ShortcutContext.Provider value={value}>

View File

@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react';
import { DragEvent, useCallback, useEffect, useState } from 'react';
function validateDragEvent(e: DragEvent): FileList | File[] | true | null {
const files: File[] = [];
@ -37,7 +37,7 @@ export function useFileDrag(dragged: (f: FileList | File[], e: DragEvent) => voi
const [dragging, setDragging] = useState(false);
const onDragEnter = useCallback(
(e: DragEvent) => {
(e: DragEvent<HTMLDivElement>) => {
if (!validateDragEvent(e)) {
return;
}
@ -47,7 +47,7 @@ export function useFileDrag(dragged: (f: FileList | File[], e: DragEvent) => voi
);
const onDrop = useCallback(
(e: DragEvent) => {
(e: DragEvent<HTMLDivElement>) => {
setDragging(false);
const files = validateDragEvent(e);
if (!files || files === true) {
@ -60,7 +60,7 @@ export function useFileDrag(dragged: (f: FileList | File[], e: DragEvent) => voi
);
const onDragOver = useCallback(
(e: DragEvent) => {
(e: DragEvent<HTMLDivElement>) => {
if (!validateDragEvent(e)) {
return;
}

View File

@ -1,9 +1,9 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import _ from 'lodash';
import { patp2dec } from 'urbit-ob';
import f, { compose, memoize } from 'lodash/fp';
import f from 'lodash/fp';
import { Association, Contact, Patp } from '@urbit/api';
import produce, { enableMapSet } from 'immer';
import { enableMapSet } from 'immer';
import useSettingsState from '../state/settings';
/* eslint-disable max-lines */
import anyAscii from 'any-ascii';
@ -50,7 +50,7 @@ export function parentPath(path: string) {
return _.dropRight(path.split('/'), 1).join('/');
}
/**
/*
* undefined -> initial
* null -> disabled feed
* string -> enabled feed
@ -67,23 +67,26 @@ export function getFeedPath(association: Association): string | null | undefined
}
export const getChord = (e: KeyboardEvent) => {
let chord = [e.key];
const chord = [e.key];
if(e.metaKey) {
chord.unshift('meta');
}
if(e.ctrlKey) {
chord.unshift('ctrl');
}
if(e.shiftKey) {
chord.unshift('shift');
}
return chord.join('+');
}
};
export function getResourcePath(workspace: Workspace, path: string, joined: boolean, mod: string) {
const base = workspace.type === 'group'
? `/~landscape${workspace.group}`
: workspace.type === 'home'
? `/~landscape/home`
: `/~landscape/messages`;
return `${base}/${joined ? 'resource' : 'join'}/${mod}${path}`
? '/~landscape/home'
: '/~landscape/messages';
return `${base}/${joined ? 'resource' : 'join'}/${mod}${path}`;
}
const DA_UNIX_EPOCH = bigInt('170141184475152167957503069145530368000'); // `@ud` ~1970.1.1
@ -135,14 +138,14 @@ export function decToUd(str: string): string {
);
}
/**
/*
* Clamp a number between a min and max
*/
export function clamp(x: number, min: number, max: number) {
return Math.max(min, Math.min(max, x));
}
/**
/*
* Euclidean modulo
*/
export function modulo(x: number, mod: number) {
@ -355,6 +358,7 @@ export function stringToTa(str: string) {
add = '~~';
break;
default:
// eslint-disable-next-line
const charCode = str.charCodeAt(i);
if (
(charCode >= 97 && charCode <= 122) || // a-z
@ -413,7 +417,7 @@ export function stringToSymbol(str: string) {
}
return result;
}
/**
/*
* Formats a numbers as a `@ud` inserting dot where needed
*/
export function numToUd(num: number) {
@ -428,7 +432,7 @@ export function numToUd(num: number) {
}
export function patpToUd(patp: Patp) {
return numToUd(patp2dec(patp))
return numToUd(patp2dec(patp));
}
export function usePreventWindowUnload(shouldPreventDefault: boolean, message = 'You have unsaved changes. Are you sure you want to exit?') {
@ -443,7 +447,7 @@ export function usePreventWindowUnload(shouldPreventDefault: boolean, message =
window.onbeforeunload = handleBeforeUnload;
return () => {
window.removeEventListener('beforeunload', handleBeforeUnload);
// @ts-ignore
// @ts-ignore need better window typings
window.onbeforeunload = undefined;
};
}, [shouldPreventDefault]);
@ -484,8 +488,8 @@ export function withHovering<T>(Component: React.ComponentType<T>) {
return React.forwardRef((props, ref) => {
const { hovering, bind } = useHovering();
// @ts-ignore needs type signature on return?
return <Component ref={ref} hovering={hovering} bind={bind} {...props} />
})
return <Component ref={ref} hovering={hovering} bind={bind} {...props} />;
});
}
const DM_REGEX = /ship\/~([a-z]|-)*\/dm--/;
@ -500,14 +504,14 @@ export function getItemTitle(association: Association): string {
return association.metadata.title ?? association.resource ?? '';
}
export const svgDataURL = (svg) => 'data:image/svg+xml;base64,' + btoa(svg);
export const svgDataURL = svg => 'data:image/svg+xml;base64,' + btoa(svg);
export const svgBlobURL = (svg) => URL.createObjectURL(new Blob([svg], { type: 'image/svg+xml' }));
export const svgBlobURL = svg => URL.createObjectURL(new Blob([svg], { type: 'image/svg+xml' }));
export const favicon = () => {
let background = '#ffffff';
const contacts = useContactState.getState().contacts;
if (contacts.hasOwnProperty(`~${window.ship}`)) {
if (Object.prototype.hasOwnProperty.call(contacts, `~${window.ship}`)) {
background = `#${uxToHex(contacts[`~${window.ship}`].color)}`;
}
const foreground = foregroundFromBackground(background);
@ -518,4 +522,4 @@ export const favicon = () => {
colors: [background, foreground]
});
return svg;
}
};

View File

@ -1,7 +1,7 @@
import { NotificationGraphConfig, Timebox, Unreads, dateToDa } from "@urbit/api";
import { NotificationGraphConfig, Timebox, Unreads } from '@urbit/api';
import { patp2dec } from 'urbit-ob';
import BigIntOrderedMap from "@urbit/api/lib/BigIntOrderedMap";
import {useCallback} from "react";
import BigIntOrderedMap from '@urbit/api/lib/BigIntOrderedMap';
import { useCallback } from 'react';
// import { harkGraphHookReducer, harkGroupHookReducer, harkReducer } from "~/logic/subscription/hark";
import { createState } from './base';
@ -70,7 +70,7 @@ const useHarkState = createState<HarkState>('Hark', {
}, ['unreadNotes', 'notifications', 'archivedNotifications', 'unreads', 'notificationsCount']);
export function useHarkDm(ship: string) {
return useHarkState(useCallback(s => {
return useHarkState(useCallback((s) => {
return s.unreads.graph[`/ship/~${window.ship}/dm-inbox`]?.[`/${patp2dec(ship)}`];
}, [ship]));
}

View File

@ -10,6 +10,7 @@ export interface ShortcutMapping {
navForward: string;
navBack: string;
hideSidebar: string;
readGroup: string;
}
export interface SettingsState extends BaseState<SettingsState> {
@ -77,7 +78,8 @@ const useSettingsState = createState<SettingsState>('Settings', {
cycleBack: 'ctrl+;',
navForward: 'ctrl+]',
navBack: 'ctrl+[',
hideSidebar: 'ctrl+\\'
hideSidebar: 'ctrl+\\',
readGroup: 'shift+Escape'
}
});

View File

@ -78,6 +78,7 @@ class App extends React.Component {
this.store.setStateHandler(this.setState.bind(this));
this.state = this.store.state;
// eslint-disable-next-line
this.appChannel = new window.channel();
this.api = new GlobalApi(this.ship, this.appChannel, this.store);
gcpManager.configure(this.api);
@ -103,7 +104,7 @@ class App extends React.Component {
this.updateTheme(this.themeWatcher);
}, 500);
this.api.local.getBaseHash();
this.api.local.getRuntimeLag(); //TODO consider polling periodically
this.api.local.getRuntimeLag(); // TODO consider polling periodically
this.api.settings.getAll();
gcpManager.start();
Mousetrap.bindGlobal(['command+/', 'ctrl+/'], (e) => {

View File

@ -136,7 +136,6 @@ export function ChatPane(props: ChatPaneProps): ReactElement {
}
return (
// @ts-ignore
<Col {...bind} height="100%" overflow="hidden" position="relative">
<ShareProfile
our={ourContact}

View File

@ -1,6 +1,6 @@
import { BaseInput, Box, Button, LoadingSpinner, Text } from '@tlon/indigo-react';
import { hasProvider } from 'oembed-parser';
import React, { useCallback, useState } from 'react';
import React, { useCallback, useState, DragEvent, useEffect } from 'react';
import GlobalApi from '~/logic/api/global';
import { createPost } from '~/logic/api/graph';
import { parsePermalink, permalinkToReference } from '~/logic/lib/permalinks';
@ -21,34 +21,11 @@ const LinkSubmit = (props: LinkSubmitProps) => {
const [submitFocused, setSubmitFocused] = useState(false);
const [urlFocused, setUrlFocused] = useState(false);
const [linkValue, setLinkValueHook] = useState('');
const [linkValue, setLinkValue] = useState('');
const [linkTitle, setLinkTitle] = useState('');
const [disabled, setDisabled] = useState(false);
const [linkValid, setLinkValid] = useState(false);
const doPost = () => {
const url = linkValue;
const text = linkTitle ? linkTitle : linkValue;
const contents = url.startsWith('web+urbitgraph:/')
? [{ text }, permalinkToReference(parsePermalink(url)!)]
: [{ text }, { url }];
setDisabled(true);
const parentIndex = props.parentIndex || '';
const post = createPost(contents, parentIndex);
props.api.graph.addPost(
`~${props.ship}`,
props.name,
post
).then(() => {
setDisabled(false);
setLinkValue('');
setLinkTitle('');
setLinkValid(false);
});
};
const validateLink = (link) => {
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}/
@ -95,6 +72,33 @@ const LinkSubmit = (props: LinkSubmitProps) => {
return link;
};
useEffect(() => {
setLinkValid(validateLink(linkValue));
}, [linkValue]);
const doPost = () => {
const url = linkValue;
const text = linkTitle ? linkTitle : linkValue;
const contents = url.startsWith('web+urbitgraph:/')
? [{ text }, permalinkToReference(parsePermalink(url)!)]
: [{ text }, { url }];
setDisabled(true);
const parentIndex = props.parentIndex || '';
const post = createPost(contents, parentIndex);
props.api.graph.addPost(
`~${props.ship}`,
props.name,
post
).then(() => {
setDisabled(false);
setLinkValue('');
setLinkTitle('');
setLinkValid(false);
});
};
const onFileDrag = useCallback(
(files: FileList | File[], e: DragEvent): void => {
if (!canUpload) {
@ -107,17 +111,6 @@ const LinkSubmit = (props: LinkSubmitProps) => {
const { bind, dragging } = useFileDrag(onFileDrag);
const onLinkChange = (linkValue: string) => {
setLinkValueHook(linkValue);
const link = validateLink(linkValue);
setLinkValid(link);
};
const setLinkValue = (linkValue: string) => {
onLinkChange(linkValue);
setLinkValueHook(linkValue);
};
const onPaste = useCallback(
(event: ClipboardEvent) => {
if (!event.clipboardData || !event.clipboardData.files.length) {
@ -192,7 +185,7 @@ const LinkSubmit = (props: LinkSubmitProps) => {
py={2}
color="black"
backgroundColor="transparent"
onChange={e => onLinkChange(e.target.value)}
onChange={e => setLinkValue(e.target.value)}
onBlur={() => [setUrlFocused(false), setSubmitFocused(false)]}
onFocus={() => [setUrlFocused(true), setSubmitFocused(true)]}
spellCheck="false"

View File

@ -42,7 +42,7 @@ interface GroupNotificationProps {
}
export function GroupNotification(props: GroupNotificationProps): ReactElement {
const { contents, index, read, time, api, timebox } = props;
const { contents, index, time } = props;
const authors = _.flatten(_.map(contents, getGroupUpdateParticipants));

View File

@ -16,7 +16,7 @@ import GlobalApi from '~/logic/api/global';
import { getNotificationKey } from '~/logic/lib/hark';
import { useLazyScroll } from '~/logic/lib/useLazyScroll';
import useLaunchState from '~/logic/state/launch';
import { daToUnix, MOMENT_CALENDAR_DATE } from '~/logic/lib/util';
import { daToUnix } from '~/logic/lib/util';
import useHarkState from '~/logic/state/hark';
import { Invites } from './invites';
import { Notification } from './notification';
@ -72,16 +72,6 @@ export default function Inbox(props: {
const notifications =
Array.from(props.showArchive ? archivedNotifications : notificationState) || [];
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<DatedTimebox, DatedTimebox>(([date, nots]) => [
date,

View File

@ -1,16 +1,16 @@
import React, { ReactElement, ReactNode } from 'react';
import React, { ReactElement } from 'react';
import _ from 'lodash';
import {
Invite,
AppInvites,
JoinRequest,
JoinRequest
} from '@urbit/api';
import GlobalApi from '~/logic/api/global';
import { alphabeticalOrder, resourceAsPath } from '~/logic/lib/util';
import useInviteState from '~/logic/state/invite';
import useGraphState from '~/logic/state/graph';
import {PendingDm} from './PendingDm';
import { PendingDm } from './PendingDm';
import InviteItem from '~/views/components/Invite';
interface InvitesProps {
@ -26,9 +26,9 @@ interface InviteRef {
export function Invites(props: InvitesProps): ReactElement {
const { api } = props;
const invites = useInviteState((state) => state.invites);
const invites = useInviteState(state => state.invites);
const pendingDms = useGraphState((s) => s.pendingDms) ?? [];
const pendingDms = useGraphState(s => s.pendingDms) ?? [];
const inviteArr: InviteRef[] = _.reduce(
invites,
@ -49,13 +49,12 @@ export function Invites(props: InvitesProps): ReactElement {
const invitesAndStatus: { [rid: string]: JoinRequest | InviteRef } = {
..._.keyBy(inviteArr, ({ invite }) => resourceAsPath(invite.resource)),
...pendingJoin,
...pendingJoin
};
return (
<>
{[...pendingDms].map((ship) => (
{[...pendingDms].map(ship => (
<PendingDm key={ship} api={api} ship={`~${ship}`} />
))}
{Object.keys(invitesAndStatus)

View File

@ -4,18 +4,14 @@ import {
GroupNotificationContents,
GroupNotificationsConfig, IndexedNotification,
IndexedNotification
NotificationGraphConfig
} from '@urbit/api';
import { BigInteger } from 'big-integer';
import _ from 'lodash';
import React, { ReactNode, useCallback } from 'react';
import GlobalApi from '~/logic/api/global';
import { getNotificationKey } from '~/logic/lib/hark';
import { getParentIndex } from '~/logic/lib/notification';
import { useHovering } from '~/logic/lib/util';
import useHarkState from '~/logic/state/hark';
import useLocalState from '~/logic/state/local';
import { StatelessAsyncAction } from '~/views/components/StatelessAsyncAction';
import { SwipeMenu } from '~/views/components/SwipeMenu';
@ -29,32 +25,6 @@ export interface NotificationProps {
unread: boolean;
}
function getMuted(
idxNotif: IndexedNotification,
groups: GroupNotificationsConfig,
graphs: NotificationGraphConfig
) {
const { index, notification } = idxNotif;
if ('graph' in idxNotif.index) {
const { graph } = idxNotif.index.graph;
if (!('graph' in notification.contents)) {
throw new Error();
}
const parent = getParentIndex(idxNotif.index.graph, notification.contents.graph);
return (
_.findIndex(
graphs?.watching || [],
g => g.graph === graph && g.index === parent
) === -1
);
}
if ('group' in index) {
return _.findIndex(groups || [], g => g === index.group.group) === -1;
}
return false;
}
export function NotificationWrapper(props: {
api: GlobalApi;
time?: BigInteger;
@ -74,12 +44,6 @@ export function NotificationWrapper(props: {
return api.hark.archive(time, notification.index);
}, [time, notification]);
const groupConfig = useHarkState(state => state.notificationsGroupConfig);
const graphConfig = useHarkState(state => state.notificationsGraphConfig);
const isMuted =
time && notification && getMuted(notification, groupConfig, graphConfig);
const onClick = (e: any) => {
if (!notification || read) {
return;

View File

@ -169,13 +169,10 @@ export function ProfileActions(props: any): ReactElement {
}
export function Profile(props: any): ReactElement | null {
const { hideAvatars } = useSettingsState(selectCalmState);
const history = useHistory();
const nackedContacts = useContactState(state => state.nackedContacts);
const { contact, hasLoaded, isEdit, ship } = props;
const nacked = nackedContacts.has(ship);
const formRef = useRef(null);
useEffect(() => {
if (hasLoaded && !contact && !nacked) {
@ -183,8 +180,6 @@ export function Profile(props: any): ReactElement | null {
}
}, [hasLoaded, contact]);
const anchorRef = useRef<HTMLElement | null>(null);
if (!props.ship) {
return null;
}

View File

@ -5,7 +5,7 @@ import 'codemirror/addon/edit/continuelist';
import 'codemirror/lib/codemirror.css';
import 'codemirror/mode/markdown/markdown';
import { useFormikContext } from 'formik';
import React, { useCallback, useRef } from 'react';
import React, { useCallback, useRef, DragEvent } from 'react';
import { UnControlled as CodeEditor } from 'react-codemirror2';
import { Prompt } from 'react-router-dom';
import { useFileDrag } from '~/logic/lib/useDrag';
@ -61,13 +61,6 @@ export function MarkdownEditor(
[onChange]
);
const handleBlur = useCallback(
(_i, e: any) => {
onBlur && onBlur(e);
},
[onBlur]
);
const { uploadDefault, canUpload } = useStorage();
const onFileDrag = useCallback(
@ -110,10 +103,10 @@ export function MarkdownEditor(
value={value}
options={options}
onChange={handleChange}
onDragLeave={(editor, e: DragEvent) => bind.onDragLeave(e)}
onDragOver={(editor, e: DragEvent) => bind.onDragOver(e)}
onDrop={(editor, e: DragEvent) => bind.onDrop(e)}
onDragEnter={(editor, e: DragEvent) => bind.onDragEnter(e)}
onDragLeave={(editor, e: any) => bind.onDragLeave(e)}
onDragOver={(editor, e: any) => bind.onDragOver(e)}
onDrop={(editor, e: any) => bind.onDrop(e)}
onDragEnter={(editor, e: any) => bind.onDragEnter(e)}
/>
{dragging && <SubmitDragger />}
</Box>

View File

@ -1,7 +1,8 @@
import { Box, Col, Row, Text } from '@tlon/indigo-react';
import { Box, Button, Col, Row, Text } from '@tlon/indigo-react';
import { Association, Graph } from '@urbit/api';
import React, { ReactElement } from 'react';
import React, { ReactElement, useCallback } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import GlobalApi from '~/logic/api/global';
import { useShowNickname } from '~/logic/lib/util';
import useContactState from '~/logic/state/contact';
import useGroupState from '~/logic/state/group';
@ -14,6 +15,7 @@ interface NotebookProps {
association: Association;
baseUrl: string;
rootUrl: string;
api: GlobalApi;
}
export function Notebook(props: NotebookProps & RouteComponentProps): ReactElement | null {
@ -21,19 +23,23 @@ export function Notebook(props: NotebookProps & RouteComponentProps): ReactEleme
ship,
book,
association,
graph
graph,
api
} = props;
const groups = useGroupState(state => state.groups);
const contacts = useContactState(state => state.contacts);
const group = groups[association?.group];
const relativePath = (p: string) => props.baseUrl + p;
const contact = contacts?.[`~${ship}`];
const showNickname = useShowNickname(contact);
const readBook = useCallback(() => {
api.hark.readGraph(association.resource);
}, [association.resource]);
if (!group) {
return null; // Waiting on groups to populate
}
@ -48,6 +54,7 @@ export function Notebook(props: NotebookProps & RouteComponentProps): ReactEleme
{showNickname ? contact?.nickname : ship}
</Text>
</Box>
<Button onClick={readBook}>Mark all as Read</Button>
</Row>
<Box borderBottom={1} borderBottomColor="lightGray" />
<NotebookPosts

View File

@ -32,7 +32,9 @@ const StoreDebugger = (props: StoreDebuggerProps) => {
let output: any = false;
try {
output = _.get(state, filterToTry, undefined);
} catch (e) { }
} catch (e) {
console.log('filter failed');
}
if (output) {
console.log(output);
setText(objectToString(output));

View File

@ -8,7 +8,7 @@ import GlobalApi from '~/logic/api/global';
import { getChord } from '~/logic/lib/util';
import useSettingsState, {
selectSettingsState,
ShortcutMapping,
ShortcutMapping
} from '~/logic/state/settings';
import { AsyncButton } from '~/views/components/AsyncButton';
import { BackButton } from './BackButton';
@ -108,6 +108,7 @@ export default function ShortcutSettings(props: ShortcutSettingsProps) {
label="Cycle backward through channel list"
/>
<ChordInput id="hideSidebar" label="Show/hide group sidebar" />
<ChordInput id="readGroup" label="Read all in a group" />
</Box>
<AsyncButton primary width="fit-content">Save Changes</AsyncButton>
</Col>

View File

@ -12,7 +12,7 @@ import { LeapSettings } from './components/lib/LeapSettings';
import { NotificationPreferences } from './components/lib/NotificationPref';
import S3Form from './components/lib/S3Form';
import SecuritySettings from './components/lib/Security';
import {DmSettings} from './components/lib/DmSettings';
import { DmSettings } from './components/lib/DmSettings';
import ShortcutSettings from './components/lib/ShortcutSettings';
export const Skeleton = (props: { children: ReactNode }) => (

View File

@ -1,4 +1,4 @@
import { Anchor, Box, Text } from '@tlon/indigo-react';
import { Anchor, Text } from '@tlon/indigo-react';
import { Contact, Group } from '@urbit/api';
import React from 'react';
import ReactMarkdown, { ReactMarkdownProps } from 'react-markdown';
@ -6,7 +6,6 @@ import RemarkDisableTokenizers from 'remark-disable-tokenizers';
import { isValidPatp } from 'urbit-ob';
import GlobalApi from '~/logic/api/global';
import { deSig } from '~/logic/lib/util';
import {PropFunc} from '~/types';
import { PermalinkEmbed } from '~/views/apps/permalinks/embed';
import { Mention } from '~/views/components/MentionText';
import RemoteContent from '~/views/components/RemoteContent';
@ -49,7 +48,7 @@ type RichTextProps = ReactMarkdownProps & {
py?: number;
overflowX?: any;
verticalAlign?: any;
};
};
const RichText = React.memo(({ disableRemoteContent = false, api, ...props }: RichTextProps) => (
<ReactMarkdown

View File

@ -1,18 +1,19 @@
import { AppName } from '@urbit/api';
import _ from 'lodash';
import React, { useEffect } from 'react';
import React, { useCallback, useEffect } from 'react';
import Helmet from 'react-helmet';
import {
Route,
RouteComponentProps, Switch
} from 'react-router-dom';
import GlobalApi from '~/logic/api/global';
import { useShortcut } from '~/logic/state/settings';
import { useLocalStorageState } from '~/logic/lib/useLocalStorageState';
import { getGroupFromWorkspace } from '~/logic/lib/workspace';
import useGroupState from '~/logic/state/group';
import useHarkState from '~/logic/state/hark';
import useMetadataState from '~/logic/state/metadata';
import {DmResource} from '~/views/apps/chat/DmResource';
import { DmResource } from '~/views/apps/chat/DmResource';
import { StoreState } from '~/logic/store/type';
import { Workspace } from '~/types/workspace';
import '~/views/apps/links/css/custom.css';
@ -42,6 +43,12 @@ export function GroupsPane(props: GroupsPaneProps) {
const groupPath = getGroupFromWorkspace(workspace);
const groups = useGroupState(state => state.groups);
useShortcut('readGroup', useCallback(() => {
if(groupPath) {
api.hark.readGroup(groupPath);
}
}, [groupPath, api]));
const groupAssociation =
(groupPath && associations.groups[groupPath]) || undefined;
const group = (groupPath && groups[groupPath]) || undefined;
@ -56,7 +63,7 @@ export function GroupsPane(props: GroupsPaneProps) {
}
return () => {
setRecentGroups(gs => _.uniq([workspace.group, ...gs]));
}
};
}, [workspace]);
if (!(associations && (groupPath ? groupPath in groups : true))) {
@ -180,7 +187,6 @@ export function GroupsPane(props: GroupsPaneProps) {
<Route
path={relativePath('/new')}
render={(routeProps) => {
const newUrl = `${baseUrl}/new`;
return (
<Skeleton mobileHide recentGroups={recentGroups} {...props} baseUrl={baseUrl}>
<NewChannel

View File

@ -21,7 +21,7 @@ import { Dropdown } from '~/views/components/Dropdown';
import { FormikOnBlur } from '~/views/components/FormikOnBlur';
import { NewChannel } from '~/views/landscape/components/NewChannel';
import { SidebarListConfig } from './types';
import {getFeedPath} from '~/logic/lib/util';
import { getFeedPath } from '~/logic/lib/util';
export function SidebarListHeader(props: {
api: GlobalApi;
@ -53,7 +53,7 @@ export function SidebarListHeader(props: {
const noun = (props.workspace?.type === 'messages') ? 'Messages' : 'Channels';
let feedPath = groupPath ? getFeedPath(associations.groups[groupPath]) : undefined;
const feedPath = groupPath ? getFeedPath(associations.groups[groupPath]) : undefined;
const unreadCount = useHarkState(
s => s.unreads?.graph?.[feedPath ?? '']?.['/']?.unreads as number ?? 0
@ -61,7 +61,7 @@ export function SidebarListHeader(props: {
return (
<Box>
{( !!feedPath) ? (
{( feedPath) ? (
<Row
flexShrink={0}
alignItems="center"

View File

@ -1,18 +1,14 @@
import { Box } from '@tlon/indigo-react';
import { PatpNoSig } from '@urbit/api';
import moment from 'moment';
import React, { ReactElement, useCallback, useEffect } from 'react';
import React from 'react';
import Helmet from 'react-helmet';
import { Route, RouteComponentProps, Switch } from 'react-router-dom';
import { Route, Switch } from 'react-router-dom';
import GlobalApi from '~/logic/api/global';
import { cite } from '~/logic/lib/util';
import useGraphState from '~/logic/state/graph';
import useHarkState from '~/logic/state/hark';
import { StoreState } from '~/logic/store/type';
import GlobalSubscription from '~/logic/subscription/global';
import { Workspace } from '~/types/workspace';
import { Body } from '../components/Body';
import { Loading } from '../components/Loading';
import { GroupsPane } from './components/GroupsPane';
import { JoinGroup } from './components/JoinGroup';
import { NewGroup } from './components/NewGroup';
@ -43,11 +39,10 @@ type LandscapeProps = StoreState & {
ship: PatpNoSig;
api: GlobalApi;
subscription: GlobalSubscription;
notificationsCount: number;
}
export default function Landscape(props) {
const notificationsCount = useHarkState(s => s.notificationsCount);
export default function Landscape(props: LandscapeProps) {
return (
<>
<Helmet defer={false}>