Moves show all media to react pane
This commit is contained in:
parent
cdb779508b
commit
d8ea9856ec
25 changed files with 572 additions and 543 deletions
|
@ -2232,42 +2232,6 @@ button.ConversationDetails__action-button {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-media-gallery__tab-container {
|
|
||||||
display: flex;
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-media-gallery__tab {
|
|
||||||
width: 100%;
|
|
||||||
padding: 20px;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
@include light-theme {
|
|
||||||
background-color: $color-gray-02;
|
|
||||||
}
|
|
||||||
@include dark-theme {
|
|
||||||
background-color: $color-gray-90;
|
|
||||||
}
|
|
||||||
|
|
||||||
outline: none;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
@include keyboard-mode {
|
|
||||||
background-color: $color-gray-15;
|
|
||||||
}
|
|
||||||
@include dark-keyboard-mode {
|
|
||||||
background-color: $color-gray-75;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-media-gallery__tab--active {
|
|
||||||
border-bottom: 2px solid $color-ultramarine;
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-media-gallery__content {
|
.module-media-gallery__content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
|
@ -84,6 +84,7 @@
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
// Using this so that the zoom cleanly goes over the footer
|
// Using this so that the zoom cleanly goes over the footer
|
||||||
|
@ -197,6 +198,7 @@
|
||||||
height: 56px;
|
height: 56px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
|
min-height: 56px;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
transition: opacity 150ms cubic-bezier(0.17, 0.17, 0, 1);
|
transition: opacity 150ms cubic-bezier(0.17, 0.17, 0, 1);
|
||||||
|
|
|
@ -160,6 +160,7 @@ import { StoryViewModeType, StoryViewTargetType } from './types/Stories';
|
||||||
import { downloadOnboardingStory } from './util/downloadOnboardingStory';
|
import { downloadOnboardingStory } from './util/downloadOnboardingStory';
|
||||||
import { clearConversationDraftAttachments } from './util/clearConversationDraftAttachments';
|
import { clearConversationDraftAttachments } from './util/clearConversationDraftAttachments';
|
||||||
import { removeLinkPreview } from './services/LinkPreview';
|
import { removeLinkPreview } from './services/LinkPreview';
|
||||||
|
import { PanelType } from './types/Panels';
|
||||||
|
|
||||||
const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000;
|
const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000;
|
||||||
|
|
||||||
|
@ -1121,6 +1122,10 @@ export async function startApp(): Promise<void> {
|
||||||
actionCreators.linkPreviews,
|
actionCreators.linkPreviews,
|
||||||
store.dispatch
|
store.dispatch
|
||||||
),
|
),
|
||||||
|
mediaGallery: bindActionCreators(
|
||||||
|
actionCreators.mediaGallery,
|
||||||
|
store.dispatch
|
||||||
|
),
|
||||||
network: bindActionCreators(actionCreators.network, store.dispatch),
|
network: bindActionCreators(actionCreators.network, store.dispatch),
|
||||||
safetyNumber: bindActionCreators(
|
safetyNumber: bindActionCreators(
|
||||||
actionCreators.safetyNumber,
|
actionCreators.safetyNumber,
|
||||||
|
@ -1522,7 +1527,10 @@ export async function startApp(): Promise<void> {
|
||||||
shiftKey &&
|
shiftKey &&
|
||||||
(key === 'm' || key === 'M')
|
(key === 'm' || key === 'M')
|
||||||
) {
|
) {
|
||||||
conversation.trigger('open-all-media');
|
window.reduxActions.conversations.pushPanelForConversation(
|
||||||
|
conversation.id,
|
||||||
|
{ type: PanelType.AllMedia }
|
||||||
|
);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -47,7 +47,6 @@ const commonProps = {
|
||||||
'onOutgoingVideoCallInConversation'
|
'onOutgoingVideoCallInConversation'
|
||||||
),
|
),
|
||||||
|
|
||||||
onShowAllMedia: action('onShowAllMedia'),
|
|
||||||
onGoBack: action('onGoBack'),
|
onGoBack: action('onGoBack'),
|
||||||
|
|
||||||
onArchive: action('onArchive'),
|
onArchive: action('onArchive'),
|
||||||
|
|
|
@ -92,7 +92,6 @@ export type PropsActionsType = {
|
||||||
onOutgoingAudioCallInConversation: (conversationId: string) => void;
|
onOutgoingAudioCallInConversation: (conversationId: string) => void;
|
||||||
onOutgoingVideoCallInConversation: (conversationId: string) => void;
|
onOutgoingVideoCallInConversation: (conversationId: string) => void;
|
||||||
onSearchInConversation: () => void;
|
onSearchInConversation: () => void;
|
||||||
onShowAllMedia: () => void;
|
|
||||||
pushPanelForConversation: PushPanelForConversationActionType;
|
pushPanelForConversation: PushPanelForConversationActionType;
|
||||||
setDisappearingMessages: (
|
setDisappearingMessages: (
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
|
@ -350,7 +349,6 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
||||||
onArchive,
|
onArchive,
|
||||||
onMarkUnread,
|
onMarkUnread,
|
||||||
onMoveToInbox,
|
onMoveToInbox,
|
||||||
onShowAllMedia,
|
|
||||||
pushPanelForConversation,
|
pushPanelForConversation,
|
||||||
setDisappearingMessages,
|
setDisappearingMessages,
|
||||||
setMuteExpiration,
|
setMuteExpiration,
|
||||||
|
@ -494,7 +492,13 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
||||||
{i18n('showMembers')}
|
{i18n('showMembers')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
) : null}
|
) : null}
|
||||||
<MenuItem onClick={onShowAllMedia}>{i18n('viewRecentMedia')}</MenuItem>
|
<MenuItem
|
||||||
|
onClick={() =>
|
||||||
|
pushPanelForConversation(id, { type: PanelType.AllMedia })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{i18n('viewRecentMedia')}
|
||||||
|
</MenuItem>
|
||||||
<MenuItem divider />
|
<MenuItem divider />
|
||||||
{!markedUnread ? (
|
{!markedUnread ? (
|
||||||
<MenuItem onClick={onMarkUnread}>{i18n('markUnread')}</MenuItem>
|
<MenuItem onClick={onMarkUnread}>{i18n('markUnread')}</MenuItem>
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { select, text } from '@storybook/addon-knobs';
|
import { select, text } from '@storybook/addon-knobs';
|
||||||
import { random, range, sample, sortBy } from 'lodash';
|
import { random, range, sample, sortBy } from 'lodash';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
|
||||||
import { setupI18n } from '../../../util/setupI18n';
|
import { setupI18n } from '../../../util/setupI18n';
|
||||||
import enMessages from '../../../../_locales/en/messages.json';
|
import enMessages from '../../../../_locales/en/messages.json';
|
||||||
|
@ -106,6 +107,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
overrideProps.type || 'media'
|
overrideProps.type || 'media'
|
||||||
),
|
),
|
||||||
mediaItems: overrideProps.mediaItems || [],
|
mediaItems: overrideProps.mediaItems || [],
|
||||||
|
onItemClick: action('onItemClick'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function Documents() {
|
export function Documents() {
|
||||||
|
|
|
@ -1,46 +1,43 @@
|
||||||
// Copyright 2018-2021 Signal Messenger, LLC
|
// Copyright 2018-2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { DocumentListItem } from './DocumentListItem';
|
|
||||||
import type { ItemClickEvent } from './types/ItemClickEvent';
|
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 { LocalizerType } from '../../../types/Util';
|
||||||
|
import type { MediaItemType } from '../../../types/MediaItem';
|
||||||
|
import { DocumentListItem } from './DocumentListItem';
|
||||||
|
import { MediaGridItem } from './MediaGridItem';
|
||||||
import { getMessageTimestamp } from '../../../util/getMessageTimestamp';
|
import { getMessageTimestamp } from '../../../util/getMessageTimestamp';
|
||||||
|
import { missingCaseError } from '../../../util/missingCaseError';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
i18n: LocalizerType;
|
|
||||||
header?: string;
|
header?: string;
|
||||||
type: 'media' | 'documents';
|
i18n: LocalizerType;
|
||||||
mediaItems: Array<MediaItemType>;
|
mediaItems: Array<MediaItemType>;
|
||||||
onItemClick?: (event: ItemClickEvent) => void;
|
onItemClick: (event: ItemClickEvent) => unknown;
|
||||||
|
type: 'media' | 'documents';
|
||||||
};
|
};
|
||||||
|
|
||||||
export class AttachmentSection extends React.Component<Props> {
|
export function AttachmentSection({
|
||||||
public override render(): JSX.Element {
|
i18n,
|
||||||
const { header } = this.props;
|
header,
|
||||||
|
type,
|
||||||
|
mediaItems,
|
||||||
|
onItemClick,
|
||||||
|
}: Props): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className="module-attachment-section">
|
<div className="module-attachment-section">
|
||||||
<h2 className="module-attachment-section__header">{header}</h2>
|
<h2 className="module-attachment-section__header">{header}</h2>
|
||||||
<div className="module-attachment-section__items">
|
<div className="module-attachment-section__items">
|
||||||
{this.renderItems()}
|
{mediaItems.map((mediaItem, position, array) => {
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderItems() {
|
|
||||||
const { i18n, mediaItems, type } = this.props;
|
|
||||||
|
|
||||||
return mediaItems.map((mediaItem, position, array) => {
|
|
||||||
const shouldShowSeparator = position < array.length - 1;
|
const shouldShowSeparator = position < array.length - 1;
|
||||||
const { message, index, attachment } = mediaItem;
|
const { message, index, attachment } = mediaItem;
|
||||||
|
|
||||||
const onClick = this.createClickHandler(mediaItem);
|
const onClick = () => {
|
||||||
|
onItemClick({ type, message, attachment });
|
||||||
|
};
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'media':
|
case 'media':
|
||||||
return (
|
return (
|
||||||
|
@ -65,17 +62,8 @@ export class AttachmentSection extends React.Component<Props> {
|
||||||
default:
|
default:
|
||||||
return missingCaseError(type);
|
return missingCaseError(type);
|
||||||
}
|
}
|
||||||
});
|
})}
|
||||||
}
|
</div>
|
||||||
|
</div>
|
||||||
private readonly createClickHandler = (mediaItem: MediaItemType) => () => {
|
);
|
||||||
const { onItemClick, type } = this.props;
|
|
||||||
const { message, attachment } = mediaItem;
|
|
||||||
|
|
||||||
if (!onItemClick) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
onItemClick({ type, message, attachment });
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
// Copyright 2020-2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
@ -24,10 +24,13 @@ export default {
|
||||||
};
|
};
|
||||||
|
|
||||||
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
i18n,
|
conversationId: '123',
|
||||||
onItemClick: action('onItemClick'),
|
|
||||||
documents: overrideProps.documents || [],
|
documents: overrideProps.documents || [],
|
||||||
|
i18n,
|
||||||
|
loadMediaItems: action('loadMediaItems'),
|
||||||
media: overrideProps.media || [],
|
media: overrideProps.media || [],
|
||||||
|
saveAttachment: action('saveAttachment'),
|
||||||
|
showLightboxWithMedia: action('showLightboxWithMedia'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function Populated(): JSX.Element {
|
export function Populated(): JSX.Element {
|
||||||
|
|
|
@ -1,130 +1,53 @@
|
||||||
// Copyright 2018-2021 Signal Messenger, LLC
|
// Copyright 2018-2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
import moment from 'moment';
|
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 { AttachmentSection } from './AttachmentSection';
|
||||||
import { EmptyState } from './EmptyState';
|
import { EmptyState } from './EmptyState';
|
||||||
import { groupMediaItemsByDate } from './groupMediaItemsByDate';
|
import { Tabs } from '../../Tabs';
|
||||||
import type { ItemClickEvent } from './types/ItemClickEvent';
|
|
||||||
import { missingCaseError } from '../../../util/missingCaseError';
|
|
||||||
import type { LocalizerType } from '../../../types/Util';
|
|
||||||
import { getMessageTimestamp } from '../../../util/getMessageTimestamp';
|
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 = {
|
export type Props = {
|
||||||
|
conversationId: string;
|
||||||
documents: Array<MediaItemType>;
|
documents: Array<MediaItemType>;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
loadMediaItems: (id: string) => unknown;
|
||||||
media: Array<MediaItemType>;
|
media: Array<MediaItemType>;
|
||||||
|
saveAttachment: SaveAttachmentActionCreatorType;
|
||||||
onItemClick?: (event: ItemClickEvent) => void;
|
showLightboxWithMedia: (
|
||||||
};
|
selectedAttachmentPath: string | undefined,
|
||||||
|
media: Array<MediaItemType>
|
||||||
type State = {
|
) => void;
|
||||||
selectedTab: 'media' | 'documents';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const MONTH_FORMAT = 'MMMM YYYY';
|
const MONTH_FORMAT = 'MMMM YYYY';
|
||||||
|
|
||||||
type TabSelectEvent = {
|
function MediaSection({
|
||||||
type: 'media' | 'documents';
|
|
||||||
};
|
|
||||||
|
|
||||||
function Tab({
|
|
||||||
isSelected,
|
|
||||||
label,
|
|
||||||
onSelect,
|
|
||||||
type,
|
type,
|
||||||
}: {
|
i18n,
|
||||||
isSelected: boolean;
|
media,
|
||||||
label: string;
|
documents,
|
||||||
onSelect?: (event: TabSelectEvent) => void;
|
saveAttachment,
|
||||||
type: 'media' | 'documents';
|
showLightboxWithMedia,
|
||||||
}) {
|
}: Pick<
|
||||||
const handleClick = onSelect
|
Props,
|
||||||
? () => {
|
'i18n' | 'media' | 'documents' | 'showLightboxWithMedia' | 'saveAttachment'
|
||||||
onSelect({ type });
|
> & { type: 'media' | 'documents' }): JSX.Element {
|
||||||
}
|
const mediaItems = type === 'media' ? media : documents;
|
||||||
: undefined;
|
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
if (!mediaItems || mediaItems.length === 0) {
|
||||||
const label = (() => {
|
const label = (() => {
|
||||||
|
@ -160,11 +83,88 @@ export class MediaGallery extends React.Component<Props, State> {
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
type={type}
|
type={type}
|
||||||
mediaItems={section.mediaItems}
|
mediaItems={section.mediaItems}
|
||||||
onItemClick={onItemClick}
|
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>;
|
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 (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,9 +56,7 @@ class ExpiringMessagesDeletionService {
|
||||||
|
|
||||||
// We do this to update the UI, if this message is being displayed somewhere
|
// We do this to update the UI, if this message is being displayed somewhere
|
||||||
message.trigger('expired');
|
message.trigger('expired');
|
||||||
window.reduxActions.lightbox.closeLightboxIfViewingExpiredMessage(
|
window.reduxActions.conversations.messageExpired(message.id);
|
||||||
message.id
|
|
||||||
);
|
|
||||||
|
|
||||||
if (conversation) {
|
if (conversation) {
|
||||||
// An expired message only counts as decrementing the message count, not
|
// An expired message only counts as decrementing the message count, not
|
||||||
|
|
|
@ -24,9 +24,7 @@ async function eraseTapToViewMessages() {
|
||||||
|
|
||||||
// We do this to update the UI, if this message is being displayed somewhere
|
// We do this to update the UI, if this message is being displayed somewhere
|
||||||
message.trigger('expired');
|
message.trigger('expired');
|
||||||
window.reduxActions.lightbox.closeLightboxIfViewingExpiredMessage(
|
window.reduxActions.conversations.messageExpired(message.id);
|
||||||
message.id
|
|
||||||
);
|
|
||||||
|
|
||||||
await message.eraseContents();
|
await message.eraseContents();
|
||||||
})
|
})
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { actions as globalModals } from './ducks/globalModals';
|
||||||
import { actions as items } from './ducks/items';
|
import { actions as items } from './ducks/items';
|
||||||
import { actions as lightbox } from './ducks/lightbox';
|
import { actions as lightbox } from './ducks/lightbox';
|
||||||
import { actions as linkPreviews } from './ducks/linkPreviews';
|
import { actions as linkPreviews } from './ducks/linkPreviews';
|
||||||
|
import { actions as mediaGallery } from './ducks/mediaGallery';
|
||||||
import { actions as network } from './ducks/network';
|
import { actions as network } from './ducks/network';
|
||||||
import { actions as safetyNumber } from './ducks/safetyNumber';
|
import { actions as safetyNumber } from './ducks/safetyNumber';
|
||||||
import { actions as search } from './ducks/search';
|
import { actions as search } from './ducks/search';
|
||||||
|
@ -44,6 +45,7 @@ export const actionCreators: ReduxActions = {
|
||||||
items,
|
items,
|
||||||
lightbox,
|
lightbox,
|
||||||
linkPreviews,
|
linkPreviews,
|
||||||
|
mediaGallery,
|
||||||
network,
|
network,
|
||||||
safetyNumber,
|
safetyNumber,
|
||||||
search,
|
search,
|
||||||
|
@ -72,6 +74,7 @@ export const mapDispatchToProps = {
|
||||||
...items,
|
...items,
|
||||||
...lightbox,
|
...lightbox,
|
||||||
...linkPreviews,
|
...linkPreviews,
|
||||||
|
...mediaGallery,
|
||||||
...network,
|
...network,
|
||||||
...safetyNumber,
|
...safetyNumber,
|
||||||
...search,
|
...search,
|
||||||
|
|
|
@ -469,8 +469,11 @@ export const SELECTED_CONVERSATION_CHANGED =
|
||||||
'conversations/SELECTED_CONVERSATION_CHANGED';
|
'conversations/SELECTED_CONVERSATION_CHANGED';
|
||||||
const PUSH_PANEL = 'conversations/PUSH_PANEL';
|
const PUSH_PANEL = 'conversations/PUSH_PANEL';
|
||||||
const POP_PANEL = 'conversations/POP_PANEL';
|
const POP_PANEL = 'conversations/POP_PANEL';
|
||||||
|
export const MESSAGE_EXPIRED = 'conversations/MESSAGE_EXPIRED';
|
||||||
|
export const MESSAGE_DELETED = 'MESSAGE_DELETED';
|
||||||
export const SET_VOICE_NOTE_PLAYBACK_RATE =
|
export const SET_VOICE_NOTE_PLAYBACK_RATE =
|
||||||
'conversations/SET_VOICE_NOTE_PLAYBACK_RATE';
|
'conversations/SET_VOICE_NOTE_PLAYBACK_RATE';
|
||||||
|
export const CONVERSATION_UNLOADED = 'CONVERSATION_UNLOADED';
|
||||||
|
|
||||||
export type CancelVerificationDataByConversationActionType = {
|
export type CancelVerificationDataByConversationActionType = {
|
||||||
type: typeof CANCEL_CONVERSATION_PENDING_VERIFICATION;
|
type: typeof CANCEL_CONVERSATION_PENDING_VERIFICATION;
|
||||||
|
@ -581,7 +584,7 @@ export type ConversationRemovedActionType = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type ConversationUnloadedActionType = {
|
export type ConversationUnloadedActionType = {
|
||||||
type: 'CONVERSATION_UNLOADED';
|
type: typeof CONVERSATION_UNLOADED;
|
||||||
payload: {
|
payload: {
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
@ -626,7 +629,7 @@ export type MessageChangedActionType = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type MessageDeletedActionType = {
|
export type MessageDeletedActionType = {
|
||||||
type: 'MESSAGE_DELETED';
|
type: typeof MESSAGE_DELETED;
|
||||||
payload: {
|
payload: {
|
||||||
id: string;
|
id: string;
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
|
@ -651,6 +654,13 @@ export type MessagesAddedActionType = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type MessageExpiredActionType = {
|
||||||
|
type: typeof MESSAGE_EXPIRED;
|
||||||
|
payload: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type RepairNewestMessageActionType = {
|
export type RepairNewestMessageActionType = {
|
||||||
type: 'REPAIR_NEWEST_MESSAGE';
|
type: 'REPAIR_NEWEST_MESSAGE';
|
||||||
payload: {
|
payload: {
|
||||||
|
@ -906,6 +916,7 @@ export const actions = {
|
||||||
messageChanged,
|
messageChanged,
|
||||||
messageDeleted,
|
messageDeleted,
|
||||||
messageExpanded,
|
messageExpanded,
|
||||||
|
messageExpired,
|
||||||
messagesAdded,
|
messagesAdded,
|
||||||
messagesReset,
|
messagesReset,
|
||||||
myProfileChanged,
|
myProfileChanged,
|
||||||
|
@ -1979,7 +1990,7 @@ function conversationRemoved(id: string): ConversationRemovedActionType {
|
||||||
}
|
}
|
||||||
function conversationUnloaded(id: string): ConversationUnloadedActionType {
|
function conversationUnloaded(id: string): ConversationUnloadedActionType {
|
||||||
return {
|
return {
|
||||||
type: 'CONVERSATION_UNLOADED',
|
type: CONVERSATION_UNLOADED,
|
||||||
payload: {
|
payload: {
|
||||||
id,
|
id,
|
||||||
},
|
},
|
||||||
|
@ -2118,7 +2129,7 @@ function messageDeleted(
|
||||||
conversationId: string
|
conversationId: string
|
||||||
): MessageDeletedActionType {
|
): MessageDeletedActionType {
|
||||||
return {
|
return {
|
||||||
type: 'MESSAGE_DELETED',
|
type: MESSAGE_DELETED,
|
||||||
payload: {
|
payload: {
|
||||||
id,
|
id,
|
||||||
conversationId,
|
conversationId,
|
||||||
|
@ -2137,6 +2148,14 @@ function messageExpanded(
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
function messageExpired(id: string): MessageExpiredActionType {
|
||||||
|
return {
|
||||||
|
type: MESSAGE_EXPIRED,
|
||||||
|
payload: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
function messagesAdded({
|
function messagesAdded({
|
||||||
conversationId,
|
conversationId,
|
||||||
isActive,
|
isActive,
|
||||||
|
@ -3707,7 +3726,7 @@ export function reducer(
|
||||||
...updateConversationLookups(undefined, existing, state),
|
...updateConversationLookups(undefined, existing, state),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (action.type === 'CONVERSATION_UNLOADED') {
|
if (action.type === CONVERSATION_UNLOADED) {
|
||||||
const { payload } = action;
|
const { payload } = action;
|
||||||
const { id } = payload;
|
const { id } = payload;
|
||||||
const existingConversation = state.messagesByConversation[id];
|
const existingConversation = state.messagesByConversation[id];
|
||||||
|
@ -4185,7 +4204,7 @@ export function reducer(
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (action.type === 'MESSAGE_DELETED') {
|
if (action.type === MESSAGE_DELETED) {
|
||||||
const { id, conversationId } = action.payload;
|
const { id, conversationId } = action.payload;
|
||||||
const { messagesByConversation, messagesLookup } = state;
|
const { messagesByConversation, messagesLookup } = state;
|
||||||
|
|
||||||
|
|
|
@ -6,9 +6,10 @@ import type { ThunkAction } from 'redux-thunk';
|
||||||
import type { AttachmentType } from '../../types/Attachment';
|
import type { AttachmentType } from '../../types/Attachment';
|
||||||
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
|
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
|
||||||
import type { MediaItemType } from '../../types/MediaItem';
|
import type { MediaItemType } from '../../types/MediaItem';
|
||||||
import type { StateType as RootStateType } from '../reducer';
|
import type { MessageExpiredActionType } from './conversations';
|
||||||
import type { ShowStickerPackPreviewActionType } from './globalModals';
|
import type { ShowStickerPackPreviewActionType } from './globalModals';
|
||||||
import type { ShowToastActionType } from './toast';
|
import type { ShowToastActionType } from './toast';
|
||||||
|
import type { StateType as RootStateType } from '../reducer';
|
||||||
|
|
||||||
import * as log from '../../logging/log';
|
import * as log from '../../logging/log';
|
||||||
import { getMessageById } from '../../messages/getMessageById';
|
import { getMessageById } from '../../messages/getMessageById';
|
||||||
|
@ -20,7 +21,7 @@ import {
|
||||||
import { isTapToView } from '../selectors/message';
|
import { isTapToView } from '../selectors/message';
|
||||||
import { SHOW_TOAST } from './toast';
|
import { SHOW_TOAST } from './toast';
|
||||||
import { ToastType } from '../../types/Toast';
|
import { ToastType } from '../../types/Toast';
|
||||||
import { saveAttachmentFromMessage } from './conversations';
|
import { MESSAGE_EXPIRED, saveAttachmentFromMessage } from './conversations';
|
||||||
import { showStickerPackPreview } from './globalModals';
|
import { showStickerPackPreview } from './globalModals';
|
||||||
import { useBoundActions } from '../../hooks/useBoundActions';
|
import { useBoundActions } from '../../hooks/useBoundActions';
|
||||||
|
|
||||||
|
@ -51,7 +52,10 @@ type ShowLightboxActionType = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type LightboxActionType = CloseLightboxActionType | ShowLightboxActionType;
|
type LightboxActionType =
|
||||||
|
| CloseLightboxActionType
|
||||||
|
| MessageExpiredActionType
|
||||||
|
| ShowLightboxActionType;
|
||||||
|
|
||||||
function closeLightbox(): ThunkAction<
|
function closeLightbox(): ThunkAction<
|
||||||
void,
|
void,
|
||||||
|
@ -83,34 +87,6 @@ function closeLightbox(): ThunkAction<
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeLightboxIfViewingExpiredMessage(
|
|
||||||
messageId: string
|
|
||||||
): ThunkAction<void, RootStateType, unknown, CloseLightboxActionType> {
|
|
||||||
return (dispatch, getState) => {
|
|
||||||
const { lightbox } = getState();
|
|
||||||
|
|
||||||
if (!lightbox.isShowingLightbox) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { isViewOnce, media } = lightbox;
|
|
||||||
|
|
||||||
if (!isViewOnce) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasExpiredMedia = media.some(item => item.message.id === messageId);
|
|
||||||
|
|
||||||
if (!hasExpiredMedia) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: CLOSE_LIGHTBOX,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function showLightboxWithMedia(
|
function showLightboxWithMedia(
|
||||||
selectedAttachmentPath: string | undefined,
|
selectedAttachmentPath: string | undefined,
|
||||||
media: Array<MediaItemType>
|
media: Array<MediaItemType>
|
||||||
|
@ -309,7 +285,6 @@ function showLightbox(opts: {
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
closeLightbox,
|
closeLightbox,
|
||||||
closeLightboxIfViewingExpiredMessage,
|
|
||||||
showLightbox,
|
showLightbox,
|
||||||
showLightboxForViewOnceMedia,
|
showLightboxForViewOnceMedia,
|
||||||
showLightboxWithMedia,
|
showLightboxWithMedia,
|
||||||
|
@ -340,5 +315,25 @@ export function reducer(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action.type === MESSAGE_EXPIRED) {
|
||||||
|
if (!state.isShowingLightbox) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state.isViewOnce) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasExpiredMedia = state.media.some(
|
||||||
|
item => item.message.id === action.payload.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasExpiredMedia) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getEmptyState();
|
||||||
|
}
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
225
ts/state/ducks/mediaGallery.ts
Normal file
225
ts/state/ducks/mediaGallery.ts
Normal file
|
@ -0,0 +1,225 @@
|
||||||
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { ThunkAction } from 'redux-thunk';
|
||||||
|
|
||||||
|
import type { AttachmentType } from '../../types/Attachment';
|
||||||
|
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
|
||||||
|
import type {
|
||||||
|
ConversationUnloadedActionType,
|
||||||
|
MessageDeletedActionType,
|
||||||
|
MessageExpiredActionType,
|
||||||
|
} from './conversations';
|
||||||
|
import type { MIMEType } from '../../types/MIME';
|
||||||
|
import type { MediaItemType } from '../../types/MediaItem';
|
||||||
|
import type { StateType as RootStateType } from '../reducer';
|
||||||
|
|
||||||
|
import dataInterface from '../../sql/Client';
|
||||||
|
import {
|
||||||
|
CONVERSATION_UNLOADED,
|
||||||
|
MESSAGE_DELETED,
|
||||||
|
MESSAGE_EXPIRED,
|
||||||
|
} from './conversations';
|
||||||
|
import { VERSION_NEEDED_FOR_DISPLAY } from '../../types/Message2';
|
||||||
|
import { isDownloading, hasFailed } from '../../types/Attachment';
|
||||||
|
import { isNotNil } from '../../util/isNotNil';
|
||||||
|
import { useBoundActions } from '../../hooks/useBoundActions';
|
||||||
|
|
||||||
|
type MediaType = {
|
||||||
|
path: string;
|
||||||
|
objectURL: string;
|
||||||
|
thumbnailObjectUrl?: string;
|
||||||
|
contentType: MIMEType;
|
||||||
|
index: number;
|
||||||
|
attachment: AttachmentType;
|
||||||
|
message: {
|
||||||
|
attachments: Array<AttachmentType>;
|
||||||
|
conversationId: string;
|
||||||
|
id: string;
|
||||||
|
received_at: number;
|
||||||
|
received_at_ms: number;
|
||||||
|
sent_at: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MediaGalleryStateType = {
|
||||||
|
documents: Array<MediaItemType>;
|
||||||
|
media: Array<MediaType>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LOAD_MEDIA_ITEMS = 'mediaGallery/LOAD_MEDIA_ITEMS';
|
||||||
|
|
||||||
|
type LoadMediaItemslActionType = {
|
||||||
|
type: typeof LOAD_MEDIA_ITEMS;
|
||||||
|
payload: {
|
||||||
|
documents: Array<MediaItemType>;
|
||||||
|
media: Array<MediaType>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type MediaGalleryActionType =
|
||||||
|
| ConversationUnloadedActionType
|
||||||
|
| LoadMediaItemslActionType
|
||||||
|
| MessageDeletedActionType
|
||||||
|
| MessageExpiredActionType;
|
||||||
|
|
||||||
|
function loadMediaItems(
|
||||||
|
conversationId: string
|
||||||
|
): ThunkAction<void, RootStateType, unknown, LoadMediaItemslActionType> {
|
||||||
|
return async dispatch => {
|
||||||
|
const { getAbsoluteAttachmentPath, upgradeMessageSchema } =
|
||||||
|
window.Signal.Migrations;
|
||||||
|
|
||||||
|
// We fetch more documents than media as they don’t require to be loaded
|
||||||
|
// into memory right away. Revisit this once we have infinite scrolling:
|
||||||
|
const DEFAULT_MEDIA_FETCH_COUNT = 50;
|
||||||
|
const DEFAULT_DOCUMENTS_FETCH_COUNT = 150;
|
||||||
|
|
||||||
|
const ourUuid = window.textsecure.storage.user.getCheckedUuid().toString();
|
||||||
|
|
||||||
|
const rawMedia = await dataInterface.getMessagesWithVisualMediaAttachments(
|
||||||
|
conversationId,
|
||||||
|
{
|
||||||
|
limit: DEFAULT_MEDIA_FETCH_COUNT,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const rawDocuments = await dataInterface.getMessagesWithFileAttachments(
|
||||||
|
conversationId,
|
||||||
|
{
|
||||||
|
limit: DEFAULT_DOCUMENTS_FETCH_COUNT,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// First we upgrade these messages to ensure that they have thumbnails
|
||||||
|
await Promise.all(
|
||||||
|
rawMedia.map(async message => {
|
||||||
|
const { schemaVersion } = message;
|
||||||
|
const model = window.MessageController.register(message.id, message);
|
||||||
|
|
||||||
|
if (schemaVersion && schemaVersion < VERSION_NEEDED_FOR_DISPLAY) {
|
||||||
|
const upgradedMsgAttributes = await upgradeMessageSchema(message);
|
||||||
|
model.set(upgradedMsgAttributes);
|
||||||
|
|
||||||
|
await dataInterface.saveMessage(upgradedMsgAttributes, { ourUuid });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const media: Array<MediaType> = rawMedia
|
||||||
|
.flatMap(message => {
|
||||||
|
return (message.attachments || []).map(
|
||||||
|
(
|
||||||
|
attachment: AttachmentType,
|
||||||
|
index: number
|
||||||
|
): MediaType | undefined => {
|
||||||
|
if (
|
||||||
|
!attachment.path ||
|
||||||
|
!attachment.thumbnail ||
|
||||||
|
isDownloading(attachment) ||
|
||||||
|
hasFailed(attachment)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { thumbnail } = attachment;
|
||||||
|
return {
|
||||||
|
path: attachment.path,
|
||||||
|
objectURL: getAbsoluteAttachmentPath(attachment.path),
|
||||||
|
thumbnailObjectUrl: thumbnail?.path
|
||||||
|
? getAbsoluteAttachmentPath(thumbnail.path)
|
||||||
|
: undefined,
|
||||||
|
contentType: attachment.contentType,
|
||||||
|
index,
|
||||||
|
attachment,
|
||||||
|
message: {
|
||||||
|
attachments: message.attachments || [],
|
||||||
|
conversationId:
|
||||||
|
window.ConversationController.lookupOrCreate({
|
||||||
|
uuid: message.sourceUuid,
|
||||||
|
e164: message.source,
|
||||||
|
reason: 'conversation_view.showAllMedia',
|
||||||
|
})?.id || message.conversationId,
|
||||||
|
id: message.id,
|
||||||
|
received_at: message.received_at,
|
||||||
|
received_at_ms: Number(message.received_at_ms),
|
||||||
|
sent_at: message.sent_at,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.filter(isNotNil);
|
||||||
|
|
||||||
|
// Unlike visual media, only one non-image attachment is supported
|
||||||
|
const documents: Array<MediaItemType> = rawDocuments
|
||||||
|
.map(message => {
|
||||||
|
const attachments = message.attachments || [];
|
||||||
|
const attachment = attachments[0];
|
||||||
|
if (!attachment) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
contentType: attachment.contentType,
|
||||||
|
index: 0,
|
||||||
|
attachment,
|
||||||
|
message: {
|
||||||
|
...message,
|
||||||
|
attachments: [attachment],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(isNotNil);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: LOAD_MEDIA_ITEMS,
|
||||||
|
payload: {
|
||||||
|
documents,
|
||||||
|
media,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
loadMediaItems,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useMediaGalleryActions = (): BoundActionCreatorsMapObject<
|
||||||
|
typeof actions
|
||||||
|
> => useBoundActions(actions);
|
||||||
|
|
||||||
|
export function getEmptyState(): MediaGalleryStateType {
|
||||||
|
return {
|
||||||
|
documents: [],
|
||||||
|
media: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reducer(
|
||||||
|
state: Readonly<MediaGalleryStateType> = getEmptyState(),
|
||||||
|
action: Readonly<MediaGalleryActionType>
|
||||||
|
): MediaGalleryStateType {
|
||||||
|
if (action.type === LOAD_MEDIA_ITEMS) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
...action.payload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.type === MESSAGE_DELETED || action.type === MESSAGE_EXPIRED) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
media: state.media.filter(item => item.message.id !== action.payload.id),
|
||||||
|
documents: state.documents.filter(
|
||||||
|
item => item.message.id !== action.payload.id
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.type === CONVERSATION_UNLOADED) {
|
||||||
|
return getEmptyState();
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ import { getEmptyState as expiration } from './ducks/expiration';
|
||||||
import { getEmptyState as globalModals } from './ducks/globalModals';
|
import { getEmptyState as globalModals } from './ducks/globalModals';
|
||||||
import { getEmptyState as lightbox } from './ducks/lightbox';
|
import { getEmptyState as lightbox } from './ducks/lightbox';
|
||||||
import { getEmptyState as linkPreviews } from './ducks/linkPreviews';
|
import { getEmptyState as linkPreviews } from './ducks/linkPreviews';
|
||||||
|
import { getEmptyState as mediaGallery } from './ducks/mediaGallery';
|
||||||
import { getEmptyState as network } from './ducks/network';
|
import { getEmptyState as network } from './ducks/network';
|
||||||
import { getEmptyState as preferredReactions } from './ducks/preferredReactions';
|
import { getEmptyState as preferredReactions } from './ducks/preferredReactions';
|
||||||
import { getEmptyState as safetyNumber } from './ducks/safetyNumber';
|
import { getEmptyState as safetyNumber } from './ducks/safetyNumber';
|
||||||
|
@ -106,6 +107,7 @@ export function getInitialState({
|
||||||
items,
|
items,
|
||||||
lightbox: lightbox(),
|
lightbox: lightbox(),
|
||||||
linkPreviews: linkPreviews(),
|
linkPreviews: linkPreviews(),
|
||||||
|
mediaGallery: mediaGallery(),
|
||||||
network: network(),
|
network: network(),
|
||||||
preferredReactions: preferredReactions(),
|
preferredReactions: preferredReactions(),
|
||||||
safetyNumber: safetyNumber(),
|
safetyNumber: safetyNumber(),
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { reducer as globalModals } from './ducks/globalModals';
|
||||||
import { reducer as items } from './ducks/items';
|
import { reducer as items } from './ducks/items';
|
||||||
import { reducer as lightbox } from './ducks/lightbox';
|
import { reducer as lightbox } from './ducks/lightbox';
|
||||||
import { reducer as linkPreviews } from './ducks/linkPreviews';
|
import { reducer as linkPreviews } from './ducks/linkPreviews';
|
||||||
|
import { reducer as mediaGallery } from './ducks/mediaGallery';
|
||||||
import { reducer as network } from './ducks/network';
|
import { reducer as network } from './ducks/network';
|
||||||
import { reducer as preferredReactions } from './ducks/preferredReactions';
|
import { reducer as preferredReactions } from './ducks/preferredReactions';
|
||||||
import { reducer as safetyNumber } from './ducks/safetyNumber';
|
import { reducer as safetyNumber } from './ducks/safetyNumber';
|
||||||
|
@ -46,6 +47,7 @@ export const reducer = combineReducers({
|
||||||
items,
|
items,
|
||||||
lightbox,
|
lightbox,
|
||||||
linkPreviews,
|
linkPreviews,
|
||||||
|
mediaGallery,
|
||||||
network,
|
network,
|
||||||
preferredReactions,
|
preferredReactions,
|
||||||
safetyNumber,
|
safetyNumber,
|
||||||
|
|
8
ts/state/selectors/mediaGallery.ts
Normal file
8
ts/state/selectors/mediaGallery.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { StateType } from '../reducer';
|
||||||
|
import type { MediaGalleryStateType } from '../ducks/mediaGallery';
|
||||||
|
|
||||||
|
export const getMediaGalleryState = (state: StateType): MediaGalleryStateType =>
|
||||||
|
state.mediaGallery;
|
34
ts/state/smart/AllMedia.tsx
Normal file
34
ts/state/smart/AllMedia.tsx
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
|
import { MediaGallery } from '../../components/conversation/media-gallery/MediaGallery';
|
||||||
|
import { getMediaGalleryState } from '../selectors/mediaGallery';
|
||||||
|
import { useConversationsActions } from '../ducks/conversations';
|
||||||
|
import { useLightboxActions } from '../ducks/lightbox';
|
||||||
|
import { useMediaGalleryActions } from '../ducks/mediaGallery';
|
||||||
|
|
||||||
|
export type PropsType = {
|
||||||
|
conversationId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SmartAllMedia({ conversationId }: PropsType): JSX.Element {
|
||||||
|
const { media, documents } = useSelector(getMediaGalleryState);
|
||||||
|
const { loadMediaItems } = useMediaGalleryActions();
|
||||||
|
const { saveAttachment } = useConversationsActions();
|
||||||
|
const { showLightboxWithMedia } = useLightboxActions();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MediaGallery
|
||||||
|
conversationId={conversationId}
|
||||||
|
i18n={window.i18n}
|
||||||
|
loadMediaItems={loadMediaItems}
|
||||||
|
media={media}
|
||||||
|
documents={documents}
|
||||||
|
showLightboxWithMedia={showLightboxWithMedia}
|
||||||
|
saveAttachment={saveAttachment}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -35,7 +35,6 @@ export type OwnProps = {
|
||||||
onMarkUnread: () => void;
|
onMarkUnread: () => void;
|
||||||
onMoveToInbox: () => void;
|
onMoveToInbox: () => void;
|
||||||
onSearchInConversation: () => void;
|
onSearchInConversation: () => void;
|
||||||
onShowAllMedia: () => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getOutgoingCallButtonStyle = (
|
const getOutgoingCallButtonStyle = (
|
||||||
|
|
|
@ -12,6 +12,7 @@ import * as log from '../../logging/log';
|
||||||
import { ContactDetail } from '../../components/conversation/ContactDetail';
|
import { ContactDetail } from '../../components/conversation/ContactDetail';
|
||||||
import { ConversationView } from '../../components/conversation/ConversationView';
|
import { ConversationView } from '../../components/conversation/ConversationView';
|
||||||
import { PanelType } from '../../types/Panels';
|
import { PanelType } from '../../types/Panels';
|
||||||
|
import { SmartAllMedia } from './AllMedia';
|
||||||
import { SmartChatColorPicker } from './ChatColorPicker';
|
import { SmartChatColorPicker } from './ChatColorPicker';
|
||||||
import { SmartCompositionArea } from './CompositionArea';
|
import { SmartCompositionArea } from './CompositionArea';
|
||||||
import { SmartConversationDetails } from './ConversationDetails';
|
import { SmartConversationDetails } from './ConversationDetails';
|
||||||
|
@ -73,6 +74,14 @@ export function SmartConversationView({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (topPanel.type === PanelType.AllMedia) {
|
||||||
|
return (
|
||||||
|
<div className="panel">
|
||||||
|
<SmartAllMedia conversationId={conversationId} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (topPanel.type === PanelType.ChatColorEditor) {
|
if (topPanel.type === PanelType.ChatColorEditor) {
|
||||||
return (
|
return (
|
||||||
<div className="panel">
|
<div className="panel">
|
||||||
|
|
|
@ -16,6 +16,7 @@ import type { actions as globalModals } from './ducks/globalModals';
|
||||||
import type { actions as items } from './ducks/items';
|
import type { actions as items } from './ducks/items';
|
||||||
import type { actions as lightbox } from './ducks/lightbox';
|
import type { actions as lightbox } from './ducks/lightbox';
|
||||||
import type { actions as linkPreviews } from './ducks/linkPreviews';
|
import type { actions as linkPreviews } from './ducks/linkPreviews';
|
||||||
|
import type { actions as mediaGallery } from './ducks/mediaGallery';
|
||||||
import type { actions as network } from './ducks/network';
|
import type { actions as network } from './ducks/network';
|
||||||
import type { actions as safetyNumber } from './ducks/safetyNumber';
|
import type { actions as safetyNumber } from './ducks/safetyNumber';
|
||||||
import type { actions as search } from './ducks/search';
|
import type { actions as search } from './ducks/search';
|
||||||
|
@ -43,6 +44,7 @@ export type ReduxActions = {
|
||||||
items: typeof items;
|
items: typeof items;
|
||||||
lightbox: typeof lightbox;
|
lightbox: typeof lightbox;
|
||||||
linkPreviews: typeof linkPreviews;
|
linkPreviews: typeof linkPreviews;
|
||||||
|
mediaGallery: typeof mediaGallery;
|
||||||
network: typeof network;
|
network: typeof network;
|
||||||
safetyNumber: typeof safetyNumber;
|
safetyNumber: typeof safetyNumber;
|
||||||
search: typeof search;
|
search: typeof search;
|
||||||
|
|
|
@ -19,6 +19,7 @@ export enum PanelType {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ReactPanelRenderType =
|
export type ReactPanelRenderType =
|
||||||
|
| { type: PanelType.AllMedia }
|
||||||
| { type: PanelType.ChatColorEditor }
|
| { type: PanelType.ChatColorEditor }
|
||||||
| {
|
| {
|
||||||
type: PanelType.ContactDetails;
|
type: PanelType.ContactDetails;
|
||||||
|
@ -38,9 +39,10 @@ export type ReactPanelRenderType =
|
||||||
| { type: PanelType.NotificationSettings }
|
| { type: PanelType.NotificationSettings }
|
||||||
| { type: PanelType.StickerManager };
|
| { type: PanelType.StickerManager };
|
||||||
|
|
||||||
export type BackbonePanelRenderType =
|
export type BackbonePanelRenderType = {
|
||||||
| { type: PanelType.AllMedia }
|
type: PanelType.MessageDetails;
|
||||||
| { type: PanelType.MessageDetails; args: { messageId: string } };
|
args: { messageId: string };
|
||||||
|
};
|
||||||
|
|
||||||
export type PanelRenderType = ReactPanelRenderType | BackbonePanelRenderType;
|
export type PanelRenderType = ReactPanelRenderType | BackbonePanelRenderType;
|
||||||
|
|
||||||
|
@ -52,6 +54,7 @@ export function isPanelHandledByReact(
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
panel.type === PanelType.AllMedia ||
|
||||||
panel.type === PanelType.ChatColorEditor ||
|
panel.type === PanelType.ChatColorEditor ||
|
||||||
panel.type === PanelType.ContactDetails ||
|
panel.type === PanelType.ContactDetails ||
|
||||||
panel.type === PanelType.ConversationDetails ||
|
panel.type === PanelType.ConversationDetails ||
|
||||||
|
|
|
@ -9222,9 +9222,9 @@
|
||||||
"updated": "2021-07-30T16:57:33.618Z"
|
"updated": "2021-07-30T16:57:33.618Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "React-createRef",
|
"rule": "React-useRef",
|
||||||
"path": "ts/components/conversation/media-gallery/MediaGallery.tsx",
|
"path": "ts/components/conversation/media-gallery/MediaGallery.tsx",
|
||||||
"line": " public readonly focusRef: React.RefObject<HTMLDivElement> = React.createRef();",
|
"line": " const focusRef = useRef<HTMLDivElement | null>(null);",
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-11-01T22:46:33.013Z",
|
"updated": "2019-11-01T22:46:33.013Z",
|
||||||
"reasonDetail": "Used for setting focus only"
|
"reasonDetail": "Used for setting focus only"
|
||||||
|
|
|
@ -4,15 +4,9 @@
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
|
|
||||||
import type * as Backbone from 'backbone';
|
import type * as Backbone from 'backbone';
|
||||||
import * as React from 'react';
|
|
||||||
import { flatten } from 'lodash';
|
|
||||||
import { render } from 'mustache';
|
import { render } from 'mustache';
|
||||||
|
|
||||||
import type { AttachmentType } from '../types/Attachment';
|
|
||||||
import type { MIMEType } from '../types/MIME';
|
|
||||||
import type { ConversationModel } from '../models/conversations';
|
import type { ConversationModel } from '../models/conversations';
|
||||||
import type { MessageAttributesType } from '../model-types.d';
|
|
||||||
import type { MediaItemType } from '../types/MediaItem';
|
|
||||||
import { getMessageById } from '../messages/getMessageById';
|
import { getMessageById } from '../messages/getMessageById';
|
||||||
import { getContactId } from '../messages/helpers';
|
import { getContactId } from '../messages/helpers';
|
||||||
import { strictAssert } from '../util/assert';
|
import { strictAssert } from '../util/assert';
|
||||||
|
@ -27,13 +21,10 @@ import { ToastConversationMarkedUnread } from '../components/ToastConversationMa
|
||||||
import { ToastConversationUnarchived } from '../components/ToastConversationUnarchived';
|
import { ToastConversationUnarchived } from '../components/ToastConversationUnarchived';
|
||||||
import { ToastMessageBodyTooLong } from '../components/ToastMessageBodyTooLong';
|
import { ToastMessageBodyTooLong } from '../components/ToastMessageBodyTooLong';
|
||||||
import { ToastOriginalMessageNotFound } from '../components/ToastOriginalMessageNotFound';
|
import { ToastOriginalMessageNotFound } from '../components/ToastOriginalMessageNotFound';
|
||||||
import { isNotNil } from '../util/isNotNil';
|
|
||||||
import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser';
|
import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser';
|
||||||
import { showToast } from '../util/showToast';
|
import { showToast } from '../util/showToast';
|
||||||
import { UUIDKind } from '../types/UUID';
|
import { UUIDKind } from '../types/UUID';
|
||||||
import type { UUIDStringType } from '../types/UUID';
|
import type { UUIDStringType } from '../types/UUID';
|
||||||
import { MediaGallery } from '../components/conversation/media-gallery/MediaGallery';
|
|
||||||
import type { ItemClickEvent } from '../components/conversation/media-gallery/types/ItemClickEvent';
|
|
||||||
import {
|
import {
|
||||||
removeLinkPreview,
|
removeLinkPreview,
|
||||||
suspendLinkPreviews,
|
suspendLinkPreviews,
|
||||||
|
@ -45,13 +36,8 @@ import { clearConversationDraftAttachments } from '../util/clearConversationDraf
|
||||||
import type { BackbonePanelRenderType, PanelRenderType } from '../types/Panels';
|
import type { BackbonePanelRenderType, PanelRenderType } from '../types/Panels';
|
||||||
import { PanelType, isPanelHandledByReact } from '../types/Panels';
|
import { PanelType, isPanelHandledByReact } from '../types/Panels';
|
||||||
|
|
||||||
const { Message } = window.Signal.Types;
|
|
||||||
|
|
||||||
type BackbonePanelType = { panelType: PanelType; view: Backbone.View };
|
type BackbonePanelType = { panelType: PanelType; view: Backbone.View };
|
||||||
|
|
||||||
const { getAbsoluteAttachmentPath, upgradeMessageSchema } =
|
|
||||||
window.Signal.Migrations;
|
|
||||||
|
|
||||||
const { getMessagesBySentAt } = window.Signal.Data;
|
const { getMessagesBySentAt } = window.Signal.Data;
|
||||||
|
|
||||||
type MessageActionsType = {
|
type MessageActionsType = {
|
||||||
|
@ -59,23 +45,6 @@ type MessageActionsType = {
|
||||||
startConversation: (e164: string, uuid: UUIDStringType) => unknown;
|
startConversation: (e164: string, uuid: UUIDStringType) => unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
type MediaType = {
|
|
||||||
path: string;
|
|
||||||
objectURL: string;
|
|
||||||
thumbnailObjectUrl?: string;
|
|
||||||
contentType: MIMEType;
|
|
||||||
index: number;
|
|
||||||
attachment: AttachmentType;
|
|
||||||
message: {
|
|
||||||
attachments: Array<AttachmentType>;
|
|
||||||
conversationId: string;
|
|
||||||
id: string;
|
|
||||||
received_at: number;
|
|
||||||
received_at_ms: number;
|
|
||||||
sent_at: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export class ConversationView extends window.Backbone.View<ConversationModel> {
|
export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
// Sub-views
|
// Sub-views
|
||||||
private contactModalView?: Backbone.View;
|
private contactModalView?: Backbone.View;
|
||||||
|
@ -101,7 +70,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
);
|
);
|
||||||
|
|
||||||
// These are triggered by background.ts for keyboard handling
|
// These are triggered by background.ts for keyboard handling
|
||||||
this.listenTo(this.model, 'open-all-media', this.showAllMedia);
|
|
||||||
this.listenTo(this.model, 'escape-pressed', () => {
|
this.listenTo(this.model, 'escape-pressed', () => {
|
||||||
window.reduxActions.conversations.popPanelForConversation(this.model.id);
|
window.reduxActions.conversations.popPanelForConversation(this.model.id);
|
||||||
});
|
});
|
||||||
|
@ -156,9 +124,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
const { searchInConversation } = window.reduxActions.search;
|
const { searchInConversation } = window.reduxActions.search;
|
||||||
searchInConversation(this.model.id);
|
searchInConversation(this.model.id);
|
||||||
},
|
},
|
||||||
onShowAllMedia: () => {
|
|
||||||
this.showAllMedia();
|
|
||||||
},
|
|
||||||
onGoBack: () => {
|
onGoBack: () => {
|
||||||
window.reduxActions.conversations.popPanelForConversation(
|
window.reduxActions.conversations.popPanelForConversation(
|
||||||
this.model.id
|
this.model.id
|
||||||
|
@ -480,207 +445,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
this.model.updateVerified();
|
this.model.updateVerified();
|
||||||
}
|
}
|
||||||
|
|
||||||
showAllMedia(): void {
|
|
||||||
window.reduxActions.conversations.pushPanelForConversation(this.model.id, {
|
|
||||||
type: PanelType.AllMedia,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getAllMedia(): Backbone.View | undefined {
|
|
||||||
if (document.querySelectorAll('.module-media-gallery').length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We fetch more documents than media as they don’t require to be loaded
|
|
||||||
// into memory right away. Revisit this once we have infinite scrolling:
|
|
||||||
const DEFAULT_MEDIA_FETCH_COUNT = 50;
|
|
||||||
const DEFAULT_DOCUMENTS_FETCH_COUNT = 150;
|
|
||||||
|
|
||||||
const conversationId = this.model.get('id');
|
|
||||||
const ourUuid = window.textsecure.storage.user.getCheckedUuid().toString();
|
|
||||||
|
|
||||||
const getProps = async () => {
|
|
||||||
const rawMedia =
|
|
||||||
await window.Signal.Data.getMessagesWithVisualMediaAttachments(
|
|
||||||
conversationId,
|
|
||||||
{
|
|
||||||
limit: DEFAULT_MEDIA_FETCH_COUNT,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const rawDocuments =
|
|
||||||
await window.Signal.Data.getMessagesWithFileAttachments(
|
|
||||||
conversationId,
|
|
||||||
{
|
|
||||||
limit: DEFAULT_DOCUMENTS_FETCH_COUNT,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// First we upgrade these messages to ensure that they have thumbnails
|
|
||||||
for (let max = rawMedia.length, i = 0; i < max; i += 1) {
|
|
||||||
const message = rawMedia[i];
|
|
||||||
const { schemaVersion } = message;
|
|
||||||
|
|
||||||
// We want these message to be cached in memory for other operations like
|
|
||||||
// listening to 'expired' events when showing the lightbox, and so any other
|
|
||||||
// code working with this message has the latest updates.
|
|
||||||
const model = window.MessageController.register(message.id, message);
|
|
||||||
|
|
||||||
if (
|
|
||||||
schemaVersion &&
|
|
||||||
schemaVersion < Message.VERSION_NEEDED_FOR_DISPLAY
|
|
||||||
) {
|
|
||||||
// Yep, we really do want to wait for each of these
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
rawMedia[i] = await upgradeMessageSchema(message);
|
|
||||||
model.set(rawMedia[i]);
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
await window.Signal.Data.saveMessage(rawMedia[i], { ourUuid });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const media: Array<MediaType> = flatten(
|
|
||||||
rawMedia.map(message => {
|
|
||||||
return (message.attachments || []).map(
|
|
||||||
(
|
|
||||||
attachment: AttachmentType,
|
|
||||||
index: number
|
|
||||||
): MediaType | undefined => {
|
|
||||||
if (
|
|
||||||
!attachment.path ||
|
|
||||||
!attachment.thumbnail ||
|
|
||||||
attachment.pending ||
|
|
||||||
attachment.error
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { thumbnail } = attachment;
|
|
||||||
return {
|
|
||||||
path: attachment.path,
|
|
||||||
objectURL: getAbsoluteAttachmentPath(attachment.path),
|
|
||||||
thumbnailObjectUrl: thumbnail?.path
|
|
||||||
? getAbsoluteAttachmentPath(thumbnail.path)
|
|
||||||
: undefined,
|
|
||||||
contentType: attachment.contentType,
|
|
||||||
index,
|
|
||||||
attachment,
|
|
||||||
message: {
|
|
||||||
attachments: message.attachments || [],
|
|
||||||
conversationId:
|
|
||||||
window.ConversationController.lookupOrCreate({
|
|
||||||
uuid: message.sourceUuid,
|
|
||||||
e164: message.source,
|
|
||||||
reason: 'conversation_view.showAllMedia',
|
|
||||||
})?.id || message.conversationId,
|
|
||||||
id: message.id,
|
|
||||||
received_at: message.received_at,
|
|
||||||
received_at_ms: Number(message.received_at_ms),
|
|
||||||
sent_at: message.sent_at,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
})
|
|
||||||
).filter(isNotNil);
|
|
||||||
|
|
||||||
// Unlike visual media, only one non-image attachment is supported
|
|
||||||
const documents: Array<MediaItemType> = [];
|
|
||||||
rawDocuments.forEach(message => {
|
|
||||||
const attachments = message.attachments || [];
|
|
||||||
const attachment = attachments[0];
|
|
||||||
if (!attachment) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
documents.push({
|
|
||||||
contentType: attachment.contentType,
|
|
||||||
index: 0,
|
|
||||||
attachment,
|
|
||||||
// We do this cast because we know there attachments (see the checks above).
|
|
||||||
message: message as MessageAttributesType & {
|
|
||||||
attachments: Array<AttachmentType>;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const onItemClick = async ({
|
|
||||||
message,
|
|
||||||
attachment,
|
|
||||||
type,
|
|
||||||
}: ItemClickEvent) => {
|
|
||||||
switch (type) {
|
|
||||||
case 'documents': {
|
|
||||||
window.reduxActions.conversations.saveAttachment(
|
|
||||||
attachment,
|
|
||||||
message.sent_at
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'media': {
|
|
||||||
window.reduxActions.lightbox.showLightboxWithMedia(
|
|
||||||
attachment.path,
|
|
||||||
media
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new TypeError(`Unknown attachment type: '${type}'`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
documents,
|
|
||||||
media,
|
|
||||||
onItemClick,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
function getMessageIds(): Array<string | undefined> | undefined {
|
|
||||||
const state = window.reduxStore.getState();
|
|
||||||
const byConversation = state?.conversations?.messagesByConversation;
|
|
||||||
const messages = byConversation && byConversation[conversationId];
|
|
||||||
if (!messages || !messages.messageIds) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return messages.messageIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detect message changes in the current conversation
|
|
||||||
let previousMessageList: Array<string | undefined> | undefined;
|
|
||||||
previousMessageList = getMessageIds();
|
|
||||||
|
|
||||||
const unsubscribe = window.reduxStore.subscribe(() => {
|
|
||||||
const currentMessageList = getMessageIds();
|
|
||||||
if (currentMessageList !== previousMessageList) {
|
|
||||||
update();
|
|
||||||
previousMessageList = currentMessageList;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const view = new ReactWrapperView({
|
|
||||||
className: 'panel',
|
|
||||||
// We present an empty panel briefly, while we wait for props to load.
|
|
||||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
|
||||||
JSX: <></>,
|
|
||||||
onClose: () => {
|
|
||||||
unsubscribe();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const update = async () => {
|
|
||||||
const props = await getProps();
|
|
||||||
view.update(<MediaGallery i18n={window.i18n} {...props} />);
|
|
||||||
};
|
|
||||||
|
|
||||||
update();
|
|
||||||
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
showMessageDetail(messageId: string): void {
|
showMessageDetail(messageId: string): void {
|
||||||
window.reduxActions.conversations.pushPanelForConversation(this.model.id, {
|
window.reduxActions.conversations.pushPanelForConversation(this.model.id, {
|
||||||
type: PanelType.MessageDetails,
|
type: PanelType.MessageDetails,
|
||||||
|
@ -753,9 +517,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
const { type } = panel as BackbonePanelRenderType;
|
const { type } = panel as BackbonePanelRenderType;
|
||||||
|
|
||||||
let view: Backbone.View | undefined;
|
let view: Backbone.View | undefined;
|
||||||
if (type === PanelType.AllMedia) {
|
if (panel.type === PanelType.MessageDetails) {
|
||||||
view = this.getAllMedia();
|
|
||||||
} else if (panel.type === PanelType.MessageDetails) {
|
|
||||||
view = this.getMessageDetail(panel.args);
|
view = this.getMessageDetail(panel.args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue