Skip to content

Commit

Permalink
File upload for dashboard (#373)
Browse files Browse the repository at this point in the history
Handles file uploads from dashboard to Slack
  • Loading branch information
cheeseblubber committed Jan 29, 2021
1 parent 5e78eb4 commit 6cc001c
Show file tree
Hide file tree
Showing 38 changed files with 866 additions and 62 deletions.
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export REACT_APP_SLACK_CLIENT_ID='xxxx.xxxx'
export REACT_APP_SENTRY_DSN=https://[email protected]/44444
export REACT_APP_LOGROCKET_ID='xxxxxx/papercups'
export REACT_APP_STRIPE_PUBLIC_KEY=pk_test_xxxxx
export REACT_APP_FILE_UPLOADS_ENABLED=1

# set to localhost:3000 when developing
export REACT_APP_URL='app.papercups.io'
Expand All @@ -30,3 +31,8 @@ export BACKEND_URL='app.papercups.io'
# Customer.io
export CUSTOMER_IO_SITE_ID='0000000000000000000'
export CUSTOMER_IO_API_KEY='xxxxxxxxxxxxx'

export AWS_ACCESS_KEY_ID='xxxxxxxxxxxxxxxxxxxxxx'
export AWS_SECRET_ACCESS_KEY='xxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
export BUCKET_NAME='SOME-S3-BUCKET'
export AWS_REGION='SOME-AWS-REGION'
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,5 @@ chat_api-*.tar

# Jetbrain IDE files
.idea/

.DS_Store
18 changes: 10 additions & 8 deletions assets/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions assets/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,16 @@ a.RelatedCustomerConversation--link:hover {
border-radius: 4px;
box-shadow: 0 24px 48px rgba(17, 16, 62, 0.12);
}

/* Wrapper class for antd Upload component style overrides */
.AttachFileButton {
display: flex;
flex-direction: column-reverse;
float: left;
}

.AttachFileButton .ant-upload-list.ant-upload-list-text {
display: flex;
flex-wrap: wrap;
float: right;
}
5 changes: 5 additions & 0 deletions assets/src/components/common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import Table from 'antd/lib/table';
import Tag from 'antd/lib/tag';
import Tooltip from 'antd/lib/tooltip';
import Typography from 'antd/lib/typography';
import Upload from 'antd/lib/upload';

import {
blue,
Expand All @@ -36,6 +37,9 @@ import {

import DatePicker from './DatePicker';

export type {UploadChangeParam} from 'antd/lib/upload';
export type {UploadFile} from 'antd/lib/upload/interface';

const {Title, Text, Paragraph} = Typography;
const {Header, Content, Footer, Sider} = Layout;
const {RangePicker} = DatePicker;
Expand Down Expand Up @@ -126,4 +130,5 @@ export {
Table,
Tag,
Tooltip,
Upload,
};
6 changes: 6 additions & 0 deletions assets/src/components/conversations/ChatMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ const ChatMessage = ({
user,
seen_at,
private: isPrivate,
attachments = [],
} = message;
const isAgent = !!user;
const tooltip = getSenderIdentifier(customer, user);
Expand All @@ -123,6 +124,8 @@ const ChatMessage = ({
const customerId = customer && customer.id;
const color = getColorByUuid(customerId);

// TODO: might be nice to push the boolean logic related to color down to the ChatMessageBox
// Maybe have PrivateChatMessageBox, ChatMessageBox, OtherCustomerMessageBox
if (isMe) {
return (
<Box pr={0} pl={4} pb={isLastInGroup ? 3 : 2}>
Expand All @@ -135,6 +138,8 @@ const ChatMessage = ({
py: 2,
background: isPrivate ? colors.note : colors.primary,
}}
attachmentTextColor={isPrivate ? colors.text : colors.white}
attachments={attachments}
/>
</Flex>
{shouldDisplayTimestamp && (
Expand Down Expand Up @@ -167,6 +172,7 @@ const ChatMessage = ({
background: isPrivate ? colors.note : 'rgb(245, 245, 245)',
maxWidth: '80%',
}}
attachments={attachments}
/>
</Flex>
{shouldDisplayTimestamp && (
Expand Down
53 changes: 51 additions & 2 deletions assets/src/components/conversations/ChatMessageBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,55 @@ import breaks from 'remark-breaks';
import {Twemoji} from 'react-emoji-render';
import {Box} from 'theme-ui';
import {allowedNodeTypes} from '../common';
import {Attachment} from '../../types';
import {PaperClipOutlined} from '../icons';

const renderers = {
text: (props: any) => {
return <Twemoji text={props.children} />;
},
};

type ChatMessageBoxProps = {
const ChatMessageAttachment = ({
attachment,
color,
}: {
attachment: Attachment;
color?: string;
}) => {
const {id, filename, file_url: fileUrl} = attachment;

return (
<Box key={id}>
<PaperClipOutlined />{' '}
<a
href={fileUrl}
style={{
color,
textDecoration: 'underline',
}}
>
{filename}
</a>
</Box>
);
};

type Props = {
className?: string;
content: string;
sx?: Record<any, any>;
attachments?: Attachment[];
attachmentTextColor?: string;
};

const ChatMessageBox = ({className, content, sx}: ChatMessageBoxProps) => {
const ChatMessageBox = ({
className,
content,
sx,
attachments = [],
attachmentTextColor,
}: Props) => {
const parsedSx = Object.assign(sx, {
borderRadius: 4,
p: {
Expand All @@ -39,6 +74,20 @@ const ChatMessageBox = ({className, content, sx}: ChatMessageBoxProps) => {
renderers={renderers}
plugins={[breaks]}
/>

{attachments && attachments.length > 0 && (
<Box mt={2} className={className}>
{attachments.map((attachment) => {
return (
<ChatMessageAttachment
key={attachment.id}
attachment={attachment}
color={attachmentTextColor}
/>
);
})}
</Box>
)}
</Box>
);
};
Expand Down
118 changes: 111 additions & 7 deletions assets/src/components/conversations/ConversationFooter.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,79 @@
import React from 'react';
import {Box, Flex} from 'theme-ui';
import {colors, Button, Menu, TextArea} from '../common';
import {Message, MessageType} from '../../types';
import {
colors,
Button,
Menu,
Text,
TextArea,
Upload,
UploadChangeParam,
UploadFile,
} from '../common';
import {Message, MessageType, User} from '../../types';
import {PaperClipOutlined} from '../icons';

const {REACT_APP_FILE_UPLOADS_ENABLED} = process.env;

const fileUploadsEnabled = (accountId?: string) => {
const enabled = REACT_APP_FILE_UPLOADS_ENABLED || '';

switch (enabled) {
case '1':
case 'true':
return true;
default:
return accountId && accountId.length && enabled.includes(accountId);
}
};

const AttachFileButton = ({
fileList,
currentUser,
onUpdateFileList,
}: {
fileList: any;
currentUser: User;
onUpdateFileList: (info: UploadChangeParam) => void;
}) => {
// Antd takes a url to make the post request and data that gets added to the request
// (See https://ant.design/components/upload/ for more information)
const action = '/api/upload';
// TODO: figure out a better way to set these!
const data = {account_id: currentUser.account_id, user_id: currentUser.id};

return (
<Upload
className="AttachFileButton"
action={action}
onChange={onUpdateFileList}
data={data}
fileList={fileList}
>
<Button icon={<PaperClipOutlined />} size="small" type="text">
<Text type="secondary">Attach a file</Text>
</Button>
</Upload>
);
};

const ConversationFooter = ({
sx = {},
onSendMessage,
currentUser,
}: {
sx?: any;
onSendMessage: (message: Partial<Message>) => void;
currentUser?: User | null;
}) => {
const [message, setMessage] = React.useState<string>('');
const [fileList, setFileList] = React.useState<Array<UploadFile>>([]);
const [messageType, setMessageType] = React.useState<MessageType>('reply');
const [isSendDisabled, setSendDisabled] = React.useState<boolean>(false);

const isPrivateNote = messageType === 'note';
const accountId = currentUser?.account_id;
const shouldDisplayUploadButton = fileUploadsEnabled(accountId);

const handleMessageChange = (e: React.ChangeEvent<HTMLTextAreaElement>) =>
setMessage(e.target.value);
Expand All @@ -36,11 +96,27 @@ const ConversationFooter = ({
body: message,
type: messageType,
private: isPrivateNote,
file_ids: fileList.map((f) => f.response?.data?.id),
});

setFileList([]);
setMessage('');
};

const onUpdateFileList = ({file, fileList, event}: UploadChangeParam) => {
setFileList(fileList);

// Disable send button when file upload is in progress
if (event) {
setSendDisabled(true);
}

// Enable send button again when the server has responded
if (file && file.response) {
setSendDisabled(false);
}
};

return (
<Box style={{flex: '0 0 auto'}}>
<Box
Expand Down Expand Up @@ -110,11 +186,39 @@ const ConversationFooter = ({
onChange={handleMessageChange}
/>
</Box>
<Flex sx={{justifyContent: 'flex-end'}}>
<Button type="primary" htmlType="submit">
Send
</Button>
</Flex>
{shouldDisplayUploadButton ? (
<Flex
sx={{
alignItems: 'flex-end',
justifyContent: 'space-between',
}}
>
{currentUser && (
<AttachFileButton
fileList={fileList}
currentUser={currentUser}
onUpdateFileList={onUpdateFileList}
/>
)}
<Button
type="primary"
htmlType="submit"
disabled={isSendDisabled}
>
Send
</Button>
</Flex>
) : (
<Flex sx={{justifyContent: 'flex-end'}}>
<Button
type="primary"
htmlType="submit"
disabled={isSendDisabled}
>
Send
</Button>
</Flex>
)}
</form>
</Box>
</Box>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ class ConversationsContainer extends React.Component<Props, State> {
<ConversationFooter
key={selectedConversation.id}
onSendMessage={this.handleSendMessage}
currentUser={currentUser}
/>
)}

Expand Down
6 changes: 4 additions & 2 deletions assets/src/components/conversations/ConversationsProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -455,9 +455,11 @@ export class ConversationsProvider extends React.Component<Props, State> {
);
}

const {body} = message;
const {body, file_ids} = message;
const hasEmptyBody = !body || body.trim().length === 0;
const hasNoAttachments = !file_ids || file_ids.length === 0;

if (!this.channel || !body || body.trim().length === 0) {
if (!this.channel || (hasEmptyBody && hasNoAttachments)) {
return;
}

Expand Down
Loading

0 comments on commit 6cc001c

Please sign in to comment.