From 780f39c28517e2aa1c6c252889093400a8bd03f9 Mon Sep 17 00:00:00 2001 From: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com> Date: Mon, 6 Oct 2025 12:23:41 -0700 Subject: [PATCH] Faster incremental builds --- .eslintrc.js | 12 + ts/background.ts | 2 +- ts/challenge.ts | 6 +- ts/components/CompositionArea.tsx | 2 +- ts/components/CompositionUpload.tsx | 2 +- ts/components/FileThumbnail.tsx | 2 +- ts/components/Lightbox.tsx | 2 +- ts/components/SafetyNumberChangeDialog.tsx | 7 - ts/components/StoryCreator.tsx | 2 +- ts/components/StoryImage.tsx | 2 +- ts/components/StoryLinkPreview.tsx | 2 +- ts/components/StoryViewer.tsx | 2 +- ts/components/conversation/AttachmentList.tsx | 2 +- .../conversation/ConversationView.tsx | 2 +- ts/components/conversation/GIF.tsx | 2 +- ts/components/conversation/Image.tsx | 2 +- .../conversation/ImageGrid.stories.tsx | 2 +- ts/components/conversation/ImageGrid.tsx | 2 +- ts/components/conversation/Message.tsx | 10 +- ts/components/conversation/MessageAudio.tsx | 2 +- ts/components/conversation/MessageBody.tsx | 2 +- .../conversation/StagedLinkPreview.tsx | 2 +- .../conversation/TimelineMessage.stories.tsx | 3 +- .../conversation/TimelineMessage.tsx | 2 +- .../media-gallery/MediaGridItem.tsx | 2 +- ts/groups.ts | 2 +- ts/groups/joinViaLink.ts | 2 +- ts/hooks/useAttachmentStatus.ts | 3 +- ts/jobs/AttachmentBackupManager.ts | 4 +- ts/jobs/AttachmentDownloadManager.ts | 6 +- ts/jobs/helpers/attachmentBackfill.ts | 4 +- .../helpers/findRetryAfterTimeFromError.ts | 2 +- ts/jobs/reportSpamJobQueue.ts | 2 +- .../shouldUseFullSizeLinkPreviewImage.ts | 2 +- ts/messageModifiers/AttachmentDownloads.ts | 2 +- ts/messageModifiers/ViewSyncs.ts | 2 +- ts/messages/copyQuote.ts | 2 +- ts/messages/handleDataMessage.ts | 2 +- ts/messages/helpers.ts | 2 +- ts/model-types.d.ts | 7 +- ts/services/backups/api.ts | 2 +- ts/services/backups/credentials.ts | 2 +- ts/services/backups/export.ts | 6 +- ts/services/backups/import.ts | 2 +- ts/services/backups/index.ts | 2 +- ts/services/backups/util/filePointers.ts | 4 +- ts/services/backups/util/mediaId.ts | 2 +- ts/services/profiles.ts | 2 +- ts/services/releaseNotesFetcher.ts | 2 +- ts/services/username.ts | 2 +- ts/signal.ts | 2 +- ts/sql/Server.ts | 7 +- ts/sql/hydration.ts | 2 +- ts/state/ducks/audioPlayer.ts | 2 +- ts/state/ducks/calling.ts | 2 +- ts/state/ducks/composer.ts | 13 +- ts/state/ducks/conversations.ts | 2 +- ts/state/ducks/globalModals.ts | 4 +- ts/state/ducks/installer.ts | 2 +- ts/state/ducks/lightbox.ts | 2 +- ts/state/ducks/mediaGallery.ts | 2 +- ts/state/ducks/stories.ts | 4 +- ts/state/selectors/audioPlayer.ts | 4 +- ts/state/selectors/message.ts | 2 +- ts/state/smart/ForwardMessagesModal.tsx | 2 +- ts/state/smart/SendAnywayDialog.tsx | 6 +- ts/test-electron/backup/attachments_test.ts | 4 +- ts/test-electron/backup/bubble_test.ts | 2 +- ts/test-electron/backup/filePointer_test.ts | 2 +- .../deleteMessageAttachments_test.ts | 6 +- .../services/AttachmentBackupManager_test.ts | 2 +- ts/test-electron/services/profiles_test.ts | 2 +- .../util/downloadAttachment_test.ts | 2 +- ts/test-electron/util/sendToGroup_test.ts | 2 +- .../findRetryAfterTimeFromError_test.ts | 2 +- .../helpers/handleMultipleSendErrors_test.ts | 6 +- .../sleepForRateLimitRetryAfterTime_test.ts | 2 +- ts/test-node/types/Attachment_test.ts | 48 +- ts/textsecure/Errors.ts | 43 +- ts/textsecure/OutgoingMessage.ts | 2 +- ts/textsecure/SendMessage.ts | 2 +- ts/textsecure/SocketManager.ts | 3 +- ts/textsecure/Types.d.ts | 2 +- ts/textsecure/UpdateKeysListener.ts | 2 +- ts/textsecure/Utils.ts | 2 +- ts/textsecure/WebAPI.ts | 4 +- ts/textsecure/WebSocket.ts | 3 +- ts/textsecure/downloadAttachment.ts | 4 +- ts/textsecure/getKeysForServiceId.ts | 7 +- ts/textsecure/processDataMessage.ts | 4 +- ts/types/Attachment.ts | 1232 +---------------- ts/types/ForwardDraft.ts | 7 +- ts/types/GiftBadgeStates.ts | 9 + ts/types/HTTPError.ts | 45 + ts/types/Message2.ts | 2 +- ts/types/SafetyNumberChangeSource.ts | 9 + ts/types/WebAPI.d.ts | 4 + ts/types/errors.ts | 2 +- ts/util/Attachment.ts | 1183 ++++++++++++++++ ts/util/attachments.ts | 2 +- .../markAttachmentAsPermanentlyErrored.ts | 2 +- ts/util/avatarTextSizeCalculator.ts | 2 + .../blockSendUntilConversationsAreVerified.ts | 2 +- ts/util/createIdenticon.tsx | 1 + ts/util/deleteForMe.ts | 2 +- ts/util/downloadAttachment.ts | 8 +- ts/util/downloadAttachmentFromLocalBackup.ts | 2 +- ts/util/getDraftPreview.ts | 2 +- ts/util/getGroupMemberships.ts | 2 + ts/util/getNotificationDataForMessage.ts | 4 +- ts/util/getStoryDuration.ts | 2 +- ts/util/getStoryReplyText.ts | 2 +- ts/util/groupAndOrderReactions.ts | 3 + ts/util/handleEditMessage.ts | 2 +- ts/util/handleImageAttachment.ts | 2 +- ts/util/isCallSafe.ts | 2 +- ts/util/longRunningTaskWrapper.tsx | 3 + ts/util/lookupConversationWithoutServiceId.ts | 2 +- ts/util/makeQuote.ts | 2 +- ts/util/maybeForwardMessages.ts | 2 +- ts/util/queueAttachmentDownloads.ts | 2 +- ts/util/resolveDraftAttachmentOnDisk.ts | 2 +- ts/util/sendToGroup.ts | 2 +- ts/util/setupI18n.tsx | 1 + ts/util/showConfirmationDialog.tsx | 3 + ts/util/timelineUtil.ts | 3 + ts/util/uploadAttachment.ts | 2 +- ts/util/uploads/tusProtocol.ts | 2 +- ts/util/uploads/uploads.ts | 2 +- ts/util/writeDraftAttachment.ts | 2 +- 130 files changed, 1479 insertions(+), 1450 deletions(-) create mode 100644 ts/types/GiftBadgeStates.ts create mode 100644 ts/types/HTTPError.ts create mode 100644 ts/types/SafetyNumberChangeSource.ts create mode 100644 ts/types/WebAPI.d.ts create mode 100644 ts/util/Attachment.ts diff --git a/.eslintrc.js b/.eslintrc.js index d07da83d00..4cc0eb7d98 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -260,6 +260,18 @@ const typescriptRules = { // TODO: DESKTOP-4655 'import/no-cycle': 'off', + 'import/no-restricted-paths': [ + 'error', + { + zones: [ + { + target: ['ts/util', 'ts/types'], + from: ['ts/components', 'ts/axo'], + message: 'Importing components is forbidden from ts/{util,types}', + }, + ], + }, + ], }; const TAILWIND_REPLACEMENTS = [ diff --git a/ts/background.ts b/ts/background.ts index bdc9da2f2f..cdbd37c3e4 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -13,7 +13,7 @@ import type { SessionResetsType, ProcessedDataMessage, } from './textsecure/Types.d.ts'; -import { HTTPError } from './textsecure/Errors.js'; +import { HTTPError } from './types/HTTPError.js'; import createTaskWithTimeout, { suspendTasksWithTimeout, resumeTasksWithTimeout, diff --git a/ts/challenge.ts b/ts/challenge.ts index 1b6e103c0f..a5b1b91351 100644 --- a/ts/challenge.ts +++ b/ts/challenge.ts @@ -18,10 +18,8 @@ import { clearTimeoutIfNecessary } from './util/clearTimeoutIfNecessary.js'; import { missingCaseError } from './util/missingCaseError.js'; import type { StorageInterface } from './types/Storage.d.ts'; import * as Errors from './types/errors.js'; -import { - HTTPError, - type SendMessageChallengeData, -} from './textsecure/Errors.js'; +import { HTTPError } from './types/HTTPError.js'; +import type { SendMessageChallengeData } from './textsecure/Errors.js'; import { createLogger } from './logging/log.js'; import { drop } from './util/drop.js'; import { findRetryAfterTimeFromError } from './jobs/helpers/findRetryAfterTimeFromError.js'; diff --git a/ts/components/CompositionArea.tsx b/ts/components/CompositionArea.tsx index bb53eaad0f..6c7a0fd349 100644 --- a/ts/components/CompositionArea.tsx +++ b/ts/components/CompositionArea.tsx @@ -38,7 +38,7 @@ import type { AttachmentDraftType, InMemoryAttachmentDraftType, } from '../types/Attachment.js'; -import { isImageAttachment, isVoiceMessage } from '../types/Attachment.js'; +import { isImageAttachment, isVoiceMessage } from '../util/Attachment.js'; import type { AciString } from '../types/ServiceId.js'; import { AudioCapture } from './conversation/AudioCapture.js'; import { CompositionUpload } from './CompositionUpload.js'; diff --git a/ts/components/CompositionUpload.tsx b/ts/components/CompositionUpload.tsx index 73c6975263..cfcb45ebb9 100644 --- a/ts/components/CompositionUpload.tsx +++ b/ts/components/CompositionUpload.tsx @@ -5,7 +5,7 @@ import type { ChangeEventHandler } from 'react'; import React, { forwardRef } from 'react'; import type { AttachmentDraftType } from '../types/Attachment.js'; -import { isVideoAttachment, isImageAttachment } from '../types/Attachment.js'; +import { isVideoAttachment, isImageAttachment } from '../util/Attachment.js'; import type { LocalizerType } from '../types/Util.js'; import { diff --git a/ts/components/FileThumbnail.tsx b/ts/components/FileThumbnail.tsx index fa50313d56..db56e29c52 100644 --- a/ts/components/FileThumbnail.tsx +++ b/ts/components/FileThumbnail.tsx @@ -3,7 +3,7 @@ import React from 'react'; -import { getExtensionForDisplay } from '../types/Attachment.js'; +import { getExtensionForDisplay } from '../util/Attachment.js'; import { isFileDangerous } from '../util/isFileDangerous.js'; import { tw } from '../axo/tw.js'; diff --git a/ts/components/Lightbox.tsx b/ts/components/Lightbox.tsx index 3444029fdf..8e1b95b186 100644 --- a/ts/components/Lightbox.tsx +++ b/ts/components/Lightbox.tsx @@ -22,7 +22,7 @@ import { Avatar, AvatarSize } from './Avatar.js'; import { IMAGE_PNG, isImage, isVideo } from '../types/MIME.js'; import { formatDateTimeForAttachment } from '../util/timestamp.js'; import { formatDuration } from '../util/formatDuration.js'; -import { isGIF, isIncremental } from '../types/Attachment.js'; +import { isGIF, isIncremental } from '../util/Attachment.js'; import { useRestoreFocus } from '../hooks/useRestoreFocus.js'; import { usePrevious } from '../hooks/usePrevious.js'; import { arrow } from '../util/keyboard.js'; diff --git a/ts/components/SafetyNumberChangeDialog.tsx b/ts/components/SafetyNumberChangeDialog.tsx index b377bf026a..2a1a55576f 100644 --- a/ts/components/SafetyNumberChangeDialog.tsx +++ b/ts/components/SafetyNumberChangeDialog.tsx @@ -27,13 +27,6 @@ import { UserText } from './UserText.js'; const { noop } = lodash; -export enum SafetyNumberChangeSource { - InitiateCall = 'InitiateCall', - JoinCall = 'JoinCall', - MessageSend = 'MessageSend', - Story = 'Story', -} - enum DialogState { StartingInReview = 'StartingInReview', ExplicitReviewNeeded = 'ExplicitReviewNeeded', diff --git a/ts/components/StoryCreator.tsx b/ts/components/StoryCreator.tsx index efd299b91c..ddad5e93f4 100644 --- a/ts/components/StoryCreator.tsx +++ b/ts/components/StoryCreator.tsx @@ -17,7 +17,7 @@ import type { PropsType as TextStoryCreatorPropsType } from './TextStoryCreator. import type { PropsType as MediaEditorPropsType } from './MediaEditor.js'; import { TEXT_ATTACHMENT } from '../types/MIME.js'; -import { isVideoAttachment } from '../types/Attachment.js'; +import { isVideoAttachment } from '../util/Attachment.js'; import { SendStoryModal } from './SendStoryModal.js'; import { MediaEditor } from './MediaEditor.js'; diff --git a/ts/components/StoryImage.tsx b/ts/components/StoryImage.tsx index e49506923a..587f4bcbe6 100644 --- a/ts/components/StoryImage.tsx +++ b/ts/components/StoryImage.tsx @@ -18,7 +18,7 @@ import { isDownloaded, isDownloading, isGIF, -} from '../types/Attachment.js'; +} from '../util/Attachment.js'; import { getClassNamesFor } from '../util/getClassNamesFor.js'; import { isVideoTypeSupported } from '../util/GoogleChrome.js'; import { createLogger } from '../logging/log.js'; diff --git a/ts/components/StoryLinkPreview.tsx b/ts/components/StoryLinkPreview.tsx index 04430db65d..aab628de29 100644 --- a/ts/components/StoryLinkPreview.tsx +++ b/ts/components/StoryLinkPreview.tsx @@ -8,7 +8,7 @@ import lodash from 'lodash'; import type { LinkPreviewForUIType } from '../types/message/LinkPreviews.js'; import type { LocalizerType } from '../types/Util.js'; import { CurveType, Image } from './conversation/Image.js'; -import { isImageAttachment } from '../types/Attachment.js'; +import { isImageAttachment } from '../util/Attachment.js'; import { getSafeDomain } from '../types/LinkPreview.js'; const { unescape } = lodash; diff --git a/ts/components/StoryViewer.tsx b/ts/components/StoryViewer.tsx index 88e09bcb05..16e751e486 100644 --- a/ts/components/StoryViewer.tsx +++ b/ts/components/StoryViewer.tsx @@ -43,7 +43,7 @@ import { ToastType } from '../types/Toast.js'; import { getAvatarColor } from '../types/Colors.js'; import { getStoryBackground } from '../util/getStoryBackground.js'; import { getStoryDuration } from '../util/getStoryDuration.js'; -import { isVideoAttachment } from '../types/Attachment.js'; +import { isVideoAttachment } from '../util/Attachment.js'; import { graphemeAndLinkAwareSlice } from '../util/graphemeAndLinkAwareSlice.js'; import { useEscapeHandling } from '../hooks/useEscapeHandling.js'; import { useRetryStorySend } from '../hooks/useRetryStorySend.js'; diff --git a/ts/components/conversation/AttachmentList.tsx b/ts/components/conversation/AttachmentList.tsx index 37835b30dc..8fd57d2dcf 100644 --- a/ts/components/conversation/AttachmentList.tsx +++ b/ts/components/conversation/AttachmentList.tsx @@ -16,7 +16,7 @@ import { canDisplayImage, isImageAttachment, isVideoAttachment, -} from '../../types/Attachment.js'; +} from '../../util/Attachment.js'; export type Props = Readonly<{ diff --git a/ts/components/conversation/ConversationView.tsx b/ts/components/conversation/ConversationView.tsx index bf6a14ec90..4570090565 100644 --- a/ts/components/conversation/ConversationView.tsx +++ b/ts/components/conversation/ConversationView.tsx @@ -4,7 +4,7 @@ import React from 'react'; import classNames from 'classnames'; import { useEscapeHandling } from '../../hooks/useEscapeHandling.js'; -import { getSuggestedFilename } from '../../types/Attachment.js'; +import { getSuggestedFilename } from '../../util/Attachment.js'; import { IMAGE_PNG, type MIMEType } from '../../types/MIME.js'; export type PropsType = { diff --git a/ts/components/conversation/GIF.tsx b/ts/components/conversation/GIF.tsx index 25d97c3621..5a177d2595 100644 --- a/ts/components/conversation/GIF.tsx +++ b/ts/components/conversation/GIF.tsx @@ -13,7 +13,7 @@ import { getImageDimensionsForTimeline, defaultBlurHash, isDownloadable, -} from '../../types/Attachment.js'; +} from '../../util/Attachment.js'; import * as Errors from '../../types/errors.js'; import { createLogger } from '../../logging/log.js'; import { useReducedMotion } from '../../hooks/useReducedMotion.js'; diff --git a/ts/components/conversation/Image.tsx b/ts/components/conversation/Image.tsx index aad91126cd..d7bf9fbebd 100644 --- a/ts/components/conversation/Image.tsx +++ b/ts/components/conversation/Image.tsx @@ -12,7 +12,7 @@ import { defaultBlurHash, isIncremental, isReadyToView, -} from '../../types/Attachment.js'; +} from '../../util/Attachment.js'; import { SpinnerV2 } from '../SpinnerV2.js'; import { useUndownloadableMediaHandler } from '../../hooks/useUndownloadableMediaHandler.js'; diff --git a/ts/components/conversation/ImageGrid.stories.tsx b/ts/components/conversation/ImageGrid.stories.tsx index 4f2ac04f7d..37a772f83d 100644 --- a/ts/components/conversation/ImageGrid.stories.tsx +++ b/ts/components/conversation/ImageGrid.stories.tsx @@ -17,7 +17,7 @@ import { import { pngUrl, squareStickerUrl } from '../../storybook/Fixtures.js'; import { fakeAttachment } from '../../test-helpers/fakeAttachment.js'; import { strictAssert } from '../../util/assert.js'; -import { isDownloadable } from '../../types/Attachment.js'; +import { isDownloadable } from '../../util/Attachment.js'; const { i18n } = window.SignalContext; diff --git a/ts/components/conversation/ImageGrid.tsx b/ts/components/conversation/ImageGrid.tsx index e03d26857c..a5090f0438 100644 --- a/ts/components/conversation/ImageGrid.tsx +++ b/ts/components/conversation/ImageGrid.tsx @@ -17,7 +17,7 @@ import { isDownloadable, isIncremental, isVideoAttachment, -} from '../../types/Attachment.js'; +} from '../../util/Attachment.js'; import { Image, CurveType } from './Image.js'; diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index ae48d7d06b..2862c1acc9 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -53,6 +53,7 @@ import type { WidthBreakpoint } from '../_util.js'; import { OutgoingGiftBadgeModal } from '../OutgoingGiftBadgeModal.js'; import { createLogger } from '../../logging/log.js'; import { StoryViewModeType } from '../../types/Stories.js'; +import { GiftBadgeStates } from '../../types/GiftBadgeStates.js'; import type { AttachmentForUIType, AttachmentType, @@ -71,7 +72,7 @@ import { isImageAttachment, isPlayed, isVideo, -} from '../../types/Attachment.js'; +} from '../../util/Attachment.js'; import type { EmbeddedContactForUIType } from '../../types/EmbeddedContact.js'; import { getIncrement } from '../../util/timer.js'; @@ -208,13 +209,6 @@ export type AudioAttachmentProps = { onCorrupted(): void; }; -export enum GiftBadgeStates { - Unopened = 'Unopened', - Opened = 'Opened', - Redeemed = 'Redeemed', - Failed = 'Failed', -} - export type GiftBadgeType = | { state: diff --git a/ts/components/conversation/MessageAudio.tsx b/ts/components/conversation/MessageAudio.tsx index 99f52ff4bd..20ab524403 100644 --- a/ts/components/conversation/MessageAudio.tsx +++ b/ts/components/conversation/MessageAudio.tsx @@ -11,7 +11,7 @@ import type { LocalizerType } from '../../types/Util.js'; import type { AttachmentForUIType } from '../../types/Attachment.js'; import type { MessageStatusType } from '../../types/message/MessageStatus.js'; import type { PushPanelForConversationActionType } from '../../state/ducks/conversations.js'; -import { isDownloaded } from '../../types/Attachment.js'; +import { isDownloaded } from '../../util/Attachment.js'; import type { DirectionType } from './Message.js'; import type { ComputePeaksResult } from '../VoiceNotesPlaybackContext.js'; diff --git a/ts/components/conversation/MessageBody.tsx b/ts/components/conversation/MessageBody.tsx index d8cd971dc6..ff1db00e07 100644 --- a/ts/components/conversation/MessageBody.tsx +++ b/ts/components/conversation/MessageBody.tsx @@ -4,7 +4,7 @@ import type { KeyboardEvent } from 'react'; import React from 'react'; import type { AttachmentType } from '../../types/Attachment.js'; -import { canBeDownloaded, isDownloaded } from '../../types/Attachment.js'; +import { canBeDownloaded, isDownloaded } from '../../util/Attachment.js'; import type { ShowConversationType } from '../../state/ducks/conversations.js'; import type { HydratedBodyRangesType } from '../../types/BodyRange.js'; import type { LocalizerType } from '../../types/Util.js'; diff --git a/ts/components/conversation/StagedLinkPreview.tsx b/ts/components/conversation/StagedLinkPreview.tsx index 0346a56677..6ab03ff9d7 100644 --- a/ts/components/conversation/StagedLinkPreview.tsx +++ b/ts/components/conversation/StagedLinkPreview.tsx @@ -11,7 +11,7 @@ import { LinkPreviewDate } from './LinkPreviewDate.js'; import type { LinkPreviewForUIType } from '../../types/message/LinkPreviews.js'; import type { LocalizerType } from '../../types/Util.js'; import { getClassNamesFor } from '../../util/getClassNamesFor.js'; -import { isImageAttachment } from '../../types/Attachment.js'; +import { isImageAttachment } from '../../util/Attachment.js'; import { isCallLink } from '../../types/LinkPreview.js'; import { Avatar } from '../Avatar.js'; import { getColorForCallLink } from '../../util/getColorForCallLink.js'; diff --git a/ts/components/conversation/TimelineMessage.stories.tsx b/ts/components/conversation/TimelineMessage.stories.tsx index 311454e70e..65deff7698 100644 --- a/ts/components/conversation/TimelineMessage.stories.tsx +++ b/ts/components/conversation/TimelineMessage.stories.tsx @@ -13,7 +13,7 @@ import { EmojiPicker } from '../emoji/EmojiPicker.js'; import type { AudioAttachmentProps } from './Message.js'; import type { Props } from './TimelineMessage.js'; import { TimelineMessage } from './TimelineMessage.js'; -import { GiftBadgeStates, TextDirection } from './Message.js'; +import { TextDirection } from './Message.js'; import { AUDIO_MP3, IMAGE_JPEG, @@ -33,6 +33,7 @@ import { getDefaultConversation } from '../../test-helpers/getDefaultConversatio import { WidthBreakpoint } from '../_util.js'; import { DAY, HOUR, MINUTE, SECOND } from '../../util/durations/index.js'; import { ContactFormType } from '../../types/EmbeddedContact.js'; +import { GiftBadgeStates } from '../../types/GiftBadgeStates.js'; import { generateAci } from '../../types/ServiceId.js'; import { diff --git a/ts/components/conversation/TimelineMessage.tsx b/ts/components/conversation/TimelineMessage.tsx index 52b4fad412..d980388b14 100644 --- a/ts/components/conversation/TimelineMessage.tsx +++ b/ts/components/conversation/TimelineMessage.tsx @@ -15,7 +15,7 @@ import { ContextMenuTrigger } from 'react-contextmenu'; import { createPortal } from 'react-dom'; import { Manager, Popper, Reference } from 'react-popper'; import type { PreventOverflowModifier } from '@popperjs/core/lib/modifiers/preventOverflow.js'; -import { isDownloaded } from '../../types/Attachment.js'; +import { isDownloaded } from '../../util/Attachment.js'; import type { LocalizerType } from '../../types/I18N.js'; import { handleOutsideClick } from '../../util/handleOutsideClick.js'; import { offsetDistanceModifier } from '../../util/popperUtil.js'; diff --git a/ts/components/conversation/media-gallery/MediaGridItem.tsx b/ts/components/conversation/media-gallery/MediaGridItem.tsx index c2fc83c10b..e21e4036a1 100644 --- a/ts/components/conversation/media-gallery/MediaGridItem.tsx +++ b/ts/components/conversation/media-gallery/MediaGridItem.tsx @@ -16,7 +16,7 @@ import { defaultBlurHash, isGIF, isVideoAttachment, -} from '../../../types/Attachment.js'; +} from '../../../util/Attachment.js'; import { ImageOrBlurhash } from '../../ImageOrBlurhash.js'; import { SpinnerV2 } from '../../SpinnerV2.js'; import { tw } from '../../../axo/tw.js'; diff --git a/ts/groups.ts b/ts/groups.ts index d410639b50..59bdd2d0e3 100644 --- a/ts/groups.ts +++ b/ts/groups.ts @@ -54,7 +54,7 @@ import type { GroupCredentialsType, GroupLogResponseType, } from './textsecure/WebAPI.js'; -import { HTTPError } from './textsecure/Errors.js'; +import { HTTPError } from './types/HTTPError.js'; import type MessageSender from './textsecure/SendMessage.js'; import { CURRENT_SCHEMA_VERSION as MAX_MESSAGE_SCHEMA } from './types/Message2.js'; import type { ConversationModel } from './models/conversations.js'; diff --git a/ts/groups/joinViaLink.ts b/ts/groups/joinViaLink.ts index 93684324a6..1a0eae75cb 100644 --- a/ts/groups/joinViaLink.ts +++ b/ts/groups/joinViaLink.ts @@ -11,7 +11,7 @@ import { DataWriter } from '../sql/Client.js'; import * as Bytes from '../Bytes.js'; import * as Errors from '../types/errors.js'; import { createLogger } from '../logging/log.js'; -import { HTTPError } from '../textsecure/Errors.js'; +import { HTTPError } from '../types/HTTPError.js'; import { SignalService as Proto } from '../protobuf/index.js'; import type { ContactAvatarType } from '../types/Avatar.js'; import { ToastType } from '../types/Toast.js'; diff --git a/ts/hooks/useAttachmentStatus.ts b/ts/hooks/useAttachmentStatus.ts index 6056493c6e..be8face9d1 100644 --- a/ts/hooks/useAttachmentStatus.ts +++ b/ts/hooks/useAttachmentStatus.ts @@ -1,7 +1,8 @@ // Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import { getUrl, type AttachmentForUIType } from '../types/Attachment.js'; +import type { AttachmentForUIType } from '../types/Attachment.js'; +import { getUrl } from '../util/Attachment.js'; import { MediaTier } from '../types/AttachmentDownload.js'; import { missingCaseError } from '../util/missingCaseError.js'; import { getAttachmentCiphertextSize } from '../util/AttachmentCrypto.js'; diff --git a/ts/jobs/AttachmentBackupManager.ts b/ts/jobs/AttachmentBackupManager.ts index 8736a4c44e..e7617d5541 100644 --- a/ts/jobs/AttachmentBackupManager.ts +++ b/ts/jobs/AttachmentBackupManager.ts @@ -45,11 +45,11 @@ import { } from '../services/backups/util/mediaId.js'; import { fromBase64, toBase64 } from '../Bytes.js'; import type { WebAPIType } from '../textsecure/WebAPI.js'; +import type { AttachmentType } from '../types/Attachment.js'; import { - type AttachmentType, canAttachmentHaveThumbnail, mightStillBeOnTransitTier, -} from '../types/Attachment.js'; +} from '../util/Attachment.js'; import { type CreatedThumbnailType, makeImageThumbnailForBackup, diff --git a/ts/jobs/AttachmentDownloadManager.ts b/ts/jobs/AttachmentDownloadManager.ts index e7b316aec3..b9994db1de 100644 --- a/ts/jobs/AttachmentDownloadManager.ts +++ b/ts/jobs/AttachmentDownloadManager.ts @@ -27,13 +27,15 @@ import { type AttachmentType, AttachmentVariant, AttachmentPermanentlyUndownloadableError, +} from '../types/Attachment.js'; +import { wasImportedFromLocalBackup, canAttachmentHaveThumbnail, shouldAttachmentEndUpInRemoteBackup, getUndownloadedAttachmentSignature, isIncremental, hasRequiredInformationForBackup, -} from '../types/Attachment.js'; +} from '../util/Attachment.js'; import type { ReadonlyMessageAttributesType } from '../model-types.d.ts'; import { backupsService } from '../services/backups/index.js'; import { getMessageById } from '../messages/getMessageById.js'; @@ -69,7 +71,7 @@ import { formatCountForLogging } from '../logging/formatCountForLogging.js'; import { strictAssert } from '../util/assert.js'; import { getAttachmentCiphertextSize } from '../util/AttachmentCrypto.js'; import { updateBackupMediaDownloadProgress } from '../util/updateBackupMediaDownloadProgress.js'; -import { HTTPError } from '../textsecure/Errors.js'; +import { HTTPError } from '../types/HTTPError.js'; import { isOlderThan } from '../util/timestamp.js'; import { getMessageQueueTime as doGetMessageQueueTime } from '../util/getMessageQueueTime.js'; import { JobCancelReason } from './types.js'; diff --git a/ts/jobs/helpers/attachmentBackfill.ts b/ts/jobs/helpers/attachmentBackfill.ts index c6948218b2..803955a8c6 100644 --- a/ts/jobs/helpers/attachmentBackfill.ts +++ b/ts/jobs/helpers/attachmentBackfill.ts @@ -5,13 +5,13 @@ import type { AttachmentBackfillResponseSyncEvent } from '../../textsecure/messa import MessageSender from '../../textsecure/SendMessage.js'; import { createLogger } from '../../logging/log.js'; import type { ReadonlyMessageAttributesType } from '../../model-types.d.ts'; +import type { AttachmentType } from '../../types/Attachment.js'; import { - type AttachmentType, isDownloading, isDownloaded, isDownloadable, getUndownloadedAttachmentSignature, -} from '../../types/Attachment.js'; +} from '../../util/Attachment.js'; import { type MessageAttachmentType, AttachmentDownloadUrgency, diff --git a/ts/jobs/helpers/findRetryAfterTimeFromError.ts b/ts/jobs/helpers/findRetryAfterTimeFromError.ts index 650f24acec..e9edb6bace 100644 --- a/ts/jobs/helpers/findRetryAfterTimeFromError.ts +++ b/ts/jobs/helpers/findRetryAfterTimeFromError.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import { isRecord } from '../../util/isRecord.js'; -import { HTTPError } from '../../textsecure/Errors.js'; +import { HTTPError } from '../../types/HTTPError.js'; import { parseRetryAfterWithDefault } from '../../util/parseRetryAfter.js'; export function findRetryAfterTimeFromError( diff --git a/ts/jobs/reportSpamJobQueue.ts b/ts/jobs/reportSpamJobQueue.ts index 914ebebb01..0675b4f756 100644 --- a/ts/jobs/reportSpamJobQueue.ts +++ b/ts/jobs/reportSpamJobQueue.ts @@ -15,7 +15,7 @@ import { JobQueue } from './JobQueue.js'; import { jobQueueDatabaseStore } from './JobQueueDatabaseStore.js'; import { parseIntWithFallback } from '../util/parseIntWithFallback.js'; import type { WebAPIType } from '../textsecure/WebAPI.js'; -import { HTTPError } from '../textsecure/Errors.js'; +import { HTTPError } from '../types/HTTPError.js'; import { sleeper } from '../util/sleeper.js'; import { parseUnknown } from '../util/schemas.js'; diff --git a/ts/linkPreviews/shouldUseFullSizeLinkPreviewImage.ts b/ts/linkPreviews/shouldUseFullSizeLinkPreviewImage.ts index 85de97635c..bf5d1fc2c3 100644 --- a/ts/linkPreviews/shouldUseFullSizeLinkPreviewImage.ts +++ b/ts/linkPreviews/shouldUseFullSizeLinkPreviewImage.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import type { LinkPreviewType } from '../types/message/LinkPreviews.js'; -import { isImageAttachment } from '../types/Attachment.js'; +import { isImageAttachment } from '../util/Attachment.js'; const MINIMUM_FULL_SIZE_DIMENSION = 200; diff --git a/ts/messageModifiers/AttachmentDownloads.ts b/ts/messageModifiers/AttachmentDownloads.ts index a8d204e209..aa746805fd 100644 --- a/ts/messageModifiers/AttachmentDownloads.ts +++ b/ts/messageModifiers/AttachmentDownloads.ts @@ -9,7 +9,7 @@ import type { AttachmentType } from '../types/Attachment.js'; import { doAttachmentsOnSameMessageMatch, isDownloaded, -} from '../types/Attachment.js'; +} from '../util/Attachment.js'; import { getMessageById } from '../messages/getMessageById.js'; import { trimMessageWhitespace } from '../types/BodyRange.js'; diff --git a/ts/messageModifiers/ViewSyncs.ts b/ts/messageModifiers/ViewSyncs.ts index 6e1e640f53..bad8f19cb0 100644 --- a/ts/messageModifiers/ViewSyncs.ts +++ b/ts/messageModifiers/ViewSyncs.ts @@ -6,7 +6,7 @@ import { z } from 'zod'; import type { ReadonlyMessageAttributesType } from '../model-types.d.ts'; import * as Errors from '../types/errors.js'; import { createLogger } from '../logging/log.js'; -import { GiftBadgeStates } from '../components/conversation/Message.js'; +import { GiftBadgeStates } from '../types/GiftBadgeStates.js'; import { ReadStatus } from '../messages/MessageReadStatus.js'; import { getMessageIdForLogging } from '../util/idForLogging.js'; import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp.js'; diff --git a/ts/messages/copyQuote.ts b/ts/messages/copyQuote.ts index 3c3707c9be..74f53baec1 100644 --- a/ts/messages/copyQuote.ts +++ b/ts/messages/copyQuote.ts @@ -14,7 +14,7 @@ import { getQuoteBodyText } from '../util/getQuoteBodyText.js'; import { isQuoteAMatch, messageHasPaymentEvent } from './helpers.js'; import * as Errors from '../types/errors.js'; import type { MessageModel } from '../models/messages.js'; -import { isDownloadable } from '../types/Attachment.js'; +import { isDownloadable } from '../util/Attachment.js'; const { omit } = lodash; diff --git a/ts/messages/handleDataMessage.ts b/ts/messages/handleDataMessage.ts index cb3020efdd..0e0dbf4b20 100644 --- a/ts/messages/handleDataMessage.ts +++ b/ts/messages/handleDataMessage.ts @@ -49,7 +49,7 @@ import { isMessageEmpty } from '../util/isMessageEmpty.js'; import { isValidTapToView } from '../util/isValidTapToView.js'; import { getNotificationTextForMessage } from '../util/getNotificationTextForMessage.js'; import { getMessageAuthorText } from '../util/getMessageAuthorText.js'; -import { GiftBadgeStates } from '../components/conversation/Message.js'; +import { GiftBadgeStates } from '../types/GiftBadgeStates.js'; import { parseBoostBadgeListFromServer } from '../badges/parseBadgesFromServer.js'; import { SignalService as Proto } from '../protobuf/index.js'; import { diff --git a/ts/messages/helpers.ts b/ts/messages/helpers.ts index 03941cc626..fdad29da85 100644 --- a/ts/messages/helpers.ts +++ b/ts/messages/helpers.ts @@ -16,7 +16,7 @@ import { PaymentEventKind } from '../types/Payment.js'; import type { AnyPaymentEvent } from '../types/Payment.js'; import type { LocalizerType } from '../types/Util.js'; import { missingCaseError } from '../util/missingCaseError.js'; -import { isDownloaded } from '../types/Attachment.js'; +import { isDownloaded } from '../util/Attachment.js'; const log = createLogger('helpers'); diff --git a/ts/model-types.d.ts b/ts/model-types.d.ts index d785b7c8b3..eaca52ca8c 100644 --- a/ts/model-types.d.ts +++ b/ts/model-types.d.ts @@ -13,10 +13,7 @@ import type { ReadStatus } from './messages/MessageReadStatus.js'; import type { SendStateByConversationId } from './messages/MessageSendState.js'; import type { GroupNameCollisionsWithIdsByTitle } from './util/groupMemberNameCollisions.js'; -import type { - AttachmentDraftType, - AttachmentType, -} from './types/Attachment.js'; +import type { AttachmentDraftType, AttachmentType } from './util/Attachment.js'; import type { EmbeddedContactType } from './types/EmbeddedContact.js'; import { SignalService as Proto } from './protobuf/index.js'; import type { AvatarDataType, ContactAvatarType } from './types/Avatar.js'; @@ -27,7 +24,7 @@ import type { } from './types/ServiceId.js'; import type { StoryDistributionIdString } from './types/StoryDistributionId.js'; import type { SeenStatus } from './MessageSeenStatus.js'; -import type { GiftBadgeStates } from './components/conversation/Message.js'; +import type { GiftBadgeStates } from './types/GiftBadgeStates.js'; import type { LinkPreviewType } from './types/message/LinkPreviews.js'; import type { StickerType } from './types/Stickers.js'; diff --git a/ts/services/backups/api.ts b/ts/services/backups/api.ts index de12b429e9..5db49b267c 100644 --- a/ts/services/backups/api.ts +++ b/ts/services/backups/api.ts @@ -21,7 +21,7 @@ import { type SubscriptionCostType, } from '../../types/backups.js'; import { uploadFile } from '../../util/uploadAttachment.js'; -import { HTTPError } from '../../textsecure/Errors.js'; +import { HTTPError } from '../../types/HTTPError.js'; import { createLogger } from '../../logging/log.js'; import { toLogFormat } from '../../types/errors.js'; diff --git a/ts/services/backups/credentials.ts b/ts/services/backups/credentials.ts index f7ce0227ac..e91526b352 100644 --- a/ts/services/backups/credentials.ts +++ b/ts/services/backups/credentials.ts @@ -33,7 +33,7 @@ import { BackupCredentialType, } from '../../types/backups.js'; import { toLogFormat } from '../../types/errors.js'; -import { HTTPError } from '../../textsecure/Errors.js'; +import { HTTPError } from '../../types/HTTPError.js'; import type { GetBackupCredentialsResponseType, GetBackupCDNCredentialsResponseType, diff --git a/ts/services/backups/export.ts b/ts/services/backups/export.ts index 521f84627e..e7805a71a4 100644 --- a/ts/services/backups/export.ts +++ b/ts/services/backups/export.ts @@ -23,7 +23,7 @@ import type { IdentityKeyType, } from '../../sql/Interface.js'; import { createLogger } from '../../logging/log.js'; -import { GiftBadgeStates } from '../../components/conversation/Message.js'; +import { GiftBadgeStates } from '../../types/GiftBadgeStates.js'; import { type CustomColorType } from '../../types/Colors.js'; import { StorySendMode, MY_STORY_ID } from '../../types/Stories.js'; import { getStickerPacksForBackup } from '../../types/Stickers.js'; @@ -125,12 +125,12 @@ import { numberToPhoneType, } from '../../types/EmbeddedContact.js'; import { toLogFormat } from '../../types/errors.js'; +import type { AttachmentType } from '../../types/Attachment.js'; import { - type AttachmentType, isGIF, isDownloaded, hasRequiredInformationForBackup, -} from '../../types/Attachment.js'; +} from '../../util/Attachment.js'; import { getFilePointerForAttachment } from './util/filePointers.js'; import { getBackupMediaRootKey } from './crypto.js'; import type { diff --git a/ts/services/backups/import.ts b/ts/services/backups/import.ts index 7ea07ce677..7f15e04cf0 100644 --- a/ts/services/backups/import.ts +++ b/ts/services/backups/import.ts @@ -21,7 +21,7 @@ import { type IdentityKeyType, } from '../../sql/Interface.js'; import { createLogger } from '../../logging/log.js'; -import { GiftBadgeStates } from '../../components/conversation/Message.js'; +import { GiftBadgeStates } from '../../types/GiftBadgeStates.js'; import { StorySendMode, MY_STORY_ID } from '../../types/Stories.js'; import type { AciString, ServiceIdString } from '../../types/ServiceId.js'; import * as LinkPreview from '../../types/LinkPreview.js'; diff --git a/ts/services/backups/index.ts b/ts/services/backups/index.ts index 4d1bc69580..0a91af1d59 100644 --- a/ts/services/backups/index.ts +++ b/ts/services/backups/index.ts @@ -42,7 +42,7 @@ import { type BackupsSubscriptionType, type BackupStatusType, } from '../../types/backups.js'; -import { HTTPError } from '../../textsecure/Errors.js'; +import { HTTPError } from '../../types/HTTPError.js'; import { constantTimeEqual } from '../../Crypto.js'; import { measureSize } from '../../AttachmentCrypto.js'; import { isTestOrMockEnvironment } from '../../environment.js'; diff --git a/ts/services/backups/util/filePointers.ts b/ts/services/backups/util/filePointers.ts index 0bf5fc3cca..fce39b680b 100644 --- a/ts/services/backups/util/filePointers.ts +++ b/ts/services/backups/util/filePointers.ts @@ -8,11 +8,11 @@ import { stringToMIMEType, } from '../../../types/MIME.js'; import { createLogger } from '../../../logging/log.js'; +import type { AttachmentType } from '../../../types/Attachment.js'; import { - type AttachmentType, hasRequiredInformationForBackup, hasRequiredInformationToDownloadFromTransitTier, -} from '../../../types/Attachment.js'; +} from '../../../util/Attachment.js'; import { Backups, SignalService } from '../../../protobuf/index.js'; import * as Bytes from '../../../Bytes.js'; import { diff --git a/ts/services/backups/util/mediaId.ts b/ts/services/backups/util/mediaId.ts index b977c37b0b..426b95a0fd 100644 --- a/ts/services/backups/util/mediaId.ts +++ b/ts/services/backups/util/mediaId.ts @@ -4,7 +4,7 @@ import { DataReader } from '../../../sql/Client.js'; import * as Bytes from '../../../Bytes.js'; import { getBackupMediaRootKey } from '../crypto.js'; -import { type BackupableAttachmentType } from '../../../types/Attachment.js'; +import type { BackupableAttachmentType } from '../../../types/Attachment.js'; export function getMediaIdFromMediaName(mediaName: string): { string: string; diff --git a/ts/services/profiles.ts b/ts/services/profiles.ts index 846bbcadb3..7a8f736674 100644 --- a/ts/services/profiles.ts +++ b/ts/services/profiles.ts @@ -33,7 +33,7 @@ import { drop } from '../util/drop.js'; import { findRetryAfterTimeFromError } from '../jobs/helpers/findRetryAfterTimeFromError.js'; import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue.js'; import { SEALED_SENDER } from '../types/SealedSender.js'; -import { HTTPError } from '../textsecure/Errors.js'; +import { HTTPError } from '../types/HTTPError.js'; import { Address } from '../types/Address.js'; import { QualifiedAddress } from '../types/QualifiedAddress.js'; import { trimForDisplay, verifyAccessKey, decryptProfile } from '../Crypto.js'; diff --git a/ts/services/releaseNotesFetcher.ts b/ts/services/releaseNotesFetcher.ts index 8a4da63074..c358be1665 100644 --- a/ts/services/releaseNotesFetcher.ts +++ b/ts/services/releaseNotesFetcher.ts @@ -9,7 +9,7 @@ import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary.js'; import * as Registration from '../util/registration.js'; import { createLogger } from '../logging/log.js'; import * as Errors from '../types/errors.js'; -import { HTTPError } from '../textsecure/Errors.js'; +import { HTTPError } from '../types/HTTPError.js'; import { drop } from '../util/drop.js'; import { strictAssert } from '../util/assert.js'; import type { MessageAttributesType } from '../model-types.js'; diff --git a/ts/services/username.ts b/ts/services/username.ts index bb3f2be50e..4b471b6a22 100644 --- a/ts/services/username.ts +++ b/ts/services/username.ts @@ -23,7 +23,7 @@ import { import * as Errors from '../types/errors.js'; import { createLogger } from '../logging/log.js'; import MessageSender from '../textsecure/SendMessage.js'; -import { HTTPError } from '../textsecure/Errors.js'; +import { HTTPError } from '../types/HTTPError.js'; import { findRetryAfterTimeFromError } from '../jobs/helpers/findRetryAfterTimeFromError.js'; import * as Bytes from '../Bytes.js'; import { storageServiceUploadJob } from './storage.js'; diff --git a/ts/signal.ts b/ts/signal.ts index 1443bceb33..fc4e1cf63d 100644 --- a/ts/signal.ts +++ b/ts/signal.ts @@ -10,7 +10,7 @@ import { isProduction } from './util/version.js'; import { DataReader, DataWriter } from './sql/Client.js'; // Types -import * as TypesAttachment from './types/Attachment.js'; +import * as TypesAttachment from './util/Attachment.js'; import * as VisualAttachment from './types/VisualAttachment.js'; import * as MessageType from './types/Message2.js'; import { Address } from './types/Address.js'; diff --git a/ts/sql/Server.ts b/ts/sql/Server.ts index 97291003cc..8aeb0309bc 100644 --- a/ts/sql/Server.ts +++ b/ts/sql/Server.ts @@ -255,11 +255,8 @@ import { INITIAL_EXPIRE_TIMER_VERSION } from '../util/expirationTimer.js'; import type { GifType } from '../components/fun/panels/FunPanelGifs.js'; import type { NotificationProfileType } from '../types/NotificationProfile.js'; import * as durations from '../util/durations/index.js'; -import { - isFile, - isVisualMedia, - type AttachmentType, -} from '../types/Attachment.js'; +import type { AttachmentType } from '../types/Attachment.js'; +import { isFile, isVisualMedia } from '../util/Attachment.js'; import { generateMessageId } from '../util/generateMessageId.js'; import type { ConversationColorType, diff --git a/ts/sql/hydration.ts b/ts/sql/hydration.ts index a396b430fe..14b955a11b 100644 --- a/ts/sql/hydration.ts +++ b/ts/sql/hydration.ts @@ -23,7 +23,7 @@ import { sql, sqlJoin, } from './util.js'; -import { type AttachmentType } from '../types/Attachment.js'; +import type { AttachmentType } from '../types/Attachment.js'; import { APPLICATION_OCTET_STREAM, IMAGE_JPEG, diff --git a/ts/state/ducks/audioPlayer.ts b/ts/state/ducks/audioPlayer.ts index da0fcb0efe..53f1499504 100644 --- a/ts/state/ducks/audioPlayer.ts +++ b/ts/state/ducks/audioPlayer.ts @@ -22,7 +22,7 @@ import type { ConversationsUpdatedActionType, } from './conversations.js'; import { createLogger } from '../../logging/log.js'; -import { isAudio } from '../../types/Attachment.js'; +import { isAudio } from '../../util/Attachment.js'; import { getLocalAttachmentUrl } from '../../util/getLocalAttachmentUrl.js'; import { assertDev } from '../../util/assert.js'; diff --git a/ts/state/ducks/calling.ts b/ts/state/ducks/calling.ts index be4a4d658f..cb23ffc0b2 100644 --- a/ts/state/ducks/calling.ts +++ b/ts/state/ducks/calling.ts @@ -84,7 +84,7 @@ import { isAnybodyInGroupCall, MAX_CALL_PARTICIPANTS_FOR_DEFAULT_MUTE, } from './callingHelpers.js'; -import { SafetyNumberChangeSource } from '../../components/SafetyNumberChangeDialog.js'; +import { SafetyNumberChangeSource } from '../../types/SafetyNumberChangeSource.js'; import { isGroupOrAdhocCallMode, isGroupOrAdhocCallState, diff --git a/ts/state/ducks/composer.ts b/ts/state/ducks/composer.ts index 26f8c4c00e..c7e136f026 100644 --- a/ts/state/ducks/composer.ts +++ b/ts/state/ducks/composer.ts @@ -11,13 +11,12 @@ import type { AddLinkPreviewActionType, RemoveLinkPreviewActionType, } from './linkPreviews.js'; -import { - type AttachmentType, - type AttachmentDraftType, - type InMemoryAttachmentDraftType, - isVideoAttachment, - isImageAttachment, +import type { + AttachmentType, + AttachmentDraftType, + InMemoryAttachmentDraftType, } from '../../types/Attachment.js'; +import { isVideoAttachment, isImageAttachment } from '../../util/Attachment.js'; import { DataReader, DataWriter } from '../../sql/Client.js'; import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions.js'; import type { DraftBodyRanges } from '../../types/BodyRange.js'; @@ -38,7 +37,7 @@ import { completeRecording, getIsRecording } from './audioRecorder.js'; import { SHOW_TOAST } from './toast.js'; import type { AnyToast } from '../../types/Toast.js'; import { ToastType } from '../../types/Toast.js'; -import { SafetyNumberChangeSource } from '../../components/SafetyNumberChangeDialog.js'; +import { SafetyNumberChangeSource } from '../../types/SafetyNumberChangeSource.js'; import { assignWithNoUnnecessaryAllocation } from '../../util/assignWithNoUnnecessaryAllocation.js'; import { blockSendUntilConversationsAreVerified } from '../../util/blockSendUntilConversationsAreVerified.js'; import { clearConversationDraftAttachments } from '../../util/clearConversationDraftAttachments.js'; diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 1ded628f29..897cb9f050 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -19,7 +19,7 @@ import { assertDev, strictAssert } from '../../util/assert.js'; import { drop } from '../../util/drop.js'; import type { DurationInSeconds } from '../../util/durations/index.js'; import * as universalExpireTimer from '../../util/universalExpireTimer.js'; -import * as Attachment from '../../types/Attachment.js'; +import * as Attachment from '../../util/Attachment.js'; import type { LocalizerType } from '../../types/I18N.js'; import { AttachmentDownloadUrgency } from '../../types/AttachmentDownload.js'; import { isFileDangerous } from '../../util/isFileDangerous.js'; diff --git a/ts/state/ducks/globalModals.ts b/ts/state/ducks/globalModals.ts index 2c09a8bece..a361dbee92 100644 --- a/ts/state/ducks/globalModals.ts +++ b/ts/state/ducks/globalModals.ts @@ -16,7 +16,7 @@ import type { } from './conversations.js'; import type { MessagePropsType } from '../selectors/message.js'; import type { RecipientsByConversation } from './stories.js'; -import type { SafetyNumberChangeSource } from '../../components/SafetyNumberChangeDialog.js'; +import type { SafetyNumberChangeSource } from '../../types/SafetyNumberChangeSource.js'; import type { StateType as RootStateType } from '../reducer.js'; import * as SingleServePromise from '../../services/singleServePromise.js'; import * as Stickers from '../../types/Stickers.js'; @@ -39,7 +39,7 @@ import { MESSAGE_EXPIRED, actions as conversationsActions, } from './conversations.js'; -import { isDownloaded } from '../../types/Attachment.js'; +import { isDownloaded } from '../../util/Attachment.js'; import { isPermanentlyUndownloadable } from '../../jobs/AttachmentDownloadManager.js'; import type { ButtonVariant } from '../../components/Button.js'; import type { MessageRequestState } from '../../components/conversation/MessageRequestActionsConfirmation.js'; diff --git a/ts/state/ducks/installer.ts b/ts/state/ducks/installer.ts index 26274fcdff..813d214e04 100644 --- a/ts/state/ducks/installer.ts +++ b/ts/state/ducks/installer.ts @@ -18,7 +18,7 @@ import { isRecord } from '../../util/isRecord.js'; import { strictAssert } from '../../util/assert.js'; import * as Registration from '../../util/registration.js'; import { missingCaseError } from '../../util/missingCaseError.js'; -import { HTTPError } from '../../textsecure/Errors.js'; +import { HTTPError } from '../../types/HTTPError.js'; import { Provisioner, EventKind as ProvisionEventKind, diff --git a/ts/state/ducks/lightbox.ts b/ts/state/ducks/lightbox.ts index 92c264805e..36a9d55f9a 100644 --- a/ts/state/ducks/lightbox.ts +++ b/ts/state/ducks/lightbox.ts @@ -22,7 +22,7 @@ import type { ReadonlyMessageAttributesType } from '../../model-types.d.ts'; import { getUndownloadedAttachmentSignature, isIncremental, -} from '../../types/Attachment.js'; +} from '../../util/Attachment.js'; import { isImageTypeSupported, isVideoTypeSupported, diff --git a/ts/state/ducks/mediaGallery.ts b/ts/state/ducks/mediaGallery.ts index 7f46b7fde1..8e3ee28d68 100644 --- a/ts/state/ducks/mediaGallery.ts +++ b/ts/state/ducks/mediaGallery.ts @@ -24,7 +24,7 @@ import type { MessageExpiredActionType, } from './conversations.js'; import type { MediaItemType } from '../../types/MediaItem.js'; -import { isFile, isVisualMedia } from '../../types/Attachment.js'; +import { isFile, isVisualMedia } from '../../util/Attachment.js'; import type { StateType as RootStateType } from '../reducer.js'; import { getPropsForAttachment } from '../selectors/message.js'; diff --git a/ts/state/ducks/stories.ts b/ts/state/ducks/stories.ts index 4b274970a0..2ded7fd193 100644 --- a/ts/state/ducks/stories.ts +++ b/ts/state/ducks/stories.ts @@ -31,7 +31,7 @@ import { SIGNAL_ACI } from '../../types/SignalConversation.js'; import { DataReader, DataWriter } from '../../sql/Client.js'; import { ReadStatus } from '../../messages/MessageReadStatus.js'; import { SendStatus } from '../../messages/MessageSendState.js'; -import { SafetyNumberChangeSource } from '../../components/SafetyNumberChangeDialog.js'; +import { SafetyNumberChangeSource } from '../../types/SafetyNumberChangeSource.js'; import { areStoryViewReceiptsEnabled, StoryViewDirectionType, @@ -53,7 +53,7 @@ import { hasFailed, isDownloaded, isDownloading, -} from '../../types/Attachment.js'; +} from '../../util/Attachment.js'; import { getConversationSelector, getHideStoryConversationIds, diff --git a/ts/state/selectors/audioPlayer.ts b/ts/state/selectors/audioPlayer.ts index 3f27409ad6..f1fc736f6d 100644 --- a/ts/state/selectors/audioPlayer.ts +++ b/ts/state/selectors/audioPlayer.ts @@ -25,9 +25,9 @@ import { getLocalAttachmentUrl } from '../../util/getLocalAttachmentUrl.js'; import type { MessageWithUIFieldsType } from '../ducks/conversations.js'; import type { ReadonlyMessageAttributesType } from '../../model-types.d.ts'; import { getMessageIdForLogging } from '../../util/idForLogging.js'; -import * as Attachment from '../../types/Attachment.js'; +import * as Attachment from '../../util/Attachment.js'; import type { ActiveAudioPlayerStateType } from '../ducks/audioPlayer.js'; -import { isPlayed } from '../../types/Attachment.js'; +import { isPlayed } from '../../util/Attachment.js'; import type { ServiceIdString } from '../../types/ServiceId.js'; const log = createLogger('audioPlayer'); diff --git a/ts/state/selectors/message.ts b/ts/state/selectors/message.ts index 4ecdc6a9fa..f6d1e83549 100644 --- a/ts/state/selectors/message.ts +++ b/ts/state/selectors/message.ts @@ -70,7 +70,7 @@ import { isVoiceMessage, isIncremental, defaultBlurHash, -} from '../../types/Attachment.js'; +} from '../../util/Attachment.js'; import type { MessageAttachmentType } from '../../types/AttachmentDownload.js'; import { type DefaultConversationColorType } from '../../types/Colors.js'; import { ReadStatus } from '../../messages/MessageReadStatus.js'; diff --git a/ts/state/smart/ForwardMessagesModal.tsx b/ts/state/smart/ForwardMessagesModal.tsx index be758b1095..76b6c18ab5 100644 --- a/ts/state/smart/ForwardMessagesModal.tsx +++ b/ts/state/smart/ForwardMessagesModal.tsx @@ -22,7 +22,7 @@ import { useGlobalModalActions } from '../ducks/globalModals.js'; import { useLinkPreviewActions } from '../ducks/linkPreviews.js'; import { SmartCompositionTextArea } from './CompositionTextArea.js'; import { useToastActions } from '../ducks/toast.js'; -import { isDownloaded } from '../../types/Attachment.js'; +import { isDownloaded } from '../../util/Attachment.js'; import { getMessageById } from '../../messages/getMessageById.js'; import { strictAssert } from '../../util/assert.js'; import type { diff --git a/ts/state/smart/SendAnywayDialog.tsx b/ts/state/smart/SendAnywayDialog.tsx index e788757f92..f28a70661c 100644 --- a/ts/state/smart/SendAnywayDialog.tsx +++ b/ts/state/smart/SendAnywayDialog.tsx @@ -3,12 +3,10 @@ import React, { memo, useCallback } from 'react'; import { useSelector } from 'react-redux'; +import { SafetyNumberChangeSource } from '../../types/SafetyNumberChangeSource.js'; import * as SingleServePromise from '../../services/singleServePromise.js'; import type { SafetyNumberProps } from '../../components/SafetyNumberChangeDialog.js'; -import { - SafetyNumberChangeDialog, - SafetyNumberChangeSource, -} from '../../components/SafetyNumberChangeDialog.js'; +import { SafetyNumberChangeDialog } from '../../components/SafetyNumberChangeDialog.js'; import { SmartSafetyNumberViewer } from './SafetyNumberViewer.js'; import { getByDistributionListConversationsStoppingSend } from '../selectors/conversations-extra.js'; import { getIntl, getTheme } from '../selectors/user.js'; diff --git a/ts/test-electron/backup/attachments_test.ts b/ts/test-electron/backup/attachments_test.ts index 54d872c5bc..2b3508474f 100644 --- a/ts/test-electron/backup/attachments_test.ts +++ b/ts/test-electron/backup/attachments_test.ts @@ -30,8 +30,8 @@ import type { import { hasRequiredInformationForBackup, isVoiceMessage, - type AttachmentType, -} from '../../types/Attachment.js'; +} from '../../util/Attachment.js'; +import type { AttachmentType } from '../../types/Attachment.js'; import { strictAssert } from '../../util/assert.js'; import { SignalService } from '../../protobuf/index.js'; import { getRandomBytes } from '../../Crypto.js'; diff --git a/ts/test-electron/backup/bubble_test.ts b/ts/test-electron/backup/bubble_test.ts index ede2e925f3..73c44b3b77 100644 --- a/ts/test-electron/backup/bubble_test.ts +++ b/ts/test-electron/backup/bubble_test.ts @@ -5,7 +5,7 @@ import { v4 as generateGuid } from 'uuid'; import { SendStatus } from '../../messages/MessageSendState.js'; import type { ConversationModel } from '../../models/conversations.js'; -import { GiftBadgeStates } from '../../components/conversation/Message.js'; +import { GiftBadgeStates } from '../../types/GiftBadgeStates.js'; import { DataWriter } from '../../sql/Client.js'; import { getRandomBytes } from '../../Crypto.js'; diff --git a/ts/test-electron/backup/filePointer_test.ts b/ts/test-electron/backup/filePointer_test.ts index 9dd6e9ea3d..a451e7b0a5 100644 --- a/ts/test-electron/backup/filePointer_test.ts +++ b/ts/test-electron/backup/filePointer_test.ts @@ -15,7 +15,7 @@ import { } from '../../services/backups/util/filePointers.js'; import { IMAGE_PNG } from '../../types/MIME.js'; import * as Bytes from '../../Bytes.js'; -import { type AttachmentType } from '../../types/Attachment.js'; +import type { AttachmentType } from '../../types/Attachment.js'; import { MASTER_KEY, MEDIA_ROOT_KEY } from './helpers.js'; import { generateKeys } from '../../AttachmentCrypto.js'; import type { GetBackupCdnInfoType } from '../../services/backups/util/mediaId.js'; diff --git a/ts/test-electron/deleteMessageAttachments_test.ts b/ts/test-electron/deleteMessageAttachments_test.ts index e5f0a9c463..ddff4b7621 100644 --- a/ts/test-electron/deleteMessageAttachments_test.ts +++ b/ts/test-electron/deleteMessageAttachments_test.ts @@ -12,10 +12,8 @@ import { getDownloadsPath, getPath } from '../windows/main/attachments.js'; import { IMAGE_JPEG, LONG_MESSAGE } from '../types/MIME.js'; import type { MessageAttributesType } from '../model-types.js'; -import { - type AttachmentType, - deleteAllAttachmentFilesOnDisk, -} from '../types/Attachment.js'; +import type { AttachmentType } from '../types/Attachment.js'; +import { deleteAllAttachmentFilesOnDisk } from '../util/Attachment.js'; import { strictAssert } from '../util/assert.js'; const { emptyDir, ensureFile } = fsExtra; diff --git a/ts/test-electron/services/AttachmentBackupManager_test.ts b/ts/test-electron/services/AttachmentBackupManager_test.ts index b3edfc46ed..b571ec6e9c 100644 --- a/ts/test-electron/services/AttachmentBackupManager_test.ts +++ b/ts/test-electron/services/AttachmentBackupManager_test.ts @@ -25,7 +25,7 @@ import { APPLICATION_OCTET_STREAM, VIDEO_MP4 } from '../../types/MIME.js'; import { createName, getRelativePath } from '../../util/attachmentPath.js'; import { encryptAttachmentV2, generateKeys } from '../../AttachmentCrypto.js'; import { SECOND } from '../../util/durations/index.js'; -import { HTTPError } from '../../textsecure/Errors.js'; +import { HTTPError } from '../../types/HTTPError.js'; const { ensureFile } = fsExtra; diff --git a/ts/test-electron/services/profiles_test.ts b/ts/test-electron/services/profiles_test.ts index 89350e71b0..8d3904c7ae 100644 --- a/ts/test-electron/services/profiles_test.ts +++ b/ts/test-electron/services/profiles_test.ts @@ -8,7 +8,7 @@ import { drop } from '../../util/drop.js'; import { ProfileService } from '../../services/profiles.js'; import { generateAci } from '../../types/ServiceId.js'; -import { HTTPError } from '../../textsecure/Errors.js'; +import { HTTPError } from '../../types/HTTPError.js'; describe('util/profiles', () => { const SERVICE_ID_1 = generateAci(); diff --git a/ts/test-electron/util/downloadAttachment_test.ts b/ts/test-electron/util/downloadAttachment_test.ts index 2ed8a4e665..0c830065ba 100644 --- a/ts/test-electron/util/downloadAttachment_test.ts +++ b/ts/test-electron/util/downloadAttachment_test.ts @@ -9,7 +9,7 @@ import { DataWriter } from '../../sql/Client.js'; import { IMAGE_PNG } from '../../types/MIME.js'; import { downloadAttachment } from '../../util/downloadAttachment.js'; import { MediaTier } from '../../types/AttachmentDownload.js'; -import { HTTPError } from '../../textsecure/Errors.js'; +import { HTTPError } from '../../types/HTTPError.js'; import { getCdnNumberForBackupTier, type downloadAttachment as downloadAttachmentFromServer, diff --git a/ts/test-electron/util/sendToGroup_test.ts b/ts/test-electron/util/sendToGroup_test.ts index 8cf83946d5..19bd4531c6 100644 --- a/ts/test-electron/util/sendToGroup_test.ts +++ b/ts/test-electron/util/sendToGroup_test.ts @@ -13,7 +13,6 @@ import { generateAci } from '../../types/ServiceId.js'; import type { DeviceType } from '../../textsecure/Types.d.ts'; import { ConnectTimeoutError, - HTTPError, IncorrectSenderKeyAuthError, MessageError, OutgoingIdentityKeyError, @@ -24,6 +23,7 @@ import { UnknownRecipientError, UnregisteredUserError, } from '../../textsecure/Errors.js'; +import { HTTPError } from '../../types/HTTPError.js'; describe('sendToGroup', () => { const serviceIdOne = generateAci(); diff --git a/ts/test-node/jobs/helpers/findRetryAfterTimeFromError_test.ts b/ts/test-node/jobs/helpers/findRetryAfterTimeFromError_test.ts index 3625c17ebe..448e7edb03 100644 --- a/ts/test-node/jobs/helpers/findRetryAfterTimeFromError_test.ts +++ b/ts/test-node/jobs/helpers/findRetryAfterTimeFromError_test.ts @@ -4,7 +4,7 @@ import { assert } from 'chai'; import { findRetryAfterTimeFromError } from '../../../jobs/helpers/findRetryAfterTimeFromError.js'; -import { HTTPError } from '../../../textsecure/Errors.js'; +import { HTTPError } from '../../../types/HTTPError.js'; import { MINUTE } from '../../../util/durations/index.js'; describe('findRetryAfterTimeFromError', () => { diff --git a/ts/test-node/jobs/helpers/handleMultipleSendErrors_test.ts b/ts/test-node/jobs/helpers/handleMultipleSendErrors_test.ts index 7fd15466ef..a66b611709 100644 --- a/ts/test-node/jobs/helpers/handleMultipleSendErrors_test.ts +++ b/ts/test-node/jobs/helpers/handleMultipleSendErrors_test.ts @@ -4,10 +4,8 @@ import { assert } from 'chai'; import * as sinon from 'sinon'; import lodash from 'lodash'; -import { - HTTPError, - SendMessageProtoError, -} from '../../../textsecure/Errors.js'; +import { SendMessageProtoError } from '../../../textsecure/Errors.js'; +import { HTTPError } from '../../../types/HTTPError.js'; import { SECOND } from '../../../util/durations/index.js'; import { diff --git a/ts/test-node/jobs/helpers/sleepForRateLimitRetryAfterTime_test.ts b/ts/test-node/jobs/helpers/sleepForRateLimitRetryAfterTime_test.ts index d48a633f85..1b1cd17585 100644 --- a/ts/test-node/jobs/helpers/sleepForRateLimitRetryAfterTime_test.ts +++ b/ts/test-node/jobs/helpers/sleepForRateLimitRetryAfterTime_test.ts @@ -3,7 +3,7 @@ import { assert } from 'chai'; import * as sinon from 'sinon'; -import { HTTPError } from '../../../textsecure/Errors.js'; +import { HTTPError } from '../../../types/HTTPError.js'; import * as durations from '../../../util/durations/index.js'; import { drop } from '../../../util/drop.js'; diff --git a/ts/test-node/types/Attachment_test.ts b/ts/test-node/types/Attachment_test.ts index 424cf753e9..7dc7bd7dd9 100644 --- a/ts/test-node/types/Attachment_test.ts +++ b/ts/test-node/types/Attachment_test.ts @@ -3,7 +3,11 @@ import { assert } from 'chai'; -import * as Attachment from '../../types/Attachment.js'; +import * as Attachment from '../../util/Attachment.js'; +import type { + LocalAttachmentV2Type, + AttachmentType, +} from '../../types/Attachment.js'; import * as MIME from '../../types/MIME.js'; import { SignalService } from '../../protobuf/index.js'; import * as Bytes from '../../Bytes.js'; @@ -15,7 +19,7 @@ import { migrateDataToFileSystem } from '../../util/attachments/migrateDataToFil const logger = createLogger('Attachment_test'); -const FAKE_LOCAL_ATTACHMENT: Attachment.LocalAttachmentV2Type = { +const FAKE_LOCAL_ATTACHMENT: LocalAttachmentV2Type = { version: 2, size: 1, plaintextHash: 'bogus', @@ -26,7 +30,7 @@ const FAKE_LOCAL_ATTACHMENT: Attachment.LocalAttachmentV2Type = { describe('Attachment', () => { describe('getFileExtension', () => { it('should return file extension from content type', () => { - const input: Attachment.AttachmentType = fakeAttachment({ + const input: AttachmentType = fakeAttachment({ data: Bytes.fromString('foo'), contentType: MIME.IMAGE_GIF, }); @@ -34,7 +38,7 @@ describe('Attachment', () => { }); it('should return file extension for QuickTime videos', () => { - const input: Attachment.AttachmentType = fakeAttachment({ + const input: AttachmentType = fakeAttachment({ data: Bytes.fromString('foo'), contentType: MIME.VIDEO_QUICKTIME, }); @@ -45,7 +49,7 @@ describe('Attachment', () => { describe('getSuggestedFilename', () => { context('for attachment with filename', () => { it('should return existing filename if present', () => { - const attachment: Attachment.AttachmentType = fakeAttachment({ + const attachment: AttachmentType = fakeAttachment({ fileName: 'funny-cat.mov', data: Bytes.fromString('foo'), contentType: MIME.VIDEO_QUICKTIME, @@ -57,7 +61,7 @@ describe('Attachment', () => { }); context('for attachment without filename', () => { it('should generate a filename based on timestamp', () => { - const attachment: Attachment.AttachmentType = fakeAttachment({ + const attachment: AttachmentType = fakeAttachment({ data: Bytes.fromString('foo'), contentType: MIME.VIDEO_QUICKTIME, }); @@ -74,7 +78,7 @@ describe('Attachment', () => { }); context('for attachment with index', () => { it('should use filename if it is provided', () => { - const attachment: Attachment.AttachmentType = fakeAttachment({ + const attachment: AttachmentType = fakeAttachment({ fileName: 'funny-cat.mov', data: Bytes.fromString('foo'), contentType: MIME.VIDEO_QUICKTIME, @@ -91,7 +95,7 @@ describe('Attachment', () => { }); it('should use filename if it is provided and index is 1', () => { - const attachment: Attachment.AttachmentType = fakeAttachment({ + const attachment: AttachmentType = fakeAttachment({ fileName: 'funny-cat.mov', data: Bytes.fromString('foo'), contentType: MIME.VIDEO_QUICKTIME, @@ -109,7 +113,7 @@ describe('Attachment', () => { }); it('should use filename if it is provided and index is >1', () => { - const attachment: Attachment.AttachmentType = fakeAttachment({ + const attachment: AttachmentType = fakeAttachment({ fileName: 'funny-cat.mov', data: Bytes.fromString('foo'), contentType: MIME.VIDEO_QUICKTIME, @@ -127,7 +131,7 @@ describe('Attachment', () => { }); it('should use provided index if > 1 and filename not provided', () => { - const attachment: Attachment.AttachmentType = fakeAttachment({ + const attachment: AttachmentType = fakeAttachment({ data: Bytes.fromString('foo'), contentType: MIME.VIDEO_QUICKTIME, }); @@ -144,7 +148,7 @@ describe('Attachment', () => { }); it('should not use provided index == 1 if filename not provided', () => { - const attachment: Attachment.AttachmentType = fakeAttachment({ + const attachment: AttachmentType = fakeAttachment({ data: Bytes.fromString('foo'), contentType: MIME.VIDEO_QUICKTIME, }); @@ -164,7 +168,7 @@ describe('Attachment', () => { describe('isVisualMedia', () => { it('should return true for images', () => { - const attachment: Attachment.AttachmentType = fakeAttachment({ + const attachment: AttachmentType = fakeAttachment({ fileName: 'meme.gif', data: Bytes.fromString('gif'), contentType: MIME.IMAGE_GIF, @@ -173,7 +177,7 @@ describe('Attachment', () => { }); it('should return true for videos', () => { - const attachment: Attachment.AttachmentType = fakeAttachment({ + const attachment: AttachmentType = fakeAttachment({ fileName: 'meme.mp4', data: Bytes.fromString('mp4'), contentType: MIME.VIDEO_MP4, @@ -182,7 +186,7 @@ describe('Attachment', () => { }); it('should return false for voice message attachment', () => { - const attachment: Attachment.AttachmentType = fakeAttachment({ + const attachment: AttachmentType = fakeAttachment({ fileName: 'Voice Message.aac', flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE, data: Bytes.fromString('voice message'), @@ -192,7 +196,7 @@ describe('Attachment', () => { }); it('should return false for other attachments', () => { - const attachment: Attachment.AttachmentType = fakeAttachment({ + const attachment: AttachmentType = fakeAttachment({ fileName: 'foo.json', data: Bytes.fromString('{"foo": "bar"}'), contentType: MIME.APPLICATION_JSON, @@ -203,7 +207,7 @@ describe('Attachment', () => { describe('isFile', () => { it('should return true for JSON', () => { - const attachment: Attachment.AttachmentType = fakeAttachment({ + const attachment: AttachmentType = fakeAttachment({ fileName: 'foo.json', data: Bytes.fromString('{"foo": "bar"}'), contentType: MIME.APPLICATION_JSON, @@ -212,7 +216,7 @@ describe('Attachment', () => { }); it('should return false for images', () => { - const attachment: Attachment.AttachmentType = fakeAttachment({ + const attachment: AttachmentType = fakeAttachment({ fileName: 'meme.gif', data: Bytes.fromString('gif'), contentType: MIME.IMAGE_GIF, @@ -221,7 +225,7 @@ describe('Attachment', () => { }); it('should return false for videos', () => { - const attachment: Attachment.AttachmentType = fakeAttachment({ + const attachment: AttachmentType = fakeAttachment({ fileName: 'meme.mp4', data: Bytes.fromString('mp4'), contentType: MIME.VIDEO_MP4, @@ -230,7 +234,7 @@ describe('Attachment', () => { }); it('should return false for voice message attachment', () => { - const attachment: Attachment.AttachmentType = fakeAttachment({ + const attachment: AttachmentType = fakeAttachment({ fileName: 'Voice Message.aac', flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE, data: Bytes.fromString('voice message'), @@ -242,7 +246,7 @@ describe('Attachment', () => { describe('isVoiceMessage', () => { it('should return true for voice message attachment', () => { - const attachment: Attachment.AttachmentType = fakeAttachment({ + const attachment: AttachmentType = fakeAttachment({ fileName: 'Voice Message.aac', flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE, data: Bytes.fromString('voice message'), @@ -252,7 +256,7 @@ describe('Attachment', () => { }); it('should return true for legacy Android voice message attachment', () => { - const attachment: Attachment.AttachmentType = fakeAttachment({ + const attachment: AttachmentType = fakeAttachment({ data: Bytes.fromString('voice message'), contentType: MIME.AUDIO_MP3, }); @@ -260,7 +264,7 @@ describe('Attachment', () => { }); it('should return false for other attachments', () => { - const attachment: Attachment.AttachmentType = fakeAttachment({ + const attachment: AttachmentType = fakeAttachment({ fileName: 'foo.gif', data: Bytes.fromString('foo'), contentType: MIME.IMAGE_GIF, diff --git a/ts/textsecure/Errors.ts b/ts/textsecure/Errors.ts index 05c062d733..a0ac2d3410 100644 --- a/ts/textsecure/Errors.ts +++ b/ts/textsecure/Errors.ts @@ -3,59 +3,20 @@ /* eslint-disable max-classes-per-file */ -import type { Response } from 'node-fetch'; import type { LibSignalErrorBase } from '@signalapp/libsignal-client'; import { parseRetryAfter } from '../util/parseRetryAfter.js'; import type { ServiceIdString } from '../types/ServiceId.js'; +import type { HTTPError } from '../types/HTTPError.js'; +import type { HeaderListType } from '../types/WebAPI.d.ts'; import type { CallbackResultType } from './Types.d.ts'; -import type { HeaderListType } from './WebAPI.js'; function appendStack(newError: Error, originalError: Error) { // eslint-disable-next-line no-param-reassign newError.stack += `\nOriginal stack:\n${originalError.stack}`; } -export class HTTPError extends Error { - public override readonly name = 'HTTPError'; - - public readonly code: number; - - public readonly responseHeaders: HeaderListType; - - public readonly response: unknown; - - static fromResponse(response: Response): HTTPError { - return new HTTPError(response.statusText, { - code: response.status, - headers: Object.fromEntries(response.headers), - response, - }); - } - - constructor( - message: string, - options: { - code: number; - headers: HeaderListType; - response?: unknown; - stack?: string; - cause?: unknown; - } - ) { - super(`${message}; code: ${options.code}`, { cause: options.cause }); - - const { code: providedCode, headers, response, stack } = options; - - this.code = providedCode > 999 || providedCode < 100 ? -1 : providedCode; - this.responseHeaders = headers; - - this.stack += `\nOriginal stack:\n${stack}`; - this.response = response; - } -} - export class ReplayableError extends Error { functionCode?: number; diff --git a/ts/textsecure/OutgoingMessage.ts b/ts/textsecure/OutgoingMessage.ts index 286f57fa7e..fc823aade7 100644 --- a/ts/textsecure/OutgoingMessage.ts +++ b/ts/textsecure/OutgoingMessage.ts @@ -31,11 +31,11 @@ import { SendMessageNetworkError, SendMessageChallengeError, UnregisteredUserError, - HTTPError, } from './Errors.js'; import type { CallbackResultType, CustomError } from './Types.d.ts'; import { Address } from '../types/Address.js'; import * as Errors from '../types/errors.js'; +import { HTTPError } from '../types/HTTPError.js'; import { QualifiedAddress } from '../types/QualifiedAddress.js'; import type { ServiceIdString } from '../types/ServiceId.js'; import { Sessions, IdentityKeys } from '../LibSignalStores.js'; diff --git a/ts/textsecure/SendMessage.ts b/ts/textsecure/SendMessage.ts index 49edf445e7..1d1dbae40c 100644 --- a/ts/textsecure/SendMessage.ts +++ b/ts/textsecure/SendMessage.ts @@ -61,10 +61,10 @@ import { getRandomBytes } from '../Crypto.js'; import { MessageError, SendMessageProtoError, - HTTPError, NoSenderKeyError, } from './Errors.js'; import { BodyRange } from '../types/BodyRange.js'; +import { HTTPError } from '../types/HTTPError.js'; import type { RawBodyRange } from '../types/BodyRange.js'; import type { StoryContextType } from '../types/Util.js'; import type { diff --git a/ts/textsecure/SocketManager.ts b/ts/textsecure/SocketManager.ts index a258e9e3c1..7301f11b59 100644 --- a/ts/textsecure/SocketManager.ts +++ b/ts/textsecure/SocketManager.ts @@ -28,6 +28,7 @@ import { drop } from '../util/drop.js'; import type { ProxyAgent } from '../util/createProxyAgent.js'; import { createProxyAgent } from '../util/createProxyAgent.js'; import { type SocketInfo, SocketStatus } from '../types/SocketStatus.js'; +import { HTTPError } from '../types/HTTPError.js'; import * as Errors from '../types/errors.js'; import * as Bytes from '../Bytes.js'; import { createLogger } from '../logging/log.js'; @@ -42,7 +43,7 @@ import WebSocketResource, { connectUnauthenticatedLibsignal, ServerRequestType, } from './WebsocketResources.js'; -import { ConnectTimeoutError, HTTPError } from './Errors.js'; +import { ConnectTimeoutError } from './Errors.js'; import type { IRequestHandler, WebAPICredentials } from './Types.d.ts'; import { connect as connectWebSocket } from './WebSocket.js'; import { type ServerAlert } from '../util/handleServerAlerts.js'; diff --git a/ts/textsecure/Types.d.ts b/ts/textsecure/Types.d.ts index 528e03d918..610a594dc1 100644 --- a/ts/textsecure/Types.d.ts +++ b/ts/textsecure/Types.d.ts @@ -11,7 +11,7 @@ import type { PniString, } from '../types/ServiceId.js'; import type { TextAttachmentType } from '../types/Attachment.js'; -import type { GiftBadgeStates } from '../components/conversation/Message.js'; +import type { GiftBadgeStates } from '../types/GiftBadgeStates.js'; import type { MIMEType } from '../types/MIME.js'; import type { DurationInSeconds } from '../util/durations/index.js'; import type { AnyPaymentEvent } from '../types/Payment.js'; diff --git a/ts/textsecure/UpdateKeysListener.ts b/ts/textsecure/UpdateKeysListener.ts index c7d429ecb9..c397d3f7c8 100644 --- a/ts/textsecure/UpdateKeysListener.ts +++ b/ts/textsecure/UpdateKeysListener.ts @@ -7,7 +7,7 @@ import * as Registration from '../util/registration.js'; import { ServiceIdKind } from '../types/ServiceId.js'; import { createLogger } from '../logging/log.js'; import * as Errors from '../types/errors.js'; -import { HTTPError } from './Errors.js'; +import { HTTPError } from '../types/HTTPError.js'; const log = createLogger('UpdateKeysListener'); diff --git a/ts/textsecure/Utils.ts b/ts/textsecure/Utils.ts index dc499a3f87..be1db7e18b 100644 --- a/ts/textsecure/Utils.ts +++ b/ts/textsecure/Utils.ts @@ -1,7 +1,7 @@ // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import type { HTTPError } from './Errors.js'; +import type { HTTPError } from '../types/HTTPError.js'; export async function handleStatusCode(status: number): Promise { if (status === 499) { diff --git a/ts/textsecure/WebAPI.ts b/ts/textsecure/WebAPI.ts index aa414b9da7..780d5b3ad0 100644 --- a/ts/textsecure/WebAPI.ts +++ b/ts/textsecure/WebAPI.ts @@ -36,6 +36,7 @@ import { createProxyAgent } from '../util/createProxyAgent.js'; import type { ProxyAgent } from '../util/createProxyAgent.js'; import type { FetchFunctionType } from '../util/uploads/tusProtocol.js'; import { VerificationTransport } from '../types/VerificationTransport.js'; +import type { HeaderListType } from '../types/WebAPI.d.ts'; import { ZERO_ACCESS_KEY } from '../types/SealedSender.js'; import { toLogFormat } from '../types/errors.js'; import { isPackIdValid, redactPackId } from '../util/Stickers.js'; @@ -51,6 +52,7 @@ import { untaggedPniSchema, } from '../types/ServiceId.js'; import type { BackupPresentationHeadersType } from '../types/backups.js'; +import { HTTPError } from '../types/HTTPError.js'; import * as Bytes from '../Bytes.js'; import { getRandomBytes, randomInt } from '../Crypto.js'; import * as linkPreviewFetch from '../linkPreviews/linkPreviewFetch.js'; @@ -66,7 +68,6 @@ import { CDSI } from './cds/CDSI.js'; import { SignalService as Proto } from '../protobuf/index.js'; import { isEnabled as isRemoteConfigEnabled } from '../RemoteConfig.js'; -import { HTTPError } from './Errors.js'; import type MessageSender from './SendMessage.js'; import type { WebAPICredentials, @@ -172,7 +173,6 @@ function getContentType(response: Response) { } type FetchHeaderListType = { [name: string]: string }; -export type HeaderListType = { [name: string]: string | ReadonlyArray }; type HTTPCodeType = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD'; type RedactUrl = (url: string) => string; diff --git a/ts/textsecure/WebSocket.ts b/ts/textsecure/WebSocket.ts index 8cce5d5fe3..65b0ed58d0 100644 --- a/ts/textsecure/WebSocket.ts +++ b/ts/textsecure/WebSocket.ts @@ -12,9 +12,10 @@ import { getUserAgent } from '../util/getUserAgent.js'; import * as durations from '../util/durations/index.js'; import type { ProxyAgent } from '../util/createProxyAgent.js'; import { createHTTPSAgent } from '../util/createHTTPSAgent.js'; +import { HTTPError } from '../types/HTTPError.js'; import { createLogger } from '../logging/log.js'; import * as Timers from '../Timers.js'; -import { ConnectTimeoutError, HTTPError } from './Errors.js'; +import { ConnectTimeoutError } from './Errors.js'; import { handleStatusCode, translateError } from './Utils.js'; const { client: WebSocketClient } = ws; diff --git a/ts/textsecure/downloadAttachment.ts b/ts/textsecure/downloadAttachment.ts index 0b7fb78786..1197ac0183 100644 --- a/ts/textsecure/downloadAttachment.ts +++ b/ts/textsecure/downloadAttachment.ts @@ -12,12 +12,12 @@ import fsExtra from 'fs-extra'; import { createLogger } from '../logging/log.js'; import * as Errors from '../types/errors.js'; import { strictAssert } from '../util/assert.js'; +import { hasRequiredInformationForBackup } from '../util/Attachment.js'; import { AttachmentSizeError, type AttachmentType, AttachmentVariant, AttachmentPermanentlyUndownloadableError, - hasRequiredInformationForBackup, type BackupableAttachmentType, } from '../types/Attachment.js'; import * as Bytes from '../Bytes.js'; @@ -49,9 +49,9 @@ import { MAX_BACKUP_THUMBNAIL_SIZE } from '../types/VisualAttachment.js'; import { missingCaseError } from '../util/missingCaseError.js'; import { IV_LENGTH, MAC_LENGTH } from '../types/Crypto.js'; import { BackupCredentialType } from '../types/backups.js'; +import { HTTPError } from '../types/HTTPError.js'; import { getValue } from '../RemoteConfig.js'; import { parseIntOrThrow } from '../util/parseIntOrThrow.js'; -import { HTTPError } from './Errors.js'; const { ensureFile } = fsExtra; diff --git a/ts/textsecure/getKeysForServiceId.ts b/ts/textsecure/getKeysForServiceId.ts index ceb171e34f..0c6bb057ae 100644 --- a/ts/textsecure/getKeysForServiceId.ts +++ b/ts/textsecure/getKeysForServiceId.ts @@ -11,11 +11,7 @@ import { PublicKey, } from '@signalapp/libsignal-client'; -import { - OutgoingIdentityKeyError, - UnregisteredUserError, - HTTPError, -} from './Errors.js'; +import { OutgoingIdentityKeyError, UnregisteredUserError } from './Errors.js'; import { Sessions, IdentityKeys } from '../LibSignalStores.js'; import { Address } from '../types/Address.js'; import { QualifiedAddress } from '../types/QualifiedAddress.js'; @@ -24,6 +20,7 @@ import type { ServerKeysType, WebAPIType } from './WebAPI.js'; import { createLogger } from '../logging/log.js'; import { isRecord } from '../util/isRecord.js'; import type { GroupSendToken } from '../types/GroupSendEndorsements.js'; +import { HTTPError } from '../types/HTTPError.js'; import { onFailedToSendWithEndorsements } from '../util/groupSendEndorsements.js'; const log = createLogger('getKeysForServiceId'); diff --git a/ts/textsecure/processDataMessage.ts b/ts/textsecure/processDataMessage.ts index 7fc6ee1410..fbe378f741 100644 --- a/ts/textsecure/processDataMessage.ts +++ b/ts/textsecure/processDataMessage.ts @@ -29,7 +29,7 @@ import type { ProcessedGiftBadge, ProcessedStoryContext, } from './Types.d.ts'; -import { GiftBadgeStates } from '../components/conversation/Message.js'; +import { GiftBadgeStates } from '../types/GiftBadgeStates.js'; import { APPLICATION_OCTET_STREAM, stringToMIMEType } from '../types/MIME.js'; import { SECOND, DurationInSeconds } from '../util/durations/index.js'; import type { AnyPaymentEvent } from '../types/Payment.js'; @@ -37,7 +37,7 @@ import { PaymentEventKind } from '../types/Payment.js'; import { filterAndClean } from '../types/BodyRange.js'; import { bytesToUuid } from '../util/uuidToBytes.js'; import { createName } from '../util/attachmentPath.js'; -import { partitionBodyAndNormalAttachments } from '../types/Attachment.js'; +import { partitionBodyAndNormalAttachments } from '../util/Attachment.js'; import { isNotNil } from '../util/isNotNil.js'; const { isNumber } = lodash; diff --git a/ts/types/Attachment.ts b/ts/types/Attachment.ts index 0aa0733f74..38968ab9f0 100644 --- a/ts/types/Attachment.ts +++ b/ts/types/Attachment.ts @@ -2,79 +2,14 @@ // SPDX-License-Identifier: AGPL-3.0-only /* eslint-disable max-classes-per-file */ -import moment from 'moment'; -import lodash from 'lodash'; -import { blobToArrayBuffer } from 'blob-util'; - import type { LinkPreviewForUIType } from './message/LinkPreviews.js'; -import type { LoggerType } from './Logging.js'; -import { createLogger } from '../logging/log.js'; -import * as MIME from './MIME.js'; -import { toLogFormat } from './errors.js'; -import { SignalService } from '../protobuf/index.js'; -import { - isImageTypeSupported, - isVideoTypeSupported, -} from '../util/GoogleChrome.js'; -import type { - LocalizerType, - WithOptionalProperties, - WithRequiredProperties, -} from './Util.js'; -import { ThemeType } from './Util.js'; -import * as GoogleChrome from '../util/GoogleChrome.js'; -import { ReadStatus } from '../messages/MessageReadStatus.js'; -import type { MessageStatusType } from './message/MessageStatus.js'; +import type { MIMEType } from './MIME.js'; +import type { WithOptionalProperties, WithRequiredProperties } from './Util.js'; import type { SignalService as Proto } from '../protobuf/index.js'; -import { isMoreRecentThan } from '../util/timestamp.js'; -import { DAY } from '../util/durations/index.js'; -import { getMessageQueueTime } from '../util/getMessageQueueTime.js'; -import { getLocalAttachmentUrl } from '../util/getLocalAttachmentUrl.js'; -import { - isValidAttachmentKey, - isValidDigest, - isValidPlaintextHash, -} from './Crypto.js'; -import { missingCaseError } from '../util/missingCaseError.js'; -import type { MakeVideoScreenshotResultType } from './VisualAttachment.js'; -import type { MessageAttachmentType } from './AttachmentDownload.js'; -import { getFilePathsOwnedByAttachment } from '../util/messageFilePaths.js'; -import { strictAssert } from '../util/assert.js'; - -const { - isNumber, - padStart, - isFunction, - isUndefined, - isString, - omit, - partition, -} = lodash; - -const logging = createLogger('Attachment'); - -const MAX_TIMELINE_IMAGE_WIDTH = 300; -const MAX_TIMELINE_IMAGE_HEIGHT = MAX_TIMELINE_IMAGE_WIDTH * 1.5; -const MIN_TIMELINE_IMAGE_WIDTH = 200; -const MIN_TIMELINE_IMAGE_HEIGHT = 50; - -const MAX_DISPLAYABLE_IMAGE_WIDTH = 8192; -const MAX_DISPLAYABLE_IMAGE_HEIGHT = 8192; -// Used for display - -export class AttachmentSizeError extends Error {} - -// Used for downlaods - -export class AttachmentPermanentlyUndownloadableError extends Error { - constructor(message: string) { - super(`AttachmentPermanentlyUndownloadableError: ${message}`); - } -} export type ThumbnailType = EphemeralAttachmentFields & { size: number; - contentType: MIME.MIMEType; + contentType: MIMEType; path?: string; plaintextHash?: string; width?: number; @@ -122,7 +57,7 @@ export type AttachmentType = EphemeralAttachmentFields & { blurHash?: string; caption?: string; clientUuid?: string; - contentType: MIME.MIMEType; + contentType: MIMEType; digest?: string; fileName?: string; plaintextHash?: string; @@ -175,7 +110,7 @@ export type AddressableAttachmentType = Readonly<{ path: string; localKey?: string; size?: number; - contentType: MIME.MIMEType; + contentType: MIMEType; // In-memory data, for outgoing attachments that are not saved to disk. data?: Uint8Array; @@ -232,8 +167,8 @@ export type TextAttachmentType = { export type BaseAttachmentDraftType = { blurHash?: string; - contentType: MIME.MIMEType; - screenshotContentType?: MIME.MIMEType; + contentType: MIMEType; + screenshotContentType?: MIMEType; size: number; flags?: number; }; @@ -251,7 +186,7 @@ export type InMemoryAttachmentDraftType = path?: string; } & BaseAttachmentDraftType) | { - contentType: MIME.MIMEType; + contentType: MIMEType; clientUuid: string; fileName?: string; path?: string; @@ -281,7 +216,7 @@ export type AttachmentDraftType = } & BaseAttachmentDraftType) | { clientUuid: string; - contentType: MIME.MIMEType; + contentType: MIMEType; fileName?: string; path?: string; pending: true; @@ -293,984 +228,10 @@ export enum AttachmentVariant { ThumbnailFromBackup = 'thumbnailFromBackup', } -// // Incoming message attachment fields -// { -// id: string -// contentType: MIMEType -// data: Uint8Array -// digest: Uint8Array -// fileName?: string -// flags: null -// key: Uint8Array -// size: integer -// thumbnail: Uint8Array -// } - -// // Outgoing message attachment fields -// { -// contentType: MIMEType -// data: Uint8Array -// fileName: string -// size: integer -// } - -// Returns true if `rawAttachment` is a valid attachment based on our current schema. -// Over time, we can expand this definition to become more narrow, e.g. require certain -// fields, etc. -export function isValid( - rawAttachment?: Pick -): rawAttachment is AttachmentType { - // NOTE: We cannot use `_.isPlainObject` because `rawAttachment` is - // deserialized by protobuf: - if (!rawAttachment) { - return false; - } - - return true; -} - -const UNICODE_LEFT_TO_RIGHT_OVERRIDE = '\u202D'; -const UNICODE_RIGHT_TO_LEFT_OVERRIDE = '\u202E'; -const UNICODE_REPLACEMENT_CHARACTER = '\uFFFD'; -const INVALID_CHARACTERS_PATTERN = new RegExp( - `[${UNICODE_LEFT_TO_RIGHT_OVERRIDE}${UNICODE_RIGHT_TO_LEFT_OVERRIDE}]`, - 'g' -); - -// NOTE: Expose synchronous version to do property-based testing using `testcheck`, -// which currently doesn’t support async testing: -// https://github.com/leebyron/testcheck-js/issues/45 -export function _replaceUnicodeOrderOverridesSync( - attachment: AttachmentType -): AttachmentType { - if (!isString(attachment.fileName)) { - return attachment; - } - - const normalizedFilename = attachment.fileName.replace( - INVALID_CHARACTERS_PATTERN, - UNICODE_REPLACEMENT_CHARACTER - ); - const newAttachment = { ...attachment, fileName: normalizedFilename }; - - return newAttachment; -} - -export const replaceUnicodeOrderOverrides = async ( - attachment: AttachmentType -): Promise => { - return _replaceUnicodeOrderOverridesSync(attachment); -}; - -// \u202A-\u202E is LRE, RLE, PDF, LRO, RLO -// \u2066-\u2069 is LRI, RLI, FSI, PDI -// \u200E is LRM -// \u200F is RLM -// \u061C is ALM -const V2_UNWANTED_UNICODE = /[\u202A-\u202E\u2066-\u2069\u200E\u200F\u061C]/g; - -export async function replaceUnicodeV2( - attachment: AttachmentType -): Promise { - if (!isString(attachment.fileName)) { - return attachment; - } - - const fileName = attachment.fileName.replace( - V2_UNWANTED_UNICODE, - UNICODE_REPLACEMENT_CHARACTER - ); - - return { - ...attachment, - fileName, - }; -} - -export function removeSchemaVersion({ - attachment, - logger, -}: { - attachment: AttachmentType; - logger: LoggerType; -}): AttachmentType { - if (!isValid(attachment)) { - logger.error( - 'Attachment.removeSchemaVersion: Invalid input attachment:', - attachment - ); - return attachment; - } - - return omit(attachment, 'schemaVersion'); -} - -export function hasData(attachment: AttachmentType): boolean { - return attachment.data instanceof Uint8Array; -} - -export function loadData( - readAttachmentV2Data: ( - attachment: Partial - ) => Promise -): ( - attachment: Partial -) => Promise { - if (!isFunction(readAttachmentV2Data)) { - throw new TypeError("'readAttachmentData' must be a function"); - } - - return async attachment => { - if (!isValid(attachment)) { - throw new TypeError("'attachment' is not valid"); - } - - const isAlreadyLoaded = Boolean(attachment.data); - if (isAlreadyLoaded) { - return attachment as AttachmentWithHydratedData; - } - - if (!isString(attachment.path)) { - throw new TypeError("'attachment.path' is required"); - } - - const data = await readAttachmentV2Data(attachment); - return { ...attachment, data, size: data.byteLength }; - }; -} - -export function deleteAllAttachmentFilesOnDisk({ - deleteAttachmentOnDisk, - deleteDownloadOnDisk, -}: { - deleteAttachmentOnDisk: (path: string) => Promise; - deleteDownloadOnDisk: (path: string) => Promise; -}): (attachment?: AttachmentType) => Promise { - if (!isFunction(deleteAttachmentOnDisk)) { - throw new TypeError( - 'deleteAttachmentOnDisk: deleteAttachmentOnDisk must be a function' - ); - } - - return async (attachment?: AttachmentType): Promise => { - if (!isValid(attachment)) { - throw new TypeError('deleteData: attachment is not valid'); - } - - const result = getFilePathsOwnedByAttachment(attachment); - await Promise.all( - [...result.externalAttachments].map(deleteAttachmentOnDisk) - ); - await Promise.all([...result.externalDownloads].map(deleteDownloadOnDisk)); - }; -} - -const THUMBNAIL_SIZE = 150; -const THUMBNAIL_CONTENT_TYPE = MIME.IMAGE_PNG; - -export async function captureDimensionsAndScreenshot( - attachment: AttachmentType, - options: { generateThumbnail: boolean }, - params: { - writeNewAttachmentData: ( - data: Uint8Array - ) => Promise; - makeObjectUrl: ( - data: Uint8Array | ArrayBuffer, - contentType: MIME.MIMEType - ) => string; - revokeObjectUrl: (path: string) => void; - getImageDimensions: (params: { - objectUrl: string; - logger: LoggerType; - }) => Promise<{ - width: number; - height: number; - }>; - makeImageThumbnail: (params: { - size: number; - objectUrl: string; - contentType: MIME.MIMEType; - logger: LoggerType; - }) => Promise; - makeVideoScreenshot: (params: { - objectUrl: string; - contentType: MIME.MIMEType; - logger: LoggerType; - }) => Promise; - logger: LoggerType; - } -): Promise { - const { contentType } = attachment; - - const { - writeNewAttachmentData, - makeObjectUrl, - revokeObjectUrl, - getImageDimensions: getImageDimensionsFromURL, - makeImageThumbnail, - makeVideoScreenshot, - logger, - } = params; - - if ( - !GoogleChrome.isImageTypeSupported(contentType) && - !GoogleChrome.isVideoTypeSupported(contentType) - ) { - return attachment; - } - - // If the attachment hasn't been downloaded yet, we won't have a path - if (!attachment.path) { - return attachment; - } - - const localUrl = getLocalAttachmentUrl(attachment); - - if (GoogleChrome.isImageTypeSupported(contentType)) { - // Already generated thumbnail / width / height - if (attachment.thumbnail?.path) { - return attachment; - } - - try { - const { width, height } = await getImageDimensionsFromURL({ - objectUrl: localUrl, - logger, - }); - let thumbnail: LocalAttachmentV2Type | undefined; - - if (options.generateThumbnail) { - const thumbnailBuffer = await blobToArrayBuffer( - await makeImageThumbnail({ - size: THUMBNAIL_SIZE, - objectUrl: localUrl, - contentType: THUMBNAIL_CONTENT_TYPE, - logger, - }) - ); - - thumbnail = await writeNewAttachmentData( - new Uint8Array(thumbnailBuffer) - ); - } - - return { - ...attachment, - width, - height, - thumbnail: thumbnail - ? { - ...thumbnail, - contentType: THUMBNAIL_CONTENT_TYPE, - width: THUMBNAIL_SIZE, - height: THUMBNAIL_SIZE, - } - : undefined, - }; - } catch (error) { - logger.error( - 'captureDimensionsAndScreenshot:', - 'error processing image; skipping screenshot generation', - toLogFormat(error) - ); - return attachment; - } - } - - strictAssert( - GoogleChrome.isVideoTypeSupported(contentType), - 'enforced by early return' - ); - - // Already generated screenshot / width / height - if (attachment.screenshot?.path) { - return attachment; - } - - let screenshotObjectUrl: string | undefined; - try { - const { blob, duration } = await makeVideoScreenshot({ - objectUrl: localUrl, - contentType: THUMBNAIL_CONTENT_TYPE, - logger, - }); - const screenshotBuffer = await blobToArrayBuffer(blob); - screenshotObjectUrl = makeObjectUrl( - screenshotBuffer, - THUMBNAIL_CONTENT_TYPE - ); - const { width, height } = await getImageDimensionsFromURL({ - objectUrl: screenshotObjectUrl, - logger, - }); - const screenshot = await writeNewAttachmentData( - new Uint8Array(screenshotBuffer) - ); - - let thumbnail: LocalAttachmentV2Type | undefined; - if (options.generateThumbnail) { - const thumbnailBuffer = await blobToArrayBuffer( - await makeImageThumbnail({ - size: THUMBNAIL_SIZE, - objectUrl: screenshotObjectUrl, - contentType: THUMBNAIL_CONTENT_TYPE, - logger, - }) - ); - - thumbnail = await writeNewAttachmentData(new Uint8Array(thumbnailBuffer)); - } - - return { - ...attachment, - duration, - screenshot: { - ...screenshot, - contentType: THUMBNAIL_CONTENT_TYPE, - width, - height, - }, - thumbnail: thumbnail - ? { - ...thumbnail, - contentType: THUMBNAIL_CONTENT_TYPE, - width: THUMBNAIL_SIZE, - height: THUMBNAIL_SIZE, - } - : undefined, - width, - height, - }; - } catch (error) { - logger.error( - 'captureDimensionsAndScreenshot: error processing video; skipping screenshot generation', - toLogFormat(error) - ); - return attachment; - } finally { - if (screenshotObjectUrl !== undefined) { - revokeObjectUrl(screenshotObjectUrl); - } - } -} - -// UI-focused functions - -export function getExtensionForDisplay({ - fileName, - contentType, -}: { - fileName?: string; - contentType: MIME.MIMEType; -}): string | undefined { - if (fileName && fileName.indexOf('.') >= 0) { - const lastPeriod = fileName.lastIndexOf('.'); - const extension = fileName.slice(lastPeriod + 1); - if (extension.length) { - return extension; - } - } - - if (!contentType) { - return undefined; - } - - const slash = contentType.indexOf('/'); - if (slash >= 0) { - return contentType.slice(slash + 1); - } - - return undefined; -} - -export function isAudio(attachments?: ReadonlyArray): boolean { - return Boolean( - attachments && - attachments[0] && - attachments[0].contentType && - !attachments[0].isCorrupted && - MIME.isAudio(attachments[0].contentType) - ); -} - -export function isPlayed( - direction: 'outgoing' | 'incoming', - status: MessageStatusType | undefined, - readStatus: ReadStatus | undefined -): boolean { - if (direction === 'outgoing') { - return status === 'viewed'; - } - return readStatus === ReadStatus.Viewed; -} - -export function canRenderAudio( - attachments?: ReadonlyArray -): boolean { - const firstAttachment = attachments && attachments[0]; - if (!firstAttachment) { - return false; - } - - return ( - isAudio(attachments) && - (isDownloaded(firstAttachment) || isDownloadable(firstAttachment)) - ); -} - -export function canDisplayImage( - attachments?: ReadonlyArray -): boolean { - const { height, width } = - attachments && attachments[0] ? attachments[0] : { height: 0, width: 0 }; - - return Boolean( - height && - height > 0 && - height <= MAX_DISPLAYABLE_IMAGE_HEIGHT && - width && - width > 0 && - width <= MAX_DISPLAYABLE_IMAGE_WIDTH - ); -} - -export function getThumbnailUrl( - attachment: AttachmentForUIType -): string | undefined { - if (attachment.thumbnail) { - return attachment.thumbnail.url; - } - - return getUrl(attachment); -} - -export function getUrl(attachment: AttachmentForUIType): string | undefined { - if (attachment.screenshot) { - return attachment.screenshot.url; - } - - if (isVideoAttachment(attachment)) { - return undefined; - } - - return attachment.url ?? attachment.thumbnailFromBackup?.url; -} - -export function isImage(attachments?: ReadonlyArray): boolean { - return Boolean( - attachments && - attachments[0] && - attachments[0].contentType && - isImageTypeSupported(attachments[0].contentType) - ); -} - -export function isImageAttachment( - attachment?: Pick -): boolean { - return Boolean( - attachment && - attachment.contentType && - isImageTypeSupported(attachment.contentType) - ); -} - -export function canBeTranscoded( - attachment?: Pick -): boolean { - return Boolean( - attachment && - isImageAttachment(attachment) && - !MIME.isGif(attachment.contentType) - ); -} - -export function hasImage(attachments?: ReadonlyArray): boolean { - return Boolean( - attachments && - attachments[0] && - (attachments[0].url || attachments[0].pending || attachments[0].blurHash) - ); -} - -export function isVideo( - attachments?: ReadonlyArray> -): boolean { - if (!attachments || attachments.length === 0) { - return false; - } - return isVideoAttachment(attachments[0]); -} - -export function isVideoAttachment( - attachment?: Pick -): boolean { - if (!attachment || !attachment.contentType) { - return false; - } - return isVideoTypeSupported(attachment.contentType); -} - -export function isGIF(attachments?: ReadonlyArray): boolean { - if (!attachments || attachments.length !== 1) { - return false; - } - - const [attachment] = attachments; - - const flag = SignalService.AttachmentPointer.Flags.GIF; - const hasFlag = - // eslint-disable-next-line no-bitwise - !isUndefined(attachment.flags) && (attachment.flags & flag) === flag; - - return hasFlag && isVideoAttachment(attachment); -} - -function resolveNestedAttachment< - T extends Pick, ->(attachment?: T): T | AttachmentType | undefined { - if (attachment?.textAttachment?.preview?.image) { - return attachment.textAttachment.preview.image; - } - return attachment; -} - -export function isIncremental( - attachment: Pick -): boolean { - return Boolean(attachment.incrementalMac && attachment.chunkSize); -} - -export function isDownloaded( - attachment?: Pick -): boolean { - const resolved = resolveNestedAttachment(attachment); - return Boolean(resolved && (resolved.path || resolved.textAttachment)); -} - -export function isReadyToView( - attachment?: Pick< - AttachmentType, - 'incrementalMac' | 'chunkSize' | 'path' | 'textAttachment' - > -): boolean { - const fullyDownloaded = isDownloaded(attachment); - if (fullyDownloaded) { - return fullyDownloaded; - } - - const resolved = resolveNestedAttachment(attachment); - return Boolean( - resolved && - (resolved.path || resolved.textAttachment || isIncremental(resolved)) - ); -} - -export function hasNotResolved(attachment?: AttachmentType): boolean { - const resolved = resolveNestedAttachment(attachment); - return Boolean(resolved && !resolved.url && !resolved.textAttachment); -} - -export function isDownloading(attachment?: AttachmentType): boolean { - const resolved = resolveNestedAttachment(attachment); - return Boolean(resolved && resolved.pending); -} - -export function hasFailed(attachment?: AttachmentType): boolean { - const resolved = resolveNestedAttachment(attachment); - return Boolean(resolved && resolved.error); -} - -export function hasVideoBlurHash( - attachments?: ReadonlyArray -): boolean { - const firstAttachment = attachments ? attachments[0] : null; - - return Boolean(firstAttachment && firstAttachment.blurHash); -} - -export function hasVideoScreenshot( - attachments?: ReadonlyArray -): string | null | undefined { - const firstAttachment = attachments ? attachments[0] : null; - - return ( - firstAttachment && - firstAttachment.screenshot && - firstAttachment.screenshot.url - ); -} - -type DimensionsType = { - height: number; - width: number; -}; - -export function getImageDimensionsForTimeline( - attachment: Pick, - forcedWidth?: number -): DimensionsType { - const { height, width } = attachment; - if (!height || !width) { - return { - height: MIN_TIMELINE_IMAGE_HEIGHT, - width: MIN_TIMELINE_IMAGE_WIDTH, - }; - } - - const aspectRatio = height / width; - const targetWidth = - forcedWidth || - Math.max( - Math.min(MAX_TIMELINE_IMAGE_WIDTH, width), - MIN_TIMELINE_IMAGE_WIDTH - ); - const candidateHeight = Math.round(targetWidth * aspectRatio); - - return { - width: targetWidth, - height: Math.max( - Math.min(MAX_TIMELINE_IMAGE_HEIGHT, candidateHeight), - MIN_TIMELINE_IMAGE_HEIGHT - ), - }; -} - -export function areAllAttachmentsVisual( - attachments?: ReadonlyArray -): boolean { - if (!attachments) { - return false; - } - - const max = attachments.length; - for (let i = 0; i < max; i += 1) { - const attachment = attachments[i]; - if (!isImageAttachment(attachment) && !isVideoAttachment(attachment)) { - return false; - } - } - - return true; -} - -export function getGridDimensions( - attachments?: ReadonlyArray -): null | DimensionsType { - if (!attachments || !attachments.length) { - return null; - } - - if (!isImage(attachments) && !isVideo(attachments)) { - return null; - } - - if (attachments.length === 1) { - return getImageDimensionsForTimeline(attachments[0]); - } - - if (attachments.length === 2) { - // A B - return { - height: 150, - width: 300, - }; - } - - if (attachments.length === 3) { - // A A B - // A A C - return { - height: 200, - width: 300, - }; - } - - if (attachments.length === 4) { - // A B - // C D - return { - height: 300, - width: 300, - }; - } - - // A A A B B B - // A A A B B B - // A A A B B B - // C C D D E E - // C C D D E E - return { - height: 250, - width: 300, - }; -} - -export function getAlt( - attachment: AttachmentType, - i18n: LocalizerType -): string { - if (isVideoAttachment(attachment)) { - return i18n('icu:videoAttachmentAlt'); - } - return i18n('icu:imageAttachmentAlt'); -} - -// Migration-related attachment stuff - -export const isVisualMedia = (attachment: AttachmentType): boolean => { - const { contentType } = attachment; - - if (isUndefined(contentType)) { - return false; - } - - if (isVoiceMessage(attachment)) { - return false; - } - - return MIME.isImage(contentType) || MIME.isVideo(contentType); -}; - -export const isFile = (attachment: AttachmentType): boolean => { - const { contentType } = attachment; - - if (isUndefined(contentType)) { - return false; - } - - if (isVisualMedia(attachment)) { - return false; - } - - if (isVoiceMessage(attachment)) { - return false; - } - - if (MIME.isLongMessage(contentType)) { - return false; - } - - return true; -}; - -export const isVoiceMessage = ( - attachment: Pick -): boolean => { - const flag = SignalService.AttachmentPointer.Flags.VOICE_MESSAGE; - const hasFlag = - // eslint-disable-next-line no-bitwise - !isUndefined(attachment.flags) && (attachment.flags & flag) === flag; - if (hasFlag) { - return true; - } - - const isLegacyAndroidVoiceMessage = - !isUndefined(attachment.contentType) && - MIME.isAudio(attachment.contentType) && - !attachment.fileName; - if (isLegacyAndroidVoiceMessage) { - return true; - } - - return false; -}; - -export const save = async ({ - attachment, - index, - getUnusedFilename, - readAttachmentData, - saveAttachmentToDisk, - timestamp, - baseDir, -}: { - attachment: AttachmentType; - index?: number; - getUnusedFilename: (options: { - filename: string; - baseDir?: string; - }) => string; - readAttachmentData: ( - attachment: Partial - ) => Promise; - saveAttachmentToDisk: (options: { - data: Uint8Array; - name: string; - baseDir?: string; - }) => Promise<{ name: string; fullPath: string } | null>; - timestamp?: number; - /** - * Base directory for saving the attachment. - * If omitted, a dialog will be opened to let the user choose a directory - */ - baseDir?: string; -}): Promise => { - let data: Uint8Array; - if (attachment.path) { - data = await readAttachmentData(attachment); - } else if (attachment.data) { - data = attachment.data; - } else { - throw new Error('Attachment had neither path nor data'); - } - - const suggestedFilename = getSuggestedFilename({ - attachment, - timestamp, - index, - }); - - /** - * When baseDir is provided, saveAttachmentToDisk() will save without prompting - * and may overwrite existing files, so we need to append a suffix - */ - const name = getUnusedFilename({ filename: suggestedFilename, baseDir }); - - const result = await saveAttachmentToDisk({ - data, - name, - baseDir, - }); - - if (!result) { - return null; - } - - return result.fullPath; -}; - -export const getSuggestedFilename = ({ - attachment, - timestamp, - index, - scenario = 'saving-locally', -}: { - attachment: Pick; - timestamp?: number | Date; - index?: number; - scenario?: 'sending' | 'saving-locally'; -}): string => { - const { fileName } = attachment; - if (fileName) { - return fileName; - } - - let prefix: string; - switch (scenario) { - case 'sending': - // when sending, we prefer a generic 'signal-less' name - prefix = 'image'; - break; - case 'saving-locally': - prefix = 'signal'; - break; - default: - throw missingCaseError(scenario); - } - - const suffix = timestamp - ? moment(timestamp).format('-YYYY-MM-DD-HHmmss') - : ''; - const fileType = getFileExtension(attachment); - const extension = fileType ? `.${fileType}` : ''; - const indexSuffix = - isNumber(index) && index > 1 - ? `_${padStart(index.toString(), 3, '0')}` - : ''; - - return `${prefix}${suffix}${indexSuffix}${extension}`; -}; - -export const getFileExtension = ( - attachment: Pick -): string | undefined => { - if (!attachment.contentType) { - return undefined; - } - - switch (attachment.contentType) { - case 'video/quicktime': - return 'mov'; - case 'audio/mpeg': - return 'mp3'; - default: - return attachment.contentType.split('/')[1]; - } -}; - -export const defaultBlurHash = (theme: ThemeType = ThemeType.light): string => { - if (theme === ThemeType.dark) { - return 'L05OQnoffQofoffQfQfQfQfQfQfQ'; - } - return 'L1Q]+w-;fQ-;~qfQfQfQfQfQfQfQ'; -}; - -export const canBeDownloaded = ( - attachment: Pick -): boolean => { - return Boolean(attachment.digest && attachment.key && !attachment.wasTooBig); -}; - -export function doAttachmentsOnSameMessageMatch( - attachmentA: AttachmentType, - attachmentB: AttachmentType -): boolean { - if ( - isValidPlaintextHash(attachmentA.plaintextHash) && - isValidPlaintextHash(attachmentB.plaintextHash) - ) { - return attachmentA.plaintextHash === attachmentB.plaintextHash; - } - - if (isValidDigest(attachmentA.digest) && isValidDigest(attachmentB.digest)) { - return attachmentA.digest === attachmentB.digest; - } - - return false; -} - -// TODO: DESKTOP-8910 -// This "undownloaded" attachment signature can change once the file is downloaded; we may -// start with only the digest or plaintextHash, but both will be filled in by the time -// it's downloaded -export function getUndownloadedAttachmentSignature( - attachment: AttachmentType -): string { - return `${attachment.digest ?? ''}.${attachment.plaintextHash ?? ''}`; -} - -export function cacheAttachmentBySignature( - attachmentMap: Map, - attachment: AttachmentType -): void { - const { digest, plaintextHash } = attachment; - if (digest) { - attachmentMap.set(digest, attachment); - } - if (plaintextHash) { - attachmentMap.set(plaintextHash, attachment); - } -} - -export function getCachedAttachmentBySignature( - attachmentMap: Map, - attachment: AttachmentType -): T | undefined { - const { digest, plaintextHash } = attachment; - if (digest) { - if (attachmentMap.has(digest)) { - return attachmentMap.get(digest); - } - } - if (plaintextHash) { - if (attachmentMap.has(plaintextHash)) { - return attachmentMap.get(plaintextHash); - } - } - return undefined; -} +export type BackupableAttachmentType = WithRequiredProperties< + AttachmentType, + 'plaintextHash' | 'key' +>; export type AttachmentDownloadableFromTransitTier = WithRequiredProperties< AttachmentType, @@ -1282,165 +243,14 @@ export type LocallySavedAttachment = WithRequiredProperties< 'path' >; -// Extend range in case the attachment is actually still there (this function is meant to -// be optimistic) -const BUFFER_TIME_ON_TRANSIT_TIER = 5 * DAY; +// Used for display -export function mightStillBeOnTransitTier( - attachment: Pick -): boolean { - if (!attachment.cdnKey) { - return false; +export class AttachmentSizeError extends Error {} + +// Used for downlaods + +export class AttachmentPermanentlyUndownloadableError extends Error { + constructor(message: string) { + super(`AttachmentPermanentlyUndownloadableError: ${message}`); } - if (attachment.cdnNumber == null) { - return false; - } - - if (!attachment.uploadTimestamp) { - // Let's be conservative and still assume it might be downloadable - return true; - } - - if ( - isMoreRecentThan( - attachment.uploadTimestamp, - getMessageQueueTime() + BUFFER_TIME_ON_TRANSIT_TIER - ) - ) { - return true; - } - - return false; -} - -export type BackupableAttachmentType = WithRequiredProperties< - AttachmentType, - 'plaintextHash' | 'key' ->; - -export function hasRequiredInformationForBackup( - attachment: AttachmentType -): attachment is BackupableAttachmentType { - return ( - isValidAttachmentKey(attachment.key) && - isValidPlaintextHash(attachment.plaintextHash) - ); -} - -export function wasImportedFromLocalBackup( - attachment: AttachmentType -): attachment is BackupableAttachmentType { - return ( - hasRequiredInformationForBackup(attachment) && - Boolean(attachment.localBackupPath) && - isValidAttachmentKey(attachment.localKey) - ); -} - -export function canAttachmentHaveThumbnail({ - contentType, -}: Pick): boolean { - return isVideoTypeSupported(contentType) || isImageTypeSupported(contentType); -} - -export function hasRequiredInformationToDownloadFromTransitTier( - attachment: AttachmentType -): attachment is AttachmentDownloadableFromTransitTier { - const hasIntegrityCheck = - isValidDigest(attachment.digest) || - isValidPlaintextHash(attachment.plaintextHash); - if (!hasIntegrityCheck) { - return false; - } - - if (!isValidAttachmentKey(attachment.key)) { - return false; - } - - if (!attachment.cdnKey || attachment.cdnNumber == null) { - return false; - } - - return true; -} - -export function shouldAttachmentEndUpInRemoteBackup({ - attachment, - hasMediaBackups, -}: { - attachment: AttachmentType; - hasMediaBackups: boolean; -}): boolean { - return hasMediaBackups && hasRequiredInformationForBackup(attachment); -} - -export function isDownloadable(attachment: AttachmentType): boolean { - return ( - hasRequiredInformationToDownloadFromTransitTier(attachment) || - shouldAttachmentEndUpInRemoteBackup({ - attachment, - // TODO: DESKTOP-8905 - hasMediaBackups: true, - }) - ); -} - -export function isAttachmentLocallySaved( - attachment: AttachmentType -): attachment is LocallySavedAttachment { - return Boolean(attachment.path); -} - -// We now partition out the bodyAttachment on receipt, but older -// messages may still have a bodyAttachment in the normal attachments field -export function partitionBodyAndNormalAttachments< - T extends Pick, ->( - { - attachments, - existingBodyAttachment, - }: { - attachments: ReadonlyArray; - existingBodyAttachment?: T; - }, - { logId, logger = logging }: { logId: string; logger?: LoggerType } -): { - bodyAttachment: T | undefined; - attachments: Array; -} { - const [bodyAttachments, normalAttachments] = partition( - attachments, - attachment => MIME.isLongMessage(attachment.contentType) - ); - - if (bodyAttachments.length > 1) { - logger.warn( - `${logId}: Received more than one long message attachment, ` + - `dropping ${bodyAttachments.length - 1}` - ); - } - - if (bodyAttachments.length > 0) { - if (existingBodyAttachment) { - logger.warn(`${logId}: there is already an existing body attachment`); - } else { - logger.info( - `${logId}: Moving a long message attachment to message.bodyAttachment` - ); - } - } - - return { - bodyAttachment: existingBodyAttachment ?? bodyAttachments[0], - attachments: normalAttachments, - }; -} - -const MESSAGE_ATTACHMENT_TYPES_NEEDING_THUMBNAILS: Set = - new Set(['attachment', 'sticker']); - -export function shouldGenerateThumbnailForAttachmentType( - type: MessageAttachmentType -): boolean { - return MESSAGE_ATTACHMENT_TYPES_NEEDING_THUMBNAILS.has(type); } diff --git a/ts/types/ForwardDraft.ts b/ts/types/ForwardDraft.ts index 1bd6d3d46a..7ac56f695c 100644 --- a/ts/types/ForwardDraft.ts +++ b/ts/types/ForwardDraft.ts @@ -3,11 +3,8 @@ import lodash from 'lodash'; import type { ReadonlyMessageAttributesType } from '../model-types.js'; -import { - isVoiceMessage, - type AttachmentForUIType, - isDownloaded, -} from './Attachment.js'; +import type { AttachmentForUIType } from './Attachment.js'; +import { isVoiceMessage, isDownloaded } from '../util/Attachment.js'; import type { HydratedBodyRangesType } from './BodyRange.js'; import type { LinkPreviewForUIType } from './message/LinkPreviews.js'; diff --git a/ts/types/GiftBadgeStates.ts b/ts/types/GiftBadgeStates.ts new file mode 100644 index 0000000000..57d3f844db --- /dev/null +++ b/ts/types/GiftBadgeStates.ts @@ -0,0 +1,9 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +export enum GiftBadgeStates { + Unopened = 'Unopened', + Opened = 'Opened', + Redeemed = 'Redeemed', + Failed = 'Failed', +} diff --git a/ts/types/HTTPError.ts b/ts/types/HTTPError.ts new file mode 100644 index 0000000000..5621bd1e87 --- /dev/null +++ b/ts/types/HTTPError.ts @@ -0,0 +1,45 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { Response } from 'node-fetch'; + +import type { HeaderListType } from './WebAPI.d.ts'; + +export class HTTPError extends Error { + public override readonly name = 'HTTPError'; + + public readonly code: number; + + public readonly responseHeaders: HeaderListType; + + public readonly response: unknown; + + static fromResponse(response: Response): HTTPError { + return new HTTPError(response.statusText, { + code: response.status, + headers: Object.fromEntries(response.headers), + response, + }); + } + + constructor( + message: string, + options: { + code: number; + headers: HeaderListType; + response?: unknown; + stack?: string; + cause?: unknown; + } + ) { + super(`${message}; code: ${options.code}`, { cause: options.cause }); + + const { code: providedCode, headers, response, stack } = options; + + this.code = providedCode > 999 || providedCode < 100 ? -1 : providedCode; + this.responseHeaders = headers; + + this.stack += `\nOriginal stack:\n${stack}`; + this.response = response; + } +} diff --git a/ts/types/Message2.ts b/ts/types/Message2.ts index 48d856ae81..0b6e463491 100644 --- a/ts/types/Message2.ts +++ b/ts/types/Message2.ts @@ -17,7 +17,7 @@ import { replaceUnicodeOrderOverrides, replaceUnicodeV2, shouldGenerateThumbnailForAttachmentType, -} from './Attachment.js'; +} from '../util/Attachment.js'; import type { MakeVideoScreenshotResultType } from './VisualAttachment.js'; import * as Errors from './errors.js'; import * as SchemaVersion from './SchemaVersion.js'; diff --git a/ts/types/SafetyNumberChangeSource.ts b/ts/types/SafetyNumberChangeSource.ts new file mode 100644 index 0000000000..9f9e07707e --- /dev/null +++ b/ts/types/SafetyNumberChangeSource.ts @@ -0,0 +1,9 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +export enum SafetyNumberChangeSource { + InitiateCall = 'InitiateCall', + JoinCall = 'JoinCall', + MessageSend = 'MessageSend', + Story = 'Story', +} diff --git a/ts/types/WebAPI.d.ts b/ts/types/WebAPI.d.ts new file mode 100644 index 0000000000..59f1c9826f --- /dev/null +++ b/ts/types/WebAPI.d.ts @@ -0,0 +1,4 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +export type HeaderListType = { [name: string]: string | ReadonlyArray }; diff --git a/ts/types/errors.ts b/ts/types/errors.ts index d9961715dd..3c790fc966 100644 --- a/ts/types/errors.ts +++ b/ts/types/errors.ts @@ -1,7 +1,7 @@ // Copyright 2018 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import { HTTPError } from '../textsecure/Errors.js'; +import { HTTPError } from './HTTPError.js'; export function toLogFormat(error: unknown): string { let result = ''; diff --git a/ts/util/Attachment.ts b/ts/util/Attachment.ts new file mode 100644 index 0000000000..c5889be221 --- /dev/null +++ b/ts/util/Attachment.ts @@ -0,0 +1,1183 @@ +// Copyright 2018 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import moment from 'moment'; +import lodash from 'lodash'; +import { blobToArrayBuffer } from 'blob-util'; + +import type { + AttachmentType, + LocalAttachmentV2Type, + AddressableAttachmentType, + AttachmentForUIType, + AttachmentWithHydratedData, + BackupableAttachmentType, + AttachmentDownloadableFromTransitTier, + LocallySavedAttachment, +} from '../types/Attachment.js'; +import type { LoggerType } from '../types/Logging.js'; +import { createLogger } from '../logging/log.js'; +import * as MIME from '../types/MIME.js'; +import { toLogFormat } from '../types/errors.js'; +import { SignalService } from '../protobuf/index.js'; +import { isImageTypeSupported, isVideoTypeSupported } from './GoogleChrome.js'; +import type { LocalizerType } from '../types/Util.js'; +import { ThemeType } from '../types/Util.js'; +import * as GoogleChrome from './GoogleChrome.js'; +import { ReadStatus } from '../messages/MessageReadStatus.js'; +import type { MessageStatusType } from '../types/message/MessageStatus.js'; +import { isMoreRecentThan } from './timestamp.js'; +import { DAY } from './durations/index.js'; +import { getMessageQueueTime } from './getMessageQueueTime.js'; +import { getLocalAttachmentUrl } from './getLocalAttachmentUrl.js'; +import { + isValidAttachmentKey, + isValidDigest, + isValidPlaintextHash, +} from '../types/Crypto.js'; +import { missingCaseError } from './missingCaseError.js'; +import type { MakeVideoScreenshotResultType } from '../types/VisualAttachment.js'; +import type { MessageAttachmentType } from '../types/AttachmentDownload.js'; +import { getFilePathsOwnedByAttachment } from './messageFilePaths.js'; + +const { + isNumber, + padStart, + isFunction, + isUndefined, + isString, + omit, + partition, +} = lodash; + +const logging = createLogger('Attachment'); + +const MAX_TIMELINE_IMAGE_WIDTH = 300; +const MAX_TIMELINE_IMAGE_HEIGHT = MAX_TIMELINE_IMAGE_WIDTH * 1.5; +const MIN_TIMELINE_IMAGE_WIDTH = 200; +const MIN_TIMELINE_IMAGE_HEIGHT = 50; + +const MAX_DISPLAYABLE_IMAGE_WIDTH = 8192; +const MAX_DISPLAYABLE_IMAGE_HEIGHT = 8192; + +// // Incoming message attachment fields +// { +// id: string +// contentType: MIMEType +// data: Uint8Array +// digest: Uint8Array +// fileName?: string +// flags: null +// key: Uint8Array +// size: integer +// thumbnail: Uint8Array +// } + +// // Outgoing message attachment fields +// { +// contentType: MIMEType +// data: Uint8Array +// fileName: string +// size: integer +// } + +// Returns true if `rawAttachment` is a valid attachment based on our current schema. +// Over time, we can expand this definition to become more narrow, e.g. require certain +// fields, etc. +export function isValid( + rawAttachment?: Pick +): rawAttachment is AttachmentType { + // NOTE: We cannot use `_.isPlainObject` because `rawAttachment` is + // deserialized by protobuf: + if (!rawAttachment) { + return false; + } + + return true; +} + +const UNICODE_LEFT_TO_RIGHT_OVERRIDE = '\u202D'; +const UNICODE_RIGHT_TO_LEFT_OVERRIDE = '\u202E'; +const UNICODE_REPLACEMENT_CHARACTER = '\uFFFD'; +const INVALID_CHARACTERS_PATTERN = new RegExp( + `[${UNICODE_LEFT_TO_RIGHT_OVERRIDE}${UNICODE_RIGHT_TO_LEFT_OVERRIDE}]`, + 'g' +); + +// NOTE: Expose synchronous version to do property-based testing using `testcheck`, +// which currently doesn’t support async testing: +// https://github.com/leebyron/testcheck-js/issues/45 +export function _replaceUnicodeOrderOverridesSync( + attachment: AttachmentType +): AttachmentType { + if (!isString(attachment.fileName)) { + return attachment; + } + + const normalizedFilename = attachment.fileName.replace( + INVALID_CHARACTERS_PATTERN, + UNICODE_REPLACEMENT_CHARACTER + ); + const newAttachment = { ...attachment, fileName: normalizedFilename }; + + return newAttachment; +} + +export const replaceUnicodeOrderOverrides = async ( + attachment: AttachmentType +): Promise => { + return _replaceUnicodeOrderOverridesSync(attachment); +}; + +// \u202A-\u202E is LRE, RLE, PDF, LRO, RLO +// \u2066-\u2069 is LRI, RLI, FSI, PDI +// \u200E is LRM +// \u200F is RLM +// \u061C is ALM +const V2_UNWANTED_UNICODE = /[\u202A-\u202E\u2066-\u2069\u200E\u200F\u061C]/g; + +export async function replaceUnicodeV2( + attachment: AttachmentType +): Promise { + if (!isString(attachment.fileName)) { + return attachment; + } + + const fileName = attachment.fileName.replace( + V2_UNWANTED_UNICODE, + UNICODE_REPLACEMENT_CHARACTER + ); + + return { + ...attachment, + fileName, + }; +} + +export function removeSchemaVersion({ + attachment, + logger, +}: { + attachment: AttachmentType; + logger: LoggerType; +}): AttachmentType { + if (!isValid(attachment)) { + logger.error( + 'Attachment.removeSchemaVersion: Invalid input attachment:', + attachment + ); + return attachment; + } + + return omit(attachment, 'schemaVersion'); +} + +export function hasData(attachment: AttachmentType): boolean { + return attachment.data instanceof Uint8Array; +} + +export function loadData( + readAttachmentV2Data: ( + attachment: Partial + ) => Promise +): ( + attachment: Partial +) => Promise { + if (!isFunction(readAttachmentV2Data)) { + throw new TypeError("'readAttachmentData' must be a function"); + } + + return async attachment => { + if (!isValid(attachment)) { + throw new TypeError("'attachment' is not valid"); + } + + const isAlreadyLoaded = Boolean(attachment.data); + if (isAlreadyLoaded) { + return attachment as AttachmentWithHydratedData; + } + + if (!isString(attachment.path)) { + throw new TypeError("'attachment.path' is required"); + } + + const data = await readAttachmentV2Data(attachment); + return { ...attachment, data, size: data.byteLength }; + }; +} + +export function deleteAllAttachmentFilesOnDisk({ + deleteAttachmentOnDisk, + deleteDownloadOnDisk, +}: { + deleteAttachmentOnDisk: (path: string) => Promise; + deleteDownloadOnDisk: (path: string) => Promise; +}): (attachment?: AttachmentType) => Promise { + if (!isFunction(deleteAttachmentOnDisk)) { + throw new TypeError( + 'deleteAttachmentOnDisk: deleteAttachmentOnDisk must be a function' + ); + } + + return async (attachment?: AttachmentType): Promise => { + if (!isValid(attachment)) { + throw new TypeError('deleteData: attachment is not valid'); + } + + const result = getFilePathsOwnedByAttachment(attachment); + await Promise.all( + [...result.externalAttachments].map(deleteAttachmentOnDisk) + ); + await Promise.all([...result.externalDownloads].map(deleteDownloadOnDisk)); + }; +} + +const THUMBNAIL_SIZE = 150; +const THUMBNAIL_CONTENT_TYPE = MIME.IMAGE_PNG; + +export async function captureDimensionsAndScreenshot( + attachment: AttachmentType, + options: { generateThumbnail: boolean }, + params: { + writeNewAttachmentData: ( + data: Uint8Array + ) => Promise; + makeObjectUrl: ( + data: Uint8Array | ArrayBuffer, + contentType: MIME.MIMEType + ) => string; + revokeObjectUrl: (path: string) => void; + getImageDimensions: (params: { + objectUrl: string; + logger: LoggerType; + }) => Promise<{ + width: number; + height: number; + }>; + makeImageThumbnail: (params: { + size: number; + objectUrl: string; + contentType: MIME.MIMEType; + logger: LoggerType; + }) => Promise; + makeVideoScreenshot: (params: { + objectUrl: string; + contentType: MIME.MIMEType; + logger: LoggerType; + }) => Promise; + logger: LoggerType; + } +): Promise { + const { contentType } = attachment; + + const { + writeNewAttachmentData, + makeObjectUrl, + revokeObjectUrl, + getImageDimensions: getImageDimensionsFromURL, + makeImageThumbnail, + makeVideoScreenshot, + logger, + } = params; + + if ( + !GoogleChrome.isImageTypeSupported(contentType) && + !GoogleChrome.isVideoTypeSupported(contentType) + ) { + return attachment; + } + + // If the attachment hasn't been downloaded yet, we won't have a path + if (!attachment.path) { + return attachment; + } + + const localUrl = getLocalAttachmentUrl(attachment); + + if (GoogleChrome.isImageTypeSupported(contentType)) { + try { + const { width, height } = await getImageDimensionsFromURL({ + objectUrl: localUrl, + logger, + }); + let thumbnail: LocalAttachmentV2Type | undefined; + + if (options.generateThumbnail) { + const thumbnailBuffer = await blobToArrayBuffer( + await makeImageThumbnail({ + size: THUMBNAIL_SIZE, + objectUrl: localUrl, + contentType: THUMBNAIL_CONTENT_TYPE, + logger, + }) + ); + + thumbnail = await writeNewAttachmentData( + new Uint8Array(thumbnailBuffer) + ); + } + + return { + ...attachment, + width, + height, + thumbnail: thumbnail + ? { + ...thumbnail, + contentType: THUMBNAIL_CONTENT_TYPE, + width: THUMBNAIL_SIZE, + height: THUMBNAIL_SIZE, + } + : undefined, + }; + } catch (error) { + logger.error( + 'captureDimensionsAndScreenshot:', + 'error processing image; skipping screenshot generation', + toLogFormat(error) + ); + return attachment; + } + } + + let screenshotObjectUrl: string | undefined; + try { + const { blob, duration } = await makeVideoScreenshot({ + objectUrl: localUrl, + contentType: THUMBNAIL_CONTENT_TYPE, + logger, + }); + const screenshotBuffer = await blobToArrayBuffer(blob); + screenshotObjectUrl = makeObjectUrl( + screenshotBuffer, + THUMBNAIL_CONTENT_TYPE + ); + const { width, height } = await getImageDimensionsFromURL({ + objectUrl: screenshotObjectUrl, + logger, + }); + const screenshot = await writeNewAttachmentData( + new Uint8Array(screenshotBuffer) + ); + + let thumbnail: LocalAttachmentV2Type | undefined; + if (options.generateThumbnail) { + const thumbnailBuffer = await blobToArrayBuffer( + await makeImageThumbnail({ + size: THUMBNAIL_SIZE, + objectUrl: screenshotObjectUrl, + contentType: THUMBNAIL_CONTENT_TYPE, + logger, + }) + ); + + thumbnail = await writeNewAttachmentData(new Uint8Array(thumbnailBuffer)); + } + + return { + ...attachment, + duration, + screenshot: { + ...screenshot, + contentType: THUMBNAIL_CONTENT_TYPE, + width, + height, + }, + thumbnail: thumbnail + ? { + ...thumbnail, + contentType: THUMBNAIL_CONTENT_TYPE, + width: THUMBNAIL_SIZE, + height: THUMBNAIL_SIZE, + } + : undefined, + width, + height, + }; + } catch (error) { + logger.error( + 'captureDimensionsAndScreenshot: error processing video; skipping screenshot generation', + toLogFormat(error) + ); + return attachment; + } finally { + if (screenshotObjectUrl !== undefined) { + revokeObjectUrl(screenshotObjectUrl); + } + } +} + +// UI-focused functions + +export function getExtensionForDisplay({ + fileName, + contentType, +}: { + fileName?: string; + contentType: MIME.MIMEType; +}): string | undefined { + if (fileName && fileName.indexOf('.') >= 0) { + const lastPeriod = fileName.lastIndexOf('.'); + const extension = fileName.slice(lastPeriod + 1); + if (extension.length) { + return extension; + } + } + + if (!contentType) { + return undefined; + } + + const slash = contentType.indexOf('/'); + if (slash >= 0) { + return contentType.slice(slash + 1); + } + + return undefined; +} + +export function isAudio(attachments?: ReadonlyArray): boolean { + return Boolean( + attachments && + attachments[0] && + attachments[0].contentType && + !attachments[0].isCorrupted && + MIME.isAudio(attachments[0].contentType) + ); +} + +export function isPlayed( + direction: 'outgoing' | 'incoming', + status: MessageStatusType | undefined, + readStatus: ReadStatus | undefined +): boolean { + if (direction === 'outgoing') { + return status === 'viewed'; + } + return readStatus === ReadStatus.Viewed; +} + +export function canRenderAudio( + attachments?: ReadonlyArray +): boolean { + const firstAttachment = attachments && attachments[0]; + if (!firstAttachment) { + return false; + } + + return ( + isAudio(attachments) && + (isDownloaded(firstAttachment) || isDownloadable(firstAttachment)) + ); +} + +export function canDisplayImage( + attachments?: ReadonlyArray +): boolean { + const { height, width } = + attachments && attachments[0] ? attachments[0] : { height: 0, width: 0 }; + + return Boolean( + height && + height > 0 && + height <= MAX_DISPLAYABLE_IMAGE_HEIGHT && + width && + width > 0 && + width <= MAX_DISPLAYABLE_IMAGE_WIDTH + ); +} + +export function getThumbnailUrl( + attachment: AttachmentForUIType +): string | undefined { + if (attachment.thumbnail) { + return attachment.thumbnail.url; + } + + return getUrl(attachment); +} + +export function getUrl(attachment: AttachmentForUIType): string | undefined { + if (attachment.screenshot) { + return attachment.screenshot.url; + } + + if (isVideoAttachment(attachment)) { + return undefined; + } + + return attachment.url ?? attachment.thumbnailFromBackup?.url; +} + +export function isImage(attachments?: ReadonlyArray): boolean { + return Boolean( + attachments && + attachments[0] && + attachments[0].contentType && + isImageTypeSupported(attachments[0].contentType) + ); +} + +export function isImageAttachment( + attachment?: Pick +): boolean { + return Boolean( + attachment && + attachment.contentType && + isImageTypeSupported(attachment.contentType) + ); +} + +export function canBeTranscoded( + attachment?: Pick +): boolean { + return Boolean( + attachment && + isImageAttachment(attachment) && + !MIME.isGif(attachment.contentType) + ); +} + +export function hasImage(attachments?: ReadonlyArray): boolean { + return Boolean( + attachments && + attachments[0] && + (attachments[0].url || attachments[0].pending || attachments[0].blurHash) + ); +} + +export function isVideo( + attachments?: ReadonlyArray> +): boolean { + if (!attachments || attachments.length === 0) { + return false; + } + return isVideoAttachment(attachments[0]); +} + +export function isVideoAttachment( + attachment?: Pick +): boolean { + if (!attachment || !attachment.contentType) { + return false; + } + return isVideoTypeSupported(attachment.contentType); +} + +export function isGIF(attachments?: ReadonlyArray): boolean { + if (!attachments || attachments.length !== 1) { + return false; + } + + const [attachment] = attachments; + + const flag = SignalService.AttachmentPointer.Flags.GIF; + const hasFlag = + // eslint-disable-next-line no-bitwise + !isUndefined(attachment.flags) && (attachment.flags & flag) === flag; + + return hasFlag && isVideoAttachment(attachment); +} + +function resolveNestedAttachment< + T extends Pick, +>(attachment?: T): T | AttachmentType | undefined { + if (attachment?.textAttachment?.preview?.image) { + return attachment.textAttachment.preview.image; + } + return attachment; +} + +export function isIncremental( + attachment: Pick +): boolean { + return Boolean(attachment.incrementalMac && attachment.chunkSize); +} + +export function isDownloaded( + attachment?: Pick +): boolean { + const resolved = resolveNestedAttachment(attachment); + return Boolean(resolved && (resolved.path || resolved.textAttachment)); +} + +export function isReadyToView( + attachment?: Pick< + AttachmentType, + 'incrementalMac' | 'chunkSize' | 'path' | 'textAttachment' + > +): boolean { + const fullyDownloaded = isDownloaded(attachment); + if (fullyDownloaded) { + return fullyDownloaded; + } + + const resolved = resolveNestedAttachment(attachment); + return Boolean( + resolved && + (resolved.path || resolved.textAttachment || isIncremental(resolved)) + ); +} + +export function hasNotResolved(attachment?: AttachmentType): boolean { + const resolved = resolveNestedAttachment(attachment); + return Boolean(resolved && !resolved.url && !resolved.textAttachment); +} + +export function isDownloading(attachment?: AttachmentType): boolean { + const resolved = resolveNestedAttachment(attachment); + return Boolean(resolved && resolved.pending); +} + +export function hasFailed(attachment?: AttachmentType): boolean { + const resolved = resolveNestedAttachment(attachment); + return Boolean(resolved && resolved.error); +} + +export function hasVideoBlurHash( + attachments?: ReadonlyArray +): boolean { + const firstAttachment = attachments ? attachments[0] : null; + + return Boolean(firstAttachment && firstAttachment.blurHash); +} + +export function hasVideoScreenshot( + attachments?: ReadonlyArray +): string | null | undefined { + const firstAttachment = attachments ? attachments[0] : null; + + return ( + firstAttachment && + firstAttachment.screenshot && + firstAttachment.screenshot.url + ); +} + +type DimensionsType = { + height: number; + width: number; +}; + +export function getImageDimensionsForTimeline( + attachment: Pick, + forcedWidth?: number +): DimensionsType { + const { height, width } = attachment; + if (!height || !width) { + return { + height: MIN_TIMELINE_IMAGE_HEIGHT, + width: MIN_TIMELINE_IMAGE_WIDTH, + }; + } + + const aspectRatio = height / width; + const targetWidth = + forcedWidth || + Math.max( + Math.min(MAX_TIMELINE_IMAGE_WIDTH, width), + MIN_TIMELINE_IMAGE_WIDTH + ); + const candidateHeight = Math.round(targetWidth * aspectRatio); + + return { + width: targetWidth, + height: Math.max( + Math.min(MAX_TIMELINE_IMAGE_HEIGHT, candidateHeight), + MIN_TIMELINE_IMAGE_HEIGHT + ), + }; +} + +export function areAllAttachmentsVisual( + attachments?: ReadonlyArray +): boolean { + if (!attachments) { + return false; + } + + const max = attachments.length; + for (let i = 0; i < max; i += 1) { + const attachment = attachments[i]; + if (!isImageAttachment(attachment) && !isVideoAttachment(attachment)) { + return false; + } + } + + return true; +} + +export function getGridDimensions( + attachments?: ReadonlyArray +): null | DimensionsType { + if (!attachments || !attachments.length) { + return null; + } + + if (!isImage(attachments) && !isVideo(attachments)) { + return null; + } + + if (attachments.length === 1) { + return getImageDimensionsForTimeline(attachments[0]); + } + + if (attachments.length === 2) { + // A B + return { + height: 150, + width: 300, + }; + } + + if (attachments.length === 3) { + // A A B + // A A C + return { + height: 200, + width: 300, + }; + } + + if (attachments.length === 4) { + // A B + // C D + return { + height: 300, + width: 300, + }; + } + + // A A A B B B + // A A A B B B + // A A A B B B + // C C D D E E + // C C D D E E + return { + height: 250, + width: 300, + }; +} + +export function getAlt( + attachment: AttachmentType, + i18n: LocalizerType +): string { + if (isVideoAttachment(attachment)) { + return i18n('icu:videoAttachmentAlt'); + } + return i18n('icu:imageAttachmentAlt'); +} + +// Migration-related attachment stuff + +export const isVisualMedia = (attachment: AttachmentType): boolean => { + const { contentType } = attachment; + + if (isUndefined(contentType)) { + return false; + } + + if (isVoiceMessage(attachment)) { + return false; + } + + return MIME.isImage(contentType) || MIME.isVideo(contentType); +}; + +export const isFile = (attachment: AttachmentType): boolean => { + const { contentType } = attachment; + + if (isUndefined(contentType)) { + return false; + } + + if (isVisualMedia(attachment)) { + return false; + } + + if (isVoiceMessage(attachment)) { + return false; + } + + if (MIME.isLongMessage(contentType)) { + return false; + } + + return true; +}; + +export const isVoiceMessage = ( + attachment: Pick +): boolean => { + const flag = SignalService.AttachmentPointer.Flags.VOICE_MESSAGE; + const hasFlag = + // eslint-disable-next-line no-bitwise + !isUndefined(attachment.flags) && (attachment.flags & flag) === flag; + if (hasFlag) { + return true; + } + + const isLegacyAndroidVoiceMessage = + !isUndefined(attachment.contentType) && + MIME.isAudio(attachment.contentType) && + !attachment.fileName; + if (isLegacyAndroidVoiceMessage) { + return true; + } + + return false; +}; + +export const save = async ({ + attachment, + index, + getUnusedFilename, + readAttachmentData, + saveAttachmentToDisk, + timestamp, + baseDir, +}: { + attachment: AttachmentType; + index?: number; + getUnusedFilename: (options: { + filename: string; + baseDir?: string; + }) => string; + readAttachmentData: ( + attachment: Partial + ) => Promise; + saveAttachmentToDisk: (options: { + data: Uint8Array; + name: string; + baseDir?: string; + }) => Promise<{ name: string; fullPath: string } | null>; + timestamp?: number; + /** + * Base directory for saving the attachment. + * If omitted, a dialog will be opened to let the user choose a directory + */ + baseDir?: string; +}): Promise => { + let data: Uint8Array; + if (attachment.path) { + data = await readAttachmentData(attachment); + } else if (attachment.data) { + data = attachment.data; + } else { + throw new Error('Attachment had neither path nor data'); + } + + const suggestedFilename = getSuggestedFilename({ + attachment, + timestamp, + index, + }); + + /** + * When baseDir is provided, saveAttachmentToDisk() will save without prompting + * and may overwrite existing files, so we need to append a suffix + */ + const name = getUnusedFilename({ filename: suggestedFilename, baseDir }); + + const result = await saveAttachmentToDisk({ + data, + name, + baseDir, + }); + + if (!result) { + return null; + } + + return result.fullPath; +}; + +export const getSuggestedFilename = ({ + attachment, + timestamp, + index, + scenario = 'saving-locally', +}: { + attachment: Pick; + timestamp?: number | Date; + index?: number; + scenario?: 'sending' | 'saving-locally'; +}): string => { + const { fileName } = attachment; + if (fileName) { + return fileName; + } + + let prefix: string; + switch (scenario) { + case 'sending': + // when sending, we prefer a generic 'signal-less' name + prefix = 'image'; + break; + case 'saving-locally': + prefix = 'signal'; + break; + default: + throw missingCaseError(scenario); + } + + const suffix = timestamp + ? moment(timestamp).format('-YYYY-MM-DD-HHmmss') + : ''; + const fileType = getFileExtension(attachment); + const extension = fileType ? `.${fileType}` : ''; + const indexSuffix = + isNumber(index) && index > 1 + ? `_${padStart(index.toString(), 3, '0')}` + : ''; + + return `${prefix}${suffix}${indexSuffix}${extension}`; +}; + +export const getFileExtension = ( + attachment: Pick +): string | undefined => { + if (!attachment.contentType) { + return undefined; + } + + switch (attachment.contentType) { + case 'video/quicktime': + return 'mov'; + case 'audio/mpeg': + return 'mp3'; + default: + return attachment.contentType.split('/')[1]; + } +}; + +export const defaultBlurHash = (theme: ThemeType = ThemeType.light): string => { + if (theme === ThemeType.dark) { + return 'L05OQnoffQofoffQfQfQfQfQfQfQ'; + } + return 'L1Q]+w-;fQ-;~qfQfQfQfQfQfQfQ'; +}; + +export const canBeDownloaded = ( + attachment: Pick +): boolean => { + return Boolean(attachment.digest && attachment.key && !attachment.wasTooBig); +}; + +export function doAttachmentsOnSameMessageMatch( + attachmentA: AttachmentType, + attachmentB: AttachmentType +): boolean { + if ( + isValidPlaintextHash(attachmentA.plaintextHash) && + isValidPlaintextHash(attachmentB.plaintextHash) + ) { + return attachmentA.plaintextHash === attachmentB.plaintextHash; + } + + if (isValidDigest(attachmentA.digest) && isValidDigest(attachmentB.digest)) { + return attachmentA.digest === attachmentB.digest; + } + + return false; +} + +// TODO: DESKTOP-8910 +// This "undownloaded" attachment signature can change once the file is downloaded; we may +// start with only the digest or plaintextHash, but both will be filled in by the time +// it's downloaded +export function getUndownloadedAttachmentSignature( + attachment: AttachmentType +): string { + return `${attachment.digest ?? ''}.${attachment.plaintextHash ?? ''}`; +} + +export function cacheAttachmentBySignature( + attachmentMap: Map, + attachment: AttachmentType +): void { + const { digest, plaintextHash } = attachment; + if (digest) { + attachmentMap.set(digest, attachment); + } + if (plaintextHash) { + attachmentMap.set(plaintextHash, attachment); + } +} + +export function getCachedAttachmentBySignature( + attachmentMap: Map, + attachment: AttachmentType +): T | undefined { + const { digest, plaintextHash } = attachment; + if (digest) { + if (attachmentMap.has(digest)) { + return attachmentMap.get(digest); + } + } + if (plaintextHash) { + if (attachmentMap.has(plaintextHash)) { + return attachmentMap.get(plaintextHash); + } + } + return undefined; +} + +// Extend range in case the attachment is actually still there (this function is meant to +// be optimistic) +const BUFFER_TIME_ON_TRANSIT_TIER = 5 * DAY; + +export function mightStillBeOnTransitTier( + attachment: Pick +): boolean { + if (!attachment.cdnKey) { + return false; + } + if (attachment.cdnNumber == null) { + return false; + } + + if (!attachment.uploadTimestamp) { + // Let's be conservative and still assume it might be downloadable + return true; + } + + if ( + isMoreRecentThan( + attachment.uploadTimestamp, + getMessageQueueTime() + BUFFER_TIME_ON_TRANSIT_TIER + ) + ) { + return true; + } + + return false; +} + +export function hasRequiredInformationForBackup( + attachment: AttachmentType +): attachment is BackupableAttachmentType { + return ( + isValidAttachmentKey(attachment.key) && + isValidPlaintextHash(attachment.plaintextHash) + ); +} + +export function wasImportedFromLocalBackup( + attachment: AttachmentType +): attachment is BackupableAttachmentType { + return ( + hasRequiredInformationForBackup(attachment) && + Boolean(attachment.localBackupPath) && + isValidAttachmentKey(attachment.localKey) + ); +} + +export function canAttachmentHaveThumbnail({ + contentType, +}: Pick): boolean { + return isVideoTypeSupported(contentType) || isImageTypeSupported(contentType); +} + +export function hasRequiredInformationToDownloadFromTransitTier( + attachment: AttachmentType +): attachment is AttachmentDownloadableFromTransitTier { + const hasIntegrityCheck = + isValidDigest(attachment.digest) || + isValidPlaintextHash(attachment.plaintextHash); + if (!hasIntegrityCheck) { + return false; + } + + if (!isValidAttachmentKey(attachment.key)) { + return false; + } + + if (!attachment.cdnKey || attachment.cdnNumber == null) { + return false; + } + + return true; +} + +export function shouldAttachmentEndUpInRemoteBackup({ + attachment, + hasMediaBackups, +}: { + attachment: AttachmentType; + hasMediaBackups: boolean; +}): boolean { + return hasMediaBackups && hasRequiredInformationForBackup(attachment); +} + +export function isDownloadable(attachment: AttachmentType): boolean { + return ( + hasRequiredInformationToDownloadFromTransitTier(attachment) || + shouldAttachmentEndUpInRemoteBackup({ + attachment, + // TODO: DESKTOP-8905 + hasMediaBackups: true, + }) + ); +} + +export function isAttachmentLocallySaved( + attachment: AttachmentType +): attachment is LocallySavedAttachment { + return Boolean(attachment.path); +} + +// We now partition out the bodyAttachment on receipt, but older +// messages may still have a bodyAttachment in the normal attachments field +export function partitionBodyAndNormalAttachments< + T extends Pick, +>( + { + attachments, + existingBodyAttachment, + }: { + attachments: ReadonlyArray; + existingBodyAttachment?: T; + }, + { logId, logger = logging }: { logId: string; logger?: LoggerType } +): { + bodyAttachment: T | undefined; + attachments: Array; +} { + const [bodyAttachments, normalAttachments] = partition( + attachments, + attachment => MIME.isLongMessage(attachment.contentType) + ); + + if (bodyAttachments.length > 1) { + logger.warn( + `${logId}: Received more than one long message attachment, ` + + `dropping ${bodyAttachments.length - 1}` + ); + } + + if (bodyAttachments.length > 0) { + if (existingBodyAttachment) { + logger.warn(`${logId}: there is already an existing body attachment`); + } else { + logger.info( + `${logId}: Moving a long message attachment to message.bodyAttachment` + ); + } + } + + return { + bodyAttachment: existingBodyAttachment ?? bodyAttachments[0], + attachments: normalAttachments, + }; +} + +const MESSAGE_ATTACHMENT_TYPES_NEEDING_THUMBNAILS: Set = + new Set(['attachment', 'sticker']); + +export function shouldGenerateThumbnailForAttachmentType( + type: MessageAttachmentType +): boolean { + return MESSAGE_ATTACHMENT_TYPES_NEEDING_THUMBNAILS.has(type); +} diff --git a/ts/util/attachments.ts b/ts/util/attachments.ts index 546089862b..ae8fcb6aee 100644 --- a/ts/util/attachments.ts +++ b/ts/util/attachments.ts @@ -11,7 +11,7 @@ import type { AttachmentType, UploadedAttachmentType, } from '../types/Attachment.js'; -import { canBeTranscoded } from '../types/Attachment.js'; +import { canBeTranscoded } from './Attachment.js'; import * as Errors from '../types/errors.js'; import * as Bytes from '../Bytes.js'; diff --git a/ts/util/attachments/markAttachmentAsPermanentlyErrored.ts b/ts/util/attachments/markAttachmentAsPermanentlyErrored.ts index 76dac613f0..575c5cf960 100644 --- a/ts/util/attachments/markAttachmentAsPermanentlyErrored.ts +++ b/ts/util/attachments/markAttachmentAsPermanentlyErrored.ts @@ -3,7 +3,7 @@ import lodash from 'lodash'; -import { type AttachmentType } from '../../types/Attachment.js'; +import type { AttachmentType } from '../../types/Attachment.js'; const { omit } = lodash; diff --git a/ts/util/avatarTextSizeCalculator.ts b/ts/util/avatarTextSizeCalculator.ts index 3265617f2d..671abb4432 100644 --- a/ts/util/avatarTextSizeCalculator.ts +++ b/ts/util/avatarTextSizeCalculator.ts @@ -1,5 +1,7 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only + +// eslint-disable-next-line import/no-restricted-paths import { getEmojifyData } from '../components/fun/data/emojis.js'; type FontSizes = { diff --git a/ts/util/blockSendUntilConversationsAreVerified.ts b/ts/util/blockSendUntilConversationsAreVerified.ts index 9e74a3aa94..3beb5752c8 100644 --- a/ts/util/blockSendUntilConversationsAreVerified.ts +++ b/ts/util/blockSendUntilConversationsAreVerified.ts @@ -1,7 +1,7 @@ // Copyright 2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import type { SafetyNumberChangeSource } from '../components/SafetyNumberChangeDialog.js'; +import type { SafetyNumberChangeSource } from '../types/SafetyNumberChangeSource.js'; import { createLogger } from '../logging/log.js'; import { explodePromise } from './explodePromise.js'; import type { diff --git a/ts/util/createIdenticon.tsx b/ts/util/createIdenticon.tsx index e3afb97801..27e514530a 100644 --- a/ts/util/createIdenticon.tsx +++ b/ts/util/createIdenticon.tsx @@ -10,6 +10,7 @@ import { IdenticonSVGForCallLink, IdenticonSVGForContact, IdenticonSVGForGroup, + // eslint-disable-next-line import/no-restricted-paths } from '../components/IdenticonSVG.js'; import { missingCaseError } from './missingCaseError.js'; diff --git a/ts/util/deleteForMe.ts b/ts/util/deleteForMe.ts index 766cd1de5f..aa38152531 100644 --- a/ts/util/deleteForMe.ts +++ b/ts/util/deleteForMe.ts @@ -5,7 +5,7 @@ import lodash from 'lodash'; import { createLogger } from '../logging/log.js'; import { DataReader, DataWriter, deleteAndCleanup } from '../sql/Client.js'; -import { deleteAllAttachmentFilesOnDisk } from '../types/Attachment.js'; +import { deleteAllAttachmentFilesOnDisk } from './Attachment.js'; import type { MessageAttributesType } from '../model-types.js'; import type { ConversationModel } from '../models/conversations.js'; diff --git a/ts/util/downloadAttachment.ts b/ts/util/downloadAttachment.ts index ac5a26bdd6..e4bec1d8ce 100644 --- a/ts/util/downloadAttachment.ts +++ b/ts/util/downloadAttachment.ts @@ -1,18 +1,20 @@ // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import { ErrorCode, LibSignalErrorBase } from '@signalapp/libsignal-client'; +import { + hasRequiredInformationForBackup, + wasImportedFromLocalBackup, +} from './Attachment.js'; import { type AttachmentType, AttachmentVariant, AttachmentPermanentlyUndownloadableError, - hasRequiredInformationForBackup, - wasImportedFromLocalBackup, } from '../types/Attachment.js'; import { downloadAttachment as doDownloadAttachment } from '../textsecure/downloadAttachment.js'; import { downloadAttachmentFromLocalBackup as doDownloadAttachmentFromLocalBackup } from './downloadAttachmentFromLocalBackup.js'; import { MediaTier } from '../types/AttachmentDownload.js'; import { createLogger } from '../logging/log.js'; -import { HTTPError } from '../textsecure/Errors.js'; +import { HTTPError } from '../types/HTTPError.js'; import { toLogFormat } from '../types/errors.js'; import type { ReencryptedAttachmentV2 } from '../AttachmentCrypto.js'; import * as RemoteConfig from '../RemoteConfig.js'; diff --git a/ts/util/downloadAttachmentFromLocalBackup.ts b/ts/util/downloadAttachmentFromLocalBackup.ts index 54331ff9e6..29adafe6b1 100644 --- a/ts/util/downloadAttachmentFromLocalBackup.ts +++ b/ts/util/downloadAttachmentFromLocalBackup.ts @@ -3,7 +3,7 @@ import { existsSync } from 'node:fs'; import lodash from 'lodash'; -import { type BackupableAttachmentType } from '../types/Attachment.js'; +import type { BackupableAttachmentType } from '../types/Attachment.js'; import { decryptAndReencryptLocally, type ReencryptedAttachmentV2, diff --git a/ts/util/getDraftPreview.ts b/ts/util/getDraftPreview.ts index d768241327..efdeb0de70 100644 --- a/ts/util/getDraftPreview.ts +++ b/ts/util/getDraftPreview.ts @@ -5,7 +5,7 @@ import type { ConversationAttributesType } from '../model-types.js'; import type { DraftPreviewType } from '../state/ducks/conversations.js'; import { findAndFormatContact } from './findAndFormatContact.js'; import { hydrateRanges } from '../types/BodyRange.js'; -import { isVoiceMessage } from '../types/Attachment.js'; +import { isVoiceMessage } from './Attachment.js'; import { stripNewlinesForLeftPane } from './stripNewlinesForLeftPane.js'; export function getDraftPreview( diff --git a/ts/util/getGroupMemberships.ts b/ts/util/getGroupMemberships.ts index 3b1c26775e..7292e4b552 100644 --- a/ts/util/getGroupMemberships.ts +++ b/ts/util/getGroupMemberships.ts @@ -1,10 +1,12 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +// eslint-disable-next-line import/no-restricted-paths import type { GroupV2Membership } from '../components/conversation/conversation-details/ConversationDetailsMembershipList.js'; import type { GroupV2PendingMembership, GroupV2RequestingMembership, + // eslint-disable-next-line import/no-restricted-paths } from '../components/conversation/conversation-details/PendingInvites.js'; import type { ConversationType } from '../state/ducks/conversations.js'; import type { ServiceIdString } from '../types/ServiceId.js'; diff --git a/ts/util/getNotificationDataForMessage.ts b/ts/util/getNotificationDataForMessage.ts index 12d0a59f40..c4ddb54e18 100644 --- a/ts/util/getNotificationDataForMessage.ts +++ b/ts/util/getNotificationDataForMessage.ts @@ -6,14 +6,14 @@ import type { ReadonlyDeep } from 'type-fest'; import type { RawBodyRange } from '../types/BodyRange.js'; import type { ReadonlyMessageAttributesType } from '../model-types.d.ts'; import type { ICUStringMessageParamsByKeyType } from '../types/Util.js'; -import * as Attachment from '../types/Attachment.js'; +import * as Attachment from './Attachment.js'; import * as EmbeddedContact from '../types/EmbeddedContact.js'; import * as GroupChange from '../groupChange.js'; import * as MIME from '../types/MIME.js'; import * as Stickers from '../types/Stickers.js'; import * as expirationTimer from './expirationTimer.js'; import { createLogger } from '../logging/log.js'; -import { GiftBadgeStates } from '../components/conversation/Message.js'; +import { GiftBadgeStates } from '../types/GiftBadgeStates.js'; import { dropNull } from './dropNull.js'; import { getCallHistorySelector } from '../state/selectors/callHistory.js'; import { getCallSelector, getActiveCall } from '../state/selectors/calling.js'; diff --git a/ts/util/getStoryDuration.ts b/ts/util/getStoryDuration.ts index 2cd9a50ce1..234bf245d1 100644 --- a/ts/util/getStoryDuration.ts +++ b/ts/util/getStoryDuration.ts @@ -8,7 +8,7 @@ import { isDownloaded, isGIF, isVideo, -} from '../types/Attachment.js'; +} from './Attachment.js'; import { count } from './grapheme.js'; import { SECOND } from './durations/index.js'; import { createLogger } from '../logging/log.js'; diff --git a/ts/util/getStoryReplyText.ts b/ts/util/getStoryReplyText.ts index b39542134b..36c6458b1e 100644 --- a/ts/util/getStoryReplyText.ts +++ b/ts/util/getStoryReplyText.ts @@ -3,7 +3,7 @@ import type { AttachmentType } from '../types/Attachment.js'; import type { LocalizerType } from '../types/Util.js'; -import { isGIF, isImage, isVideo } from '../types/Attachment.js'; +import { isGIF, isImage, isVideo } from './Attachment.js'; export function getStoryReplyText( i18n: LocalizerType, diff --git a/ts/util/groupAndOrderReactions.ts b/ts/util/groupAndOrderReactions.ts index 4183a3bde7..c913c0897a 100644 --- a/ts/util/groupAndOrderReactions.ts +++ b/ts/util/groupAndOrderReactions.ts @@ -4,6 +4,7 @@ import lodash from 'lodash'; import { useMemo } from 'react'; +// eslint-disable-next-line import/no-restricted-paths import type { Reaction } from '../components/conversation/ReactionViewer.js'; import { isEmojiVariantValue, @@ -12,8 +13,10 @@ import { getEmojiVariantByKey, type EmojiVariantKey, type EmojiParentKey, + // eslint-disable-next-line import/no-restricted-paths } from '../components/fun/data/emojis.js'; import { isNotNil } from './isNotNil.js'; +// eslint-disable-next-line import/no-restricted-paths import { useFunEmojiLocalizer } from '../components/fun/useFunEmojiLocalizer.js'; const { groupBy, orderBy } = lodash; diff --git a/ts/util/handleEditMessage.ts b/ts/util/handleEditMessage.ts index a802f7f85e..0222ffe98d 100644 --- a/ts/util/handleEditMessage.ts +++ b/ts/util/handleEditMessage.ts @@ -17,7 +17,7 @@ import { cacheAttachmentBySignature, getCachedAttachmentBySignature, isVoiceMessage, -} from '../types/Attachment.js'; +} from './Attachment.js'; import { isAciString } from './isAciString.js'; import { getMessageIdForLogging } from './idForLogging.js'; import { hasErrors } from '../state/selectors/message.js'; diff --git a/ts/util/handleImageAttachment.ts b/ts/util/handleImageAttachment.ts index 129c1a1d2c..e24b628051 100644 --- a/ts/util/handleImageAttachment.ts +++ b/ts/util/handleImageAttachment.ts @@ -9,7 +9,7 @@ import { blobToArrayBuffer } from '../types/VisualAttachment.js'; import type { MIMEType } from '../types/MIME.js'; import { IMAGE_JPEG, isHeic, stringToMIMEType } from '../types/MIME.js'; import type { InMemoryAttachmentDraftType } from '../types/Attachment.js'; -import { canBeTranscoded } from '../types/Attachment.js'; +import { canBeTranscoded } from './Attachment.js'; import { imageToBlurHash } from './imageToBlurHash.js'; import { scaleImageToLevel } from './scaleImageToLevel.js'; diff --git a/ts/util/isCallSafe.ts b/ts/util/isCallSafe.ts index 0fc2b2158c..007a33c4cf 100644 --- a/ts/util/isCallSafe.ts +++ b/ts/util/isCallSafe.ts @@ -7,7 +7,7 @@ import { createLogger } from '../logging/log.js'; import { blockSendUntilConversationsAreVerified } from './blockSendUntilConversationsAreVerified.js'; import { getRecipientsByConversation } from './getRecipientsByConversation.js'; -import type { SafetyNumberChangeSource } from '../components/SafetyNumberChangeDialog.js'; +import type { SafetyNumberChangeSource } from '../types/SafetyNumberChangeSource.js'; const log = createLogger('isCallSafe'); diff --git a/ts/util/longRunningTaskWrapper.tsx b/ts/util/longRunningTaskWrapper.tsx index c259f1df57..14b0ebe4ff 100644 --- a/ts/util/longRunningTaskWrapper.tsx +++ b/ts/util/longRunningTaskWrapper.tsx @@ -6,10 +6,13 @@ import { createRoot, type Root } from 'react-dom/client'; import * as Errors from '../types/errors.js'; import { createLogger } from '../logging/log.js'; +// eslint-disable-next-line import/no-restricted-paths import { ProgressModal } from '../components/ProgressModal.js'; import { clearTimeoutIfNecessary } from './clearTimeoutIfNecessary.js'; import { sleep } from './sleep.js'; +// eslint-disable-next-line import/no-restricted-paths import { FunDefaultEnglishEmojiLocalizationProvider } from '../components/fun/FunEmojiLocalizationProvider.js'; +// eslint-disable-next-line import/no-restricted-paths import { AxoProvider } from '../axo/AxoProvider.js'; const log = createLogger('longRunningTaskWrapper'); diff --git a/ts/util/lookupConversationWithoutServiceId.ts b/ts/util/lookupConversationWithoutServiceId.ts index e827103e06..2d53557f0f 100644 --- a/ts/util/lookupConversationWithoutServiceId.ts +++ b/ts/util/lookupConversationWithoutServiceId.ts @@ -8,7 +8,7 @@ import { createLogger } from '../logging/log.js'; import type { AciString } from '../types/ServiceId.js'; import * as Errors from '../types/errors.js'; import { ToastType } from '../types/Toast.js'; -import { HTTPError } from '../textsecure/Errors.js'; +import { HTTPError } from '../types/HTTPError.js'; import { strictAssert } from './assert.js'; import type { UUIDFetchStateKeyType } from './uuidFetchState.js'; import { getServiceIdsForE164s } from './getServiceIdsForE164s.js'; diff --git a/ts/util/makeQuote.ts b/ts/util/makeQuote.ts index ac9e1e5367..22c6d54a23 100644 --- a/ts/util/makeQuote.ts +++ b/ts/util/makeQuote.ts @@ -11,7 +11,7 @@ import type { StickerType } from '../types/Stickers.js'; import { IMAGE_JPEG, IMAGE_GIF } from '../types/MIME.js'; import { getAuthor } from '../messages/helpers.js'; import { getQuoteBodyText } from './getQuoteBodyText.js'; -import { isGIF } from '../types/Attachment.js'; +import { isGIF } from './Attachment.js'; import { isGiftBadge, isTapToView } from '../state/selectors/message.js'; import { createLogger } from '../logging/log.js'; import { map, take, collect } from './iterables.js'; diff --git a/ts/util/maybeForwardMessages.ts b/ts/util/maybeForwardMessages.ts index 4bfcc97047..79852007f4 100644 --- a/ts/util/maybeForwardMessages.ts +++ b/ts/util/maybeForwardMessages.ts @@ -5,7 +5,7 @@ import type { AttachmentType } from '../types/Attachment.js'; import type { LinkPreviewWithHydratedData } from '../types/message/LinkPreviews.js'; import type { QuotedMessageType } from '../model-types.js'; import { createLogger } from '../logging/log.js'; -import { SafetyNumberChangeSource } from '../components/SafetyNumberChangeDialog.js'; +import { SafetyNumberChangeSource } from '../types/SafetyNumberChangeSource.js'; import { blockSendUntilConversationsAreVerified } from './blockSendUntilConversationsAreVerified.js'; import { getMessageIdForLogging, diff --git a/ts/util/queueAttachmentDownloads.ts b/ts/util/queueAttachmentDownloads.ts index dbb04a3e67..a51ef213e4 100644 --- a/ts/util/queueAttachmentDownloads.ts +++ b/ts/util/queueAttachmentDownloads.ts @@ -27,7 +27,7 @@ import { getCachedAttachmentBySignature, cacheAttachmentBySignature, getUndownloadedAttachmentSignature, -} from '../types/Attachment.js'; +} from './Attachment.js'; import { AttachmentDownloadUrgency } from '../types/AttachmentDownload.js'; import type { StickerType } from '../types/Stickers.js'; import type { LinkPreviewType } from '../types/message/LinkPreviews.js'; diff --git a/ts/util/resolveDraftAttachmentOnDisk.ts b/ts/util/resolveDraftAttachmentOnDisk.ts index eb8e34f0f6..c5ef79256c 100644 --- a/ts/util/resolveDraftAttachmentOnDisk.ts +++ b/ts/util/resolveDraftAttachmentOnDisk.ts @@ -3,7 +3,7 @@ import { createLogger } from '../logging/log.js'; import type { AttachmentDraftType } from '../types/Attachment.js'; -import { isVideoAttachment } from '../types/Attachment.js'; +import { isVideoAttachment } from './Attachment.js'; import { getLocalAttachmentUrl, AttachmentDisposition, diff --git a/ts/util/sendToGroup.ts b/ts/util/sendToGroup.ts index db8c11f419..633ae0c059 100644 --- a/ts/util/sendToGroup.ts +++ b/ts/util/sendToGroup.ts @@ -42,7 +42,6 @@ import { SendMessageProtoError, UnknownRecipientError, UnregisteredUserError, - HTTPError, } from '../textsecure/Errors.js'; import { IdentityKeys, SenderKeys, Sessions } from '../LibSignalStores.js'; import type { ConversationModel } from '../models/conversations.js'; @@ -55,6 +54,7 @@ import type { import type { SendTypesType } from './handleMessageSend.js'; import { handleMessageSend, shouldSaveProto } from './handleMessageSend.js'; import { SEALED_SENDER, ZERO_ACCESS_KEY } from '../types/SealedSender.js'; +import { HTTPError } from '../types/HTTPError.js'; import { parseIntOrThrow } from './parseIntOrThrow.js'; import { multiRecipient200ResponseSchema, diff --git a/ts/util/setupI18n.tsx b/ts/util/setupI18n.tsx index 06b8ab0b73..9556c28eb4 100644 --- a/ts/util/setupI18n.tsx +++ b/ts/util/setupI18n.tsx @@ -5,6 +5,7 @@ import type { IntlShape } from 'react-intl'; import React from 'react'; import type { LocaleMessagesType } from '../types/I18N.js'; import type { LocalizerType } from '../types/Util.js'; +// eslint-disable-next-line import/no-restricted-paths import { Emojify } from '../components/conversation/Emojify.js'; import { createCachedIntl as createCachedIntlMain, diff --git a/ts/util/showConfirmationDialog.tsx b/ts/util/showConfirmationDialog.tsx index 0faa9afa54..f28457d67b 100644 --- a/ts/util/showConfirmationDialog.tsx +++ b/ts/util/showConfirmationDialog.tsx @@ -3,8 +3,11 @@ import React, { StrictMode } from 'react'; import { createRoot, type Root } from 'react-dom/client'; +// eslint-disable-next-line import/no-restricted-paths import { ConfirmationDialog } from '../components/ConfirmationDialog.js'; +// eslint-disable-next-line import/no-restricted-paths import { FunDefaultEnglishEmojiLocalizationProvider } from '../components/fun/FunEmojiLocalizationProvider.js'; +// eslint-disable-next-line import/no-restricted-paths import { AxoProvider } from '../axo/AxoProvider.js'; type ConfirmationDialogViewProps = { diff --git a/ts/util/timelineUtil.ts b/ts/util/timelineUtil.ts index 7db27faa9c..7a6a52e384 100644 --- a/ts/util/timelineUtil.ts +++ b/ts/util/timelineUtil.ts @@ -3,8 +3,11 @@ import lodash from 'lodash'; import { createLogger } from '../logging/log.js'; +// eslint-disable-next-line import/no-restricted-paths import type { PropsType as TimelinePropsType } from '../components/conversation/Timeline.js'; +// eslint-disable-next-line import/no-restricted-paths import type { TimelineItemType } from '../components/conversation/TimelineItem.js'; +// eslint-disable-next-line import/no-restricted-paths import { WidthBreakpoint } from '../components/_util.js'; import { toLogFormat } from '../types/errors.js'; import { MINUTE } from './durations/index.js'; diff --git a/ts/util/uploadAttachment.ts b/ts/util/uploadAttachment.ts index 7ad7bc8c8a..4f986e9d06 100644 --- a/ts/util/uploadAttachment.ts +++ b/ts/util/uploadAttachment.ts @@ -21,7 +21,7 @@ import { } from '../AttachmentCrypto.js'; import { missingCaseError } from './missingCaseError.js'; import { uuidToBytes } from './uuidToBytes.js'; -import { isVisualMedia } from '../types/Attachment.js'; +import { isVisualMedia } from './Attachment.js'; const CDNS_SUPPORTING_TUS = new Set([3]); diff --git a/ts/util/uploads/tusProtocol.ts b/ts/util/uploads/tusProtocol.ts index 834eb5c4fd..08489e6dd3 100644 --- a/ts/util/uploads/tusProtocol.ts +++ b/ts/util/uploads/tusProtocol.ts @@ -3,7 +3,7 @@ import { type Readable } from 'node:stream'; import fetch, { type RequestInit, type Response } from 'node-fetch'; -import { HTTPError } from '../../textsecure/Errors.js'; +import { HTTPError } from '../../types/HTTPError.js'; import { createLogger } from '../../logging/log.js'; import * as Errors from '../../types/errors.js'; import { sleep } from '../sleep.js'; diff --git a/ts/util/uploads/uploads.ts b/ts/util/uploads/uploads.ts index c449daedaa..2f35f550dc 100644 --- a/ts/util/uploads/uploads.ts +++ b/ts/util/uploads/uploads.ts @@ -5,7 +5,7 @@ import { createReadStream, createWriteStream } from 'node:fs'; import { pipeline } from 'node:stream/promises'; import type { TusFileReader, FetchFunctionType } from './tusProtocol.js'; import { tusResumeUpload, tusUpload } from './tusProtocol.js'; -import { HTTPError } from '../../textsecure/Errors.js'; +import { HTTPError } from '../../types/HTTPError.js'; export const defaultFileReader: TusFileReader = (filePath, offset) => { return createReadStream(filePath, { start: offset }); diff --git a/ts/util/writeDraftAttachment.ts b/ts/util/writeDraftAttachment.ts index f3b564949e..83503ebabc 100644 --- a/ts/util/writeDraftAttachment.ts +++ b/ts/util/writeDraftAttachment.ts @@ -6,7 +6,7 @@ import type { InMemoryAttachmentDraftType, AttachmentDraftType, } from '../types/Attachment.js'; -import { isImageAttachment } from '../types/Attachment.js'; +import { isImageAttachment } from './Attachment.js'; import { getImageDimensions } from '../types/VisualAttachment.js'; import { IMAGE_PNG } from '../types/MIME.js'; import * as Errors from '../types/errors.js';