mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-05-08 08:58:38 +01:00
Add option to sort by file size in Media Gallery
This commit is contained in:
@@ -6872,6 +6872,22 @@
|
||||
"messageformat": "Links",
|
||||
"description": "Header of the links pane in the media gallery, showing links"
|
||||
},
|
||||
"icu:MediaGallery__sort": {
|
||||
"messageformat": "Sort by",
|
||||
"description": "Accessible label of the drop down trigger for sorting media gallery"
|
||||
},
|
||||
"icu:MediaGallery__sort--header": {
|
||||
"messageformat": "Sort by",
|
||||
"description": "Header displayed on top of the drop down menu for sorting media gallery"
|
||||
},
|
||||
"icu:MediaGallery__sort__date": {
|
||||
"messageformat": "Date",
|
||||
"description": "Sort by date option of drop down menu in media gallery"
|
||||
},
|
||||
"icu:MediaGallery__sort__size": {
|
||||
"messageformat": "Size",
|
||||
"description": "Sort by file size option of drop down menu in media gallery"
|
||||
},
|
||||
"icu:MediaGallery__EmptyState__title--media": {
|
||||
"messageformat": "No Media",
|
||||
"description": "Title of the empty state view of media gallery for media tab"
|
||||
|
||||
@@ -88,9 +88,11 @@ export function AttachmentSection({
|
||||
case 'media':
|
||||
return (
|
||||
<section className={tw('@container px-5')}>
|
||||
<h2 className={tw('ps-1 pt-4 pb-2 type-body-medium font-semibold')}>
|
||||
{header}
|
||||
</h2>
|
||||
{header != null && (
|
||||
<h2 className={tw('ps-1 pt-4 pb-2 type-body-medium font-semibold')}>
|
||||
{header}
|
||||
</h2>
|
||||
)}
|
||||
<div
|
||||
className={tw(
|
||||
'grid gap-1',
|
||||
@@ -118,9 +120,13 @@ export function AttachmentSection({
|
||||
case 'link':
|
||||
return (
|
||||
<section>
|
||||
<h2 className={tw('px-6 pt-1.5 pb-2 type-body-medium font-semibold')}>
|
||||
{header}
|
||||
</h2>
|
||||
{header != null && (
|
||||
<h2
|
||||
className={tw('px-6 pt-1.5 pb-2 type-body-medium font-semibold')}
|
||||
>
|
||||
{header}
|
||||
</h2>
|
||||
)}
|
||||
<div>
|
||||
{verified.entries.map(mediaItem => {
|
||||
return (
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import type { MediaTabType } from '../../../types/MediaItem.std.js';
|
||||
import type {
|
||||
MediaTabType,
|
||||
MediaSortOrderType,
|
||||
} from '../../../types/MediaItem.std.js';
|
||||
import type { Props } from './MediaGallery.dom.js';
|
||||
import { MediaGallery } from './MediaGallery.dom.js';
|
||||
import { PanelHeader } from './PanelHeader.dom.js';
|
||||
@@ -39,6 +42,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||
links: overrideProps.links || [],
|
||||
documents: overrideProps.documents || [],
|
||||
tab: overrideProps.tab || 'media',
|
||||
sortOrder: overrideProps.sortOrder || 'date',
|
||||
|
||||
initialLoad: action('initialLoad'),
|
||||
loadMore: action('loadMore'),
|
||||
@@ -54,6 +58,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||
|
||||
function Panel(props: Props): React.JSX.Element {
|
||||
const [tab, setTab] = useState<MediaTabType>(props.tab);
|
||||
const [sortOrder, setSortOrder] = useState<MediaSortOrderType>('date');
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
@@ -71,14 +76,30 @@ function Panel(props: Props): React.JSX.Element {
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PanelHeader i18n={i18n} tab={tab} setTab={setTab} />
|
||||
<MediaGallery
|
||||
{...props}
|
||||
tab={tab}
|
||||
loading={loading}
|
||||
loadMore={loadMore}
|
||||
/>
|
||||
<div className="ConversationPanel">
|
||||
<div className="ConversationPanel__header">
|
||||
<button
|
||||
aria-label={i18n('icu:goBack')}
|
||||
className="ConversationPanel__header__back-button"
|
||||
type="button"
|
||||
/>
|
||||
<PanelHeader
|
||||
i18n={i18n}
|
||||
tab={tab}
|
||||
setTab={setTab}
|
||||
sortOrder={sortOrder}
|
||||
setSortOrder={setSortOrder}
|
||||
/>
|
||||
</div>
|
||||
<div className="ConversationPanel__body">
|
||||
<MediaGallery
|
||||
{...props}
|
||||
tab={tab}
|
||||
loading={loading}
|
||||
loadMore={loadMore}
|
||||
sortOrder={sortOrder}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import React, {
|
||||
useRef,
|
||||
useCallback,
|
||||
useState,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
|
||||
import moment from 'moment';
|
||||
@@ -15,6 +16,7 @@ import type { ItemClickEvent } from './types/ItemClickEvent.std.js';
|
||||
import type { LocalizerType } from '../../../types/Util.std.js';
|
||||
import type {
|
||||
MediaTabType,
|
||||
MediaSortOrderType,
|
||||
LinkPreviewMediaItemType,
|
||||
ContactMediaItemType,
|
||||
MediaItemType,
|
||||
@@ -49,6 +51,7 @@ export type Props = {
|
||||
links: ReadonlyArray<LinkPreviewMediaItemType>;
|
||||
documents: ReadonlyArray<MediaItemType | ContactMediaItemType>;
|
||||
tab: MediaTabType;
|
||||
sortOrder: MediaSortOrderType;
|
||||
saveAttachment: SaveAttachmentActionCreatorType;
|
||||
pushPanelForConversation: PushPanelForConversationActionType;
|
||||
kickOffAttachmentDownload: (options: { messageId: string }) => void;
|
||||
@@ -71,6 +74,7 @@ function MediaSection({
|
||||
i18n,
|
||||
loading,
|
||||
tab,
|
||||
sortOrder,
|
||||
mediaItems,
|
||||
saveAttachment,
|
||||
pushPanelForConversation,
|
||||
@@ -92,6 +96,7 @@ function MediaSection({
|
||||
| 'renderMediaItem'
|
||||
> & {
|
||||
tab: MediaTabType;
|
||||
sortOrder: MediaSortOrderType;
|
||||
mediaItems: ReadonlyArray<GenericMediaItemType>;
|
||||
}): React.JSX.Element {
|
||||
const onItemClick = useCallback(
|
||||
@@ -142,6 +147,10 @@ function MediaSection({
|
||||
]
|
||||
);
|
||||
|
||||
const reversedMediaItems = useMemo(() => {
|
||||
return mediaItems.toReversed();
|
||||
}, [mediaItems]);
|
||||
|
||||
if (mediaItems.length === 0) {
|
||||
if (loading) {
|
||||
return <div />;
|
||||
@@ -155,6 +164,20 @@ function MediaSection({
|
||||
|
||||
const isGrid = mediaItems.at(0)?.type === 'media';
|
||||
|
||||
if (sortOrder === 'size') {
|
||||
return (
|
||||
<div className={tw('grow', 'mx-auto', 'max-w-[660px] min-w-[360px]')}>
|
||||
<div className={tw('flex flex-col')}>
|
||||
<AttachmentSection
|
||||
mediaItems={reversedMediaItems}
|
||||
onItemClick={onItemClick}
|
||||
renderMediaItem={renderMediaItem}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const sections = groupedItems.map((section, index) => {
|
||||
const isLast = index === groupedItems.length - 1;
|
||||
const first = section.mediaItems[0];
|
||||
@@ -219,6 +242,7 @@ export function MediaGallery({
|
||||
links,
|
||||
documents,
|
||||
tab,
|
||||
sortOrder,
|
||||
saveAttachment,
|
||||
pushPanelForConversation,
|
||||
kickOffAttachmentDownload,
|
||||
@@ -266,6 +290,7 @@ export function MediaGallery({
|
||||
audio.length,
|
||||
links.length,
|
||||
documents.length,
|
||||
sortOrder,
|
||||
]);
|
||||
|
||||
const [setObserverRef, observerEntry] = useIntersectionObserver();
|
||||
@@ -339,6 +364,7 @@ export function MediaGallery({
|
||||
i18n={i18n}
|
||||
loading={loading}
|
||||
tab={tab}
|
||||
sortOrder={sortOrder}
|
||||
mediaItems={mediaItems}
|
||||
saveAttachment={saveAttachment}
|
||||
pushPanelForConversation={pushPanelForConversation}
|
||||
|
||||
@@ -32,6 +32,7 @@ const createProps = (
|
||||
i18n,
|
||||
theme,
|
||||
mediaItem: overrideProps.mediaItem,
|
||||
showSize: false,
|
||||
onClick: action('onClick'),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
|
||||
export type Props = Readonly<{
|
||||
mediaItem: ReadonlyDeep<MediaItemType>;
|
||||
showSize: boolean;
|
||||
onClick?: (attachmentState: AttachmentStatusType['state']) => void;
|
||||
i18n: LocalizerType;
|
||||
theme?: ThemeType;
|
||||
@@ -36,6 +37,7 @@ export type Props = Readonly<{
|
||||
export function MediaGridItem(props: Props): React.JSX.Element {
|
||||
const {
|
||||
mediaItem: { attachment },
|
||||
showSize,
|
||||
i18n,
|
||||
theme,
|
||||
onClick,
|
||||
@@ -93,7 +95,12 @@ export function MediaGridItem(props: Props): React.JSX.Element {
|
||||
>
|
||||
{imageOrBlurHash}
|
||||
|
||||
<MetadataOverlay i18n={i18n} status={status} attachment={attachment} />
|
||||
<MetadataOverlay
|
||||
i18n={i18n}
|
||||
status={status}
|
||||
attachment={attachment}
|
||||
showSize={showSize}
|
||||
/>
|
||||
<SpinnerOverlay status={status} />
|
||||
</button>
|
||||
);
|
||||
@@ -145,25 +152,31 @@ type MetadataOverlayProps = Readonly<{
|
||||
i18n: LocalizerType;
|
||||
status: AttachmentStatusType;
|
||||
attachment: AttachmentForUIType;
|
||||
showSize: boolean;
|
||||
}>;
|
||||
|
||||
function MetadataOverlay(
|
||||
props: MetadataOverlayProps
|
||||
): React.JSX.Element | undefined {
|
||||
const { i18n, status, attachment } = props;
|
||||
const { i18n, status, attachment, showSize } = props;
|
||||
|
||||
if (
|
||||
status.state === 'ReadyToShow' &&
|
||||
!isGIF([attachment]) &&
|
||||
!isVideoAttachment(attachment)
|
||||
!isVideoAttachment(attachment) &&
|
||||
!showSize
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let text: string;
|
||||
if (isGIF([attachment]) && status.state === 'ReadyToShow') {
|
||||
if (!showSize && isGIF([attachment]) && status.state === 'ReadyToShow') {
|
||||
text = i18n('icu:message--getNotificationText--gif');
|
||||
} else if (isVideoAttachment(attachment) && attachment.duration != null) {
|
||||
} else if (
|
||||
!showSize &&
|
||||
isVideoAttachment(attachment) &&
|
||||
attachment.duration != null
|
||||
) {
|
||||
text = formatDuration(attachment.duration);
|
||||
} else {
|
||||
text = formatFileSize(attachment.size);
|
||||
|
||||
@@ -6,17 +6,30 @@ 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 { AxoDropdownMenu } from '../../../axo/AxoDropdownMenu.dom.js';
|
||||
import { AxoIconButton } from '../../../axo/AxoIconButton.dom.js';
|
||||
import type { LocalizerType } from '../../../types/Util.std.js';
|
||||
import type { MediaTabType } from '../../../types/MediaItem.std.js';
|
||||
import type {
|
||||
MediaTabType,
|
||||
MediaSortOrderType,
|
||||
} from '../../../types/MediaItem.std.js';
|
||||
|
||||
// Provided by smart layer
|
||||
export type Props = Readonly<{
|
||||
i18n: LocalizerType;
|
||||
tab: MediaTabType;
|
||||
setTab: (newTab: MediaTabType) => void;
|
||||
sortOrder: MediaSortOrderType;
|
||||
setSortOrder: (newOrder: MediaSortOrderType) => void;
|
||||
}>;
|
||||
|
||||
export function PanelHeader({ i18n, tab, setTab }: Props): React.JSX.Element {
|
||||
export function PanelHeader({
|
||||
i18n,
|
||||
tab,
|
||||
setTab,
|
||||
sortOrder,
|
||||
setSortOrder,
|
||||
}: Props): React.JSX.Element {
|
||||
const setSelectedTabWithDefault = useCallback(
|
||||
(value: string | null) => {
|
||||
switch (value) {
|
||||
@@ -36,16 +49,14 @@ export function PanelHeader({ i18n, tab, setTab }: Props): React.JSX.Element {
|
||||
[setTab]
|
||||
);
|
||||
|
||||
const isNonDefaultSorting = sortOrder !== 'date';
|
||||
|
||||
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]'
|
||||
)}
|
||||
className={tw('@container', 'grow', 'flex flex-row justify-center-safe')}
|
||||
>
|
||||
<div className={tw('grow')} />
|
||||
|
||||
<div className={tw('hidden max-w-[320px] grow @min-[260px]:block')}>
|
||||
<ExperimentalAxoSegmentedControl.Root
|
||||
variant="no-track"
|
||||
@@ -107,6 +118,38 @@ export function PanelHeader({ i18n, tab, setTab }: Props): React.JSX.Element {
|
||||
</AxoSelect.Content>
|
||||
</AxoSelect.Root>
|
||||
</div>
|
||||
|
||||
<div className={tw('grow')} />
|
||||
|
||||
<AxoDropdownMenu.Root>
|
||||
<AxoDropdownMenu.Trigger>
|
||||
<AxoIconButton.Root
|
||||
variant={isNonDefaultSorting ? 'primary' : 'borderless-secondary'}
|
||||
size="md"
|
||||
symbol="sort-vertical"
|
||||
aria-label={i18n('icu:MediaGallery__sort')}
|
||||
/>
|
||||
</AxoDropdownMenu.Trigger>
|
||||
<AxoDropdownMenu.Content>
|
||||
<AxoDropdownMenu.Label>
|
||||
{i18n('icu:MediaGallery__sort--header')}
|
||||
</AxoDropdownMenu.Label>
|
||||
<AxoDropdownMenu.CheckboxItem
|
||||
checked={sortOrder === 'date'}
|
||||
onCheckedChange={() => setSortOrder('date')}
|
||||
>
|
||||
{i18n('icu:MediaGallery__sort__date')}
|
||||
</AxoDropdownMenu.CheckboxItem>
|
||||
<AxoDropdownMenu.CheckboxItem
|
||||
checked={sortOrder === 'size'}
|
||||
onCheckedChange={() => setSortOrder('size')}
|
||||
>
|
||||
{i18n('icu:MediaGallery__sort__size')}
|
||||
</AxoDropdownMenu.CheckboxItem>
|
||||
</AxoDropdownMenu.Content>
|
||||
</AxoDropdownMenu.Root>
|
||||
|
||||
<div className={tw('min-w-4.5')} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,7 +44,12 @@ export function MediaItem({
|
||||
);
|
||||
case 'media':
|
||||
return (
|
||||
<MediaGridItem mediaItem={mediaItem} onClick={onClick} i18n={i18n} />
|
||||
<MediaGridItem
|
||||
mediaItem={mediaItem}
|
||||
onClick={onClick}
|
||||
i18n={i18n}
|
||||
showSize={false}
|
||||
/>
|
||||
);
|
||||
case 'document':
|
||||
return (
|
||||
|
||||
+12
-7
@@ -610,24 +610,29 @@ export type GetSortedMediaOptionsType = Readonly<{
|
||||
messageId?: string;
|
||||
receivedAt?: number;
|
||||
sentAt?: number;
|
||||
order: 'older' | 'newer';
|
||||
size?: number;
|
||||
order: 'older' | 'newer' | 'bigger';
|
||||
type: 'media' | 'audio' | 'documents';
|
||||
}>;
|
||||
|
||||
export type GetOlderDocumentsOptionsType = Readonly<{
|
||||
export type GetSortedDocumentsOptionsType = Readonly<{
|
||||
conversationId: string;
|
||||
limit: number;
|
||||
messageId?: string;
|
||||
receivedAt?: number;
|
||||
sentAt?: number;
|
||||
size?: number;
|
||||
order: 'older' | 'bigger';
|
||||
}>;
|
||||
|
||||
export type GetOlderNonAttachmentMediaOptionsType = Readonly<{
|
||||
export type GetSortedNonAttachmentMediaOptionsType = Readonly<{
|
||||
conversationId: string;
|
||||
limit: number;
|
||||
messageId?: string;
|
||||
receivedAt?: number;
|
||||
sentAt?: number;
|
||||
size?: number;
|
||||
order: 'older' | 'bigger';
|
||||
type: 'links' | 'contacts';
|
||||
}>;
|
||||
|
||||
@@ -886,11 +891,11 @@ type ReadableInterface = {
|
||||
getSortedMedia: (
|
||||
options: GetSortedMediaOptionsType
|
||||
) => Array<MediaItemDBType>;
|
||||
getOlderNonAttachmentMedia: (
|
||||
options: GetOlderNonAttachmentMediaOptionsType
|
||||
getSortedNonAttachmentMedia: (
|
||||
options: GetSortedNonAttachmentMediaOptionsType
|
||||
) => Array<NonAttachmentMediaItemDBType>;
|
||||
getOlderDocuments: (
|
||||
options: GetOlderDocumentsOptionsType
|
||||
getSortedDocuments: (
|
||||
options: GetSortedDocumentsOptionsType
|
||||
) => Array<MediaItemDBType | ContactMediaItemDBType>;
|
||||
getAllStories: (options: {
|
||||
conversationId?: string;
|
||||
|
||||
+82
-29
@@ -49,7 +49,7 @@ import { isNormalNumber } from '../util/isNormalNumber.std.js';
|
||||
import { isNotNil } from '../util/isNotNil.std.js';
|
||||
import { parseIntOrThrow } from '../util/parseIntOrThrow.std.js';
|
||||
import { updateSchema } from './migrations/index.node.js';
|
||||
import type { JSONRows, QueryFragment } from './util.std.js';
|
||||
import type { JSONRows, QueryFragment, QueryTemplate } from './util.std.js';
|
||||
import {
|
||||
batchMultiVarQuery,
|
||||
bulkAdd,
|
||||
@@ -136,8 +136,8 @@ import type {
|
||||
GetKnownMessageAttachmentsResultType,
|
||||
GetNearbyMessageFromDeletedSetOptionsType,
|
||||
GetSortedMediaOptionsType,
|
||||
GetOlderNonAttachmentMediaOptionsType,
|
||||
GetOlderDocumentsOptionsType,
|
||||
GetSortedNonAttachmentMediaOptionsType,
|
||||
GetSortedDocumentsOptionsType,
|
||||
GetRecentStoryRepliesOptionsType,
|
||||
GetUnreadByConversationAndMarkReadResultType,
|
||||
IdentityKeyIdType,
|
||||
@@ -479,8 +479,8 @@ export const DataReader: ServerReadableInterface = {
|
||||
|
||||
hasMedia,
|
||||
getSortedMedia,
|
||||
getOlderNonAttachmentMedia,
|
||||
getOlderDocuments,
|
||||
getSortedNonAttachmentMedia,
|
||||
getSortedDocuments,
|
||||
|
||||
getAllNotificationProfiles,
|
||||
getNotificationProfileById,
|
||||
@@ -5332,19 +5332,23 @@ function getSortedMedia(
|
||||
messageId,
|
||||
receivedAt: givenReceivedAt,
|
||||
sentAt: givenSentAt,
|
||||
size: givenSize,
|
||||
type,
|
||||
}: GetSortedMediaOptionsType
|
||||
): Array<MediaItemDBType> {
|
||||
let timeFilters: {
|
||||
let index: QueryFragment;
|
||||
let sortFilters: {
|
||||
first: QueryFragment;
|
||||
second: QueryFragment;
|
||||
third?: QueryFragment;
|
||||
};
|
||||
let timeOrder: QueryFragment;
|
||||
let orderFragment: QueryFragment;
|
||||
if (order === 'older') {
|
||||
const maxReceivedAt = givenReceivedAt ?? Number.MAX_VALUE;
|
||||
const maxSentAt = givenSentAt ?? Number.MAX_VALUE;
|
||||
|
||||
timeFilters = {
|
||||
index = sqlFragment`message_attachments_getOlderMedia`;
|
||||
sortFilters = {
|
||||
first: sqlFragment`
|
||||
message_attachments.receivedAt = ${maxReceivedAt}
|
||||
AND
|
||||
@@ -5352,12 +5356,16 @@ function getSortedMedia(
|
||||
`,
|
||||
second: sqlFragment`message_attachments.receivedAt < ${maxReceivedAt}`,
|
||||
};
|
||||
timeOrder = sqlFragment`DESC`;
|
||||
orderFragment = sqlFragment`
|
||||
message_attachments.receivedAt DESC,
|
||||
message_attachments.sentAt DESC
|
||||
`;
|
||||
} else if (order === 'newer') {
|
||||
const minReceivedAt = givenReceivedAt ?? Number.MIN_VALUE;
|
||||
const minSentAt = givenSentAt ?? Number.MIN_VALUE;
|
||||
|
||||
timeFilters = {
|
||||
index = sqlFragment`message_attachments_getOlderMedia`;
|
||||
sortFilters = {
|
||||
first: sqlFragment`
|
||||
message_attachments.receivedAt = ${minReceivedAt}
|
||||
AND
|
||||
@@ -5365,7 +5373,38 @@ function getSortedMedia(
|
||||
`,
|
||||
second: sqlFragment`message_attachments.receivedAt > ${minReceivedAt}`,
|
||||
};
|
||||
timeOrder = sqlFragment`ASC`;
|
||||
orderFragment = sqlFragment`
|
||||
message_attachments.receivedAt ASC,
|
||||
message_attachments.sentAt ASC
|
||||
`;
|
||||
} else if (order === 'bigger') {
|
||||
const maxSize = givenSize ?? Number.MAX_VALUE;
|
||||
const maxReceivedAt = givenReceivedAt ?? Number.MAX_VALUE;
|
||||
const maxSentAt = givenSentAt ?? Number.MAX_VALUE;
|
||||
|
||||
index = sqlFragment`message_attachments_sortBiggerMedia`;
|
||||
sortFilters = {
|
||||
first: sqlFragment`
|
||||
message_attachments.size = ${maxSize}
|
||||
AND
|
||||
message_attachments.receivedAt = ${maxReceivedAt}
|
||||
AND
|
||||
message_attachments.sentAt < ${maxSentAt}
|
||||
`,
|
||||
second: sqlFragment`
|
||||
message_attachments.size = ${maxSize}
|
||||
AND
|
||||
message_attachments.receivedAt < ${maxReceivedAt}
|
||||
`,
|
||||
third: sqlFragment`
|
||||
message_attachments.size < ${maxSize}
|
||||
`,
|
||||
};
|
||||
orderFragment = sqlFragment`
|
||||
message_attachments.size DESC,
|
||||
message_attachments.receivedAt DESC,
|
||||
message_attachments.sentAt DESC
|
||||
`;
|
||||
} else {
|
||||
throw missingCaseError(order);
|
||||
}
|
||||
@@ -5401,7 +5440,7 @@ function getSortedMedia(
|
||||
throw missingCaseError(type);
|
||||
}
|
||||
|
||||
const createQuery = (timeFilter: QueryFragment): QueryFragment => sqlFragment`
|
||||
const createQuery = (sortFilter: QueryFragment): QueryFragment => sqlFragment`
|
||||
SELECT
|
||||
message_attachments.*,
|
||||
messages.json -> '$.sendStateByConversationId' AS messageSendState,
|
||||
@@ -5411,7 +5450,7 @@ function getSortedMedia(
|
||||
messages.source AS messageSource,
|
||||
messages.sourceServiceId AS messageSourceServiceId
|
||||
FROM message_attachments
|
||||
INDEXED BY message_attachments_getOlderMedia
|
||||
INDEXED BY ${index}
|
||||
INNER JOIN messages ON
|
||||
messages.id = message_attachments.messageId
|
||||
WHERE
|
||||
@@ -5419,23 +5458,37 @@ function getSortedMedia(
|
||||
message_attachments.editHistoryIndex IS -1 AND
|
||||
message_attachments.attachmentType IS 'attachment' AND
|
||||
(
|
||||
${timeFilter}
|
||||
${sortFilter}
|
||||
) AND
|
||||
(${contentFilter}) AND
|
||||
message_attachments.isViewOnce IS NOT 1 AND
|
||||
message_attachments.messageType IN ('incoming', 'outgoing') AND
|
||||
(${messageId ?? null} IS NULL OR message_attachments.messageId IS NOT ${messageId ?? null})
|
||||
ORDER BY
|
||||
message_attachments.receivedAt ${timeOrder},
|
||||
message_attachments.sentAt ${timeOrder}
|
||||
ORDER BY ${orderFragment}
|
||||
LIMIT ${limit}
|
||||
`;
|
||||
|
||||
const [query, params] = sql`
|
||||
SELECT first.* FROM (${createQuery(timeFilters.first)}) as first
|
||||
UNION ALL
|
||||
SELECT second.* FROM (${createQuery(timeFilters.second)}) as second
|
||||
`;
|
||||
let template: QueryTemplate;
|
||||
if (order === 'older' || order === 'newer') {
|
||||
template = sql`
|
||||
SELECT first.* FROM (${createQuery(sortFilters.first)}) as first
|
||||
UNION ALL
|
||||
SELECT second.* FROM (${createQuery(sortFilters.second)}) as second
|
||||
`;
|
||||
} else if (order === 'bigger') {
|
||||
strictAssert(sortFilters.third != null, 'file size filter is required');
|
||||
template = sql`
|
||||
SELECT first.* FROM (${createQuery(sortFilters.first)}) as first
|
||||
UNION ALL
|
||||
SELECT second.* FROM (${createQuery(sortFilters.second)}) as second
|
||||
UNION ALL
|
||||
SELECT third.* FROM (${createQuery(sortFilters.third)}) as third
|
||||
`;
|
||||
} else {
|
||||
throw missingCaseError(order);
|
||||
}
|
||||
|
||||
const [query, params] = template;
|
||||
|
||||
const results: Array<
|
||||
MessageAttachmentDBType & {
|
||||
@@ -5486,7 +5539,7 @@ function getSortedMedia(
|
||||
});
|
||||
}
|
||||
|
||||
function getOlderNonAttachmentMedia(
|
||||
function getSortedNonAttachmentMedia(
|
||||
db: ReadableDB,
|
||||
{
|
||||
conversationId,
|
||||
@@ -5495,7 +5548,7 @@ function getOlderNonAttachmentMedia(
|
||||
receivedAt: maxReceivedAt = Number.MAX_VALUE,
|
||||
sentAt: maxSentAt = Number.MAX_VALUE,
|
||||
type,
|
||||
}: GetOlderNonAttachmentMediaOptionsType
|
||||
}: GetSortedNonAttachmentMediaOptionsType
|
||||
): Array<NonAttachmentMediaItemDBType> {
|
||||
const timeFilters = {
|
||||
first: sqlFragment`received_at = ${maxReceivedAt} AND sent_at < ${maxSentAt}`,
|
||||
@@ -5557,7 +5610,7 @@ function getOlderNonAttachmentMedia(
|
||||
if (type === 'links') {
|
||||
strictAssert(
|
||||
row.preview != null && row.preview.length >= 1,
|
||||
`getOlderNonAttachmentMedia: got message without preview ${row.id}`
|
||||
`getSortedNonAttachmentMedia: got message without preview ${row.id}`
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -5570,7 +5623,7 @@ function getOlderNonAttachmentMedia(
|
||||
if (type === 'contacts') {
|
||||
strictAssert(
|
||||
row.contact != null && row.contact.length >= 1,
|
||||
`getOlderNonAttachmentMedia: got message without contact ${row.id}`
|
||||
`getSortedNonAttachmentMedia: got message without contact ${row.id}`
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -5584,9 +5637,9 @@ function getOlderNonAttachmentMedia(
|
||||
});
|
||||
}
|
||||
|
||||
function getOlderDocuments(
|
||||
function getSortedDocuments(
|
||||
db: ReadableDB,
|
||||
options: GetOlderDocumentsOptionsType
|
||||
options: GetSortedDocumentsOptionsType
|
||||
): Array<MediaItemDBType | ContactMediaItemDBType> {
|
||||
return db.transaction(() => {
|
||||
const documents = getSortedMedia(db, {
|
||||
@@ -5594,7 +5647,7 @@ function getOlderDocuments(
|
||||
order: 'older',
|
||||
type: 'documents',
|
||||
});
|
||||
const contacts = getOlderNonAttachmentMedia(db, {
|
||||
const contacts = getSortedNonAttachmentMedia(db, {
|
||||
...options,
|
||||
type: 'contacts',
|
||||
}) as Array<ContactMediaItemDBType>;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { WritableDB } from '../Interface.std.js';
|
||||
|
||||
export default function updateToSchemaVersion1620(db: WritableDB): void {
|
||||
db.exec(`
|
||||
CREATE INDEX message_attachments_sortBiggerMedia ON message_attachments
|
||||
(conversationId, attachmentType, size DESC, receivedAt DESC, sentAt DESC)
|
||||
WHERE
|
||||
editHistoryIndex IS -1 AND
|
||||
messageType IN ('incoming', 'outgoing') AND
|
||||
isViewOnce IS NOT 1;
|
||||
`);
|
||||
}
|
||||
@@ -138,6 +138,7 @@ import updateToSchemaVersion1580 from './1580-expired-group-replies.std.js';
|
||||
import updateToSchemaVersion1590 from './1590-megaphones.std.js';
|
||||
import updateToSchemaVersion1600 from './1600-deduplicate-usernames.std.js';
|
||||
import updateToSchemaVersion1610 from './1610-has-contacts.std.js';
|
||||
import updateToSchemaVersion1620 from './1620-sort-bigger-media.std.js';
|
||||
|
||||
import { DataWriter } from '../Server.node.js';
|
||||
|
||||
@@ -1636,6 +1637,7 @@ export const SCHEMA_VERSIONS: ReadonlyArray<SchemaUpdateType> = [
|
||||
|
||||
{ version: 1600, update: updateToSchemaVersion1600 },
|
||||
{ version: 1610, update: updateToSchemaVersion1610 },
|
||||
{ version: 1620, update: updateToSchemaVersion1620 },
|
||||
];
|
||||
|
||||
export class DBVersionFromFutureError extends Error {
|
||||
|
||||
@@ -30,11 +30,14 @@ import type {
|
||||
} from './conversations.preload.js';
|
||||
import type {
|
||||
MediaTabType,
|
||||
MediaSortOrderType,
|
||||
MediaItemMessageType,
|
||||
MediaItemType,
|
||||
LinkPreviewMediaItemType,
|
||||
ContactMediaItemType,
|
||||
GenericMediaItemType,
|
||||
} from '../../types/MediaItem.std.js';
|
||||
import type { AttachmentForUIType } from '../../types/Attachment.std.js';
|
||||
import {
|
||||
isFile,
|
||||
isVisualMedia,
|
||||
@@ -52,6 +55,7 @@ const log = createLogger('mediaGallery');
|
||||
|
||||
export type MediaGalleryStateType = ReadonlyDeep<{
|
||||
tab: MediaTabType;
|
||||
sortOrder: MediaSortOrderType;
|
||||
conversationId: string | undefined;
|
||||
haveOldestMedia: boolean;
|
||||
haveOldestAudio: boolean;
|
||||
@@ -70,6 +74,7 @@ const INITIAL_LOAD = 'mediaGallery/INITIAL_LOAD';
|
||||
const LOAD_MORE = 'mediaGallery/LOAD_MORE';
|
||||
const SET_LOADING = 'mediaGallery/SET_LOADING';
|
||||
const SET_TAB = 'mediaGallery/SET_TAB';
|
||||
const SET_SORT_ORDER = 'mediaGallery/SET_SORT_ORDER';
|
||||
|
||||
type InitialLoadActionType = ReadonlyDeep<{
|
||||
type: typeof INITIAL_LOAD;
|
||||
@@ -103,6 +108,12 @@ type SetTabActionType = ReadonlyDeep<{
|
||||
tab: MediaGalleryStateType['tab'];
|
||||
};
|
||||
}>;
|
||||
type SetSortOrderActionType = ReadonlyDeep<{
|
||||
type: typeof SET_SORT_ORDER;
|
||||
payload: {
|
||||
sortOrder: MediaGalleryStateType['sortOrder'];
|
||||
};
|
||||
}>;
|
||||
|
||||
type MediaGalleryActionType = ReadonlyDeep<
|
||||
| ConversationUnloadedActionType
|
||||
@@ -113,16 +124,107 @@ type MediaGalleryActionType = ReadonlyDeep<
|
||||
| MessageExpiredActionType
|
||||
| SetLoadingActionType
|
||||
| SetTabActionType
|
||||
| SetSortOrderActionType
|
||||
>;
|
||||
|
||||
function getMediaItemSize(item: GenericMediaItemType): number {
|
||||
switch (item.type) {
|
||||
case 'media':
|
||||
case 'audio':
|
||||
case 'document':
|
||||
return item.attachment.size;
|
||||
case 'link':
|
||||
case 'contact':
|
||||
return 0;
|
||||
default:
|
||||
throw missingCaseError(item);
|
||||
}
|
||||
}
|
||||
|
||||
function _updateMedia<ItemType extends GenericMediaItemType>({
|
||||
message,
|
||||
haveOldest,
|
||||
media,
|
||||
newMedia,
|
||||
sortOrder,
|
||||
}: {
|
||||
message: ReadonlyMessageAttributesType;
|
||||
haveOldest: boolean;
|
||||
media: ReadonlyArray<ItemType>;
|
||||
newMedia: ReadonlyArray<ItemType>;
|
||||
sortOrder: MediaSortOrderType;
|
||||
}): [ReadonlyArray<ItemType>, boolean] {
|
||||
const mediaWithout = media.filter(item => item.message.id !== message.id);
|
||||
const difference = media.length - mediaWithout.length;
|
||||
|
||||
if (message.deletedForEveryone || message.isErased) {
|
||||
// If message is erased and there was media from this message - update state
|
||||
if (difference > 0) {
|
||||
return [mediaWithout, haveOldest];
|
||||
}
|
||||
return [media, haveOldest];
|
||||
}
|
||||
|
||||
const oldest = media[0];
|
||||
|
||||
let inMediaTimeRange: boolean;
|
||||
|
||||
if (oldest == null) {
|
||||
inMediaTimeRange = true;
|
||||
} else if (sortOrder === 'date') {
|
||||
inMediaTimeRange =
|
||||
message.received_at >= oldest.message.receivedAt &&
|
||||
message.sent_at >= oldest.message.sentAt;
|
||||
} else if (sortOrder === 'size') {
|
||||
const messageLatest = _sortItems(newMedia, sortOrder).at(-1);
|
||||
inMediaTimeRange =
|
||||
messageLatest == null ||
|
||||
(getMediaItemSize(messageLatest) >= getMediaItemSize(oldest) &&
|
||||
message.received_at >= oldest.message.receivedAt &&
|
||||
message.sent_at >= oldest.message.sentAt);
|
||||
} else {
|
||||
throw missingCaseError(sortOrder);
|
||||
}
|
||||
|
||||
// If message is updated out of current range - it means that the oldest
|
||||
// message in the view might no longer be the oldest in the database.
|
||||
if (!inMediaTimeRange) {
|
||||
return [media, false];
|
||||
}
|
||||
|
||||
// If the message is in the view and attachments might have changed - update
|
||||
if (difference > 0 || newMedia.length > 0) {
|
||||
return [_sortItems(mediaWithout.concat(newMedia), sortOrder), haveOldest];
|
||||
}
|
||||
|
||||
return [media, haveOldest];
|
||||
}
|
||||
|
||||
function _sortItems<
|
||||
Item extends ReadonlyDeep<{ message: MediaItemMessageType }>,
|
||||
>(items: ReadonlyArray<Item>): ReadonlyArray<Item> {
|
||||
return orderBy(items, [
|
||||
'message.receivedAt',
|
||||
'message.sentAt',
|
||||
'message.index',
|
||||
]);
|
||||
Item extends ReadonlyDeep<{
|
||||
attachment?: AttachmentForUIType;
|
||||
message: MediaItemMessageType;
|
||||
}>,
|
||||
>(
|
||||
items: ReadonlyArray<Item>,
|
||||
sortOrder: MediaSortOrderType
|
||||
): ReadonlyArray<Item> {
|
||||
if (sortOrder === 'date') {
|
||||
return orderBy(items, [
|
||||
'message.receivedAt',
|
||||
'message.sentAt',
|
||||
'message.index',
|
||||
]);
|
||||
}
|
||||
if (sortOrder === 'size') {
|
||||
return orderBy(items, [
|
||||
'attachment.size',
|
||||
'message.receivedAt',
|
||||
'message.sentAt',
|
||||
'message.index',
|
||||
]);
|
||||
}
|
||||
throw missingCaseError(sortOrder);
|
||||
}
|
||||
|
||||
function _cleanMessage(
|
||||
@@ -163,6 +265,28 @@ function _cleanAttachments(
|
||||
return rawMedia.map(media => _cleanAttachment(type, media));
|
||||
}
|
||||
|
||||
function _cleanContact(raw: ContactMediaItemDBType): ContactMediaItemType {
|
||||
const { message, contact } = raw;
|
||||
return {
|
||||
type: 'contact',
|
||||
contact: {
|
||||
...contact,
|
||||
avatar:
|
||||
contact.avatar?.avatar == null
|
||||
? undefined
|
||||
: {
|
||||
...contact.avatar,
|
||||
avatar: getPropsForAttachment(
|
||||
contact.avatar.avatar,
|
||||
'contact',
|
||||
message
|
||||
),
|
||||
},
|
||||
},
|
||||
message,
|
||||
};
|
||||
}
|
||||
|
||||
function _cleanDocuments(
|
||||
rawDocuments: ReadonlyArray<MediaItemDBType | ContactMediaItemDBType>
|
||||
): ReadonlyArray<MediaItemType | ContactMediaItemType> {
|
||||
@@ -175,26 +299,7 @@ function _cleanDocuments(
|
||||
rawDocument.type === 'contact',
|
||||
`Unexpected documen type ${rawDocument.type}`
|
||||
);
|
||||
|
||||
const { message, contact } = rawDocument;
|
||||
return {
|
||||
type: 'contact',
|
||||
contact: {
|
||||
...contact,
|
||||
avatar:
|
||||
contact.avatar?.avatar == null
|
||||
? undefined
|
||||
: {
|
||||
...contact.avatar,
|
||||
avatar: getPropsForAttachment(
|
||||
contact.avatar.avatar,
|
||||
'contact',
|
||||
message
|
||||
),
|
||||
},
|
||||
},
|
||||
message,
|
||||
};
|
||||
return _cleanContact(rawDocument);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -219,6 +324,17 @@ function _cleanLinkPreviews(
|
||||
});
|
||||
}
|
||||
|
||||
function sortOrderToOrder(sortOrder: MediaSortOrderType): 'older' | 'bigger' {
|
||||
switch (sortOrder) {
|
||||
case 'date':
|
||||
return 'older';
|
||||
case 'size':
|
||||
return 'bigger';
|
||||
default:
|
||||
throw missingCaseError(sortOrder);
|
||||
}
|
||||
}
|
||||
|
||||
function initialLoad(
|
||||
conversationId: string
|
||||
): ThunkAction<
|
||||
@@ -227,35 +343,42 @@ function initialLoad(
|
||||
unknown,
|
||||
InitialLoadActionType | SetLoadingActionType
|
||||
> {
|
||||
return async dispatch => {
|
||||
return async (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: SET_LOADING,
|
||||
payload: { loading: true },
|
||||
});
|
||||
|
||||
const {
|
||||
mediaGallery: { sortOrder },
|
||||
} = getState();
|
||||
const order = sortOrderToOrder(sortOrder);
|
||||
|
||||
const [rawMedia, rawAudio, rawDocuments, rawLinkPreviews] =
|
||||
await Promise.all([
|
||||
DataReader.getSortedMedia({
|
||||
conversationId,
|
||||
limit: FETCH_CHUNK_COUNT,
|
||||
type: 'media',
|
||||
order: 'older',
|
||||
order,
|
||||
}),
|
||||
DataReader.getSortedMedia({
|
||||
conversationId,
|
||||
limit: FETCH_CHUNK_COUNT,
|
||||
type: 'audio',
|
||||
order: 'older',
|
||||
order,
|
||||
}),
|
||||
// Note: `getOlderDocuments` mixes in contacts
|
||||
DataReader.getOlderDocuments({
|
||||
// Note: `getSortedDocuments` mixes in contacts
|
||||
DataReader.getSortedDocuments({
|
||||
conversationId,
|
||||
limit: FETCH_CHUNK_COUNT,
|
||||
order,
|
||||
}),
|
||||
DataReader.getOlderNonAttachmentMedia({
|
||||
DataReader.getSortedNonAttachmentMedia({
|
||||
conversationId,
|
||||
limit: FETCH_CHUNK_COUNT,
|
||||
type: 'links',
|
||||
order,
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -288,7 +411,7 @@ function loadMore(
|
||||
> {
|
||||
return async (dispatch, getState) => {
|
||||
const { mediaGallery } = getState();
|
||||
const { conversationId: previousConversationId } = mediaGallery;
|
||||
const { conversationId: previousConversationId, sortOrder } = mediaGallery;
|
||||
|
||||
if (conversationId !== previousConversationId) {
|
||||
log.warn('loadMore: conversationId mismatch; calling initialLoad()');
|
||||
@@ -331,6 +454,8 @@ function loadMore(
|
||||
messageId,
|
||||
receivedAt,
|
||||
sentAt,
|
||||
size: getMediaItemSize(oldestLoadedItem),
|
||||
order: sortOrderToOrder(sortOrder),
|
||||
};
|
||||
|
||||
let media: ReadonlyArray<MediaItemType> = [];
|
||||
@@ -338,9 +463,10 @@ function loadMore(
|
||||
let documents: ReadonlyArray<MediaItemType | ContactMediaItemType> = [];
|
||||
let links: ReadonlyArray<LinkPreviewMediaItemType> = [];
|
||||
if (type === 'media' || type === 'audio') {
|
||||
strictAssert(oldestLoadedItem.type === type, 'must be a media item');
|
||||
|
||||
const rawMedia = await DataReader.getSortedMedia({
|
||||
...sharedOptions,
|
||||
order: 'older',
|
||||
type,
|
||||
});
|
||||
|
||||
@@ -353,12 +479,12 @@ function loadMore(
|
||||
throw missingCaseError(type);
|
||||
}
|
||||
} else if (type === 'documents') {
|
||||
// Note: `getOlderDocuments` mixes in contacts
|
||||
const rawDocuments = await DataReader.getOlderDocuments(sharedOptions);
|
||||
// Note: `getSortedDocuments` mixes in contacts
|
||||
const rawDocuments = await DataReader.getSortedDocuments(sharedOptions);
|
||||
|
||||
documents = _cleanDocuments(rawDocuments);
|
||||
} else if (type === 'links') {
|
||||
const rawPreviews = await DataReader.getOlderNonAttachmentMedia({
|
||||
const rawPreviews = await DataReader.getSortedNonAttachmentMedia({
|
||||
...sharedOptions,
|
||||
type,
|
||||
});
|
||||
@@ -389,10 +515,22 @@ function setTab(tab: MediaGalleryStateType['tab']): SetTabActionType {
|
||||
};
|
||||
}
|
||||
|
||||
function setSortOrder(
|
||||
sortOrder: MediaGalleryStateType['sortOrder']
|
||||
): SetSortOrderActionType {
|
||||
return {
|
||||
type: SET_SORT_ORDER,
|
||||
payload: {
|
||||
sortOrder,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
initialLoad,
|
||||
loadMore,
|
||||
setTab,
|
||||
setSortOrder,
|
||||
};
|
||||
|
||||
export const useMediaGalleryActions = (): BoundActionCreatorsMapObject<
|
||||
@@ -402,6 +540,7 @@ export const useMediaGalleryActions = (): BoundActionCreatorsMapObject<
|
||||
export function getEmptyState(): MediaGalleryStateType {
|
||||
return {
|
||||
tab: 'media',
|
||||
sortOrder: 'date',
|
||||
conversationId: undefined,
|
||||
haveOldestDocument: false,
|
||||
haveOldestMedia: false,
|
||||
@@ -433,16 +572,17 @@ export function reducer(
|
||||
|
||||
return {
|
||||
tab: 'media',
|
||||
sortOrder: state.sortOrder,
|
||||
loading: false,
|
||||
conversationId: payload.conversationId,
|
||||
haveOldestMedia: payload.media.length === 0,
|
||||
haveOldestAudio: payload.audio.length === 0,
|
||||
haveOldestLink: payload.links.length === 0,
|
||||
haveOldestDocument: payload.documents.length === 0,
|
||||
media: _sortItems(payload.media),
|
||||
audio: _sortItems(payload.audio),
|
||||
links: _sortItems(payload.links),
|
||||
documents: _sortItems(payload.documents),
|
||||
media: _sortItems(payload.media, state.sortOrder),
|
||||
audio: _sortItems(payload.audio, state.sortOrder),
|
||||
links: _sortItems(payload.links, 'date'),
|
||||
documents: _sortItems(payload.documents, state.sortOrder),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -459,10 +599,10 @@ export function reducer(
|
||||
haveOldestAudio: audio.length === 0,
|
||||
haveOldestDocument: documents.length === 0,
|
||||
haveOldestLink: links.length === 0,
|
||||
media: _sortItems(media.concat(state.media)),
|
||||
audio: _sortItems(audio.concat(state.audio)),
|
||||
links: _sortItems(links.concat(state.links)),
|
||||
documents: _sortItems(documents.concat(state.documents)),
|
||||
media: _sortItems(media.concat(state.media), state.sortOrder),
|
||||
audio: _sortItems(audio.concat(state.audio), state.sortOrder),
|
||||
links: _sortItems(links.concat(state.links), 'date'),
|
||||
documents: _sortItems(documents.concat(state.documents), state.sortOrder),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -475,6 +615,17 @@ export function reducer(
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === SET_SORT_ORDER) {
|
||||
const { sortOrder } = action.payload;
|
||||
|
||||
return {
|
||||
...getEmptyState(),
|
||||
loading: true,
|
||||
tab: state.tab,
|
||||
sortOrder,
|
||||
};
|
||||
}
|
||||
|
||||
// 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,
|
||||
// and if so insert it into the loaded media.
|
||||
@@ -486,46 +637,6 @@ export function reducer(
|
||||
return state;
|
||||
}
|
||||
|
||||
const mediaWithout = state.media.filter(
|
||||
item => item.message.id !== message.id
|
||||
);
|
||||
const audioWithout = state.audio.filter(
|
||||
item => item.message.id !== message.id
|
||||
);
|
||||
const documentsWithout = state.documents.filter(
|
||||
item => item.message.id !== message.id
|
||||
);
|
||||
const linksWithout = state.links.filter(
|
||||
item => item.message.id !== message.id
|
||||
);
|
||||
const mediaDifference = state.media.length - mediaWithout.length;
|
||||
const audioDifference = state.audio.length - audioWithout.length;
|
||||
const documentDifference = state.documents.length - documentsWithout.length;
|
||||
const linkDifference = state.links.length - linksWithout.length;
|
||||
|
||||
if (message.deletedForEveryone || message.isErased) {
|
||||
if (
|
||||
mediaDifference > 0 ||
|
||||
audioDifference > 0 ||
|
||||
documentDifference > 0 ||
|
||||
linkDifference > 0
|
||||
) {
|
||||
return {
|
||||
...state,
|
||||
media: mediaWithout,
|
||||
audio: audioWithout,
|
||||
documents: documentsWithout,
|
||||
links: linksWithout,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
const oldestLoadedMedia = state.media[0];
|
||||
const oldestLoadedAudio = state.audio[0];
|
||||
const oldestLoadedDocument = state.documents[0];
|
||||
const oldestLoadedLink = state.links[0];
|
||||
|
||||
const messageMediaItems: Array<MediaItemDBType> = (
|
||||
message.attachments ?? []
|
||||
).map((attachment, index) => {
|
||||
@@ -547,10 +658,6 @@ export function reducer(
|
||||
({ attachment }) => isVoiceMessage(attachment) || isAudio([attachment])
|
||||
)
|
||||
);
|
||||
const newDocuments = _cleanAttachments(
|
||||
'documents',
|
||||
messageMediaItems.filter(({ attachment }) => isFile(attachment))
|
||||
);
|
||||
const newLinks = _cleanLinkPreviews(
|
||||
message.preview != null && message.preview.length > 0
|
||||
? [
|
||||
@@ -562,75 +669,51 @@ export function reducer(
|
||||
]
|
||||
: []
|
||||
);
|
||||
const newContacts = _cleanDocuments(
|
||||
message.contact != null && message.contact.length > 0
|
||||
? [
|
||||
{
|
||||
type: 'contact',
|
||||
contact: message.contact[0],
|
||||
message: _cleanMessage(message),
|
||||
},
|
||||
]
|
||||
: []
|
||||
);
|
||||
|
||||
let {
|
||||
media,
|
||||
audio,
|
||||
links,
|
||||
documents,
|
||||
haveOldestMedia,
|
||||
haveOldestAudio,
|
||||
haveOldestLink,
|
||||
haveOldestDocument,
|
||||
} = state;
|
||||
|
||||
const inMediaTimeRange =
|
||||
!oldestLoadedMedia ||
|
||||
(message.received_at >= oldestLoadedMedia.message.receivedAt &&
|
||||
message.sent_at >= oldestLoadedMedia.message.sentAt);
|
||||
if ((mediaDifference > 0 || newMedia.length > 0) && inMediaTimeRange) {
|
||||
media = _sortItems(mediaWithout.concat(newMedia));
|
||||
} else if (!inMediaTimeRange) {
|
||||
haveOldestMedia = false;
|
||||
}
|
||||
|
||||
const inAudioTimeRange =
|
||||
!oldestLoadedAudio ||
|
||||
(message.received_at >= oldestLoadedAudio.message.receivedAt &&
|
||||
message.sent_at >= oldestLoadedAudio.message.sentAt);
|
||||
if ((audioDifference > 0 || newAudio.length > 0) && inAudioTimeRange) {
|
||||
audio = _sortItems(audioWithout.concat(newAudio));
|
||||
} else if (!inAudioTimeRange) {
|
||||
haveOldestAudio = false;
|
||||
}
|
||||
|
||||
const inDocumentTimeRange =
|
||||
!oldestLoadedDocument ||
|
||||
(message.received_at >= oldestLoadedDocument.message.receivedAt &&
|
||||
message.sent_at >= oldestLoadedDocument.message.sentAt);
|
||||
if (
|
||||
(documentDifference > 0 ||
|
||||
newDocuments.length > 0 ||
|
||||
newContacts.length > 0) &&
|
||||
inDocumentTimeRange
|
||||
) {
|
||||
documents = _sortItems(
|
||||
documentsWithout.concat(newDocuments, newContacts)
|
||||
let newDocuments: ReadonlyArray<MediaItemType | ContactMediaItemType> =
|
||||
_cleanAttachments(
|
||||
'documents',
|
||||
messageMediaItems.filter(({ attachment }) => isFile(attachment))
|
||||
);
|
||||
if (message.contact != null && message.contact.length > 0) {
|
||||
newDocuments = newDocuments.concat(
|
||||
_cleanContact({
|
||||
type: 'contact',
|
||||
contact: message.contact[0],
|
||||
message: _cleanMessage(message),
|
||||
})
|
||||
);
|
||||
} else if (!inDocumentTimeRange) {
|
||||
haveOldestDocument = false;
|
||||
}
|
||||
|
||||
const inLinkTimeRange =
|
||||
!oldestLoadedLink ||
|
||||
(message.received_at >= oldestLoadedLink.message.receivedAt &&
|
||||
message.sent_at >= oldestLoadedLink.message.sentAt);
|
||||
if ((linkDifference > 0 || newLinks.length > 0) && inLinkTimeRange) {
|
||||
links = _sortItems(linksWithout.concat(newLinks));
|
||||
} else if (!inLinkTimeRange) {
|
||||
haveOldestLink = false;
|
||||
}
|
||||
const { sortOrder } = state;
|
||||
|
||||
const [media, haveOldestMedia] = _updateMedia({
|
||||
message,
|
||||
haveOldest: state.haveOldestMedia,
|
||||
media: state.media,
|
||||
newMedia,
|
||||
sortOrder,
|
||||
});
|
||||
const [audio, haveOldestAudio] = _updateMedia({
|
||||
message,
|
||||
haveOldest: state.haveOldestAudio,
|
||||
media: state.audio,
|
||||
newMedia: newAudio,
|
||||
sortOrder,
|
||||
});
|
||||
const [documents, haveOldestDocument] = _updateMedia({
|
||||
message,
|
||||
haveOldest: state.haveOldestDocument,
|
||||
media: state.documents,
|
||||
newMedia: newDocuments,
|
||||
sortOrder,
|
||||
});
|
||||
const [links, haveOldestLink] = _updateMedia({
|
||||
message,
|
||||
haveOldest: state.haveOldestLink,
|
||||
media: state.links,
|
||||
newMedia: newLinks,
|
||||
sortOrder: 'date',
|
||||
});
|
||||
|
||||
if (
|
||||
state.haveOldestMedia !== haveOldestMedia ||
|
||||
|
||||
@@ -42,6 +42,7 @@ export const SmartAllMedia = memo(function SmartAllMedia({
|
||||
haveOldestDocument,
|
||||
loading,
|
||||
tab,
|
||||
sortOrder,
|
||||
} = useSelector(getMediaGalleryState);
|
||||
const { initialLoad, loadMore } = useMediaGalleryActions();
|
||||
const {
|
||||
@@ -107,6 +108,7 @@ export const SmartAllMedia = memo(function SmartAllMedia({
|
||||
links={links}
|
||||
documents={documents}
|
||||
tab={tab}
|
||||
sortOrder={sortOrder}
|
||||
showLightbox={showLightbox}
|
||||
playAudio={playAudio}
|
||||
kickOffAttachmentDownload={kickOffAttachmentDownload}
|
||||
|
||||
@@ -8,9 +8,17 @@ 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 { tab, sortOrder } = useSelector(getMediaGalleryState);
|
||||
const { setTab, setSortOrder } = useMediaGalleryActions();
|
||||
const i18n = useSelector(getIntl);
|
||||
|
||||
return <PanelHeader i18n={i18n} tab={tab} setTab={setTab} />;
|
||||
return (
|
||||
<PanelHeader
|
||||
i18n={i18n}
|
||||
tab={tab}
|
||||
setTab={setTab}
|
||||
sortOrder={sortOrder}
|
||||
setSortOrder={setSortOrder}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
getUserConversationId,
|
||||
} from '../selectors/user.std.js';
|
||||
import { getConversationSelector } from '../selectors/conversations.dom.js';
|
||||
import { getMediaGalleryState } from '../selectors/mediaGallery.std.js';
|
||||
import { useConversationsActions } from '../ducks/conversations.preload.js';
|
||||
|
||||
export type PropsType = Readonly<{
|
||||
@@ -34,6 +35,7 @@ export const MediaItem = memo(function MediaItem({
|
||||
const theme = useSelector(getTheme);
|
||||
const ourConversationId = useSelector(getUserConversationId);
|
||||
const getConversation = useSelector(getConversationSelector);
|
||||
const { sortOrder } = useSelector(getMediaGalleryState);
|
||||
|
||||
const { showConversation } = useConversationsActions();
|
||||
|
||||
@@ -58,6 +60,8 @@ export const MediaItem = memo(function MediaItem({
|
||||
});
|
||||
}, [message.conversationId, message.id, showConversation]);
|
||||
|
||||
const showSize = sortOrder === 'size';
|
||||
|
||||
switch (mediaItem.type) {
|
||||
case 'audio':
|
||||
return (
|
||||
@@ -74,6 +78,7 @@ export const MediaItem = memo(function MediaItem({
|
||||
return (
|
||||
<MediaGridItem
|
||||
mediaItem={mediaItem}
|
||||
showSize={showSize}
|
||||
onClick={onClick}
|
||||
i18n={i18n}
|
||||
theme={theme}
|
||||
|
||||
@@ -11,6 +11,8 @@ import type { EmbeddedContactForUIType } from './EmbeddedContact.std.js';
|
||||
|
||||
export type MediaTabType = 'media' | 'audio' | 'links' | 'documents';
|
||||
|
||||
export type MediaSortOrderType = 'date' | 'size';
|
||||
|
||||
export type MediaItemMessageType = Readonly<{
|
||||
id: string;
|
||||
type: MessageAttributesType['type'];
|
||||
|
||||
Reference in New Issue
Block a user