Merge pull request #4646 from urbit/lf/group-feed

group feed: interface improvements
This commit is contained in:
L 2021-03-23 13:09:42 -05:00 committed by GitHub
commit c9ea6a599b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 366 additions and 145 deletions

View File

@ -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

View File

@ -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

View File

@ -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 -)]

View File

@ -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

View File

@ -67,4 +67,4 @@
preview %.n
hidden %.y
==
(pure:m !>(~))
(pure:m !>(feed-rid))

View File

@ -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
}
}
},

View File

@ -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': {

View 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;
}

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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 '';
}

View File

@ -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}

View File

@ -22,7 +22,7 @@ interface OverlaySigilState {
};
}
export const OverlaySigil = (props: OverlaySigilProps): React.FC => {
export const OverlaySigil = (props: OverlaySigilProps) => {
const {
api,
className,

View File

@ -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;

View File

@ -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>
);

View File

@ -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>
);
}

View File

@ -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} />

View File

@ -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}

View File

@ -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>
);
}

View File

@ -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"

View File

@ -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}