diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index 73a07e6708..b78e4b20e0 100644
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -1252,7 +1252,11 @@
},
"icu:viewRecentMedia": {
"messageformat": "View recent media",
- "description": "This is a menu item for viewing all media (images + video) in a conversation, using the imperative case, as in a command."
+ "description": "(Deleted 2024/09/03) This is a menu item for viewing all media (images + video) in a conversation, using the imperative case, as in a command."
+ },
+ "icu:allMediaMenuItem": {
+ "messageformat": "All media",
+ "description": "This is a menu item for viewing all media (images + video) in a conversation"
},
"icu:back": {
"messageformat": "Back",
diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss
index 926fdc0f0c..2a195ed61a 100644
--- a/stylesheets/_modules.scss
+++ b/stylesheets/_modules.scss
@@ -2374,6 +2374,7 @@ button.ConversationDetails__action-button {
// Module: Media Gallery
.module-media-gallery {
+ position: relative;
display: flex;
flex-direction: column;
flex-grow: 1;
@@ -2389,6 +2390,19 @@ button.ConversationDetails__action-button {
padding: 20px;
}
+.module-media-gallery__scroll-observer {
+ position: absolute;
+ bottom: 0;
+ height: 30px;
+ width: 100%;
+
+ &::after {
+ content: '';
+ height: 1px; // Always show the element to not mess with the height of the scroll area
+ display: block;
+ }
+}
+
.module-media-gallery__sections {
display: flex;
flex-grow: 1;
diff --git a/ts/components/Lightbox.stories.tsx b/ts/components/Lightbox.stories.tsx
index c37ff602c0..8cc03e2bf4 100644
--- a/ts/components/Lightbox.stories.tsx
+++ b/ts/components/Lightbox.stories.tsx
@@ -46,9 +46,9 @@ function createMediaItem(
attachments: [],
conversationId: '1234',
id: 'image-msg',
- received_at: 0,
- received_at_ms: Date.now(),
- sent_at: Date.now(),
+ receivedAt: 0,
+ receivedAtMs: Date.now(),
+ sentAt: Date.now(),
},
objectURL: '',
...overrideProps,
@@ -96,9 +96,9 @@ export function Multimedia(): JSX.Element {
attachments: [],
conversationId: '1234',
id: 'image-msg',
- received_at: 1,
- received_at_ms: Date.now(),
- sent_at: Date.now(),
+ receivedAt: 1,
+ receivedAtMs: Date.now(),
+ sentAt: Date.now(),
},
objectURL: '/fixtures/tina-rolf-269345-unsplash.jpg',
},
@@ -114,9 +114,9 @@ export function Multimedia(): JSX.Element {
attachments: [],
conversationId: '1234',
id: 'video-msg',
- received_at: 2,
- received_at_ms: Date.now(),
- sent_at: Date.now(),
+ receivedAt: 2,
+ receivedAtMs: Date.now(),
+ sentAt: Date.now(),
},
objectURL: '/fixtures/pixabay-Soap-Bubble-7141.mp4',
},
@@ -153,9 +153,9 @@ export function MissingMedia(): JSX.Element {
attachments: [],
conversationId: '1234',
id: 'image-msg',
- received_at: 3,
- received_at_ms: Date.now(),
- sent_at: Date.now(),
+ receivedAt: 3,
+ receivedAtMs: Date.now(),
+ sentAt: Date.now(),
},
objectURL: undefined,
},
diff --git a/ts/components/Lightbox.tsx b/ts/components/Lightbox.tsx
index 71ff71dc8d..278aed962b 100644
--- a/ts/components/Lightbox.tsx
+++ b/ts/components/Lightbox.tsx
@@ -181,7 +181,7 @@ export function Lightbox({
const mediaItem = media[selectedIndex];
const { attachment, message, index } = mediaItem;
- saveAttachment(attachment, message.sent_at, index + 1);
+ saveAttachment(attachment, message.sentAt, index + 1);
},
[isViewOnce, media, saveAttachment, selectedIndex]
);
@@ -828,7 +828,7 @@ function LightboxHeader({
{conversation.title}
- {formatDateTimeForAttachment(i18n, message.sent_at ?? now)}
+ {formatDateTimeForAttachment(i18n, message.sentAt ?? now)}
diff --git a/ts/components/conversation/ConversationHeader.stories.tsx b/ts/components/conversation/ConversationHeader.stories.tsx
index c238fab0b3..1aa29342c5 100644
--- a/ts/components/conversation/ConversationHeader.stories.tsx
+++ b/ts/components/conversation/ConversationHeader.stories.tsx
@@ -74,8 +74,8 @@ const commonProps: PropsType = {
onSearchInConversation: action('onSearchInConversation'),
onSelectModeEnter: action('onSelectModeEnter'),
onShowMembers: action('onShowMembers'),
+ onViewAllMedia: action('onViewAllMedia'),
onViewConversationDetails: action('onViewConversationDetails'),
- onViewRecentMedia: action('onViewRecentMedia'),
onViewUserStories: action('onViewUserStories'),
};
diff --git a/ts/components/conversation/ConversationHeader.tsx b/ts/components/conversation/ConversationHeader.tsx
index 7ca06fd975..24a86d6456 100644
--- a/ts/components/conversation/ConversationHeader.tsx
+++ b/ts/components/conversation/ConversationHeader.tsx
@@ -130,8 +130,8 @@ export type PropsActionsType = {
onSearchInConversation: () => void;
onSelectModeEnter: () => void;
onShowMembers: () => void;
+ onViewAllMedia: () => void;
onViewConversationDetails: () => void;
- onViewRecentMedia: () => void;
onViewUserStories: () => void;
};
@@ -180,8 +180,8 @@ export const ConversationHeader = memo(function ConversationHeader({
onSearchInConversation,
onSelectModeEnter,
onShowMembers,
+ onViewAllMedia,
onViewConversationDetails,
- onViewRecentMedia,
onViewUserStories,
outgoingCallButtonStyle,
setLocalDeleteWarningShown,
@@ -380,7 +380,7 @@ export const ConversationHeader = memo(function ConversationHeader({
setHasCustomDisappearingTimeoutModal(true);
}}
onShowMembers={onShowMembers}
- onViewRecentMedia={onViewRecentMedia}
+ onViewAllMedia={onViewAllMedia}
onViewConversationDetails={onViewConversationDetails}
triggerId={triggerId}
/>
@@ -544,7 +544,7 @@ function HeaderMenu({
onSelectModeEnter,
onSetupCustomDisappearingTimeout,
onShowMembers,
- onViewRecentMedia,
+ onViewAllMedia,
onViewConversationDetails,
triggerId,
}: {
@@ -570,7 +570,7 @@ function HeaderMenu({
onSelectModeEnter: () => void;
onSetupCustomDisappearingTimeout: () => void;
onShowMembers: () => void;
- onViewRecentMedia: () => void;
+ onViewAllMedia: () => void;
onViewConversationDetails: () => void;
triggerId: string;
}) {
@@ -639,8 +639,8 @@ function HeaderMenu({
return (
-
) : null}
-
- {i18n('icu:viewRecentMedia')}
+
+ {i18n('icu:allMediaMenuItem')}
diff --git a/ts/components/conversation/conversation-details/ConversationDetails.stories.tsx b/ts/components/conversation/conversation-details/ConversationDetails.stories.tsx
index 88d6510a8e..b191ec3a97 100644
--- a/ts/components/conversation/conversation-details/ConversationDetails.stories.tsx
+++ b/ts/components/conversation/conversation-details/ConversationDetails.stories.tsx
@@ -87,7 +87,7 @@ const createProps = (
showContactModal: action('showContactModal'),
pushPanelForConversation: action('pushPanelForConversation'),
showConversation: action('showConversation'),
- showLightboxWithMedia: action('showLightboxWithMedia'),
+ showLightbox: action('showLightbox'),
updateGroupAttributes: async () => {
action('updateGroupAttributes')();
},
diff --git a/ts/components/conversation/conversation-details/ConversationDetails.tsx b/ts/components/conversation/conversation-details/ConversationDetails.tsx
index 080790662f..a45e63943c 100644
--- a/ts/components/conversation/conversation-details/ConversationDetails.tsx
+++ b/ts/components/conversation/conversation-details/ConversationDetails.tsx
@@ -144,7 +144,7 @@ type ActionProps = {
onFailure?: () => unknown;
}
) => unknown;
-} & Pick;
+} & Pick;
export type Props = StateProps & ActionProps;
@@ -203,7 +203,7 @@ export function ConversationDetails({
setMuteExpiration,
showContactModal,
showConversation,
- showLightboxWithMedia,
+ showLightbox,
theme,
toggleAboutContactModal,
toggleSafetyNumberModal,
@@ -702,7 +702,7 @@ export function ConversationDetails({
type: PanelType.AllMedia,
})
}
- showLightboxWithMedia={showLightboxWithMedia}
+ showLightbox={showLightbox}
/>
{!isGroup && !conversation.isMe && (
diff --git a/ts/components/conversation/conversation-details/ConversationDetailsMediaList.stories.tsx b/ts/components/conversation/conversation-details/ConversationDetailsMediaList.stories.tsx
index 0ba75c5100..1364f4c251 100644
--- a/ts/components/conversation/conversation-details/ConversationDetailsMediaList.stories.tsx
+++ b/ts/components/conversation/conversation-details/ConversationDetailsMediaList.stories.tsx
@@ -28,7 +28,7 @@ const createProps = (mediaItems?: Array): Props => ({
i18n,
loadRecentMediaItems: action('loadRecentMediaItems'),
showAllMedia: action('showAllMedia'),
- showLightboxWithMedia: action('showLightboxWithMedia'),
+ showLightbox: action('showLightbox'),
});
export function Basic(): JSX.Element {
diff --git a/ts/components/conversation/conversation-details/ConversationDetailsMediaList.tsx b/ts/components/conversation/conversation-details/ConversationDetailsMediaList.tsx
index 11e3868cde..2ef3d51f3d 100644
--- a/ts/components/conversation/conversation-details/ConversationDetailsMediaList.tsx
+++ b/ts/components/conversation/conversation-details/ConversationDetailsMediaList.tsx
@@ -3,11 +3,9 @@
import React from 'react';
-import type { ReadonlyDeep } from 'type-fest';
import type { LocalizerType } from '../../../types/Util';
-
-import type { MediaItemType } from '../../../types/MediaItem';
import type { ConversationType } from '../../../state/ducks/conversations';
+import type { AttachmentType } from '../../../types/Attachment';
import { PanelSection } from './PanelSection';
import { bemGenerator } from './util';
@@ -18,10 +16,10 @@ export type Props = {
i18n: LocalizerType;
loadRecentMediaItems: (id: string, limit: number) => void;
showAllMedia: () => void;
- showLightboxWithMedia: (
- selectedIndex: number,
- media: ReadonlyArray>
- ) => void;
+ showLightbox: (options: {
+ attachment: AttachmentType;
+ messageId: string;
+ }) => void;
};
const MEDIA_ITEM_LIMIT = 6;
@@ -33,7 +31,7 @@ export function ConversationDetailsMediaList({
i18n,
loadRecentMediaItems,
showAllMedia,
- showLightboxWithMedia,
+ showLightbox,
}: Props): JSX.Element | null {
const mediaItems = conversation.recentMediaItems || [];
@@ -66,7 +64,12 @@ export function ConversationDetailsMediaList({
key={`${mediaItem.message.id}-${mediaItem.index}`}
mediaItem={mediaItem}
i18n={i18n}
- onClick={() => showLightboxWithMedia(mediaItem.index, mediaItems)}
+ onClick={() =>
+ showLightbox({
+ attachment: mediaItem.attachment,
+ messageId: mediaItem.message.id,
+ })
+ }
/>
))}
diff --git a/ts/components/conversation/media-gallery/AttachmentSection.tsx b/ts/components/conversation/media-gallery/AttachmentSection.tsx
index 22c3de1c42..ce67ca3d5f 100644
--- a/ts/components/conversation/media-gallery/AttachmentSection.tsx
+++ b/ts/components/conversation/media-gallery/AttachmentSection.tsx
@@ -8,7 +8,6 @@ import type { LocalizerType } from '../../../types/Util';
import type { MediaItemType } from '../../../types/MediaItem';
import { DocumentListItem } from './DocumentListItem';
import { MediaGridItem } from './MediaGridItem';
-import { getMessageTimestamp } from '../../../util/getMessageTimestamp';
import { missingCaseError } from '../../../util/missingCaseError';
export type Props = {
@@ -35,7 +34,7 @@ export function AttachmentSection({
const { message, index, attachment } = mediaItem;
const onClick = () => {
- onItemClick({ type, message, attachment, index: mediaItem.index });
+ onItemClick({ type, message, attachment });
};
switch (type) {
@@ -56,7 +55,7 @@ export function AttachmentSection({
fileSize={attachment.size}
shouldShowSeparator={shouldShowSeparator}
onClick={onClick}
- timestamp={getMessageTimestamp(message)}
+ timestamp={message.receivedAtMs || message.receivedAt}
/>
);
default:
diff --git a/ts/components/conversation/media-gallery/MediaGallery.stories.tsx b/ts/components/conversation/media-gallery/MediaGallery.stories.tsx
index 2c5c9182ee..40cd2ca135 100644
--- a/ts/components/conversation/media-gallery/MediaGallery.stories.tsx
+++ b/ts/components/conversation/media-gallery/MediaGallery.stories.tsx
@@ -22,13 +22,20 @@ export default {
} satisfies Meta;
const createProps = (overrideProps: Partial = {}): Props => ({
+ i18n,
+
conversationId: '123',
documents: overrideProps.documents || [],
- i18n,
- loadMediaItems: action('loadMediaItems'),
+ haveOldestDocument: overrideProps.haveOldestDocument || false,
+ haveOldestMedia: overrideProps.haveOldestMedia || false,
+ loading: overrideProps.loading || false,
media: overrideProps.media || [],
+
+ initialLoad: action('initialLoad'),
+ loadMoreDocuments: action('loadMoreDocuments'),
+ loadMoreMedia: action('loadMoreMedia'),
saveAttachment: action('saveAttachment'),
- showLightboxWithMedia: action('showLightboxWithMedia'),
+ showLightbox: action('showLightbox'),
});
export function Populated(): JSX.Element {
diff --git a/ts/components/conversation/media-gallery/MediaGallery.tsx b/ts/components/conversation/media-gallery/MediaGallery.tsx
index 6b5565bf59..95892b68b0 100644
--- a/ts/components/conversation/media-gallery/MediaGallery.tsx
+++ b/ts/components/conversation/media-gallery/MediaGallery.tsx
@@ -12,9 +12,10 @@ import type { SaveAttachmentActionCreatorType } from '../../../state/ducks/conve
import { AttachmentSection } from './AttachmentSection';
import { EmptyState } from './EmptyState';
import { Tabs } from '../../Tabs';
-import { getMessageTimestamp } from '../../../util/getMessageTimestamp';
import { groupMediaItemsByDate } from './groupMediaItemsByDate';
import { missingCaseError } from '../../../util/missingCaseError';
+import { usePrevious } from '../../../hooks/usePrevious';
+import type { AttachmentType } from '../../../types/Attachment';
enum TabViews {
Media = 'Media',
@@ -23,33 +24,43 @@ enum TabViews {
export type Props = {
conversationId: string;
- documents: Array;
+ documents: ReadonlyArray;
i18n: LocalizerType;
- loadMediaItems: (id: string) => unknown;
- media: Array;
+ haveOldestMedia: boolean;
+ haveOldestDocument: boolean;
+ loading: boolean;
+ initialLoad: (id: string) => unknown;
+ loadMoreMedia: (id: string) => unknown;
+ loadMoreDocuments: (id: string) => unknown;
+ media: ReadonlyArray;
saveAttachment: SaveAttachmentActionCreatorType;
- showLightboxWithMedia: (
- selectedIndex: number,
- media: Array
- ) => void;
+ showLightbox: (options: {
+ attachment: AttachmentType;
+ messageId: string;
+ }) => void;
};
const MONTH_FORMAT = 'MMMM YYYY';
function MediaSection({
- type,
- i18n,
- media,
documents,
+ i18n,
+ loading,
+ media,
saveAttachment,
- showLightboxWithMedia,
+ showLightbox,
+ type,
}: Pick<
Props,
- 'i18n' | 'media' | 'documents' | 'showLightboxWithMedia' | 'saveAttachment'
+ 'documents' | 'i18n' | 'loading' | 'media' | 'saveAttachment' | 'showLightbox'
> & { type: 'media' | 'documents' }): JSX.Element {
const mediaItems = type === 'media' ? media : documents;
if (!mediaItems || mediaItems.length === 0) {
+ if (loading) {
+ return ;
+ }
+
const label = (() => {
switch (type) {
case 'media':
@@ -70,7 +81,7 @@ function MediaSection({
const sections = groupMediaItemsByDate(now, mediaItems).map(section => {
const first = section.mediaItems[0];
const { message } = first;
- const date = moment(getMessageTimestamp(message));
+ const date = moment(message.receivedAtMs || message.receivedAt);
function getHeader(): string {
switch (section.type) {
@@ -101,12 +112,15 @@ function MediaSection({
onItemClick={(event: ItemClickEvent) => {
switch (event.type) {
case 'documents': {
- saveAttachment(event.attachment, event.message.sent_at);
+ saveAttachment(event.attachment, event.message.sentAt);
break;
}
case 'media': {
- showLightboxWithMedia(event.index, media);
+ showLightbox({
+ attachment: event.attachment,
+ messageId: event.message.id,
+ });
break;
}
@@ -124,21 +138,87 @@ function MediaSection({
export function MediaGallery({
conversationId,
documents,
+ haveOldestDocument,
+ haveOldestMedia,
i18n,
- loadMediaItems,
+ initialLoad,
+ loading,
+ loadMoreDocuments,
+ loadMoreMedia,
media,
saveAttachment,
- showLightboxWithMedia,
+ showLightbox,
}: Props): JSX.Element {
const focusRef = useRef(null);
+ const scrollObserverRef = useRef(null);
+ const intersectionObserver = useRef(null);
+ const loadingRef = useRef(false);
+ const tabViewRef = useRef(TabViews.Media);
useEffect(() => {
focusRef.current?.focus();
}, []);
useEffect(() => {
- loadMediaItems(conversationId);
- }, [conversationId, loadMediaItems]);
+ if (media.length > 0 || documents.length > 0) {
+ return;
+ }
+ initialLoad(conversationId);
+ loadingRef.current = true;
+ }, [conversationId, initialLoad, media, documents]);
+
+ const previousLoading = usePrevious(loading, loading);
+ if (previousLoading && !loading) {
+ loadingRef.current = false;
+ }
+
+ useEffect(() => {
+ if (!scrollObserverRef.current) {
+ return;
+ }
+
+ intersectionObserver.current?.disconnect();
+ intersectionObserver.current = null;
+
+ intersectionObserver.current = new IntersectionObserver(
+ (entries: ReadonlyArray) => {
+ if (loadingRef.current) {
+ return;
+ }
+
+ const entry = entries.find(
+ item => item.target === scrollObserverRef.current
+ );
+
+ if (entry && entry.intersectionRatio > 0) {
+ if (tabViewRef.current === TabViews.Media) {
+ if (!haveOldestMedia) {
+ loadMoreMedia(conversationId);
+ loadingRef.current = true;
+ }
+ } else {
+ // eslint-disable-next-line no-lonely-if
+ if (!haveOldestDocument) {
+ loadMoreDocuments(conversationId);
+ loadingRef.current = true;
+ }
+ }
+ }
+ }
+ );
+ intersectionObserver.current.observe(scrollObserverRef.current);
+
+ return () => {
+ intersectionObserver.current?.disconnect();
+ intersectionObserver.current = null;
+ };
+ }, [
+ conversationId,
+ haveOldestDocument,
+ haveOldestMedia,
+ loadMoreDocuments,
+ loadMoreMedia,
+ ]);
return (
@@ -155,31 +235,44 @@ export function MediaGallery({
},
]}
>
- {({ selectedTab }) => (
-
- {selectedTab === TabViews.Media && (
-
- )}
- {selectedTab === TabViews.Documents && (
-
- )}
-
- )}
+ {({ selectedTab }) => {
+ tabViewRef.current =
+ selectedTab === TabViews.Media
+ ? TabViews.Media
+ : TabViews.Documents;
+
+ return (
+
+ {selectedTab === TabViews.Media && (
+
+ )}
+ {selectedTab === TabViews.Documents && (
+
+ )}
+
+ );
+ }}
+
);
}
diff --git a/ts/components/conversation/media-gallery/MediaGridItem.stories.tsx b/ts/components/conversation/media-gallery/MediaGridItem.stories.tsx
index 10672f0bfc..677208c33c 100644
--- a/ts/components/conversation/media-gallery/MediaGridItem.stories.tsx
+++ b/ts/components/conversation/media-gallery/MediaGridItem.stories.tsx
@@ -37,9 +37,9 @@ const createMediaItem = (
attachments: [],
conversationId: '1234',
id: 'id',
- received_at: Date.now(),
- received_at_ms: Date.now(),
- sent_at: Date.now(),
+ receivedAt: Date.now(),
+ receivedAtMs: Date.now(),
+ sentAt: Date.now(),
},
});
diff --git a/ts/components/conversation/media-gallery/groupMediaItemsByDate.ts b/ts/components/conversation/media-gallery/groupMediaItemsByDate.ts
index ceda3a98b9..ad4a811e40 100644
--- a/ts/components/conversation/media-gallery/groupMediaItemsByDate.ts
+++ b/ts/components/conversation/media-gallery/groupMediaItemsByDate.ts
@@ -6,7 +6,6 @@ import { compact, groupBy, sortBy } from 'lodash';
import * as log from '../../../logging/log';
import type { MediaItemType } from '../../../types/MediaItem';
-import { getMessageTimestamp } from '../../../util/getMessageTimestamp';
import { missingCaseError } from '../../../util/missingCaseError';
@@ -27,12 +26,12 @@ export const groupMediaItemsByDate = (
timestamp: number,
mediaItems: ReadonlyArray
): Array => {
- const referenceDateTime = moment.utc(timestamp);
+ const referenceDateTime = moment(timestamp);
const sortedMediaItem = sortBy(mediaItems, mediaItem => {
const { message } = mediaItem;
- return -message.received_at;
+ return -message.receivedAt;
});
const messagesWithSection = sortedMediaItem.map(
withSection(referenceDateTime)
@@ -105,40 +104,38 @@ type MediaItemWithSection =
| MediaItemWithStaticSection
| MediaItemWithYearMonthSection;
-const withSection =
- (referenceDateTime: moment.Moment) =>
- (mediaItem: MediaItemType): MediaItemWithSection => {
- const today = moment(referenceDateTime).startOf('day');
- const yesterday = moment(referenceDateTime)
- .subtract(1, 'day')
- .startOf('day');
- const thisWeek = moment(referenceDateTime).startOf('isoWeek');
- const thisMonth = moment(referenceDateTime).startOf('month');
+const withSection = (referenceDateTime: moment.Moment) => {
+ const today = moment(referenceDateTime).startOf('day');
+ const yesterday = moment(referenceDateTime).subtract(1, 'day').startOf('day');
+ const thisWeek = moment(referenceDateTime).subtract(7, 'day').startOf('day');
+ const thisMonth = moment(referenceDateTime).startOf('month');
+ return (mediaItem: MediaItemType): MediaItemWithSection => {
const { message } = mediaItem;
- const mediaItemReceivedDate = moment.utc(getMessageTimestamp(message));
- if (mediaItemReceivedDate.isAfter(today)) {
+ const messageTimestamp = moment(message.receivedAtMs || message.receivedAt);
+
+ if (messageTimestamp.isAfter(today)) {
return {
order: 0,
type: 'today',
mediaItem,
};
}
- if (mediaItemReceivedDate.isAfter(yesterday)) {
+ if (messageTimestamp.isAfter(yesterday)) {
return {
order: 1,
type: 'yesterday',
mediaItem,
};
}
- if (mediaItemReceivedDate.isAfter(thisWeek)) {
+ if (messageTimestamp.isAfter(thisWeek)) {
return {
order: 2,
type: 'thisWeek',
mediaItem,
};
}
- if (mediaItemReceivedDate.isAfter(thisMonth)) {
+ if (messageTimestamp.isAfter(thisMonth)) {
return {
order: 3,
type: 'thisMonth',
@@ -146,8 +143,8 @@ const withSection =
};
}
- const month: number = mediaItemReceivedDate.month();
- const year: number = mediaItemReceivedDate.year();
+ const month: number = messageTimestamp.month();
+ const year: number = messageTimestamp.year();
return {
order: year * 100 + month,
@@ -157,3 +154,4 @@ const withSection =
mediaItem,
};
};
+};
diff --git a/ts/components/conversation/media-gallery/types/ItemClickEvent.ts b/ts/components/conversation/media-gallery/types/ItemClickEvent.ts
index 1d938f06a4..d890f6702d 100644
--- a/ts/components/conversation/media-gallery/types/ItemClickEvent.ts
+++ b/ts/components/conversation/media-gallery/types/ItemClickEvent.ts
@@ -1,12 +1,10 @@
// Copyright 2018 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
-import type { ReadonlyMessageAttributesType } from '../../../../model-types.d';
import type { AttachmentType } from '../../../../types/Attachment';
export type ItemClickEvent = {
- message: Pick;
+ message: { id: string; sentAt: number };
attachment: AttachmentType;
- index: number;
type: 'media' | 'documents';
};
diff --git a/ts/components/conversation/media-gallery/utils/mocks.ts b/ts/components/conversation/media-gallery/utils/mocks.ts
index 4894de4ad8..744eba6545 100644
--- a/ts/components/conversation/media-gallery/utils/mocks.ts
+++ b/ts/components/conversation/media-gallery/utils/mocks.ts
@@ -32,10 +32,10 @@ function createRandomFile(
message: {
conversationId: '123',
id: random(Date.now()).toString(),
- received_at: Math.floor(Math.random() * 10),
- received_at_ms: random(startTime, startTime + timeWindow),
+ receivedAt: Math.floor(Math.random() * 10),
+ receivedAtMs: random(startTime, startTime + timeWindow),
attachments: [],
- sent_at: Date.now(),
+ sentAt: Date.now(),
},
attachment: {
url: '',
@@ -86,6 +86,6 @@ export function createPreparedMediaItems(
...fn(now - days(30), days(15)),
...fn(now - days(365), days(300)),
],
- (item: MediaItemType) => -item.message.received_at
+ (item: MediaItemType) => -item.message.receivedAt
);
}
diff --git a/ts/sql/Interface.ts b/ts/sql/Interface.ts
index 91702d9d75..41d941ccb1 100644
--- a/ts/sql/Interface.ts
+++ b/ts/sql/Interface.ts
@@ -55,6 +55,7 @@ export type AdjacentMessagesByConversationOptionsType = Readonly<{
sentAt?: number;
storyId: string | undefined;
requireVisualMediaAttachments?: boolean;
+ requireFileAttachments?: boolean;
}>;
export type GetNearbyMessageFromDeletedSetOptionsType = Readonly<{
@@ -637,14 +638,6 @@ type ReadableInterface = {
limit: number,
options: { maxVersion: number }
) => Array;
- getMessagesWithVisualMediaAttachments: (
- conversationId: string,
- options: { limit: number }
- ) => Array;
- getMessagesWithFileAttachments: (
- conversationId: string,
- options: { limit: number }
- ) => Array;
getMessageServerGuidsForSpam: (conversationId: string) => Array;
getJobsInQueue(queueType: string): Array;
diff --git a/ts/sql/Server.ts b/ts/sql/Server.ts
index 0b72e22fa0..fa6bbd7080 100644
--- a/ts/sql/Server.ts
+++ b/ts/sql/Server.ts
@@ -336,8 +336,6 @@ export const DataReader: ServerReadableInterface = {
_getAllStoryReads,
getLastStoryReadsForAuthor,
getMessagesNeedingUpgrade,
- getMessagesWithVisualMediaAttachments,
- getMessagesWithFileAttachments,
getMessageServerGuidsForSpam,
getJobsInQueue,
@@ -2872,6 +2870,7 @@ function getAdjacentMessagesByConversation(
receivedAt = direction === AdjacentDirection.Older ? Number.MAX_VALUE : 0,
sentAt = direction === AdjacentDirection.Older ? Number.MAX_VALUE : 0,
requireVisualMediaAttachments,
+ requireFileAttachments,
storyId,
}: AdjacentMessagesByConversationOptionsType
): Array {
@@ -2893,7 +2892,9 @@ function getAdjacentMessagesByConversation(
}
const requireDifferentMessage =
- direction === AdjacentDirection.Older || requireVisualMediaAttachments;
+ direction === AdjacentDirection.Older ||
+ requireVisualMediaAttachments ||
+ requireFileAttachments;
const createQuery = (timeFilter: QueryFragment): QueryFragment => sqlFragment`
SELECT json FROM messages WHERE
@@ -2908,6 +2909,11 @@ function getAdjacentMessagesByConversation(
? sqlFragment`hasVisualMediaAttachments IS 1 AND`
: sqlFragment``
}
+ ${
+ requireFileAttachments
+ ? sqlFragment`hasFileAttachments IS 1 AND`
+ : sqlFragment``
+ }
isStory IS 0 AND
(${_storyIdPredicate(storyId, includeStoryReplies)}) AND
(
@@ -2938,6 +2944,20 @@ function getAdjacentMessagesByConversation(
) > 0
LIMIT ${limit};
`;
+ } else if (requireFileAttachments) {
+ template = sqlFragment`
+ SELECT json
+ FROM (${template}) as messages
+ WHERE
+ (
+ SELECT COUNT(*)
+ FROM json_each(messages.json ->> 'attachments') AS attachment
+ WHERE
+ attachment.value ->> 'pending' IS NOT 1 AND
+ attachment.value ->> 'error' IS NULL
+ ) > 0
+ LIMIT ${limit};
+ `;
} else {
template = sqlFragment`${template} LIMIT ${limit}`;
}
@@ -6515,60 +6535,6 @@ export function incrementMessagesMigrationAttempts(
});
}
-function getMessagesWithVisualMediaAttachments(
- db: ReadableDB,
- conversationId: string,
- { limit }: { limit: number }
-): Array {
- const rows: JSONRows = db
- .prepare(
- `
- SELECT json FROM messages
- INDEXED BY messages_hasVisualMediaAttachments
- WHERE
- isStory IS 0 AND
- storyId IS NULL AND
- conversationId = $conversationId AND
- -- Note that this check has to use 'IS' to utilize
- -- 'messages_hasVisualMediaAttachments' INDEX
- hasVisualMediaAttachments IS 1
- ORDER BY received_at DESC, sent_at DESC
- LIMIT $limit;
- `
- )
- .all({
- conversationId,
- limit,
- });
-
- return rows.map(row => jsonToObject(row.json));
-}
-
-function getMessagesWithFileAttachments(
- db: ReadableDB,
- conversationId: string,
- { limit }: { limit: number }
-): Array {
- const rows = db
- .prepare(
- `
- SELECT json FROM messages WHERE
- isStory IS 0 AND
- storyId IS NULL AND
- conversationId = $conversationId AND
- hasFileAttachments = 1
- ORDER BY received_at DESC, sent_at DESC
- LIMIT $limit;
- `
- )
- .all({
- conversationId,
- limit,
- });
-
- return map(rows, row => jsonToObject(row.json));
-}
-
function getMessageServerGuidsForSpam(
db: ReadableDB,
conversationId: string
diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts
index 1a74ff2fb6..d510a13914 100644
--- a/ts/state/ducks/conversations.ts
+++ b/ts/state/ducks/conversations.ts
@@ -3733,8 +3733,12 @@ function loadRecentMediaItems(
): ThunkAction {
return async dispatch => {
const messages: Array =
- await DataReader.getMessagesWithVisualMediaAttachments(conversationId, {
+ await DataReader.getOlderMessagesByConversation({
+ conversationId,
limit,
+ requireVisualMediaAttachments: true,
+ storyId: undefined,
+ includeStoryReplies: false,
});
// Cache these messages in memory to ensure Lightbox can find them
@@ -3772,9 +3776,9 @@ function loadRecentMediaItems(
window.ConversationController.get(message.sourceServiceId)
?.id || message.conversationId,
id: message.id,
- received_at: message.received_at,
- received_at_ms: Number(message.received_at_ms),
- sent_at: message.sent_at,
+ receivedAt: message.received_at,
+ receivedAtMs: Number(message.received_at_ms),
+ sentAt: message.sent_at,
},
};
diff --git a/ts/state/ducks/lightbox.ts b/ts/state/ducks/lightbox.ts
index b494614a5b..9d4ed4d543 100644
--- a/ts/state/ducks/lightbox.ts
+++ b/ts/state/ducks/lightbox.ts
@@ -150,22 +150,6 @@ function setPlaybackDisabled(
};
}
-function showLightboxWithMedia(
- selectedIndex: number | undefined,
- media: ReadonlyArray>
-): ShowLightboxActionType {
- return {
- type: SHOW_LIGHTBOX,
- payload: {
- isViewOnce: false,
- media,
- selectedIndex,
- hasPrevMessage: false,
- hasNextMessage: false,
- },
- };
-}
-
function showLightboxForViewOnceMedia(
messageId: string
): ThunkAction {
@@ -224,9 +208,9 @@ function showLightboxForViewOnceMedia(
attachments: message.get('attachments') || [],
id: message.get('id'),
conversationId: message.get('conversationId'),
- received_at: message.get('received_at'),
- received_at_ms: Number(message.get('received_at_ms')),
- sent_at: message.get('sent_at'),
+ receivedAt: message.get('received_at'),
+ receivedAtMs: Number(message.get('received_at_ms')),
+ sentAt: message.get('sent_at'),
},
},
];
@@ -313,9 +297,9 @@ function showLightbox(opts: {
attachments: message.get('attachments') || [],
id: messageId,
conversationId: authorId,
- received_at: receivedAt,
- received_at_ms: Number(message.get('received_at_ms')),
- sent_at: sentAt,
+ receivedAt,
+ receivedAtMs: Number(message.get('received_at_ms')),
+ sentAt,
},
attachment: item,
thumbnailObjectUrl:
@@ -401,11 +385,7 @@ function showLightboxForAdjacentMessage(
}
const [media] = lightbox.media;
- const {
- id: messageId,
- received_at: receivedAt,
- sent_at: sentAt,
- } = media.message;
+ const { id: messageId, receivedAt, sentAt } = media.message;
const message = await __DEPRECATED$getMessageById(messageId);
if (!message) {
@@ -511,7 +491,6 @@ export const actions = {
closeLightbox,
showLightbox,
showLightboxForViewOnceMedia,
- showLightboxWithMedia,
showLightboxForPrevMessage,
showLightboxForNextMessage,
setSelectedLightboxIndex,
diff --git a/ts/state/ducks/mediaGallery.ts b/ts/state/ducks/mediaGallery.ts
index 457b798ece..9d65b90f54 100644
--- a/ts/state/ducks/mediaGallery.ts
+++ b/ts/state/ducks/mediaGallery.ts
@@ -2,19 +2,10 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { ThunkAction } from 'redux-thunk';
+import type { ReadonlyDeep } from 'type-fest';
-import type { AttachmentType } from '../../types/Attachment';
-import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
-import type {
- ConversationUnloadedActionType,
- MessageChangedActionType,
- MessageDeletedActionType,
- MessageExpiredActionType,
-} from './conversations';
-import type { MIMEType } from '../../types/MIME';
-import type { MediaItemType } from '../../types/MediaItem';
-import type { StateType as RootStateType } from '../reducer';
-
+import * as log from '../../logging/log';
+import * as Errors from '../../types/errors';
import { DataReader, DataWriter } from '../../sql/Client';
import {
CONVERSATION_UNLOADED,
@@ -28,165 +19,250 @@ import { isNotNil } from '../../util/isNotNil';
import { getLocalAttachmentUrl } from '../../util/getLocalAttachmentUrl';
import { useBoundActions } from '../../hooks/useBoundActions';
-// eslint-disable-next-line local-rules/type-alias-readonlydeep
-type MediaType = {
+import type { AttachmentType } from '../../types/Attachment';
+import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
+import type {
+ ConversationUnloadedActionType,
+ MessageChangedActionType,
+ MessageDeletedActionType,
+ MessageExpiredActionType,
+} from './conversations';
+import type { MIMEType } from '../../types/MIME';
+import type { MediaItemType } from '../../types/MediaItem';
+import type { StateType as RootStateType } from '../reducer';
+import type { MessageAttributesType } from '../../model-types';
+
+type MediaItemMessage = ReadonlyDeep<{
+ attachments: Array;
+ conversationId: string;
+ id: string;
+ receivedAt: number;
+ receivedAtMs: number;
+ sentAt: number;
+}>;
+type MediaType = ReadonlyDeep<{
path: string;
objectURL: string;
thumbnailObjectUrl?: string;
contentType: MIMEType;
index: number;
attachment: AttachmentType;
- message: {
- attachments: Array;
- conversationId: string;
- id: string;
- received_at: number;
- received_at_ms: number;
- sent_at: number;
- };
-};
+ message: MediaItemMessage;
+}>;
-// eslint-disable-next-line local-rules/type-alias-readonlydeep
-export type MediaGalleryStateType = {
- documents: Array;
- media: Array;
-};
+export type MediaGalleryStateType = ReadonlyDeep<{
+ conversationId: string | undefined;
+ documents: ReadonlyArray;
+ haveOldestDocument: boolean;
+ haveOldestMedia: boolean;
+ loading: boolean;
+ media: ReadonlyArray;
+}>;
-const LOAD_MEDIA_ITEMS = 'mediaGallery/LOAD_MEDIA_ITEMS';
+const FETCH_CHUNK_COUNT = 50;
-// eslint-disable-next-line local-rules/type-alias-readonlydeep
-type LoadMediaItemslActionType = {
- type: typeof LOAD_MEDIA_ITEMS;
+const INITIAL_LOAD = 'mediaGallery/INITIAL_LOAD';
+const LOAD_MORE_MEDIA = 'mediaGallery/LOAD_MORE_MEDIA';
+const LOAD_MORE_DOCUMENTS = 'mediaGallery/LOAD_MORE_DOCUMENTS';
+const SET_LOADING = 'mediaGallery/SET_LOADING';
+
+type InitialLoadActionType = ReadonlyDeep<{
+ type: typeof INITIAL_LOAD;
payload: {
- documents: Array;
- media: Array;
+ conversationId: string;
+ documents: ReadonlyArray;
+ media: ReadonlyArray;
};
-};
+}>;
+type LoadMoreMediaActionType = ReadonlyDeep<{
+ type: typeof LOAD_MORE_MEDIA;
+ payload: {
+ conversationId: string;
+ media: ReadonlyArray;
+ };
+}>;
+type LoadMoreDocumentsActionType = ReadonlyDeep<{
+ type: typeof LOAD_MORE_DOCUMENTS;
+ payload: {
+ conversationId: string;
+ documents: ReadonlyArray;
+ };
+}>;
+type SetLoadingActionType = ReadonlyDeep<{
+ type: typeof SET_LOADING;
+ payload: {
+ loading: boolean;
+ };
+}>;
-// eslint-disable-next-line local-rules/type-alias-readonlydeep
-type MediaGalleryActionType =
+type MediaGalleryActionType = ReadonlyDeep<
| ConversationUnloadedActionType
- | LoadMediaItemslActionType
+ | InitialLoadActionType
+ | LoadMoreDocumentsActionType
+ | LoadMoreMediaActionType
| MessageChangedActionType
| MessageDeletedActionType
- | MessageExpiredActionType;
+ | MessageExpiredActionType
+ | SetLoadingActionType
+>;
-function loadMediaItems(
- conversationId: string
-): ThunkAction {
- return async dispatch => {
- const { upgradeMessageSchema } = window.Signal.Migrations;
+function _getMediaItemMessage(
+ message: ReadonlyDeep
+): MediaItemMessage {
+ return {
+ attachments: message.attachments || [],
+ conversationId:
+ window.ConversationController.lookupOrCreate({
+ serviceId: message.sourceServiceId,
+ e164: message.source,
+ reason: 'conversation_view.showAllMedia',
+ })?.id || message.conversationId,
+ id: message.id,
+ receivedAt: message.received_at,
+ receivedAtMs: Number(message.received_at_ms),
+ sentAt: message.sent_at,
+ };
+}
- // We fetch more documents than media as they don’t require to be loaded
- // into memory right away. Revisit this once we have infinite scrolling:
- const DEFAULT_MEDIA_FETCH_COUNT = 50;
- const DEFAULT_DOCUMENTS_FETCH_COUNT = 150;
+function _cleanVisualAttachments(
+ rawMedia: ReadonlyDeep>
+): ReadonlyArray {
+ let index = 0;
- const ourAci = window.textsecure.storage.user.getCheckedAci();
+ return rawMedia
+ .flatMap(message => {
+ return (message.attachments || []).map(
+ (attachment: AttachmentType): MediaType | undefined => {
+ if (
+ !attachment.path ||
+ !attachment.thumbnail ||
+ isDownloading(attachment) ||
+ hasFailed(attachment)
+ ) {
+ return;
+ }
- const rawMedia = await DataReader.getMessagesWithVisualMediaAttachments(
- conversationId,
- {
- limit: DEFAULT_MEDIA_FETCH_COUNT,
+ const { thumbnail } = attachment;
+ const result = {
+ path: attachment.path,
+ objectURL: getLocalAttachmentUrl(attachment),
+ thumbnailObjectUrl: thumbnail?.path
+ ? getLocalAttachmentUrl(thumbnail)
+ : undefined,
+ contentType: attachment.contentType,
+ index,
+ attachment,
+ message: _getMediaItemMessage(message),
+ };
+
+ index += 1;
+
+ return result;
+ }
+ );
+ })
+ .filter(isNotNil);
+}
+
+function _cleanFileAttachments(
+ rawDocuments: ReadonlyDeep>
+): ReadonlyArray {
+ return rawDocuments
+ .map(message => {
+ const attachments = message.attachments || [];
+ const attachment = attachments[0];
+ if (!attachment) {
+ return;
}
- );
- const rawDocuments = await DataReader.getMessagesWithFileAttachments(
- conversationId,
- {
- limit: DEFAULT_DOCUMENTS_FETCH_COUNT,
- }
- );
- // First we upgrade these messages to ensure that they have thumbnails
- await Promise.all(
- rawMedia.map(async message => {
- const { schemaVersion } = message;
- const model = window.MessageCache.__DEPRECATED$register(
- message.id,
- message,
- 'loadMediaItems'
- );
+ return {
+ contentType: attachment.contentType,
+ index: 0,
+ attachment,
+ message: {
+ ..._getMediaItemMessage(message),
+ attachments: [attachment],
+ },
+ };
+ })
+ .filter(isNotNil);
+}
+async function _upgradeMessages(
+ messages: ReadonlyArray
+): Promise> {
+ const { upgradeMessageSchema } = window.Signal.Migrations;
+ const ourAci = window.textsecure.storage.user.getCheckedAci();
+
+ // We upgrade these messages so they are sure to have thumbnails
+ const upgraded = await Promise.all(
+ messages.map(async message => {
+ const { schemaVersion } = message;
+ const model = window.MessageCache.__DEPRECATED$register(
+ message.id,
+ message,
+ 'loadMediaItems'
+ );
+
+ try {
if (schemaVersion && schemaVersion < VERSION_NEEDED_FOR_DISPLAY) {
const upgradedMsgAttributes = await upgradeMessageSchema(message);
model.set(upgradedMsgAttributes);
await DataWriter.saveMessage(upgradedMsgAttributes, { ourAci });
}
- })
- );
-
- let index = 0;
- const media: Array = rawMedia
- .flatMap(message => {
- return (message.attachments || []).map(
- (attachment: AttachmentType): MediaType | undefined => {
- if (
- !attachment.path ||
- !attachment.thumbnail ||
- isDownloading(attachment) ||
- hasFailed(attachment)
- ) {
- return;
- }
-
- const { thumbnail } = attachment;
- const result = {
- path: attachment.path,
- objectURL: getLocalAttachmentUrl(attachment),
- thumbnailObjectUrl: thumbnail?.path
- ? getLocalAttachmentUrl(thumbnail)
- : undefined,
- contentType: attachment.contentType,
- index,
- attachment,
- message: {
- attachments: message.attachments || [],
- conversationId:
- window.ConversationController.lookupOrCreate({
- serviceId: message.sourceServiceId,
- e164: message.source,
- reason: 'conversation_view.showAllMedia',
- })?.id || message.conversationId,
- id: message.id,
- received_at: message.received_at,
- received_at_ms: Number(message.received_at_ms),
- sent_at: message.sent_at,
- },
- };
-
- index += 1;
-
- return result;
- }
+ } catch (error) {
+ log.warn(
+ `_upgradeMessages: Failed to upgrade message ${model.idForLogging()}: ${Errors.toLogFormat(error)}`
);
- })
- .filter(isNotNil);
+ return undefined;
+ }
- // Unlike visual media, only one non-image attachment is supported
- const documents: Array = rawDocuments
- .map(message => {
- const attachments = message.attachments || [];
- const attachment = attachments[0];
- if (!attachment) {
- return;
- }
+ return model.attributes;
+ })
+ );
- return {
- contentType: attachment.contentType,
- index: 0,
- attachment,
- message: {
- ...message,
- attachments: [attachment],
- },
- };
- })
- .filter(isNotNil);
+ return upgraded.filter(isNotNil);
+}
+
+function initialLoad(
+ conversationId: string
+): ThunkAction<
+ void,
+ RootStateType,
+ unknown,
+ InitialLoadActionType | SetLoadingActionType
+> {
+ return async dispatch => {
+ dispatch({
+ type: SET_LOADING,
+ payload: { loading: true },
+ });
+
+ const rawMedia = await DataReader.getOlderMessagesByConversation({
+ conversationId,
+ includeStoryReplies: false,
+ limit: FETCH_CHUNK_COUNT,
+ requireVisualMediaAttachments: true,
+ storyId: undefined,
+ });
+ const rawDocuments = await DataReader.getOlderMessagesByConversation({
+ conversationId,
+ includeStoryReplies: false,
+ limit: FETCH_CHUNK_COUNT,
+ requireFileAttachments: true,
+ storyId: undefined,
+ });
+
+ const upgraded = await _upgradeMessages(rawMedia);
+ const media = _cleanVisualAttachments(upgraded);
+
+ const documents = _cleanFileAttachments(rawDocuments);
dispatch({
- type: LOAD_MEDIA_ITEMS,
+ type: INITIAL_LOAD,
payload: {
+ conversationId,
documents,
media,
},
@@ -194,8 +270,127 @@ function loadMediaItems(
};
}
+function loadMoreMedia(
+ conversationId: string
+): ThunkAction<
+ void,
+ RootStateType,
+ unknown,
+ InitialLoadActionType | LoadMoreMediaActionType | SetLoadingActionType
+> {
+ return async (dispatch, getState) => {
+ const { conversationId: previousConversationId, media: previousMedia } =
+ getState().mediaGallery;
+
+ if (conversationId !== previousConversationId) {
+ log.warn('loadMoreMedia: conversationId mismatch; calling initialLoad()');
+ initialLoad(conversationId)(dispatch, getState, {});
+ return;
+ }
+
+ const firstMedia = previousMedia[0];
+ if (!firstMedia) {
+ log.warn('loadMoreMedia: no previous media; calling initialLoad()');
+ initialLoad(conversationId)(dispatch, getState, {});
+ return;
+ }
+
+ dispatch({
+ type: SET_LOADING,
+ payload: { loading: true },
+ });
+
+ const { sentAt, receivedAt, id: messageId } = firstMedia.message;
+
+ const rawMedia = await DataReader.getOlderMessagesByConversation({
+ conversationId,
+ includeStoryReplies: false,
+ limit: FETCH_CHUNK_COUNT,
+ messageId,
+ receivedAt,
+ requireVisualMediaAttachments: true,
+ sentAt,
+ storyId: undefined,
+ });
+
+ const upgraded = await _upgradeMessages(rawMedia);
+ const media = _cleanVisualAttachments(upgraded);
+
+ dispatch({
+ type: LOAD_MORE_MEDIA,
+ payload: {
+ conversationId,
+ media,
+ },
+ });
+ };
+}
+
+function loadMoreDocuments(
+ conversationId: string
+): ThunkAction<
+ void,
+ RootStateType,
+ unknown,
+ InitialLoadActionType | LoadMoreDocumentsActionType | SetLoadingActionType
+> {
+ return async (dispatch, getState) => {
+ const {
+ conversationId: previousConversationId,
+ documents: previousDocuments,
+ } = getState().mediaGallery;
+
+ if (conversationId !== previousConversationId) {
+ log.warn(
+ 'loadMoreDocuments: conversationId mismatch; calling initialLoad()'
+ );
+ initialLoad(conversationId)(dispatch, getState, {});
+ return;
+ }
+
+ const firstDocument = previousDocuments[0];
+ if (!firstDocument) {
+ log.warn(
+ 'loadMoreDocuments: no previous documents; calling initialLoad()'
+ );
+ initialLoad(conversationId)(dispatch, getState, {});
+ return;
+ }
+
+ dispatch({
+ type: SET_LOADING,
+ payload: { loading: true },
+ });
+
+ const { sentAt, receivedAt, id: messageId } = firstDocument.message;
+
+ const rawDocuments = await DataReader.getOlderMessagesByConversation({
+ conversationId,
+ includeStoryReplies: false,
+ limit: FETCH_CHUNK_COUNT,
+ messageId,
+ receivedAt,
+ requireFileAttachments: true,
+ sentAt,
+ storyId: undefined,
+ });
+
+ const documents = _cleanFileAttachments(rawDocuments);
+
+ dispatch({
+ type: LOAD_MORE_DOCUMENTS,
+ payload: {
+ conversationId,
+ documents,
+ },
+ });
+ };
+}
+
export const actions = {
- loadMediaItems,
+ initialLoad,
+ loadMoreMedia,
+ loadMoreDocuments,
};
export const useMediaGalleryActions = (): BoundActionCreatorsMapObject<
@@ -204,7 +399,11 @@ export const useMediaGalleryActions = (): BoundActionCreatorsMapObject<
export function getEmptyState(): MediaGalleryStateType {
return {
+ conversationId: undefined,
documents: [],
+ haveOldestDocument: false,
+ haveOldestMedia: false,
+ loading: true,
media: [],
};
}
@@ -213,27 +412,100 @@ export function reducer(
state: Readonly = getEmptyState(),
action: Readonly
): MediaGalleryStateType {
- if (action.type === LOAD_MEDIA_ITEMS) {
+ if (action.type === SET_LOADING) {
+ const { loading } = action.payload;
+
return {
...state,
- ...action.payload,
+ loading,
};
}
- if (action.type === MESSAGE_CHANGED) {
- if (!action.payload.data.deletedForEveryone) {
+ if (action.type === INITIAL_LOAD) {
+ return {
+ ...state,
+ loading: false,
+ ...action.payload,
+ haveOldestDocument: false,
+ haveOldestMedia: false,
+ };
+ }
+
+ if (action.type === LOAD_MORE_MEDIA) {
+ const { conversationId, media } = action.payload;
+ if (state.conversationId !== conversationId) {
return state;
}
return {
...state,
- media: state.media.filter(item => item.message.id !== action.payload.id),
- documents: state.documents.filter(
- item => item.message.id !== action.payload.id
- ),
+ loading: false,
+ haveOldestMedia: media.length === 0,
+ media: media.concat(state.media),
};
}
+ if (action.type === LOAD_MORE_DOCUMENTS) {
+ const { conversationId, documents } = action.payload;
+ if (state.conversationId !== conversationId) {
+ return state;
+ }
+
+ return {
+ ...state,
+ loading: false,
+ haveOldestDocument: documents.length === 0,
+ documents: documents.concat(state.documents),
+ };
+ }
+
+ // We don't capture the initial message add, but we do capture the moment when its
+ // attachments have been downloaded
+ if (action.type === MESSAGE_CHANGED) {
+ const { payload } = action;
+ const { conversationId, data: message } = payload;
+
+ if (conversationId !== state.conversationId) {
+ return state;
+ }
+
+ if (!message.attachments || message.attachments.length === 0) {
+ return state;
+ }
+
+ const mediaWithout = state.media.filter(
+ item => item.message.id !== message.id
+ );
+ const documentsWithout = state.documents.filter(
+ item => item.message.id !== message.id
+ );
+
+ if (message.deletedForEveryone) {
+ return {
+ ...state,
+ media: mediaWithout,
+ documents: documentsWithout,
+ };
+ }
+
+ // Check whether we have new downloaded media, or an attachment has been deleted
+ const mediaCount = state.media.length - mediaWithout.length;
+ const documentCount = state.documents.length - mediaWithout.length;
+
+ const media = _cleanVisualAttachments([message]);
+ const documents = _cleanFileAttachments([message]);
+
+ if (mediaCount !== media.length || documentCount !== documents.length) {
+ return {
+ ...state,
+ media: mediaWithout.concat(media),
+ documents: documentsWithout.concat(documents),
+ };
+ }
+
+ return state;
+ }
+
if (action.type === MESSAGE_DELETED || action.type === MESSAGE_EXPIRED) {
return {
...state,
diff --git a/ts/state/smart/AllMedia.tsx b/ts/state/smart/AllMedia.tsx
index 5bc5509327..02ac06768e 100644
--- a/ts/state/smart/AllMedia.tsx
+++ b/ts/state/smart/AllMedia.tsx
@@ -15,19 +15,26 @@ export type PropsType = {
export const SmartAllMedia = memo(function SmartAllMedia({
conversationId,
}: PropsType) {
- const { media, documents } = useSelector(getMediaGalleryState);
- const { loadMediaItems } = useMediaGalleryActions();
+ const { media, documents, haveOldestDocument, haveOldestMedia, loading } =
+ useSelector(getMediaGalleryState);
+ const { initialLoad, loadMoreMedia, loadMoreDocuments } =
+ useMediaGalleryActions();
const { saveAttachment } = useConversationsActions();
- const { showLightboxWithMedia } = useLightboxActions();
+ const { showLightbox } = useLightboxActions();
return (
);
diff --git a/ts/state/smart/ConversationDetails.tsx b/ts/state/smart/ConversationDetails.tsx
index 6355c4b704..edbca885eb 100644
--- a/ts/state/smart/ConversationDetails.tsx
+++ b/ts/state/smart/ConversationDetails.tsx
@@ -128,7 +128,7 @@ export const SmartConversationDetails = memo(function SmartConversationDetails({
toggleEditNicknameAndNoteModal,
toggleSafetyNumberModal,
} = useGlobalModalActions();
- const { showLightboxWithMedia } = useLightboxActions();
+ const { showLightbox } = useLightboxActions();
const conversation = conversationSelector(conversationId);
assertDev(
@@ -215,7 +215,7 @@ export const SmartConversationDetails = memo(function SmartConversationDetails({
setMuteExpiration={setMuteExpiration}
showContactModal={showContactModal}
showConversation={showConversation}
- showLightboxWithMedia={showLightboxWithMedia}
+ showLightbox={showLightbox}
theme={theme}
toggleAboutContactModal={toggleAboutContactModal}
toggleAddUserToAnotherGroupModal={toggleAddUserToAnotherGroupModal}
diff --git a/ts/state/smart/ConversationHeader.tsx b/ts/state/smart/ConversationHeader.tsx
index d83bf0c152..176613a1de 100644
--- a/ts/state/smart/ConversationHeader.tsx
+++ b/ts/state/smart/ConversationHeader.tsx
@@ -241,7 +241,7 @@ export const SmartConversationHeader = memo(function SmartConversationHeader({
pushPanelForConversation({ type: PanelType.ConversationDetails });
}, [pushPanelForConversation]);
- const onViewRecentMedia = useCallback(() => {
+ const onViewAllMedia = useCallback(() => {
pushPanelForConversation({ type: PanelType.AllMedia });
}, [pushPanelForConversation]);
@@ -298,7 +298,7 @@ export const SmartConversationHeader = memo(function SmartConversationHeader({
onSelectModeEnter={onSelectModeEnter}
onShowMembers={onShowMembers}
onViewConversationDetails={onViewConversationDetails}
- onViewRecentMedia={onViewRecentMedia}
+ onViewAllMedia={onViewAllMedia}
onViewUserStories={onViewUserStories}
outgoingCallButtonStyle={outgoingCallButtonStyle}
setLocalDeleteWarningShown={setLocalDeleteWarningShown}
diff --git a/ts/test-electron/sql/allMedia_test.ts b/ts/test-electron/sql/allMedia_test.ts
deleted file mode 100644
index ead09f5413..0000000000
--- a/ts/test-electron/sql/allMedia_test.ts
+++ /dev/null
@@ -1,237 +0,0 @@
-// Copyright 2021 Signal Messenger, LLC
-// SPDX-License-Identifier: AGPL-3.0-only
-
-import { assert } from 'chai';
-import { v4 as generateUuid } from 'uuid';
-
-import { DataReader, DataWriter } from '../../sql/Client';
-import { generateAci } from '../../types/ServiceId';
-
-import type { MessageAttributesType } from '../../model-types.d';
-
-const {
- _getAllMessages,
- getMessagesWithVisualMediaAttachments,
- getMessagesWithFileAttachments,
-} = DataReader;
-const { removeAll, saveMessages } = DataWriter;
-
-describe('sql/allMedia', () => {
- beforeEach(async () => {
- await removeAll();
- });
-
- describe('getMessagesWithVisualMediaAttachments', () => {
- it('returns messages matching with visual attachments', async () => {
- assert.lengthOf(await _getAllMessages(), 0);
-
- const now = Date.now();
- const conversationId = generateUuid();
- const ourAci = generateAci();
- const message1: MessageAttributesType = {
- id: generateUuid(),
- body: 'message 1',
- type: 'outgoing',
- conversationId,
- sent_at: now - 20,
- received_at: now - 20,
- timestamp: now - 20,
- hasVisualMediaAttachments: true,
- };
- const message2: MessageAttributesType = {
- id: generateUuid(),
- body: 'message 2',
- type: 'outgoing',
- conversationId,
- sent_at: now - 10,
- received_at: now - 10,
- timestamp: now - 10,
- };
- const message3: MessageAttributesType = {
- id: generateUuid(),
- body: 'message 3',
- type: 'outgoing',
- conversationId: generateUuid(),
- sent_at: now,
- received_at: now,
- timestamp: now,
- hasVisualMediaAttachments: true,
- };
-
- await saveMessages([message1, message2, message3], {
- forceSave: true,
- ourAci,
- });
-
- assert.lengthOf(await _getAllMessages(), 3);
-
- const searchResults = await getMessagesWithVisualMediaAttachments(
- conversationId,
- { limit: 5 }
- );
- assert.lengthOf(searchResults, 1);
- assert.strictEqual(searchResults[0].id, message1.id);
- });
-
- it('excludes stories and story replies', async () => {
- assert.lengthOf(await _getAllMessages(), 0);
-
- const now = Date.now();
- const conversationId = generateUuid();
- const ourAci = generateAci();
- const message1: MessageAttributesType = {
- id: generateUuid(),
- body: 'message 1',
- type: 'outgoing',
- conversationId,
- sent_at: now - 20,
- received_at: now - 20,
- timestamp: now - 20,
- hasVisualMediaAttachments: true,
- };
- const message2: MessageAttributesType = {
- id: generateUuid(),
- body: 'message 2',
- type: 'outgoing',
- conversationId,
- sent_at: now - 10,
- received_at: now - 10,
- timestamp: now - 10,
- storyId: generateUuid(),
- hasVisualMediaAttachments: true,
- };
- const message3: MessageAttributesType = {
- id: generateUuid(),
- body: 'message 3',
- type: 'story',
- conversationId,
- sent_at: now,
- received_at: now,
- timestamp: now,
- storyId: generateUuid(),
- hasVisualMediaAttachments: true,
- };
-
- await saveMessages([message1, message2, message3], {
- forceSave: true,
- ourAci,
- });
-
- assert.lengthOf(await _getAllMessages(), 3);
-
- const searchResults = await getMessagesWithVisualMediaAttachments(
- conversationId,
- { limit: 5 }
- );
- assert.lengthOf(searchResults, 1);
- assert.strictEqual(searchResults[0].id, message1.id);
- });
- });
-
- describe('getMessagesWithFileAttachments', () => {
- it('returns messages matching with visual attachments', async () => {
- assert.lengthOf(await _getAllMessages(), 0);
-
- const now = Date.now();
- const conversationId = generateUuid();
- const ourAci = generateAci();
- const message1: MessageAttributesType = {
- id: generateUuid(),
- body: 'message 1',
- type: 'outgoing',
- conversationId,
- sent_at: now - 20,
- received_at: now - 20,
- timestamp: now - 20,
- hasFileAttachments: true,
- };
- const message2: MessageAttributesType = {
- id: generateUuid(),
- body: 'message 2',
- type: 'outgoing',
- conversationId,
- sent_at: now - 10,
- received_at: now - 10,
- timestamp: now - 10,
- };
- const message3: MessageAttributesType = {
- id: generateUuid(),
- body: 'message 3',
- type: 'outgoing',
- conversationId: generateUuid(),
- sent_at: now,
- received_at: now,
- timestamp: now,
- hasFileAttachments: true,
- };
-
- await saveMessages([message1, message2, message3], {
- forceSave: true,
- ourAci,
- });
-
- assert.lengthOf(await _getAllMessages(), 3);
-
- const searchResults = await getMessagesWithFileAttachments(
- conversationId,
- { limit: 5 }
- );
- assert.lengthOf(searchResults, 1);
- assert.strictEqual(searchResults[0].id, message1.id);
- });
-
- it('excludes stories and story replies', async () => {
- assert.lengthOf(await _getAllMessages(), 0);
-
- const now = Date.now();
- const conversationId = generateUuid();
- const ourAci = generateAci();
- const message1: MessageAttributesType = {
- id: generateUuid(),
- body: 'message 1',
- type: 'outgoing',
- conversationId,
- sent_at: now - 20,
- received_at: now - 20,
- timestamp: now - 20,
- hasFileAttachments: true,
- };
- const message2: MessageAttributesType = {
- id: generateUuid(),
- body: 'message 2',
- type: 'outgoing',
- conversationId,
- sent_at: now - 10,
- received_at: now - 10,
- timestamp: now - 10,
- storyId: generateUuid(),
- hasFileAttachments: true,
- };
- const message3: MessageAttributesType = {
- id: generateUuid(),
- body: 'message 3',
- type: 'story',
- conversationId,
- sent_at: now,
- received_at: now,
- timestamp: now,
- storyId: generateUuid(),
- hasFileAttachments: true,
- };
-
- await saveMessages([message1, message2, message3], {
- forceSave: true,
- ourAci,
- });
-
- assert.lengthOf(await _getAllMessages(), 3);
-
- const searchResults = await getMessagesWithFileAttachments(
- conversationId,
- { limit: 5 }
- );
- assert.lengthOf(searchResults, 1);
- assert.strictEqual(searchResults[0].id, message1.id);
- });
- });
-});
diff --git a/ts/test-node/components/media-gallery/groupMediaItemsByDate.ts b/ts/test-node/components/media-gallery/groupMediaItemsByDate.ts
new file mode 100644
index 0000000000..0ada15e6f7
--- /dev/null
+++ b/ts/test-node/components/media-gallery/groupMediaItemsByDate.ts
@@ -0,0 +1,97 @@
+// Copyright 2024 Signal Messenger, LLC
+// SPDX-License-Identifier: AGPL-3.0-only
+
+import { assert } from 'chai';
+import { shuffle } from 'lodash';
+
+import { IMAGE_JPEG } from '../../../types/MIME';
+import { groupMediaItemsByDate } from '../../../components/conversation/media-gallery/groupMediaItemsByDate';
+import type { MediaItemType } from '../../../types/MediaItem';
+import { fakeAttachment } from '../../../test-both/helpers/fakeAttachment';
+
+const testDate = (
+ year: number,
+ month: number,
+ day: number,
+ hour: number,
+ minute: number,
+ second = 0
+): Date => new Date(year, month - 1, day, hour, minute, second, 0);
+
+const toMediaItem = (id: string, date: Date): MediaItemType => {
+ return {
+ objectURL: id,
+ index: 0,
+ message: {
+ conversationId: '1234',
+ id: 'id',
+ receivedAt: date.getTime(),
+ receivedAtMs: date.getTime(),
+ attachments: [],
+ sentAt: date.getTime(),
+ },
+ attachment: fakeAttachment({
+ fileName: 'fileName',
+ contentType: IMAGE_JPEG,
+ url: 'url',
+ }),
+ };
+};
+
+describe('groupMediaItemsByDate', () => {
+ it('should group mediaItems', () => {
+ const referenceTime = testDate(2024, 4, 12, 18, 0, 0).getTime(); // Friday
+ const input: Array = shuffle([
+ toMediaItem('today-1', testDate(2024, 4, 12, 17, 59)), // Friday, one minute ago
+ toMediaItem('today-2', testDate(2024, 4, 12, 0, 1)), // Friday early morning
+ toMediaItem('yesterday-1', testDate(2024, 4, 11, 18, 0)), // Thursday
+ toMediaItem('yesterday-2', testDate(2024, 4, 11, 0, 1)), // Thursday early morning
+ toMediaItem('thisWeek-1', testDate(2024, 4, 10, 18, 0)), // Wednesday
+ toMediaItem('thisWeek-2', testDate(2024, 4, 8, 18, 0)), // Monday
+ toMediaItem('thisWeek-3', testDate(2024, 4, 5, 18, 0)), // Last Friday
+ toMediaItem('thisWeek-4', testDate(2024, 4, 5, 0, 1)), // Last Friday early morning
+ toMediaItem('thisMonth-1', testDate(2024, 4, 2, 18, 0)), // Second day of moth
+ toMediaItem('thisMonth-2', testDate(2024, 4, 1, 18, 0)), // First day of month
+ toMediaItem('mar2024-1', testDate(2024, 3, 31, 23, 59)),
+ toMediaItem('mar2024-2', testDate(2024, 3, 1, 0, 1)),
+ toMediaItem('feb2011-1', testDate(2011, 2, 28, 23, 59)),
+ toMediaItem('feb2011-2', testDate(2011, 2, 1, 0, 1)),
+ ]);
+
+ const actual = groupMediaItemsByDate(referenceTime, input);
+
+ assert.strictEqual(actual[0].type, 'today');
+ assert.strictEqual(actual[0].mediaItems.length, 2, 'today');
+ assert.strictEqual(actual[0].mediaItems[0].objectURL, 'today-1');
+ assert.strictEqual(actual[0].mediaItems[1].objectURL, 'today-2');
+
+ assert.strictEqual(actual[1].type, 'yesterday');
+ assert.strictEqual(actual[1].mediaItems.length, 2, 'yesterday');
+ assert.strictEqual(actual[1].mediaItems[0].objectURL, 'yesterday-1');
+ assert.strictEqual(actual[1].mediaItems[1].objectURL, 'yesterday-2');
+
+ assert.strictEqual(actual[2].type, 'thisWeek');
+ assert.strictEqual(actual[2].mediaItems.length, 4, 'thisWeek');
+ assert.strictEqual(actual[2].mediaItems[0].objectURL, 'thisWeek-1');
+ assert.strictEqual(actual[2].mediaItems[1].objectURL, 'thisWeek-2');
+ assert.strictEqual(actual[2].mediaItems[2].objectURL, 'thisWeek-3');
+ assert.strictEqual(actual[2].mediaItems[3].objectURL, 'thisWeek-4');
+
+ assert.strictEqual(actual[3].type, 'thisMonth');
+ assert.strictEqual(actual[3].mediaItems.length, 2, 'thisMonth');
+ assert.strictEqual(actual[3].mediaItems[0].objectURL, 'thisMonth-1');
+ assert.strictEqual(actual[3].mediaItems[1].objectURL, 'thisMonth-2');
+
+ assert.strictEqual(actual[4].type, 'yearMonth');
+ assert.strictEqual(actual[4].mediaItems.length, 2, 'mar2024');
+ assert.strictEqual(actual[4].mediaItems[0].objectURL, 'mar2024-1');
+ assert.strictEqual(actual[4].mediaItems[1].objectURL, 'mar2024-2');
+
+ assert.strictEqual(actual[5].type, 'yearMonth');
+ assert.strictEqual(actual[5].mediaItems.length, 2, 'feb2011');
+ assert.strictEqual(actual[5].mediaItems[0].objectURL, 'feb2011-1');
+ assert.strictEqual(actual[5].mediaItems[1].objectURL, 'feb2011-2');
+
+ assert.strictEqual(actual.length, 6, 'total sections');
+ });
+});
diff --git a/ts/test-node/components/media-gallery/groupMessagesByDate_test.ts b/ts/test-node/components/media-gallery/groupMessagesByDate_test.ts
deleted file mode 100644
index 0a20c1a976..0000000000
--- a/ts/test-node/components/media-gallery/groupMessagesByDate_test.ts
+++ /dev/null
@@ -1,271 +0,0 @@
-// Copyright 2018 Signal Messenger, LLC
-// SPDX-License-Identifier: AGPL-3.0-only
-
-import { assert } from 'chai';
-import { shuffle } from 'lodash';
-
-import { IMAGE_JPEG } from '../../../types/MIME';
-import type { Section } from '../../../components/conversation/media-gallery/groupMediaItemsByDate';
-import { groupMediaItemsByDate } from '../../../components/conversation/media-gallery/groupMediaItemsByDate';
-import type { MediaItemType } from '../../../types/MediaItem';
-import { fakeAttachment } from '../../../test-both/helpers/fakeAttachment';
-
-const testDate = (
- year: number,
- month: number,
- day: number,
- hour: number,
- minute: number,
- second = 0
-): Date => new Date(Date.UTC(year, month - 1, day, hour, minute, second, 0));
-
-const toMediaItem = (date: Date): MediaItemType => ({
- objectURL: date.toUTCString(),
- index: 0,
- message: {
- conversationId: '1234',
- id: 'id',
- received_at: date.getTime(),
- received_at_ms: date.getTime(),
- attachments: [],
- sent_at: date.getTime(),
- },
- attachment: fakeAttachment({
- fileName: 'fileName',
- contentType: IMAGE_JPEG,
- url: 'url',
- }),
-});
-
-describe('groupMediaItemsByDate', () => {
- it('should group mediaItems', () => {
- const referenceTime = testDate(2018, 4, 12, 18, 0, 0).getTime(); // Thu
- const input: Array = shuffle([
- // Today
- toMediaItem(testDate(2018, 4, 12, 12, 0)), // Thu
- toMediaItem(testDate(2018, 4, 12, 0, 1)), // Thu
- // This week
- toMediaItem(testDate(2018, 4, 11, 23, 59)), // Wed
- toMediaItem(testDate(2018, 4, 9, 0, 1)), // Mon
- // This month
- toMediaItem(testDate(2018, 4, 8, 23, 59)), // Sun
- toMediaItem(testDate(2018, 4, 1, 0, 1)),
- // March 2018
- toMediaItem(testDate(2018, 3, 31, 23, 59)),
- toMediaItem(testDate(2018, 3, 1, 14, 0)),
- // February 2011
- toMediaItem(testDate(2011, 2, 28, 23, 59)),
- toMediaItem(testDate(2011, 2, 1, 10, 0)),
- ]);
-
- const expected: Array = [
- {
- type: 'today',
- mediaItems: [
- {
- objectURL: 'Thu, 12 Apr 2018 12:00:00 GMT',
- index: 0,
- message: {
- conversationId: '1234',
- id: 'id',
- received_at: 1523534400000,
- received_at_ms: 1523534400000,
- attachments: [],
- sent_at: 1523534400000,
- },
- attachment: fakeAttachment({
- fileName: 'fileName',
- contentType: IMAGE_JPEG,
- url: 'url',
- }),
- },
- {
- objectURL: 'Thu, 12 Apr 2018 00:01:00 GMT',
- index: 0,
- message: {
- conversationId: '1234',
- id: 'id',
- received_at: 1523491260000,
- received_at_ms: 1523491260000,
- attachments: [],
- sent_at: 1523491260000,
- },
- attachment: fakeAttachment({
- fileName: 'fileName',
- contentType: IMAGE_JPEG,
- url: 'url',
- }),
- },
- ],
- },
- {
- type: 'yesterday',
- mediaItems: [
- {
- objectURL: 'Wed, 11 Apr 2018 23:59:00 GMT',
- index: 0,
- message: {
- conversationId: '1234',
- id: 'id',
- received_at: 1523491140000,
- received_at_ms: 1523491140000,
- attachments: [],
- sent_at: 1523491140000,
- },
- attachment: fakeAttachment({
- fileName: 'fileName',
- contentType: IMAGE_JPEG,
- url: 'url',
- }),
- },
- ],
- },
- {
- type: 'thisWeek',
- mediaItems: [
- {
- objectURL: 'Mon, 09 Apr 2018 00:01:00 GMT',
- index: 0,
- message: {
- conversationId: '1234',
- id: 'id',
- received_at: 1523232060000,
- received_at_ms: 1523232060000,
- attachments: [],
- sent_at: 1523232060000,
- },
- attachment: fakeAttachment({
- fileName: 'fileName',
- contentType: IMAGE_JPEG,
- url: 'url',
- }),
- },
- ],
- },
- {
- type: 'thisMonth',
- mediaItems: [
- {
- objectURL: 'Sun, 08 Apr 2018 23:59:00 GMT',
- index: 0,
- message: {
- conversationId: '1234',
- id: 'id',
- received_at: 1523231940000,
- received_at_ms: 1523231940000,
- attachments: [],
- sent_at: 1523231940000,
- },
- attachment: fakeAttachment({
- fileName: 'fileName',
- contentType: IMAGE_JPEG,
- url: 'url',
- }),
- },
- {
- objectURL: 'Sun, 01 Apr 2018 00:01:00 GMT',
- index: 0,
- message: {
- conversationId: '1234',
- id: 'id',
- received_at: 1522540860000,
- received_at_ms: 1522540860000,
- attachments: [],
- sent_at: 1522540860000,
- },
- attachment: fakeAttachment({
- fileName: 'fileName',
- contentType: IMAGE_JPEG,
- url: 'url',
- }),
- },
- ],
- },
- {
- type: 'yearMonth',
- year: 2018,
- month: 2,
- mediaItems: [
- {
- objectURL: 'Sat, 31 Mar 2018 23:59:00 GMT',
- index: 0,
- message: {
- conversationId: '1234',
- id: 'id',
- received_at: 1522540740000,
- received_at_ms: 1522540740000,
- attachments: [],
- sent_at: 1522540740000,
- },
- attachment: fakeAttachment({
- fileName: 'fileName',
- contentType: IMAGE_JPEG,
- url: 'url',
- }),
- },
- {
- objectURL: 'Thu, 01 Mar 2018 14:00:00 GMT',
- index: 0,
- message: {
- conversationId: '1234',
- id: 'id',
- received_at: 1519912800000,
- received_at_ms: 1519912800000,
- attachments: [],
- sent_at: 1519912800000,
- },
- attachment: fakeAttachment({
- fileName: 'fileName',
- contentType: IMAGE_JPEG,
- url: 'url',
- }),
- },
- ],
- },
- {
- type: 'yearMonth',
- year: 2011,
- month: 1,
- mediaItems: [
- {
- objectURL: 'Mon, 28 Feb 2011 23:59:00 GMT',
- index: 0,
- message: {
- conversationId: '1234',
- id: 'id',
- received_at: 1298937540000,
- received_at_ms: 1298937540000,
- attachments: [],
- sent_at: 1298937540000,
- },
- attachment: fakeAttachment({
- fileName: 'fileName',
- contentType: IMAGE_JPEG,
- url: 'url',
- }),
- },
- {
- objectURL: 'Tue, 01 Feb 2011 10:00:00 GMT',
- index: 0,
- message: {
- conversationId: '1234',
- id: 'id',
- received_at: 1296554400000,
- received_at_ms: 1296554400000,
- attachments: [],
- sent_at: 1296554400000,
- },
- attachment: fakeAttachment({
- fileName: 'fileName',
- contentType: IMAGE_JPEG,
- url: 'url',
- }),
- },
- ],
- },
- ];
-
- const actual = groupMediaItemsByDate(referenceTime, input);
- assert.deepEqual(actual, expected);
- });
-});
diff --git a/ts/types/Colors.ts b/ts/types/Colors.ts
index 563e4aa5a5..2e741a3dec 100644
--- a/ts/types/Colors.ts
+++ b/ts/types/Colors.ts
@@ -88,7 +88,7 @@ export const AvatarColorMap = new Map([
],
]);
-export const AvatarColors = Array.from(AvatarColorMap.keys());
+export const AvatarColors = Array.from(AvatarColorMap.keys()).sort();
export const AVATAR_COLOR_COUNT = AvatarColors.length;
diff --git a/ts/types/MediaItem.ts b/ts/types/MediaItem.ts
index 15d8438f51..2f5be69523 100644
--- a/ts/types/MediaItem.ts
+++ b/ts/types/MediaItem.ts
@@ -7,13 +7,12 @@ import type { MIMEType } from './MIME';
export type MediaItemMessageType = Pick<
ReadonlyMessageAttributesType,
- | 'attachments'
- | 'conversationId'
- | 'id'
- | 'received_at'
- | 'received_at_ms'
- | 'sent_at'
->;
+ 'attachments' | 'conversationId' | 'id'
+> & {
+ receivedAt: number;
+ receivedAtMs?: number;
+ sentAt: number;
+};
export type MediaItemType = {
attachment: AttachmentType;
diff --git a/ts/util/getColorForCallLink.ts b/ts/util/getColorForCallLink.ts
index 105afa1f15..330f8a42d0 100644
--- a/ts/util/getColorForCallLink.ts
+++ b/ts/util/getColorForCallLink.ts
@@ -10,7 +10,7 @@ const BASE_16_CONSONANT_ALPHABET = 'bcdfghkmnpqrstxz';
export function getColorForCallLink(rootKey: string): string {
const rootKeyStart = rootKey.slice(0, 2);
- const upper = BASE_16_CONSONANT_ALPHABET.indexOf(rootKeyStart[0]) || 0 * 16;
+ const upper = (BASE_16_CONSONANT_ALPHABET.indexOf(rootKeyStart[0]) || 0) * 16;
const lower = BASE_16_CONSONANT_ALPHABET.indexOf(rootKeyStart[1]) || 0;
const firstByte = upper + lower;
diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json
index cae84b0733..b932b0f9f2 100644
--- a/ts/util/lint/exceptions.json
+++ b/ts/util/lint/exceptions.json
@@ -2877,6 +2877,38 @@
"updated": "2019-11-01T22:46:33.013Z",
"reasonDetail": "Used for setting focus only"
},
+ {
+ "rule": "React-useRef",
+ "path": "ts/components/conversation/media-gallery/MediaGallery.tsx",
+ "line": " const loadingRef = useRef(false);",
+ "reasonCategory": "usageTrusted",
+ "updated": "2024-09-03T00:45:23.978Z",
+ "reasonDetail": "A boolean to help us avoid making too many 'load more' requests"
+ },
+ {
+ "rule": "React-useRef",
+ "path": "ts/components/conversation/media-gallery/MediaGallery.tsx",
+ "line": " const intersectionObserver = useRef(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.tsx",
+ "line": " const scrollObserverRef = useRef(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.tsx",
+ "line": " const tabViewRef = useRef(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",
"path": "ts/components/emoji/EmojiButton.tsx",