mirror of
https://github.com/urbit/shrub.git
synced 2024-12-25 04:52:06 +03:00
Merge pull request #4646 from urbit/lf/group-feed
group feed: interface improvements
This commit is contained in:
commit
c9ea6a599b
@ -459,7 +459,6 @@
|
||||
=notif-kind:hook
|
||||
==
|
||||
^+ update-core
|
||||
?: ?=(%none mode.notif-kind) update-core
|
||||
=/ =stats-index:store
|
||||
(to-stats-index:store index)
|
||||
=. update-core
|
||||
|
@ -28,7 +28,6 @@
|
||||
?+ index.p.i ~
|
||||
[@ ~] `[%link [0 1] %each %children]
|
||||
[@ @ %1 ~] `[%comment [1 2] %count %siblings]
|
||||
[@ @ @ ~] `[%edit-comment [1 2] %none %none]
|
||||
==
|
||||
::
|
||||
++ transform-add-nodes
|
||||
|
@ -14,8 +14,11 @@
|
||||
[%yes %self %self]
|
||||
:: +notification-kind: no notifications for now
|
||||
::
|
||||
++ notification-kind ~
|
||||
++ transform-add-nodes
|
||||
++ notification-kind
|
||||
=/ len (lent index.p.i)
|
||||
`[%post [(dec len) len] %none %children]
|
||||
::
|
||||
++ transform-add-nodes
|
||||
|= [=index =post =atom was-parent-modified=?]
|
||||
^- [^index ^post]
|
||||
=- [- post(index -)]
|
||||
|
@ -26,9 +26,7 @@
|
||||
++ notification-kind
|
||||
?+ index.p.i ~
|
||||
[@ %1 %1 ~] `[%note [0 1] %each %children]
|
||||
[@ %1 @ ~] `[%edit-note [0 1] %none %none]
|
||||
[@ %2 @ %1 ~] `[%comment [1 3] %count %siblings]
|
||||
[@ %2 @ @ ~] `[%edit-comment [1 3] %none %none]
|
||||
==
|
||||
::
|
||||
++ transform-add-nodes
|
||||
|
@ -67,4 +67,4 @@
|
||||
preview %.n
|
||||
hidden %.y
|
||||
==
|
||||
(pure:m !>(~))
|
||||
(pure:m !>(feed-rid))
|
||||
|
18
pkg/interface/package-lock.json
generated
18
pkg/interface/package-lock.json
generated
@ -1783,36 +1783,30 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": {
|
||||
"version": "7.12.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz",
|
||||
"integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==",
|
||||
"bundled": true,
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.13.4"
|
||||
}
|
||||
},
|
||||
"@types/lodash": {
|
||||
"version": "4.14.168",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.168.tgz",
|
||||
"integrity": "sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q=="
|
||||
"bundled": true
|
||||
},
|
||||
"@urbit/eslint-config": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@urbit/eslint-config/-/eslint-config-1.0.0.tgz",
|
||||
"integrity": "sha512-Xmzb6MvM7KorlPJEq/hURZZ4BHSVy/7CoQXWogsBSTv5MOZnMqwNKw6yt24k2AO/2UpHwjGptimaNLqFfesJbw=="
|
||||
"bundled": true
|
||||
},
|
||||
"big-integer": {
|
||||
"version": "1.6.48",
|
||||
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz",
|
||||
"integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w=="
|
||||
"bundled": true
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.20",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
|
||||
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
|
||||
"bundled": true
|
||||
},
|
||||
"regenerator-runtime": {
|
||||
"version": "0.13.7",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
|
||||
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
|
||||
"bundled": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1,6 +1,6 @@
|
||||
import BaseApi from './base';
|
||||
import { StoreState } from '../store/type';
|
||||
import { Patp, Path } from '@urbit/api';
|
||||
import { Patp, Path, Resource } from '@urbit/api';
|
||||
import _ from 'lodash';
|
||||
import { makeResource, resourceFromPath } from '../lib/group';
|
||||
import { GroupPolicy, Enc, Post, Content } from '@urbit/api';
|
||||
@ -259,6 +259,18 @@ export default class GraphApi extends BaseApi<StoreState> {
|
||||
*/
|
||||
}
|
||||
|
||||
async enableGroupFeed(group: Resource): Promise<Resource> {
|
||||
const { resource } = await this.spider(
|
||||
'graph-view-action',
|
||||
'resource',
|
||||
'graph-create-group-feed',
|
||||
{
|
||||
"create-group-feed": { resource: group }
|
||||
}
|
||||
);
|
||||
return resource;
|
||||
}
|
||||
|
||||
removeNodes(ship: Patp, name: string, indices: string[]) {
|
||||
return this.hookAction(ship, {
|
||||
'remove-nodes': {
|
||||
|
11
pkg/interface/src/logic/lib/useToggleState.ts
Normal file
11
pkg/interface/src/logic/lib/useToggleState.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { useState, useCallback } from "react";
|
||||
|
||||
export function useToggleState(initial: boolean) {
|
||||
const [state, setState] = useState(initial);
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
setState((s) => !s);
|
||||
}, [setState]);
|
||||
|
||||
return [state, toggle] as const;
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import { Patp, Rolodex, Scry } from "@urbit/api";
|
||||
import { Patp, Rolodex, Scry, Contact } from "@urbit/api";
|
||||
|
||||
import { BaseState, createState } from "./base";
|
||||
import {useCallback} from "react";
|
||||
|
||||
export interface ContactState extends BaseState<ContactState> {
|
||||
contacts: Rolodex;
|
||||
@ -9,6 +10,12 @@ export interface ContactState extends BaseState<ContactState> {
|
||||
// fetchIsAllowed: (entity, name, ship, personal) => Promise<boolean>;
|
||||
};
|
||||
|
||||
export function useContact(ship: string) {
|
||||
return useContactState(
|
||||
useCallback(s => s.contacts[ship] as Contact | null, [ship])
|
||||
);
|
||||
}
|
||||
|
||||
const useContactState = createState<ContactState>('Contact', {
|
||||
contacts: {},
|
||||
nackedContacts: new Set(),
|
||||
@ -28,4 +35,4 @@ const useContactState = createState<ContactState>('Contact', {
|
||||
// },
|
||||
}, ['nackedContacts']);
|
||||
|
||||
export default useContactState;
|
||||
export default useContactState;
|
||||
|
@ -1,15 +1,22 @@
|
||||
import { Path, JoinRequests } from "@urbit/api";
|
||||
import { Path, JoinRequests, Group } from "@urbit/api";
|
||||
|
||||
import { BaseState, createState } from "./base";
|
||||
import {useCallback} from "react";
|
||||
|
||||
export interface GroupState extends BaseState<GroupState> {
|
||||
groups: Set<Path>;
|
||||
groups: {
|
||||
[groupPath: string]: Group;
|
||||
};
|
||||
pendingJoin: JoinRequests;
|
||||
};
|
||||
|
||||
const useGroupState = createState<GroupState>('Group', {
|
||||
groups: new Set(),
|
||||
groups: {},
|
||||
pendingJoin: {},
|
||||
}, ['groups']);
|
||||
|
||||
export default useGroupState;
|
||||
export function useGroup(group: string) {
|
||||
return useGroupState(useCallback(s => s.groups[group], [group]));
|
||||
}
|
||||
|
||||
export default useGroupState;
|
||||
|
@ -40,6 +40,7 @@ import useSettingsState, { selectCalmState } from '~/logic/state/settings';
|
||||
import Timestamp from '~/views/components/Timestamp';
|
||||
import useContactState from '~/logic/state/contact';
|
||||
import { useIdlingState } from '~/logic/lib/idling';
|
||||
import ProfileOverlay from '~/views/components/ProfileOverlay';
|
||||
|
||||
export const DATESTAMP_FORMAT = '[~]YYYY.M.D';
|
||||
|
||||
@ -477,21 +478,9 @@ export const MessageAuthor = ({
|
||||
cursor='pointer'
|
||||
position='relative'
|
||||
>
|
||||
{showOverlay && (
|
||||
<OverlaySigil
|
||||
cursor='auto'
|
||||
ship={msg.author}
|
||||
contact={contact}
|
||||
color={`#${uxToHex(contact?.color ?? '0x0')}`}
|
||||
group={group}
|
||||
onDismiss={() => toggleOverlay()}
|
||||
history={history}
|
||||
className='relative'
|
||||
scrollWindow={scrollWindow}
|
||||
api={api}
|
||||
/>
|
||||
)}
|
||||
{img}
|
||||
<ProfileOverlay cursor='auto' ship={msg.author} api={api}>
|
||||
{img}
|
||||
</ProfileOverlay>
|
||||
</Box>
|
||||
<Box flexGrow={1} display='block' className='clamp-message' {...bind}>
|
||||
<Box
|
||||
|
@ -25,6 +25,9 @@ function getGraphModuleIcon(module: string) {
|
||||
if (module === 'link') {
|
||||
return 'Collection';
|
||||
}
|
||||
if(module === 'post') {
|
||||
return 'Groups';
|
||||
}
|
||||
return _.capitalize(module);
|
||||
}
|
||||
|
||||
@ -38,6 +41,8 @@ const FilterBox = styled(Box)`
|
||||
|
||||
function describeNotification(description: string, plural: boolean): string {
|
||||
switch (description) {
|
||||
case 'post':
|
||||
return 'replied to you';
|
||||
case 'link':
|
||||
return `added ${pluralize('new link', plural)} to`;
|
||||
case 'comment':
|
||||
@ -117,6 +122,9 @@ const GraphNodeContent = ({
|
||||
);
|
||||
}
|
||||
}
|
||||
if(mod === 'post') {
|
||||
return <MentionText content={contents} group={group} />;
|
||||
}
|
||||
|
||||
if (mod === 'chat') {
|
||||
return (
|
||||
@ -166,6 +174,9 @@ function getNodeUrl(
|
||||
return `${graphUrl}/${linkId}`;
|
||||
} else if (mod === 'chat') {
|
||||
return graphUrl;
|
||||
} else if( mod === 'post') {
|
||||
const [last, ...rest] = idx.reverse();
|
||||
return `/~landscape${groupPath}/feed/${rest.join('/')}?post=${last}`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import OverlaySigil from './OverlaySigil';
|
||||
import { Sigil } from '~/logic/lib/sigil';
|
||||
import Timestamp from './Timestamp';
|
||||
import useContactState from '~/logic/state/contact';
|
||||
import ProfileOverlay from './ProfileOverlay';
|
||||
|
||||
interface AuthorProps {
|
||||
ship: string;
|
||||
@ -26,7 +27,7 @@ interface AuthorProps {
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
export default function Author(props: AuthorProps): ReactElement {
|
||||
const { date, showImage, fullNotIcon } = props;
|
||||
const { ship, date, showImage, fullNotIcon } = props;
|
||||
|
||||
const showAsCol = props.showAsCol || false;
|
||||
const time = props.time || false;
|
||||
@ -116,16 +117,10 @@ export default function Author(props: AuthorProps): ReactElement {
|
||||
position='relative'
|
||||
cursor='pointer'
|
||||
>
|
||||
{showImage && img}
|
||||
{showOverlay && (
|
||||
<OverlaySigil
|
||||
ship={ship}
|
||||
contact={contact}
|
||||
color={`#${uxToHex(contact?.color ?? '0x0')}`}
|
||||
onDismiss={() => toggleOverlay()}
|
||||
history={history}
|
||||
className='relative'
|
||||
/>
|
||||
{showImage && (
|
||||
<ProfileOverlay ship={ship} api={props.api} >
|
||||
{img}
|
||||
</ProfileOverlay>
|
||||
)}
|
||||
</Box>
|
||||
{rowOrCol}
|
||||
|
@ -22,7 +22,7 @@ interface OverlaySigilState {
|
||||
};
|
||||
}
|
||||
|
||||
export const OverlaySigil = (props: OverlaySigilProps): React.FC => {
|
||||
export const OverlaySigil = (props: OverlaySigilProps) => {
|
||||
const {
|
||||
api,
|
||||
className,
|
||||
|
@ -1,5 +1,8 @@
|
||||
import React, { PureComponent, useCallback, useEffect, useRef } from 'react';
|
||||
import { Contact, Group } from '@urbit/api';
|
||||
import React, { PureComponent, useCallback, useEffect, useRef, useState, useMemo } from 'react';
|
||||
import { Contact, Group, uxToHex } from '@urbit/api';
|
||||
import VisibilitySensor from 'react-visibility-sensor';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { cite, useShowNickname } from '~/logic/lib/util';
|
||||
import { Sigil } from '~/logic/lib/sigil';
|
||||
|
||||
@ -10,76 +13,99 @@ import {
|
||||
Button,
|
||||
Text,
|
||||
BaseImage,
|
||||
ColProps,
|
||||
Icon
|
||||
Icon,
|
||||
BoxProps
|
||||
} from '@tlon/indigo-react';
|
||||
import RichText from './RichText';
|
||||
import { ProfileStatus } from './ProfileStatus';
|
||||
import useSettingsState from '~/logic/state/settings';
|
||||
import {useOutsideClick} from '~/logic/lib/useOutsideClick';
|
||||
import {useContact} from '~/logic/state/contact';
|
||||
import {useHistory} from 'react-router-dom';
|
||||
import {Portal} from './Portal';
|
||||
import {getRelativePosition} from '~/logic/lib/relativePosition';
|
||||
|
||||
export const OVERLAY_HEIGHT = 250;
|
||||
const FixedOverlay = styled(Col)`
|
||||
position: fixed;
|
||||
-webkit-transition: all 0.2s ease-out;
|
||||
-moz-transition: all 0.2s ease-out;
|
||||
-o-transition: all 0.2s ease-out;
|
||||
transition: all 0.2s ease-out;
|
||||
`;
|
||||
|
||||
type ProfileOverlayProps = ColProps & {
|
||||
type ProfileOverlayProps = BoxProps & {
|
||||
ship: string;
|
||||
contact?: Contact;
|
||||
color: string;
|
||||
topSpace: number | 'auto';
|
||||
bottomSpace: number | 'auto';
|
||||
group?: Group;
|
||||
onDismiss(): void;
|
||||
hideAvatars: boolean;
|
||||
hideNicknames: boolean;
|
||||
history: any;
|
||||
api: any;
|
||||
};
|
||||
|
||||
const ProfileOverlay = (props: ProfileOverlayProps) => {
|
||||
const {
|
||||
contact,
|
||||
ship,
|
||||
color,
|
||||
topSpace,
|
||||
bottomSpace,
|
||||
history,
|
||||
onDismiss,
|
||||
api,
|
||||
children,
|
||||
...rest
|
||||
} = props;
|
||||
const [open, _setOpen] = useState(false);
|
||||
const [coords, setCoords] = useState({});
|
||||
const [visible, setVisible] = useState(false);
|
||||
const history = useHistory();
|
||||
const outerRef = useRef<HTMLElement | null>(null);
|
||||
const innerRef = useRef<HTMLElement | null>(null);
|
||||
const hideAvatars = useSettingsState(state => state.calm.hideAvatars);
|
||||
const hideNicknames = useSettingsState(state => state.calm.hideNicknames);
|
||||
const popoverRef = useRef<typeof Col>(null);
|
||||
const isOwn = useMemo(() => `~${window.ship}` === ship, [ship])
|
||||
|
||||
const onDocumentClick = useCallback((event) => {
|
||||
if (!popoverRef.current || popoverRef?.current?.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
onDismiss();
|
||||
}, [onDismiss, popoverRef]);
|
||||
const contact = useContact(ship)
|
||||
const color = uxToHex(contact?.color ?? '0x0');
|
||||
const showNickname = useShowNickname(contact, hideNicknames);
|
||||
|
||||
const setClosed = useCallback(() => {
|
||||
_setOpen(false);
|
||||
}, [_setOpen]);
|
||||
|
||||
const setOpen = useCallback(() => {
|
||||
_setOpen(true);
|
||||
}, [_setOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('mousedown', onDocumentClick);
|
||||
document.addEventListener('touchstart', onDocumentClick);
|
||||
if(!visible) {
|
||||
setClosed();
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
useOutsideClick(innerRef, setClosed);
|
||||
|
||||
useEffect(() => {
|
||||
if(!open) {
|
||||
return () => {};
|
||||
}
|
||||
function _updateCoords() {
|
||||
if(outerRef.current) {
|
||||
const outer = outerRef.current;
|
||||
const { left, right, top } = outer.getBoundingClientRect();
|
||||
const spaceAtTop = top > 300;
|
||||
const spaceAtRight = right > 300 || right > left;
|
||||
setCoords(getRelativePosition(
|
||||
outer,
|
||||
spaceAtRight ? 'left' : 'right',
|
||||
spaceAtTop ? 'bottom' : 'top',
|
||||
-1* outer.clientWidth,
|
||||
-1 * outer.clientHeight
|
||||
));
|
||||
}
|
||||
}
|
||||
const updateCoords = _.throttle(_updateCoords, 50);
|
||||
updateCoords();
|
||||
const interval = setInterval(updateCoords, 300);
|
||||
window.addEventListener('scroll', updateCoords);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', onDocumentClick);
|
||||
document.removeEventListener('touchstart', onDocumentClick);
|
||||
}
|
||||
}, [onDocumentClick]);
|
||||
clearInterval(interval);
|
||||
window.removeEventListener('scroll', updateCoords);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
let top, bottom;
|
||||
if (topSpace < OVERLAY_HEIGHT / 2) {
|
||||
top = '0px';
|
||||
}
|
||||
if (bottomSpace < OVERLAY_HEIGHT / 2) {
|
||||
bottom = '0px';
|
||||
}
|
||||
if (!(top || bottom)) {
|
||||
bottom = `-${Math.round(OVERLAY_HEIGHT / 2)}px`;
|
||||
}
|
||||
const containerStyle = { top, bottom, left: '100%' };
|
||||
|
||||
const isOwn = window.ship === ship;
|
||||
|
||||
const img =
|
||||
const img =
|
||||
contact?.avatar && !hideAvatars ? (
|
||||
<BaseImage
|
||||
referrerPolicy="no-referrer"
|
||||
@ -93,26 +119,29 @@ const ProfileOverlay = (props: ProfileOverlayProps) => {
|
||||
) : (
|
||||
<Sigil ship={ship} size={72} color={color} />
|
||||
);
|
||||
const showNickname = useShowNickname(contact, hideNicknames);
|
||||
|
||||
return (
|
||||
<Col
|
||||
ref={popoverRef}
|
||||
return (
|
||||
<Box ref={outerRef} {...rest} onClick={setOpen} cursor="pointer">
|
||||
<VisibilitySensor onChange={setVisible}>
|
||||
{children}
|
||||
</VisibilitySensor>
|
||||
{ open && (
|
||||
<Portal>
|
||||
<FixedOverlay
|
||||
ref={innerRef}
|
||||
{...coords}
|
||||
backgroundColor='white'
|
||||
color='washedGray'
|
||||
border={1}
|
||||
borderRadius={2}
|
||||
borderColor='lightGray'
|
||||
boxShadow='0px 0px 0px 3px'
|
||||
position='absolute'
|
||||
zIndex='3'
|
||||
zIndex={3}
|
||||
fontSize='0'
|
||||
height='250px'
|
||||
width='250px'
|
||||
padding={3}
|
||||
justifyContent='center'
|
||||
style={containerStyle}
|
||||
{...rest}
|
||||
>
|
||||
<Row color='black' padding={3} position='absolute' top={0} left={0}>
|
||||
{!isOwn && (
|
||||
@ -177,8 +206,11 @@ const ProfileOverlay = (props: ProfileOverlayProps) => {
|
||||
</RichText>
|
||||
)}
|
||||
</Col>
|
||||
</Col>
|
||||
);
|
||||
</FixedOverlay>
|
||||
</Portal>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileOverlay;
|
||||
export default ProfileOverlay;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Box, Row, Col, Text, BaseImage } from '@tlon/indigo-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { resourceFromPath } from '~/logic/lib/group';
|
||||
|
||||
|
||||
@ -29,25 +30,6 @@ export const AddFeedBanner = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const enableFeed = () => {
|
||||
if (!groupPath) {
|
||||
console.error('no group path, cannot enable feed');
|
||||
return;
|
||||
}
|
||||
const resource = resourceFromPath(groupPath);
|
||||
if (!resource) {
|
||||
console.error('cannot make resource, cannot enable feed');
|
||||
return;
|
||||
}
|
||||
|
||||
api.spider(
|
||||
'graph-view-action',
|
||||
'json',
|
||||
'graph-create-group-feed',
|
||||
{ 'create-group-feed': { resource } }
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Row
|
||||
height="48px"
|
||||
@ -64,9 +46,11 @@ export const AddFeedBanner = (props) => {
|
||||
<Text mr="2" color="gray" bold cursor="pointer" onClick={disableFeed}>
|
||||
Dismiss
|
||||
</Text>
|
||||
<Text color="blue" bold cursor="pointer" onClick={enableFeed}>
|
||||
Enable Feed
|
||||
</Text>
|
||||
<Link to={`/~landscape${groupPath}/enable`}>
|
||||
<Text color="blue" bold cursor="pointer">
|
||||
Enable Feed
|
||||
</Text>
|
||||
</Link>
|
||||
</Row>
|
||||
</Row>
|
||||
);
|
||||
|
@ -0,0 +1,68 @@
|
||||
import React, { useCallback } from "react";
|
||||
import { ModalOverlay } from "~/views/components/ModalOverlay";
|
||||
import { Formik, Form, FormikHelpers } from "formik";
|
||||
import {
|
||||
GroupFeedPermissions,
|
||||
GroupFeedPermsInput,
|
||||
} from "./Post/GroupFeedPerms";
|
||||
import { Text, Button, Col, Row } from "@tlon/indigo-react";
|
||||
import { AsyncButton } from "~/views/components/AsyncButton";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { resourceFromPath, Tag, resourceAsPath } from "@urbit/api";
|
||||
import useGroupState, { useGroup } from "~/logic/state/group";
|
||||
|
||||
interface FormSchema {
|
||||
permissions: GroupFeedPermissions;
|
||||
}
|
||||
|
||||
export function EnableGroupFeed(props: {
|
||||
groupPath: string;
|
||||
dismiss: () => void;
|
||||
api: GlobalApi;
|
||||
}) {
|
||||
const { api, groupPath, dismiss } = props;
|
||||
const initialValues: FormSchema = {
|
||||
permissions: "everyone",
|
||||
};
|
||||
const group = useGroup(groupPath);
|
||||
const onSubmit = useCallback(
|
||||
async (values: FormSchema, actions: FormikHelpers<FormSchema>) => {
|
||||
const resource = resourceFromPath(groupPath);
|
||||
const feed = resourceAsPath(await api.graph.enableGroupFeed(resource));
|
||||
const tag: Tag = {
|
||||
app: "graph",
|
||||
resource: feed,
|
||||
tag: "writers",
|
||||
};
|
||||
if (values.permissions === "admins") {
|
||||
const admins =
|
||||
Array.from(group.tags?.role?.admin).map((s) => `~${s}`) ?? [];
|
||||
await api.groups.addTag(resource, tag, admins);
|
||||
} else if (values.permissions === "host") {
|
||||
await api.groups.addTag(resource, tag, [`~${window.ship}`]);
|
||||
}
|
||||
actions.setStatus({ success: null });
|
||||
dismiss();
|
||||
},
|
||||
[groupPath, dismiss]
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalOverlay spacing={[3, 5, 7]} bg="white" dismiss={dismiss}>
|
||||
<Formik initialValues={initialValues} onSubmit={onSubmit}>
|
||||
<Form>
|
||||
<Col gapY="4" p="4">
|
||||
<Text fontWeight="medium" fontSize="2">
|
||||
Enable Feed
|
||||
</Text>
|
||||
<GroupFeedPermsInput id="permissions" />
|
||||
<Row gapX="2">
|
||||
<AsyncButton primary>Enable Feed</AsyncButton>
|
||||
<Button onClick={dismiss}>Cancel</Button>
|
||||
</Row>
|
||||
</Col>
|
||||
</Form>
|
||||
</Formik>
|
||||
</ModalOverlay>
|
||||
);
|
||||
}
|
@ -38,6 +38,7 @@ export function GroupFeed(props) {
|
||||
width="100%"
|
||||
height="100%"
|
||||
display="flex"
|
||||
position="relative"
|
||||
alignItems="center"
|
||||
overflow="hidden">
|
||||
<GroupFeedHeader baseUrl={baseUrl} history={history} />
|
||||
|
@ -1,9 +1,11 @@
|
||||
import React from 'react';
|
||||
import { Box, Col } from '@tlon/indigo-react';
|
||||
import { Box } from '@tlon/indigo-react';
|
||||
|
||||
import { EnableGroupFeed } from './EnableGroupFeed';
|
||||
import { EmptyGroupHome } from './EmptyGroupHome';
|
||||
import { GroupFeed } from './GroupFeed';
|
||||
import { AddFeedBanner } from './AddFeedBanner';
|
||||
import {Route, useHistory} from 'react-router-dom';
|
||||
|
||||
|
||||
export function GroupHome(props) {
|
||||
@ -15,7 +17,6 @@ export function GroupHome(props) {
|
||||
graphs,
|
||||
baseUrl,
|
||||
contacts,
|
||||
history
|
||||
} = props;
|
||||
|
||||
const metadata = associations?.groups[groupPath]?.metadata;
|
||||
@ -32,9 +33,21 @@ export function GroupHome(props) {
|
||||
'resource' in metadata.config.group;
|
||||
|
||||
const graphPath = metadata?.config?.group?.resource;
|
||||
const history = useHistory();
|
||||
|
||||
return (
|
||||
<Box width="100%" height="100%">
|
||||
<Route path={`${baseUrl}/enable`}
|
||||
render={() => {
|
||||
return (
|
||||
<EnableGroupFeed
|
||||
groupPath={groupPath}
|
||||
dismiss={() => history.push(baseUrl)}
|
||||
api={api}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{ askFeedBanner ? (
|
||||
<AddFeedBanner
|
||||
api={api}
|
||||
|
@ -0,0 +1,42 @@
|
||||
import React from "react";
|
||||
import {
|
||||
ManagedRadioButtonField as Radio,
|
||||
Col,
|
||||
Label,
|
||||
Text,
|
||||
} from "@tlon/indigo-react";
|
||||
import { PropFunc } from "~/types";
|
||||
|
||||
export type GroupFeedPermissions = "everyone" | "host" | "admins";
|
||||
|
||||
export function GroupFeedPermsInput(
|
||||
props: {
|
||||
id: string;
|
||||
} & PropFunc<typeof Col>
|
||||
) {
|
||||
const { id, ...rest } = props;
|
||||
|
||||
return (
|
||||
<Col gapY="2" {...rest}>
|
||||
<Text fontWeight="medium">Permissions</Text>
|
||||
<Radio
|
||||
name={id}
|
||||
id="everyone"
|
||||
label="Everyone"
|
||||
caption="Everyone in this group can post and edit this feed"
|
||||
/>
|
||||
<Radio
|
||||
name={id}
|
||||
id="host"
|
||||
label="Host Only"
|
||||
caption="Only the host can post and edit this feed"
|
||||
/>
|
||||
<Radio
|
||||
name={id}
|
||||
id="admins"
|
||||
label="Host & Admins Only"
|
||||
caption="Only Hosts and Admins can post and edit this feed"
|
||||
/>
|
||||
</Col>
|
||||
);
|
||||
}
|
@ -1,24 +1,52 @@
|
||||
import React, {
|
||||
useState
|
||||
useState,
|
||||
useCallback
|
||||
} from 'react';
|
||||
import { Button, Text, Box, Row, BaseTextArea } from '@tlon/indigo-react';
|
||||
import { LoadingSpinner, Icon, Button, Text, Box, Row, BaseTextArea } from '@tlon/indigo-react';
|
||||
import tokenizeMessage from '~/logic/lib/tokenizeMessage';
|
||||
import { useToggleState } from '~/logic/lib/useToggleState';
|
||||
import { createPost } from '~/logic/api/graph';
|
||||
import useStorage from '~/logic/lib/useStorage';
|
||||
|
||||
|
||||
export function PostInput(props) {
|
||||
const { api, graphResource, index, submitCallback } = props;
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
const [postContent, setPostContent] = useState('');
|
||||
|
||||
const sendPost = () => {
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
const [code, toggleCode] = useToggleState(false);
|
||||
const { canUpload, promptUpload, uploading } = useStorage();
|
||||
const [postContent, setPostContent] = useState('');
|
||||
const uploadImage = useCallback(async () => {
|
||||
try {
|
||||
setDisabled(true);
|
||||
const url = await promptUpload();
|
||||
const { ship, name } = graphResource;
|
||||
await api.graph.addPost(ship, name, createPost([{ url }]));
|
||||
} catch (e) {
|
||||
// TODO: better handling
|
||||
console.error(e);
|
||||
} finally {
|
||||
setDisabled(false);
|
||||
|
||||
}
|
||||
|
||||
}, [promptUpload]);
|
||||
|
||||
const sendPost = async () => {
|
||||
if (!graphResource) {
|
||||
console.error("graphResource is undefined, cannot post");
|
||||
return;
|
||||
}
|
||||
let contents = [];
|
||||
if(code) {
|
||||
const output = await props.api.graph.eval(postContent);
|
||||
contents = [{ code: { output, expression: postContent } }];
|
||||
} else {
|
||||
contents = tokenizeMessage(postContent);
|
||||
}
|
||||
|
||||
setDisabled(true);
|
||||
const post = createPost(tokenizeMessage(postContent), index || '');
|
||||
const post = createPost(contents, index || '');
|
||||
|
||||
api.graph.addPost(
|
||||
graphResource.ship,
|
||||
@ -26,6 +54,9 @@ export function PostInput(props) {
|
||||
post
|
||||
).then(() => {
|
||||
setDisabled(false);
|
||||
if(code) {
|
||||
toggleCode();
|
||||
}
|
||||
setPostContent('');
|
||||
|
||||
if (submitCallback) {
|
||||
@ -48,11 +79,12 @@ export function PostInput(props) {
|
||||
color="black"
|
||||
fontSize={1}
|
||||
height="62px"
|
||||
fontFamily={code ? 'mono' : 'sans'}
|
||||
lineNumber={3}
|
||||
style={{
|
||||
resize: 'none',
|
||||
}}
|
||||
placeholder="What's on your mind?"
|
||||
placeholder={code ? "(add 2 2)" : "What's on your mind?"}
|
||||
spellCheck="false"
|
||||
value={postContent}
|
||||
onChange={e => setPostContent(e.target.value)}
|
||||
@ -66,7 +98,31 @@ export function PostInput(props) {
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
alignItems="center">
|
||||
<Box></Box>
|
||||
<Row>
|
||||
{false && (
|
||||
<Box mr={2} flexShrink={0} height='16px' width='16px' flexBasis='16px'>
|
||||
<Icon
|
||||
icon='Dojo'
|
||||
onClick={toggleCode}
|
||||
color={code ? 'blue' : 'black'}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{ canUpload && (
|
||||
<Box mr={2} flexShrink={0} height='16px' width='16px' flexBasis='16px'>
|
||||
{ uploading ? (
|
||||
<LoadingSpinner />
|
||||
) : (
|
||||
<Icon
|
||||
icon='Links'
|
||||
width='16'
|
||||
height='16'
|
||||
onClick={uploadImage}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Row>
|
||||
<Button
|
||||
pl="2"
|
||||
pr="2"
|
||||
|
@ -37,7 +37,7 @@ export function PostTimeline(props) {
|
||||
) : null
|
||||
}
|
||||
</Box>
|
||||
<Box height="calc(100% - 136px)" width="100%" alignItems="center" pl="1">
|
||||
<Box height="calc(100% - 176px)" width="100%" alignItems="center" pl="1">
|
||||
{ shouldRenderFeed ? (
|
||||
<PostFeed
|
||||
graphResource={graphResource}
|
||||
|
Loading…
Reference in New Issue
Block a user