Add scaffolding for media gallery
This commit is contained in:
parent
a8be4f2d8d
commit
fc1c3aabf5
10 changed files with 382 additions and 4 deletions
|
@ -12,6 +12,11 @@ module.exports = {
|
|||
description: 'Everything necessary to render a conversation',
|
||||
components: 'ts/components/conversation/*.tsx',
|
||||
},
|
||||
{
|
||||
name: 'Media Gallery',
|
||||
description: 'Display media and documents in a conversation',
|
||||
components: 'ts/components/conversation/media-gallery/*.tsx',
|
||||
},
|
||||
{
|
||||
name: 'Utility',
|
||||
description: 'Utility components used across the application',
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
import React from 'react';
|
||||
|
||||
import { ImageThumbnail } from './ImageThumbnail';
|
||||
import { DocumentListEntry } from './DocumentListEntry';
|
||||
import { Message } from './propTypes/Message';
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
width: '100%',
|
||||
},
|
||||
header: {
|
||||
fontFamily: '',
|
||||
},
|
||||
itemContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'flex-start',
|
||||
} as React.CSSProperties,
|
||||
};
|
||||
|
||||
interface Props {
|
||||
i18n: (value: string) => string;
|
||||
header?: string;
|
||||
type: 'media' | 'documents';
|
||||
messages: Array<Message>;
|
||||
}
|
||||
|
||||
export class AttachmentListSection extends React.Component<Props, {}> {
|
||||
public renderItems() {
|
||||
const { i18n, messages, type } = this.props;
|
||||
const Component = type === 'media' ? ImageThumbnail : DocumentListEntry;
|
||||
|
||||
return messages.map((message) => (
|
||||
<Component
|
||||
key={message.id}
|
||||
i18n={i18n}
|
||||
message={message}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { header } = this.props;
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<div style={styles.header}>{header}</div>
|
||||
<div style={styles.itemContainer}>
|
||||
{this.renderItems()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
DocumentListEntry example:
|
||||
|
||||
```js
|
||||
<DocumentListEntry
|
||||
fileName="meow.jpg"
|
||||
fileSize={1024 * 1000 * 2}
|
||||
timestamp={Date.now()}
|
||||
/>
|
||||
<DocumentListEntry
|
||||
fileName="rickroll.wmv"
|
||||
fileSize={1024 * 1000 * 8}
|
||||
timestamp={Date.now() - 24 * 60 * 1000}
|
||||
/>
|
||||
<DocumentListEntry
|
||||
fileName="kitten.gif"
|
||||
fileSize={1024 * 1000 * 1.2}
|
||||
timestamp={Date.now() - 14 * 24 * 60 * 1000}
|
||||
/>
|
||||
```
|
|
@ -0,0 +1,97 @@
|
|||
import React from 'react';
|
||||
|
||||
import moment from 'moment';
|
||||
import formatFileSize from 'filesize';
|
||||
|
||||
// import { LoadingIndicator } from './LoadingIndicator';
|
||||
|
||||
|
||||
interface Props {
|
||||
fileName: string | null;
|
||||
fileSize?: number;
|
||||
i18n: (key: string, values?: Array<string>) => string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
width: '100%',
|
||||
height: 72,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#ccc',
|
||||
borderBottomStyle: 'solid',
|
||||
},
|
||||
itemContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'nowrap',
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
} as React.CSSProperties,
|
||||
itemMetadata: {
|
||||
display: 'inline-flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
flexShrink: 0,
|
||||
marginLeft: 8,
|
||||
marginRight: 8,
|
||||
} as React.CSSProperties,
|
||||
itemDate: {
|
||||
display: 'inline-block',
|
||||
flexShrink: 0,
|
||||
},
|
||||
itemIcon: {
|
||||
flexShrink: 0,
|
||||
},
|
||||
itemFileSize: {
|
||||
display: 'inline-block',
|
||||
marginTop: 8,
|
||||
fontSize: '80%',
|
||||
},
|
||||
};
|
||||
|
||||
export class DocumentListEntry extends React.Component<Props, {}> {
|
||||
public renderContent() {
|
||||
const { fileName, fileSize, timestamp } = this.props;
|
||||
|
||||
// if (!attachment.data) {
|
||||
// return <LoadingIndicator />;
|
||||
// }
|
||||
|
||||
return (
|
||||
<div
|
||||
style={styles.itemContainer}
|
||||
>
|
||||
<img
|
||||
src="images/file.svg"
|
||||
width="48"
|
||||
height="48"
|
||||
style={styles.itemIcon}
|
||||
/>
|
||||
<div
|
||||
style={styles.itemMetadata}
|
||||
>
|
||||
<strong>{fileName}</strong>
|
||||
<span
|
||||
style={styles.itemFileSize}
|
||||
>
|
||||
{typeof fileSize === 'number' ? formatFileSize(fileSize) : ''}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
style={styles.itemDate}
|
||||
>
|
||||
{moment(timestamp).format('dddd, MMMM D, Y')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
{this.renderContent()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
44
ts/components/conversation/media-gallery/ImageThumbnail.tsx
Normal file
44
ts/components/conversation/media-gallery/ImageThumbnail.tsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
import React from 'react';
|
||||
|
||||
import { LoadingIndicator } from './LoadingIndicator';
|
||||
import { Message } from './propTypes/Message';
|
||||
|
||||
interface Props {
|
||||
message: Message;
|
||||
i18n: (value: string) => string;
|
||||
}
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
backgroundColor: '#f3f3f3',
|
||||
marginRight: 4,
|
||||
marginBottom: 4,
|
||||
width: 94,
|
||||
height: 94,
|
||||
},
|
||||
};
|
||||
|
||||
export class ImageThumbnail extends React.Component<Props, {}> {
|
||||
public renderContent() {
|
||||
const { i18n, message } = this.props;
|
||||
|
||||
if (!message.imageUrl) {
|
||||
return <LoadingIndicator />;
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
src={message.imageUrl}
|
||||
alt={`${i18n('messageCaption')}: ${message.body}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
{this.renderContent()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import React from 'react';
|
||||
|
||||
export const LoadingIndicator = () => {
|
||||
return (
|
||||
<div className="loading-widget">
|
||||
<div className="container">
|
||||
<span className="dot" />
|
||||
<span className="dot" />
|
||||
<span className="dot" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
33
ts/components/conversation/media-gallery/MediaGallery.md
Normal file
33
ts/components/conversation/media-gallery/MediaGallery.md
Normal file
|
@ -0,0 +1,33 @@
|
|||
```jsx
|
||||
const YEAR_MS = 1 * 12 * 30 * 24 * 60 * 60 * 1000;
|
||||
const tokens = ['foo', 'bar', 'baz', 'qux', 'quux'];
|
||||
const fileExtensions = ['docx', 'pdf', 'txt', 'mp3', 'wmv', 'tiff'];
|
||||
const createRandomMessage = (props) => {
|
||||
const now = Date.now();
|
||||
const fileName =
|
||||
`${_.sample(tokens)}${_.sample(tokens)}.${_.sample(fileExtensions)}`;
|
||||
return {
|
||||
id: _.random(now).toString(),
|
||||
received_at: _.random(now - YEAR_MS, now),
|
||||
attachments: [{
|
||||
fileName,
|
||||
data: null,
|
||||
}],
|
||||
|
||||
// TODO: Revisit
|
||||
imageUrl: 'https://placekitten.com/94/94',
|
||||
...props,
|
||||
};
|
||||
};
|
||||
|
||||
const startTime = Date.now();
|
||||
const messages = _.sortBy(
|
||||
_.range(30).map(createRandomMessage),
|
||||
message => -message.received_at
|
||||
);
|
||||
|
||||
<MediaGallery
|
||||
i18n={(key) => key}
|
||||
messages={messages}
|
||||
/>
|
||||
```
|
|
@ -1,13 +1,112 @@
|
|||
import React from 'react';
|
||||
|
||||
import { AttachmentListSection } from './AttachmentListSection';
|
||||
import { Message } from './propTypes/Message';
|
||||
|
||||
|
||||
type AttachmentType = 'media' | 'documents';
|
||||
|
||||
interface Props {
|
||||
number: number;
|
||||
i18n: (key: string, values?: Array<string>) => string;
|
||||
messages: Array<Message>;
|
||||
}
|
||||
|
||||
export class MediaGallery extends React.Component<Props, {}> {
|
||||
interface State {
|
||||
selectedTab: AttachmentType;
|
||||
}
|
||||
|
||||
const COLOR_GREY = '#f3f3f3';
|
||||
|
||||
const tabStyle = {
|
||||
width: '100%',
|
||||
backgroundColor: COLOR_GREY,
|
||||
padding: 20,
|
||||
textAlign: 'center',
|
||||
};
|
||||
|
||||
const styles = {
|
||||
tabContainer: {
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
},
|
||||
tab: {
|
||||
default: tabStyle,
|
||||
active: {
|
||||
...tabStyle,
|
||||
borderBottom: '2px solid #08f',
|
||||
},
|
||||
},
|
||||
attachmentsContainer: {
|
||||
padding: 20,
|
||||
},
|
||||
};
|
||||
|
||||
interface TabSelectEvent {
|
||||
type: AttachmentType;
|
||||
}
|
||||
|
||||
const Tab = ({
|
||||
isSelected,
|
||||
label,
|
||||
onSelect,
|
||||
type,
|
||||
}: {
|
||||
isSelected: boolean,
|
||||
label: string,
|
||||
onSelect?: (event: TabSelectEvent) => void,
|
||||
type: AttachmentType,
|
||||
}) => {
|
||||
const handleClick = onSelect ?
|
||||
() => onSelect({ type }) : undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={isSelected ? styles.tab.active : styles.tab.default}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export class MediaGallery extends React.Component<Props, State> {
|
||||
public state: State = {
|
||||
selectedTab: 'media',
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { selectedTab } = this.state;
|
||||
|
||||
return (
|
||||
<div>Hello Media Gallery! Number: {this.props.number}</div>
|
||||
<div>
|
||||
<div style={styles.tabContainer}>
|
||||
<Tab
|
||||
label="Media"
|
||||
type="media"
|
||||
isSelected={selectedTab === 'media'}
|
||||
onSelect={this.handleTabSelect}
|
||||
/>
|
||||
<Tab
|
||||
label="Documents"
|
||||
type="documents"
|
||||
isSelected={selectedTab === 'documents'}
|
||||
onSelect={this.handleTabSelect}
|
||||
/>
|
||||
</div>
|
||||
<div style={styles.attachmentsContainer}>
|
||||
<AttachmentListSection
|
||||
type={selectedTab}
|
||||
i18n={this.props.i18n}
|
||||
messages={this.props.messages}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private handleTabSelect = (event: TabSelectEvent): void => {
|
||||
this.setState({selectedTab: event.type});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
export interface Message {
|
||||
id: string;
|
||||
body?: string;
|
||||
received_at: number;
|
||||
attachments: Array<{
|
||||
data?: ArrayBuffer;
|
||||
fileName?: string;
|
||||
}>;
|
||||
|
||||
// TODO: Revisit
|
||||
imageUrl: string;
|
||||
}
|
|
@ -19,7 +19,7 @@ export interface Attachment {
|
|||
// key?: ArrayBuffer;
|
||||
// digest?: ArrayBuffer;
|
||||
// flags?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const isVisualMedia = (attachment: Attachment): boolean => {
|
||||
const { contentType } = attachment;
|
||||
|
|
Loading…
Add table
Reference in a new issue