mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2025-12-20 10:19:08 +00:00
Update tabs UI in MediaGallery
This commit is contained in:
@@ -74,7 +74,7 @@ export function AttachmentSection({
|
|||||||
switch (verified.type) {
|
switch (verified.type) {
|
||||||
case 'media':
|
case 'media':
|
||||||
return (
|
return (
|
||||||
<section className={tw('@container ps-5')}>
|
<section className={tw('@container px-5')}>
|
||||||
<h2 className={tw('ps-1 pt-4 pb-2 type-body-medium')}>{header}</h2>
|
<h2 className={tw('ps-1 pt-4 pb-2 type-body-medium')}>{header}</h2>
|
||||||
<div
|
<div
|
||||||
className={tw(
|
className={tw(
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import * as React from 'react';
|
|||||||
import type { Meta } from '@storybook/react';
|
import type { Meta } from '@storybook/react';
|
||||||
import type { Props } from './EmptyState.dom.js';
|
import type { Props } from './EmptyState.dom.js';
|
||||||
import { EmptyState } from './EmptyState.dom.js';
|
import { EmptyState } from './EmptyState.dom.js';
|
||||||
import { TabViews } from './types/TabViews.std.js';
|
|
||||||
|
|
||||||
const { i18n } = window.SignalContext;
|
const { i18n } = window.SignalContext;
|
||||||
|
|
||||||
@@ -14,12 +13,12 @@ export default {
|
|||||||
argTypes: {
|
argTypes: {
|
||||||
tab: {
|
tab: {
|
||||||
control: { type: 'select' },
|
control: { type: 'select' },
|
||||||
options: [TabViews.Media, TabViews.Documents, TabViews.Links],
|
options: ['media', 'audio', 'links', 'documents'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
args: {
|
args: {
|
||||||
i18n,
|
i18n,
|
||||||
tab: TabViews.Media,
|
tab: 'media',
|
||||||
},
|
},
|
||||||
} satisfies Meta<Props>;
|
} satisfies Meta<Props>;
|
||||||
|
|
||||||
@@ -28,13 +27,17 @@ export function Default(args: Props): JSX.Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Media(args: Props): JSX.Element {
|
export function Media(args: Props): JSX.Element {
|
||||||
return <EmptyState {...args} tab={TabViews.Media} />;
|
return <EmptyState {...args} tab="media" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Documents(args: Props): JSX.Element {
|
export function Audio(args: Props): JSX.Element {
|
||||||
return <EmptyState {...args} tab={TabViews.Documents} />;
|
return <EmptyState {...args} tab="audio" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Links(args: Props): JSX.Element {
|
export function Links(args: Props): JSX.Element {
|
||||||
return <EmptyState {...args} tab={TabViews.Links} />;
|
return <EmptyState {...args} tab="links" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Documents(args: Props): JSX.Element {
|
||||||
|
return <EmptyState {...args} tab="documents" />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import type { LocalizerType } from '../../../types/Util.std.js';
|
import type { LocalizerType } from '../../../types/Util.std.js';
|
||||||
|
import type { MediaTabType } from '../../../types/MediaItem.std.js';
|
||||||
import { tw } from '../../../axo/tw.dom.js';
|
import { tw } from '../../../axo/tw.dom.js';
|
||||||
import { missingCaseError } from '../../../util/missingCaseError.std.js';
|
import { missingCaseError } from '../../../util/missingCaseError.std.js';
|
||||||
import { TabViews } from './types/TabViews.std.js';
|
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
tab: TabViews;
|
tab: MediaTabType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function EmptyState({ i18n, tab }: Props): JSX.Element {
|
export function EmptyState({ i18n, tab }: Props): JSX.Element {
|
||||||
@@ -18,21 +18,21 @@ export function EmptyState({ i18n, tab }: Props): JSX.Element {
|
|||||||
let description: string;
|
let description: string;
|
||||||
|
|
||||||
switch (tab) {
|
switch (tab) {
|
||||||
case TabViews.Media:
|
case 'media':
|
||||||
title = i18n('icu:MediaGallery__EmptyState__title--media');
|
title = i18n('icu:MediaGallery__EmptyState__title--media');
|
||||||
description = i18n('icu:MediaGallery__EmptyState__description--media');
|
description = i18n('icu:MediaGallery__EmptyState__description--media');
|
||||||
break;
|
break;
|
||||||
case TabViews.Audio:
|
case 'audio':
|
||||||
title = i18n('icu:MediaGallery__EmptyState__title--audio');
|
title = i18n('icu:MediaGallery__EmptyState__title--audio');
|
||||||
description = i18n('icu:MediaGallery__EmptyState__description--audio');
|
description = i18n('icu:MediaGallery__EmptyState__description--audio');
|
||||||
break;
|
break;
|
||||||
case TabViews.Documents:
|
case 'documents':
|
||||||
title = i18n('icu:MediaGallery__EmptyState__title--documents');
|
title = i18n('icu:MediaGallery__EmptyState__title--documents');
|
||||||
description = i18n(
|
description = i18n(
|
||||||
'icu:MediaGallery__EmptyState__description--documents-2'
|
'icu:MediaGallery__EmptyState__description--documents-2'
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case TabViews.Links:
|
case 'links':
|
||||||
title = i18n('icu:MediaGallery__EmptyState__title--links');
|
title = i18n('icu:MediaGallery__EmptyState__title--links');
|
||||||
description = i18n('icu:MediaGallery__EmptyState__description--links-2');
|
description = i18n('icu:MediaGallery__EmptyState__description--links-2');
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
// Copyright 2020 Signal Messenger, LLC
|
// Copyright 2020 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import * as React from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import { action } from '@storybook/addon-actions';
|
import { action } from '@storybook/addon-actions';
|
||||||
import type { Meta } from '@storybook/react';
|
import type { Meta } from '@storybook/react';
|
||||||
|
import type { MediaTabType } from '../../../types/MediaItem.std.js';
|
||||||
import type { Props } from './MediaGallery.dom.js';
|
import type { Props } from './MediaGallery.dom.js';
|
||||||
import { MediaGallery } from './MediaGallery.dom.js';
|
import { MediaGallery } from './MediaGallery.dom.js';
|
||||||
|
import { PanelHeader } from './PanelHeader.dom.js';
|
||||||
import {
|
import {
|
||||||
createPreparedMediaItems,
|
createPreparedMediaItems,
|
||||||
createRandomDocuments,
|
createRandomDocuments,
|
||||||
@@ -36,6 +38,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||||||
audio: overrideProps.audio || [],
|
audio: overrideProps.audio || [],
|
||||||
links: overrideProps.links || [],
|
links: overrideProps.links || [],
|
||||||
documents: overrideProps.documents || [],
|
documents: overrideProps.documents || [],
|
||||||
|
tab: overrideProps.tab || 'media',
|
||||||
|
|
||||||
initialLoad: action('initialLoad'),
|
initialLoad: action('initialLoad'),
|
||||||
loadMore: action('loadMore'),
|
loadMore: action('loadMore'),
|
||||||
@@ -46,29 +49,59 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||||||
cancelAttachmentDownload: action('cancelAttachmentDownload'),
|
cancelAttachmentDownload: action('cancelAttachmentDownload'),
|
||||||
|
|
||||||
renderMediaItem: props => <MediaItem {...props} />,
|
renderMediaItem: props => <MediaItem {...props} />,
|
||||||
renderMiniPlayer: () => <div />,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function Panel(props: Props): JSX.Element {
|
||||||
|
const [tab, setTab] = useState<MediaTabType>(props.tab);
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const { loadMore: givenLoadMore } = props;
|
||||||
|
|
||||||
|
const loadMore = useCallback(
|
||||||
|
(...args: Parameters<typeof givenLoadMore>) => {
|
||||||
|
setLoading(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setLoading(false);
|
||||||
|
}, 250);
|
||||||
|
return givenLoadMore(...args);
|
||||||
|
},
|
||||||
|
[givenLoadMore]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<PanelHeader i18n={i18n} tab={tab} setTab={setTab} />
|
||||||
|
<MediaGallery
|
||||||
|
{...props}
|
||||||
|
tab={tab}
|
||||||
|
loading={loading}
|
||||||
|
loadMore={loadMore}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function Populated(): JSX.Element {
|
export function Populated(): JSX.Element {
|
||||||
const documents = createRandomDocuments(Date.now() - days(5), days(5));
|
const documents = createRandomDocuments(Date.now() - days(5), days(5));
|
||||||
const media = createPreparedMediaItems(createRandomMedia);
|
const media = createPreparedMediaItems(createRandomMedia);
|
||||||
const props = createProps({ documents, media });
|
const props = createProps({ documents, media });
|
||||||
|
|
||||||
return <MediaGallery {...props} />;
|
return <Panel {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NoDocuments(): JSX.Element {
|
export function NoDocuments(): JSX.Element {
|
||||||
const media = createPreparedMediaItems(createRandomMedia);
|
const media = createPreparedMediaItems(createRandomMedia);
|
||||||
const props = createProps({ media });
|
const props = createProps({ media, haveOldestDocument: true });
|
||||||
|
|
||||||
return <MediaGallery {...props} />;
|
return <Panel {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NoMedia(): JSX.Element {
|
export function NoMedia(): JSX.Element {
|
||||||
const documents = createPreparedMediaItems(createRandomDocuments);
|
const documents = createPreparedMediaItems(createRandomDocuments);
|
||||||
const props = createProps({ documents });
|
const props = createProps({ documents, haveOldestMedia: true });
|
||||||
|
|
||||||
return <MediaGallery {...props} />;
|
return <Panel {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OneEach(): JSX.Element {
|
export function OneEach(): JSX.Element {
|
||||||
@@ -79,11 +112,16 @@ export function OneEach(): JSX.Element {
|
|||||||
|
|
||||||
const props = createProps({ documents, audio, media, links });
|
const props = createProps({ documents, audio, media, links });
|
||||||
|
|
||||||
return <MediaGallery {...props} />;
|
return <Panel {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Empty(): JSX.Element {
|
export function Empty(): JSX.Element {
|
||||||
const props = createProps();
|
const props = createProps({
|
||||||
|
haveOldestMedia: true,
|
||||||
|
haveOldestAudio: true,
|
||||||
|
haveOldestDocument: true,
|
||||||
|
haveOldestLink: true,
|
||||||
|
});
|
||||||
|
|
||||||
return <MediaGallery {...props} />;
|
return <Panel {...props} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
// Copyright 2018 Signal Messenger, LLC
|
// Copyright 2018 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React, { Fragment, useEffect, useRef, useCallback } from 'react';
|
import React, {
|
||||||
|
Fragment,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useCallback,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
import type { ItemClickEvent } from './types/ItemClickEvent.std.js';
|
import type { ItemClickEvent } from './types/ItemClickEvent.std.js';
|
||||||
import type { LocalizerType } from '../../../types/Util.std.js';
|
import type { LocalizerType } from '../../../types/Util.std.js';
|
||||||
import type {
|
import type {
|
||||||
|
MediaTabType,
|
||||||
LinkPreviewMediaItemType,
|
LinkPreviewMediaItemType,
|
||||||
MediaItemType,
|
MediaItemType,
|
||||||
GenericMediaItemType,
|
GenericMediaItemType,
|
||||||
@@ -15,12 +22,10 @@ import type {
|
|||||||
import type { SaveAttachmentActionCreatorType } from '../../../state/ducks/conversations.preload.js';
|
import type { SaveAttachmentActionCreatorType } from '../../../state/ducks/conversations.preload.js';
|
||||||
import { AttachmentSection } from './AttachmentSection.dom.js';
|
import { AttachmentSection } from './AttachmentSection.dom.js';
|
||||||
import { EmptyState } from './EmptyState.dom.js';
|
import { EmptyState } from './EmptyState.dom.js';
|
||||||
import { Tabs } from '../../Tabs.dom.js';
|
|
||||||
import { TabViews } from './types/TabViews.std.js';
|
|
||||||
import { groupMediaItemsByDate } from './groupMediaItemsByDate.std.js';
|
import { groupMediaItemsByDate } from './groupMediaItemsByDate.std.js';
|
||||||
import { missingCaseError } from '../../../util/missingCaseError.std.js';
|
import { missingCaseError } from '../../../util/missingCaseError.std.js';
|
||||||
import { openLinkInWebBrowser } from '../../../util/openLinkInWebBrowser.dom.js';
|
import { openLinkInWebBrowser } from '../../../util/openLinkInWebBrowser.dom.js';
|
||||||
import { usePrevious } from '../../../hooks/usePrevious.std.js';
|
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver.std.js';
|
||||||
import type { AttachmentForUIType } from '../../../types/Attachment.std.js';
|
import type { AttachmentForUIType } from '../../../types/Attachment.std.js';
|
||||||
import { tw } from '../../../axo/tw.dom.js';
|
import { tw } from '../../../axo/tw.dom.js';
|
||||||
|
|
||||||
@@ -33,14 +38,12 @@ export type Props = {
|
|||||||
haveOldestDocument: boolean;
|
haveOldestDocument: boolean;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
initialLoad: (id: string) => unknown;
|
initialLoad: (id: string) => unknown;
|
||||||
loadMore: (
|
loadMore: (id: string, type: MediaTabType) => unknown;
|
||||||
id: string,
|
|
||||||
type: 'media' | 'audio' | 'documents' | 'links'
|
|
||||||
) => unknown;
|
|
||||||
media: ReadonlyArray<MediaItemType>;
|
media: ReadonlyArray<MediaItemType>;
|
||||||
audio: ReadonlyArray<MediaItemType>;
|
audio: ReadonlyArray<MediaItemType>;
|
||||||
documents: ReadonlyArray<MediaItemType>;
|
|
||||||
links: ReadonlyArray<LinkPreviewMediaItemType>;
|
links: ReadonlyArray<LinkPreviewMediaItemType>;
|
||||||
|
documents: ReadonlyArray<MediaItemType>;
|
||||||
|
tab: MediaTabType;
|
||||||
saveAttachment: SaveAttachmentActionCreatorType;
|
saveAttachment: SaveAttachmentActionCreatorType;
|
||||||
kickOffAttachmentDownload: (options: { messageId: string }) => void;
|
kickOffAttachmentDownload: (options: { messageId: string }) => void;
|
||||||
cancelAttachmentDownload: (options: { messageId: string }) => void;
|
cancelAttachmentDownload: (options: { messageId: string }) => void;
|
||||||
@@ -50,7 +53,6 @@ export type Props = {
|
|||||||
messageId: string;
|
messageId: string;
|
||||||
}) => void;
|
}) => void;
|
||||||
|
|
||||||
renderMiniPlayer: () => JSX.Element;
|
|
||||||
renderMediaItem: (props: {
|
renderMediaItem: (props: {
|
||||||
onItemClick: (event: ItemClickEvent) => unknown;
|
onItemClick: (event: ItemClickEvent) => unknown;
|
||||||
mediaItem: GenericMediaItemType;
|
mediaItem: GenericMediaItemType;
|
||||||
@@ -81,7 +83,7 @@ function MediaSection({
|
|||||||
| 'playAudio'
|
| 'playAudio'
|
||||||
| 'renderMediaItem'
|
| 'renderMediaItem'
|
||||||
> & {
|
> & {
|
||||||
tab: TabViews;
|
tab: MediaTabType;
|
||||||
mediaItems: ReadonlyArray<GenericMediaItemType>;
|
mediaItems: ReadonlyArray<GenericMediaItemType>;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const onItemClick = useCallback(
|
const onItemClick = useCallback(
|
||||||
@@ -194,25 +196,29 @@ export function MediaGallery({
|
|||||||
haveOldestDocument,
|
haveOldestDocument,
|
||||||
i18n,
|
i18n,
|
||||||
initialLoad,
|
initialLoad,
|
||||||
loading,
|
loading: reduxLoading,
|
||||||
loadMore,
|
loadMore,
|
||||||
media,
|
media,
|
||||||
audio,
|
audio,
|
||||||
links,
|
links,
|
||||||
documents,
|
documents,
|
||||||
|
tab,
|
||||||
saveAttachment,
|
saveAttachment,
|
||||||
kickOffAttachmentDownload,
|
kickOffAttachmentDownload,
|
||||||
cancelAttachmentDownload,
|
cancelAttachmentDownload,
|
||||||
playAudio,
|
playAudio,
|
||||||
showLightbox,
|
showLightbox,
|
||||||
renderMediaItem,
|
renderMediaItem,
|
||||||
renderMiniPlayer,
|
|
||||||
}: Props): JSX.Element {
|
}: Props): JSX.Element {
|
||||||
const focusRef = useRef<HTMLDivElement | null>(null);
|
const focusRef = useRef<HTMLDivElement | null>(null);
|
||||||
const scrollObserverRef = useRef<HTMLDivElement | null>(null);
|
const [loading, setLoading] = useState(reduxLoading);
|
||||||
const intersectionObserver = useRef<IntersectionObserver | null>(null);
|
|
||||||
const loadingRef = useRef<boolean>(false);
|
// Reset local state when redux finishes loading
|
||||||
const tabViewRef = useRef<TabViews>(TabViews.Media);
|
useEffect(() => {
|
||||||
|
if (reduxLoading === false) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [reduxLoading]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
focusRef.current?.focus();
|
focusRef.current?.focus();
|
||||||
@@ -232,7 +238,6 @@ export function MediaGallery({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
initialLoad(conversationId);
|
initialLoad(conversationId);
|
||||||
loadingRef.current = true;
|
|
||||||
}, [
|
}, [
|
||||||
conversationId,
|
conversationId,
|
||||||
haveOldestMedia,
|
haveOldestMedia,
|
||||||
@@ -246,63 +251,42 @@ export function MediaGallery({
|
|||||||
documents.length,
|
documents.length,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const previousLoading = usePrevious(loading, loading);
|
const [setObserverRef, observerEntry] = useIntersectionObserver();
|
||||||
if (previousLoading && !loading) {
|
|
||||||
loadingRef.current = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loading || !scrollObserverRef.current) {
|
if (loading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
intersectionObserver.current?.disconnect();
|
if (!observerEntry?.isIntersecting) {
|
||||||
intersectionObserver.current = null;
|
|
||||||
|
|
||||||
intersectionObserver.current = new IntersectionObserver(
|
|
||||||
(entries: ReadonlyArray<IntersectionObserverEntry>) => {
|
|
||||||
if (loadingRef.current) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const entry = entries.find(
|
if (tab === 'media') {
|
||||||
item => item.target === scrollObserverRef.current
|
if (haveOldestMedia) {
|
||||||
);
|
return;
|
||||||
|
}
|
||||||
if (entry && entry.intersectionRatio > 0) {
|
|
||||||
if (tabViewRef.current === TabViews.Media) {
|
|
||||||
if (!haveOldestMedia) {
|
|
||||||
loadMore(conversationId, 'media');
|
loadMore(conversationId, 'media');
|
||||||
loadingRef.current = true;
|
} else if (tab === 'audio') {
|
||||||
|
if (haveOldestAudio) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} else if (tabViewRef.current === TabViews.Audio) {
|
|
||||||
if (!haveOldestAudio) {
|
|
||||||
loadMore(conversationId, 'audio');
|
loadMore(conversationId, 'audio');
|
||||||
loadingRef.current = true;
|
} else if (tab === 'documents') {
|
||||||
|
if (haveOldestDocument) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} else if (tabViewRef.current === TabViews.Documents) {
|
|
||||||
if (!haveOldestDocument) {
|
|
||||||
loadMore(conversationId, 'documents');
|
loadMore(conversationId, 'documents');
|
||||||
loadingRef.current = true;
|
} else if (tab === 'links') {
|
||||||
|
if (haveOldestLink) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} else if (tabViewRef.current === TabViews.Links) {
|
|
||||||
if (!haveOldestLink) {
|
|
||||||
loadMore(conversationId, 'links');
|
loadMore(conversationId, 'links');
|
||||||
loadingRef.current = true;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
throw missingCaseError(tabViewRef.current);
|
throw missingCaseError(tab);
|
||||||
}
|
}
|
||||||
}
|
setLoading(true);
|
||||||
}
|
|
||||||
);
|
|
||||||
intersectionObserver.current.observe(scrollObserverRef.current);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
intersectionObserver.current?.disconnect();
|
|
||||||
intersectionObserver.current = null;
|
|
||||||
};
|
|
||||||
}, [
|
}, [
|
||||||
|
observerEntry,
|
||||||
conversationId,
|
conversationId,
|
||||||
haveOldestDocument,
|
haveOldestDocument,
|
||||||
haveOldestMedia,
|
haveOldestMedia,
|
||||||
@@ -310,68 +294,34 @@ export function MediaGallery({
|
|||||||
haveOldestLink,
|
haveOldestLink,
|
||||||
loading,
|
loading,
|
||||||
loadMore,
|
loadMore,
|
||||||
|
tab,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
let mediaItems: ReadonlyArray<GenericMediaItemType>;
|
||||||
|
|
||||||
|
if (tab === 'media') {
|
||||||
|
mediaItems = media;
|
||||||
|
} else if (tab === 'audio') {
|
||||||
|
mediaItems = audio;
|
||||||
|
} else if (tab === 'documents') {
|
||||||
|
mediaItems = documents;
|
||||||
|
} else if (tab === 'links') {
|
||||||
|
mediaItems = links;
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unexpected select tab: ${tab}`);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={tw('flex size-full grow flex-col outline-none')}
|
className={tw('flex size-full grow flex-col outline-none')}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
ref={focusRef}
|
ref={focusRef}
|
||||||
>
|
>
|
||||||
<Tabs
|
<div className={tw('grow overflow-y-auto')}>
|
||||||
initialSelectedTab={TabViews.Media}
|
|
||||||
tabs={[
|
|
||||||
{
|
|
||||||
id: TabViews.Media,
|
|
||||||
label: i18n('icu:media'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: TabViews.Audio,
|
|
||||||
label: i18n('icu:MediaGallery__tab__audio'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: TabViews.Links,
|
|
||||||
label: i18n('icu:MediaGallery__tab__links'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: TabViews.Documents,
|
|
||||||
label: i18n('icu:MediaGallery__tab__files'),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{({ selectedTab }) => {
|
|
||||||
let mediaItems: ReadonlyArray<GenericMediaItemType>;
|
|
||||||
|
|
||||||
if (selectedTab === TabViews.Media) {
|
|
||||||
tabViewRef.current = TabViews.Media;
|
|
||||||
mediaItems = media;
|
|
||||||
} else if (selectedTab === TabViews.Audio) {
|
|
||||||
tabViewRef.current = TabViews.Audio;
|
|
||||||
mediaItems = audio;
|
|
||||||
} else if (selectedTab === TabViews.Documents) {
|
|
||||||
tabViewRef.current = TabViews.Documents;
|
|
||||||
mediaItems = documents;
|
|
||||||
} else if (selectedTab === TabViews.Links) {
|
|
||||||
tabViewRef.current = TabViews.Links;
|
|
||||||
mediaItems = links;
|
|
||||||
} else {
|
|
||||||
throw new Error(`Unexpected select tab: ${selectedTab}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{renderMiniPlayer()}
|
|
||||||
<div
|
|
||||||
className={tw(
|
|
||||||
'grow',
|
|
||||||
'overflow-x-hidden overflow-y-auto',
|
|
||||||
'p-5'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<MediaSection
|
<MediaSection
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
tab={tabViewRef.current}
|
tab={tab}
|
||||||
mediaItems={mediaItems}
|
mediaItems={mediaItems}
|
||||||
saveAttachment={saveAttachment}
|
saveAttachment={saveAttachment}
|
||||||
showLightbox={showLightbox}
|
showLightbox={showLightbox}
|
||||||
@@ -380,12 +330,8 @@ export function MediaGallery({
|
|||||||
playAudio={playAudio}
|
playAudio={playAudio}
|
||||||
renderMediaItem={renderMediaItem}
|
renderMediaItem={renderMediaItem}
|
||||||
/>
|
/>
|
||||||
<div ref={scrollObserverRef} className={tw('h-px')} />
|
<div ref={setObserverRef} className={tw('h-px')} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
112
ts/components/conversation/media-gallery/PanelHeader.dom.tsx
Normal file
112
ts/components/conversation/media-gallery/PanelHeader.dom.tsx
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
// Copyright 2025 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { tw } from '../../../axo/tw.dom.js';
|
||||||
|
import { ExperimentalAxoSegmentedControl } from '../../../axo/AxoSegmentedControl.dom.js';
|
||||||
|
import { AxoSelect } from '../../../axo/AxoSelect.dom.js';
|
||||||
|
import type { LocalizerType } from '../../../types/Util.std.js';
|
||||||
|
import type { MediaTabType } from '../../../types/MediaItem.std.js';
|
||||||
|
|
||||||
|
// Provided by smart layer
|
||||||
|
export type Props = Readonly<{
|
||||||
|
i18n: LocalizerType;
|
||||||
|
tab: MediaTabType;
|
||||||
|
setTab: (newTab: MediaTabType) => void;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export function PanelHeader({ i18n, tab, setTab }: Props): JSX.Element {
|
||||||
|
const setSelectedTabWithDefault = useCallback(
|
||||||
|
(value: string | null) => {
|
||||||
|
switch (value) {
|
||||||
|
case 'media':
|
||||||
|
case 'audio':
|
||||||
|
case 'documents':
|
||||||
|
case 'links':
|
||||||
|
setTab(value);
|
||||||
|
break;
|
||||||
|
case null:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
setTab('media');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setTab]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={tw(
|
||||||
|
'@container',
|
||||||
|
'grow',
|
||||||
|
'flex flex-row justify-center-safe',
|
||||||
|
// This matches the width of back button so that tabs are centered
|
||||||
|
'pe-[50px]'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={tw('hidden max-w-[320px] grow @min-[260px]:block')}>
|
||||||
|
<ExperimentalAxoSegmentedControl.Root
|
||||||
|
variant="no-track"
|
||||||
|
width="full"
|
||||||
|
itemWidth="equal"
|
||||||
|
value={tab}
|
||||||
|
onValueChange={setSelectedTabWithDefault}
|
||||||
|
>
|
||||||
|
<ExperimentalAxoSegmentedControl.Item value="media">
|
||||||
|
<ExperimentalAxoSegmentedControl.ItemText>
|
||||||
|
{i18n('icu:media')}
|
||||||
|
</ExperimentalAxoSegmentedControl.ItemText>
|
||||||
|
</ExperimentalAxoSegmentedControl.Item>
|
||||||
|
<ExperimentalAxoSegmentedControl.Item value="audio">
|
||||||
|
<ExperimentalAxoSegmentedControl.ItemText>
|
||||||
|
{i18n('icu:MediaGallery__tab__audio')}
|
||||||
|
</ExperimentalAxoSegmentedControl.ItemText>
|
||||||
|
</ExperimentalAxoSegmentedControl.Item>
|
||||||
|
<ExperimentalAxoSegmentedControl.Item value="links">
|
||||||
|
<ExperimentalAxoSegmentedControl.ItemText>
|
||||||
|
{i18n('icu:MediaGallery__tab__links')}
|
||||||
|
</ExperimentalAxoSegmentedControl.ItemText>
|
||||||
|
</ExperimentalAxoSegmentedControl.Item>
|
||||||
|
<ExperimentalAxoSegmentedControl.Item value="documents">
|
||||||
|
<ExperimentalAxoSegmentedControl.ItemText>
|
||||||
|
{i18n('icu:MediaGallery__tab__files')}
|
||||||
|
</ExperimentalAxoSegmentedControl.ItemText>
|
||||||
|
</ExperimentalAxoSegmentedControl.Item>
|
||||||
|
</ExperimentalAxoSegmentedControl.Root>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={tw('block @min-[260px]:hidden')}>
|
||||||
|
<AxoSelect.Root value={tab} onValueChange={setSelectedTabWithDefault}>
|
||||||
|
<AxoSelect.Trigger
|
||||||
|
variant="floating"
|
||||||
|
width="fit"
|
||||||
|
placeholder=""
|
||||||
|
chevron="always"
|
||||||
|
/>
|
||||||
|
<AxoSelect.Content position="dropdown">
|
||||||
|
<AxoSelect.Item value="media">
|
||||||
|
<AxoSelect.ItemText>{i18n('icu:media')}</AxoSelect.ItemText>
|
||||||
|
</AxoSelect.Item>
|
||||||
|
<AxoSelect.Item value="audio">
|
||||||
|
<AxoSelect.ItemText>
|
||||||
|
{i18n('icu:MediaGallery__tab__audio')}
|
||||||
|
</AxoSelect.ItemText>
|
||||||
|
</AxoSelect.Item>
|
||||||
|
<AxoSelect.Item value="links">
|
||||||
|
<AxoSelect.ItemText>
|
||||||
|
{i18n('icu:MediaGallery__tab__links')}
|
||||||
|
</AxoSelect.ItemText>
|
||||||
|
</AxoSelect.Item>
|
||||||
|
<AxoSelect.Item value="documents">
|
||||||
|
<AxoSelect.ItemText>
|
||||||
|
{i18n('icu:MediaGallery__tab__files')}
|
||||||
|
</AxoSelect.ItemText>
|
||||||
|
</AxoSelect.Item>
|
||||||
|
</AxoSelect.Content>
|
||||||
|
</AxoSelect.Root>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
// Copyright 2025 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
export enum TabViews {
|
|
||||||
Media = 'Media',
|
|
||||||
Audio = 'Audio',
|
|
||||||
Documents = 'Documents',
|
|
||||||
Links = 'Links',
|
|
||||||
}
|
|
||||||
@@ -28,6 +28,7 @@ import type {
|
|||||||
MessageExpiredActionType,
|
MessageExpiredActionType,
|
||||||
} from './conversations.preload.js';
|
} from './conversations.preload.js';
|
||||||
import type {
|
import type {
|
||||||
|
MediaTabType,
|
||||||
MediaItemMessageType,
|
MediaItemMessageType,
|
||||||
MediaItemType,
|
MediaItemType,
|
||||||
LinkPreviewMediaItemType,
|
LinkPreviewMediaItemType,
|
||||||
@@ -47,16 +48,17 @@ const { orderBy } = lodash;
|
|||||||
const log = createLogger('mediaGallery');
|
const log = createLogger('mediaGallery');
|
||||||
|
|
||||||
export type MediaGalleryStateType = ReadonlyDeep<{
|
export type MediaGalleryStateType = ReadonlyDeep<{
|
||||||
|
tab: MediaTabType;
|
||||||
conversationId: string | undefined;
|
conversationId: string | undefined;
|
||||||
haveOldestDocument: boolean;
|
|
||||||
haveOldestMedia: boolean;
|
haveOldestMedia: boolean;
|
||||||
haveOldestAudio: boolean;
|
haveOldestAudio: boolean;
|
||||||
haveOldestLink: boolean;
|
haveOldestLink: boolean;
|
||||||
|
haveOldestDocument: boolean;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
media: ReadonlyArray<MediaItemType>;
|
media: ReadonlyArray<MediaItemType>;
|
||||||
audio: ReadonlyArray<MediaItemType>;
|
audio: ReadonlyArray<MediaItemType>;
|
||||||
documents: ReadonlyArray<MediaItemType>;
|
|
||||||
links: ReadonlyArray<LinkPreviewMediaItemType>;
|
links: ReadonlyArray<LinkPreviewMediaItemType>;
|
||||||
|
documents: ReadonlyArray<MediaItemType>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
const FETCH_CHUNK_COUNT = 50;
|
const FETCH_CHUNK_COUNT = 50;
|
||||||
@@ -64,15 +66,16 @@ const FETCH_CHUNK_COUNT = 50;
|
|||||||
const INITIAL_LOAD = 'mediaGallery/INITIAL_LOAD';
|
const INITIAL_LOAD = 'mediaGallery/INITIAL_LOAD';
|
||||||
const LOAD_MORE = 'mediaGallery/LOAD_MORE';
|
const LOAD_MORE = 'mediaGallery/LOAD_MORE';
|
||||||
const SET_LOADING = 'mediaGallery/SET_LOADING';
|
const SET_LOADING = 'mediaGallery/SET_LOADING';
|
||||||
|
const SET_TAB = 'mediaGallery/SET_TAB';
|
||||||
|
|
||||||
type InitialLoadActionType = ReadonlyDeep<{
|
type InitialLoadActionType = ReadonlyDeep<{
|
||||||
type: typeof INITIAL_LOAD;
|
type: typeof INITIAL_LOAD;
|
||||||
payload: {
|
payload: {
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
documents: ReadonlyArray<MediaItemType>;
|
|
||||||
media: ReadonlyArray<MediaItemType>;
|
media: ReadonlyArray<MediaItemType>;
|
||||||
audio: ReadonlyArray<MediaItemType>;
|
audio: ReadonlyArray<MediaItemType>;
|
||||||
links: ReadonlyArray<LinkPreviewMediaItemType>;
|
links: ReadonlyArray<LinkPreviewMediaItemType>;
|
||||||
|
documents: ReadonlyArray<MediaItemType>;
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
type LoadMoreActionType = ReadonlyDeep<{
|
type LoadMoreActionType = ReadonlyDeep<{
|
||||||
@@ -81,8 +84,8 @@ type LoadMoreActionType = ReadonlyDeep<{
|
|||||||
conversationId: string;
|
conversationId: string;
|
||||||
media: ReadonlyArray<MediaItemType>;
|
media: ReadonlyArray<MediaItemType>;
|
||||||
audio: ReadonlyArray<MediaItemType>;
|
audio: ReadonlyArray<MediaItemType>;
|
||||||
documents: ReadonlyArray<MediaItemType>;
|
|
||||||
links: ReadonlyArray<LinkPreviewMediaItemType>;
|
links: ReadonlyArray<LinkPreviewMediaItemType>;
|
||||||
|
documents: ReadonlyArray<MediaItemType>;
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
type SetLoadingActionType = ReadonlyDeep<{
|
type SetLoadingActionType = ReadonlyDeep<{
|
||||||
@@ -91,6 +94,12 @@ type SetLoadingActionType = ReadonlyDeep<{
|
|||||||
loading: boolean;
|
loading: boolean;
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
|
type SetTabActionType = ReadonlyDeep<{
|
||||||
|
type: typeof SET_TAB;
|
||||||
|
payload: {
|
||||||
|
tab: MediaGalleryStateType['tab'];
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
|
||||||
type MediaGalleryActionType = ReadonlyDeep<
|
type MediaGalleryActionType = ReadonlyDeep<
|
||||||
| ConversationUnloadedActionType
|
| ConversationUnloadedActionType
|
||||||
@@ -100,6 +109,7 @@ type MediaGalleryActionType = ReadonlyDeep<
|
|||||||
| MessageDeletedActionType
|
| MessageDeletedActionType
|
||||||
| MessageExpiredActionType
|
| MessageExpiredActionType
|
||||||
| SetLoadingActionType
|
| SetLoadingActionType
|
||||||
|
| SetTabActionType
|
||||||
>;
|
>;
|
||||||
|
|
||||||
function _sortItems<
|
function _sortItems<
|
||||||
@@ -223,7 +233,7 @@ function initialLoad(
|
|||||||
|
|
||||||
function loadMore(
|
function loadMore(
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
type: 'media' | 'audio' | 'documents' | 'links'
|
type: MediaTabType
|
||||||
): ThunkAction<
|
): ThunkAction<
|
||||||
void,
|
void,
|
||||||
RootStateType,
|
RootStateType,
|
||||||
@@ -316,9 +326,19 @@ function loadMore(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setTab(tab: MediaGalleryStateType['tab']): SetTabActionType {
|
||||||
|
return {
|
||||||
|
type: SET_TAB,
|
||||||
|
payload: {
|
||||||
|
tab,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
initialLoad,
|
initialLoad,
|
||||||
loadMore,
|
loadMore,
|
||||||
|
setTab,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useMediaGalleryActions = (): BoundActionCreatorsMapObject<
|
export const useMediaGalleryActions = (): BoundActionCreatorsMapObject<
|
||||||
@@ -327,6 +347,7 @@ export const useMediaGalleryActions = (): BoundActionCreatorsMapObject<
|
|||||||
|
|
||||||
export function getEmptyState(): MediaGalleryStateType {
|
export function getEmptyState(): MediaGalleryStateType {
|
||||||
return {
|
return {
|
||||||
|
tab: 'media',
|
||||||
conversationId: undefined,
|
conversationId: undefined,
|
||||||
haveOldestDocument: false,
|
haveOldestDocument: false,
|
||||||
haveOldestMedia: false,
|
haveOldestMedia: false,
|
||||||
@@ -357,7 +378,7 @@ export function reducer(
|
|||||||
const { payload } = action;
|
const { payload } = action;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
tab: 'media',
|
||||||
loading: false,
|
loading: false,
|
||||||
conversationId: payload.conversationId,
|
conversationId: payload.conversationId,
|
||||||
haveOldestMedia: payload.media.length === 0,
|
haveOldestMedia: payload.media.length === 0,
|
||||||
@@ -391,6 +412,15 @@ export function reducer(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action.type === SET_TAB) {
|
||||||
|
const { tab } = action.payload;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
tab,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// A time-ordered subset of all conversation media is loaded at once.
|
// A time-ordered subset of all conversation media is loaded at once.
|
||||||
// When a message changes, check that the changed message falls within this time range,
|
// When a message changes, check that the changed message falls within this time range,
|
||||||
// and if so insert it into the loaded media.
|
// and if so insert it into the loaded media.
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ import { createLogger } from '../../logging/log.std.js';
|
|||||||
import { TimelineMessageLoadingState } from '../../util/timelineUtil.std.js';
|
import { TimelineMessageLoadingState } from '../../util/timelineUtil.std.js';
|
||||||
import { isSignalConversation } from '../../util/isSignalConversation.dom.js';
|
import { isSignalConversation } from '../../util/isSignalConversation.dom.js';
|
||||||
import { reduce } from '../../util/iterables.std.js';
|
import { reduce } from '../../util/iterables.std.js';
|
||||||
import { getConversationTitleForPanelType } from '../../util/getConversationTitleForPanelType.std.js';
|
|
||||||
import type { PanelRenderType } from '../../types/Panels.std.js';
|
import type { PanelRenderType } from '../../types/Panels.std.js';
|
||||||
import type { HasStories } from '../../types/Stories.std.js';
|
import type { HasStories } from '../../types/Stories.std.js';
|
||||||
import { getHasStoriesSelector } from './stories2.dom.js';
|
import { getHasStoriesSelector } from './stories2.dom.js';
|
||||||
@@ -1439,13 +1438,6 @@ export const getWasPanelAnimated = createSelector(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getConversationTitle = createSelector(
|
|
||||||
getIntl,
|
|
||||||
getActivePanel,
|
|
||||||
(i18n, panel): string | undefined =>
|
|
||||||
getConversationTitleForPanelType(i18n, panel?.type)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Note that this doesn't take into account max edit count. See canEditMessage.
|
// Note that this doesn't take into account max edit count. See canEditMessage.
|
||||||
export const getLastEditableMessageId = createSelector(
|
export const getLastEditableMessageId = createSelector(
|
||||||
getConversationMessages,
|
getConversationMessages,
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import {
|
|||||||
MediaItem,
|
MediaItem,
|
||||||
type PropsType as MediaItemPropsType,
|
type PropsType as MediaItemPropsType,
|
||||||
} from './MediaItem.preload.js';
|
} from './MediaItem.preload.js';
|
||||||
import { SmartMiniPlayer } from './MiniPlayer.preload.js';
|
|
||||||
|
|
||||||
const log = createLogger('AllMedia');
|
const log = createLogger('AllMedia');
|
||||||
|
|
||||||
@@ -25,10 +24,6 @@ export type PropsType = {
|
|||||||
conversationId: string;
|
conversationId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function renderMiniPlayer(): JSX.Element {
|
|
||||||
return <SmartMiniPlayer shouldFlow />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderMediaItem(props: MediaItemPropsType): JSX.Element {
|
function renderMediaItem(props: MediaItemPropsType): JSX.Element {
|
||||||
return <MediaItem {...props} />;
|
return <MediaItem {...props} />;
|
||||||
}
|
}
|
||||||
@@ -46,6 +41,7 @@ export const SmartAllMedia = memo(function SmartAllMedia({
|
|||||||
haveOldestLink,
|
haveOldestLink,
|
||||||
haveOldestDocument,
|
haveOldestDocument,
|
||||||
loading,
|
loading,
|
||||||
|
tab,
|
||||||
} = useSelector(getMediaGalleryState);
|
} = useSelector(getMediaGalleryState);
|
||||||
const { initialLoad, loadMore } = useMediaGalleryActions();
|
const { initialLoad, loadMore } = useMediaGalleryActions();
|
||||||
const {
|
const {
|
||||||
@@ -109,13 +105,13 @@ export const SmartAllMedia = memo(function SmartAllMedia({
|
|||||||
audio={audio}
|
audio={audio}
|
||||||
links={links}
|
links={links}
|
||||||
documents={documents}
|
documents={documents}
|
||||||
|
tab={tab}
|
||||||
showLightbox={showLightbox}
|
showLightbox={showLightbox}
|
||||||
playAudio={playAudio}
|
playAudio={playAudio}
|
||||||
kickOffAttachmentDownload={kickOffAttachmentDownload}
|
kickOffAttachmentDownload={kickOffAttachmentDownload}
|
||||||
cancelAttachmentDownload={cancelAttachmentDownload}
|
cancelAttachmentDownload={cancelAttachmentDownload}
|
||||||
saveAttachment={saveAttachment}
|
saveAttachment={saveAttachment}
|
||||||
renderMediaItem={renderMediaItem}
|
renderMediaItem={renderMediaItem}
|
||||||
renderMiniPlayer={renderMiniPlayer}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
16
ts/state/smart/AllMediaHeader.preload.tsx
Normal file
16
ts/state/smart/AllMediaHeader.preload.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
// Copyright 2025 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
import React, { memo } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { PanelHeader } from '../../components/conversation/media-gallery/PanelHeader.dom.js';
|
||||||
|
import { getMediaGalleryState } from '../selectors/mediaGallery.std.js';
|
||||||
|
import { getIntl } from '../selectors/user.std.js';
|
||||||
|
import { useMediaGalleryActions } from '../ducks/mediaGallery.preload.js';
|
||||||
|
|
||||||
|
export const SmartAllMediaHeader = memo(function SmartAllMediaHeader() {
|
||||||
|
const { tab } = useSelector(getMediaGalleryState);
|
||||||
|
const { setTab } = useMediaGalleryActions();
|
||||||
|
const i18n = useSelector(getIntl);
|
||||||
|
|
||||||
|
return <PanelHeader i18n={i18n} tab={tab} setTab={setTab} />;
|
||||||
|
});
|
||||||
@@ -17,6 +17,7 @@ import { createLogger } from '../../logging/log.std.js';
|
|||||||
import { PanelType } from '../../types/Panels.std.js';
|
import { PanelType } from '../../types/Panels.std.js';
|
||||||
import { toLogFormat } from '../../types/errors.std.js';
|
import { toLogFormat } from '../../types/errors.std.js';
|
||||||
import { SmartAllMedia } from './AllMedia.preload.js';
|
import { SmartAllMedia } from './AllMedia.preload.js';
|
||||||
|
import { SmartAllMediaHeader } from './AllMediaHeader.preload.js';
|
||||||
import { SmartChatColorPicker } from './ChatColorPicker.preload.js';
|
import { SmartChatColorPicker } from './ChatColorPicker.preload.js';
|
||||||
import { SmartContactDetail } from './ContactDetail.preload.js';
|
import { SmartContactDetail } from './ContactDetail.preload.js';
|
||||||
import { SmartConversationDetails } from './ConversationDetails.preload.js';
|
import { SmartConversationDetails } from './ConversationDetails.preload.js';
|
||||||
@@ -39,6 +40,7 @@ import { useConversationsActions } from '../ducks/conversations.preload.js';
|
|||||||
import { useReducedMotion } from '../../hooks/useReducedMotion.dom.js';
|
import { useReducedMotion } from '../../hooks/useReducedMotion.dom.js';
|
||||||
import { itemStorage } from '../../textsecure/Storage.preload.js';
|
import { itemStorage } from '../../textsecure/Storage.preload.js';
|
||||||
import { SmartPinnedMessagesPanel } from './PinnedMessagesPanel.preload.js';
|
import { SmartPinnedMessagesPanel } from './PinnedMessagesPanel.preload.js';
|
||||||
|
import { SmartMiniPlayer } from './MiniPlayer.preload.js';
|
||||||
|
|
||||||
const log = createLogger('ConversationPanel');
|
const log = createLogger('ConversationPanel');
|
||||||
|
|
||||||
@@ -288,6 +290,19 @@ const PanelContainer = forwardRef<
|
|||||||
const { popPanelForConversation } = useConversationsActions();
|
const { popPanelForConversation } = useConversationsActions();
|
||||||
const conversationTitle = getConversationTitleForPanelType(i18n, panel.type);
|
const conversationTitle = getConversationTitleForPanelType(i18n, panel.type);
|
||||||
|
|
||||||
|
let info: JSX.Element | undefined;
|
||||||
|
if (panel.type === PanelType.AllMedia) {
|
||||||
|
info = <SmartAllMediaHeader />;
|
||||||
|
} else if (conversationTitle != null) {
|
||||||
|
info = (
|
||||||
|
<div className="ConversationPanel__header__info">
|
||||||
|
<div className="ConversationPanel__header__info__title">
|
||||||
|
{conversationTitle}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const focusRef = useRef<HTMLDivElement | null>(null);
|
const focusRef = useRef<HTMLDivElement | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isActive) {
|
if (!isActive) {
|
||||||
@@ -315,18 +330,14 @@ const PanelContainer = forwardRef<
|
|||||||
onClick={popPanelForConversation}
|
onClick={popPanelForConversation}
|
||||||
type="button"
|
type="button"
|
||||||
/>
|
/>
|
||||||
{conversationTitle && (
|
{info}
|
||||||
<div className="ConversationPanel__header__info">
|
|
||||||
<div className="ConversationPanel__header__info__title">
|
|
||||||
{conversationTitle}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
<SmartMiniPlayer shouldFlow />
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'ConversationPanel__body',
|
'ConversationPanel__body',
|
||||||
panel.type !== PanelType.PinnedMessages &&
|
panel.type !== PanelType.PinnedMessages &&
|
||||||
|
panel.type !== PanelType.AllMedia &&
|
||||||
'ConversationPanel__body--padding'
|
'ConversationPanel__body--padding'
|
||||||
)}
|
)}
|
||||||
ref={focusRef}
|
ref={focusRef}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import type { AttachmentForUIType } from './Attachment.std.js';
|
|||||||
import type { LinkPreviewForUIType } from './message/LinkPreviews.std.js';
|
import type { LinkPreviewForUIType } from './message/LinkPreviews.std.js';
|
||||||
import type { ServiceIdString } from './ServiceId.std.js';
|
import type { ServiceIdString } from './ServiceId.std.js';
|
||||||
|
|
||||||
|
export type MediaTabType = 'media' | 'audio' | 'links' | 'documents';
|
||||||
|
|
||||||
export type MediaItemMessageType = Readonly<{
|
export type MediaItemMessageType = Readonly<{
|
||||||
id: string;
|
id: string;
|
||||||
type: MessageAttributesType['type'];
|
type: MessageAttributesType['type'];
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export function getConversationTitleForPanelType(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (panelType === PanelType.AllMedia) {
|
if (panelType === PanelType.AllMedia) {
|
||||||
return i18n('icu:allMedia');
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (panelType === PanelType.ChatColorEditor) {
|
if (panelType === PanelType.ChatColorEditor) {
|
||||||
@@ -24,11 +24,11 @@ export function getConversationTitleForPanelType(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (panelType === PanelType.ContactDetails) {
|
if (panelType === PanelType.ContactDetails) {
|
||||||
return '';
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (panelType === PanelType.ConversationDetails) {
|
if (panelType === PanelType.ConversationDetails) {
|
||||||
return '';
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (panelType === PanelType.GroupInvites) {
|
if (panelType === PanelType.GroupInvites) {
|
||||||
@@ -52,7 +52,7 @@ export function getConversationTitleForPanelType(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (panelType === PanelType.StickerManager) {
|
if (panelType === PanelType.StickerManager) {
|
||||||
return '';
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -1912,35 +1912,10 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "React-useRef",
|
"rule": "React-useRef",
|
||||||
"path": "ts/components/conversation/media-gallery/MediaGallery.dom.tsx",
|
"path": "ts/components/conversation/pinned-messages/PinnedMessagesPanel.dom.tsx",
|
||||||
"line": " const loadingRef = useRef<boolean>(false);",
|
"line": " const containerElementRef = useRef<HTMLDivElement>(null);",
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2024-09-03T00:45:23.978Z",
|
"updated": "2025-11-20T18:33:59.075Z"
|
||||||
"reasonDetail": "A boolean to help us avoid making too many 'load more' requests"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "React-useRef",
|
|
||||||
"path": "ts/components/conversation/media-gallery/MediaGallery.dom.tsx",
|
|
||||||
"line": " const intersectionObserver = useRef<IntersectionObserver | null>(null);",
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2024-09-03T00:45:23.978Z",
|
|
||||||
"reasonDetail": "A non-modifying reference to IntersectionObserver"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "React-useRef",
|
|
||||||
"path": "ts/components/conversation/media-gallery/MediaGallery.dom.tsx",
|
|
||||||
"line": " const scrollObserverRef = useRef<HTMLDivElement | null>(null);",
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2024-09-03T00:45:23.978Z",
|
|
||||||
"reasonDetail": "A non-modifying reference to the DOM"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "React-useRef",
|
|
||||||
"path": "ts/components/conversation/media-gallery/MediaGallery.dom.tsx",
|
|
||||||
"line": " const tabViewRef = useRef<TabViews>(TabViews.Media);",
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2024-09-03T00:45:23.978Z",
|
|
||||||
"reasonDetail": "Because we need the current tab value outside the callback"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "React-useRef",
|
"rule": "React-useRef",
|
||||||
@@ -1950,13 +1925,6 @@
|
|||||||
"updated": "2025-11-06T20:28:00.760Z",
|
"updated": "2025-11-06T20:28:00.760Z",
|
||||||
"reasonDetail": "Ref for timer"
|
"reasonDetail": "Ref for timer"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"rule": "React-useRef",
|
|
||||||
"path": "ts/components/conversation/pinned-messages/PinnedMessagesPanel.dom.tsx",
|
|
||||||
"line": " const containerElementRef = useRef<HTMLDivElement>(null);",
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2025-11-20T18:33:59.075Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"rule": "React-useRef",
|
"rule": "React-useRef",
|
||||||
"path": "ts/components/fun/FunGif.dom.tsx",
|
"path": "ts/components/fun/FunGif.dom.tsx",
|
||||||
|
|||||||
Reference in New Issue
Block a user