Moves show all media to react pane

This commit is contained in:
Josh Perez 2022-12-20 12:50:23 -05:00 committed by GitHub
parent cdb779508b
commit d8ea9856ec
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 572 additions and 543 deletions

View file

@ -47,7 +47,6 @@ const commonProps = {
'onOutgoingVideoCallInConversation'
),
onShowAllMedia: action('onShowAllMedia'),
onGoBack: action('onGoBack'),
onArchive: action('onArchive'),

View file

@ -92,7 +92,6 @@ export type PropsActionsType = {
onOutgoingAudioCallInConversation: (conversationId: string) => void;
onOutgoingVideoCallInConversation: (conversationId: string) => void;
onSearchInConversation: () => void;
onShowAllMedia: () => void;
pushPanelForConversation: PushPanelForConversationActionType;
setDisappearingMessages: (
conversationId: string,
@ -350,7 +349,6 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
onArchive,
onMarkUnread,
onMoveToInbox,
onShowAllMedia,
pushPanelForConversation,
setDisappearingMessages,
setMuteExpiration,
@ -494,7 +492,13 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
{i18n('showMembers')}
</MenuItem>
) : null}
<MenuItem onClick={onShowAllMedia}>{i18n('viewRecentMedia')}</MenuItem>
<MenuItem
onClick={() =>
pushPanelForConversation(id, { type: PanelType.AllMedia })
}
>
{i18n('viewRecentMedia')}
</MenuItem>
<MenuItem divider />
{!markedUnread ? (
<MenuItem onClick={onMarkUnread}>{i18n('markUnread')}</MenuItem>

View file

@ -6,6 +6,7 @@
import * as React from 'react';
import { select, text } from '@storybook/addon-knobs';
import { random, range, sample, sortBy } from 'lodash';
import { action } from '@storybook/addon-actions';
import { setupI18n } from '../../../util/setupI18n';
import enMessages from '../../../../_locales/en/messages.json';
@ -106,6 +107,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
overrideProps.type || 'media'
),
mediaItems: overrideProps.mediaItems || [],
onItemClick: action('onItemClick'),
});
export function Documents() {

View file

@ -1,81 +1,69 @@
// Copyright 2018-2021 Signal Messenger, LLC
// Copyright 2018-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { DocumentListItem } from './DocumentListItem';
import type { ItemClickEvent } from './types/ItemClickEvent';
import { MediaGridItem } from './MediaGridItem';
import type { MediaItemType } from '../../../types/MediaItem';
import { missingCaseError } from '../../../util/missingCaseError';
import type { LocalizerType } from '../../../types/Util';
import type { MediaItemType } from '../../../types/MediaItem';
import { DocumentListItem } from './DocumentListItem';
import { MediaGridItem } from './MediaGridItem';
import { getMessageTimestamp } from '../../../util/getMessageTimestamp';
import { missingCaseError } from '../../../util/missingCaseError';
export type Props = {
i18n: LocalizerType;
header?: string;
type: 'media' | 'documents';
i18n: LocalizerType;
mediaItems: Array<MediaItemType>;
onItemClick?: (event: ItemClickEvent) => void;
onItemClick: (event: ItemClickEvent) => unknown;
type: 'media' | 'documents';
};
export class AttachmentSection extends React.Component<Props> {
public override render(): JSX.Element {
const { header } = this.props;
export function AttachmentSection({
i18n,
header,
type,
mediaItems,
onItemClick,
}: Props): JSX.Element {
return (
<div className="module-attachment-section">
<h2 className="module-attachment-section__header">{header}</h2>
<div className="module-attachment-section__items">
{mediaItems.map((mediaItem, position, array) => {
const shouldShowSeparator = position < array.length - 1;
const { message, index, attachment } = mediaItem;
return (
<div className="module-attachment-section">
<h2 className="module-attachment-section__header">{header}</h2>
<div className="module-attachment-section__items">
{this.renderItems()}
</div>
const onClick = () => {
onItemClick({ type, message, attachment });
};
switch (type) {
case 'media':
return (
<MediaGridItem
key={`${message.id}-${index}`}
mediaItem={mediaItem}
onClick={onClick}
i18n={i18n}
/>
);
case 'documents':
return (
<DocumentListItem
key={`${message.id}-${index}`}
fileName={attachment.fileName}
fileSize={attachment.size}
shouldShowSeparator={shouldShowSeparator}
onClick={onClick}
timestamp={getMessageTimestamp(message)}
/>
);
default:
return missingCaseError(type);
}
})}
</div>
);
}
private renderItems() {
const { i18n, mediaItems, type } = this.props;
return mediaItems.map((mediaItem, position, array) => {
const shouldShowSeparator = position < array.length - 1;
const { message, index, attachment } = mediaItem;
const onClick = this.createClickHandler(mediaItem);
switch (type) {
case 'media':
return (
<MediaGridItem
key={`${message.id}-${index}`}
mediaItem={mediaItem}
onClick={onClick}
i18n={i18n}
/>
);
case 'documents':
return (
<DocumentListItem
key={`${message.id}-${index}`}
fileName={attachment.fileName}
fileSize={attachment.size}
shouldShowSeparator={shouldShowSeparator}
onClick={onClick}
timestamp={getMessageTimestamp(message)}
/>
);
default:
return missingCaseError(type);
}
});
}
private readonly createClickHandler = (mediaItem: MediaItemType) => () => {
const { onItemClick, type } = this.props;
const { message, attachment } = mediaItem;
if (!onItemClick) {
return;
}
onItemClick({ type, message, attachment });
};
</div>
);
}

View file

@ -1,4 +1,4 @@
// Copyright 2020 Signal Messenger, LLC
// Copyright 2020-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
@ -24,10 +24,13 @@ export default {
};
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
i18n,
onItemClick: action('onItemClick'),
conversationId: '123',
documents: overrideProps.documents || [],
i18n,
loadMediaItems: action('loadMediaItems'),
media: overrideProps.media || [],
saveAttachment: action('saveAttachment'),
showLightboxWithMedia: action('showLightboxWithMedia'),
});
export function Populated(): JSX.Element {

View file

@ -1,170 +1,170 @@
// Copyright 2018-2021 Signal Messenger, LLC
// Copyright 2018-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import classNames from 'classnames';
import React, { useEffect, useRef } from 'react';
import moment from 'moment';
import type { ItemClickEvent } from './types/ItemClickEvent';
import type { LocalizerType } from '../../../types/Util';
import type { MediaItemType } from '../../../types/MediaItem';
import type { SaveAttachmentActionCreatorType } from '../../../state/ducks/conversations';
import { AttachmentSection } from './AttachmentSection';
import { EmptyState } from './EmptyState';
import { groupMediaItemsByDate } from './groupMediaItemsByDate';
import type { ItemClickEvent } from './types/ItemClickEvent';
import { missingCaseError } from '../../../util/missingCaseError';
import type { LocalizerType } from '../../../types/Util';
import { Tabs } from '../../Tabs';
import { getMessageTimestamp } from '../../../util/getMessageTimestamp';
import { groupMediaItemsByDate } from './groupMediaItemsByDate';
import { missingCaseError } from '../../../util/missingCaseError';
import type { MediaItemType } from '../../../types/MediaItem';
enum TabViews {
Media = 'Media',
Documents = 'Documents',
}
export type Props = {
conversationId: string;
documents: Array<MediaItemType>;
i18n: LocalizerType;
loadMediaItems: (id: string) => unknown;
media: Array<MediaItemType>;
onItemClick?: (event: ItemClickEvent) => void;
};
type State = {
selectedTab: 'media' | 'documents';
saveAttachment: SaveAttachmentActionCreatorType;
showLightboxWithMedia: (
selectedAttachmentPath: string | undefined,
media: Array<MediaItemType>
) => void;
};
const MONTH_FORMAT = 'MMMM YYYY';
type TabSelectEvent = {
type: 'media' | 'documents';
};
function Tab({
isSelected,
label,
onSelect,
function MediaSection({
type,
}: {
isSelected: boolean;
label: string;
onSelect?: (event: TabSelectEvent) => void;
type: 'media' | 'documents';
}) {
const handleClick = onSelect
? () => {
onSelect({ type });
i18n,
media,
documents,
saveAttachment,
showLightboxWithMedia,
}: Pick<
Props,
'i18n' | 'media' | 'documents' | 'showLightboxWithMedia' | 'saveAttachment'
> & { type: 'media' | 'documents' }): JSX.Element {
const mediaItems = type === 'media' ? media : documents;
if (!mediaItems || mediaItems.length === 0) {
const label = (() => {
switch (type) {
case 'media':
return i18n('mediaEmptyState');
case 'documents':
return i18n('documentsEmptyState');
default:
throw missingCaseError(type);
}
: undefined;
})();
return <EmptyState data-test="EmptyState" label={label} />;
}
const now = Date.now();
const sections = groupMediaItemsByDate(now, mediaItems).map(section => {
const first = section.mediaItems[0];
const { message } = first;
const date = moment(getMessageTimestamp(message));
const header =
section.type === 'yearMonth'
? date.format(MONTH_FORMAT)
: i18n(section.type);
return (
<AttachmentSection
key={header}
header={header}
i18n={i18n}
type={type}
mediaItems={section.mediaItems}
onItemClick={(event: ItemClickEvent) => {
switch (event.type) {
case 'documents': {
saveAttachment(event.attachment, event.message.sent_at);
break;
}
case 'media': {
showLightboxWithMedia(event.attachment.path, media);
break;
}
default:
throw new TypeError(`Unknown attachment type: '${event.type}'`);
}
}}
/>
);
});
return <div className="module-media-gallery__sections">{sections}</div>;
}
export function MediaGallery({
conversationId,
documents,
i18n,
loadMediaItems,
media,
saveAttachment,
showLightboxWithMedia,
}: Props): JSX.Element {
const focusRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
focusRef.current?.focus();
}, []);
useEffect(() => {
loadMediaItems(conversationId);
}, [conversationId, loadMediaItems]);
return (
// Has key events handled elsewhere
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
<div
className={classNames(
'module-media-gallery__tab',
isSelected ? 'module-media-gallery__tab--active' : null
)}
onClick={handleClick}
role="tab"
tabIndex={0}
>
{label}
<div className="module-media-gallery" tabIndex={-1} ref={focusRef}>
<Tabs
initialSelectedTab={TabViews.Media}
tabs={[
{
id: TabViews.Media,
label: i18n('media'),
},
{
id: TabViews.Documents,
label: i18n('documents'),
},
]}
>
{({ selectedTab }) => (
<div className="module-media-gallery__content">
{selectedTab === TabViews.Media && (
<MediaSection
documents={documents}
i18n={i18n}
media={media}
saveAttachment={saveAttachment}
showLightboxWithMedia={showLightboxWithMedia}
type="media"
/>
)}
{selectedTab === TabViews.Documents && (
<MediaSection
documents={documents}
i18n={i18n}
media={media}
saveAttachment={saveAttachment}
showLightboxWithMedia={showLightboxWithMedia}
type="documents"
/>
)}
</div>
)}
</Tabs>
</div>
);
}
export class MediaGallery extends React.Component<Props, State> {
public readonly focusRef: React.RefObject<HTMLDivElement> = React.createRef();
constructor(props: Props) {
super(props);
this.state = {
selectedTab: 'media',
};
}
public override componentDidMount(): void {
// When this component is created, it's initially not part of the DOM, and then it's
// added off-screen and animated in. This ensures that the focus takes.
setTimeout(() => {
if (this.focusRef.current) {
this.focusRef.current.focus();
}
});
}
public override render(): JSX.Element {
const { i18n } = this.props;
const { selectedTab } = this.state;
return (
<div className="module-media-gallery" tabIndex={-1} ref={this.focusRef}>
<div className="module-media-gallery__tab-container">
<Tab
label={i18n('media')}
type="media"
isSelected={selectedTab === 'media'}
onSelect={this.handleTabSelect}
/>
<Tab
label={i18n('documents')}
type="documents"
isSelected={selectedTab === 'documents'}
onSelect={this.handleTabSelect}
/>
</div>
<div className="module-media-gallery__content">
{this.renderSections()}
</div>
</div>
);
}
private readonly handleTabSelect = (event: TabSelectEvent): void => {
this.setState({ selectedTab: event.type });
};
private renderSections() {
const { i18n, media, documents, onItemClick } = this.props;
const { selectedTab } = this.state;
const mediaItems = selectedTab === 'media' ? media : documents;
const type = selectedTab;
if (!mediaItems || mediaItems.length === 0) {
const label = (() => {
switch (type) {
case 'media':
return i18n('mediaEmptyState');
case 'documents':
return i18n('documentsEmptyState');
default:
throw missingCaseError(type);
}
})();
return <EmptyState data-test="EmptyState" label={label} />;
}
const now = Date.now();
const sections = groupMediaItemsByDate(now, mediaItems).map(section => {
const first = section.mediaItems[0];
const { message } = first;
const date = moment(getMessageTimestamp(message));
const header =
section.type === 'yearMonth'
? date.format(MONTH_FORMAT)
: i18n(section.type);
return (
<AttachmentSection
key={header}
header={header}
i18n={i18n}
type={type}
mediaItems={section.mediaItems}
onItemClick={onItemClick}
/>
);
});
return <div className="module-media-gallery__sections">{sections}</div>;
}
}