mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-11-13 08:38:43 +03:00
Merge pull request #4936 from urbit/lf/read-graph-group
hark: read all in group, graph
This commit is contained in:
commit
96daff6b7f
@ -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)]
|
||||
|
@ -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
|
||||
==
|
||||
|
@ -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)
|
||||
|
||||
--
|
||||
|
@ -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 ~]
|
||||
|
@ -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': {
|
||||
|
@ -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}>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
@ -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]));
|
||||
}
|
||||
|
@ -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'
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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) => {
|
||||
|
@ -136,7 +136,6 @@ export function ChatPane(props: ChatPaneProps): ReactElement {
|
||||
}
|
||||
|
||||
return (
|
||||
// @ts-ignore
|
||||
<Col {...bind} height="100%" overflow="hidden" position="relative">
|
||||
<ShareProfile
|
||||
our={ourContact}
|
||||
|
@ -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"
|
||||
|
@ -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));
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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));
|
||||
|
@ -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>
|
||||
|
@ -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 }) => (
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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}>
|
||||
|
Loading…
Reference in New Issue
Block a user