Add scaffolding for media gallery
							parent
							
								
									a8be4f2d8d
								
							
						
					
					
						commit
						fc1c3aabf5
					
				@ -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>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -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>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@ -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>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface State {
 | 
			
		||||
  selectedTab: AttachmentType;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class MediaGallery extends React.Component<Props, {}> {
 | 
			
		||||
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;
 | 
			
		||||
}
 | 
			
		||||
					Loading…
					
					
				
		Reference in New Issue