mirror of
https://github.com/ilyakooo0/urbit.git
synced 2025-01-07 07:30:23 +03:00
Merge remote-tracking branch 'origin/master' into pp/wire
This commit is contained in:
commit
0f069a08e8
9
.github/ISSUE_TEMPLATE/config.yml
vendored
9
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,11 +1,8 @@
|
|||||||
blank_issues_enabled: true
|
blank_issues_enabled: true
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Landscape design issue
|
- name: Submit a Landscape issue
|
||||||
url: https://github.com/urbit/landscape/issues/new?assignees=&labels=design+issue&template=report-a-design-issue.md&title=
|
url: https://github.com/urbit/landscape/issues/new/choose
|
||||||
about: Submit non-functionality, design-specific issues to the Landscape team here.
|
about: Issues with Landscape (Tlon's flagship client) should be filed at urbit/landscape. This includes groups, chats, collections, notebooks, and more.
|
||||||
- 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: urbit-dev mailing list
|
- name: urbit-dev mailing list
|
||||||
url: https://groups.google.com/a/urbit.org/g/dev
|
url: https://groups.google.com/a/urbit.org/g/dev
|
||||||
about: Developer questions and discussions also take place on the urbit-dev mailing list.
|
about: Developer questions and discussions also take place on the urbit-dev mailing list.
|
||||||
|
39
.github/ISSUE_TEMPLATE/os1-bug-report.md
vendored
39
.github/ISSUE_TEMPLATE/os1-bug-report.md
vendored
@ -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.
|
|
@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:271d575a87373f4ed73b195780973ed41cb72be21b428a645c42a49ab5f786ee
|
oid sha256:6b4b198b552066fdee2a694a3134bf641b20591bebda21aa90920f4107f04f20
|
||||||
size 8873583
|
size 9065500
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
/- glob
|
/- glob
|
||||||
/+ default-agent, verb, dbug
|
/+ 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))]
|
+$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))]
|
||||||
+$ all-states
|
+$ all-states
|
||||||
$% state-0
|
$% state-0
|
||||||
|
@ -726,7 +726,8 @@
|
|||||||
$: %0
|
$: %0
|
||||||
p=time
|
p=time
|
||||||
$= q
|
$= 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)]
|
[%remove-nodes =resource:store indices=(tree index:store)]
|
||||||
[%add-signatures =uid:store signatures=tree-signatures]
|
[%add-signatures =uid:store signatures=tree-signatures]
|
||||||
[%remove-signatures =uid:store signatures=tree-signatures]
|
[%remove-signatures =uid:store signatures=tree-signatures]
|
||||||
@ -806,6 +807,14 @@
|
|||||||
^- logged-update:store
|
^- logged-update:store
|
||||||
:+ %0 p.t
|
:+ %0 p.t
|
||||||
?- -.q.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
|
||||||
:- %add-nodes
|
:- %add-nodes
|
||||||
:- resource.q.t
|
:- resource.q.t
|
||||||
|
@ -24,6 +24,6 @@
|
|||||||
<div id="portal-root"></div>
|
<div id="portal-root"></div>
|
||||||
<script src="/~landscape/js/channel.js"></script>
|
<script src="/~landscape/js/channel.js"></script>
|
||||||
<script src="/~landscape/js/session.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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -29,8 +29,6 @@
|
|||||||
%contact-store
|
%contact-store
|
||||||
%contact-hook
|
%contact-hook
|
||||||
%invite-store
|
%invite-store
|
||||||
%chat-store
|
|
||||||
%chat-hook
|
|
||||||
%graph-store
|
%graph-store
|
||||||
==
|
==
|
||||||
|= app=@tas
|
|= app=@tas
|
||||||
|
3106
pkg/interface/package-lock.json
generated
3106
pkg/interface/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -18,7 +18,7 @@
|
|||||||
"codemirror": "^5.59.2",
|
"codemirror": "^5.59.2",
|
||||||
"css-loader": "^3.6.0",
|
"css-loader": "^3.6.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"formik": "^2.2.6",
|
"formik": "^2.1.5",
|
||||||
"immer": "^8.0.1",
|
"immer": "^8.0.1",
|
||||||
"lodash": "^4.17.20",
|
"lodash": "^4.17.20",
|
||||||
"markdown-to-jsx": "^6.11.4",
|
"markdown-to-jsx": "^6.11.4",
|
||||||
|
@ -196,10 +196,11 @@ export class HarkApi extends BaseApi<StoreState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getMore() {
|
async getMore(): Promise<boolean> {
|
||||||
const offset = this.store.state['notifications']?.size || 0;
|
const offset = this.store.state['notifications']?.size || 0;
|
||||||
const count = 3;
|
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) {
|
async getSubset(offset:number, count:number, isArchive: boolean) {
|
||||||
|
51
pkg/interface/src/logic/lib/useLazyScroll.ts
Normal file
51
pkg/interface/src/logic/lib/useLazyScroll.ts
Normal 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;
|
||||||
|
}
|
@ -363,11 +363,19 @@ export function useShowNickname(contact: Contact | null, hide?: boolean): boolea
|
|||||||
return !!(contact && contact.nickname && !hideNicknames);
|
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 [hovering, setHovering] = useState(false);
|
||||||
const bind = {
|
const bind = {
|
||||||
onMouseEnter: () => setHovering(true),
|
onMouseOver: () => setHovering(true),
|
||||||
onMouseLeave: () => setHovering(false)
|
onMouseLeave: () => setHovering(false)
|
||||||
};
|
};
|
||||||
return { hovering, bind };
|
return { hovering, bind };
|
||||||
}
|
};
|
||||||
|
@ -40,7 +40,8 @@ const useLocalState = create<LocalState>(persist((set, get) => ({
|
|||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
set: fn => set(produce(fn))
|
set: fn => set(produce(fn))
|
||||||
}), {
|
}), {
|
||||||
|
blacklist: ['suspendedFocus', 'toggleOmnibox', 'omniboxShown'],
|
||||||
name: 'localReducer'
|
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 };
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
import BaseStore from './base';
|
import BaseStore from './base';
|
||||||
import InviteReducer from '../reducers/invite-update';
|
import InviteReducer from '../reducers/invite-update';
|
||||||
import MetadataReducer from '../reducers/metadata-update';
|
import MetadataReducer from '../reducers/metadata-update';
|
||||||
@ -40,6 +42,18 @@ export default class GlobalStore extends BaseStore<StoreState> {
|
|||||||
launchReducer = new LaunchReducer();
|
launchReducer = new LaunchReducer();
|
||||||
connReducer = new ConnectionReducer();
|
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() {
|
rehydrate() {
|
||||||
this.localReducer.rehydrate(this.state);
|
this.localReducer.rehydrate(this.state);
|
||||||
}
|
}
|
||||||
@ -94,6 +108,11 @@ export default class GlobalStore extends BaseStore<StoreState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reduce(data: Cage, state: 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.inviteReducer.reduce(data, this.state);
|
||||||
this.metadataReducer.reduce(data, this.state);
|
this.metadataReducer.reduce(data, this.state);
|
||||||
this.localReducer.reduce(data, this.state);
|
this.localReducer.reduce(data, this.state);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Serial, PatpNoSig, Path } from './noun';
|
import { Serial, PatpNoSig, Path } from './noun';
|
||||||
|
import {Resource} from './group-update';
|
||||||
|
|
||||||
export type InviteUpdate =
|
export type InviteUpdate =
|
||||||
InviteUpdateInitial
|
InviteUpdateInitial
|
||||||
@ -60,8 +61,8 @@ export type AppInvites = {
|
|||||||
|
|
||||||
export interface Invite {
|
export interface Invite {
|
||||||
app: string;
|
app: string;
|
||||||
path: Path;
|
recipient: PatpNoSig;
|
||||||
recipeint: PatpNoSig;
|
resource: Resource;
|
||||||
ship: PatpNoSig;
|
ship: PatpNoSig;
|
||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import _ from "lodash";
|
|||||||
import { Box, Row, Text, Rule } from "@tlon/indigo-react";
|
import { Box, Row, Text, Rule } from "@tlon/indigo-react";
|
||||||
|
|
||||||
import OverlaySigil from '~/views/components/OverlaySigil';
|
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 { Group, Association, Contacts, Post } from "~/types";
|
||||||
import TextContent from './content/text';
|
import TextContent from './content/text';
|
||||||
import CodeContent from './content/code';
|
import CodeContent from './content/code';
|
||||||
@ -134,6 +134,7 @@ export default class ChatMessage extends Component<ChatMessageProps> {
|
|||||||
className={containerClass}
|
className={containerClass}
|
||||||
style={style}
|
style={style}
|
||||||
mb={1}
|
mb={1}
|
||||||
|
position="relative"
|
||||||
>
|
>
|
||||||
{dayBreak && !isLastRead ? <DayBreak when={msg['time-sent']} /> : null}
|
{dayBreak && !isLastRead ? <DayBreak when={msg['time-sent']} /> : null}
|
||||||
{renderSigil
|
{renderSigil
|
||||||
@ -194,6 +195,8 @@ export const MessageWithSigil = (props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { hovering, bind } = useHovering();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<OverlaySigil
|
<OverlaySigil
|
||||||
@ -206,9 +209,11 @@ export const MessageWithSigil = (props) => {
|
|||||||
history={history}
|
history={history}
|
||||||
api={api}
|
api={api}
|
||||||
bg="white"
|
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
|
<Box
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
className="hide-child"
|
className="hide-child"
|
||||||
@ -231,8 +236,15 @@ export const MessageWithSigil = (props) => {
|
|||||||
}}
|
}}
|
||||||
title={`~${msg.author}`}
|
title={`~${msg.author}`}
|
||||||
>{name}</Text>
|
>{name}</Text>
|
||||||
<Text flexShrink='0' fontSize='0' gray mono className="v-mid">{timestamp}</Text>
|
<Text flexShrink={0} fontSize={0} gray mono>{timestamp}</Text>
|
||||||
<Text flexShrink={0} gray mono ml={2} className="v-mid child dn-s">{datestamp}</Text>
|
<Text
|
||||||
|
flexShrink={0}
|
||||||
|
fontSize={0}
|
||||||
|
gray
|
||||||
|
mono
|
||||||
|
ml={2}
|
||||||
|
display={['none', hovering ? 'block' : 'none']}
|
||||||
|
>{datestamp}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<ContentBox flexShrink={0} fontSize={fontSize ? fontSize : '14px'}>
|
<ContentBox flexShrink={0} fontSize={fontSize ? fontSize : '14px'}>
|
||||||
{msg.contents.map(c =>
|
{msg.contents.map(c =>
|
||||||
@ -257,20 +269,40 @@ const ContentBox = styled(Box)`
|
|||||||
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const MessageWithoutSigil = ({ timestamp, contacts, msg, measure, group }) => (
|
export const MessageWithoutSigil = ({ timestamp, contacts, msg, measure, group }) => {
|
||||||
<>
|
const { hovering, bind } = useHovering();
|
||||||
<Text flexShrink={0} mono gray display='inline-block' pt='2px' lineHeight='tall' className="child" fontSize='0'>{timestamp}</Text>
|
return (
|
||||||
<ContentBox flexShrink={0} fontSize='14px' className="clamp-message" style={{ flexGrow: 1 }}>
|
<>
|
||||||
{msg.contents.map((c, i) => (
|
<Text
|
||||||
<MessageContent
|
flexShrink={0}
|
||||||
key={i}
|
mono
|
||||||
contacts={contacts}
|
gray
|
||||||
content={c}
|
display={hovering ? 'block': 'none'}
|
||||||
group={group}
|
pt='2px'
|
||||||
measure={measure}/>))}
|
lineHeight='tall'
|
||||||
</ContentBox>
|
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 }) => {
|
export const MessageContent = ({ content, contacts, measure, fontSize, group }) => {
|
||||||
if ('code' in content) {
|
if ('code' in content) {
|
||||||
@ -292,7 +324,8 @@ export const MessageContent = ({ content, contacts, measure, fontSize, group })
|
|||||||
}}
|
}}
|
||||||
textProps={{style: {
|
textProps={{style: {
|
||||||
fontSize: 'inherit',
|
fontSize: 'inherit',
|
||||||
textDecoration: 'underline'
|
borderBottom: '1px solid',
|
||||||
|
textDecoration: 'none'
|
||||||
}}}
|
}}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -53,6 +53,9 @@ const MessageMarkdown = React.memo(props => (
|
|||||||
{...props}
|
{...props}
|
||||||
unwrapDisallowed={true}
|
unwrapDisallowed={true}
|
||||||
renderers={renderers}
|
renderers={renderers}
|
||||||
|
// shim until we uncover why RemarkBreaks and
|
||||||
|
// RemarkDisableTokenizers can't be loaded simultaneously
|
||||||
|
disallowedTypes={['heading', 'list', 'listItem', 'link']}
|
||||||
allowNode={(node, index, parent) => {
|
allowNode={(node, index, parent) => {
|
||||||
if (
|
if (
|
||||||
node.type === 'blockquote'
|
node.type === 'blockquote'
|
||||||
@ -67,11 +70,7 @@ const MessageMarkdown = React.memo(props => (
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}}
|
}}
|
||||||
plugins={[[
|
plugins={[RemarkBreaks]} />
|
||||||
RemarkBreaks,
|
|
||||||
RemarkDisableTokenizers,
|
|
||||||
{ block: DISABLED_BLOCK_TOKENS, inline: DISABLED_INLINE_TOKENS }
|
|
||||||
]]} />
|
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ const ModalButton = (props) => {
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
<Box
|
<Button
|
||||||
onClick={() => setModalShown(true)}
|
onClick={() => setModalShown(true)}
|
||||||
display="flex"
|
display="flex"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
@ -73,7 +73,7 @@ const ModalButton = (props) => {
|
|||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
<Icon icon={props.icon} mr={2} color={color}></Icon><Text color={color}>{props.text}</Text>
|
<Icon icon={props.icon} mr={2} color={color}></Icon><Text color={color}>{props.text}</Text>
|
||||||
</Box>
|
</Button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -48,10 +48,9 @@ export function LinkWindow(props: LinkWindowProps) {
|
|||||||
}, [graph.size]);
|
}, [graph.size]);
|
||||||
|
|
||||||
const first = graph.peekLargest()?.[0];
|
const first = graph.peekLargest()?.[0];
|
||||||
|
|
||||||
const [,,ship, name] = association['app-path'].split('/');
|
const [,,ship, name] = association['app-path'].split('/');
|
||||||
|
|
||||||
const style = useMemo(() =>
|
const style = useMemo(() =>
|
||||||
({
|
({
|
||||||
height: "100%",
|
height: "100%",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
@ -60,6 +59,14 @@ export function LinkWindow(props: LinkWindowProps) {
|
|||||||
alignItems: 'center'
|
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 (
|
return (
|
||||||
<VirtualScroller
|
<VirtualScroller
|
||||||
ref={(l) => (virtualList.current = l ?? undefined)}
|
ref={(l) => (virtualList.current = l ?? undefined)}
|
||||||
@ -82,7 +89,7 @@ export function LinkWindow(props: LinkWindowProps) {
|
|||||||
if(index.eq(first ?? bigInt.zero)) {
|
if(index.eq(first ?? bigInt.zero)) {
|
||||||
return (
|
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} />
|
<LinkSubmit s3={props.s3} name={name} ship={ship.slice(1)} api={api} />
|
||||||
</Col>
|
</Col>
|
||||||
<LinkItem {...linkProps} />
|
<LinkItem {...linkProps} />
|
||||||
|
@ -1,18 +1,16 @@
|
|||||||
import React, { useEffect, useCallback } from "react";
|
import React, { useEffect, useCallback, useRef, useState } from "react";
|
||||||
import f from "lodash/fp";
|
import f from "lodash/fp";
|
||||||
import _ from "lodash";
|
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 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 { MOMENT_CALENDAR_DATE, daToUnix, resourceAsPath } from "~/logic/lib/util";
|
||||||
import { BigInteger } from "big-integer";
|
import { BigInteger } from "big-integer";
|
||||||
import GlobalApi from "~/logic/api/global";
|
import GlobalApi from "~/logic/api/global";
|
||||||
import { Notification } from "./notification";
|
import { Notification } from "./notification";
|
||||||
import { Associations } from "~/types";
|
import { Associations } from "~/types";
|
||||||
import { cite } from '~/logic/lib/util';
|
import {Invites} from "./invites";
|
||||||
import { InviteItem } from '~/views/components/Invite';
|
import {useLazyScroll} from "~/logic/lib/useLazyScroll";
|
||||||
import { useWaitForProps } from "~/logic/lib/useWaitForProps";
|
|
||||||
import { useHistory } from "react-router-dom";
|
|
||||||
|
|
||||||
type DatedTimebox = [BigInteger, Timebox];
|
type DatedTimebox = [BigInteger, Timebox];
|
||||||
|
|
||||||
@ -45,10 +43,10 @@ export default function Inbox(props: {
|
|||||||
contacts: Rolodex;
|
contacts: Rolodex;
|
||||||
filter: string[];
|
filter: string[];
|
||||||
invites: any;
|
invites: any;
|
||||||
|
notificationsGroupConfig: GroupNotificationsConfig;
|
||||||
|
notificationsGraphConfig: NotificationGraphConfig;
|
||||||
}) {
|
}) {
|
||||||
const { api, associations, invites } = props;
|
const { api, associations, invites } = props;
|
||||||
const waiter = useWaitForProps(props)
|
|
||||||
const history = useHistory();
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let seen = false;
|
let seen = false;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -75,12 +73,12 @@ export default function Inbox(props: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let notificationsByDay = f.flow(
|
let notificationsByDay = f.flow(
|
||||||
f.map<DatedTimebox>(([date, nots]) => [
|
f.map<DatedTimebox, DatedTimebox>(([date, nots]) => [
|
||||||
date,
|
date,
|
||||||
nots.filter(filterNotification(associations, props.filter)),
|
nots.filter(filterNotification(associations, props.filter)),
|
||||||
]),
|
]),
|
||||||
f.groupBy<DatedTimebox>(([date]) => {
|
f.groupBy<DatedTimebox>(([d]) => {
|
||||||
date = moment(daToUnix(date));
|
const date = moment(daToUnix(d));
|
||||||
if (moment().subtract(6, 'hours').isBefore(date)) {
|
if (moment().subtract(6, 'hours').isBefore(date)) {
|
||||||
return 'latest';
|
return 'latest';
|
||||||
} else {
|
} else {
|
||||||
@ -88,69 +86,27 @@ export default function Inbox(props: {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)(notifications);
|
)(notifications);
|
||||||
notificationsByDay = new Map(Object.keys(notificationsByDay).sort().reverse().map(timebox => {
|
|
||||||
return [timebox, notificationsByDay[timebox]];
|
|
||||||
}));
|
|
||||||
|
|
||||||
useEffect(() => {
|
const notificationsByDayMap = new Map<string, DatedTimebox[]>(
|
||||||
api.hark.getMore(props.showArchive);
|
Object.keys(notificationsByDay).map(timebox => {
|
||||||
}, [props.showArchive]);
|
return [timebox, notificationsByDay[timebox]];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const onScroll = useCallback((e) => {
|
const scrollRef = useRef(null);
|
||||||
let container = e.target;
|
|
||||||
const { scrollHeight, scrollTop, clientHeight } = container;
|
|
||||||
if((scrollHeight - scrollTop) < 1.5 * clientHeight) {
|
|
||||||
api.hark.getMore(props.showArchive);
|
|
||||||
}
|
|
||||||
}, [props.showArchive]);
|
|
||||||
|
|
||||||
const acceptInvite = (app: string, uid: string) => async (invite) => {
|
const loadMore = useCallback(async () => {
|
||||||
const resource = {
|
return api.hark.getMore();
|
||||||
ship: `~${invite.resource.ship}`,
|
}, [api]);
|
||||||
name: invite.resource.name
|
|
||||||
};
|
|
||||||
|
|
||||||
const resourcePath = resourceAsPath(invite.resource);
|
const loadedAll = useLazyScroll(scrollRef, 0.2, loadMore);
|
||||||
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 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 (
|
return (
|
||||||
<Col position="relative" height="100%" overflowY="auto" onScroll={onScroll} >
|
<Col ref={scrollRef} position="relative" height="100%" overflowY="auto">
|
||||||
<Col zIndex={4} gapY={2} bg="white" top="0px" position="sticky" flexShrink={0}>
|
<Invites invites={invites} api={api} associations={associations} />
|
||||||
{inviteItems(invites, api)}
|
{[...notificationsByDayMap.keys()].sort().reverse().map((day, index) => {
|
||||||
</Col>
|
const timeboxes = notificationsByDayMap.get(day)!;
|
||||||
{[...notificationsByDay.keys()].map((day, index) => {
|
|
||||||
const timeboxes = notificationsByDay.get(day);
|
|
||||||
return timeboxes.length > 0 && (
|
return timeboxes.length > 0 && (
|
||||||
<DaySection
|
<DaySection
|
||||||
key={day}
|
key={day}
|
||||||
@ -163,10 +119,14 @@ export default function Inbox(props: {
|
|||||||
groups={props.groups}
|
groups={props.groups}
|
||||||
graphConfig={props.notificationsGraphConfig}
|
graphConfig={props.notificationsGraphConfig}
|
||||||
groupConfig={props.notificationsGroupConfig}
|
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>
|
</Col>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -192,7 +152,6 @@ function DaySection({
|
|||||||
api,
|
api,
|
||||||
groupConfig,
|
groupConfig,
|
||||||
graphConfig,
|
graphConfig,
|
||||||
chatConfig,
|
|
||||||
}) {
|
}) {
|
||||||
|
|
||||||
const lent = timeboxes.map(([,nots]) => nots.length).reduce(f.add, 0);
|
const lent = timeboxes.map(([,nots]) => nots.length).reduce(f.add, 0);
|
||||||
@ -202,23 +161,22 @@ function DaySection({
|
|||||||
|
|
||||||
return (
|
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">
|
<Box p="2" bg="scales.black05">
|
||||||
<Text>
|
<Text>
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
{_.map(timeboxes.sort(sortTimeboxes), ([date, nots], i) =>
|
{_.map(timeboxes.sort(sortTimeboxes), ([date, nots], i: number) =>
|
||||||
_.map(nots.sort(sortIndexedNotification), (not, j: number) => (
|
_.map(nots.sort(sortIndexedNotification), (not, j: number) => (
|
||||||
<React.Fragment key={j}>
|
<React.Fragment key={j}>
|
||||||
{(i !== 0 || j !== 0) && (
|
{(i !== 0 || j !== 0) && (
|
||||||
<Box flexShrink="0" height="4px" bg="scales.black05" />
|
<Box flexShrink={0} height="4px" bg="scales.black05" />
|
||||||
)}
|
)}
|
||||||
<Notification
|
<Notification
|
||||||
graphConfig={graphConfig}
|
graphConfig={graphConfig}
|
||||||
groupConfig={groupConfig}
|
groupConfig={groupConfig}
|
||||||
chatConfig={chatConfig}
|
|
||||||
api={api}
|
api={api}
|
||||||
associations={associations}
|
associations={associations}
|
||||||
notification={not}
|
notification={not}
|
||||||
|
74
pkg/interface/src/views/apps/notifications/invites.tsx
Normal file
74
pkg/interface/src/views/apps/notifications/invites.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -29,7 +29,6 @@ interface NotificationProps {
|
|||||||
contacts: Contacts;
|
contacts: Contacts;
|
||||||
graphConfig: NotificationGraphConfig;
|
graphConfig: NotificationGraphConfig;
|
||||||
groupConfig: GroupNotificationsConfig;
|
groupConfig: GroupNotificationsConfig;
|
||||||
chatConfig: string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMuted(
|
function getMuted(
|
||||||
|
@ -90,6 +90,8 @@ class OverlaySigil extends PureComponent<OverlaySigilProps, OverlaySigilState> {
|
|||||||
api,
|
api,
|
||||||
sigilClass,
|
sigilClass,
|
||||||
hideAvatars,
|
hideAvatars,
|
||||||
|
pr = 0,
|
||||||
|
pl = 0,
|
||||||
...rest
|
...rest
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@ -113,6 +115,8 @@ class OverlaySigil extends PureComponent<OverlaySigilProps, OverlaySigilState> {
|
|||||||
onClick={this.profileShow}
|
onClick={this.profileShow}
|
||||||
ref={this.containerRef}
|
ref={this.containerRef}
|
||||||
className={className}
|
className={className}
|
||||||
|
pr={pr}
|
||||||
|
pl={pl}
|
||||||
>
|
>
|
||||||
{state.clicked && (
|
{state.clicked && (
|
||||||
<ProfileOverlay
|
<ProfileOverlay
|
||||||
|
Loading…
Reference in New Issue
Block a user