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