From 1d45a52da7b9565ade641b18fba09c8d99c5b891 Mon Sep 17 00:00:00 2001 From: Jamie <113370520+jamiebuilds-signal@users.noreply.github.com> Date: Thu, 12 Mar 2026 16:24:01 -0700 Subject: [PATCH] Enable tsconfig noUncheckedIndexedAccess --- app/attachment_channel.main.ts | 4 +- app/main.main.ts | 18 +- app/menu.std.ts | 11 +- build/intl-linter/linter.node.ts | 3 +- build/intl-linter/rules/wrapEmoji.std.ts | 3 +- ts/ConversationController.preload.ts | 4 +- ts/Crypto.node.ts | 17 +- ts/Curve.node.ts | 13 +- ts/RemoteConfig.dom.ts | 4 +- ts/SignalProtocolStore.preload.ts | 21 +- ts/axo/AxoAvatar.dom.stories.tsx | 3 +- ts/axo/AxoTokens.std.ts | 6 +- ts/calling/VideoSupport.preload.ts | 2 +- ts/calling/findBestMatchingDevice.std.ts | 9 +- .../AddUserToAnotherGroupModal.dom.tsx | 2 + .../AnnouncementsOnlyGroupBanner.dom.tsx | 3 +- ts/components/Avatar.dom.stories.tsx | 3 +- ts/components/BadgeDialog.dom.tsx | 3 +- ts/components/CallManager.dom.stories.tsx | 46 ++-- ts/components/CallReactionBurstEmoji.dom.tsx | 3 +- ts/components/CallScreen.dom.stories.tsx | 16 +- ts/components/CallScreen.dom.tsx | 3 +- ts/components/CallingDeviceSelection.dom.tsx | 3 +- ...CallingPendingParticipants.dom.stories.tsx | 12 +- ts/components/CallingPip.dom.tsx | 6 +- .../CallingPreCallInfo.dom.stories.tsx | 12 +- ts/components/CallingPreCallInfo.dom.tsx | 56 ++--- ts/components/CallingToast.dom.tsx | 6 +- ts/components/ChatColorPicker.dom.tsx | 3 +- ts/components/CompositionArea.dom.stories.tsx | 9 +- ts/components/CompositionArea.dom.tsx | 9 +- ts/components/CompositionInput.dom.tsx | 4 +- ts/components/ContactPills.dom.stories.tsx | 29 ++- ts/components/ContextMenu.dom.tsx | 3 +- .../ConversationList.dom.stories.tsx | 4 +- .../EditHistoryMessagesModal.dom.tsx | 2 + .../GroupCallRemoteParticipants.dom.tsx | 19 +- ts/components/GroupMembersNames.dom.tsx | 44 ++-- ts/components/IncomingCallBar.dom.tsx | 17 +- ts/components/LeftPane.dom.stories.tsx | 4 +- ts/components/Lightbox.dom.tsx | 9 +- ts/components/MyStories.dom.stories.tsx | 9 +- ts/components/MyStoryButton.dom.tsx | 9 +- ...GroupInvitedContactsDialog.dom.stories.tsx | 4 +- ...yCreatedGroupInvitedContactsDialog.dom.tsx | 3 +- .../NotificationProfilesMenu.dom.stories.tsx | 14 +- ts/components/PlaybackRateButton.dom.tsx | 5 +- ts/components/PollCreateModal.dom.tsx | 2 +- ts/components/Preferences.dom.stories.tsx | 12 +- .../PreferencesNotificationProfiles.dom.tsx | 3 +- ts/components/ProfileEditor.dom.tsx | 7 +- ts/components/SafetyTipsModal.dom.tsx | 3 +- ts/components/SharedGroupNames.dom.tsx | 26 ++- ts/components/TextAttachment.dom.tsx | 2 +- .../VoiceNotesPlaybackContext.dom.tsx | 4 +- .../conversation/AttachmentList.dom.tsx | 3 +- .../conversation/ContactModal.dom.tsx | 3 +- .../conversation/ConversationView.dom.tsx | 4 +- .../conversation/GroupNotification.dom.tsx | 6 +- .../conversation/GroupV1Migration.dom.tsx | 3 +- ts/components/conversation/ImageGrid.dom.tsx | 186 ++++++++------- ts/components/conversation/Message.dom.tsx | 19 +- .../conversation/ReactionViewer.dom.tsx | 1 + ts/components/conversation/Timeline.dom.tsx | 4 + .../TimelineMessage.dom.stories.tsx | 3 +- .../conversation/TimelineMessage.dom.tsx | 3 +- .../conversation/TypingBubble.dom.tsx | 6 +- .../ChooseGroupMembersModal.dom.tsx | 1 + .../ConversationDetails.dom.stories.tsx | 11 +- .../ConversationDetails.dom.tsx | 3 +- ...ationDetailsMembershipList.dom.stories.tsx | 6 +- .../ConversationDetailsMembershipList.dom.tsx | 3 +- .../GroupMemberLabelEditor.dom.tsx | 5 +- .../PendingInvites.dom.tsx | 42 ++-- .../conversation-details/util.std.ts | 12 +- .../media-gallery/MediaGallery.dom.tsx | 3 +- .../media-gallery/utils/mocks.std.ts | 39 ++-- .../pinned-messages/PinnedMessagesBar.dom.tsx | 3 +- ts/components/fun/FunEmoji.dom.stories.tsx | 3 +- ts/components/fun/panels/FunPanelGifs.dom.tsx | 5 +- .../fun/panels/FunPanelStickers.dom.tsx | 1 + .../fun/virtual/useFunVirtualGrid.dom.tsx | 11 +- .../LeftPaneChooseGroupMembersHelper.dom.tsx | 3 +- .../leftPane/LeftPaneInboxHelper.dom.tsx | 6 +- .../leftPane/LeftPaneSearchHelper.dom.tsx | 3 +- ts/groups.preload.ts | 30 +-- ts/hooks/useRestoreFocus.dom.ts | 2 +- ts/hooks/useSizeObserver.dom.tsx | 3 +- ts/hooks/useTabs.dom.tsx | 3 +- ts/jobs/AttachmentBackupManager.preload.ts | 1 + ts/jobs/conversationJobQueue.preload.ts | 3 +- ts/jobs/helpers/sendNormalMessage.preload.ts | 6 +- ts/jobs/helpers/sendPollTerminate.preload.ts | 3 +- ts/jobs/helpers/sendPollVote.preload.ts | 3 +- ts/jobs/helpers/sendReaction.preload.ts | 3 +- ts/jobs/helpers/sendStory.preload.ts | 59 +++-- ts/logging/log.std.ts | 3 +- ts/logging/main_process_logging.main.ts | 6 +- .../MessageReceipts.preload.ts | 6 +- ts/messageModifiers/Polls.preload.ts | 3 +- ts/messages/copyQuote.preload.ts | 3 +- ts/messages/migrateMessageData.preload.ts | 3 +- ts/models/conversations.preload.ts | 30 ++- .../index.dom.tsx | 3 +- ts/quill/emoji/completion.dom.tsx | 22 +- ts/quill/formatting/matchers.dom.ts | 4 +- ts/quill/mentions/completion.dom.tsx | 12 +- ts/quill/signal-clipboard/util.dom.ts | 7 +- .../build-localized-display-names.node.ts | 18 +- ts/scripts/check-min-os-version.node.ts | 11 +- ts/scripts/constants.std.ts | 1 + ts/scripts/gen-nsis-script.node.ts | 3 +- ts/scripts/notarize-universal-dmg.node.ts | 3 +- ts/scripts/publish-installer-size.node.ts | 3 +- ts/scripts/remove-strings.node.ts | 5 +- ts/scripts/symbolicate-crash-report.node.ts | 9 +- ts/scripts/update-gha.node.ts | 9 +- ts/services/BeforeNavigate.std.ts | 3 +- ts/services/LinkPreview.preload.ts | 3 +- ts/services/MessageCache.preload.ts | 3 +- .../addGlobalKeyboardShortcuts.preload.ts | 2 + ts/services/backups/credentials.preload.ts | 6 +- ts/services/backups/import.preload.ts | 4 +- ts/services/calling.preload.ts | 39 +++- ts/services/groupCredentialFetcher.preload.ts | 6 +- .../releaseNoteAndMegaphoneFetcher.preload.ts | 8 +- ts/services/retryPlaceholders.std.ts | 4 +- ts/services/storage.preload.ts | 6 +- ts/services/storyLoader.preload.ts | 3 +- ts/services/username.preload.ts | 3 +- ts/services/writeProfile.preload.ts | 3 +- ts/sql/Server.node.ts | 48 ++-- ts/sql/hydration.std.ts | 3 +- ts/sql/main.main.ts | 6 + .../migrations/1280-blob-unprocessed.std.ts | 3 + ts/sql/migrations/88-service-ids.std.ts | 6 +- ts/sql/migrations/index.node.ts | 17 +- ts/sql/util.std.ts | 3 +- ts/state/ducks/audioPlayer.preload.ts | 3 +- ts/state/ducks/calling.preload.ts | 2 +- ts/state/ducks/composer.preload.ts | 17 +- ts/state/ducks/conversations.preload.ts | 41 ++-- ts/state/ducks/lightbox.preload.ts | 12 +- ts/state/ducks/mediaGallery.preload.ts | 6 +- ts/state/ducks/safetyNumber.preload.ts | 4 + ts/state/ducks/search.preload.ts | 5 +- ts/state/ducks/stickers.preload.ts | 6 +- ts/state/ducks/stories.preload.ts | 77 +++++-- .../ducks/storyDistributionLists.preload.ts | 12 +- ts/state/selectors/callHistory.std.ts | 4 +- ts/state/selectors/conversations.dom.ts | 44 ++-- ts/state/selectors/message.preload.ts | 12 +- ts/state/selectors/stickers.std.ts | 2 +- ts/state/selectors/stories.preload.ts | 9 +- ts/state/selectors/timeline.preload.ts | 3 +- ts/state/smart/CallManager.preload.tsx | 16 +- ts/state/smart/ToastManager.preload.tsx | 3 +- ts/test-electron/Crypto_test.preload.ts | 12 +- .../MessageReceipts_test.preload.ts | 28 +-- .../SignalProtocolStore_test.preload.ts | 6 +- ts/test-electron/backup/helpers.preload.ts | 3 +- .../linkPreviewFetch_test.preload.ts | 13 +- .../models/conversations_test.preload.ts | 4 +- .../models/messages_test.preload.ts | 4 +- .../quill/emoji/completion_test.dom.tsx | 26 ++- .../quill/mentions/matchers_test.std.ts | 4 + .../AttachmentBackupManager_test.preload.ts | 91 +++++--- .../AttachmentDownloadManager_test.preload.ts | 212 +++++++++++------- ...aseNoteAndMegaphoneFetcher_test.preload.ts | 8 +- .../sql/fullTextSearch_test.preload.ts | 34 +-- .../sql/getRecentStoryReplies_test.preload.ts | 6 +- ts/test-electron/sql/markRead_test.preload.ts | 28 +-- .../sql/notificationProfiles_test.preload.ts | 2 +- .../sql/pollVoteMarkRead_test.preload.ts | 12 +- ts/test-electron/sql/sendLog_test.preload.ts | 10 +- ts/test-electron/sql/stories_test.preload.ts | 18 +- .../sql/storyDistribution_test.preload.ts | 5 +- .../sql/timelineFetches_test.preload.ts | 30 +-- .../state/ducks/calling_test.preload.ts | 16 +- .../state/ducks/conversations_test.preload.ts | 18 +- .../textsecure/generate_keys_test.preload.ts | 6 +- .../addAttachmentToMessage_test.preload.ts | 12 +- .../util/migrateMessageData_test.preload.ts | 10 +- ts/test-helpers/expireTimers.std.ts | 21 +- ts/test-helpers/generateBackup.node.ts | 3 +- ts/test-helpers/getRandomColor.std.ts | 3 +- ts/test-mock/backups/backups_test.node.ts | 7 +- .../call_history_search_bench.node.ts | 3 +- .../benchmarks/convo_open_bench.node.ts | 2 +- .../benchmarks/group_send_bench.node.ts | 6 +- ts/test-mock/benchmarks/send_bench.node.ts | 3 +- ts/test-mock/benchmarks/startup_bench.node.ts | 3 +- ts/test-mock/bootstrap.node.ts | 6 +- .../messaging/attachments_test.node.ts | 20 +- ts/test-mock/messaging/backfill_test.node.ts | 2 +- ts/test-mock/messaging/edit_test.node.ts | 33 +-- ts/test-mock/messaging/lightbox_test.node.ts | 6 +- ts/test-mock/messaging/reaction_test.node.ts | 24 +- ts/test-mock/messaging/readSync_test.node.ts | 3 +- ts/test-mock/messaging/relink_test.node.ts | 19 +- ts/test-mock/messaging/retries_test.node.ts | 9 +- .../messaging/safety_number_test.node.ts | 19 +- .../messaging/sender_key_test.node.ts | 6 +- ts/test-mock/messaging/stories_test.node.ts | 9 +- .../messaging/unknown_contact_test.node.ts | 2 +- .../messaging/unprocessed_test.node.ts | 13 +- .../network/serverAlerts_test.node.ts | 2 +- .../pnp/accept_gv2_invite_test.node.ts | 17 +- ts/test-mock/pnp/change_number_test.node.ts | 3 +- ts/test-mock/pnp/merge_test.node.ts | 2 +- ts/test-mock/pnp/send_gv2_invite_test.node.ts | 2 +- ts/test-mock/pnp/username_test.node.ts | 4 +- ts/test-mock/routing/routing_test.node.ts | 3 +- ts/test-mock/storage/archive_test.node.ts | 3 +- ts/test-mock/storage/conflict_test.node.ts | 14 +- ts/test-mock/storage/fixtures.node.ts | 10 +- .../storage/max_read_keys_test.node.ts | 5 +- ts/test-mock/storage/sticker_test.node.ts | 7 +- .../LeftPaneArchiveHelper_test.dom.ts | 12 +- ...ftPaneChooseGroupMembersHelper_test.dom.ts | 8 +- .../LeftPaneComposeHelper_test.dom.ts | 10 +- .../leftPane/LeftPaneInboxHelper_test.dom.tsx | 42 ++-- .../leftPane/LeftPaneSearchHelper_test.dom.ts | 43 ++-- ...LeftPaneSetGroupMetadataHelper_test.dom.ts | 2 +- .../getConversationInDirection_test.dom.ts | 4 +- .../groupMediaItemsByDate.std.ts | 44 ++-- ts/test-node/jobs/JobQueue_test.node.ts | 4 +- .../messages/MessageSendState_test.std.ts | 2 +- ts/test-node/reactions/util_test.std.ts | 2 +- ts/test-node/sql/migration_1000_test.node.ts | 8 +- ts/test-node/sql/migration_1040_test.node.ts | 2 +- ts/test-node/sql/migration_1060_test.node.ts | 4 +- ts/test-node/sql/migration_1200_test.node.ts | 2 +- ts/test-node/sql/migration_89_test.preload.ts | 42 ++-- ts/test-node/sql/migrations_test.node.ts | 82 ++++--- .../selectors/conversations_test.preload.ts | 26 +-- .../state/selectors/search_test.preload.ts | 4 +- ts/test-node/types/BodyRange_test.std.ts | 2 +- .../types/EmbeddedContact_test.std.ts | 18 +- .../types/NotificationProfile_test.node.ts | 6 +- .../updater/differential_test.node.ts | 14 +- ts/test-node/updater/signature_test.main.ts | 3 +- .../util/getFontNameByTextScript_test.dom.ts | 2 + ts/test-node/util/iterables_test.std.ts | 4 +- ts/test-node/util/logPadding_test.node.ts | 23 +- .../queueAttachmentDownloads_test.preload.ts | 126 +++++------ ts/textsecure/Errors.std.ts | 10 +- ts/textsecure/EventTarget.std.ts | 2 +- ts/textsecure/Helpers.std.ts | 3 +- ts/textsecure/MessageReceiver.preload.ts | 5 +- ts/textsecure/Provisioner.preload.ts | 10 +- ts/textsecure/SendMessage.preload.ts | 6 +- ts/textsecure/WebAPI.preload.ts | 28 ++- ts/types/AttachmentSize.std.ts | 3 +- ts/types/BodyRange.std.ts | 7 +- ts/types/Colors.std.ts | 5 +- ts/types/Megaphone.std.ts | 3 +- ts/types/Message2.preload.ts | 2 + ts/types/QualifiedAddress.std.ts | 8 +- ts/types/Stickers.preload.ts | 25 +-- ts/updater/common.main.ts | 3 +- ts/updater/curve.node.ts | 7 +- ts/updater/differential.node.ts | 11 +- ts/updater/generateSignature.main.ts | 4 +- ts/util/Attachment.std.ts | 5 +- ts/util/BackOff.std.ts | 3 +- ts/util/DelimitedStream.node.ts | 3 +- ts/util/arePinnedConversationsEqual.node.ts | 2 +- ts/util/benchmark/stats.std.ts | 6 +- ts/util/cleanup.preload.ts | 6 +- ts/util/computeBlurHashUrl.std.ts | 9 +- ts/util/createHTTPSAgent.node.ts | 12 +- ts/util/createIdenticon.preload.tsx | 2 + ts/util/createProxyAgent.node.ts | 3 +- ts/util/createSupportUrl.std.ts | 4 +- ts/util/deleteStoryForEveryone.preload.ts | 3 +- ts/util/deliveryReceipt.preload.ts | 4 +- ts/util/desktopCapturer.preload.ts | 6 +- ts/util/downloadOnboardingStory.preload.ts | 6 +- ts/util/editHelpers.std.ts | 6 +- ts/util/expirationTimer.std.ts | 2 +- ts/util/exponentialBackoff.std.ts | 3 +- ts/util/filterAndSortConversations.std.ts | 5 +- ...ndDeleteOnboardingStoryIfExists.preload.ts | 3 +- ts/util/generateDonationReceipt.dom.ts | 2 +- ts/util/generateMessageId.node.ts | 12 +- ts/util/getColorForCallLink.std.ts | 7 +- ts/util/getCountryData.dom.ts | 3 +- ts/util/getFontNameByTextScript.std.ts | 171 +++++++------- ts/util/getHSL.std.ts | 12 +- ts/util/getInitials.std.ts | 3 +- .../getNotificationDataForMessage.preload.ts | 9 +- ts/util/getQuoteBodyText.std.ts | 3 +- ts/util/graphemeAndLinkAwareSlice.std.ts | 3 +- ts/util/groupAndOrderReactions.dom.ts | 3 +- ts/util/groupSendEndorsements.preload.ts | 1 + ts/util/handleRetry.preload.ts | 3 +- ts/util/lint/license_comments.node.ts | 6 +- ts/util/objectMap.std.ts | 3 +- ts/util/scaleImageToLevel.preload.ts | 3 +- ts/util/search.std.ts | 6 +- ts/util/sendStoryMessage.preload.ts | 3 +- ts/util/sendToGroup.preload.ts | 4 +- ts/util/sniffImageMimeType.std.ts | 7 +- ts/util/syncTasks.preload.ts | 3 +- ts/util/userLanguages.std.ts | 3 +- ts/util/whitespaceStringUtil.std.ts | 47 ---- ts/window.d.ts | 30 +++ ts/windows/main/preload_test.preload.ts | 6 +- ts/windows/main/start.preload.ts | 2 +- tsconfig.json | 3 +- 311 files changed, 2146 insertions(+), 1589 deletions(-) delete mode 100644 ts/util/whitespaceStringUtil.std.ts diff --git a/app/attachment_channel.main.ts b/app/attachment_channel.main.ts index 24c059945a..2b70c77e82 100644 --- a/app/attachment_channel.main.ts +++ b/app/attachment_channel.main.ts @@ -791,13 +791,15 @@ function handleRangeRequest({ } // Chromium only sends open-ended ranges: "start-" + type Match = RegExpMatchArray & { 1: string }; const match = range.match(/^bytes=(\d+)-$/); if (match == null) { log.error(`invalid range header: ${range}`); return create200Response(); } - const startParam = safeParseInteger(match[1]); + const [startInput] = match as Match; + const startParam = safeParseInteger(startInput); if (startParam == null) { log.error(`invalid range header: ${range}`); return create200Response(); diff --git a/app/main.main.ts b/app/main.main.ts index 60097d4531..da85a29df2 100644 --- a/app/main.main.ts +++ b/app/main.main.ts @@ -819,10 +819,14 @@ async function createWindow() { maximized: mainWindow.isMaximized(), autoHideMenuBar: mainWindow.autoHideMenuBar, fullscreen: mainWindow.isFullScreen(), - width: size[0], - height: size[1], - x: position[0], - y: position[1], + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + width: size[0]!, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + height: size[1]!, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + x: position[0]!, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + y: position[1]!, }; if ( @@ -1565,8 +1569,10 @@ function showPermissionsPopupWindow(forCalling: boolean, forCamera: boolean) { const size = mainWindow.getSize(); const options = { - width: Math.min(400, size[0]), - height: Math.min(150, size[1]), + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + width: Math.min(400, size[0]!), + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + height: Math.min(150, size[1]!), resizable: false, title: getResolvedMessagesLocale().i18n('icu:allowAccess'), titleBarStyle: nonMainTitleBarStyle, diff --git a/app/menu.std.ts b/app/menu.std.ts index 0f47420e1d..b854d59915 100644 --- a/app/menu.std.ts +++ b/app/menu.std.ts @@ -9,6 +9,7 @@ import type { MenuOptionsType, MenuActionsType, } from '../ts/types/menu.std.js'; +import { strictAssert } from '../ts/util/assert.std.js'; const { isString } = lodash; @@ -220,6 +221,7 @@ export const createTemplate = ( if (includeSetup) { const fileMenu = template[0]; + strictAssert(fileMenu, 'Missing fileMenu'); if (Array.isArray(fileMenu.submenu)) { // These are in reverse order, since we're prepending them one at a time @@ -265,6 +267,7 @@ function updateForMac( // Remove About item and separator from Help menu, since they're in the app menu const aboutMenu = template[4]; + strictAssert(aboutMenu, 'Missing aboutMenu'); if (Array.isArray(aboutMenu.submenu)) { aboutMenu.submenu.pop(); aboutMenu.submenu.pop(); @@ -275,6 +278,7 @@ function updateForMac( // Remove preferences, separator, and quit from the File menu, since they're // in the app menu const fileMenu = template[0]; + strictAssert(fileMenu, 'Missing fileMenu'); if (Array.isArray(fileMenu.submenu)) { fileMenu.submenu.pop(); fileMenu.submenu.pop(); @@ -343,6 +347,7 @@ function updateForMac( }); const editMenu = template[2]; + strictAssert(editMenu, 'Missing editMenu'); if (Array.isArray(editMenu.submenu)) { editMenu.submenu.push( { @@ -366,9 +371,11 @@ function updateForMac( throw new Error('updateForMac: edit.submenu was not an array!'); } + const windowMenu = template[4]; + strictAssert(windowMenu, 'Missing windowMenu'); // Replace Window menu - // eslint-disable-next-line no-param-reassign - template[4].submenu = [ + + windowMenu.submenu = [ { label: i18n('icu:windowMenuMinimize'), accelerator: 'CmdOrCtrl+M', diff --git a/build/intl-linter/linter.node.ts b/build/intl-linter/linter.node.ts index eeb0f4ea9f..9183c0d342 100644 --- a/build/intl-linter/linter.node.ts +++ b/build/intl-linter/linter.node.ts @@ -174,7 +174,8 @@ async function lintMessages() { const key = topProp.key.value; if (process.argv.includes('--test')) { - const test = tests[key]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const test = tests[key]!; const actualErrors = reports.map(report => report.id); deepEqual(actualErrors, test.expectErrors); continue; diff --git a/build/intl-linter/rules/wrapEmoji.std.ts b/build/intl-linter/rules/wrapEmoji.std.ts index 115bfe3df6..272957c9d1 100644 --- a/build/intl-linter/rules/wrapEmoji.std.ts +++ b/build/intl-linter/rules/wrapEmoji.std.ts @@ -37,7 +37,8 @@ export default rule('wrapEmoji', context => { return; } - const child = element.children[0]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const child = element.children[0]!; if (!isLiteralElement(child)) { // non-literal context.report( diff --git a/ts/ConversationController.preload.ts b/ts/ConversationController.preload.ts index ae05b76b4f..0a708450d1 100644 --- a/ts/ConversationController.preload.ts +++ b/ts/ConversationController.preload.ts @@ -1587,9 +1587,7 @@ export class ConversationController { `forceRerender: Starting to loop through ${conversations.length} conversations` ); - for (let i = 0, max = conversations.length; i < max; i += 1) { - const conversation = conversations[i]; - + for (const conversation of conversations) { if (conversation.cachedProps) { conversation.oldCachedProps = conversation.cachedProps; conversation.cachedProps = null; diff --git a/ts/Crypto.node.ts b/ts/Crypto.node.ts index 78f2735fe2..8b588d0fa5 100644 --- a/ts/Crypto.node.ts +++ b/ts/Crypto.node.ts @@ -357,8 +357,8 @@ export function verifyHmacSha256( let result = 0; for (let i = 0; i < theirMac.byteLength; i += 1) { - // eslint-disable-next-line no-bitwise - result |= ourMac[i] ^ theirMac[i]; + // eslint-disable-next-line no-bitwise, @typescript-eslint/no-non-null-assertion + result |= ourMac[i]! ^ theirMac[i]!; } if (result !== 0) { throw new Error('Bad MAC'); @@ -482,8 +482,8 @@ function verifyDigest(data: Uint8Array, theirDigest: Uint8Array): void { const ourDigest = sha256(data); let result = 0; for (let i = 0; i < theirDigest.byteLength; i += 1) { - // eslint-disable-next-line no-bitwise - result |= ourDigest[i] ^ theirDigest[i]; + // eslint-disable-next-line no-bitwise, @typescript-eslint/no-non-null-assertion + result |= ourDigest[i]! ^ theirDigest[i]!; } if (result !== 0) { throw new Error('Bad digest'); @@ -744,7 +744,8 @@ export function getIdentifierHash({ } const digest = hash(HashType.size256, identifier); - return digest[0]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return digest[0]!; } export function generateAvatarColor({ @@ -761,8 +762,10 @@ export function generateAvatarColor({ const hashValue = getIdentifierHash({ aci, e164, pni, groupId }); if (hashValue == null) { - return sample(AvatarColors) || AvatarColors[0]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return sample(AvatarColors) || AvatarColors[0]!; } - return AvatarColors[hashValue % AVATAR_COLOR_COUNT]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return AvatarColors[hashValue % AVATAR_COLOR_COUNT]!; } diff --git a/ts/Curve.node.ts b/ts/Curve.node.ts index 605734c712..dd351e3420 100644 --- a/ts/Curve.node.ts +++ b/ts/Curve.node.ts @@ -145,10 +145,11 @@ export function setPublicKeyTypeByte(publicKey: Uint8Array): void { } export function clampPrivateKey(privateKey: Uint8Array): void { - // eslint-disable-next-line no-bitwise, no-param-reassign - privateKey[0] &= 248; - // eslint-disable-next-line no-bitwise, no-param-reassign - privateKey[31] &= 127; - // eslint-disable-next-line no-bitwise, no-param-reassign - privateKey[31] |= 64; + /* eslint-disable no-bitwise, no-param-reassign */ + /* eslint-disable @typescript-eslint/no-non-null-assertion */ + privateKey[0]! &= 248; + privateKey[31]! &= 127; + privateKey[31]! |= 64; + /* eslint-enable no-bitwise, no-param-reassign */ + /* eslint-enable @typescript-eslint/no-non-null-assertion */ } diff --git a/ts/RemoteConfig.dom.ts b/ts/RemoteConfig.dom.ts index 18edf9e61d..0de6dba541 100644 --- a/ts/RemoteConfig.dom.ts +++ b/ts/RemoteConfig.dom.ts @@ -166,7 +166,9 @@ export function onChange( listeners[key] = keyListeners; return () => { - listeners[key] = listeners[key].filter(l => l !== fn); + if (listeners[key]) { + listeners[key] = listeners[key].filter(l => l !== fn); + } }; } diff --git a/ts/SignalProtocolStore.preload.ts b/ts/SignalProtocolStore.preload.ts index 2a0ff4cf82..dea6fcfc9c 100644 --- a/ts/SignalProtocolStore.preload.ts +++ b/ts/SignalProtocolStore.preload.ts @@ -176,8 +176,7 @@ async function _fillCaches, HydratedType>( const items = await itemsPromise; const cache = new Map>(); - for (let i = 0, max = items.length; i < max; i += 1) { - const fromDB = items[i]; + for (const fromDB of items) { const { id } = fromDB; cache.set(id, { @@ -304,12 +303,12 @@ export class SignalProtocolStore extends EventEmitter { return; } - for (const serviceId of Object.keys(map.value)) { + for (const [serviceId, keyPair] of Object.entries(map.value)) { strictAssert( isServiceIdString(serviceId), 'Invalid identity key serviceId' ); - const { privKey, pubKey } = map.value[serviceId]; + const { privKey, pubKey } = keyPair; const privateKey = PrivateKey.deserialize(privKey); const publicKey = PublicKey.deserialize(pubKey); this.#ourIdentityKeys.set( @@ -327,12 +326,12 @@ export class SignalProtocolStore extends EventEmitter { return; } - for (const serviceId of Object.keys(map.value)) { + for (const [serviceId, registrationId] of Object.entries(map.value)) { strictAssert( isServiceIdString(serviceId), 'Invalid registration id serviceId' ); - this.#ourRegistrationIds.set(serviceId, map.value[serviceId]); + this.#ourRegistrationIds.set(serviceId, registrationId); } })(), (async () => { @@ -1670,10 +1669,7 @@ export class SignalProtocolStore extends EventEmitter { `removeSessionsByConversation: Conversation not found: ${identifier}` ); - const entries = Array.from(this.sessions.values()); - - for (let i = 0, max = entries.length; i < max; i += 1) { - const entry = entries[i]; + for (const entry of this.sessions.values()) { if (entry.fromDB.conversationId === id) { this.sessions.delete(entry.fromDB.id); this.#pendingSessions.delete(entry.fromDB.id); @@ -1695,10 +1691,7 @@ export class SignalProtocolStore extends EventEmitter { log.info('removeSessionsByServiceId: deleting sessions for', serviceId); - const entries = Array.from(this.sessions.values()); - - for (let i = 0, max = entries.length; i < max; i += 1) { - const entry = entries[i]; + for (const entry of this.sessions.values()) { if (entry.fromDB.serviceId === serviceId) { this.sessions.delete(entry.fromDB.id); this.#pendingSessions.delete(entry.fromDB.id); diff --git a/ts/axo/AxoAvatar.dom.stories.tsx b/ts/axo/AxoAvatar.dom.stories.tsx index 53b2736452..bec32f22a8 100644 --- a/ts/axo/AxoAvatar.dom.stories.tsx +++ b/ts/axo/AxoAvatar.dom.stories.tsx @@ -185,7 +185,8 @@ export function Icons(): JSX.Element { {size => ( - + {/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */} + )} diff --git a/ts/axo/AxoTokens.std.ts b/ts/axo/AxoTokens.std.ts index 570153827e..965e0a7c98 100644 --- a/ts/axo/AxoTokens.std.ts +++ b/ts/axo/AxoTokens.std.ts @@ -60,7 +60,8 @@ export namespace AxoTokens { Number.isInteger(hash) && hash >= 0, 'Hash must be positive integer' ); - return ALL_COLOR_NAMES[hash % ALL_COLOR_NAMES.length]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return ALL_COLOR_NAMES[hash % ALL_COLOR_NAMES.length]!; } export type GradientValues = Readonly<{ @@ -96,7 +97,8 @@ export namespace AxoTokens { Number.isInteger(hash) && hash >= 0, 'Hash must be positive integer' ); - return Gradients[hash % Gradients.length]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return Gradients[hash % Gradients.length]!; } export function getGradientsCount(): number { diff --git a/ts/calling/VideoSupport.preload.ts b/ts/calling/VideoSupport.preload.ts index cef7cd9b4c..5878b4a989 100644 --- a/ts/calling/VideoSupport.preload.ts +++ b/ts/calling/VideoSupport.preload.ts @@ -92,7 +92,7 @@ export class GumVideoCapturer { return; } - const settings = this.mediaStream.getVideoTracks()?.[0].getSettings(); + const settings = this.mediaStream.getVideoTracks()?.[0]?.getSettings(); if (!settings?.width || !settings?.height) { return; } diff --git a/ts/calling/findBestMatchingDevice.std.ts b/ts/calling/findBestMatchingDevice.std.ts index 0929085f17..e066878b7f 100644 --- a/ts/calling/findBestMatchingDevice.std.ts +++ b/ts/calling/findBestMatchingDevice.std.ts @@ -58,12 +58,5 @@ export function findBestMatchingCameraId( // By default, pick the first non-IR camera (but allow the user to pick the // infrared if they so desire) - if (matchingId.length > 0) { - return matchingId[0].deviceId; - } - if (nonInfrared.length > 0) { - return nonInfrared[0].deviceId; - } - - return undefined; + return matchingId[0]?.deviceId ?? nonInfrared[0]?.deviceId; } diff --git a/ts/components/AddUserToAnotherGroupModal.dom.tsx b/ts/components/AddUserToAnotherGroupModal.dom.tsx index 1e444c153c..954d156e2e 100644 --- a/ts/components/AddUserToAnotherGroupModal.dom.tsx +++ b/ts/components/AddUserToAnotherGroupModal.dom.tsx @@ -22,6 +22,7 @@ import { ListView } from './ListView.dom.js'; import { ListTile } from './ListTile.dom.js'; import type { ShowToastAction } from '../state/ducks/toast.preload.js'; import { SizeObserver } from '../hooks/useSizeObserver.dom.js'; +import { strictAssert } from '../util/assert.std.js'; const { pick } = lodash; @@ -111,6 +112,7 @@ export function AddUserToAnotherGroupModal({ const handleGetRow = React.useCallback( (idx: number): GroupListItemConversationType => { const convo = filteredConversations[idx]; + strictAssert(convo, 'Missing conversation'); // these are always populated in the case of a group const memberships = convo.memberships ?? []; diff --git a/ts/components/AnnouncementsOnlyGroupBanner.dom.tsx b/ts/components/AnnouncementsOnlyGroupBanner.dom.tsx index 2810e10963..fffe766822 100644 --- a/ts/components/AnnouncementsOnlyGroupBanner.dom.tsx +++ b/ts/components/AnnouncementsOnlyGroupBanner.dom.tsx @@ -12,11 +12,12 @@ import type { PreferredBadgeSelectorType } from '../state/selectors/badges.prelo import { tw } from '../axo/tw.dom.js'; import type { AdminMembershipType } from '../state/selectors/conversations.dom.js'; import { UserText } from './UserText.dom.js'; +import type { ContactNameColorType } from '../types/Colors.std.js'; type PropsType = { getPreferredBadge: PreferredBadgeSelectorType; groupAdmins: Array; - memberColors: Map; + memberColors: Map; i18n: LocalizerType; showConversation: ShowConversationType; theme: ThemeType; diff --git a/ts/components/Avatar.dom.stories.tsx b/ts/components/Avatar.dom.stories.tsx index 45f1be2f37..2e27f69a0d 100644 --- a/ts/components/Avatar.dom.stories.tsx +++ b/ts/components/Avatar.dom.stories.tsx @@ -109,7 +109,8 @@ Default.play = async (context: any) => { const { args, canvasElement } = context; const canvas = within(canvasElement); const [avatar] = canvas.getAllByRole('button'); - await userEvent.click(avatar); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await userEvent.click(avatar!); await expect(args.onClick).toHaveBeenCalled(); }; diff --git a/ts/components/BadgeDialog.dom.tsx b/ts/components/BadgeDialog.dom.tsx index 68e2941aed..ded375d167 100644 --- a/ts/components/BadgeDialog.dom.tsx +++ b/ts/components/BadgeDialog.dom.tsx @@ -78,7 +78,8 @@ function BadgeDialogWithBadges({ currentBadgeIndex = 0; currentBadge = firstBadge; } else { - currentBadge = badges[currentBadgeIndex]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + currentBadge = badges[currentBadgeIndex]!; } const setCurrentBadgeIndex = (index: number): void => { diff --git a/ts/components/CallManager.dom.stories.tsx b/ts/components/CallManager.dom.stories.tsx index 5e56bdb7cc..df79b671e0 100644 --- a/ts/components/CallManager.dom.stories.tsx +++ b/ts/components/CallManager.dom.stories.tsx @@ -36,6 +36,14 @@ import { allRemoteParticipants } from './CallScreen.dom.stories.js'; const { i18n } = window.SignalContext; +const [participant1, participant2, participant3, participant4] = + allRemoteParticipants as [ + GroupCallRemoteParticipantType, + GroupCallRemoteParticipantType, + GroupCallRemoteParticipantType, + GroupCallRemoteParticipantType, + ]; + const getConversation = () => getDefaultConversation({ id: '3051234567', @@ -344,7 +352,7 @@ export function CallLinkLobbyParticipants1Known1Unknown(): React.JSX.Element { = [ - allRemoteParticipants[0], - ]; + const peekedParticipants: Array = [participant1]; for (let n = 12; n > 0; n -= 1) { peekedParticipants.push(getUnknownContact()); } @@ -412,8 +418,8 @@ export function CallLinkWithJoinRequestsOne(): React.JSX.Element { activeCall: getActiveCallForCallLink({ connectionState: GroupCallConnectionState.Connected, joinState: GroupCallJoinState.Joined, - peekedParticipants: [allRemoteParticipants[0]], - pendingParticipants: [allRemoteParticipants[1]], + peekedParticipants: [participant1], + pendingParticipants: [participant2], showParticipantsList: false, }), callLink: FAKE_CALL_LINK_WITH_ADMIN_KEY, @@ -429,7 +435,7 @@ export function CallLinkWithJoinRequestsTwo(): React.JSX.Element { activeCall: getActiveCallForCallLink({ connectionState: GroupCallConnectionState.Connected, joinState: GroupCallJoinState.Joined, - peekedParticipants: [allRemoteParticipants[0]], + peekedParticipants: [participant1], pendingParticipants: allRemoteParticipants.slice(1, 3), showParticipantsList: false, }), @@ -446,7 +452,7 @@ export function CallLinkWithJoinRequestsMany(): React.JSX.Element { activeCall: getActiveCallForCallLink({ connectionState: GroupCallConnectionState.Connected, joinState: GroupCallJoinState.Joined, - peekedParticipants: [allRemoteParticipants[0]], + peekedParticipants: [participant1], pendingParticipants: allRemoteParticipants.slice(1, 11), showParticipantsList: false, }), @@ -463,11 +469,11 @@ export function CallLinkWithJoinRequestUnknownContact(): React.JSX.Element { activeCall: getActiveCallForCallLink({ connectionState: GroupCallConnectionState.Connected, joinState: GroupCallJoinState.Joined, - peekedParticipants: [allRemoteParticipants[0]], + peekedParticipants: [participant1], pendingParticipants: [ getUnknownContact(), - allRemoteParticipants[1], - allRemoteParticipants[2], + participant2, + participant3, ], showParticipantsList: false, }), @@ -484,9 +490,9 @@ export function CallLinkWithJoinRequestsSystemContact(): React.JSX.Element { activeCall: getActiveCallForCallLink({ connectionState: GroupCallConnectionState.Connected, joinState: GroupCallJoinState.Joined, - peekedParticipants: [allRemoteParticipants[0]], + peekedParticipants: [participant1], pendingParticipants: [ - { ...allRemoteParticipants[1], name: 'My System Contact Friend' }, + { ...participant2, name: 'My System Contact Friend' }, ], showParticipantsList: false, }), @@ -503,11 +509,11 @@ export function CallLinkWithJoinRequestsSystemContactMany(): React.JSX.Element { activeCall: getActiveCallForCallLink({ connectionState: GroupCallConnectionState.Connected, joinState: GroupCallJoinState.Joined, - peekedParticipants: [allRemoteParticipants[0]], + peekedParticipants: [participant1], pendingParticipants: [ - { ...allRemoteParticipants[1], name: 'My System Contact Friend' }, - allRemoteParticipants[2], - allRemoteParticipants[3], + { ...participant2, name: 'My System Contact Friend' }, + participant3, + participant4, ], showParticipantsList: false, }), @@ -524,7 +530,7 @@ export function CallLinkWithJoinRequestsParticipantsOpen(): React.JSX.Element { activeCall: getActiveCallForCallLink({ connectionState: GroupCallConnectionState.Connected, joinState: GroupCallJoinState.Joined, - peekedParticipants: [allRemoteParticipants[0]], + peekedParticipants: [participant1], pendingParticipants: allRemoteParticipants.slice(1, 4), }), callLink: FAKE_CALL_LINK_WITH_ADMIN_KEY, @@ -541,7 +547,7 @@ export function CallLinkWithUnknownContacts(): React.JSX.Element { connectionState: GroupCallConnectionState.Connected, joinState: GroupCallJoinState.Joined, remoteParticipants: [ - allRemoteParticipants[0], + participant1, getUnknownParticipant(), getUnknownParticipant(), ], diff --git a/ts/components/CallReactionBurstEmoji.dom.tsx b/ts/components/CallReactionBurstEmoji.dom.tsx index 0efaa641f8..5f4387fd49 100644 --- a/ts/components/CallReactionBurstEmoji.dom.tsx +++ b/ts/components/CallReactionBurstEmoji.dom.tsx @@ -40,7 +40,8 @@ export function CallReactionBurstEmoji({ (index: number) => { return { key: uuid(), - value: values[index % values.length], + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + value: values[index % values.length]!, springConfig: { mass: random(10, 20), tension: random(45, 60), diff --git a/ts/components/CallScreen.dom.stories.tsx b/ts/components/CallScreen.dom.stories.tsx index 48f3a58105..955ce67005 100644 --- a/ts/components/CallScreen.dom.stories.tsx +++ b/ts/components/CallScreen.dom.stories.tsx @@ -36,6 +36,7 @@ import { fakeGetGroupCallVideoFrameSource } from '../test-helpers/fakeGetGroupCa import { CallingToastProvider, useCallingToasts } from './CallingToast.dom.js'; import type { CallingImageDataCache } from './CallManager.dom.js'; import { MINUTE } from '../util/durations/index.std.js'; +import { strictAssert } from '../util/assert.std.js'; const { sample, shuffle, times } = lodash; @@ -542,7 +543,7 @@ export const allRemoteParticipants = times(MAX_PARTICIPANTS).map(index => { : '' } ${index + 1}`, }), - }; + } satisfies GroupCallRemoteParticipantType; }); export function GroupCallManyPaginated(): React.JSX.Element { @@ -851,6 +852,7 @@ export function GroupCallReactionsManyInOrder(): React.JSX.Element { DEFAULT_PREFERRED_REACTION_EMOJI[ i % DEFAULT_PREFERRED_REACTION_EMOJI.length ]; + strictAssert(value, 'Missing value'); return { timestamp, demuxId, value }; }); const [props] = React.useState( @@ -886,7 +888,9 @@ function useReactionsEmitter({ const participantIndex = Math.floor( Math.random() * call.remoteParticipants.length ); - const { demuxId } = call.remoteParticipants[participantIndex]; + const participant = call.remoteParticipants[participantIndex]; + strictAssert(participant, 'Missing participant'); + const { demuxId } = participant; const reactions: ActiveCallReactionsType = [ ...(state.reactions ?? []).filter( @@ -986,9 +990,11 @@ function useHandRaiser( ); const raisedHands = new Set( - participantIndices.map( - index => call.remoteParticipants[index].demuxId - ) + participantIndices.map(index => { + const participant = call.remoteParticipants[index]; + strictAssert(participant, 'Missing participant'); + return participant.demuxId; + }) ); return { diff --git a/ts/components/CallScreen.dom.tsx b/ts/components/CallScreen.dom.tsx index 5c6d6bdcca..ab717b354f 100644 --- a/ts/components/CallScreen.dom.tsx +++ b/ts/components/CallScreen.dom.tsx @@ -483,7 +483,8 @@ export function CallScreen({ isRinging = activeCall.outgoingRing && !activeCall.remoteParticipants.length && - !(groupMembers?.length === 1 && groupMembers[0].id === me.id); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + !(groupMembers?.length === 1 && groupMembers[0]!.id === me.id); hasCallStarted = activeCall.joinState !== GroupCallJoinState.NotJoined; participantCount = activeCall.remoteParticipants.length + 1; conversationsByDemuxId = activeCall.conversationsByDemuxId; diff --git a/ts/components/CallingDeviceSelection.dom.tsx b/ts/components/CallingDeviceSelection.dom.tsx index dd2befbd81..cb0c93b769 100644 --- a/ts/components/CallingDeviceSelection.dom.tsx +++ b/ts/components/CallingDeviceSelection.dom.tsx @@ -80,7 +80,8 @@ function createAudioChangeHandler( return (value: string): void => { changeIODevice({ type, - selectedDevice: devices[Number(value)], + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + selectedDevice: devices[Number(value)]!, }); }; } diff --git a/ts/components/CallingPendingParticipants.dom.stories.tsx b/ts/components/CallingPendingParticipants.dom.stories.tsx index 87bc7ad9bc..9e06b97a79 100644 --- a/ts/components/CallingPendingParticipants.dom.stories.tsx +++ b/ts/components/CallingPendingParticipants.dom.stories.tsx @@ -7,12 +7,18 @@ import type { Meta } from '@storybook/react'; import type { PropsType } from './CallingPendingParticipants.dom.js'; import { CallingPendingParticipants } from './CallingPendingParticipants.dom.js'; import { allRemoteParticipants } from './CallScreen.dom.stories.js'; +import { strictAssert } from '../util/assert.std.js'; const { i18n } = window.SignalContext; +strictAssert(allRemoteParticipants[0], 'Missing allRemoteParticipants[0]'); +strictAssert(allRemoteParticipants[1], 'Missing allRemoteParticipants[1]'); +const participant1 = allRemoteParticipants[0]; +const participant2 = allRemoteParticipants[1]; + const createProps = (storyProps: Partial = {}): PropsType => ({ i18n, - participants: [allRemoteParticipants[0], allRemoteParticipants[1]], + participants: [participant1, participant2], approveUser: action('approve-user'), batchUserAction: action('batch-user-action'), denyUser: action('deny-user'), @@ -32,7 +38,7 @@ export function One(): React.JSX.Element { return ( ); @@ -83,7 +89,7 @@ export function ExpandedOne(): React.JSX.Element { ); diff --git a/ts/components/CallingPip.dom.tsx b/ts/components/CallingPip.dom.tsx index 77c9f45793..368e68d285 100644 --- a/ts/components/CallingPip.dom.tsx +++ b/ts/components/CallingPip.dom.tsx @@ -204,7 +204,7 @@ export function CallingPip({ distanceToRightEdge = innerWidth - (offsetX + width); } - const snapCandidates: Array = [ + const snapCandidates = [ { mode: PositionMode.SnapToLeft, distanceToEdge: distanceToLeftEdge, @@ -221,7 +221,7 @@ export function CallingPip({ mode: PositionMode.SnapToBottom, distanceToEdge: innerHeight - (offsetY + height), }, - ]; + ] as const satisfies Array; // This fallback is mostly for TypeScript, because `minBy` says it can return // `undefined`. @@ -245,7 +245,7 @@ export function CallingPip({ }); break; default: - throw missingCaseError(snapTo.mode); + throw missingCaseError(snapTo); } } }, [height, isRTL, positionState, setPositionState, width]); diff --git a/ts/components/CallingPreCallInfo.dom.stories.tsx b/ts/components/CallingPreCallInfo.dom.stories.tsx index 93d385b287..20ba4e2805 100644 --- a/ts/components/CallingPreCallInfo.dom.stories.tsx +++ b/ts/components/CallingPreCallInfo.dom.stories.tsx @@ -2,7 +2,6 @@ // SPDX-License-Identifier: AGPL-3.0-only import React from 'react'; -import lodash from 'lodash'; import type { Meta } from '@storybook/react'; import { getDefaultConversation } from '../test-helpers/getDefaultConversation.std.js'; import type { PropsType } from './CallingPreCallInfo.dom.js'; @@ -12,8 +11,6 @@ import { generateAci } from '../types/ServiceId.std.js'; import { FAKE_CALL_LINK } from '../test-helpers/fakeCallLink.std.js'; import { callLinkToConversation } from '../util/callLinks.std.js'; -const { times } = lodash; - const { i18n } = window.SignalContext; const getDefaultGroupConversation = () => getDefaultConversation({ @@ -23,7 +20,14 @@ const getDefaultGroupConversation = () => title: 'Tahoe Trip', type: 'group', }); -const otherMembers = times(6, () => getDefaultConversation()); +const otherMembers = [ + getDefaultConversation(), + getDefaultConversation(), + getDefaultConversation(), + getDefaultConversation(), + getDefaultConversation(), + getDefaultConversation(), +] as const; const getUnknownContact = (): ConversationType => ({ acceptedMessageRequest: false, diff --git a/ts/components/CallingPreCallInfo.dom.tsx b/ts/components/CallingPreCallInfo.dom.tsx index 8723bec29c..bde9a7c4b4 100644 --- a/ts/components/CallingPreCallInfo.dom.tsx +++ b/ts/components/CallingPreCallInfo.dom.tsx @@ -50,7 +50,7 @@ export type PropsType = { ringMode: RingMode; // The following should only be set for group conversations. - groupMembers?: Array< + groupMembers?: ReadonlyArray< Pick< ConversationType, 'id' | 'firstName' | 'systemGivenName' | 'systemNickname' | 'title' @@ -58,7 +58,7 @@ export type PropsType = { >; isCallFull?: boolean; isConnecting?: boolean; - peekedParticipants?: Array; + peekedParticipants?: ReadonlyArray; }; export function CallingPreCallInfo({ @@ -110,36 +110,38 @@ export function CallingPreCallInfo({ } return getParticipantName(participant); }); + /* eslint-disable @typescript-eslint/no-non-null-assertion */ switch (participantNames.length) { case 1: subtitle = hasYou ? i18n('icu:calling__pre-call-info--another-device-in-call') : i18n('icu:calling__pre-call-info--1-person-in-call', { - first: participantNames[0], + first: participantNames[0]!, }); break; case 2: subtitle = i18n('icu:calling__pre-call-info--2-people-in-call', { - first: participantNames[0], - second: participantNames[1], + first: participantNames[0]!, + second: participantNames[1]!, }); break; case 3: subtitle = i18n('icu:calling__pre-call-info--3-people-in-call', { - first: participantNames[0], - second: participantNames[1], - third: participantNames[2], + first: participantNames[0]!, + second: participantNames[1]!, + third: participantNames[2]!, }); break; default: subtitle = i18n('icu:calling__pre-call-info--many-people-in-call', { - first: participantNames[0], - second: participantNames[1], + first: participantNames[0]!, + second: participantNames[1]!, others: participantNames.length - 2, }); break; } } + /* eslint-enable @typescript-eslint/no-non-null-assertion */ } else { let memberNames: Array; switch (conversation.type) { @@ -158,6 +160,7 @@ export function CallingPreCallInfo({ const ring = ringMode === RingMode.WillRing; + /* eslint-disable @typescript-eslint/no-non-null-assertion */ switch (memberNames.length) { case 0: subtitle = i18n('icu:calling__pre-call-info--empty-group'); @@ -165,55 +168,56 @@ export function CallingPreCallInfo({ case 1: { subtitle = ring ? i18n('icu:calling__pre-call-info--will-ring-1', { - person: memberNames[0], + person: memberNames[0]!, }) : i18n('icu:calling__pre-call-info--will-notify-1', { - person: memberNames[0], + person: memberNames[0]!, }); break; } case 2: { subtitle = ring ? i18n('icu:calling__pre-call-info--will-ring-2', { - first: memberNames[0], - second: memberNames[1], + first: memberNames[0]!, + second: memberNames[1]!, }) : i18n('icu:calling__pre-call-info--will-notify-2', { - first: memberNames[0], - second: memberNames[1], + first: memberNames[0]!, + second: memberNames[1]!, }); break; } case 3: { subtitle = ring ? i18n('icu:calling__pre-call-info--will-ring-3', { - first: memberNames[0], - second: memberNames[1], - third: memberNames[2], + first: memberNames[0]!, + second: memberNames[1]!, + third: memberNames[2]!, }) : i18n('icu:calling__pre-call-info--will-notify-3', { - first: memberNames[0], - second: memberNames[1], - third: memberNames[2], + first: memberNames[0]!, + second: memberNames[1]!, + third: memberNames[2]!, }); break; } default: { subtitle = ring ? i18n('icu:calling__pre-call-info--will-ring-many', { - first: memberNames[0], - second: memberNames[1], + first: memberNames[0]!, + second: memberNames[1]!, others: memberNames.length - 2, }) : i18n('icu:calling__pre-call-info--will-notify-many', { - first: memberNames[0], - second: memberNames[1], + first: memberNames[0]!, + second: memberNames[1]!, others: memberNames.length - 2, }); break; } } } + /* eslint-enable @typescript-eslint/no-non-null-assertion */ return (
diff --git a/ts/components/CallingToast.dom.tsx b/ts/components/CallingToast.dom.tsx index 4bd9770c41..96f63c8a3f 100644 --- a/ts/components/CallingToast.dom.tsx +++ b/ts/components/CallingToast.dom.tsx @@ -235,7 +235,8 @@ export function CallingToastProvider({ toast => toast.key === item.key ); const isToastReplacingAnExistingOneAtThisPosition = toastsRemoved.has( - previousToasts[enteringItemIndex] + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + previousToasts[enteringItemIndex]! ); return { ...transitionFrom, @@ -275,7 +276,8 @@ export function CallingToastProvider({ toast => toast.key === item.key ); const isToastBeingReplacedByANewOneAtThisPosition = toastsAdded.has( - toasts[leavingItemIndex] + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + toasts[leavingItemIndex]! ); return { zIndex: 0, diff --git a/ts/components/ChatColorPicker.dom.tsx b/ts/components/ChatColorPicker.dom.tsx index 17ff0fb570..c4609b27b9 100644 --- a/ts/components/ChatColorPicker.dom.tsx +++ b/ts/components/ChatColorPicker.dom.tsx @@ -196,8 +196,7 @@ export function ChatColorPicker({ ref={i === 0 ? focusRef : undefined} /> ))} - {Object.keys(customColors).map(colorId => { - const colorValues = customColors[colorId]; + {Object.entries(customColors).map(([colorId, colorValues]) => { return ( { + .map((admin, i): [string, ContactNameColorType] | null => { if (!admin.member.id) { return null; } - return [admin.member.id?.toString(), ContactNameColors[i]]; + return [ + admin.member.id?.toString(), + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + ContactNameColors[i % ContactNameColors.length]!, + ]; }) .filter(isNotNil) ); diff --git a/ts/components/CompositionArea.dom.tsx b/ts/components/CompositionArea.dom.tsx index 75b33378e6..39b66b532b 100644 --- a/ts/components/CompositionArea.dom.tsx +++ b/ts/components/CompositionArea.dom.tsx @@ -91,6 +91,7 @@ import type { PollCreateType } from '../types/Polls.dom.js'; import { PollCreateModal } from './PollCreateModal.dom.js'; import { useDocumentKeyDown } from '../hooks/useDocumentKeyDown.dom.js'; import { hasDraft } from '../util/hasDraft.std.js'; +import type { ContactNameColorType } from '../types/Colors.std.js'; export type OwnProps = Readonly<{ acceptedMessageRequest: boolean | null; @@ -139,7 +140,7 @@ export type OwnProps = Readonly<{ lastEditableMessageId: string | null; recordingState: RecordingState; messageCompositionId: string; - memberColors: Map; + memberColors: Map; shouldHidePopovers: boolean | null; isMuted: boolean; isSmsOnlyOrUnregistered: boolean | null; @@ -1101,7 +1102,11 @@ export const CompositionArea = memo(function CompositionArea({ return renderSmartCompositionRecording(); } - if (draftAttachments.length === 1 && isVoiceMessage(draftAttachments[0])) { + if ( + draftAttachments.length === 1 && + draftAttachments[0] != null && + isVoiceMessage(draftAttachments[0]) + ) { const voiceNoteAttachment = draftAttachments[0]; if (!voiceNoteAttachment.pending && voiceNoteAttachment.url) { diff --git a/ts/components/CompositionInput.dom.tsx b/ts/components/CompositionInput.dom.tsx index b2628f4e10..65f3af1a8d 100644 --- a/ts/components/CompositionInput.dom.tsx +++ b/ts/components/CompositionInput.dom.tsx @@ -934,9 +934,9 @@ export function CompositionInput(props: Props): React.ReactElement { key: tabKey, handler: () => callbacksRef.current.onTab(), }); - const ourHandler = quill.keyboard.bindings[tabKey].pop(); + const ourHandler = quill.keyboard.bindings[tabKey]?.pop(); if (ourHandler) { - quill.keyboard.bindings[tabKey].unshift(ourHandler); + quill.keyboard.bindings[tabKey]?.unshift(ourHandler); } const emojiCompletion = quill.getModule('emojiCompletion'); diff --git a/ts/components/ContactPills.dom.stories.tsx b/ts/components/ContactPills.dom.stories.tsx index 74cfd9bb0f..1f75d138bd 100644 --- a/ts/components/ContactPills.dom.stories.tsx +++ b/ts/components/ContactPills.dom.stories.tsx @@ -2,7 +2,6 @@ // SPDX-License-Identifier: AGPL-3.0-only import React from 'react'; -import lodash from 'lodash'; import { action } from '@storybook/addon-actions'; @@ -13,8 +12,6 @@ import { ContactPill } from './ContactPill.dom.js'; import { gifUrl } from '../storybook/Fixtures.std.js'; import { getDefaultConversation } from '../test-helpers/getDefaultConversation.std.js'; -const { times } = lodash; - const { i18n } = window.SignalContext; export default { @@ -23,15 +20,15 @@ export default { type ContactType = Omit; -const contacts: Array = times(50, index => - getDefaultConversation({ +function createContact(index: number) { + return getDefaultConversation({ id: `contact-${index}`, name: `Contact ${index}`, phoneNumber: '(202) 555-0001', profileName: `C${index}`, title: `Contact ${index}`, - }) -); + }); +} const contactPillProps = ( overrideProps?: ContactType @@ -67,9 +64,9 @@ export function OneContact(): React.JSX.Element { export function ThreeContacts(): React.JSX.Element { return ( - - - + + + ); } @@ -77,16 +74,16 @@ export function ThreeContacts(): React.JSX.Element { export function FourContactsOneWithALongName(): React.JSX.Element { return ( - + - - + + ); } @@ -94,8 +91,8 @@ export function FourContactsOneWithALongName(): React.JSX.Element { export function FiftyContacts(): React.JSX.Element { return ( - {contacts.map(contact => ( - + {Array.from({ length: 50 }, (_, index) => ( + ))} ); diff --git a/ts/components/ContextMenu.dom.tsx b/ts/components/ContextMenu.dom.tsx index 29cb676b42..0f553cbc7e 100644 --- a/ts/components/ContextMenu.dom.tsx +++ b/ts/components/ContextMenu.dom.tsx @@ -191,7 +191,8 @@ export function ContextMenu({ if (ev.key === 'Enter') { if (focusedIndex !== undefined) { - const focusedOption = menuOptions[focusedIndex]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const focusedOption = menuOptions[focusedIndex]!; focusedOption.onClick(focusedOption.value); } setIsMenuShowing(false); diff --git a/ts/components/ConversationList.dom.stories.tsx b/ts/components/ConversationList.dom.stories.tsx index 179225ae99..c5ed24f28a 100644 --- a/ts/components/ConversationList.dom.stories.tsx +++ b/ts/components/ConversationList.dom.stories.tsx @@ -27,7 +27,7 @@ export default { args: {}, } satisfies Meta; -const defaultConversations: Array = [ +const defaultConversations = [ getDefaultConversation({ id: 'fred-convo', title: 'Fred Willard', @@ -48,7 +48,7 @@ const defaultConversations: Array = [ 'Pablo Diego José Francisco de Paula Juan Nepomuceno María de los Remedios Cipriano de la Santísima Trinidad Ruiz y Picasso', }), getDefaultConversation(), -]; +] as const satisfies Array; function Wrapper({ rows, diff --git a/ts/components/EditHistoryMessagesModal.dom.tsx b/ts/components/EditHistoryMessagesModal.dom.tsx index 87c64434a7..e7cb976204 100644 --- a/ts/components/EditHistoryMessagesModal.dom.tsx +++ b/ts/components/EditHistoryMessagesModal.dom.tsx @@ -23,6 +23,7 @@ import { TimelineDateHeader } from './conversation/TimelineDateHeader.dom.js'; import { AxoContextMenu } from '../axo/AxoContextMenu.dom.js'; import { drop } from '../util/drop.std.js'; import type { AxoMenuBuilder } from '../axo/AxoMenuBuilder.dom.js'; +import { strictAssert } from '../util/assert.std.js'; const { noop } = lodash; @@ -115,6 +116,7 @@ export function EditHistoryMessagesModal({ >({}); const [currentMessage, ...pastEdits] = editHistoryMessages; + strictAssert(currentMessage, 'Missing currentMessage'); const currentMessageId = `${currentMessage.id}.${currentMessage.timestamp}`; let previousItem = currentMessage; diff --git a/ts/components/GroupCallRemoteParticipants.dom.tsx b/ts/components/GroupCallRemoteParticipants.dom.tsx index 54447c40ef..fed88b54f1 100644 --- a/ts/components/GroupCallRemoteParticipants.dom.tsx +++ b/ts/components/GroupCallRemoteParticipants.dom.tsx @@ -192,9 +192,12 @@ export function GroupCallRemoteParticipants({ } if (isInSpeakerView) { + const firstParticipiant = prioritySortedParticipants[0]; + strictAssert(firstParticipiant, 'Missing firstParticipiant'); + return [ { - rows: [[prioritySortedParticipants[0]]], + rows: [[firstParticipiant]], hasSpaceRemaining: false, numParticipants: 1, }, @@ -222,7 +225,7 @@ export function GroupCallRemoteParticipants({ const isSomeonePresenting = prioritySortedParticipants.length && - prioritySortedParticipants[0].presenting; + prioritySortedParticipants[0]?.presenting; // Make sure we're not on a page that no longer exists (e.g. if people left the call) if ( @@ -750,6 +753,10 @@ function getGridParticipantsByPage({ PARTICIPANTS_TO_REMOVE_PER_ATTEMPT.length - 1 ) ]; + strictAssert( + numLeastPrioritizedParticipantsToRemove, + 'Missing numLeastPrioritizedParticipantsToRemove' + ); const leastPrioritizedParticipantIds = new Set( priorityParticipantsOnNextPage @@ -804,7 +811,9 @@ function getGridParticipantsByPage({ // Add a previous page tile if needed if (pages.length > 0) { - nextPageTiles.rows[0].unshift({ + const firstRow = nextPageTiles.rows[0]; + strictAssert(firstRow, 'Missing firstRow'); + firstRow.unshift({ isPaginationButton: true, paginationButtonType: 'prev', videoAspectRatio: PAGINATION_BUTTON_ASPECT_RATIO, @@ -877,11 +886,11 @@ function getNextPage({ // Initialize fresh page with empty first row const rows: Array> = [[]]; let row = rows[0]; + strictAssert(row, 'Missing row'); let numParticipants = 0; // Start looping through participants and adding them to the rows one-by-one - for (let i = 0; i < participants.length; i += 1) { - const participant = participants[i]; + for (const [i, participant] of participants.entries()) { const isLastParticipant = !isSubsetOfAllParticipants && i === participants.length - 1; diff --git a/ts/components/GroupMembersNames.dom.tsx b/ts/components/GroupMembersNames.dom.tsx index e16e003540..cb9805fc19 100644 --- a/ts/components/GroupMembersNames.dom.tsx +++ b/ts/components/GroupMembersNames.dom.tsx @@ -54,33 +54,35 @@ function MemberList({ i18n: LocalizerType; onOtherMembersClick?: () => void; }): React.JSX.Element { + const [member1, member2, member3] = firstThreeMemberNames; + if (areWeInGroup) { - if (otherMemberNames.length === 0) { + if (member1 == null) { return ( ); } - if (otherMemberNames.length === 1) { + if (member2 == null) { return ( ); } - if (otherMemberNames.length === 2) { + if (member3 == null) { return ( ); @@ -93,9 +95,9 @@ function MemberList({ i18n={i18n} id="icu:ConversationHero--group-members-other-and-you" components={{ - member1: firstThreeMemberNames[0], - member2: firstThreeMemberNames[1], - member3: firstThreeMemberNames[2], + member1, + member2, + member3, clickable: (parts: ReactNode) => renderClickableButton(parts, onOtherMembersClick), remainingCount, @@ -106,30 +108,30 @@ function MemberList({ // When the user is not in the group - if (otherMemberNames.length === 0) { + if (member1 == null) { return ; } - if (otherMemberNames.length === 1) { + if (member2 == null) { return ( ); } - if (otherMemberNames.length === 2) { + if (member3 == null) { return ( ); @@ -141,9 +143,9 @@ function MemberList({ i18n={i18n} id="icu:ConversationHero--group-members-three" components={{ - member1: firstThreeMemberNames[0], - member2: firstThreeMemberNames[1], - member3: firstThreeMemberNames[2], + member1, + member2, + member3, }} /> ); @@ -156,9 +158,9 @@ function MemberList({ i18n={i18n} id="icu:ConversationHero--group-members-other" components={{ - member1: firstThreeMemberNames[0], - member2: firstThreeMemberNames[1], - member3: firstThreeMemberNames[2], + member1, + member2, + member3, clickable: (parts: ReactNode) => renderClickableButton(parts, onOtherMembersClick), remainingCount, diff --git a/ts/components/IncomingCallBar.dom.tsx b/ts/components/IncomingCallBar.dom.tsx index 694d06d833..b3e1f99443 100644 --- a/ts/components/IncomingCallBar.dom.tsx +++ b/ts/components/IncomingCallBar.dom.tsx @@ -123,6 +123,7 @@ function GroupCallMessage({ .map(member => ); const ringerNode = ; + /* eslint-disable @typescript-eslint/no-non-null-assertion */ switch (otherMembersRung.length) { case 0: return ( @@ -139,7 +140,8 @@ function GroupCallMessage({ i18n={i18n} components={{ ringer: ringerNode, - otherMember: first, + + otherMember: first!, }} /> ); @@ -150,8 +152,8 @@ function GroupCallMessage({ i18n={i18n} components={{ ringer: ringerNode, - first, - second, + first: first!, + second: second!, }} /> ); @@ -162,8 +164,8 @@ function GroupCallMessage({ i18n={i18n} components={{ ringer: ringerNode, - first, - second, + first: first!, + second: second!, }} /> ); @@ -174,13 +176,14 @@ function GroupCallMessage({ i18n={i18n} components={{ ringer: ringerNode, - first, - second, + first: first!, + second: second!, remaining: otherMembersRung.length - 2, }} /> ); } + /* eslint-enable @typescript-eslint/no-non-null-assertion */ } export function IncomingCallBar(props: PropsType): React.JSX.Element | null { diff --git a/ts/components/LeftPane.dom.stories.tsx b/ts/components/LeftPane.dom.stories.tsx index 8ba05b1132..a97fe72f5b 100644 --- a/ts/components/LeftPane.dom.stories.tsx +++ b/ts/components/LeftPane.dom.stories.tsx @@ -51,7 +51,7 @@ export default { args: {}, } satisfies Meta; -const defaultConversations: Array = [ +const defaultConversations = [ getDefaultConversation({ id: 'fred-convo', title: 'Fred Willard', @@ -61,7 +61,7 @@ const defaultConversations: Array = [ isSelected: true, title: 'Marc Barraca', }), -]; +] as const satisfies Array; const defaultSearchProps = { filterByUnread: false, diff --git a/ts/components/Lightbox.dom.tsx b/ts/components/Lightbox.dom.tsx index 2a0ff91f09..0a3b52f72f 100644 --- a/ts/components/Lightbox.dom.tsx +++ b/ts/components/Lightbox.dom.tsx @@ -35,6 +35,7 @@ import { formatFileSize } from '../util/formatFileSize.std.js'; import { SECOND } from '../util/durations/index.std.js'; import { Toast } from './Toast.dom.js'; import { isAbortError } from '../util/isAbortError.std.js'; +import { strictAssert } from '../util/assert.std.js'; const { noop } = lodash; @@ -146,6 +147,7 @@ export function Lightbox({ >(); const currentItem = media[selectedIndex]; + const attachment = currentItem?.attachment; const url = attachment?.url; const incrementalUrl = attachment?.incrementalUrl; @@ -242,6 +244,7 @@ export function Lightbox({ event.preventDefault(); const mediaItem = media[selectedIndex]; + strictAssert(mediaItem, 'Missing mediaItem'); const { attachment: attachmentToSave, message, index } = mediaItem; saveAttachment(attachmentToSave, message.sentAt, index + 1); @@ -261,6 +264,8 @@ export function Lightbox({ closeLightbox(); const mediaItem = media[selectedIndex]; + strictAssert(mediaItem, 'Missing mediaItem'); + toggleForwardMessagesModal({ type: ForwardMessagesModalType.Forward, messageIds: [mediaItem.message.id], @@ -473,6 +478,7 @@ export function Lightbox({ const handleTouchStart = useCallback( (ev: TouchEvent) => { const [touch] = ev.touches; + strictAssert(touch, 'Missing touch'); dragCacheRef.current = { startX: touch.clientX, @@ -493,6 +499,7 @@ export function Lightbox({ } const [touch] = ev.touches; + strictAssert(touch, 'Missing touch'); const deltaX = touch.clientX - dragCache.startX; const deltaY = touch.clientY - dragCache.startY; @@ -724,7 +731,7 @@ export function Lightbox({ ref={focusRef} >
- {getConversation ? ( + {getConversation && currentItem != null ? ( = async ({ }) => { const canvas = within(canvasElement); const [btnDownload] = canvas.getAllByLabelText('Download story'); - await userEvent.click(btnDownload); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await userEvent.click(btnDownload!); await expect(args.onSave).toHaveBeenCalled(); const btnBack = canvas.getByText('Back'); @@ -63,10 +64,12 @@ const interactionTest: PlayFunction = async ({ const [btnCtxMenu] = canvas.getAllByLabelText('Context menu'); - await userEvent.click(btnCtxMenu); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await userEvent.click(btnCtxMenu!); await sleep(300); const [btnFwd] = canvas.getAllByLabelText('Forward'); - await userEvent.click(btnFwd); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await userEvent.click(btnFwd!); await expect(args.onForward).toHaveBeenCalled(); }; diff --git a/ts/components/MyStoryButton.dom.tsx b/ts/components/MyStoryButton.dom.tsx index 9e615e332a..eb6dd8f795 100644 --- a/ts/components/MyStoryButton.dom.tsx +++ b/ts/components/MyStoryButton.dom.tsx @@ -28,7 +28,8 @@ export type PropsType = { }; function getNewestMyStory(story: MyStoryType): StoryViewType { - return story.stories[0]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return story.stories[0]!; } export function MyStoryButton({ @@ -45,7 +46,8 @@ export function MyStoryButton({ const [active, setActive] = useState(false); const newestStory = myStories.length - ? getNewestMyStory(myStories[0]) + ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + getNewestMyStory(myStories[0]!) : undefined; const { avatarUrl, color, profileName, title } = me; @@ -85,7 +87,8 @@ export function MyStoryButton({ } const hasMultiple = myStories.length - ? myStories[0].stories.length > 1 + ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + myStories[0]!.stories.length > 1 : false; const reducedSendStatus: ResolvedSendStatus = myStories.reduce( diff --git a/ts/components/NewlyCreatedGroupInvitedContactsDialog.dom.stories.tsx b/ts/components/NewlyCreatedGroupInvitedContactsDialog.dom.stories.tsx index 2c378461b2..c8c60272dc 100644 --- a/ts/components/NewlyCreatedGroupInvitedContactsDialog.dom.stories.tsx +++ b/ts/components/NewlyCreatedGroupInvitedContactsDialog.dom.stories.tsx @@ -14,10 +14,10 @@ import { ThemeType } from '../types/Util.std.js'; const { i18n } = window.SignalContext; -const conversations: Array = [ +const conversations = [ getDefaultConversation({ title: 'Fred Willard' }), getDefaultConversation({ title: 'Marc Barraca' }), -]; +] as const satisfies Array; export default { title: 'Components/NewlyCreatedGroupInvitedContactsDialog', diff --git a/ts/components/NewlyCreatedGroupInvitedContactsDialog.dom.tsx b/ts/components/NewlyCreatedGroupInvitedContactsDialog.dom.tsx index ea30a57203..417f2dd71f 100644 --- a/ts/components/NewlyCreatedGroupInvitedContactsDialog.dom.tsx +++ b/ts/components/NewlyCreatedGroupInvitedContactsDialog.dom.tsx @@ -29,7 +29,8 @@ export function NewlyCreatedGroupInvitedContactsDialog({ }: PropsType): React.JSX.Element { let body: ReactNode; if (contacts.length === 1) { - const contact = contacts[0]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const contact = contacts[0]!; body = ( <> diff --git a/ts/components/NotificationProfilesMenu.dom.stories.tsx b/ts/components/NotificationProfilesMenu.dom.stories.tsx index 5715047a41..88005db261 100644 --- a/ts/components/NotificationProfilesMenu.dom.stories.tsx +++ b/ts/components/NotificationProfilesMenu.dom.stories.tsx @@ -17,6 +17,7 @@ import type { NotificationProfileIdString } from '../types/NotificationProfile.s import { HOUR } from '../util/durations/index.std.js'; import { AxoDropdownMenu } from '../axo/AxoDropdownMenu.dom.js'; import { AxoButton } from '../axo/AxoButton.dom.js'; +import type { ConversationType } from '../state/ducks/conversations.preload.js'; const { i18n } = window.SignalContext; @@ -25,6 +26,11 @@ const conversations = shuffle([ ...Array.from(Array(20), getDefaultConversation), ]); +const [conversation1, conversation2] = conversations as [ + ConversationType, + ConversationType, +]; + const threeProfiles = [ { id: 'Weekday' as NotificationProfileIdString, @@ -37,7 +43,7 @@ const threeProfiles = [ allowAllCalls: true, allowAllMentions: true, - allowedMembers: new Set([conversations[0].id, conversations[1].id]), + allowedMembers: new Set([conversation1.id, conversation2.id]), scheduleEnabled: true, scheduleStartTime: 1800, @@ -66,7 +72,7 @@ const threeProfiles = [ allowAllCalls: true, allowAllMentions: true, - allowedMembers: new Set([conversations[0].id, conversations[1].id]), + allowedMembers: new Set([conversation1.id, conversation2.id]), scheduleEnabled: true, scheduleStartTime: 1800, @@ -95,7 +101,7 @@ const threeProfiles = [ allowAllCalls: true, allowAllMentions: true, - allowedMembers: new Set([conversations[0].id, conversations[1].id]), + allowedMembers: new Set([conversation1.id, conversation2.id]), scheduleEnabled: true, scheduleStartTime: 1800, @@ -113,7 +119,7 @@ const threeProfiles = [ deletedAtTimestampMs: undefined, storageNeedsSync: true, }, -]; +] as const; export default { title: 'Components/NotificationProfilesMenu', diff --git a/ts/components/PlaybackRateButton.dom.tsx b/ts/components/PlaybackRateButton.dom.tsx index f1b5d85e53..f4a7f257ec 100644 --- a/ts/components/PlaybackRateButton.dom.tsx +++ b/ts/components/PlaybackRateButton.dom.tsx @@ -76,7 +76,7 @@ export function PlaybackRateButton({ }; const label = playbackRate - ? playbackRateLabels[playbackRate].toString() + ? playbackRateLabels[playbackRate]?.toString() : undefined; return ( @@ -105,7 +105,8 @@ const playbackRates = [1, 1.5, 2, 0.5]; PlaybackRateButton.nextPlaybackRate = (currentRate: number): number => { // cycle through the rates + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return playbackRates[ (playbackRates.indexOf(currentRate) + 1) % playbackRates.length - ]; + ]!; }; diff --git a/ts/components/PollCreateModal.dom.tsx b/ts/components/PollCreateModal.dom.tsx index f52c5bf90a..7b4377b550 100644 --- a/ts/components/PollCreateModal.dom.tsx +++ b/ts/components/PollCreateModal.dom.tsx @@ -75,7 +75,7 @@ export function PollCreateModal({ const isLastOption = changedIndex === resultOptions.length - 1; const isSecondToLast = changedIndex === resultOptions.length - 2; const changedOption = resultOptions[changedIndex]; - const hasText = changedOption?.value.trim().length > 0; + const hasText = (changedOption?.value.trim().length ?? 0) > 0; const canAddMore = resultOptions.length < POLL_OPTIONS_MAX_COUNT; const canRemove = resultOptions.length > POLL_OPTIONS_MIN_COUNT; let removedIndex: number | undefined; diff --git a/ts/components/Preferences.dom.stories.tsx b/ts/components/Preferences.dom.stories.tsx index 18de3c9b6e..b5cfa6b9a2 100644 --- a/ts/components/Preferences.dom.stories.tsx +++ b/ts/components/Preferences.dom.stories.tsx @@ -76,6 +76,10 @@ const conversations = shuffle([ ...Array.from(Array(20), getDefaultConversation), ]); +const [conversation1, conversation2] = conversations; +strictAssert(conversation1, 'Missing conversation1'); +strictAssert(conversation2, 'Missing conversation2'); + function conversationSelector(conversationId?: string) { strictAssert(conversationId, 'Missing conversation id'); const found = conversations.find(conversation => { @@ -734,7 +738,7 @@ const threeProfiles = [ allowAllCalls: true, allowAllMentions: true, - allowedMembers: new Set([conversations[0].id, conversations[1].id]), + allowedMembers: new Set([conversation1.id, conversation2.id]), scheduleEnabled: true, scheduleStartTime: 1800, @@ -763,7 +767,7 @@ const threeProfiles = [ allowAllCalls: true, allowAllMentions: true, - allowedMembers: new Set([conversations[0].id, conversations[1].id]), + allowedMembers: new Set([conversation1.id, conversation2.id]), scheduleEnabled: true, scheduleStartTime: 100, @@ -792,7 +796,7 @@ const threeProfiles = [ allowAllCalls: true, allowAllMentions: true, - allowedMembers: new Set([conversations[0].id, conversations[1].id]), + allowedMembers: new Set([conversation1.id, conversation2.id]), scheduleEnabled: true, scheduleStartTime: 1800, @@ -810,7 +814,7 @@ const threeProfiles = [ deletedAtTimestampMs: undefined, storageNeedsSync: true, }, -]; +] as const; NotificationsPageWithThreeProfiles.args = { settingsLocation: { page: SettingsPage.Notifications }, diff --git a/ts/components/PreferencesNotificationProfiles.dom.tsx b/ts/components/PreferencesNotificationProfiles.dom.tsx index 985f6bd314..6aa68d7dac 100644 --- a/ts/components/PreferencesNotificationProfiles.dom.tsx +++ b/ts/components/PreferencesNotificationProfiles.dom.tsx @@ -243,7 +243,8 @@ const ARGB_BITS = 0xff000000; const A100_BACKGROUND_ARGB = 0xffe3e3fe; function getRandomColor(): number { - const colorName = sample(AvatarColors) || AvatarColors[0]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const colorName = sample(AvatarColors) || AvatarColors[0]!; const color = AvatarColorMap.get(colorName); if (!color) { return A100_BACKGROUND_ARGB; // A100, background, with bits for ARGB diff --git a/ts/components/ProfileEditor.dom.tsx b/ts/components/ProfileEditor.dom.tsx index 86cb7b592c..3440269346 100644 --- a/ts/components/ProfileEditor.dom.tsx +++ b/ts/components/ProfileEditor.dom.tsx @@ -29,7 +29,6 @@ import { ConversationDetailsIcon, IconType, } from './conversation/conversation-details/ConversationDetailsIcon.dom.js'; -import { isWhitespace, trim } from '../util/whitespaceStringUtil.std.js'; import { UserText } from './UserText.dom.js'; import { Tooltip, TooltipPlacement } from './Tooltip.dom.js'; import { offsetDistanceModifier } from '../util/popperUtil.std.js'; @@ -294,9 +293,9 @@ export function ProfileEditor({ onProfileChanged( { ...stagedProfile, - firstName: trim(stagedProfile.firstName), + firstName: stagedProfile.firstName.trim(), familyName: stagedProfile.familyName - ? trim(stagedProfile.familyName) + ? stagedProfile.familyName.trim() : undefined, }, { @@ -376,7 +375,7 @@ export function ProfileEditor({ !stagedProfile.firstName || (stagedProfile.firstName === fullName.firstName && stagedProfile.familyName === fullName.familyName) || - isWhitespace(stagedProfile.firstName); + stagedProfile.firstName.trim() === ''; content = ( <> diff --git a/ts/components/SafetyTipsModal.dom.tsx b/ts/components/SafetyTipsModal.dom.tsx index 3405e5adde..6594db3dbe 100644 --- a/ts/components/SafetyTipsModal.dom.tsx +++ b/ts/components/SafetyTipsModal.dom.tsx @@ -56,7 +56,8 @@ export function SafetyTipsModal({ const [cardWrapperId] = useState(() => uuid()); function getCardIdForPage(pageIndex: number) { - return `${cardWrapperId}_${pages[pageIndex].key}`; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return `${cardWrapperId}_${pages[pageIndex]!.key}`; } const maxPageIndex = pages.length - 1; diff --git a/ts/components/SharedGroupNames.dom.tsx b/ts/components/SharedGroupNames.dom.tsx index a7d3fba655..5f2890fd1d 100644 --- a/ts/components/SharedGroupNames.dom.tsx +++ b/ts/components/SharedGroupNames.dom.tsx @@ -29,6 +29,7 @@ export function SharedGroupNames({ )); + /* eslint-disable @typescript-eslint/no-non-null-assertion */ if (sharedGroupNames.length >= 5) { const remainingCount = sharedGroupNames.length - 3; return ( @@ -36,9 +37,9 @@ export function SharedGroupNames({ i18n={i18n} id="icu:member-of-more-than-3-groups--multiple-more" components={{ - group1: firstThreeGroups[0], - group2: firstThreeGroups[1], - group3: firstThreeGroups[2], + group1: firstThreeGroups[0]!, + group2: firstThreeGroups[1]!, + group3: firstThreeGroups[2]!, remainingCount, }} /> @@ -50,9 +51,9 @@ export function SharedGroupNames({ i18n={i18n} id="icu:member-of-more-than-3-groups--one-more" components={{ - group1: firstThreeGroups[0], - group2: firstThreeGroups[1], - group3: firstThreeGroups[2], + group1: firstThreeGroups[0]!, + group2: firstThreeGroups[1]!, + group3: firstThreeGroups[2]!, }} /> ); @@ -63,9 +64,9 @@ export function SharedGroupNames({ i18n={i18n} id="icu:member-of-3-groups" components={{ - group1: firstThreeGroups[0], - group2: firstThreeGroups[1], - group3: firstThreeGroups[2], + group1: firstThreeGroups[0]!, + group2: firstThreeGroups[1]!, + group3: firstThreeGroups[2]!, }} /> ); @@ -76,8 +77,8 @@ export function SharedGroupNames({ i18n={i18n} id="icu:member-of-2-groups" components={{ - group1: firstThreeGroups[0], - group2: firstThreeGroups[1], + group1: firstThreeGroups[0]!, + group2: firstThreeGroups[1]!, }} /> ); @@ -88,11 +89,12 @@ export function SharedGroupNames({ i18n={i18n} id="icu:member-of-1-group" components={{ - group: firstThreeGroups[0], + group: firstThreeGroups[0]!, }} /> ); } + /* eslint-enable @typescript-eslint/no-non-null-assertion */ return <>{i18n('icu:no-groups-in-common')}; } diff --git a/ts/components/TextAttachment.dom.tsx b/ts/components/TextAttachment.dom.tsx index 7380b07bc6..f519ddc842 100644 --- a/ts/components/TextAttachment.dom.tsx +++ b/ts/components/TextAttachment.dom.tsx @@ -75,7 +75,7 @@ function getFont( textStyle?: TextAttachmentStyleType | null, i18n?: LocalizerType ): string { - const textStyleIndex = Number(textStyle) || 0; + const textStyleIndex = textStyle ?? TextAttachmentStyleType.DEFAULT; const fontName = getFontNameByTextScript(text, textStyleIndex, i18n); let fontSize = FONT_SIZE_SMALL; diff --git a/ts/components/VoiceNotesPlaybackContext.dom.tsx b/ts/components/VoiceNotesPlaybackContext.dom.tsx index b9d42be2be..ae0ecbddd9 100644 --- a/ts/components/VoiceNotesPlaybackContext.dom.tsx +++ b/ts/components/VoiceNotesPlaybackContext.dom.tsx @@ -122,9 +122,9 @@ async function doComputePeaks( ) { const channel = data.getChannelData(channelNum); - for (let sample = 0; sample < channel.length; sample += 1) { + for (const [sample, sampleData] of channel.entries()) { const i = Math.floor(sample / samplesPerPeak); - peaks[i] += channel[sample] ** 2; + peaks[i] += sampleData ** 2; norms[i] += 1; } } diff --git a/ts/components/conversation/AttachmentList.dom.tsx b/ts/components/conversation/AttachmentList.dom.tsx index ec540af5a3..ff6b417fc4 100644 --- a/ts/components/conversation/AttachmentList.dom.tsx +++ b/ts/components/conversation/AttachmentList.dom.tsx @@ -97,7 +97,8 @@ export function AttachmentList<
{attachments.map((attachment, index) => { const url = getUrl(attachment); - const forUI = attachmentsForUI[index]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const forUI = attachmentsForUI[index]!; const key = attachment.clientUuid || diff --git a/ts/components/conversation/ContactModal.dom.tsx b/ts/components/conversation/ContactModal.dom.tsx index 51e487df17..2887222321 100644 --- a/ts/components/conversation/ContactModal.dom.tsx +++ b/ts/components/conversation/ContactModal.dom.tsx @@ -39,6 +39,7 @@ import { AxoSymbol } from '../../axo/AxoSymbol.dom.js'; import { tw } from '../../axo/tw.dom.js'; import { strictAssert } from '../../util/assert.std.js'; import type { RemoveClientType } from '../../types/Calling.std.js'; +import type { ContactNameColorType } from '../../types/Colors.std.js'; const ACCESS_ENUM = Proto.AccessControl.AccessRequired; @@ -52,7 +53,7 @@ export type PropsDataType = { contact?: ConversationType; contactLabelEmoji: string | undefined; contactLabelString: string | undefined; - contactNameColor: string | undefined; + contactNameColor: ContactNameColorType | undefined; conversation?: ConversationType; hasStories?: HasStories; readonly i18n: LocalizerType; diff --git a/ts/components/conversation/ConversationView.dom.tsx b/ts/components/conversation/ConversationView.dom.tsx index e5432c8610..17454b6e9a 100644 --- a/ts/components/conversation/ConversationView.dom.tsx +++ b/ts/components/conversation/ConversationView.dom.tsx @@ -114,8 +114,8 @@ export function ConversationView({ }); if (allVisual) { const files: Array = []; - for (let i = 0; i < items.length; i += 1) { - const file = getAsFile(items[i]); + for (const item of items) { + const file = getAsFile(item); if (file) { files.push(file); } diff --git a/ts/components/conversation/GroupNotification.dom.tsx b/ts/components/conversation/GroupNotification.dom.tsx index 32bb8de487..cbcadff62f 100644 --- a/ts/components/conversation/GroupNotification.dom.tsx +++ b/ts/components/conversation/GroupNotification.dom.tsx @@ -92,7 +92,8 @@ function GroupNotificationChange({ ) : ( ) : ( ; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const contact = ; return (

{kind === 'invited' && ( diff --git a/ts/components/conversation/ImageGrid.dom.tsx b/ts/components/conversation/ImageGrid.dom.tsx index c1e3bb0a48..3420a4951e 100644 --- a/ts/components/conversation/ImageGrid.dom.tsx +++ b/ts/components/conversation/ImageGrid.dom.tsx @@ -23,6 +23,7 @@ import { Image, CurveType } from './Image.dom.js'; import type { LocalizerType, ThemeType } from '../../types/Util.std.js'; import { AttachmentDetailPill } from './AttachmentDetailPill.dom.js'; +import { strictAssert } from '../../util/assert.std.js'; export type DirectionType = 'incoming' | 'outgoing'; @@ -158,10 +159,13 @@ export function ImageGrid({ ); const showAttachmentOrNoLongerAvailableToast = React.useCallback( - (attachmentIndex: number) => - attachments[attachmentIndex].isPermanentlyUndownloadable + (attachmentIndex: number) => { + const attachment = attachments[attachmentIndex]; + strictAssert(attachment, 'Missing attachment'); + return attachment.isPermanentlyUndownloadable ? showMediaNoLongerAvailableToast - : showVisualAttachment, + : showVisualAttachment; + }, [attachments, showVisualAttachment, showMediaNoLongerAvailableToast] ); @@ -189,8 +193,10 @@ export function ImageGrid({ }); if (attachments.length === 1 || !areAllAttachmentsVisual(attachments)) { + const [attachment] = attachments; + strictAssert(attachment, 'Missing attachment'); const { height, width } = getImageDimensionsForTimeline( - attachments[0], + attachment, isSticker ? stickerSize : undefined ); @@ -203,10 +209,10 @@ export function ImageGrid({ )} > {getAlt(attachments[0], {getAlt(attachments[0], {getAlt(attachments[1], {getAlt(attachments[0],

{getAlt(attachments[1], {getAlt(attachments[2],
{getAlt(attachments[0], {getAlt(attachments[1],
{getAlt(attachments[2], {getAlt(attachments[3],
{getAlt(attachments[0], {getAlt(attachments[1],
{getAlt(attachments[2], {getAlt(attachments[3], {getAlt(attachments[4], { - const isMe = res.some(re => Boolean(re.from.isMe)); - const count = res.length; - const { emoji } = res[0]; + const toRender = take(ordered, 3).map(group => { + const isMe = group.some(re => Boolean(re.from.isMe)); + const count = group.length; + const firstReaction = group[0]; + strictAssert(firstReaction, 'Missing firstReaction'); + const { emoji } = firstReaction; let label: string; if (isMe) { label = i18n('icu:Message__reaction-emoji-label--you', { emoji }); } else if (count === 1) { label = i18n('icu:Message__reaction-emoji-label--single', { - title: res[0].from.title, + title: firstReaction.from.title, emoji, }); } else { @@ -2543,6 +2545,7 @@ export class Message extends React.PureComponent { } const onlyPreview = previews[0]; + strictAssert(onlyPreview, 'Missing onlyPreview'); return Boolean(onlyPreview.isCallLink); } @@ -2551,6 +2554,7 @@ export class Message extends React.PureComponent { if (this.#shouldShowJoinButton()) { const firstPreview = previews[0]; + strictAssert(firstPreview, 'Missing firstPreview'); const inAnotherCall = Boolean( activeCallConversationId && (!firstPreview.callLinkRoomId || @@ -2672,6 +2676,7 @@ export class Message extends React.PureComponent { if (previews && previews.length) { const first = previews[0]; + strictAssert(first, 'Missing first'); const { image } = first; return isImageAttachment(image); @@ -2688,6 +2693,7 @@ export class Message extends React.PureComponent { } const first = attachments[0]; + strictAssert(first, 'Missing first'); return Boolean(first.pending); } @@ -3216,6 +3222,7 @@ export class Message extends React.PureComponent { event.stopPropagation(); const attachment = attachments[0]; + strictAssert(attachment, 'Missing attachment'); showLightbox({ attachment, messageId: id }); } diff --git a/ts/components/conversation/ReactionViewer.dom.tsx b/ts/components/conversation/ReactionViewer.dom.tsx index e3925f3ebb..345a577ce5 100644 --- a/ts/components/conversation/ReactionViewer.dom.tsx +++ b/ts/components/conversation/ReactionViewer.dom.tsx @@ -168,6 +168,7 @@ export const ReactionViewer = React.forwardRef( // Find the local user's reaction first, then fall back to most recent const localUserReaction = groupedReactions.find(r => r.from.isMe); const firstReaction = localUserReaction || groupedReactions[0]; + strictAssert(firstReaction, 'Missing firstReaction'); return { id: firstReaction.parentKey, index: DEFAULT_EMOJI_ORDER.includes(firstReaction.parentKey) diff --git a/ts/components/conversation/Timeline.dom.tsx b/ts/components/conversation/Timeline.dom.tsx index 823e819f6e..b7d2270d18 100644 --- a/ts/components/conversation/Timeline.dom.tsx +++ b/ts/components/conversation/Timeline.dom.tsx @@ -260,6 +260,7 @@ export class Timeline extends React.Component< if (setFocus && items && items.length > 0) { const lastIndex = items.length - 1; const lastMessageId = items[lastIndex]; + strictAssert(lastMessageId, 'Missing lastMessageId'); targetMessage(lastMessageId, id); } else { const containerEl = this.#containerRef.current; @@ -307,6 +308,7 @@ export class Timeline extends React.Component< ) { if (setFocus) { const messageId = items[oldestUnseenIndex]; + strictAssert(messageId, 'Missing messageId'); targetMessage(messageId, id); } else { this.#lastSeenIndicatorRef.current?.scrollIntoView(); @@ -824,6 +826,7 @@ export class Timeline extends React.Component< } const messageId = items[targetIndex]; + strictAssert(messageId, 'Missing messageId'); targetMessage(messageId, id); event.preventDefault(); @@ -851,6 +854,7 @@ export class Timeline extends React.Component< } const messageId = items[targetIndex]; + strictAssert(messageId, 'Missing messageId'); targetMessage(messageId, id); event.preventDefault(); diff --git a/ts/components/conversation/TimelineMessage.dom.stories.tsx b/ts/components/conversation/TimelineMessage.dom.stories.tsx index 4bdffe6a92..c9c5cd9916 100644 --- a/ts/components/conversation/TimelineMessage.dom.stories.tsx +++ b/ts/components/conversation/TimelineMessage.dom.stories.tsx @@ -3424,7 +3424,8 @@ export const EmbeddedContactWithSendMessage = Template.bind({}); EmbeddedContactWithSendMessage.args = { contact: { ...fullContact, - firstNumber: fullContact.number[0].value, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + firstNumber: fullContact.number[0]!.value, serviceId: generateAci(), }, direction: 'incoming', diff --git a/ts/components/conversation/TimelineMessage.dom.tsx b/ts/components/conversation/TimelineMessage.dom.tsx index 5f812252cf..99ba4d446a 100644 --- a/ts/components/conversation/TimelineMessage.dom.tsx +++ b/ts/components/conversation/TimelineMessage.dom.tsx @@ -250,7 +250,8 @@ export function TimelineMessage(props: Props): React.JSX.Element { if (attachments.length !== 1) { saveAttachments(attachments, timestamp); } else { - saveAttachment(attachments[0], timestamp); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + saveAttachment(attachments[0]!, timestamp); } }, [ diff --git a/ts/components/conversation/TypingBubble.dom.tsx b/ts/components/conversation/TypingBubble.dom.tsx index cb752e6e24..671b00e945 100644 --- a/ts/components/conversation/TypingBubble.dom.tsx +++ b/ts/components/conversation/TypingBubble.dom.tsx @@ -341,8 +341,10 @@ export function TypingBubble({ return; } - const lastTypingContactId = typingContactIds[0]; - const lastTypingTimestamp = typingContactIdTimestamps[lastTypingContactId]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const lastTypingContactId = typingContactIds[0]!; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const lastTypingTimestamp = typingContactIdTimestamps[lastTypingContactId]!; if ( lastItemAuthorId === lastTypingContactId && lastItemTimestamp > lastTypingTimestamp diff --git a/ts/components/conversation/conversation-details/AddGroupMembersModal/ChooseGroupMembersModal.dom.tsx b/ts/components/conversation/conversation-details/AddGroupMembersModal/ChooseGroupMembersModal.dom.tsx index 0100dff7d9..8916e04f75 100644 --- a/ts/components/conversation/conversation-details/AddGroupMembersModal/ChooseGroupMembersModal.dom.tsx +++ b/ts/components/conversation/conversation-details/AddGroupMembersModal/ChooseGroupMembersModal.dom.tsx @@ -216,6 +216,7 @@ export function ChooseGroupMembersModal({ if (virtualIndex < filteredContacts.length) { const contact = filteredContacts[virtualIndex]; + strictAssert(contact, 'Missing contact'); const isSelected = selectedConversationIdsSet.has(contact.id); const isAlreadyInGroup = conversationIdsAlreadyInGroup.has(contact.id); diff --git a/ts/components/conversation/conversation-details/ConversationDetails.dom.stories.tsx b/ts/components/conversation/conversation-details/ConversationDetails.dom.stories.tsx index c750c85253..f3dddc00ee 100644 --- a/ts/components/conversation/conversation-details/ConversationDetails.dom.stories.tsx +++ b/ts/components/conversation/conversation-details/ConversationDetails.dom.stories.tsx @@ -18,6 +18,7 @@ import { ThemeType } from '../../../types/Util.std.js'; import { DurationInSeconds } from '../../../util/durations/index.std.js'; import { NavTab } from '../../../types/Nav.std.js'; import { getFakeCallHistoryGroup } from '../../../test-helpers/getFakeCallHistoryGroup.std.js'; +import type { ContactNameColorType } from '../../../types/Colors.std.js'; import { ContactNameColors } from '../../../types/Colors.std.js'; import { isNotNil } from '../../../util/isNotNil.std.js'; @@ -52,16 +53,20 @@ const createProps = ( isMe: i === 2, }), })); - const memberColors = new Map( + const memberColors = new Map( memberships - .map((membership, i): [string, string] | null => { + .map((membership, i): [string, ContactNameColorType] | null => { const { serviceId } = membership.member; if (!serviceId) { return null; } - return [serviceId.toString(), ContactNameColors[i]]; + return [ + serviceId.toString(), + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + ContactNameColors[i % ContactNameColors.length]!, + ]; }) .filter(isNotNil) ); diff --git a/ts/components/conversation/conversation-details/ConversationDetails.dom.tsx b/ts/components/conversation/conversation-details/ConversationDetails.dom.tsx index cc8fbe3260..62b9f4675c 100644 --- a/ts/components/conversation/conversation-details/ConversationDetails.dom.tsx +++ b/ts/components/conversation/conversation-details/ConversationDetails.dom.tsx @@ -68,6 +68,7 @@ import { BadgeSustainerInstructionsDialog } from '../../BadgeSustainerInstructio import type { ContactModalStateType } from '../../../types/globalModals.std.js'; import type { ShowToastAction } from '../../../state/ducks/toast.preload.js'; import { ToastType } from '../../../types/Toast.dom.js'; +import type { ContactNameColorType } from '../../../types/Colors.std.js'; enum ModalState { AddingGroupMembers, @@ -101,7 +102,7 @@ export type StateProps = { maxGroupSize: number; maxRecommendedGroupSize: number; memberships: ReadonlyArray; - memberColors: Map; + memberColors: Map; pendingApprovalMemberships: ReadonlyArray; pendingAvatarDownload?: boolean; pendingMemberships: ReadonlyArray; diff --git a/ts/components/conversation/conversation-details/ConversationDetailsMembershipList.dom.stories.tsx b/ts/components/conversation/conversation-details/ConversationDetailsMembershipList.dom.stories.tsx index f652f30044..ae34ba22d8 100644 --- a/ts/components/conversation/conversation-details/ConversationDetailsMembershipList.dom.stories.tsx +++ b/ts/components/conversation/conversation-details/ConversationDetailsMembershipList.dom.stories.tsx @@ -12,6 +12,7 @@ import type { GroupV2Membership, } from './ConversationDetailsMembershipList.dom.js'; import { ConversationDetailsMembershipList } from './ConversationDetailsMembershipList.dom.js'; +import type { ContactNameColorType } from '../../../types/Colors.std.js'; import { ContactNameColors } from '../../../types/Colors.std.js'; const { i18n } = window.SignalContext; @@ -33,11 +34,12 @@ const createMemberships = ( const getMemberColors = ( memberships: Array -): Map => +): Map => new Map( memberships.map((membership, i) => [ membership.member.id, - ContactNameColors[i], + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + ContactNameColors[i % ContactNameColors.length]!, ]) ); diff --git a/ts/components/conversation/conversation-details/ConversationDetailsMembershipList.dom.tsx b/ts/components/conversation/conversation-details/ConversationDetailsMembershipList.dom.tsx index 6b1789a6dd..ec550ef00d 100644 --- a/ts/components/conversation/conversation-details/ConversationDetailsMembershipList.dom.tsx +++ b/ts/components/conversation/conversation-details/ConversationDetailsMembershipList.dom.tsx @@ -19,6 +19,7 @@ import { PanelSection } from './PanelSection.dom.js'; import { GroupMemberLabel } from '../ContactName.dom.js'; import { AriaClickable } from '../../../axo/AriaClickable.dom.js'; import type { ContactModalStateType } from '../../../types/globalModals.std.js'; +import type { ContactNameColorType } from '../../../types/Colors.std.js'; export type GroupV2Membership = { isAdmin: boolean; @@ -36,7 +37,7 @@ export type Props = { isEditMemberLabelEnabled: boolean; maxShownMemberCount?: number; memberships: ReadonlyArray; - memberColors: Map; + memberColors: Map; showContactModal: (payload: ContactModalStateType) => void; showLabelEditor: () => void; startAddingNewMembers?: () => void; diff --git a/ts/components/conversation/conversation-details/GroupMemberLabelEditor.dom.tsx b/ts/components/conversation/conversation-details/GroupMemberLabelEditor.dom.tsx index b3315dd061..eb5f2cc12b 100644 --- a/ts/components/conversation/conversation-details/GroupMemberLabelEditor.dom.tsx +++ b/ts/components/conversation/conversation-details/GroupMemberLabelEditor.dom.tsx @@ -23,6 +23,7 @@ import { MessageInteractivity, TextDirection, } from '../Message.dom.js'; +import type { ContactNameColorType } from '../../../types/Colors.std.js'; import { ConversationColors } from '../../../types/Colors.std.js'; import { WidthBreakpoint } from '../../_util.std.js'; import { AxoAlertDialog } from '../../../axo/AxoAlertDialog.dom.js'; @@ -52,13 +53,13 @@ export type PropsDataType = { isActive: boolean; me: ConversationType; membersWithLabel: Array<{ - contactNameColor: string; + contactNameColor: ContactNameColorType; isAdmin: boolean; labelEmoji: string | undefined; labelString: string; member: ConversationType; }>; - ourColor: string | undefined; + ourColor: ContactNameColorType | undefined; theme: ThemeType; }; diff --git a/ts/components/conversation/conversation-details/PendingInvites.dom.tsx b/ts/components/conversation/conversation-details/PendingInvites.dom.tsx index 833f744a96..43d3d72c66 100644 --- a/ts/components/conversation/conversation-details/PendingInvites.dom.tsx +++ b/ts/components/conversation/conversation-details/PendingInvites.dom.tsx @@ -18,7 +18,6 @@ import { } from './ConversationDetailsIcon.dom.js'; import { isAccessControlEnabled } from '../../../groups/util.std.js'; import { Tabs } from '../../Tabs.dom.js'; -import { assertDev } from '../../../util/assert.std.js'; export type PropsDataType = { readonly conversation?: ConversationType; @@ -197,11 +196,13 @@ function MembershipActionConfirmation({ } approvePendingMembershipFromGroupV2( conversation.id, - stagedMemberships[0].membership.member.id + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + stagedMemberships[0]!.membership.member.id ); }; - const membershipType = stagedMemberships[0].type; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const membershipType = stagedMemberships[0]!.type; const modalAction = membershipType === StageType.APPROVE_REQUEST @@ -255,12 +256,13 @@ function getConfirmationMessage({ ourAci: AciString; stagedMemberships: ReadonlyArray; }>): string { - if (!stagedMemberships || !stagedMemberships.length) { + const [stagedMembership] = stagedMemberships; + if (stagedMembership == null) { return ''; } - const membershipType = stagedMemberships[0].type; - const firstMembership = stagedMemberships[0].membership; + const membershipType = stagedMembership.type; + const firstMembership = stagedMembership.membership; // Requesting a membership since they weren't added by anyone if (membershipType === StageType.DENY_REQUEST) { @@ -419,19 +421,23 @@ function MembersPendingProfileKey({ const { [ourAci]: ourPendingMemberships, ...otherPendingMembershipGroups } = groupedPendingMemberships; - const otherPendingMemberships = Object.keys(otherPendingMembershipGroups) - .map(id => members.find(member => member.serviceId === id)) - .filter((member): member is ConversationType => member !== undefined) - .map(member => { - assertDev( - member.serviceId, - 'We just verified that member has serviceId above' - ); - return { - member, - pendingMemberships: otherPendingMembershipGroups[member.serviceId], - }; + const otherPendingMemberships: Array<{ + member: ConversationType; + pendingMemberships: Array; + }> = []; + + for (const [id, pendingMemberships] of Object.entries( + otherPendingMembershipGroups + )) { + const member = members.find(m => m.serviceId === id); + if (member == null) { + continue; + } + otherPendingMemberships.push({ + member, + pendingMemberships, }); + } return ( diff --git a/ts/components/conversation/conversation-details/util.std.ts b/ts/components/conversation/conversation-details/util.std.ts index c98f6022eb..c7cb2602a5 100644 --- a/ts/components/conversation/conversation-details/util.std.ts +++ b/ts/components/conversation/conversation-details/util.std.ts @@ -15,19 +15,15 @@ export const bemGenerator = const base = `${block}__${element}`; const classes = [base]; - let conditionals: Record = {}; + const conditionals: Record = {}; if (modifier) { if (typeof modifier === 'string') { classes.push(`${base}--${modifier}`); } else { - conditionals = Object.keys(modifier).reduce( - (acc, key) => ({ - ...acc, - [`${base}--${key}`]: modifier[key], - }), - {} as Record - ); + for (const [key, value] of Object.entries(modifier)) { + conditionals[`${base}--${key}`] = value; + } } } diff --git a/ts/components/conversation/media-gallery/MediaGallery.dom.tsx b/ts/components/conversation/media-gallery/MediaGallery.dom.tsx index 7f642ed3af..740b16e9d3 100644 --- a/ts/components/conversation/media-gallery/MediaGallery.dom.tsx +++ b/ts/components/conversation/media-gallery/MediaGallery.dom.tsx @@ -180,7 +180,8 @@ function MediaSection({ const sections = groupedItems.map((section, index) => { const isLast = index === groupedItems.length - 1; - const first = section.mediaItems[0]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const first = section.mediaItems[0]!; const { message } = first; const date = moment(message.receivedAtMs || message.receivedAt); diff --git a/ts/components/conversation/media-gallery/utils/mocks.std.ts b/ts/components/conversation/media-gallery/utils/mocks.std.ts index 65588ba39b..927b785e39 100644 --- a/ts/components/conversation/media-gallery/utils/mocks.std.ts +++ b/ts/components/conversation/media-gallery/utils/mocks.std.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import lodash from 'lodash'; -import { type MIMEType, IMAGE_JPEG } from '../../../../types/MIME.std.js'; +import { IMAGE_JPEG, stringToMIMEType } from '../../../../types/MIME.std.js'; import type { MediaItemType, ContactMediaItemType, @@ -21,17 +21,22 @@ export const days = (n: number): number => n * DAY_MS; const tokens = ['foo', 'bar', 'baz', 'qux', 'quux']; const contentTypes = { - gif: 'image/gif', - jpg: 'image/jpeg', - png: 'image/png', - mp4: 'video/mp4', - docx: 'application/text', - pdf: 'application/pdf', - exe: 'application/exe', - txt: 'application/text', -} as unknown as Record; + gif: stringToMIMEType('image/gif'), + jpg: stringToMIMEType('image/jpeg'), + png: stringToMIMEType('image/png'), + mp3: stringToMIMEType('audio/mp3'), + mp4: stringToMIMEType('video/mp4'), + docx: stringToMIMEType('application/text'), + pdf: stringToMIMEType('application/pdf'), + exe: stringToMIMEType('application/exe'), + txt: stringToMIMEType('application/text'), +} as const; -function createRandomAttachment(fileExtension: string): AttachmentForUIType { +type FileExtension = keyof typeof contentTypes; + +function createRandomAttachment( + fileExtension: FileExtension +): AttachmentForUIType { const contentType = contentTypes[fileExtension]; const fileName = `${sample(tokens)}${sample(tokens)}.${fileExtension}`; @@ -101,7 +106,7 @@ function createRandomFile( type: 'media' | 'document' | 'audio', startTime: number, timeWindow: number, - fileExtension: string + fileExtension: FileExtension ): MediaItemType { return { type, @@ -154,15 +159,11 @@ function createRandomFiles( type: 'media' | 'document' | 'audio', startTime: number, timeWindow: number, - fileExtensions: Array + fileExtensions: Array ): Array { return range(random(5, 20)).map(() => - createRandomFile( - type, - startTime, - timeWindow, - sample(fileExtensions) as string - ) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + createRandomFile(type, startTime, timeWindow, sample(fileExtensions)!) ); } export function createRandomDocuments( diff --git a/ts/components/conversation/pinned-messages/PinnedMessagesBar.dom.tsx b/ts/components/conversation/pinned-messages/PinnedMessagesBar.dom.tsx index 3da4991d37..f7e598c98a 100644 --- a/ts/components/conversation/pinned-messages/PinnedMessagesBar.dom.tsx +++ b/ts/components/conversation/pinned-messages/PinnedMessagesBar.dom.tsx @@ -105,8 +105,7 @@ export const PinnedMessagesBar = memo(function PinnedMessagesBar( return null; } - for (let index = 0; index < pins.length; index += 1) { - const value = pins[index]; + for (const [index, value] of pins.entries()) { if (value.id === current) { return { index, value }; } diff --git a/ts/components/fun/FunEmoji.dom.stories.tsx b/ts/components/fun/FunEmoji.dom.stories.tsx index fa1290575a..103b57fed6 100644 --- a/ts/components/fun/FunEmoji.dom.stories.tsx +++ b/ts/components/fun/FunEmoji.dom.stories.tsx @@ -77,7 +77,8 @@ export function All(props: AllProps): React.JSX.Element { }} > {rowVirtualizer.getVirtualItems().map(rowItem => { - const row = rows[rowItem.index]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const row = rows[rowItem.index]!; return (
{ const gif = items[index]; + strictAssert(gif, 'Missing gif'); const aspectRatio = gif.previewMedia.width / gif.previewMedia.height; const baseHeight = GIF_WATERFALL_ITEM_WIDTH / aspectRatio; return baseHeight + GIF_WATERFALL_ITEM_MARGIN + GIF_WATERFALL_ITEM_MARGIN; @@ -350,7 +351,8 @@ export function FunPanelGifs({ const getItemKey = useCallback( (index: number) => { - return items[index].id; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return items[index]!.id; }, [items] ); @@ -578,6 +580,7 @@ export function FunPanelGifs({ {virtualizer.getVirtualItems().map(item => { const gif = items[item.index]; + strictAssert(gif, 'Missing gif'); const key = String(item.key); const isTabbable = selectedItemKey != null diff --git a/ts/components/fun/panels/FunPanelStickers.dom.tsx b/ts/components/fun/panels/FunPanelStickers.dom.tsx index 1b83dd18be..7608cd4d2f 100644 --- a/ts/components/fun/panels/FunPanelStickers.dom.tsx +++ b/ts/components/fun/panels/FunPanelStickers.dom.tsx @@ -605,6 +605,7 @@ const Cell = memo(function Cell(props: { }): React.JSX.Element { const { onClickSticker, onClickTimeSticker } = props; const stickerLookupItem = props.stickerLookup[props.value]; + strictAssert(stickerLookupItem, 'Missing stickerLookupItem'); const handleClick = useCallback( (event: PointerEvent) => { diff --git a/ts/components/fun/virtual/useFunVirtualGrid.dom.tsx b/ts/components/fun/virtual/useFunVirtualGrid.dom.tsx index c19f1510d7..ce8cf54700 100644 --- a/ts/components/fun/virtual/useFunVirtualGrid.dom.tsx +++ b/ts/components/fun/virtual/useFunVirtualGrid.dom.tsx @@ -207,11 +207,15 @@ function buildLayout( totalHeight: number ): Layout { const groups = groupBy(virtualItems, virtualItem => { - return list.listItems[virtualItem.index].sectionMeta.sectionKey; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return list.listItems[virtualItem.index]!.sectionMeta.sectionKey; }); const sections = Object.keys(groups).map((sectionKey): SectionLayoutNode => { - const [headerVirtualItem, ...rowVirtualItems] = groups[sectionKey]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const [maybeHeaderVirtualItem, ...rowVirtualItems] = groups[sectionKey]!; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const headerVirtualItem = maybeHeaderVirtualItem!; const headerListItem = list.listItems.at(headerVirtualItem.index); strictAssert( @@ -446,7 +450,8 @@ export function useFunVirtualGrid({ const getItemKey = useCallback( (index: number) => { - return list.listItems[index].key; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return list.listItems[index]!.key; }, [list] ); diff --git a/ts/components/leftPane/LeftPaneChooseGroupMembersHelper.dom.tsx b/ts/components/leftPane/LeftPaneChooseGroupMembersHelper.dom.tsx index f2c3270dd4..fb803629d3 100644 --- a/ts/components/leftPane/LeftPaneChooseGroupMembersHelper.dom.tsx +++ b/ts/components/leftPane/LeftPaneChooseGroupMembersHelper.dom.tsx @@ -315,7 +315,8 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper if (index < pinnedConversations.length) { return { type: RowType.Conversation, - conversation: pinnedConversations[index], + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + conversation: pinnedConversations[index]!, }; } index -= pinnedConversations.length; @@ -259,7 +260,8 @@ export class LeftPaneInboxHelper extends LeftPaneHelper if (index < conversations.length) { return { type: RowType.Conversation, - conversation: conversations[index], + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + conversation: conversations[index]!, }; } index -= conversations.length; diff --git a/ts/components/leftPane/LeftPaneSearchHelper.dom.tsx b/ts/components/leftPane/LeftPaneSearchHelper.dom.tsx index 2be6f1abb6..bf9ec6d1f4 100644 --- a/ts/components/leftPane/LeftPaneSearchHelper.dom.tsx +++ b/ts/components/leftPane/LeftPaneSearchHelper.dom.tsx @@ -18,7 +18,7 @@ import type { import { LeftPaneSearchInput } from '../LeftPaneSearchInput.dom.js'; import { I18n } from '../I18n.dom.js'; -import { assertDev } from '../../util/assert.std.js'; +import { assertDev, strictAssert } from '../../util/assert.std.js'; import { UserText } from '../UserText.dom.js'; // The "correct" thing to do is to measure the size of the left pane and render enough @@ -381,6 +381,7 @@ export class LeftPaneSearchHelper extends LeftPaneHelper> = []; const finalNewProfileKeys = new Map(); - const imax = changes.length; - for (let i = 0; i < imax; i += 1) { - const { groupChanges } = changes[i]; + for (const change of changes) { + const { groupChanges } = change; if (!groupChanges) { continue; } - const jmax = groupChanges.length; - for (let j = 0; j < jmax; j += 1) { - const changeState = groupChanges[j]; - + for (const changeState of groupChanges) { const { groupChange, groupState } = changeState; if (!groupChange && !groupState) { @@ -4323,8 +4324,8 @@ async function integrateGroupChanges({ if (isFirstFetch && moreThanOneVersion) { // The first array in finalMessages is from the first revision we could process. It // should contain a message about how we joined the group. - const joinMessages = finalMessages[0]; - const alreadyHaveJoinMessage = joinMessages && joinMessages.length > 0; + const joinMessage = finalMessages[0]?.[0]; + const alreadyHaveJoinMessage = joinMessage != null; // There have been other changes since that first revision, so we generate diffs for // the whole of the change since then, likely without the initial join message. @@ -4334,9 +4335,8 @@ async function integrateGroupChanges({ dropInitialJoinMessage: alreadyHaveJoinMessage, }); - const groupChangeMessages = alreadyHaveJoinMessage - ? [joinMessages[0], ...otherMessages] - : otherMessages; + const groupChangeMessages = + joinMessage != null ? [joinMessage, ...otherMessages] : otherMessages; return { newAttributes: attributes, @@ -4897,6 +4897,7 @@ function extractDiffs({ const removedPendingMemberIds = Array.from(oldPendingMemberLookup.keys()); if (removedPendingMemberIds.length > 1) { const firstUuid = removedPendingMemberIds[0]; + strictAssert(firstUuid, 'Missing firstUuid'); const firstRemovedMember = oldPendingMemberLookup.get(firstUuid); strictAssert( firstRemovedMember !== undefined, @@ -4913,6 +4914,7 @@ function extractDiffs({ }); } else if (removedPendingMemberIds.length === 1) { const serviceId = removedPendingMemberIds[0]; + strictAssert(serviceId, 'Missing serviceId'); const removedMember = oldPendingMemberLookup.get(serviceId); strictAssert(removedMember !== undefined, 'Removed member not found'); @@ -5434,7 +5436,7 @@ async function applyGroupChange({ members[aci] = { aci, joinedAtVersion: version, - role: previousRecord.role || MemberRole.DEFAULT, + role: previousRecord?.role || MemberRole.DEFAULT, }; newProfileKeys.push({ @@ -5478,7 +5480,7 @@ async function applyGroupChange({ members[aci] = { aci, joinedAtVersion: version, - role: previousRecord.role || MemberRole.DEFAULT, + role: previousRecord?.role || MemberRole.DEFAULT, }; newProfileKeys.push({ diff --git a/ts/hooks/useRestoreFocus.dom.ts b/ts/hooks/useRestoreFocus.dom.ts index a5d92fe5ee..1f8e88cfbd 100644 --- a/ts/hooks/useRestoreFocus.dom.ts +++ b/ts/hooks/useRestoreFocus.dom.ts @@ -6,7 +6,7 @@ import * as React from 'react'; type CallbackType = (toFocus: HTMLElement | null | undefined) => void; // Restore focus on teardown -export const useRestoreFocus = (): Array => { +export const useRestoreFocus = (): [CallbackType] => { const toFocusRef = React.useRef(null); const lastFocusedRef = React.useRef(null); diff --git a/ts/hooks/useSizeObserver.dom.tsx b/ts/hooks/useSizeObserver.dom.tsx index b61be5467b..91a24b85f4 100644 --- a/ts/hooks/useSizeObserver.dom.tsx +++ b/ts/hooks/useSizeObserver.dom.tsx @@ -52,7 +52,8 @@ export function useSizeObserver( // We're only ever observing one element, and `ResizeObserver` for some // reason is an array of exactly one rect (I assume to support wrapped // inline elements in the future) - const borderBoxSize = entries[0].borderBoxSize[0]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const borderBoxSize = entries[0]!.borderBoxSize[0]!; // We are assuming a horizontal writing-mode here, we could call // `getBoundingClientRect()` here but MDN says not to. In the future if // we are adding support for a vertical locale we may need to change this diff --git a/ts/hooks/useTabs.dom.tsx b/ts/hooks/useTabs.dom.tsx index 78dd2cc1a8..2bdae93683 100644 --- a/ts/hooks/useTabs.dom.tsx +++ b/ts/hooks/useTabs.dom.tsx @@ -52,7 +52,8 @@ export function useTabs(options: TabsOptionsType): TabsProps { // This is enforced by the type system. // eslint-disable-next-line react-hooks/rules-of-hooks const [tabState, setTabState] = useState( - options.initialSelectedTab || options.tabs[0].id + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + options.initialSelectedTab || options.tabs[0]!.id ); selectedTab = tabState; diff --git a/ts/jobs/AttachmentBackupManager.preload.ts b/ts/jobs/AttachmentBackupManager.preload.ts index 66dc41a69a..6e21b906d8 100644 --- a/ts/jobs/AttachmentBackupManager.preload.ts +++ b/ts/jobs/AttachmentBackupManager.preload.ts @@ -604,6 +604,7 @@ async function copyToBackupTier({ }); const response = responses[0]; + strictAssert(response, 'Missing response'); if (!response.isSuccess) { if (response.status === FILE_NOT_FOUND_ON_TRANSIT_TIER_STATUS) { throw new FileNotFoundOnTransitTierError(); diff --git a/ts/jobs/conversationJobQueue.preload.ts b/ts/jobs/conversationJobQueue.preload.ts index c43a085997..f348eb27c2 100644 --- a/ts/jobs/conversationJobQueue.preload.ts +++ b/ts/jobs/conversationJobQueue.preload.ts @@ -615,7 +615,8 @@ export class ConversationJobQueue extends JobQueue { #getRetryWithBackoff(attempts: number) { return ( Date.now() + - MINUTE * (FIBONACCI[attempts] ?? FIBONACCI[FIBONACCI.length - 1]) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + MINUTE * (FIBONACCI[attempts] ?? FIBONACCI[FIBONACCI.length - 1]!) ); } diff --git a/ts/jobs/helpers/sendNormalMessage.preload.ts b/ts/jobs/helpers/sendNormalMessage.preload.ts index ddabbfc051..6dc84b0df0 100644 --- a/ts/jobs/helpers/sendNormalMessage.preload.ts +++ b/ts/jobs/helpers/sendNormalMessage.preload.ts @@ -392,7 +392,8 @@ export async function sendNormalMessage( log.info('sending direct message'); innerPromise = messaging.sendMessageToServiceId({ - serviceId: recipientServiceIdsWithoutMe[0], + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + serviceId: recipientServiceIdsWithoutMe[0]!, messageOptions: { attachments, body, @@ -951,7 +952,8 @@ async function uploadMessageQuote({ ); const attachmentAfterThumbnailUpload = - attachmentsAfterThumbnailUpload[index]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + attachmentsAfterThumbnailUpload[index]!; return { ...attachment, thumbnail: { diff --git a/ts/jobs/helpers/sendPollTerminate.preload.ts b/ts/jobs/helpers/sendPollTerminate.preload.ts index 3dec46e289..48ae804648 100644 --- a/ts/jobs/helpers/sendPollTerminate.preload.ts +++ b/ts/jobs/helpers/sendPollTerminate.preload.ts @@ -137,7 +137,8 @@ export async function sendPollTerminate( return; } - const recipientServiceId = recipients[0]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const recipientServiceId = recipients[0]!; jobLog.info( `${logId}: Sending direct poll terminate for poll timestamp ${targetTimestamp}` diff --git a/ts/jobs/helpers/sendPollVote.preload.ts b/ts/jobs/helpers/sendPollVote.preload.ts index b920530b2b..18b37cc0d3 100644 --- a/ts/jobs/helpers/sendPollVote.preload.ts +++ b/ts/jobs/helpers/sendPollVote.preload.ts @@ -242,7 +242,8 @@ export async function sendPollVote( promise = messaging.sendMessageProtoAndWait({ timestamp: currentTimestamp, - recipients: [recipientServiceIdsWithoutMe[0]], + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + recipients: [recipientServiceIdsWithoutMe[0]!], proto: contentMessage, contentHint: ContentHint.Resendable, groupId: undefined, diff --git a/ts/jobs/helpers/sendReaction.preload.ts b/ts/jobs/helpers/sendReaction.preload.ts index d210250d36..21be9c264b 100644 --- a/ts/jobs/helpers/sendReaction.preload.ts +++ b/ts/jobs/helpers/sendReaction.preload.ts @@ -231,7 +231,8 @@ export async function sendReaction( log.info('sending direct reaction message'); promise = messaging.sendMessageToServiceId({ - serviceId: recipientServiceIdsWithoutMe[0], + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + serviceId: recipientServiceIdsWithoutMe[0]!, messageOptions: { reaction: reactionForSend, timestamp: pendingReaction.timestamp, diff --git a/ts/jobs/helpers/sendStory.preload.ts b/ts/jobs/helpers/sendStory.preload.ts index cd1f14f062..3e3eebb257 100644 --- a/ts/jobs/helpers/sendStory.preload.ts +++ b/ts/jobs/helpers/sendStory.preload.ts @@ -52,6 +52,7 @@ import { saveErrorsOnMessage, } from '../../test-node/util/messageFailures.preload.js'; import { send } from '../../messages/send.preload.js'; +import { strictAssert } from '../../util/assert.std.js'; const { isEqual } = lodash; @@ -167,9 +168,9 @@ export async function sendStory( preview: undefined, }; } else { - const hydratedPreview = ( - await loadPreviewData([localAttachment.preview]) - )[0]; + const previewData = await loadPreviewData([localAttachment.preview]); + const hydratedPreview = previewData[0]; + strictAssert(hydratedPreview, 'Missing hydratedPreview'); textAttachment = { ...localAttachment, @@ -495,57 +496,48 @@ export async function sendStory( let hasFailedSends = false; - const newSendStateByConversationId = Object.keys( - oldSendStateByConversationId - ).reduce((acc, conversationId) => { - const sendState = sentConversationIds.get(conversationId); - if (sendState) { - return { - ...acc, - [conversationId]: sendState, - }; - } + const newSendStateByConversationId: SendStateByConversationId = {}; - const oldSendState = { - ...oldSendStateByConversationId[conversationId], - }; - if (!oldSendState) { - return acc; + for (const [conversationId, oldSendState] of Object.entries( + oldSendStateByConversationId + )) { + const sendState = sentConversationIds.get(conversationId); + + if (sendState) { + newSendStateByConversationId[conversationId] = sendState; + continue; } const recipient = window.ConversationController.get(conversationId); if (!recipient) { - return acc; + continue; } if (isMe(recipient.attributes)) { - return acc; + continue; } if (recipient.isEverUnregistered()) { if (!isSent(oldSendState.status)) { // We should have filtered this out on initial send, but we'll drop them from // send list here if needed. - return acc; + continue; } // If a previous send to them did succeed, we'll keep that status around - return { - ...acc, - [conversationId]: oldSendState, - }; + newSendStateByConversationId[conversationId] = oldSendState; } hasFailedSends = true; - return { - ...acc, - [conversationId]: sendStateReducer(oldSendState, { + newSendStateByConversationId[conversationId] = sendStateReducer( + oldSendState, + { type: SendActionType.Failed, updatedAt: Date.now(), - }), - }; - }, {} as SendStateByConversationId); + } + ); + } if (hasFailedSends) { notifyStorySendFailed(message); @@ -609,8 +601,9 @@ export async function sendStory( sendErrors.push(result); } }); - if (sendErrors.length) { - throw sendErrors[0].reason; + const [firstError] = sendErrors; + if (firstError != null) { + throw firstError.reason; } } diff --git a/ts/logging/log.std.ts b/ts/logging/log.std.ts index b08d06f5c8..cac658b719 100644 --- a/ts/logging/log.std.ts +++ b/ts/logging/log.std.ts @@ -69,7 +69,8 @@ function getSubsystemColor(name: string): string { hash >>>= 0; /* eslint-enable no-bitwise */ - const result = COLORS[hash % COLORS.length]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const result = COLORS[hash % COLORS.length]!; SUBSYSTEM_COLORS.set(name, result); return result; diff --git a/ts/logging/main_process_logging.main.ts b/ts/logging/main_process_logging.main.ts index cf31913e78..616ef4d434 100644 --- a/ts/logging/main_process_logging.main.ts +++ b/ts/logging/main_process_logging.main.ts @@ -210,8 +210,10 @@ export function eliminateOutOfDateFiles( path: target, start: isLineAfterDate(start, date), end: - isLineAfterDate(end[end.length - 1], date) || - isLineAfterDate(end[end.length - 2], date), + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + isLineAfterDate(end[end.length - 1]!, date) || + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + isLineAfterDate(end[end.length - 2]!, date), }; if (!file.start && !file.end) { diff --git a/ts/messageModifiers/MessageReceipts.preload.ts b/ts/messageModifiers/MessageReceipts.preload.ts index f550c6fa2f..8f00e9be0e 100644 --- a/ts/messageModifiers/MessageReceipts.preload.ts +++ b/ts/messageModifiers/MessageReceipts.preload.ts @@ -105,7 +105,8 @@ const processReceiptBatcher = createWaitBatcher({ continue; } // All receipts have the same sentAt, so we can grab it from the first - const sentAt = receiptsForMessageSentAt[0].receiptSync.messageSentAt; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const sentAt = receiptsForMessageSentAt[0]!.receiptSync.messageSentAt; const messagesMatchingTimestamp = // eslint-disable-next-line no-await-in-loop @@ -374,7 +375,8 @@ function getTargetMessage({ `); } - const message = matchingMessages[0]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const message = matchingMessages[0]!; return window.MessageCache.register(new MessageModel(message)); } const wasDeliveredWithSealedSender = ( diff --git a/ts/messageModifiers/Polls.preload.ts b/ts/messageModifiers/Polls.preload.ts index 267a86af37..20e48640ad 100644 --- a/ts/messageModifiers/Polls.preload.ts +++ b/ts/messageModifiers/Polls.preload.ts @@ -436,7 +436,8 @@ export async function handlePollVote( ); if (existingVoteIndex !== -1) { - const existingVote = currentVotes[existingVoteIndex]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const existingVote = currentVotes[existingVoteIndex]!; if (newVote.voteCount > existingVote.voteCount) { updatedVotes = [...currentVotes]; diff --git a/ts/messages/copyQuote.preload.ts b/ts/messages/copyQuote.preload.ts index ba70d9130c..04dd28541b 100644 --- a/ts/messages/copyQuote.preload.ts +++ b/ts/messages/copyQuote.preload.ts @@ -185,7 +185,8 @@ export const copyQuoteContentFromOriginal = async ( } if (queryPreview.length > 0) { - const { image: quotedPreviewImage } = queryPreview[0]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const { image: quotedPreviewImage } = queryPreview[0]!; if (quotedPreviewImage && quotedPreviewImage.path) { quoteAttachment.thumbnail = { ...quotedPreviewImage, diff --git a/ts/messages/migrateMessageData.preload.ts b/ts/messages/migrateMessageData.preload.ts index 1ada5cd422..1a221a28b7 100644 --- a/ts/messages/migrateMessageData.preload.ts +++ b/ts/messages/migrateMessageData.preload.ts @@ -129,7 +129,8 @@ export async function _migrateMessageData({ ); const failedToSaveMessageIds = failedToSaveIndices.map( - idx => upgradedMessages[idx].id + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + idx => upgradedMessages[idx]!.id ); if (failedToUpgradeMessageIds.length || failedToSaveMessageIds.length) { diff --git a/ts/models/conversations.preload.ts b/ts/models/conversations.preload.ts index 108fbffde9..2cf39c9e3b 100644 --- a/ts/models/conversations.preload.ts +++ b/ts/models/conversations.preload.ts @@ -3111,7 +3111,7 @@ export class ConversationModel { return false; } - if (contacts.length === 1 && isMe(contacts[0]?.attributes)) { + if (contacts.length === 1 && contacts[0] && isMe(contacts[0].attributes)) { return false; } @@ -4514,7 +4514,8 @@ export class ConversationModel { preview = window.MessageCache.register( new MessageModel(previewAttributes) ); - const updates = (await this.cleanAttributes([preview.attributes]))?.[0]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const updates = (await this.cleanAttributes([preview.attributes]))[0]!; preview.set(updates); } @@ -4522,7 +4523,8 @@ export class ConversationModel { activity = window.MessageCache.register( new MessageModel(activityAttributes) ); - const updates = (await this.cleanAttributes([activity.attributes]))?.[0]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const updates = (await this.cleanAttributes([activity.attributes]))[0]!; activity.set(updates); } @@ -5809,18 +5811,22 @@ export class ConversationModel { } if (isTyping) { - this.contactTypingTimers[typingToken] = this.contactTypingTimers[ - typingToken - ] || { - timestamp: Date.now(), - senderId, - senderDevice, - }; - - this.contactTypingTimers[typingToken].timer = setTimeout( + const timer = setTimeout( this.clearContactTypingTimer.bind(this, typingToken), 15 * 1000 ); + + const prev = this.contactTypingTimers[typingToken]; + if (prev != null) { + prev.timer = timer; + } else { + this.contactTypingTimers[typingToken] ??= { + timestamp: Date.now(), + senderId, + timer, + }; + } + // User was not previously typing before. State change! if (!record) { window.ConversationController.conversationUpdated( diff --git a/ts/quill/auto-substitute-ascii-emojis/index.dom.tsx b/ts/quill/auto-substitute-ascii-emojis/index.dom.tsx index 9218b31159..4467beaa6b 100644 --- a/ts/quill/auto-substitute-ascii-emojis/index.dom.tsx +++ b/ts/quill/auto-substitute-ascii-emojis/index.dom.tsx @@ -48,6 +48,7 @@ function buildRegexp(obj: EmojiShortcutMap): RegExp { return new RegExp(`(${sanitizedKeys.join('|')})$`); } +type EmojiRegExpMatch = RegExpExecArray & { 1: string }; const EMOJI_REGEXP = buildRegexp(emojiShortcutMap); let isEnabled = true; @@ -108,7 +109,7 @@ export class AutoSubstituteAsciiEmojis { return; } - const [, textEmoji] = match; + const [, textEmoji] = match as EmojiRegExpMatch; const emojiParentKey = emojiShortcutMap[textEmoji]; if (emojiParentKey != null) { diff --git a/ts/quill/emoji/completion.dom.tsx b/ts/quill/emoji/completion.dom.tsx index a0a1137e64..0eaf27ccb6 100644 --- a/ts/quill/emoji/completion.dom.tsx +++ b/ts/quill/emoji/completion.dom.tsx @@ -26,6 +26,7 @@ import type { } from '../../components/fun/useFunEmojiSearch.dom.js'; import { type FunEmojiLocalizer } from '../../components/fun/useFunEmojiLocalizer.dom.js'; import type { FunEmojiSelection } from '../../components/fun/panels/FunPanelEmojis.dom.js'; +import { strictAssert } from '../../util/assert.std.js'; const { isNumber, debounce } = lodash; @@ -164,8 +165,12 @@ export class EmojiCompletion { /^([-+0-9\p{Alpha}_]*):/iu ); + type LeftTokenMatch = RegExpMatchArray & { 1: string; 2: string }; + type RightTokenMatch = RegExpMatchArray & { 1: string }; + if (leftTokenTextMatch) { - const [, leftTokenText, isSelfClosing] = leftTokenTextMatch; + const [, leftTokenText, isSelfClosing] = + leftTokenTextMatch as LeftTokenMatch; if (isSelfClosing || justPressedColon) { const parentKey = @@ -186,7 +191,7 @@ export class EmojiCompletion { } if (rightTokenTextMatch) { - const [, rightTokenText] = rightTokenTextMatch; + const [, rightTokenText] = rightTokenTextMatch as RightTokenMatch; const tokenText = leftTokenText + rightTokenText; const parentKey = this.options.emojiLocalizer.getParentKeyForText(tokenText); @@ -240,15 +245,20 @@ export class EmojiCompletion { } const result = this.results[this.index]; + if (result == null) { + return; + } + const [leafText] = this.getCurrentLeafTextPartitions(); const tokenTextMatch = /:([-+0-9\p{Alpha}_]*)(:?)$/iu.exec(leafText); + type TokenTextMatch = RegExpExecArray & { 1: string }; if (tokenTextMatch == null) { return; } - const [, tokenText] = tokenTextMatch; + const [, tokenText] = tokenTextMatch as TokenTextMatch; this.insertEmoji({ emojiParentKey: result.parentKey, @@ -355,7 +365,9 @@ export class EmojiCompletion { `Could not find the beginning of the emoji word to be completed. startOfEmojiText=${startOfEmojiText}, textBeforeCursor.length=${textBeforeCursor?.length}, range.offsets=${range.startOffset}-${range.endOffset}` ); } - return range.getClientRects()[0]; + const [clientRect] = range.getClientRects(); + strictAssert(clientRect, 'Missing clientRect'); + return clientRect; } log.warn('No selection range when auto-completing emoji'); return new DOMRect(); // don't crash just because we couldn't get a rectangle @@ -373,7 +385,7 @@ export class EmojiCompletion { aria-expanded aria-activedescendant={`emoji-result--${ emojiResults.length - ? emojiResults[emojiResultsIndex].parentKey + ? emojiResults[emojiResultsIndex]?.parentKey : '' }`} tabIndex={0} diff --git a/ts/quill/formatting/matchers.dom.ts b/ts/quill/formatting/matchers.dom.ts index f1529f1b11..9a7b2178b6 100644 --- a/ts/quill/formatting/matchers.dom.ts +++ b/ts/quill/formatting/matchers.dom.ts @@ -72,7 +72,7 @@ export const matchMonospace: Matcher = ( const classes = [ 'MessageTextRenderer__formatting--monospace', 'quill--monospace', - ]; + ] as const; // Note: This is defined as $monospace in _variables.scss const fontFamily = 'font-family: "SF Mono", SFMono-Regular, ui-monospace, "DejaVu Sans Mono", Menlo, Consolas, monospace;'; @@ -99,7 +99,7 @@ export const matchSpoiler: Matcher = ( 'quill--spoiler', 'MessageTextRenderer__formatting--spoiler', 'MessageTextRenderer__formatting--spoiler--revealed', - ]; + ] as const; if ( delta.length() > 0 && diff --git a/ts/quill/mentions/completion.dom.tsx b/ts/quill/mentions/completion.dom.tsx index 6e1170e3f7..f234ad1c82 100644 --- a/ts/quill/mentions/completion.dom.tsx +++ b/ts/quill/mentions/completion.dom.tsx @@ -31,6 +31,7 @@ export type MentionCompletionOptions = { }; const MENTION_REGEX = /(?:^|\W)@([-+\p{L}\p{M}\p{N}]*)$/u; +type MentionRegexMatch = RegExpMatchArray & { 1: string }; export class MentionCompletion { results: ReadonlyArray; @@ -126,7 +127,7 @@ export class MentionCompletion { ); if (leftTokenTextMatch) { - const [, leftTokenText] = leftTokenTextMatch; + const [, leftTokenText] = leftTokenTextMatch as MentionRegexMatch; let results: ReadonlyArray = []; @@ -175,6 +176,9 @@ export class MentionCompletion { } const member = this.results[resultIndex]; + if (member == null) { + return; + } const [blot, index] = this.quill.getLeaf(range.index); @@ -185,7 +189,7 @@ export class MentionCompletion { ); if (leftTokenTextMatch) { - const [, leftTokenText] = leftTokenTextMatch; + const [, leftTokenText] = leftTokenTextMatch as MentionRegexMatch; this.insertMention( member, @@ -267,7 +271,9 @@ export class MentionCompletion { role="listbox" aria-expanded aria-activedescendant={`mention-result--${ - memberResults.length ? memberResults[memberResultsIndex].name : '' + memberResults.length + ? (memberResults[memberResultsIndex]?.name ?? '') + : '' }`} tabIndex={0} > diff --git a/ts/quill/signal-clipboard/util.dom.ts b/ts/quill/signal-clipboard/util.dom.ts index 4ef2ef1d78..150d3096ed 100644 --- a/ts/quill/signal-clipboard/util.dom.ts +++ b/ts/quill/signal-clipboard/util.dom.ts @@ -97,7 +97,8 @@ function getStringFromNode( } let result = ''; for (let i = 0; i < childCount; i += 1) { - const child = element.childNodes[i]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const child = element.childNodes[i]!; const nextChild = element.childNodes[i + 1]; result += getStringFromNode(child, node, nextChild); } @@ -122,12 +123,12 @@ const STRIKETHROUGH_TAG = 's'; const MONOSPACE_CLASSES = [ 'quill--monospace', 'MessageTextRenderer__formatting--monospace', -]; +] as const; const SPOILER_CLASSES = [ 'quill--spoiler', 'MessageTextRenderer__formatting--spoiler', 'MessageTextRenderer__formatting--spoiler--revealed', -]; +] as const; // When the user cuts/copies single-line text which don't cross any mentions/emojo or // formatting boundaries, we don't get the surrounding formatting nodes in our selection. diff --git a/ts/scripts/build-localized-display-names.node.ts b/ts/scripts/build-localized-display-names.node.ts index c36ea0a3ce..c4c34a51a9 100644 --- a/ts/scripts/build-localized-display-names.node.ts +++ b/ts/scripts/build-localized-display-names.node.ts @@ -12,14 +12,15 @@ if (type !== 'countries' && type !== 'locales') { throw new Error('Invalid first argument, expceted "countries" or "locales"'); } -const localeDisplayNamesDataPath = process.argv[3]; -if (!localeDisplayNamesDataPath) { +if (!process.argv[3]) { throw new Error('Missing second argument: source csv file'); } -const localeDisplayNamesBuildPath = process.argv[4]; -if (!localeDisplayNamesBuildPath) { +const localeDisplayNamesDataPath = process.argv[3]; + +if (!process.argv[4]) { throw new Error('Missing third argument: output json file'); } +const localeDisplayNamesBuildPath = process.argv[4]; const availableLocales = _getAvailableLocales(); @@ -68,7 +69,8 @@ function convertData( result[subKey] = {}; for (const [index, message] of messages.entries()) { - result[subKey][keys[index]] = message; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + result[subKey][keys[index]!] = message; } } } else { @@ -81,7 +83,8 @@ function convertData( const [subKey, ...messages] = row; for (const [index, message] of messages.entries()) { - result[keys[index]][subKey] = message; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + result[keys[index]!]![subKey] = message; } } } @@ -103,7 +106,8 @@ function assertValuesForAllLocales(result: LocaleDisplayNamesResult) { } function assertValuesForAllCountries(result: LocaleDisplayNamesResult) { - const availableCountries = Object.keys(result.en); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const availableCountries = Object.keys(result.en!); for (const locale of availableLocales) { const values = result[locale]; if (values == null) { diff --git a/ts/scripts/check-min-os-version.node.ts b/ts/scripts/check-min-os-version.node.ts index 7e5d6acd02..b9ba96807b 100644 --- a/ts/scripts/check-min-os-version.node.ts +++ b/ts/scripts/check-min-os-version.node.ts @@ -62,12 +62,13 @@ async function macosVersionCheck(file: string) { const { stdout } = await execFile('otool', ['-l', file]); + type MacosMatch = RegExpMatchArray & { 1: string }; const match = stdout.match(/minos\s+([\d.]+)/); if (match == null) { throw new Error(`Failed to detect min OS version of ${file}`); } - const [, macosVersion] = match; + const [, macosVersion] = match as MacosMatch; const darwinVersion = MACOS_TO_DARWIN_VERSIONS.get(macosVersion); if (darwinVersion == null) { throw new Error(`No matching darwin version for macOS ${macosVersion}`); @@ -108,7 +109,9 @@ async function linuxVersionCheck(file: string) { }); let minGlibcVersion: string | undefined; - for (const [, unpaddedVersion] of stdout.matchAll(/GLIBC_([\d.]+)/g)) { + type GlibcMatch = RegExpExecArray & { 1: string }; + for (const match of stdout.matchAll(/GLIBC_([\d.]+)/g)) { + const [, unpaddedVersion] = match as GlibcMatch; const glibcVersion = padGlibcVersion(unpaddedVersion); if (minGlibcVersion == null || gte(glibcVersion, minGlibcVersion)) { minGlibcVersion = glibcVersion; @@ -126,6 +129,7 @@ async function linuxVersionCheck(file: string) { throw new Error('Missing libc6 dependency in package.json'); } + type Libc6Match = RegExpMatchArray & { 1: string }; const match = libc6Dependency.match(/^libc6 \(>= ([\d.]+)\)$/); if (match == null) { throw new Error( @@ -133,7 +137,8 @@ async function linuxVersionCheck(file: string) { ); } - const minSupported = padGlibcVersion(match[1]); + const [, unpaddedVersion] = match as Libc6Match; + const minSupported = padGlibcVersion(unpaddedVersion); if (gte(minSupported, minGlibcVersion)) { console.log(`${file}: required version ${minGlibcVersion}`); return; diff --git a/ts/scripts/constants.std.ts b/ts/scripts/constants.std.ts index 273b1f5756..7abd6ded08 100644 --- a/ts/scripts/constants.std.ts +++ b/ts/scripts/constants.std.ts @@ -1,4 +1,5 @@ // Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +export type DeletedMatch = RegExpMatchArray & { 1: string }; export const DELETED_REGEXP = /\(\s*deleted\s+(\d{2,4}\/\d{2}\/\d{2,4})\s*\)/i; diff --git a/ts/scripts/gen-nsis-script.node.ts b/ts/scripts/gen-nsis-script.node.ts index efe3193e35..8dbc806d3f 100644 --- a/ts/scripts/gen-nsis-script.node.ts +++ b/ts/scripts/gen-nsis-script.node.ts @@ -46,7 +46,8 @@ const fallbackMessages = JSON.parse( const nsisStrings = new Array(); for (const lang of REQUIRED_LANGUAGES) { - const langId = LCID[lang] ?? LCID.en_US; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const langId = LCID[lang] ?? LCID.en_US!; if (USED.has(langId)) { continue; } diff --git a/ts/scripts/notarize-universal-dmg.node.ts b/ts/scripts/notarize-universal-dmg.node.ts index 5c6856b7ef..0035b26bae 100644 --- a/ts/scripts/notarize-universal-dmg.node.ts +++ b/ts/scripts/notarize-universal-dmg.node.ts @@ -63,7 +63,8 @@ export async function afterAllArtifactBuild({ return; } - const [dmgPath] = artifactsToStaple; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const dmgPath = artifactsToStaple[0]!; console.log(`Notarizing dmg: ${dmgPath}`); await notarize({ diff --git a/ts/scripts/publish-installer-size.node.ts b/ts/scripts/publish-installer-size.node.ts index 0f13d3220a..1f0c9c3e21 100644 --- a/ts/scripts/publish-installer-size.node.ts +++ b/ts/scripts/publish-installer-size.node.ts @@ -18,7 +18,8 @@ const RELEASE_DIR = join(__dirname, '..', '..', 'release'); // TODO: DESKTOP-9836 async function main(): Promise { - const config = process.argv[2]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const config = process.argv[2]!; if (!SUPPORT_CONFIG.has(config)) { throw new Error(`Invalid argument: ${config}`); } diff --git a/ts/scripts/remove-strings.node.ts b/ts/scripts/remove-strings.node.ts index 46ba85cdc4..bb1fbaf08d 100644 --- a/ts/scripts/remove-strings.node.ts +++ b/ts/scripts/remove-strings.node.ts @@ -9,7 +9,7 @@ import path from 'node:path'; import { MONTH } from '../util/durations/index.std.js'; import { isOlderThan } from '../util/timestamp.std.js'; -import { DELETED_REGEXP } from './constants.std.js'; +import { DELETED_REGEXP, type DeletedMatch } from './constants.std.js'; const ROOT_DIR = path.join(__dirname, '..', '..'); const MESSAGES_FILE = path.join(ROOT_DIR, '_locales', 'en', 'messages.json'); @@ -33,7 +33,8 @@ async function main() { return; } - const deletedAt = new Date(match[1]).getTime(); + const [deletedAtStr] = match as DeletedMatch; + const deletedAt = new Date(deletedAtStr).getTime(); if (!isOlderThan(deletedAt, MONTH)) { return; } diff --git a/ts/scripts/symbolicate-crash-report.node.ts b/ts/scripts/symbolicate-crash-report.node.ts index ae00052f42..460e49801e 100644 --- a/ts/scripts/symbolicate-crash-report.node.ts +++ b/ts/scripts/symbolicate-crash-report.node.ts @@ -11,11 +11,10 @@ import pMap from 'p-map'; const gunzip = promisify(gunzipCb); -const INPUT_FILE = process.argv[2]; - -if (!INPUT_FILE) { +if (!process.argv[2]) { throw new Error('Usage: node symbolicate-crash-report.js '); } +const INPUT_FILE = process.argv[2]; async function main(): Promise { let file = await readFile(INPUT_FILE); @@ -30,6 +29,7 @@ async function main(): Promise { .matchAll( /WARN[^\n]*crashReports:\s+dump=\[REDACTED\]([0-9a-z]+).dmp\s+mtime="([\d\-T:.Z]+)"\s+({(\n|.)*?\n})/g ); + type CrashDumpMatch = RegExpExecArray & { 1: string; 2: string; 3: string }; function moduleName(filename: string | undefined): string { if (!filename) { @@ -47,7 +47,8 @@ async function main(): Promise { } const dumps = new Array(); - for (const [, dump, mtime, json] of matches) { + for (const match of matches) { + const [, dump, mtime, json] = match as CrashDumpMatch; const out = []; let info; diff --git a/ts/scripts/update-gha.node.ts b/ts/scripts/update-gha.node.ts index 9079b0b4b7..41241d5fca 100644 --- a/ts/scripts/update-gha.node.ts +++ b/ts/scripts/update-gha.node.ts @@ -16,6 +16,12 @@ const WORKFLOWS_DIR = join(__dirname, '..', '..', '.github', 'workflows'); const REGEXP = /uses:\s*(?[^\s]+)@(?[^\s#]+)(?:\s*#\s*(?[^\n]+))?/g; +type RegexGroups = { + path: string; + ref: string; + originalRef?: string; +}; + const CACHE = new Map(); const TagsSchema = z @@ -32,7 +38,7 @@ async function updateAction(fullPath: string): Promise { for (const { groups } of source.matchAll(REGEXP)) { strictAssert(groups != null, 'Expected regexp to fully match'); - const { path, ref, originalRef } = groups; + const { path, ref, originalRef } = groups as RegexGroups; // Skip local actions if (path.startsWith('.')) { @@ -50,6 +56,7 @@ async function updateAction(fullPath: string): Promise { } const [org, repo] = path.split('/', 2); + strictAssert(repo, 'Missing repo'); const url = `https://api.github.com/repos/${encodeURIComponent(org)}/` + diff --git a/ts/services/BeforeNavigate.std.ts b/ts/services/BeforeNavigate.std.ts index 0d95b5261f..8766a68f2b 100644 --- a/ts/services/BeforeNavigate.std.ts +++ b/ts/services/BeforeNavigate.std.ts @@ -83,8 +83,7 @@ export class BeforeNavigateService { const logId = `shouldCancelNavigation/${context}`; const entries = Array.from(this.#beforeNavigateCallbacks); - for (let i = 0, max = entries.length; i < max; i += 1) { - const entry = entries[i]; + for (const entry of entries) { // eslint-disable-next-line no-await-in-loop const response = await Promise.race([ entry.callback({ existingLocation, newLocation }), diff --git a/ts/services/LinkPreview.preload.ts b/ts/services/LinkPreview.preload.ts index 6aa52e244b..2f0cac0bb9 100644 --- a/ts/services/LinkPreview.preload.ts +++ b/ts/services/LinkPreview.preload.ts @@ -475,7 +475,8 @@ async function getStickerPackPreview( } const { title, coverStickerId } = pack; - const sticker = pack.stickers[coverStickerId]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const sticker = pack.stickers[coverStickerId]!; const data = pack.status === 'ephemeral' ? await readTempData(sticker) diff --git a/ts/services/MessageCache.preload.ts b/ts/services/MessageCache.preload.ts index a3e2c9913c..5afada0f11 100644 --- a/ts/services/MessageCache.preload.ts +++ b/ts/services/MessageCache.preload.ts @@ -163,7 +163,8 @@ export class MessageCache { } const { [obsoleteId]: obsoleteSendState, ...rest } = sendState; return { - [conversationId]: obsoleteSendState, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + [conversationId]: obsoleteSendState!, ...rest, }; }; diff --git a/ts/services/addGlobalKeyboardShortcuts.preload.ts b/ts/services/addGlobalKeyboardShortcuts.preload.ts index 123612860c..49debba08d 100644 --- a/ts/services/addGlobalKeyboardShortcuts.preload.ts +++ b/ts/services/addGlobalKeyboardShortcuts.preload.ts @@ -11,6 +11,7 @@ import { getQuotedMessageSelector } from '../state/selectors/composer.preload.js import { removeLinkPreview } from './LinkPreview.preload.js'; import { ForwardMessagesModalType } from '../components/ForwardMessagesModal.dom.js'; import { getSelectedConversationId } from '../state/selectors/nav.std.js'; +import { strictAssert } from '../util/assert.std.js'; const log = createLogger('addGlobalKeyboardShortcuts'); @@ -96,6 +97,7 @@ export function addGlobalKeyboardShortcuts(): void { } const node = targets[index]; + strictAssert(node, 'Missing node'); const firstFocusableElement = matchOrQueryFocusable(node); if (firstFocusableElement) { diff --git a/ts/services/backups/credentials.preload.ts b/ts/services/backups/credentials.preload.ts index edbb7df389..99c8e6774d 100644 --- a/ts/services/backups/credentials.preload.ts +++ b/ts/services/backups/credentials.preload.ts @@ -359,8 +359,10 @@ export class BackupCredentials { result.sort((a, b) => a.redemptionTimeMs - b.redemptionTimeMs); await this.#updateCache(result); - const startMs = result[0].redemptionTimeMs; - const endMs = result[result.length - 1].redemptionTimeMs; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const startMs = result[0]!.redemptionTimeMs; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const endMs = result[result.length - 1]!.redemptionTimeMs; log.info(`saved [${startMs}, ${endMs}]`); strictAssert(result.length === 14, 'Expected one week of credentials'); diff --git a/ts/services/backups/import.preload.ts b/ts/services/backups/import.preload.ts index d0b5e531ab..c51e78902f 100644 --- a/ts/services/backups/import.preload.ts +++ b/ts/services/backups/import.preload.ts @@ -3425,7 +3425,7 @@ export class BackupImportStream extends Writable { if (Bytes.isNotEmpty(updaterAci)) { from = fromAciObject(Aci.fromUuidBytes(updaterAci)); } - if (!invitees || invitees.length === 0) { + if (!invitees || invitees[0] == null) { throw new Error( `${logId}: groupInvitationRevokedUpdate had missing invitees list!` ); @@ -3707,7 +3707,7 @@ export class BackupImportStream extends Writable { additionalMessages.push(migrationMessage); } - if (finalDetails.length === 0 && additionalMessages.length > 0) { + if (finalDetails.length === 0 && additionalMessages[0] != null) { return { message: additionalMessages[0], additionalMessages: additionalMessages.slice(1), diff --git a/ts/services/calling.preload.ts b/ts/services/calling.preload.ts index c26ffe5327..7ac97bd6e8 100644 --- a/ts/services/calling.preload.ts +++ b/ts/services/calling.preload.ts @@ -2814,27 +2814,38 @@ export class CallingClass { return false; } for (let i = 0; i < a.availableCameras.length; i += 1) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const aCamera = a.availableCameras[i]!; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const bCamera = a.availableCameras[i]!; if ( - a.availableCameras[i].deviceId !== b.availableCameras[i].deviceId || - a.availableCameras[i].groupId !== b.availableCameras[i].groupId || - a.availableCameras[i].label !== b.availableCameras[i].label + aCamera.deviceId !== bCamera.deviceId || + aCamera.groupId !== bCamera.groupId || + aCamera.label !== bCamera.label ) { return false; } } for (let i = 0; i < a.availableMicrophones.length; i += 1) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const aMicrophone = a.availableMicrophones[i]!; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const bMicrophone = b.availableMicrophones[i]!; if ( - a.availableMicrophones[i].name !== b.availableMicrophones[i].name || - a.availableMicrophones[i].uniqueId !== - b.availableMicrophones[i].uniqueId + aMicrophone.name !== bMicrophone.name || + aMicrophone.uniqueId !== bMicrophone.uniqueId ) { return false; } } for (let i = 0; i < a.availableSpeakers.length; i += 1) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const aSpeaker = a.availableSpeakers[i]!; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const bSpeaker = b.availableSpeakers[i]!; if ( - a.availableSpeakers[i].name !== b.availableSpeakers[i].name || - a.availableSpeakers[i].uniqueId !== b.availableSpeakers[i].uniqueId + aSpeaker.name !== bSpeaker.name || + aSpeaker.uniqueId !== bSpeaker.uniqueId ) { return false; } @@ -3875,11 +3886,15 @@ export class CallingClass { if (this._iceServerOverride) { if (typeof this._iceServerOverride === 'string') { if (ICE_SERVER_IS_IP_LIKE.test(this._iceServerOverride)) { - iceServers[0].urls = [this._iceServerOverride]; - iceServers = [iceServers[0]]; + const iceServer = iceServers[0]; + strictAssert(iceServer, 'Missing iceServers[0]'); + iceServer.urls = [this._iceServerOverride]; + iceServers = [iceServer]; } else { - iceServers[1].urls = [this._iceServerOverride]; - iceServers = [iceServers[1]]; + const iceServer = iceServers[1]; + strictAssert(iceServer, 'Missing iceServers[1]'); + iceServer.urls = [this._iceServerOverride]; + iceServers = [iceServer]; } } else { iceServers = iceServerConfigToList(this._iceServerOverride); diff --git a/ts/services/groupCredentialFetcher.preload.ts b/ts/services/groupCredentialFetcher.preload.ts index 6684d417bc..50b92eb499 100644 --- a/ts/services/groupCredentialFetcher.preload.ts +++ b/ts/services/groupCredentialFetcher.preload.ts @@ -135,8 +135,10 @@ function getCredentialsForToday( } return { - today: credentials[todayIndex], - tomorrow: credentials[todayIndex + 1], + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + today: credentials[todayIndex]!, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + tomorrow: credentials[todayIndex + 1]!, }; } diff --git a/ts/services/releaseNoteAndMegaphoneFetcher.preload.ts b/ts/services/releaseNoteAndMegaphoneFetcher.preload.ts index dd89b1a277..f8598423f2 100644 --- a/ts/services/releaseNoteAndMegaphoneFetcher.preload.ts +++ b/ts/services/releaseNoteAndMegaphoneFetcher.preload.ts @@ -474,19 +474,23 @@ export class ReleaseNoteAndMegaphoneFetcher { range.length == null || range.start == null || range.style == null || - !STYLE_MAPPING[range.style] || range.start + range.length - 1 >= body.length ) { return null; } + const style = STYLE_MAPPING[range.style]; + if (style == null) { + return null; + } + const relativeStart = range.start + title.length + titleBodySeparator.length; return { start: relativeStart, length: range.length, - style: STYLE_MAPPING[range.style], + style, }; }) .filter(isNotNil); diff --git a/ts/services/retryPlaceholders.std.ts b/ts/services/retryPlaceholders.std.ts index a7a253f56c..876bffa7da 100644 --- a/ts/services/retryPlaceholders.std.ts +++ b/ts/services/retryPlaceholders.std.ts @@ -161,11 +161,9 @@ export class RetryPlaceholders { throw new Error('RetryPlaceholders: not started'); } const expiration = getDeltaIntoPast(this.#retryReceiptLifespan); - const max = this.#items.length; const result: Array = []; - for (let i = 0; i < max; i += 1) { - const item = this.#items[i]; + for (const item of this.#items) { if (item.receivedAt <= expiration) { result.push(item); } else { diff --git a/ts/services/storage.preload.ts b/ts/services/storage.preload.ts index b1d3a83945..5c550a2fce 100644 --- a/ts/services/storage.preload.ts +++ b/ts/services/storage.preload.ts @@ -269,10 +269,7 @@ async function generateManifest( }; } - const conversations = window.ConversationController.getAll(); - for (let i = 0; i < conversations.length; i += 1) { - const conversation = conversations[i]; - + for (const conversation of window.ConversationController.getAll()) { let identifierType; let storageRecord: Proto.StorageRecord.Params | undefined; @@ -2334,6 +2331,7 @@ async function upload({ uploadBucket.push(Date.now()); if (uploadBucket.length >= 3) { const [firstMostRecentWrite] = uploadBucket; + strictAssert(firstMostRecentWrite, 'Missing firstMostRecentWrite'); if (isMoreRecentThan(5 * durations.MINUTE, firstMostRecentWrite)) { throw new Error(`${logId}: too many writes too soon.`); diff --git a/ts/services/storyLoader.preload.ts b/ts/services/storyLoader.preload.ts index e0d1ca327f..647e1cf267 100644 --- a/ts/services/storyLoader.preload.ts +++ b/ts/services/storyLoader.preload.ts @@ -63,7 +63,8 @@ export function getStoryDataFromMessageAttributes( message.preview.length === 1, 'getStoryDataFromMessageAttributes: story can have only one preview' ); - [preview] = message.preview; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + preview = message.preview[0]!; strictAssert( attachment?.textAttachment, diff --git a/ts/services/username.preload.ts b/ts/services/username.preload.ts index 8fc7aa030b..a19469338a 100644 --- a/ts/services/username.preload.ts +++ b/ts/services/username.preload.ts @@ -129,7 +129,8 @@ export async function reserveUsername( return { ok: false, error: ReserveUsernameError.Unprocessable }; } - const username = candidates[index]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const username = candidates[index]!; return { ok: true, diff --git a/ts/services/writeProfile.preload.ts b/ts/services/writeProfile.preload.ts index dc67ccb29f..d2d5b6ab01 100644 --- a/ts/services/writeProfile.preload.ts +++ b/ts/services/writeProfile.preload.ts @@ -15,7 +15,6 @@ import { writeNewAttachmentData, maybeDeleteAttachmentFile, } from '../util/migrations.preload.js'; -import { isWhitespace } from '../util/whitespaceStringUtil.std.js'; import { imagePathToBytes } from '../util/imagePathToBytes.dom.js'; import { getLocalAvatarUrl } from '../util/avatarUtils.preload.js'; import type { @@ -55,7 +54,7 @@ export async function writeProfile( } = conversation; strictAssert( - !isWhitespace(String(conversation.firstName)), + conversation.firstName != null && conversation.firstName.trim() !== '', 'writeProfile: Cannot set an empty profile name' ); diff --git a/ts/sql/Server.node.ts b/ts/sql/Server.node.ts index 3e28ea5e0d..104c3633f9 100644 --- a/ts/sql/Server.node.ts +++ b/ts/sql/Server.node.ts @@ -1415,13 +1415,11 @@ function insertSentProto( ` ); - const recipientServiceIds = Object.keys(recipients); - for (const recipientServiceId of recipientServiceIds) { + for (const [recipientServiceId, deviceIds] of Object.entries(recipients)) { strictAssert( isServiceIdString(recipientServiceId), 'Recipient must be a service id' ); - const deviceIds = recipients[recipientServiceId]; for (const deviceId of deviceIds) { recipientStatement.run({ @@ -1550,18 +1548,24 @@ function deleteSentProtoRecipient( sendLogRecipients.deviceId = $deviceId; ` ) - .all({ timestamp, recipientServiceId, deviceId }); - if (!rows.length) { + .all<{ id: string; hasPniSignatureMessage: string }>({ + timestamp, + recipientServiceId, + deviceId, + }); + + const [row, ...others] = rows; + if (row == null) { continue; } - if (rows.length > 1) { + if (others.length > 0) { logger.warn( 'deleteSentProtoRecipient: More than one payload matches ' + `recipient and timestamp ${timestamp}. Using the first.` ); } - const { id, hasPniSignatureMessage } = rows[0]; + const { id, hasPniSignatureMessage } = row; // 2. Delete the recipient/device combination in question. db.prepare( @@ -2309,9 +2313,11 @@ function searchMessages( ` ); const hydrated = hydrateMessages(db, queryResult); - return queryResult.map((row, idx) => { + return queryResult.map((row, idx): ServerSearchResultMessageType => { + const message = hydrated[idx]; + strictAssert(message, 'Missing message'); return { - ...hydrated[idx], + ...message, ftsSnippet: row.ftsSnippet, mentionAci: row.mentionAci, mentionStart: row.mentionStart, @@ -3530,17 +3536,19 @@ function getMessageByAuthorAciAndSentAt( const rows = db.prepare(query).all(params); - if (rows.length > 1) { + const [row, ...others] = rows; + + if (others.length > 0) { logger.warn( `getMessageByAuthorAciAndSentAt(${authorAci}, ${sentAtTimestamp}): More than one message found` ); } - if (rows.length < 1) { + if (row == null) { return null; } - return hydrateMessage(db, rows[0]); + return hydrateMessage(db, row); })(); } @@ -5784,28 +5792,30 @@ function getSortedNonAttachmentMedia( }; if (type === 'links') { + const preview = row.preview?.[0]; strictAssert( - row.preview != null && row.preview.length >= 1, + preview, `getSortedNonAttachmentMedia: got message without preview ${row.id}` ); return { type: 'link', message, - preview: row.preview[0], + preview, }; } if (type === 'contacts') { + const contact = row.contact?.[0]; strictAssert( - row.contact != null && row.contact.length >= 1, + contact, `getSortedNonAttachmentMedia: got message without contact ${row.id}` ); return { type: 'contact', message, - contact: row.contact[0], + contact, }; } @@ -9431,8 +9441,10 @@ function getUnreadEditedMessagesAndMarkRead( .prepare(selectQuery) .all(selectParams); - if (rows.length) { - const newestSentAt = rows[0].sent_at; + const [first] = rows; + + if (first != null) { + const newestSentAt = first.sent_at; const [updateStatusQuery, updateStatusParams] = sql` UPDATE edited_messages diff --git a/ts/sql/hydration.std.ts b/ts/sql/hydration.std.ts index 8b90b2246a..d9264ac883 100644 --- a/ts/sql/hydration.std.ts +++ b/ts/sql/hydration.std.ts @@ -50,7 +50,8 @@ export function hydrateMessage( db: ReadableDB, row: MessageTypeUnhydrated ): MessageType { - return hydrateMessages(db, [row])[0]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return hydrateMessages(db, [row])[0]!; } export function hydrateMessages( diff --git a/ts/sql/main.main.ts b/ts/sql/main.main.ts index d22ff99070..74927a7271 100644 --- a/ts/sql/main.main.ts +++ b/ts/sql/main.main.ts @@ -171,6 +171,7 @@ export class MainSQL { this.#onReady = (async () => { const primary = this.#pool[0]; + strictAssert(primary, 'Missing primary'); const rest = this.#pool.slice(1); await this.#send(primary, { @@ -319,6 +320,7 @@ export class MainSQL { } const primary = this.#pool[0]; + strictAssert(primary, 'Missing primary'); const { result, duration } = await this.#send(primary, { type: 'sqlCall:write', @@ -393,6 +395,8 @@ export class MainSQL { async #terminate(request: WorkerRequest): Promise { const primary = this.#pool[0]; + strictAssert(primary, 'Missing primary'); + const rest = this.#pool.slice(1); // Terminate non-primary workers first @@ -532,6 +536,8 @@ export class MainSQL { // Find first pool entry with minimal load #getWorker(): PoolEntry { let min = this.#pool[0]; + strictAssert(min, 'Missing min'); + for (const entry of this.#pool) { if (min && min.load < entry.load) { continue; diff --git a/ts/sql/migrations/1280-blob-unprocessed.std.ts b/ts/sql/migrations/1280-blob-unprocessed.std.ts index f337b57fc9..0844959507 100644 --- a/ts/sql/migrations/1280-blob-unprocessed.std.ts +++ b/ts/sql/migrations/1280-blob-unprocessed.std.ts @@ -14,6 +14,7 @@ import { Migrations as Proto } from '../../protobuf/index.std.js'; import { sql } from '../util.std.js'; import type { WritableDB } from '../Interface.std.js'; import { getOurUuid } from './41-uuid-keys.std.js'; +import { strictAssert } from '../../util/assert.std.js'; export default function updateToSchemaVersion1280( db: WritableDB, @@ -118,6 +119,8 @@ export default function updateToSchemaVersion1280( ? Buffer.from(String(decrypted), 'base64') : decoded.content; + strictAssert(id != null, 'Missing id'); + insertStmt.run({ ...rest, id, diff --git a/ts/sql/migrations/88-service-ids.std.ts b/ts/sql/migrations/88-service-ids.std.ts index 68b1090792..9038e6807f 100644 --- a/ts/sql/migrations/88-service-ids.std.ts +++ b/ts/sql/migrations/88-service-ids.std.ts @@ -594,6 +594,7 @@ function migrateSessions( logger.info(`updating ${sessions.length} sessions`); for (const { id, serviceId, ourServiceId, json } of sessions) { + type Match = RegExpMatchArray & { 1: string; 2: string; 3: string }; const match = id.match(/^(.*):(.*)\.(.*)$/); if (!match) { logger.warn(`invalid session id ${id}`); @@ -607,7 +608,7 @@ function migrateSessions( continue; } - const [, from, to, device] = match; + const [, from, to, device] = match as Match; const newId = `${migrateServiceId(from, ourServiceIds, logger)}:` + @@ -844,6 +845,7 @@ function migratePreKeys( logger.info(`updating ${preKeys.length} ${table}`); for (const { id, json } of preKeys) { + type Match = RegExpMatchArray & { 1: string; 2: string }; const match = id.match(/^(.*):(.*)$/); if (!match) { logger.warn(`invalid ${table} id ${id}`); @@ -858,7 +860,7 @@ function migratePreKeys( continue; } - const [, ourUuid, keyId] = match; + const [, ourUuid, keyId] = match as Match; const ourServiceId = migrateServiceId(ourUuid, ourServiceIds, logger); const newId = `${ourServiceId}:${keyId}`; diff --git a/ts/sql/migrations/index.node.ts b/ts/sql/migrations/index.node.ts index a17bed3f44..0668af4079 100644 --- a/ts/sql/migrations/index.node.ts +++ b/ts/sql/migrations/index.node.ts @@ -147,6 +147,7 @@ import updateToSchemaVersion1670 from './1670-drop-call-link-epoch.std.js'; import updateToSchemaVersion1680 from './1680-cleanup-empty-strings.std.js'; import { DataWriter } from '../Server.node.js'; +import { strictAssert } from '../../util/assert.std.js'; const { keyBy } = lodash; @@ -775,7 +776,8 @@ function updateToSchemaVersion20(db: Database): void { for (const row of allConversations) { const oldId = row.id; const newId = generateUuid(); - allConversationsByOldId[oldId].id = newId; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + allConversationsByOldId[oldId]!.id = newId; const patchObj: { id: string; e164?: string; groupId?: string } = { id: newId, }; @@ -1687,11 +1689,14 @@ export function updateSchema(db: WritableDB, logger: LoggerType): void { const startingVersion = getUserVersion(db); const schemaVersion = getSchemaVersion(db); - const MAX_VERSION = SCHEMA_VERSIONS[SCHEMA_VERSIONS.length - 1].version; + const MAX_VERSION = SCHEMA_VERSIONS[SCHEMA_VERSIONS.length - 1]?.version; + strictAssert(MAX_VERSION, 'Missing MAX_VERSION'); for (let i = 1; i < SCHEMA_VERSIONS.length; i += 1) { - const prev = SCHEMA_VERSIONS[i - 1].version; - const next = SCHEMA_VERSIONS[i].version; + const prev = SCHEMA_VERSIONS[i - 1]?.version; + const next = SCHEMA_VERSIONS[i]?.version; + strictAssert(prev, 'Missing prev'); + strictAssert(next, 'Missing next'); if (prev >= next) { throw new Error( `Migration versions are not monotonic: ${prev} >= ${next}` @@ -1722,7 +1727,9 @@ export function updateSchema(db: WritableDB, logger: LoggerType): void { // eslint-disable-next-line no-loop-func const needsVacuum = db.transaction(() => { for (; i < SCHEMA_VERSIONS.length; i += 1) { - const { version, update } = SCHEMA_VERSIONS[i]; + const schema = SCHEMA_VERSIONS[i]; + strictAssert(schema, 'Missing schema'); + const { version, update } = schema; if (version <= startingVersion) { continue; } diff --git a/ts/sql/util.std.ts b/ts/sql/util.std.ts index 3cc53f9bfe..79f5276401 100644 --- a/ts/sql/util.std.ts +++ b/ts/sql/util.std.ts @@ -73,7 +73,8 @@ export function sqlFragment( const params: Array = []; strings.forEach((string, index) => { - const value = values[index]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const value = values[index]!; query += string; diff --git a/ts/state/ducks/audioPlayer.preload.ts b/ts/state/ducks/audioPlayer.preload.ts index 28765086e9..fa7ceb5f0c 100644 --- a/ts/state/ducks/audioPlayer.preload.ts +++ b/ts/state/ducks/audioPlayer.preload.ts @@ -110,7 +110,8 @@ async function getNextVoiceNote({ return undefined; } - const { message, attachment } = results[0]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const { message, attachment } = results[0]!; return extractVoiceNoteForPlayback( { ...message, diff --git a/ts/state/ducks/calling.preload.ts b/ts/state/ducks/calling.preload.ts index e16f4804bc..374cee7738 100644 --- a/ts/state/ducks/calling.preload.ts +++ b/ts/state/ducks/calling.preload.ts @@ -3398,7 +3398,7 @@ export function reducer( rootKey: callLinks[conversationId]?.rootKey ?? action.payload.callLinkRootKey, - adminKey: callLinks[conversationId]?.adminKey, + adminKey: callLinks[conversationId]?.adminKey ?? null, storageNeedsSync: false, }, } diff --git a/ts/state/ducks/composer.preload.ts b/ts/state/ducks/composer.preload.ts index 34005e8bbe..f31f94292a 100644 --- a/ts/state/ducks/composer.preload.ts +++ b/ts/state/ducks/composer.preload.ts @@ -1206,8 +1206,7 @@ function processAttachments({ file: File; pendingAttachment: AttachmentDraftType; }> = []; - for (let i = 0; i < files.length; i += 1) { - const file = files[i]; + for (const file of files) { const processingResult = preProcessAttachment(file, nextDraftAttachments); if (processingResult != null) { toastToShow = processingResult; @@ -1374,6 +1373,8 @@ function removeAttachment( } const targetAttachment = attachments[targetAttachmentIndex]; + strictAssert(targetAttachment, 'Missing targetAttachment'); + const nextAttachments = attachments .slice(0, targetAttachmentIndex) .concat(attachments.slice(targetAttachmentIndex + 1)); @@ -1734,13 +1735,11 @@ export function reducer( if (action.type === CONVERSATION_UNLOADED) { const nextConversations: Record = {}; - Object.keys(state.conversations).forEach(conversationId => { - if (conversationId === action.payload.conversationId) { - return; - } - - nextConversations[conversationId] = state.conversations[conversationId]; - }); + for (const [conversationId, conversation] of Object.entries( + state.conversations + )) { + nextConversations[conversationId] = conversation; + } return { ...state, diff --git a/ts/state/ducks/conversations.preload.ts b/ts/state/ducks/conversations.preload.ts index bcff03d583..103edc75ba 100644 --- a/ts/state/ducks/conversations.preload.ts +++ b/ts/state/ducks/conversations.preload.ts @@ -1624,7 +1624,7 @@ async function getAvatarsAndUpdateConversation( const { conversationLookup } = conversations; const conversationAttrs = conversationLookup[conversationId]; const avatars = - conversationAttrs.avatars || getAvatarData(conversation.attributes); + conversationAttrs?.avatars || getAvatarData(conversation.attributes); const nextAvatarId = getNextAvatarId(avatars); const nextAvatars = getNextAvatarsData(avatars, nextAvatarId); @@ -3224,6 +3224,8 @@ function toggleSelectMessage( [toggledMessage, conversations.lastSelectedMessage], message => message ); + strictAssert(after, 'Missing after'); + strictAssert(before, 'Missing before'); const betweenIds = await DataReader.getMessagesBetween(conversationId, { after: { @@ -3738,6 +3740,7 @@ function revokePendingMembershipsFromGroupV2( } const [memberId] = memberIds; + strictAssert(memberId, 'Missing memberId'); const pendingMember = window.ConversationController.get(memberId); if (!pendingMember) { @@ -5700,9 +5703,9 @@ function maybeDropMessageIdsFromMessagesLookup( } const updatedMessagesLookup: Record = {}; - for (const messageId of Object.keys(messagesLookup)) { + for (const [messageId, message] of Object.entries(messagesLookup)) { if (!messageIdsToRemove.has(messageId)) { - updatedMessagesLookup[messageId] = messagesLookup[messageId]; + updatedMessagesLookup[messageId] = message; } } @@ -6334,6 +6337,7 @@ export function reducer( } const conversationAttrs = state.conversationLookup[conversationId]; + strictAssert(conversationAttrs, 'Missing conversationAttrs'); const isGroupStoryReply = isGroup(conversationAttrs) && data.storyId; if (isGroupStoryReply) { return dropPreloadData(state); @@ -6563,13 +6567,15 @@ export function reducer( const lastId = oldIds[oldIds.length - 1]; if (oldest && oldest.id === firstId && firstId === id) { - const second = messagesLookup[oldIds[1]]; + const secondId = oldIds[1]; + const second = secondId && messagesLookup[secondId]; oldest = second ? pick(second, ['id', 'received_at', 'sent_at']) : undefined; } if (newest && newest.id === lastId && lastId === id) { - const penultimate = messagesLookup[oldIds[oldIds.length - 2]]; + const secondLastId = oldIds[oldIds.length - 2]; + const penultimate = secondLastId && messagesLookup[secondLastId]; newest = penultimate ? pick(penultimate, ['id', 'received_at', 'sent_at']) : undefined; @@ -6708,7 +6714,11 @@ export function reducer( existingConversation.metrics; const lookup = fromPairs( - existingConversation.messageIds.map(id => [id, messagesLookup[id]]) + existingConversation.messageIds.map(id => { + const message = messagesLookup[id]; + strictAssert(message, 'Missing message'); + return [id, message]; + }) ); messages.forEach(message => { lookup[message.id] = message; @@ -6724,10 +6734,10 @@ export function reducer( const first = sorted[0]; const last = sorted[sorted.length - 1]; - if (!newest) { + if (!newest && first != null) { newest = pick(first, ['id', 'received_at', 'sent_at']); } - if (!oldest) { + if (!oldest && last != null) { oldest = pick(last, ['id', 'received_at', 'sent_at']); } @@ -6801,7 +6811,7 @@ export function reducer( ...existingConversation, messageIds, messageLoadingState: undefined, - scrollToMessageId: isJustSent ? last.id : undefined, + scrollToMessageId: isJustSent ? last?.id : undefined, metrics: { ...existingConversation.metrics, newest, @@ -7372,8 +7382,7 @@ export function reducer( ...state, }; - Object.keys(conversationLookup).forEach(id => { - const existing = conversationLookup[id]; + for (const [id, existing] of Object.entries(conversationLookup)) { const added = { ...existing, conversationColor, @@ -7391,7 +7400,7 @@ export function reducer( }, } ); - }); + } return nextState; } @@ -7431,11 +7440,9 @@ export function reducer( ...state, }; - Object.keys(conversationLookup).forEach(id => { - const existing = conversationLookup[id]; - + for (const [id, existing] of Object.entries(conversationLookup)) { if (existing.customColorId !== colorId) { - return; + continue; } const changed = { @@ -7455,7 +7462,7 @@ export function reducer( }, } ); - }); + } return nextState; } diff --git a/ts/state/ducks/lightbox.preload.ts b/ts/state/ducks/lightbox.preload.ts index 7c6f516423..703a6fc386 100644 --- a/ts/state/ducks/lightbox.preload.ts +++ b/ts/state/ducks/lightbox.preload.ts @@ -404,9 +404,9 @@ function showLightbox(opts: { media, selectedIndex: index === -1 ? 0 : index, hasPrevMessage: - older.length > 0 && filterValidAttachments(older[0]).length > 0, + older[0] != null && filterValidAttachments(older[0]).length > 0, hasNextMessage: - newer.length > 0 && filterValidAttachments(newer[0]).length > 0, + newer[0] != null && filterValidAttachments(newer[0]).length > 0, playbackDisabled: false, }, }); @@ -429,7 +429,7 @@ function showLightboxForAdjacentMessage( return async (dispatch, getState) => { const { lightbox } = getState(); - if (!lightbox.isShowingLightbox || lightbox.media.length === 0) { + if (!lightbox.isShowingLightbox || lightbox.media[0] == null) { log.warn('showLightboxForAdjacentMessage: empty lightbox'); return; } @@ -502,8 +502,10 @@ function showLightboxForAdjacentMessage( showLightbox({ attachment: direction === AdjacentMessageDirection.Previous - ? attachments[attachments.length - 1] - : attachments[0], + ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + attachments[attachments.length - 1]! + : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + attachments[0]!, messageId: adjacent.id, }) ); diff --git a/ts/state/ducks/mediaGallery.preload.ts b/ts/state/ducks/mediaGallery.preload.ts index 1e619afdf4..a96bf4a6a4 100644 --- a/ts/state/ducks/mediaGallery.preload.ts +++ b/ts/state/ducks/mediaGallery.preload.ts @@ -660,7 +660,8 @@ export function reducer( ? [ { type: 'link', - preview: message.preview[0], + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + preview: message.preview[0]!, message: _cleanMessage(message), }, ] @@ -675,7 +676,8 @@ export function reducer( newDocuments = newDocuments.concat( _cleanContact({ type: 'contact', - contact: message.contact[0], + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + contact: message.contact[0]!, message: _cleanMessage(message), }) ); diff --git a/ts/state/ducks/safetyNumber.preload.ts b/ts/state/ducks/safetyNumber.preload.ts index dcce2b77af..47c9fbb839 100644 --- a/ts/state/ducks/safetyNumber.preload.ts +++ b/ts/state/ducks/safetyNumber.preload.ts @@ -17,6 +17,7 @@ import * as Errors from '../../types/errors.std.js'; import type { StateType as RootStateType } from '../reducer.preload.js'; import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions.std.js'; import { useBoundActions } from '../../hooks/useBoundActions.std.js'; +import { strictAssert } from '../../util/assert.std.js'; const { omit } = lodash; @@ -205,6 +206,7 @@ export function reducer( const { contact } = action.payload; const { id } = contact; const record = state.contacts[id]; + strictAssert(record, 'Missing record'); return { contacts: { ...state.contacts, @@ -221,6 +223,7 @@ export function reducer( const { contact, ...restProps } = action.payload; const { id } = contact; const record = state.contacts[id]; + strictAssert(record, 'Missing record'); return { contacts: { ...state.contacts, @@ -237,6 +240,7 @@ export function reducer( const { contact, safetyNumber } = action.payload; const { id } = contact; const record = state.contacts[id]; + strictAssert(record, 'Missing record'); return { contacts: { ...state.contacts, diff --git a/ts/state/ducks/search.preload.ts b/ts/state/ducks/search.preload.ts index 97c0313b03..83cabf58a3 100644 --- a/ts/state/ducks/search.preload.ts +++ b/ts/state/ducks/search.preload.ts @@ -624,10 +624,7 @@ async function queryConversationsAndContacts( // Split into two groups - active conversations and items just from address book let conversationIds: Array = []; let contactIds: Array = []; - const max = searchResults.length; - for (let i = 0; i < max; i += 1) { - const conversation = searchResults[i]; - + for (const conversation of searchResults) { if (conversation.type === 'direct' && !conversation.lastMessage) { contactIds.push(conversation.id); } else { diff --git a/ts/state/ducks/stickers.preload.ts b/ts/state/ducks/stickers.preload.ts index 200c15913f..de355e31af 100644 --- a/ts/state/ducks/stickers.preload.ts +++ b/ts/state/ducks/stickers.preload.ts @@ -27,6 +27,7 @@ import type { EraseStorageServiceStateAction } from './user.preload.js'; import type { NoopActionType } from './noop.std.js'; import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions.std.js'; import { useBoundActions } from '../../hooks/useBoundActions.std.js'; +import { strictAssert } from '../../util/assert.std.js'; const { omit, reject } = lodash; @@ -419,6 +420,7 @@ export function reducer( if (action.type === 'stickers/STICKER_ADDED') { const { payload } = action; const packToUpdate = state.packs[payload.packId]; + strictAssert(packToUpdate, 'Missing packToUpdate'); return { ...state, @@ -438,6 +440,7 @@ export function reducer( if (action.type === 'stickers/STICKER_PACK_UPDATED') { const { payload } = action; const packToUpdate = state.packs[payload.packId]; + strictAssert(packToUpdate, 'Missing packToUpdate'); return { ...state, @@ -480,7 +483,7 @@ export function reducer( packs: { ...packs, [packId]: { - ...packs[packId], + ...existingPack, status, installedAt, }, @@ -515,6 +518,7 @@ export function reducer( item => item.packId === packId && item.stickerId === stickerId ); const pack = packs[packId]; + strictAssert(pack, 'Missing pack'); const sticker = pack.stickers[stickerId]; return { diff --git a/ts/state/ducks/stories.preload.ts b/ts/state/ducks/stories.preload.ts index 265559302e..f5656fcdca 100644 --- a/ts/state/ducks/stories.preload.ts +++ b/ts/state/ducks/stories.preload.ts @@ -916,6 +916,7 @@ const viewUserStories: ViewUserStoriesActionCreatorType = ({ }); const story = storiesByConversationId[currentIndex]; + strictAssert(story, 'Missing story'); const state = getState(); const hiddenConversationIds = new Set(getHideStoryConversationIds(state)); @@ -1081,6 +1082,7 @@ const viewStory: ViewStoryActionCreatorType = ( const currentDistributionListIndex = myStories.findIndex(item => { for (let i = item.stories.length - 1; i >= 0; i -= 1) { const myStory = item.stories[i]; + strictAssert(myStory, 'Missing myStory'); if (myStory.messageId === storyId) { // [1] reversed currentStoryIndex = item.stories.length - 1 - i; @@ -1107,37 +1109,51 @@ const viewStory: ViewStoryActionCreatorType = ( let nextSentStoryIndex = -1; let nextNumStories = numStories; + const currentSentStoryContainer = myStories[currentDistributionListIndex]; + strictAssert( + currentSentStoryContainer, + 'Missing currentSentStoryContainer' + ); + // [2] reversed - const currentStories = myStories[currentDistributionListIndex].stories + const currentStories = currentSentStoryContainer.stories .slice() .reverse(); if (viewDirection === StoryViewDirectionType.Next) { if (currentStoryIndex < currentStories.length - 1) { nextSentStoryIndex = currentStoryIndex + 1; - nextSentStoryId = currentStories[nextSentStoryIndex].messageId; + nextSentStoryId = currentStories[nextSentStoryIndex]?.messageId; } else if (currentDistributionListIndex < myStories.length - 1) { const nextSentStoryContainer = myStories[currentDistributionListIndex + 1]; + strictAssert( + nextSentStoryContainer, + 'Missing nextSentStoryContainer' + ); nextNumStories = nextSentStoryContainer.stories.length; nextSentStoryIndex = 0; nextSentStoryId = - nextSentStoryContainer.stories[nextNumStories - 1].messageId; + nextSentStoryContainer.stories[nextNumStories - 1]?.messageId; } } if (viewDirection === StoryViewDirectionType.Previous) { if (currentStoryIndex > 0) { nextSentStoryIndex = currentStoryIndex - 1; - nextSentStoryId = currentStories[nextSentStoryIndex].messageId; + nextSentStoryId = currentStories[nextSentStoryIndex]?.messageId; } else if (currentDistributionListIndex > 0) { const nextSentStoryContainer = myStories[currentDistributionListIndex - 1]; + strictAssert( + nextSentStoryContainer, + 'Missing nextSentStoryContainer' + ); nextNumStories = nextSentStoryContainer.stories.length; nextSentStoryIndex = nextNumStories - 1; - nextSentStoryId = nextSentStoryContainer.stories[0].messageId; + nextSentStoryId = nextSentStoryContainer.stories[0]?.messageId; } } @@ -1169,6 +1185,7 @@ const viewStory: ViewStoryActionCreatorType = ( ) { const nextIndex = currentIndex + 1; const nextStory = storiesByConversationId[nextIndex]; + strictAssert(nextStory, 'Missing nextStory'); dispatch({ type: VIEW_STORY, @@ -1187,6 +1204,7 @@ const viewStory: ViewStoryActionCreatorType = ( if (viewDirection === StoryViewDirectionType.Previous && currentIndex > 0) { const nextIndex = currentIndex - 1; const nextStory = storiesByConversationId[nextIndex]; + strictAssert(nextStory, 'Missing nextStory'); dispatch({ type: VIEW_STORY, @@ -1245,15 +1263,17 @@ const viewStory: ViewStoryActionCreatorType = ( onlyFromSelf: false, } ); + const nextStory = + nextSelectedStoryData.storiesByConversationId[ + nextSelectedStoryData.currentIndex + ]; + strictAssert(nextStory, 'Missing nextStory'); dispatch({ type: VIEW_STORY, payload: { currentIndex: nextSelectedStoryData.currentIndex, - messageId: - nextSelectedStoryData.storiesByConversationId[ - nextSelectedStoryData.currentIndex - ].messageId, + messageId: nextStory.messageId, numStories: nextSelectedStoryData.numStories, storyViewMode, unviewedStoryConversationIdsSorted, @@ -1298,6 +1318,7 @@ const viewStory: ViewStoryActionCreatorType = ( // Touch area for tapping right should be 80% of width of the screen const nextConversationStoryIndex = conversationStoryIndex + 1; const conversationStory = conversationStories[nextConversationStoryIndex]; + strictAssert(conversationStory, 'Missing conversationStory'); const nextSelectedStoryData = getSelectedStoryDataForConversationId( dispatch, @@ -1308,11 +1329,14 @@ const viewStory: ViewStoryActionCreatorType = ( } ); + const nextStory = nextSelectedStoryData.storiesByConversationId[0]; + strictAssert(nextStory, 'Missing nextStory'); + dispatch({ type: VIEW_STORY, payload: { currentIndex: 0, - messageId: nextSelectedStoryData.storiesByConversationId[0].messageId, + messageId: nextStory.messageId, numStories: nextSelectedStoryData.numStories, storyViewMode, unviewedStoryConversationIdsSorted, @@ -1336,6 +1360,7 @@ const viewStory: ViewStoryActionCreatorType = ( // Touch area for tapping left should be 20% of width of the screen const nextConversationStoryIndex = conversationStoryIndex - 1; const conversationStory = conversationStories[nextConversationStoryIndex]; + strictAssert(conversationStory, 'Missing conversationStory'); const nextSelectedStoryData = getSelectedStoryDataForConversationId( dispatch, @@ -1346,11 +1371,14 @@ const viewStory: ViewStoryActionCreatorType = ( } ); + const nextStory = nextSelectedStoryData.storiesByConversationId[0]; + strictAssert(nextStory, 'Missing nextStory'); + dispatch({ type: VIEW_STORY, payload: { currentIndex: 0, - messageId: nextSelectedStoryData.storiesByConversationId[0].messageId, + messageId: nextStory.messageId, numStories: nextSelectedStoryData.numStories, storyViewMode, unviewedStoryConversationIdsSorted, @@ -1558,6 +1586,7 @@ export function reducer( ); if (prevStoryIndex >= 0) { const prevStory = state.stories[prevStoryIndex]; + strictAssert(prevStory, 'Missing prevStory'); // Stories rarely need to change, here are the following exceptions... @@ -1708,17 +1737,20 @@ export function reducer( story => story.messageId === replyState.messageId ); - const stories = - storyIndex >= 0 - ? replaceIndex(state.stories, storyIndex, { - ...state.stories[storyIndex], - hasReplies: true, - hasRepliesFromSelf: - state.stories[storyIndex].hasRepliesFromSelf || - state.stories[storyIndex].conversationId === - action.payload.conversationId, - }) - : state.stories; + let stories: ReadonlyArray; + if (storyIndex >= 0) { + const currentStory = state.stories[storyIndex]; + strictAssert(currentStory, 'Missing currentStory'); + stories = replaceIndex(state.stories, storyIndex, { + ...currentStory, + hasReplies: true, + hasRepliesFromSelf: + currentStory.hasRepliesFromSelf || + currentStory.conversationId === action.payload.conversationId, + }); + } else { + stories = state.stories; + } return { ...state, @@ -1775,6 +1807,7 @@ export function reducer( } const existingStory = state.stories[storyIndex]; + strictAssert(existingStory, 'Missing existingStory'); return { ...state, diff --git a/ts/state/ducks/storyDistributionLists.preload.ts b/ts/state/ducks/storyDistributionLists.preload.ts index 1293c6274b..46a6f34df3 100644 --- a/ts/state/ducks/storyDistributionLists.preload.ts +++ b/ts/state/ducks/storyDistributionLists.preload.ts @@ -19,6 +19,7 @@ import { storageServiceUploadJob } from '../../services/storage.preload.js'; import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions.std.js'; import { useBoundActions } from '../../hooks/useBoundActions.std.js'; import { itemStorage } from '../../textsecure/Storage.preload.js'; +import { strictAssert } from '../../util/assert.std.js'; const { omit } = lodash; @@ -530,9 +531,11 @@ function replaceDistributionListData( return; } + const list = distributionLists[listIndex]; + strictAssert(list, 'Missing list'); return replaceIndex(distributionLists, listIndex, { - ...distributionLists[listIndex], - ...getNextDistributionListData(distributionLists[listIndex]), + ...list, + ...getNextDistributionListData(list), }); } @@ -551,6 +554,11 @@ export function reducer( ); if (listIndex >= 0) { const existingDistributionList = state.distributionLists[listIndex]; + strictAssert( + existingDistributionList, + 'Missing existingDistributionList' + ); + const memberServiceIds = new Set( existingDistributionList.memberServiceIds ); diff --git a/ts/state/selectors/callHistory.std.ts b/ts/state/selectors/callHistory.std.ts index b2c3c0f928..e52d442779 100644 --- a/ts/state/selectors/callHistory.std.ts +++ b/ts/state/selectors/callHistory.std.ts @@ -46,9 +46,7 @@ export const getCallHistoryLatestCall = createSelector( callHistory => { let latestCall = null; - for (const callId of Object.keys(callHistory.callHistoryByCallId)) { - const call = callHistory.callHistoryByCallId[callId]; - + for (const call of Object.values(callHistory.callHistoryByCallId)) { // Skip unused call links if ( call.type === CallType.Adhoc && diff --git a/ts/state/selectors/conversations.dom.ts b/ts/state/selectors/conversations.dom.ts index 08d3607f9b..959d747c15 100644 --- a/ts/state/selectors/conversations.dom.ts +++ b/ts/state/selectors/conversations.dom.ts @@ -456,11 +456,7 @@ export const _getLeftPaneLists = ({ const archivedConversations: Array = []; const pinnedConversations: Array = []; - const values = Object.values(conversationLookup); - const max = values.length; - for (let i = 0; i < max; i += 1) { - let conversation = values[i]; - + for (let conversation of Object.values(conversationLookup)) { if ( isChatFoldersEnabled(window.SignalContext.getVersion()) && !_shouldIncludeInChatFolder( @@ -1180,7 +1176,8 @@ export const getCachedConversationMemberColorsSelector = createSelector( contactNameColors.set( conversation.id, - ContactNameColors[i % ContactNameColors.length] + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + ContactNameColors[i % ContactNameColors.length]! ); }); @@ -1211,9 +1208,9 @@ export const getContactNameColorSelector = createSelector( ); export const getContactNameColor = ( - contactNameColors: Map, + contactNameColors: Map, contactId: string | undefined -): string => { +): ContactNameColorType => { if (!contactId) { log.warn('No color generated for missing contactId'); return ContactNameColors[0]; @@ -1460,10 +1457,17 @@ export const getConversationsStoppingSend = createSelector( export const getHideStoryConversationIds = createSelector( getConversationLookup, - (conversationLookup): Array => - Object.keys(conversationLookup).filter( - conversationId => conversationLookup[conversationId].hideStory - ) + (conversationLookup): Array => { + const result: Array = []; + for (const [conversationId, conversation] of Object.entries( + conversationLookup + )) { + if (conversation.hideStory) { + result.push(conversationId); + } + } + return result; + } ); export const getStoriesState = (state: StateType): StoriesStateType => @@ -1546,20 +1550,10 @@ export const getLastEditableMessageId = createSelector( return; } - for (let i = conversationMessages.messageIds.length - 1; i >= 0; i -= 1) { - const messageId = conversationMessages.messageIds[i]; + return conversationMessages.messageIds.findLast(messageId => { const message = messagesLookup[messageId]; - - if (!message) { - continue; - } - - if (isOutgoing(message)) { - return canEditMessage(message) ? message.id : undefined; - } - } - - return undefined; + return message != null && isOutgoing(message) && canEditMessage(message); + }); } ); diff --git a/ts/state/selectors/message.preload.ts b/ts/state/selectors/message.preload.ts index 74a6f70de5..dce3096329 100644 --- a/ts/state/selectors/message.preload.ts +++ b/ts/state/selectors/message.preload.ts @@ -72,7 +72,10 @@ import { defaultBlurHash, } from '../../util/Attachment.std.js'; import type { MessageAttachmentType } from '../../types/AttachmentDownload.std.js'; -import { type DefaultConversationColorType } from '../../types/Colors.std.js'; +import type { + ContactNameColorType, + DefaultConversationColorType, +} from '../../types/Colors.std.js'; import { ReadStatus } from '../../messages/MessageReadStatus.std.js'; import type { CallingNotificationType } from '../../util/callingNotification.std.js'; @@ -212,7 +215,7 @@ export type GetPropsForBubbleOptions = Readonly<{ callHistorySelector: CallHistorySelectorType; activeCall?: CallStateType; accountSelector: AccountSelectorType; - contactNameColors: Map; + contactNameColors: Map; defaultConversationColor: DefaultConversationColorType; }>; @@ -2115,7 +2118,7 @@ function getDeletedForEveryoneByAdmin( message: MessageWithUIFieldsType, options: { conversationSelector: GetConversationByIdType; - contactNameColors: Map; + contactNameColors: Map; ourAci: AciString | undefined; } ): PropsData['deletedForEveryoneByAdmin'] { @@ -2157,7 +2160,8 @@ export function getPropsForEmbeddedContact( return undefined; } - const firstContact = contacts[0]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const firstContact = contacts[0]!; const numbers = firstContact?.number; const firstNumber = numbers && numbers[0] ? numbers[0].value : undefined; diff --git a/ts/state/selectors/stickers.std.ts b/ts/state/selectors/stickers.std.ts index eca1e08bcf..5dc3352d5c 100644 --- a/ts/state/selectors/stickers.std.ts +++ b/ts/state/selectors/stickers.std.ts @@ -185,7 +185,7 @@ export const getBlessedStickerPacks = createSelector( ): Array => { return filterAndTransformPacks( packs, - pack => blessedPacks[pack.id] && pack.status !== 'installed', + pack => blessedPacks[pack.id] != null && pack.status !== 'installed', pack => pack.createdAt, blessedPacks ); diff --git a/ts/state/selectors/stories.preload.ts b/ts/state/selectors/stories.preload.ts index 4e1a6fd4b9..0f801b3d18 100644 --- a/ts/state/selectors/stories.preload.ts +++ b/ts/state/selectors/stories.preload.ts @@ -113,15 +113,18 @@ function sortMyStories(storyA: MyStoryType, storyB: MyStoryType): number { return 1; } - if (!storyA.stories.length) { + const a = storyA.stories[0]; + const b = storyB.stories[0]; + + if (a == null) { return 1; } - if (!storyB.stories.length) { + if (b == null) { return -1; } - return storyA.stories[0].timestamp > storyB.stories[0].timestamp ? -1 : 1; + return b.timestamp - a.timestamp; } function getAvatarData( diff --git a/ts/state/selectors/timeline.preload.ts b/ts/state/selectors/timeline.preload.ts index 26a260ca02..8b46a31bc4 100644 --- a/ts/state/selectors/timeline.preload.ts +++ b/ts/state/selectors/timeline.preload.ts @@ -41,11 +41,12 @@ import { import type { ConversationType } from '../ducks/conversations.preload.js'; import { missingCaseError } from '../../util/missingCaseError.std.js'; import { getGroupMemberships } from '../../util/getGroupMemberships.dom.js'; +import type { ContactNameColorType } from '../../types/Colors.std.js'; const getTimelineItem = ( state: StateType, messageId: string | undefined, - contactNameColors: Map + contactNameColors: Map ): TimelineItemType | undefined => { if (messageId === undefined) { return undefined; diff --git a/ts/state/smart/CallManager.preload.tsx b/ts/state/smart/CallManager.preload.tsx index d17d10adce..67937d5343 100644 --- a/ts/state/smart/CallManager.preload.tsx +++ b/ts/state/smart/CallManager.preload.tsx @@ -194,8 +194,8 @@ const mapStateToActiveCallProp = ( }, } = call; - for (let i = 0; i < memberships.length; i += 1) { - const { aci } = memberships[i]; + for (const membership of memberships) { + const { aci } = membership; const member = conversationSelector(aci); if (!member) { @@ -206,9 +206,7 @@ const mapStateToActiveCallProp = ( groupMembers.push(member); } - for (let i = 0; i < call.remoteParticipants.length; i += 1) { - const remoteParticipant = call.remoteParticipants[i]; - + for (const remoteParticipant of call.remoteParticipants) { const remoteConversation = conversationSelectorByAci( remoteParticipant.aci ); @@ -248,9 +246,7 @@ const mapStateToActiveCallProp = ( } }); - for (let i = 0; i < peekInfo.acis.length; i += 1) { - const peekedParticipantAci = peekInfo.acis[i]; - + for (const peekedParticipantAci of peekInfo.acis) { const peekedConversation = conversationSelectorByAci(peekedParticipantAci); if (!peekedConversation) { @@ -261,9 +257,7 @@ const mapStateToActiveCallProp = ( peekedParticipants.push(peekedConversation); } - for (let i = 0; i < peekInfo.pendingAcis.length; i += 1) { - const aci = peekInfo.pendingAcis[i]; - + for (const aci of peekInfo.pendingAcis) { // In call links, pending users may be unknown until they share profile keys. // conversationSelectorByAci should create conversations for new contacts. const pendingConversation = conversationSelectorByAci(aci); diff --git a/ts/state/smart/ToastManager.preload.tsx b/ts/state/smart/ToastManager.preload.tsx index 2e39729af3..c9c28180e4 100644 --- a/ts/state/smart/ToastManager.preload.tsx +++ b/ts/state/smart/ToastManager.preload.tsx @@ -104,7 +104,8 @@ export const SmartToastManager = memo(function SmartToastManager({ }; } else if (megaphones.length > 0) { megaphone = { - ...megaphones[0], + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + ...megaphones[0]!, type: MegaphoneType.Remote, onInteractWithMegaphone: interactWithMegaphone, }; diff --git a/ts/test-electron/Crypto_test.preload.ts b/ts/test-electron/Crypto_test.preload.ts index 9276f2f5a9..61494f4d93 100644 --- a/ts/test-electron/Crypto_test.preload.ts +++ b/ts/test-electron/Crypto_test.preload.ts @@ -305,7 +305,8 @@ describe('Crypto', () => { const key = getRandomBytes(32); const encrypted = encryptSymmetric(key, plaintext); - encrypted[2] += 2; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + encrypted[2]! += 2; try { decryptSymmetric(key, encrypted); @@ -326,7 +327,8 @@ describe('Crypto', () => { const key = getRandomBytes(32); const encrypted = encryptSymmetric(key, plaintext); - encrypted[encrypted.length - 3] += 2; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + encrypted[encrypted.length - 3]! += 2; try { decryptSymmetric(key, encrypted); @@ -347,7 +349,8 @@ describe('Crypto', () => { const key = getRandomBytes(32); const encrypted = encryptSymmetric(key, plaintext); - encrypted[35] += 9; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + encrypted[35]! += 9; try { decryptSymmetric(key, encrypted); @@ -751,7 +754,8 @@ describe('Crypto', () => { isNumber(macLength) && encryptedAttachment.incrementalMac ) { - encryptedAttachment.incrementalMac[macLength / 2] += 1; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + encryptedAttachment.incrementalMac[macLength / 2]! += 1; } // Decrypt it via plaintextHash first diff --git a/ts/test-electron/MessageReceipts_test.preload.ts b/ts/test-electron/MessageReceipts_test.preload.ts index 613a1bba91..580d13d126 100644 --- a/ts/test-electron/MessageReceipts_test.preload.ts +++ b/ts/test-electron/MessageReceipts_test.preload.ts @@ -106,10 +106,10 @@ describe('MessageReceipts', () => { const messageFromDatabase = await DataReader.getMessageById(id); const savedSendState = messageFromDatabase?.sendStateByConversationId; - assert.equal(savedSendState?.aaaa.status, SendStatus.Read, 'aaaa'); - assert.equal(savedSendState?.bbbb.status, SendStatus.Delivered, 'bbbb'); - assert.equal(savedSendState?.cccc.status, SendStatus.Read, 'cccc'); - assert.equal(savedSendState?.dddd.status, SendStatus.Sent, 'dddd'); + assert.equal(savedSendState?.aaaa?.status, SendStatus.Read, 'aaaa'); + assert.equal(savedSendState?.bbbb?.status, SendStatus.Delivered, 'bbbb'); + assert.equal(savedSendState?.cccc?.status, SendStatus.Read, 'cccc'); + assert.equal(savedSendState?.dddd?.status, SendStatus.Sent, 'dddd'); }); it('updates sendStateByConversationId for edits', async () => { @@ -221,34 +221,34 @@ describe('MessageReceipts', () => { assert.deepEqual( rootSendState, - messageFromDatabase?.editHistory?.[0].sendStateByConversationId, + messageFromDatabase?.editHistory?.[0]?.sendStateByConversationId, 'edit history version should match root version' ); - assert.equal(rootSendState?.aaaa.status, SendStatus.Delivered, 'aaaa'); - assert.equal(rootSendState?.bbbb.status, SendStatus.Read, 'bbbb'); - assert.equal(rootSendState?.cccc.status, SendStatus.Read, 'cccc'); - assert.equal(rootSendState?.dddd.status, SendStatus.Sent, 'dddd'); + assert.equal(rootSendState?.aaaa?.status, SendStatus.Delivered, 'aaaa'); + assert.equal(rootSendState?.bbbb?.status, SendStatus.Read, 'bbbb'); + assert.equal(rootSendState?.cccc?.status, SendStatus.Read, 'cccc'); + assert.equal(rootSendState?.dddd?.status, SendStatus.Sent, 'dddd'); const originalMessageSendState = - messageFromDatabase?.editHistory?.[1].sendStateByConversationId; + messageFromDatabase?.editHistory?.[1]?.sendStateByConversationId; assert.equal( - originalMessageSendState?.aaaa.status, + originalMessageSendState?.aaaa?.status, SendStatus.Read, 'original-aaaa' ); assert.equal( - originalMessageSendState?.bbbb.status, + originalMessageSendState?.bbbb?.status, SendStatus.Delivered, 'original-bbbb' ); assert.equal( - originalMessageSendState?.cccc.status, + originalMessageSendState?.cccc?.status, SendStatus.Read, 'original-cccc' ); assert.equal( - originalMessageSendState?.dddd.status, + originalMessageSendState?.dddd?.status, SendStatus.Sent, 'original-dddd' ); diff --git a/ts/test-electron/SignalProtocolStore_test.preload.ts b/ts/test-electron/SignalProtocolStore_test.preload.ts index 3494edf479..d2fcdc76d0 100644 --- a/ts/test-electron/SignalProtocolStore_test.preload.ts +++ b/ts/test-electron/SignalProtocolStore_test.preload.ts @@ -1527,9 +1527,9 @@ describe('SignalProtocolStore', () => { // they are in the proper order because the collection comparator is // 'receivedAtCounter' - assert.strictEqual(Bytes.toString(items[0].content || ZERO), 'first'); - assert.strictEqual(Bytes.toString(items[1].content || ZERO), 'second'); - assert.strictEqual(Bytes.toString(items[2].content || ZERO), 'third'); + assert.strictEqual(Bytes.toString(items[0]?.content || ZERO), 'first'); + assert.strictEqual(Bytes.toString(items[1]?.content || ZERO), 'second'); + assert.strictEqual(Bytes.toString(items[2]?.content || ZERO), 'third'); }); it('removeUnprocessed successfully deletes item', async () => { diff --git a/ts/test-electron/backup/helpers.preload.ts b/ts/test-electron/backup/helpers.preload.ts index 94890c95fd..f02cfbe586 100644 --- a/ts/test-electron/backup/helpers.preload.ts +++ b/ts/test-electron/backup/helpers.preload.ts @@ -267,7 +267,8 @@ export async function asymmetricRoundtripHarness( if (options.comparator) { assert.strictEqual(actual.length, expected.length); for (let i = 0; i < actual.length; i += 1) { - options.comparator(expected[i], actual[i]); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + options.comparator(expected[i]!, actual[i]!); } } else { assert.deepEqual(actual, expected); diff --git a/ts/test-electron/linkPreviews/linkPreviewFetch_test.preload.ts b/ts/test-electron/linkPreviews/linkPreviewFetch_test.preload.ts index b6f4802aa7..7d69d292cc 100644 --- a/ts/test-electron/linkPreviews/linkPreviewFetch_test.preload.ts +++ b/ts/test-electron/linkPreviews/linkPreviewFetch_test.preload.ts @@ -157,8 +157,11 @@ describe('link preview fetching', () => { tag: '', expectedHref: 'https://example.com/icon.jpg', }, - ]; - for (let i = orderedImageHrefSources.length - 1; i >= 0; i -= 1) { + ] as const; + for (const [i, orderedImageHrefSource] of orderedImageHrefSources + .entries() + .toArray() + .toReversed()) { const imageTags = orderedImageHrefSources .slice(i) .map(({ tag }) => tag) @@ -180,11 +183,7 @@ describe('link preview fetching', () => { 'https://example.com', new AbortController().signal ); - assert.propertyVal( - val, - 'image', - orderedImageHrefSources[i].expectedHref - ); + assert.propertyVal(val, 'image', orderedImageHrefSource.expectedHref); } }); diff --git a/ts/test-electron/models/conversations_test.preload.ts b/ts/test-electron/models/conversations_test.preload.ts index e273f41fcc..b1419955d2 100644 --- a/ts/test-electron/models/conversations_test.preload.ts +++ b/ts/test-electron/models/conversations_test.preload.ts @@ -163,8 +163,8 @@ describe('Conversations', () => { ] ); - assert.equal(resultWithImage.contentType, 'image/png'); - assert.equal(resultWithImage.fileName, null); + assert.equal(resultWithImage?.contentType, 'image/png'); + assert.equal(resultWithImage?.fileName, null); }); describe('updateExpirationTimer', () => { diff --git a/ts/test-electron/models/messages_test.preload.ts b/ts/test-electron/models/messages_test.preload.ts index 18df7d81a1..10723e718f 100644 --- a/ts/test-electron/models/messages_test.preload.ts +++ b/ts/test-electron/models/messages_test.preload.ts @@ -184,7 +184,7 @@ describe('Message', () => { const errors = message.get('errors') || []; assert.lengthOf(errors, 1); - assert.strictEqual(errors[0].message, 'foo bar'); + assert.strictEqual(errors[0]?.message, 'foo bar'); }); it('saves errors from promise rejections with objects', async () => { @@ -201,7 +201,7 @@ describe('Message', () => { const errors = message.get('errors') || []; assert.lengthOf(errors, 1); - assert.strictEqual(errors[0].message, 'baz qux'); + assert.strictEqual(errors[0]?.message, 'baz qux'); }); }); diff --git a/ts/test-electron/quill/emoji/completion_test.dom.tsx b/ts/test-electron/quill/emoji/completion_test.dom.tsx index a25e4f50a3..bdfff0c016 100644 --- a/ts/test-electron/quill/emoji/completion_test.dom.tsx +++ b/ts/test-electron/quill/emoji/completion_test.dom.tsx @@ -252,7 +252,10 @@ describe('emojiCompletion', () => { }); it('inserts the emoji at the current cursor position', () => { - const [{ emojiParentKey, index, range }] = insertEmojiStub.args[0]; + const firstArgs = insertEmojiStub.args[0]; + assert.exists(firstArgs); + + const [{ emojiParentKey, index, range }] = firstArgs; assert.equal(emojiParentKey, PARENT_KEYS.SMILE); assert.equal(index, 0); @@ -281,7 +284,10 @@ describe('emojiCompletion', () => { }); it('inserts the emoji at the current cursor position', () => { - const [{ emojiParentKey, index, range }] = insertEmojiStub.args[0]; + const firstArgs = insertEmojiStub.args[0]; + assert.exists(firstArgs); + + const [{ emojiParentKey, index, range }] = firstArgs; assert.equal(emojiParentKey, PARENT_KEYS.SMILE); assert.equal(index, 7); @@ -341,7 +347,10 @@ describe('emojiCompletion', () => { }); it('inserts the emoji at the current cursor position', () => { - const [{ emojiParentKey, index, range }] = insertEmojiStub.args[0]; + const firstArgs = insertEmojiStub.args[0]; + assert.exists(firstArgs); + + const [{ emojiParentKey, index, range }] = firstArgs; assert.equal(emojiParentKey, PARENT_KEYS.SMILE); assert.equal(index, 0); @@ -390,7 +399,10 @@ describe('emojiCompletion', () => { }); it('inserts the emoji at the current cursor position', () => { - const [{ emojiParentKey, index, range }] = insertEmojiStub.args[0]; + const firstArgs = insertEmojiStub.args[0]; + assert.exists(firstArgs); + + const [{ emojiParentKey, index, range }] = firstArgs; assert.equal(emojiParentKey, PARENT_KEYS.SMILE); assert.equal(index, 0); @@ -435,8 +447,10 @@ describe('emojiCompletion', () => { }); it('inserts the currently selected emoji at the current cursor position', () => { - const [{ emojiParentKey, index: insertIndex, range }] = - insertEmojiStub.args[0]; + const firstArgs = insertEmojiStub.args[0]; + assert.exists(firstArgs); + + const [{ emojiParentKey, index: insertIndex, range }] = firstArgs; assert.equal(emojiParentKey, PARENT_KEYS.SMILE_CAT); assert.equal(insertIndex, 0); diff --git a/ts/test-electron/quill/mentions/matchers_test.std.ts b/ts/test-electron/quill/mentions/matchers_test.std.ts index b37e678b3c..a2366967ad 100644 --- a/ts/test-electron/quill/mentions/matchers_test.std.ts +++ b/ts/test-electron/quill/mentions/matchers_test.std.ts @@ -109,6 +109,7 @@ describe('matchMention', () => { assert.isNotEmpty(ops); const [op] = ops; + assert.exists(op); const { insert, attributes } = op; if (isMention(insert)) { @@ -138,6 +139,7 @@ describe('matchMention', () => { assert.isNotEmpty(ops); const [op] = ops; + assert.exists(op); const { insert } = op; if (isMention(insert)) { @@ -165,6 +167,7 @@ describe('matchMention', () => { assert.isNotEmpty(ops); const [op] = ops; + assert.exists(op); const { insert } = op; if (isMention(insert)) { @@ -189,6 +192,7 @@ describe('matchMention', () => { assert.isNotEmpty(ops); const [op] = ops; + assert.exists(op); const { insert } = op; if (isMention(insert)) { diff --git a/ts/test-electron/services/AttachmentBackupManager_test.preload.ts b/ts/test-electron/services/AttachmentBackupManager_test.preload.ts index 944fe0f00c..1cc4a72b5b 100644 --- a/ts/test-electron/services/AttachmentBackupManager_test.preload.ts +++ b/ts/test-electron/services/AttachmentBackupManager_test.preload.ts @@ -175,6 +175,15 @@ describe('AttachmentBackupManager/JobManager', function attachmentBackupManager( await itemStorage.fetch(); }); + function assertAt(array: Array, index: number): T { + assert.ok( + index >= 0 && index < array.length, + `index out of bounds: ${index}` + ); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return array[index]!; + } + async function addJobs( num: number, overrides: Partial = {} @@ -240,37 +249,47 @@ describe('AttachmentBackupManager/JobManager', function attachmentBackupManager( assert.strictEqual(allJobs.length, 10, 'initial setup'); await backupManager?.start(); - await waitForJobToBeStarted(jobs[2]); + await waitForJobToBeStarted(assertAt(jobs, 2)); assert.strictEqual(runJob.callCount, 3); - assertRunJobCalledWith([jobs[4], jobs[3], jobs[2]]); + assertRunJobCalledWith([ + assertAt(jobs, 4), + assertAt(jobs, 3), + assertAt(jobs, 2), + ]); - await waitForJobToBeStarted(jobs[0]); + await waitForJobToBeStarted(assertAt(jobs, 0)); assert.strictEqual(runJob.callCount, 5, 'first calls'); - assertRunJobCalledWith([jobs[4], jobs[3], jobs[2], jobs[1], jobs[0]]); + assertRunJobCalledWith([ + assertAt(jobs, 4), + assertAt(jobs, 3), + assertAt(jobs, 2), + assertAt(jobs, 1), + assertAt(jobs, 0), + ]); - await waitForJobToBeCompleted(thumbnailJobs[0]); + await waitForJobToBeCompleted(assertAt(thumbnailJobs, 0)); assert.strictEqual(runJob.callCount, 10, 'all calls'); assertRunJobCalledWith([ - jobs[4], - jobs[3], - jobs[2], - jobs[1], - jobs[0], - thumbnailJobs[4], - thumbnailJobs[3], - thumbnailJobs[2], - thumbnailJobs[1], - thumbnailJobs[0], + assertAt(jobs, 4), + assertAt(jobs, 3), + assertAt(jobs, 2), + assertAt(jobs, 1), + assertAt(jobs, 0), + assertAt(thumbnailJobs, 4), + assertAt(thumbnailJobs, 3), + assertAt(thumbnailJobs, 2), + assertAt(thumbnailJobs, 1), + assertAt(thumbnailJobs, 0), ]); assert.strictEqual((await getAllSavedJobs()).length, 0); }); it('with transitCdnInfo, will copy to backup tier', async () => { - const [job] = await addJobs(1); + const jobs = await addJobs(1); await backupManager?.start(); - await waitForJobToBeCompleted(job); + await waitForJobToBeCompleted(assertAt(jobs, 0)); assert.strictEqual(backupMediaBatch.callCount, 1); assert.strictEqual(encryptAndUploadAttachment.callCount, 0); @@ -295,9 +314,9 @@ describe('AttachmentBackupManager/JobManager', function attachmentBackupManager( }) ); - const [job] = await addJobs(1); + const jobs = await addJobs(1); await backupManager?.start(); - await waitForJobToBeCompleted(job); + await waitForJobToBeCompleted(assertAt(jobs, 0)); assert.strictEqual(encryptAndUploadAttachment.callCount, 1); assert.strictEqual(backupMediaBatch.callCount, 2); @@ -315,10 +334,10 @@ describe('AttachmentBackupManager/JobManager', function attachmentBackupManager( }); it('without transitCdnInfo, will upload then copy', async () => { - const [job] = await addJobs(1, { transitCdnInfo: undefined }); + const jobs = await addJobs(1, { transitCdnInfo: undefined }); await backupManager?.start(); - await waitForJobToBeCompleted(job); + await waitForJobToBeCompleted(assertAt(jobs, 0)); assert.strictEqual(backupMediaBatch.callCount, 1); assert.strictEqual(encryptAndUploadAttachment.callCount, 1); @@ -329,13 +348,13 @@ describe('AttachmentBackupManager/JobManager', function attachmentBackupManager( }); it('without transitCdnInfo, will permanently remove job if file not found at path', async () => { - const [job] = await addJobs(1, { + const jobs = await addJobs(1, { transitCdnInfo: undefined, path: 'nothing/here', }); await backupManager?.start(); - await waitForJobToBeCompleted(job); + await waitForJobToBeCompleted(assertAt(jobs, 0)); assert.strictEqual(backupMediaBatch.callCount, 0); assert.strictEqual(encryptAndUploadAttachment.callCount, 0); @@ -355,10 +374,14 @@ describe('AttachmentBackupManager/JobManager', function attachmentBackupManager( }) ); await backupManager?.start(); - await waitForJobToBeStarted(jobs[2]); + await waitForJobToBeStarted(assertAt(jobs, 2)); assert.strictEqual(runJob.callCount, 3); - assertRunJobCalledWith([jobs[4], jobs[3], jobs[2]]); + assertRunJobCalledWith([ + assertAt(jobs, 4), + assertAt(jobs, 3), + assertAt(jobs, 2), + ]); // no jobs have occurred await clock.tickAsync(50000); @@ -370,17 +393,17 @@ describe('AttachmentBackupManager/JobManager', function attachmentBackupManager( }); await clock.tickAsync(100000); - await waitForJobToBeStarted(jobs[0]); + await waitForJobToBeStarted(assertAt(jobs, 0)); assert.strictEqual(runJob.callCount, 8); assertRunJobCalledWith([ - jobs[4], - jobs[3], - jobs[2], - jobs[4], - jobs[3], - jobs[2], - jobs[1], - jobs[0], + assertAt(jobs, 4), + assertAt(jobs, 3), + assertAt(jobs, 2), + assertAt(jobs, 4), + assertAt(jobs, 3), + assertAt(jobs, 2), + assertAt(jobs, 1), + assertAt(jobs, 0), ]); }); diff --git a/ts/test-electron/services/AttachmentDownloadManager_test.preload.ts b/ts/test-electron/services/AttachmentDownloadManager_test.preload.ts index bd3b6e46b8..bc9b3d4062 100644 --- a/ts/test-electron/services/AttachmentDownloadManager_test.preload.ts +++ b/ts/test-electron/services/AttachmentDownloadManager_test.preload.ts @@ -180,6 +180,15 @@ describe('AttachmentDownloadManager', () => { await itemStorage.fetch(); }); + function assertAt(array: ReadonlyArray, index: number): T { + assert.ok( + index >= 0 && index < array.length, + `index out of bounds: ${index}` + ); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return array[index]!; + } + async function addJob( job: AttachmentDownloadJobType, urgency: AttachmentDownloadUrgency @@ -306,14 +315,24 @@ describe('AttachmentDownloadManager', () => { ); await downloadManager?.start(); - await waitForJobToBeStarted(jobs[2]); + await waitForJobToBeStarted(assertAt(jobs, 2)); assert.strictEqual(runJob.callCount, 3); - assertRunJobCalledWith([jobs[4], jobs[3], jobs[2]]); + assertRunJobCalledWith([ + assertAt(jobs, 4), + assertAt(jobs, 3), + assertAt(jobs, 2), + ]); - await waitForJobToBeStarted(jobs[0]); + await waitForJobToBeStarted(assertAt(jobs, 0)); assert.strictEqual(runJob.callCount, 5); - assertRunJobCalledWith([jobs[4], jobs[3], jobs[2], jobs[1], jobs[0]]); + assertRunJobCalledWith([ + assertAt(jobs, 4), + assertAt(jobs, 3), + assertAt(jobs, 2), + assertAt(jobs, 1), + assertAt(jobs, 0), + ]); }); it('runs a job immediately if urgency is IMMEDIATE', async () => { @@ -330,18 +349,23 @@ describe('AttachmentDownloadManager', () => { await waitForJobToBeStarted(urgentJobForOldMessage); assert.strictEqual(runJob.callCount, 4); - assertRunJobCalledWith([jobs[5], jobs[4], jobs[3], urgentJobForOldMessage]); + assertRunJobCalledWith([ + assertAt(jobs, 5), + assertAt(jobs, 4), + assertAt(jobs, 3), + urgentJobForOldMessage, + ]); - await waitForJobToBeStarted(jobs[0]); + await waitForJobToBeStarted(assertAt(jobs, 0)); assert.strictEqual(runJob.callCount, 7); assertRunJobCalledWith([ - jobs[5], - jobs[4], - jobs[3], + assertAt(jobs, 5), + assertAt(jobs, 4), + assertAt(jobs, 3), urgentJobForOldMessage, - jobs[2], - jobs[1], - jobs[0], + assertAt(jobs, 2), + assertAt(jobs, 1), + assertAt(jobs, 0), ]); }); @@ -352,13 +376,23 @@ describe('AttachmentDownloadManager', () => { await downloadManager?.start(); - await waitForJobToBeStarted(jobs[4]); + await waitForJobToBeStarted(assertAt(jobs, 4)); assert.strictEqual(runJob.callCount, 3); - assertRunJobCalledWith([jobs[0], jobs[1], jobs[4]]); + assertRunJobCalledWith([ + assertAt(jobs, 0), + assertAt(jobs, 1), + assertAt(jobs, 4), + ]); - await waitForJobToBeStarted(jobs[2]); + await waitForJobToBeStarted(assertAt(jobs, 2)); assert.strictEqual(runJob.callCount, 5); - assertRunJobCalledWith([jobs[0], jobs[1], jobs[4], jobs[3], jobs[2]]); + assertRunJobCalledWith([ + assertAt(jobs, 0), + assertAt(jobs, 1), + assertAt(jobs, 4), + assertAt(jobs, 3), + assertAt(jobs, 2), + ]); }); it("does not start a job if we're in a call", async () => { @@ -373,7 +407,7 @@ describe('AttachmentDownloadManager', () => { isInCall.callsFake(() => false); await advanceTime(2 * MINUTE); - await waitForJobToBeStarted(jobs[0]); + await waitForJobToBeStarted(assertAt(jobs, 0)); assert.strictEqual(runJob.callCount, 5); }); @@ -382,12 +416,12 @@ describe('AttachmentDownloadManager', () => { source: AttachmentDownloadSource.BACKUP_IMPORT_WITH_MEDIA, })); - const jobAttempts = getPromisesForAttempts(jobs[0], 2); + const jobAttempts = getPromisesForAttempts(assertAt(jobs, 0), 2); statfs.callsFake(() => Promise.resolve({ bavail: 0, bsize: 8 })); await downloadManager?.start(); - await jobAttempts[0].completed; + await assertAt(jobAttempts, 0).completed; assert.strictEqual(runJob.callCount, 0); assert.strictEqual(onLowDiskSpaceBackupImport.callCount, 1); @@ -400,18 +434,18 @@ describe('AttachmentDownloadManager', () => { await advanceTime(2 * MINUTE); assert.strictEqual(runJob.callCount, 1); - await jobAttempts[1].completed; + await assertAt(jobAttempts, 1).completed; }); it('handles retries for failed', async () => { const jobs = await addJobs(2); - const job0Attempts = getPromisesForAttempts(jobs[0], 1); - const job1Attempts = getPromisesForAttempts(jobs[1], 5); + const job0Attempts = getPromisesForAttempts(assertAt(jobs, 0), 1); + const job1Attempts = getPromisesForAttempts(assertAt(jobs, 1), 5); runJob.callsFake(async ({ job }: { job: AttachmentDownloadJobType }) => { return new Promise<{ status: 'finished' | 'retry' }>(resolve => { Promise.resolve().then(() => { - if (job.messageId === jobs[0].messageId) { + if (job.messageId === assertAt(jobs, 0).messageId) { resolve({ status: 'finished' }); } else { resolve({ status: 'retry' }); @@ -422,12 +456,16 @@ describe('AttachmentDownloadManager', () => { await downloadManager?.start(); - await job0Attempts[0].completed; + await assertAt(job0Attempts, 0).completed; assert.strictEqual(runJob.callCount, 2); - assertRunJobCalledWith([jobs[1], jobs[0]]); + assertRunJobCalledWith([assertAt(jobs, 1), assertAt(jobs, 0)]); - const retriedJob = await DataReader._getAttachmentDownloadJob(jobs[1]); - const finishedJob = await DataReader._getAttachmentDownloadJob(jobs[0]); + const retriedJob = await DataReader._getAttachmentDownloadJob( + assertAt(jobs, 1) + ); + const finishedJob = await DataReader._getAttachmentDownloadJob( + assertAt(jobs, 0) + ); assert.isUndefined(finishedJob); assert.strictEqual(retriedJob?.attempts, 1); @@ -435,32 +473,34 @@ describe('AttachmentDownloadManager', () => { await advanceTime(MINUTE); - await job1Attempts[1].completed; + await assertAt(job1Attempts, 1).completed; assert.strictEqual(runJob.callCount, 3); await advanceTime(2 * MINUTE); - await job1Attempts[2].completed; + await assertAt(job1Attempts, 2).completed; assert.strictEqual(runJob.callCount, 4); await advanceTime(4 * MINUTE); - await job1Attempts[3].completed; + await assertAt(job1Attempts, 3).completed; assert.strictEqual(runJob.callCount, 5); await advanceTime(8 * MINUTE); - await job1Attempts[4].completed; + await assertAt(job1Attempts, 4).completed; assert.strictEqual(runJob.callCount, 6); assertRunJobCalledWith([ - jobs[1], - jobs[0], - jobs[1], - jobs[1], - jobs[1], - jobs[1], + assertAt(jobs, 1), + assertAt(jobs, 0), + assertAt(jobs, 1), + assertAt(jobs, 1), + assertAt(jobs, 1), + assertAt(jobs, 1), ]); // Ensure it's been removed after completed - assert.isUndefined(await DataReader._getAttachmentDownloadJob(jobs[1])); + assert.isUndefined( + await DataReader._getAttachmentDownloadJob(assertAt(jobs, 1)) + ); }); it('will reset attempts if addJob is called again', async () => { @@ -473,47 +513,49 @@ describe('AttachmentDownloadManager', () => { }); }); - let attempts = getPromisesForAttempts(jobs[0], 4); + let attempts = getPromisesForAttempts(assertAt(jobs, 0), 4); await downloadManager?.start(); - await attempts[0].completed; + await assertAt(attempts, 0).completed; assert.strictEqual(runJob.callCount, 1); await advanceTime(1 * MINUTE); - await attempts[1].completed; + await assertAt(attempts, 1).completed; assert.strictEqual(runJob.callCount, 2); await advanceTime(5 * MINUTE); - await attempts[2].completed; + await assertAt(attempts, 2).completed; assert.strictEqual(runJob.callCount, 3); // add the same job again and it should retry ASAP and reset attempts - attempts = getPromisesForAttempts(jobs[0], 5); + attempts = getPromisesForAttempts(assertAt(jobs, 0), 5); await downloadManager?.addJob({ - ...jobs[0], - isManualDownload: Boolean(jobs[0].isManualDownload), + ...assertAt(jobs, 0), + isManualDownload: Boolean(assertAt(jobs, 0).isManualDownload), }); - await attempts[0].completed; + await assertAt(attempts, 0).completed; assert.strictEqual(runJob.callCount, 4); await advanceTime(1 * MINUTE); - await attempts[1].completed; + await assertAt(attempts, 1).completed; assert.strictEqual(runJob.callCount, 5); await advanceTime(2 * MINUTE); - await attempts[2].completed; + await assertAt(attempts, 2).completed; assert.strictEqual(runJob.callCount, 6); await advanceTime(4 * MINUTE); - await attempts[3].completed; + await assertAt(attempts, 3).completed; assert.strictEqual(runJob.callCount, 7); await advanceTime(8 * MINUTE); - await attempts[4].completed; + await assertAt(attempts, 4).completed; assert.strictEqual(runJob.callCount, 8); // Ensure it's been removed - assert.isUndefined(await DataReader._getAttachmentDownloadJob(jobs[0])); + assert.isUndefined( + await DataReader._getAttachmentDownloadJob(assertAt(jobs, 0)) + ); }); it('only selects backup_import jobs if the mediaDownload is not paused', async () => { @@ -528,21 +570,29 @@ describe('AttachmentDownloadManager', () => { // make one of the backup job messages visible to test that code path as well downloadManager?.updateVisibleTimelineMessages(['message-0', 'message-1']); await downloadManager?.start(); - await waitForJobToBeCompleted(jobs[3]); - assertRunJobCalledWith([jobs[1], jobs[5], jobs[3]]); + await waitForJobToBeCompleted(assertAt(jobs, 3)); + assertRunJobCalledWith([ + assertAt(jobs, 1), + assertAt(jobs, 5), + assertAt(jobs, 3), + ]); await advanceTime((downloadManager?.tickInterval ?? MINUTE) * 5); - assertRunJobCalledWith([jobs[1], jobs[5], jobs[3]]); + assertRunJobCalledWith([ + assertAt(jobs, 1), + assertAt(jobs, 5), + assertAt(jobs, 3), + ]); // resume backups await itemStorage.put('backupMediaDownloadPaused', false); await advanceTime((downloadManager?.tickInterval ?? MINUTE) * 5); assertRunJobCalledWith([ - jobs[1], - jobs[5], - jobs[3], - jobs[0], - jobs[4], - jobs[2], + assertAt(jobs, 1), + assertAt(jobs, 5), + assertAt(jobs, 3), + assertAt(jobs, 0), + assertAt(jobs, 4), + assertAt(jobs, 2), ]); }); @@ -551,7 +601,7 @@ describe('AttachmentDownloadManager', () => { const jobs = await addJobs(1, { source: AttachmentDownloadSource.BACKUP_IMPORT_WITH_MEDIA, }); - const jobAttempts = getPromisesForAttempts(jobs[0], 2); + const jobAttempts = getPromisesForAttempts(assertAt(jobs, 0), 2); runJob.callsFake(async () => { return new Promise<{ status: 'finished' | 'retry' }>(resolve => { @@ -562,22 +612,21 @@ describe('AttachmentDownloadManager', () => { }); await downloadManager?.start(); - await jobAttempts[0].completed; - assertRunJobCalledWith([jobs[0]]); + await assertAt(jobAttempts, 0).completed; + assertRunJobCalledWith([assertAt(jobs, 0)]); await DataWriter.resetBackupAttachmentDownloadJobsRetryAfter(); await downloadManager.start(); - await jobAttempts[1].completed; + await assertAt(jobAttempts, 1).completed; }); it('retries job with updated job if provided', async () => { strictAssert(downloadManager, 'must exist'); - const job = ( - await addJobs(1, { - source: AttachmentDownloadSource.BACKUP_IMPORT_WITH_MEDIA, - }) - )[0]; + const jobs = await addJobs(1, { + source: AttachmentDownloadSource.BACKUP_IMPORT_WITH_MEDIA, + }); + const job = assertAt(jobs, 0); const jobAttempts = getPromisesForAttempts(job, 3); runJob.callsFake(async args => { @@ -595,9 +644,9 @@ describe('AttachmentDownloadManager', () => { }); await downloadManager?.start(); - await jobAttempts[0].completed; + await assertAt(jobAttempts, 0).completed; assertRunJobCalledWith([job]); - await jobAttempts[1].completed; + await assertAt(jobAttempts, 1).completed; assert.deepStrictEqual( runJob.getCall(0).args[0].job.attachment, job.attachment @@ -651,42 +700,47 @@ describe('AttachmentDownloadManager', () => { }); it('will retry a job when aborted b/c of shutdown', async () => { const jobs = await addJobs(1); - const jobAttempts = getPromisesForAttempts(jobs[0], 2); + const jobAttempts = getPromisesForAttempts(assertAt(jobs, 0), 2); await downloadManager?.start(); - await jobAttempts[0].started; + await assertAt(jobAttempts, 0).started; await downloadStarted.promise; // Shutdown behavior downloadManager?.stop(); inflightRequestAbortController.abort(); - await jobAttempts[0].completed; + await assertAt(jobAttempts, 0).completed; // Ensure it will be retried assert.strictEqual( - (await DataReader._getAttachmentDownloadJob(jobs[0]))?.attempts, + (await DataReader._getAttachmentDownloadJob(assertAt(jobs, 0))) + ?.attempts, 1 ); assert.strictEqual(runJob.callCount, 1); }); it('will not retry a job if manually canceled', async () => { const jobs = await addJobs(1); - const jobAttempts = getPromisesForAttempts(jobs[0], 2); + const jobAttempts = getPromisesForAttempts(assertAt(jobs, 0), 2); await downloadManager?.start(); const downloadManagerIdled = downloadManager?.waitForIdle(); - await jobAttempts[0].started; + await assertAt(jobAttempts, 0).started; await downloadStarted.promise; // user-canceled behavior downloadManager?.cancelJobs(JobCancelReason.UserInitiated, () => true); - await assert.isRejected(jobAttempts[0].completed as Promise); + await assert.isRejected( + assertAt(jobAttempts, 0).completed as Promise + ); await downloadManagerIdled; // Ensure it will not be retried - assert.isUndefined(await DataReader._getAttachmentDownloadJob(jobs[0])); + assert.isUndefined( + await DataReader._getAttachmentDownloadJob(assertAt(jobs, 0)) + ); assert.strictEqual(runJob.callCount, 1); }); }); diff --git a/ts/test-electron/services/ReleaseNoteAndMegaphoneFetcher_test.preload.ts b/ts/test-electron/services/ReleaseNoteAndMegaphoneFetcher_test.preload.ts index dfe8326ac6..e749774e11 100644 --- a/ts/test-electron/services/ReleaseNoteAndMegaphoneFetcher_test.preload.ts +++ b/ts/test-electron/services/ReleaseNoteAndMegaphoneFetcher_test.preload.ts @@ -508,7 +508,7 @@ describe('ReleaseNoteAndMegaphoneFetcher', () => { const dbMegaphones = await getAllMegaphones(); const dbMegaphone = dbMegaphones[0]; assert.strictEqual(dbMegaphones.length, 1); - assert.strictEqual(dbMegaphone.id, myMegaphone.uuid); + assert.strictEqual(dbMegaphone?.id, myMegaphone.uuid); assert.strictEqual( dbMegaphone.dontShowBeforeEpochMs, myMegaphone.dontShowBeforeEpochSeconds * 1000 @@ -588,8 +588,8 @@ describe('ReleaseNoteAndMegaphoneFetcher', () => { const dbMegaphones = await getAllMegaphones(); assert.strictEqual(dbMegaphones.length, 2); - assert.strictEqual(dbMegaphones[0].id, megaphoneForMyCountry.uuid); - assert.strictEqual(dbMegaphones[1].id, wildcardMegaphone.uuid); + assert.strictEqual(dbMegaphones[0]?.id, megaphoneForMyCountry.uuid); + assert.strictEqual(dbMegaphones[1]?.id, wildcardMegaphone.uuid); }); }); @@ -616,7 +616,7 @@ describe('ReleaseNoteAndMegaphoneFetcher', () => { const dbMegaphones = await getAllMegaphones(); assert.strictEqual(dbMegaphones.length, 1); const dbMegaphone = dbMegaphones[0]; - assert.strictEqual(dbMegaphone.id, fakeMegaphoneUuid); + assert.strictEqual(dbMegaphone?.id, fakeMegaphoneUuid); }); it('deletes saved megaphones if the manifest has empty megaphones', async () => { diff --git a/ts/test-electron/sql/fullTextSearch_test.preload.ts b/ts/test-electron/sql/fullTextSearch_test.preload.ts index 016b042be4..da1faf4208 100644 --- a/ts/test-electron/sql/fullTextSearch_test.preload.ts +++ b/ts/test-electron/sql/fullTextSearch_test.preload.ts @@ -62,15 +62,15 @@ describe('sql/searchMessages', () => { const searchResults = await searchMessages({ query: 'unique' }); assert.lengthOf(searchResults, 1); - assert.strictEqual(searchResults[0].id, message2.id); + assert.strictEqual(searchResults[0]?.id, message2.id); message3.body = 'message 3 - unique string'; await saveMessage(message3, { ourAci, postSaveUpdates }); const searchResults2 = await searchMessages({ query: 'unique' }); assert.lengthOf(searchResults2, 2); - assert.strictEqual(searchResults2[0].id, message3.id); - assert.strictEqual(searchResults2[1].id, message2.id); + assert.strictEqual(searchResults2[0]?.id, message3.id); + assert.strictEqual(searchResults2[1]?.id, message2.id); }); it('excludes messages with isViewOnce = true', async () => { @@ -119,14 +119,14 @@ describe('sql/searchMessages', () => { const searchResults = await searchMessages({ query: 'unique' }); assert.lengthOf(searchResults, 1); - assert.strictEqual(searchResults[0].id, message1.id); + assert.strictEqual(searchResults[0]?.id, message1.id); message1.body = 'message 3 - unique string'; await saveMessage(message3, { ourAci, postSaveUpdates }); const searchResults2 = await searchMessages({ query: 'unique' }); assert.lengthOf(searchResults2, 1); - assert.strictEqual(searchResults2[0].id, message1.id); + assert.strictEqual(searchResults2[0]?.id, message1.id); }); it('excludes messages with storyId !== null', async () => { @@ -175,14 +175,14 @@ describe('sql/searchMessages', () => { const searchResults = await searchMessages({ query: 'unique' }); assert.lengthOf(searchResults, 1); - assert.strictEqual(searchResults[0].id, message1.id); + assert.strictEqual(searchResults[0]?.id, message1.id); message1.body = 'message 3 - unique string'; await saveMessage(message3, { ourAci, postSaveUpdates }); const searchResults2 = await searchMessages({ query: 'unique' }); assert.lengthOf(searchResults2, 1); - assert.strictEqual(searchResults2[0].id, message1.id); + assert.strictEqual(searchResults2[0]?.id, message1.id); }); it('limits messages returned to a specific conversation if specified', async () => { @@ -228,7 +228,7 @@ describe('sql/searchMessages', () => { conversationId: otherConversationId, }); assert.lengthOf(searchResultsWithConversationId, 1); - assert.strictEqual(searchResultsWithConversationId[0].id, message2.id); + assert.strictEqual(searchResultsWithConversationId[0]?.id, message2.id); }); }); @@ -262,7 +262,7 @@ describe('sql/searchMessages/withMentions', () => { } it('includes messages with mentions', async () => { - const mentionedAcis = [generateAci(), generateAci()]; + const mentionedAcis = [generateAci(), generateAci()] as const; const messages = await storeMessages([ { bodyRanges: [{ start: 0, length: 1, mentionAci: mentionedAcis[0] }], @@ -286,7 +286,7 @@ describe('sql/searchMessages/withMentions', () => { assert.sameOrderedMembers( searchResults.map(res => res.id), - [messages[0].id, messages[2].id] + [messages[0]?.id, messages[2]?.id] ); const searchResultsForMultipleMatchingUuids = await searchMessages({ @@ -297,12 +297,12 @@ describe('sql/searchMessages/withMentions', () => { assert.sameOrderedMembers( searchResultsForMultipleMatchingUuids.map(res => res.id), // TODO: should only return unique messages - [messages[0].id, messages[1].id, messages[2].id] + [messages[0]?.id, messages[1]?.id, messages[2]?.id] ); }); it('includes messages with mentions and those that match the body text', async () => { - const mentionedAcis = [generateAci(), generateAci()]; + const mentionedAcis = [generateAci(), generateAci()] as const; const messages = await storeMessages([ { body: 'cat', @@ -326,7 +326,7 @@ describe('sql/searchMessages/withMentions', () => { assert.sameOrderedMembers( searchResults.map(res => res.id), - [messages[0].id, messages[1].id] + [messages[0]?.id, messages[1]?.id] ); // check that results get returned in the right order, independent of whether they @@ -337,11 +337,11 @@ describe('sql/searchMessages/withMentions', () => { }); assert.sameOrderedMembers( searchResultsForDog.map(res => res.id), - [messages[1].id, messages[2].id] + [messages[1]?.id, messages[2]?.id] ); }); it('respects conversationId for mention matches', async () => { - const mentionedAcis = [generateAci(), generateAci()]; + const mentionedAcis = [generateAci(), generateAci()] as const; const conversationId = generateUuid(); const messages = await storeMessages([ { @@ -370,7 +370,7 @@ describe('sql/searchMessages/withMentions', () => { assert.sameOrderedMembers( searchResults.map(res => res.id), - [messages[0].id, messages[1].id] + [messages[0]?.id, messages[1]?.id] ); const searchResultsWithoutConversationid = await searchMessages({ @@ -380,7 +380,7 @@ describe('sql/searchMessages/withMentions', () => { assert.sameOrderedMembers( searchResultsWithoutConversationid.map(res => res.id), - [messages[0].id, messages[1].id, messages[2].id, messages[3].id] + [messages[0]?.id, messages[1]?.id, messages[2]?.id, messages[3]?.id] ); }); }); diff --git a/ts/test-electron/sql/getRecentStoryReplies_test.preload.ts b/ts/test-electron/sql/getRecentStoryReplies_test.preload.ts index f0a0a5596f..79b0eb4067 100644 --- a/ts/test-electron/sql/getRecentStoryReplies_test.preload.ts +++ b/ts/test-electron/sql/getRecentStoryReplies_test.preload.ts @@ -102,8 +102,8 @@ describe('sql/getRecentStoryReplies', () => { limit: 2, }); assert.lengthOf(searchResultsPage1, 2, 'page 1'); - assert.strictEqual(searchResultsPage1[0].body, message3.body); - assert.strictEqual(searchResultsPage1[1].body, message2.body); + assert.strictEqual(searchResultsPage1[0]?.body, message3.body); + assert.strictEqual(searchResultsPage1[1]?.body, message2.body); const searchResultsPage2 = await getRecentStoryReplies(storyId, { messageId: message2.id, @@ -111,6 +111,6 @@ describe('sql/getRecentStoryReplies', () => { limit: 2, }); assert.lengthOf(searchResultsPage2, 1, 'page 2'); - assert.strictEqual(searchResultsPage2[0].body, message1.body); + assert.strictEqual(searchResultsPage2[0]?.body, message1.body); }); }); diff --git a/ts/test-electron/sql/markRead_test.preload.ts b/ts/test-electron/sql/markRead_test.preload.ts index ab7df8b0b8..973d5e234f 100644 --- a/ts/test-electron/sql/markRead_test.preload.ts +++ b/ts/test-electron/sql/markRead_test.preload.ts @@ -160,12 +160,12 @@ describe('sql/markRead', () => { // Sorted in descending order assert.strictEqual( - markedRead[0].id, + markedRead[0]?.id, unread.id, 'no stories/first should be "unread" message' ); assert.strictEqual( - markedRead[1].id, + markedRead[1]?.id, oldestUnread.id, 'no stories/second should be oldestUnread' ); @@ -180,12 +180,12 @@ describe('sql/markRead', () => { assert.lengthOf(markedRead2, 2, 'with stories/two messages marked read'); assert.strictEqual( - markedRead2[0].id, + markedRead2[0]?.id, newestUnread.id, 'with stories/should be newestUnread' ); assert.strictEqual( - markedRead2[1].id, + markedRead2[1]?.id, unreadStoryReply.id, 'with stories/should be unreadStoryReply' ); @@ -310,17 +310,17 @@ describe('sql/markRead', () => { // Sorted in descending order assert.strictEqual( - markedRead[0].id, + markedRead[0]?.id, message7.id, 'first should be message7' ); assert.strictEqual( - markedRead[1].id, + markedRead[1]?.id, message4.id, 'first should be message4' ); assert.strictEqual( - markedRead[2].id, + markedRead[2]?.id, message2.id, 'second should be message2' ); @@ -418,7 +418,7 @@ describe('sql/markRead', () => { assert.lengthOf(markedRead, 1, 'one message marked read'); assert.strictEqual( - markedRead[0].id, + markedRead[0]?.id, message4.id, 'first should be message4' ); @@ -436,21 +436,21 @@ describe('sql/markRead', () => { (left, right) => left.timestamp - right.timestamp ); - assert.strictEqual(sorted[0].id, message1.id, 'checking message 1'); + assert.strictEqual(sorted[0]?.id, message1.id, 'checking message 1'); assert.strictEqual( sorted[0].expirationStartTimestamp, now, "message1's expirationStartTimestamp was moved earlier" ); - assert.strictEqual(sorted[1].id, message2.id, 'checking message 2'); + assert.strictEqual(sorted[1]?.id, message2.id, 'checking message 2'); assert.strictEqual( sorted[1].expirationStartTimestamp, now, 'checking message 2 expirationStartTimestamp' ); - assert.strictEqual(sorted[3].id, message4.id, 'checking message 4'); + assert.strictEqual(sorted[3]?.id, message4.id, 'checking message 4'); assert.strictEqual( sorted[3].expirationStartTimestamp, now, @@ -610,7 +610,7 @@ describe('sql/markRead', () => { assert.lengthOf(markedRead2, 1); assert.strictEqual( - markedRead2[0].messageId, + markedRead2[0]?.messageId, reaction5.messageId, 'should be reaction5' ); @@ -760,7 +760,7 @@ describe('sql/markRead', () => { assert.lengthOf(markedRead2, 1); assert.strictEqual( - markedRead2[0].messageId, + markedRead2[0]?.messageId, reaction5.messageId, 'should be reaction5' ); @@ -837,7 +837,7 @@ describe('sql/markRead', () => { // Sorted in descending order assert.strictEqual( - markedRead[0].id, + markedRead[0]?.id, message3.id, 'first should be message3' ); diff --git a/ts/test-electron/sql/notificationProfiles_test.preload.ts b/ts/test-electron/sql/notificationProfiles_test.preload.ts index c2cff10356..decebb4d85 100644 --- a/ts/test-electron/sql/notificationProfiles_test.preload.ts +++ b/ts/test-electron/sql/notificationProfiles_test.preload.ts @@ -170,7 +170,7 @@ describe('sql/notificationProfiles', () => { const twoProfiles = await getAllNotificationProfiles(); assert.lengthOf(twoProfiles, 2); - assert.strictEqual(twoProfiles[1].deletedAtTimestampMs, timestamp); + assert.strictEqual(twoProfiles[1]?.deletedAtTimestampMs, timestamp); }); it('can update a profile', async () => { diff --git a/ts/test-electron/sql/pollVoteMarkRead_test.preload.ts b/ts/test-electron/sql/pollVoteMarkRead_test.preload.ts index 3195b47531..38319c797c 100644 --- a/ts/test-electron/sql/pollVoteMarkRead_test.preload.ts +++ b/ts/test-electron/sql/pollVoteMarkRead_test.preload.ts @@ -110,7 +110,7 @@ describe('sql/pollVoteMarkRead', () => { }); assert.lengthOf(markedRead2, 1, 'only one poll vote remains unread'); - assert.strictEqual(markedRead2[0].id, pollMessage3.id); + assert.strictEqual(markedRead2[0]?.id, pollMessage3.id); }); it('respects received_at cutoff', async () => { @@ -167,7 +167,7 @@ describe('sql/pollVoteMarkRead', () => { }); assert.lengthOf(markedRead, 1, 'only one poll vote within cutoff'); - assert.strictEqual(markedRead[0].id, pollMessage1.id); + assert.strictEqual(markedRead[0]?.id, pollMessage1.id); }); it('filters by conversationId correctly', async () => { @@ -224,7 +224,7 @@ describe('sql/pollVoteMarkRead', () => { }); assert.lengthOf(markedRead, 1, 'only polls from conversationId1'); - assert.strictEqual(markedRead[0].id, pollMessage1.id); + assert.strictEqual(markedRead[0]?.id, pollMessage1.id); }); it('only returns messages with hasUnreadPollVotes = true', async () => { @@ -280,7 +280,7 @@ describe('sql/pollVoteMarkRead', () => { }); assert.lengthOf(markedRead, 1, 'only unread poll votes'); - assert.strictEqual(markedRead[0].id, pollMessage1.id); + assert.strictEqual(markedRead[0]?.id, pollMessage1.id); }); it('marks multiple poll votes as read in single call', async () => { @@ -536,7 +536,7 @@ describe('sql/pollVoteMarkRead', () => { }); assert.lengthOf(markedRead, 1, 'second poll still unread'); - assert.strictEqual(markedRead[0].id, pollMessage2.id); + assert.strictEqual(markedRead[0]?.id, pollMessage2.id); }); it('returns full MessageAttributesType on success', async () => { @@ -626,7 +626,7 @@ describe('sql/pollVoteMarkRead', () => { const result = await markPollVoteAsRead(start + 2000); assert.isNotNull(result); - assert.strictEqual(result?.id, pollMessages[2].id); + assert.strictEqual(result?.id, pollMessages[2]?.id); // Other polls should still be unread const markedRead = await getUnreadPollVotesAndMarkRead({ diff --git a/ts/test-electron/sql/sendLog_test.preload.ts b/ts/test-electron/sql/sendLog_test.preload.ts index 650ed8432b..25a0064d2a 100644 --- a/ts/test-electron/sql/sendLog_test.preload.ts +++ b/ts/test-electron/sql/sendLog_test.preload.ts @@ -56,7 +56,7 @@ describe('sql/sendLog', () => { assert.lengthOf(allProtos, 1); const actual = allProtos[0]; - assert.strictEqual(actual.contentHint, proto.contentHint); + assert.strictEqual(actual?.contentHint, proto.contentHint); assert.isTrue(constantTimeEqual(actual.proto, proto.proto)); assert.strictEqual(actual.timestamp, proto.timestamp); assert.strictEqual(actual.urgent, proto.urgent); @@ -96,7 +96,7 @@ describe('sql/sendLog', () => { assert.lengthOf(allProtos, 1); const actual = allProtos[0]; - assert.strictEqual(actual.contentHint, proto.contentHint); + assert.strictEqual(actual?.contentHint, proto.contentHint); assert.isTrue(constantTimeEqual(actual.proto, proto.proto)); assert.strictEqual(actual.timestamp, proto.timestamp); assert.strictEqual(actual.urgent, proto.urgent); @@ -153,7 +153,7 @@ describe('sql/sendLog', () => { assert.lengthOf(allProtos, 1); const actual = allProtos[0]; - assert.strictEqual(actual.timestamp, proto.timestamp); + assert.strictEqual(actual?.timestamp, proto.timestamp); await removeMessageById(id, { cleanupMessages }); @@ -294,12 +294,12 @@ describe('sql/sendLog', () => { assert.lengthOf(allProtos, 2); const actual1 = allProtos[0]; - assert.strictEqual(actual1.contentHint, proto1.contentHint); + assert.strictEqual(actual1?.contentHint, proto1.contentHint); assert.isTrue(constantTimeEqual(actual1.proto, proto1.proto)); assert.strictEqual(actual1.timestamp, proto1.timestamp); const actual2 = allProtos[1]; - assert.strictEqual(actual2.contentHint, proto2.contentHint); + assert.strictEqual(actual2?.contentHint, proto2.contentHint); assert.isTrue(constantTimeEqual(actual2.proto, proto2.proto)); assert.strictEqual(actual2.timestamp, proto2.timestamp); }); diff --git a/ts/test-electron/sql/stories_test.preload.ts b/ts/test-electron/sql/stories_test.preload.ts index f0704c35e7..32605a27fb 100644 --- a/ts/test-electron/sql/stories_test.preload.ts +++ b/ts/test-electron/sql/stories_test.preload.ts @@ -91,12 +91,12 @@ describe('sql/stories', () => { // They are in ASC order assert.strictEqual( - stories[0].id, + stories[0]?.id, story1.id, 'stories first should be story5' ); assert.strictEqual( - stories[3].id, + stories[3]?.id, story5.id, 'stories last should be story1' ); @@ -112,12 +112,12 @@ describe('sql/stories', () => { // They are in ASC order assert.strictEqual( - storiesInConversation[0].id, + storiesInConversation[0]?.id, story1.id, 'storiesInConversation first should be story4' ); assert.strictEqual( - storiesInConversation[1].id, + storiesInConversation[1]?.id, story4.id, 'storiesInConversation last should be story1' ); @@ -129,12 +129,12 @@ describe('sql/stories', () => { // They are in ASC order assert.strictEqual( - storiesByAuthor[0].id, + storiesByAuthor[0]?.id, story2.id, 'storiesByAuthor first should be story5' ); assert.strictEqual( - storiesByAuthor[1].id, + storiesByAuthor[1]?.id, story5.id, 'storiesByAuthor last should be story2' ); @@ -230,12 +230,12 @@ describe('sql/stories', () => { // They are in ASC order assert.strictEqual( - stories[0].id, + stories[0]?.id, story1.id, 'stories first should be story1' ); assert.strictEqual( - stories[2].id, + stories[2]?.id, story3.id, 'stories last should be story3' ); @@ -243,7 +243,7 @@ describe('sql/stories', () => { assert.strictEqual(stories[0].hasReplies, true); assert.strictEqual(stories[0].hasRepliesFromSelf, true); - assert.strictEqual(stories[1].hasReplies, true); + assert.strictEqual(stories[1]?.hasReplies, true); assert.strictEqual(stories[1].hasRepliesFromSelf, false); assert.strictEqual(stories[2].hasReplies, false); diff --git a/ts/test-electron/sql/storyDistribution_test.preload.ts b/ts/test-electron/sql/storyDistribution_test.preload.ts index 6f883f6ddf..4d36c482fa 100644 --- a/ts/test-electron/sql/storyDistribution_test.preload.ts +++ b/ts/test-electron/sql/storyDistribution_test.preload.ts @@ -233,6 +233,9 @@ describe('sql/storyDistribution', () => { const allHydratedLists = await getAllStoryDistributionsWithMembers(); assert.lengthOf(allHydratedLists, 1); - assert.deepEqual(allHydratedLists[0].members, [SERVICE_ID_1, SERVICE_ID_2]); + assert.deepEqual(allHydratedLists[0]?.members, [ + SERVICE_ID_1, + SERVICE_ID_2, + ]); }); }); diff --git a/ts/test-electron/sql/timelineFetches_test.preload.ts b/ts/test-electron/sql/timelineFetches_test.preload.ts index 9c49a914ed..7b39d4b66e 100644 --- a/ts/test-electron/sql/timelineFetches_test.preload.ts +++ b/ts/test-electron/sql/timelineFetches_test.preload.ts @@ -101,8 +101,8 @@ describe('sql/timelineFetches', () => { assert.lengthOf(messages, 3); // Fetched with DESC query, but with reverse() call afterwards - assert.strictEqual(messages[0].id, message1.id); - assert.strictEqual(messages[1].id, message2.id); + assert.strictEqual(messages[0]?.id, message1.id); + assert.strictEqual(messages[1]?.id, message2.id); }); it('returns N most recent messages for a given story', async () => { @@ -158,7 +158,7 @@ describe('sql/timelineFetches', () => { storyId, }); assert.lengthOf(messages, 1); - assert.strictEqual(messages[0].id, message2.id); + assert.strictEqual(messages[0]?.id, message2.id); }); it('returns N most recent messages excluding group story replies', async () => { @@ -214,7 +214,7 @@ describe('sql/timelineFetches', () => { storyId: undefined, }); assert.lengthOf(messages, 1); - assert.strictEqual(messages[0].id, message3.id); + assert.strictEqual(messages[0]?.id, message3.id); }); it('returns N messages older than provided received_at', async () => { @@ -269,7 +269,7 @@ describe('sql/timelineFetches', () => { storyId: undefined, }); assert.lengthOf(messages, 1); - assert.strictEqual(messages[0].id, message1.id); + assert.strictEqual(messages[0]?.id, message1.id); }); it('returns N older messages with received_at, lesser sent_at', async () => { @@ -327,8 +327,8 @@ describe('sql/timelineFetches', () => { assert.lengthOf(messages, 2); // Fetched with DESC query, but with reverse() call afterwards - assert.strictEqual(messages[0].id, message1.id, 'checking message 1'); - assert.strictEqual(messages[1].id, message2.id, 'checking message 2'); + assert.strictEqual(messages[0]?.id, message1.id, 'checking message 1'); + assert.strictEqual(messages[1]?.id, message2.id, 'checking message 2'); }); it('returns N older messages, same received_at/sent_at but excludes messageId', async () => { @@ -385,7 +385,7 @@ describe('sql/timelineFetches', () => { }); assert.lengthOf(messages, 1); - assert.strictEqual(messages[0].id, message1.id); + assert.strictEqual(messages[0]?.id, message1.id); }); }); @@ -462,8 +462,8 @@ describe('sql/timelineFetches', () => { }); assert.lengthOf(messages, 3); - assert.strictEqual(messages[0].id, message3.id, 'checking message 3'); - assert.strictEqual(messages[1].id, message4.id, 'checking message 4'); + assert.strictEqual(messages[0]?.id, message3.id, 'checking message 3'); + assert.strictEqual(messages[1]?.id, message4.id, 'checking message 4'); }); it('returns N oldest messages for a given story with no parameters', async () => { @@ -520,7 +520,7 @@ describe('sql/timelineFetches', () => { }); assert.lengthOf(messages, 1); - assert.strictEqual(messages[0].id, message2.id); + assert.strictEqual(messages[0]?.id, message2.id); }); it('returns N messages newer than provided received_at', async () => { @@ -575,7 +575,7 @@ describe('sql/timelineFetches', () => { storyId: undefined, }); assert.lengthOf(messages, 1); - assert.strictEqual(messages[0].id, message3.id); + assert.strictEqual(messages[0]?.id, message3.id); }); it('returns N messages excluding group story replies', async () => { @@ -632,7 +632,7 @@ describe('sql/timelineFetches', () => { sentAt: target, }); assert.lengthOf(messages, 1); - assert.strictEqual(messages[0].id, message2.id); + assert.strictEqual(messages[0]?.id, message2.id); }); it('returns N newer messages with same received_at, greater sent_at', async () => { @@ -689,8 +689,8 @@ describe('sql/timelineFetches', () => { assert.lengthOf(messages, 2); // They are not in DESC order because MessageCollection is sorting them - assert.strictEqual(messages[0].id, message2.id); - assert.strictEqual(messages[1].id, message3.id); + assert.strictEqual(messages[0]?.id, message2.id); + assert.strictEqual(messages[1]?.id, message3.id); }); }); diff --git a/ts/test-electron/state/ducks/calling_test.preload.ts b/ts/test-electron/state/ducks/calling_test.preload.ts index 327979a4f1..d01c71d954 100644 --- a/ts/test-electron/state/ducks/calling_test.preload.ts +++ b/ts/test-electron/state/ducks/calling_test.preload.ts @@ -117,7 +117,7 @@ describe('calling duck', () => { const remoteAci = generateAci(); const ringerAci = generateAci(); - const stateWithGroupCall: CallingStateType = { + const stateWithGroupCall = { ...getEmptyState(), callsByConversation: { 'fake-group-call-conversation-id': { @@ -148,9 +148,9 @@ describe('calling duck', () => { ], } satisfies GroupCallStateType, }, - }; + } as const satisfies CallingStateType; - const stateWithNotJoinedGroupCall: CallingStateType = { + const stateWithNotJoinedGroupCall = { ...getEmptyState(), callsByConversation: { 'fake-group-call-conversation-id': { @@ -170,9 +170,9 @@ describe('calling duck', () => { remoteParticipants: [], } satisfies GroupCallStateType, }, - }; + } as const satisfies CallingStateType; - const stateWithIncomingGroupCall: CallingStateType = { + const stateWithIncomingGroupCall = { ...stateWithGroupCall, callsByConversation: { ...stateWithGroupCall.callsByConversation, @@ -184,7 +184,7 @@ describe('calling duck', () => { ringerAci: generateAci(), }, }, - }; + } as const satisfies CallingStateType; const groupCallActiveCallState: ActiveCallStateType = { state: 'Active', @@ -202,10 +202,10 @@ describe('calling duck', () => { joinedAt: null, }; - const stateWithActiveGroupCall: CallingStateTypeWithActiveCall = { + const stateWithActiveGroupCall = { ...stateWithGroupCall, activeCallState: groupCallActiveCallState, - }; + } as const satisfies CallingStateTypeWithActiveCall; const ourAci = generateAci(); diff --git a/ts/test-electron/state/ducks/conversations_test.preload.ts b/ts/test-electron/state/ducks/conversations_test.preload.ts index f321ae8983..a8b03af357 100644 --- a/ts/test-electron/state/ducks/conversations_test.preload.ts +++ b/ts/test-electron/state/ducks/conversations_test.preload.ts @@ -2217,19 +2217,21 @@ describe('both/state/ducks/conversations', () => { const nextState = reducer(getState().conversations, action); sinon.assert.calledOnce(dispatch); - assert.isUndefined(nextState.conversationLookup.abc.conversationColor); - assert.isUndefined(nextState.conversationLookup.def.conversationColor); - assert.isUndefined(nextState.conversationLookup.ghi.conversationColor); - assert.isUndefined(nextState.conversationLookup.jkl.conversationColor); + assert.isUndefined(nextState.conversationLookup.abc?.conversationColor); + assert.isUndefined(nextState.conversationLookup.def?.conversationColor); + assert.isUndefined(nextState.conversationLookup.ghi?.conversationColor); + assert.isUndefined(nextState.conversationLookup.jkl?.conversationColor); assert.isUndefined( - nextState.conversationsByServiceId[abc.serviceId].conversationColor + nextState.conversationsByServiceId[abc.serviceId]?.conversationColor ); assert.isUndefined( - nextState.conversationsByServiceId[def.serviceId].conversationColor + nextState.conversationsByServiceId[def.serviceId]?.conversationColor ); - assert.isUndefined(nextState.conversationsByE164.ghi.conversationColor); assert.isUndefined( - nextState.conversationsByGroupId.jkl.conversationColor + nextState.conversationsByE164.ghi?.conversationColor + ); + assert.isUndefined( + nextState.conversationsByGroupId.jkl?.conversationColor ); await itemStorage.remove('defaultConversationColor'); }); diff --git a/ts/test-electron/textsecure/generate_keys_test.preload.ts b/ts/test-electron/textsecure/generate_keys_test.preload.ts index 61507ac8f2..415ef17ff8 100644 --- a/ts/test-electron/textsecure/generate_keys_test.preload.ts +++ b/ts/test-electron/textsecure/generate_keys_test.preload.ts @@ -116,7 +116,7 @@ describe('Key generation', function (this: Mocha.Suite) { it('result contains the correct keyIds', () => { const preKeys = result.preKeys || []; for (let i = 0; i < count; i += 1) { - assert.strictEqual(preKeys[i].keyId, i + 1); + assert.strictEqual(preKeys[i]?.keyId, i + 1); } }); it('result contains the correct public keys', async () => { @@ -153,7 +153,7 @@ describe('Key generation', function (this: Mocha.Suite) { it('result contains the correct keyIds', () => { const preKeys = result.preKeys || []; for (let i = 1; i <= count; i += 1) { - assert.strictEqual(preKeys[i - 1].keyId, i + count); + assert.strictEqual(preKeys[i - 1]?.keyId, i + count); } }); it('result contains the correct public keys', async () => { @@ -194,7 +194,7 @@ describe('Key generation', function (this: Mocha.Suite) { it('result contains the correct keyIds', () => { const preKeys = result.preKeys || []; for (let i = 1; i <= count; i += 1) { - assert.strictEqual(preKeys[i - 1].keyId, i + 2 * count); + assert.strictEqual(preKeys[i - 1]?.keyId, i + 2 * count); } }); it('result contains the correct public keys', async () => { diff --git a/ts/test-electron/util/addAttachmentToMessage_test.preload.ts b/ts/test-electron/util/addAttachmentToMessage_test.preload.ts index e7eff9aea3..bb293bda3e 100644 --- a/ts/test-electron/util/addAttachmentToMessage_test.preload.ts +++ b/ts/test-electron/util/addAttachmentToMessage_test.preload.ts @@ -147,7 +147,7 @@ describe('addAttachmentToMessage', () => { ); const message = await getMessageById(messageId); - assert.deepStrictEqual(message?.attributes.preview?.[0].image, { + assert.deepStrictEqual(message?.attributes.preview?.[0]?.image, { ...attachment, path: '/path/to/attachment', }); @@ -181,7 +181,7 @@ describe('addAttachmentToMessage', () => { const message = await getMessageById(messageId); assert.deepStrictEqual( - message?.attributes.editHistory?.[0].preview?.[0].image, + message?.attributes.editHistory?.[0]?.preview?.[0]?.image, { ...attachment, path: '/path/to/attachment', @@ -215,7 +215,7 @@ describe('addAttachmentToMessage', () => { const message = await getMessageById(messageId); assert.deepStrictEqual( - message?.attributes.quote?.attachments[0].thumbnail, + message?.attributes.quote?.attachments[0]?.thumbnail, { ...attachment, path: '/path/to/attachment', @@ -258,7 +258,7 @@ describe('addAttachmentToMessage', () => { const message = await getMessageById(messageId); assert.deepStrictEqual( - message?.attributes.editHistory?.[0].quote?.attachments[0].thumbnail, + message?.attributes.editHistory?.[0]?.quote?.attachments[0]?.thumbnail, { ...attachment, path: '/path/to/attachment', @@ -394,11 +394,11 @@ describe('addAttachmentToMessage', () => { const message = await getMessageById(messageId); assert.deepStrictEqual( - message?.attributes.editHistory?.[0].body, + message?.attributes.editHistory?.[0]?.body, 'attachmenttext' ); assert.deepStrictEqual( - message?.attributes.editHistory?.[0].bodyAttachment, + message?.attributes.editHistory?.[0]?.bodyAttachment, { ...attachment, path: 'bodyAttachmentPath', diff --git a/ts/test-electron/util/migrateMessageData_test.preload.ts b/ts/test-electron/util/migrateMessageData_test.preload.ts index 4524cf8b31..91609d4bc3 100644 --- a/ts/test-electron/util/migrateMessageData_test.preload.ts +++ b/ts/test-electron/util/migrateMessageData_test.preload.ts @@ -34,9 +34,13 @@ describe('utils/migrateMessageData', async () => { await itemStorage.fetch(); }); it('increments attempts for messages which fail to save', async () => { - const messages = new Array(5) - .fill(null) - .map((_, idx) => composeMessage(idx + 1)); + const messages = [ + composeMessage(1), + composeMessage(2), + composeMessage(3), + composeMessage(4), + composeMessage(5), + ] as const; const CANNOT_UPGRADE_MESSAGE_ID = messages[1].id; const CANNOT_SAVE_MESSAGE_ID = messages[2].id; diff --git a/ts/test-helpers/expireTimers.std.ts b/ts/test-helpers/expireTimers.std.ts index 8668216bd3..439537384b 100644 --- a/ts/test-helpers/expireTimers.std.ts +++ b/ts/test-helpers/expireTimers.std.ts @@ -4,20 +4,17 @@ import * as durations from '../util/durations/index.std.js'; import { DurationInSeconds } from '../util/durations/index.std.js'; +const { fromMillis } = DurationInSeconds; + export type TestExpireTimer = Readonly<{ value: DurationInSeconds; label: string; }>; -export const EXPIRE_TIMERS: ReadonlyArray = [ - { value: 42 * durations.SECOND, label: '42 seconds' }, - { value: 5 * durations.MINUTE, label: '5 minutes' }, - { value: 1 * durations.HOUR, label: '1 hour' }, - { value: 6 * durations.DAY, label: '6 days' }, - { value: 3 * durations.WEEK, label: '3 weeks' }, -].map(({ value, label }) => { - return { - value: DurationInSeconds.fromMillis(value), - label, - }; -}); +export const EXPIRE_TIMERS = [ + { value: fromMillis(42 * durations.SECOND), label: '42 seconds' }, + { value: fromMillis(5 * durations.MINUTE), label: '5 minutes' }, + { value: fromMillis(1 * durations.HOUR), label: '1 hour' }, + { value: fromMillis(6 * durations.DAY), label: '6 days' }, + { value: fromMillis(3 * durations.WEEK), label: '3 weeks' }, +] as const satisfies ReadonlyArray; diff --git a/ts/test-helpers/generateBackup.node.ts b/ts/test-helpers/generateBackup.node.ts index 526d7c4fa5..2a91a8a632 100644 --- a/ts/test-helpers/generateBackup.node.ts +++ b/ts/test-helpers/generateBackup.node.ts @@ -253,7 +253,8 @@ function* createRecords({ } for (let i = 0; i < messages; i += 1) { - const chat = chats[i % chats.length]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const chat = chats[i % chats.length]!; const isIncoming = i % 2 === 0; diff --git a/ts/test-helpers/getRandomColor.std.ts b/ts/test-helpers/getRandomColor.std.ts index f4913309bb..5cdf33a500 100644 --- a/ts/test-helpers/getRandomColor.std.ts +++ b/ts/test-helpers/getRandomColor.std.ts @@ -8,5 +8,6 @@ import { AvatarColors } from '../types/Colors.std.js'; const { sample } = lodash; export function getRandomColor(): AvatarColorType { - return sample(AvatarColors) || AvatarColors[0]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return sample(AvatarColors) || AvatarColors[0]!; } diff --git a/ts/test-mock/backups/backups_test.node.ts b/ts/test-mock/backups/backups_test.node.ts index d90c6e396d..763544f63d 100644 --- a/ts/test-mock/backups/backups_test.node.ts +++ b/ts/test-mock/backups/backups_test.node.ts @@ -6,6 +6,7 @@ import { randomBytes } from 'node:crypto'; import { join } from 'node:path'; import os from 'node:os'; import createDebug from 'debug'; +import type { PrimaryDevice } from '@signalapp/mock-server'; import { Proto, StorageState } from '@signalapp/mock-server'; import { assert } from 'chai'; import { expect } from 'playwright/test'; @@ -74,7 +75,7 @@ describe('backups', function (this: Mocha.Suite) { let state = StorageState.getEmpty(); const { phone, contacts } = bootstrap; - const [friend, pinned] = contacts; + const [friend, pinned] = contacts as [PrimaryDevice, PrimaryDevice]; state = state.updateAccount({ profileKey: phone.profileKey.serialize(), @@ -289,7 +290,7 @@ describe('backups', function (this: Mocha.Suite) { .waitFor(); const [catMessage] = await app.getMessagesBySentAt(catTimestamp); - const [image] = catMessage.attachments ?? []; + const [image] = catMessage?.attachments ?? []; strictAssert(image.plaintextHash, 'plaintextHash was calculated'); strictAssert(image.digest, 'digest was calculated at download time'); strictAssert( @@ -395,7 +396,7 @@ describe('backups', function (this: Mocha.Suite) { { const [catMessage] = await app.getMessagesBySentAt(catTimestamp); - const [image] = catMessage.attachments ?? []; + const [image] = catMessage?.attachments ?? []; if (!bootstrapLinkParams.localBackup) { strictAssert( image.digest, diff --git a/ts/test-mock/benchmarks/call_history_search_bench.node.ts b/ts/test-mock/benchmarks/call_history_search_bench.node.ts index 0f9b091458..cfd63129c3 100644 --- a/ts/test-mock/benchmarks/call_history_search_bench.node.ts +++ b/ts/test-mock/benchmarks/call_history_search_bench.node.ts @@ -166,7 +166,8 @@ Bootstrap.benchmark(async (bootstrap: Bootstrap): Promise => { async function measure(runId: number): Promise { // setup - const searchContact = contacts[runId % contacts.length]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const searchContact = contacts[runId % contacts.length]!; const OtherCallListItems = CallListItem.filter({ hasNotText: searchContact.profileName, }); diff --git a/ts/test-mock/benchmarks/convo_open_bench.node.ts b/ts/test-mock/benchmarks/convo_open_bench.node.ts index 1af2cd57c0..3092d96902 100644 --- a/ts/test-mock/benchmarks/convo_open_bench.node.ts +++ b/ts/test-mock/benchmarks/convo_open_bench.node.ts @@ -17,7 +17,7 @@ Bootstrap.benchmark(async (bootstrap: Bootstrap): Promise => { const app = await bootstrap.link(); const { server, contacts, phone, desktop } = bootstrap; - const [first, second] = contacts; + const [first, second] = contacts as [PrimaryDevice, PrimaryDevice]; const messages = new Array(); debug('encrypting'); diff --git a/ts/test-mock/benchmarks/group_send_bench.node.ts b/ts/test-mock/benchmarks/group_send_bench.node.ts index 0de6310ae0..afe92afeb2 100644 --- a/ts/test-mock/benchmarks/group_send_bench.node.ts +++ b/ts/test-mock/benchmarks/group_send_bench.node.ts @@ -4,6 +4,7 @@ import assert from 'node:assert'; +import type { PrimaryDevice } from '@signalapp/mock-server'; import { StorageState, EnvelopeType, @@ -46,7 +47,7 @@ Bootstrap.benchmark(async (bootstrap: Bootstrap): Promise => { const app = await bootstrap.link(); const { server, desktop } = bootstrap; - const [first] = members; + const [first] = members as [PrimaryDevice]; const messages = new Array(); debug('encrypting'); @@ -94,7 +95,8 @@ Bootstrap.benchmark(async (bootstrap: Bootstrap): Promise => { // Fill group for (let i = 0; i < CONVERSATION_SIZE; i += 1) { - const contact = unblockedMembers[i % unblockedMembers.length]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const contact = unblockedMembers[i % unblockedMembers.length]!; const messageTimestamp = bootstrap.getTimestamp(); const isLast = i === CONVERSATION_SIZE - 1; diff --git a/ts/test-mock/benchmarks/send_bench.node.ts b/ts/test-mock/benchmarks/send_bench.node.ts index 567212659b..c94f78b4b7 100644 --- a/ts/test-mock/benchmarks/send_bench.node.ts +++ b/ts/test-mock/benchmarks/send_bench.node.ts @@ -4,6 +4,7 @@ import assert from 'node:assert'; +import type { PrimaryDevice } from '@signalapp/mock-server'; import { ReceiptType } from '@signalapp/mock-server'; import { Bootstrap, debug, RUN_COUNT, DISCARD_COUNT } from './fixtures.node.js'; @@ -19,7 +20,7 @@ Bootstrap.benchmark(async (bootstrap: Bootstrap): Promise => { const { server, contacts, phone, desktop } = bootstrap; - const [first] = contacts; + const [first] = contacts as [PrimaryDevice]; const messages = new Array(); debug('encrypting'); diff --git a/ts/test-mock/benchmarks/startup_bench.node.ts b/ts/test-mock/benchmarks/startup_bench.node.ts index fb96a0fe2b..aca8bbd025 100644 --- a/ts/test-mock/benchmarks/startup_bench.node.ts +++ b/ts/test-mock/benchmarks/startup_bench.node.ts @@ -25,7 +25,8 @@ Bootstrap.regressionBenchmark( debug('started generating messages'); for (let i = 0; i < messageCount; i += 1) { - const contact = contacts[Math.floor(i / 2) % contacts.length]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const contact = contacts[Math.floor(i / 2) % contacts.length]!; const direction = i % 2 ? 'message' : 'reply'; const messageTimestamp = bootstrap.getTimestamp(); diff --git a/ts/test-mock/bootstrap.node.ts b/ts/test-mock/bootstrap.node.ts index cb89c967a0..5cce73f693 100644 --- a/ts/test-mock/bootstrap.node.ts +++ b/ts/test-mock/bootstrap.node.ts @@ -888,7 +888,8 @@ export class Bootstrap { } const result: Record = Object.create(null); - const keys = Object.keys(samples[0].data).filter( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const keys = Object.keys(samples[0]!.data).filter( (key: string): key is `${string}Duration` => key.endsWith('Duration') ); const human = new Array(); @@ -896,7 +897,8 @@ export class Bootstrap { let worstError = 0; for (const key of keys) { const { yIntercept, slope, confidence, outliers, severeOutliers } = - regress(samples.map(s => ({ y: s.value, x: s.data[key] }))); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + regress(samples.map(s => ({ y: s.value, x: s.data[key]! }))); const delay = -yIntercept / slope; const perSecond = slope * SECOND; diff --git a/ts/test-mock/messaging/attachments_test.node.ts b/ts/test-mock/messaging/attachments_test.node.ts index 193bbee22d..af9232eba1 100644 --- a/ts/test-mock/messaging/attachments_test.node.ts +++ b/ts/test-mock/messaging/attachments_test.node.ts @@ -60,7 +60,7 @@ describe('attachments', function (this: Mocha.Suite) { let state = StorageState.getEmpty(); const { phone, contacts } = bootstrap; - [pinned] = contacts; + [pinned] = contacts as [PrimaryDevice]; state = state.addContact(pinned, { identityKey: pinned.publicKey.serialize(), @@ -94,6 +94,7 @@ describe('attachments', function (this: Mocha.Suite) { } = await sendMessageWithAttachments(page, pinned, 'This is my cat', [ CAT_PATH, ]); + strictAssert(attachmentCat, 'attachment must exist'); const Message = getTimelineMessageWithText(page, 'This is my cat'); const MessageSent = Message.locator( @@ -278,12 +279,15 @@ describe('attachments', function (this: Mocha.Suite) { strictAssert(sentTimestamp, 'outgoing timestamp must exist'); const phoneDBMessage = (await app.getMessagesBySentAt(phoneTimestamp))[0]; - const phoneDBAttachment: AttachmentType = phoneDBMessage.attachments?.[0]; - const friendDBMessage = (await app.getMessagesBySentAt(friendTimestamp))[0]; - const friendDBAttachment: AttachmentType = friendDBMessage.attachments?.[0]; - const sentDBMessage = (await app.getMessagesBySentAt(sentTimestamp))[0]; + + strictAssert(phoneDBMessage, 'outgoing sync message exists in DB'); + strictAssert(friendDBMessage, 'incoming message exists in DB'); + strictAssert(sentDBMessage, 'sent message exists in DB'); + + const phoneDBAttachment: AttachmentType = phoneDBMessage.attachments?.[0]; + const friendDBAttachment: AttachmentType = friendDBMessage.attachments?.[0]; const sentDBAttachment: AttachmentType = sentDBMessage.attachments?.[0]; strictAssert(phoneDBAttachment, 'outgoing sync message exists in DB'); @@ -383,11 +387,11 @@ describe('attachments', function (this: Mocha.Suite) { const firstAttachment: AttachmentType = ( await app.getMessagesBySentAt(firstTimestamp) - )[0].attachments?.[0]; + )[0]?.attachments?.[0]; const secondAttachment: AttachmentType = ( await app.getMessagesBySentAt(secondTimestamp) - )[0].attachments?.[0]; + )[0]?.attachments?.[0]; strictAssert(firstAttachment, 'firstAttachment exists in DB'); strictAssert(secondAttachment, 'secondAttachment exists in DB'); @@ -470,6 +474,7 @@ describe('attachments', function (this: Mocha.Suite) { ]); const sentVideo = sentVideoAttachments[0]; + assert.exists(sentVideo); assert.deepStrictEqual( sentVideo.attachmentIdentifier, recentPointer.attachmentIdentifier @@ -486,6 +491,7 @@ describe('attachments', function (this: Mocha.Suite) { assert.notDeepEqual(sentVideo.fileName, recentPointer.fileName); const sentCat = sentCatAttachments[0]; + assert.exists(sentCat); assert.notDeepEqual( sentCat.attachmentIdentifier, stalePointer.attachmentIdentifier diff --git a/ts/test-mock/messaging/backfill_test.node.ts b/ts/test-mock/messaging/backfill_test.node.ts index 431e11f36f..856fdfb104 100644 --- a/ts/test-mock/messaging/backfill_test.node.ts +++ b/ts/test-mock/messaging/backfill_test.node.ts @@ -66,7 +66,7 @@ describe('attachment backfill', function (this: Mocha.Suite) { page = await app.getWindow(); const { unknownContacts } = bootstrap; - [unknownContact] = unknownContacts; + [unknownContact] = unknownContacts as [PrimaryDevice]; textAttachment = await bootstrap.encryptAndStoreAttachmentOnCDN( Buffer.from('look at this pic, it is gorgeous!'), diff --git a/ts/test-mock/messaging/edit_test.node.ts b/ts/test-mock/messaging/edit_test.node.ts index 9d5826ed04..19436c57f1 100644 --- a/ts/test-mock/messaging/edit_test.node.ts +++ b/ts/test-mock/messaging/edit_test.node.ts @@ -3,6 +3,7 @@ // `window` use below is actually executed in the browser. // eslint-disable-next-line local-rules/file-suffix +import type { PrimaryDevice } from '@signalapp/mock-server'; import { Proto, EMPTY_DATA_MESSAGE } from '@signalapp/mock-server'; import { Aci } from '@signalapp/libsignal-client'; import { assert } from 'chai'; @@ -250,7 +251,7 @@ describe('editing', function (this: Mocha.Suite) { const window = await app.getWindow(); - const [friend] = contacts; + const [friend] = contacts as [PrimaryDevice]; const initialMessageBody = 'hey yhere'; const originalMessage = createMessage(initialMessageBody); @@ -341,7 +342,7 @@ describe('editing', function (this: Mocha.Suite) { const window = await app.getWindow(); - const [friend] = contacts; + const [friend] = contacts as [PrimaryDevice]; debug('incoming message'); await friend.sendText(desktop, 'hello', { @@ -562,14 +563,14 @@ describe('editing', function (this: Mocha.Suite) { originalMessageTimestamp ); strictAssert(messages, 'messages does not exist'); - strictAssert(messages.length === 1, 'message does not exist'); + strictAssert(messages[0], 'message does not exist'); return messages[0]; } const { contacts, desktop } = bootstrap; - const [friend] = contacts; + const [friend] = contacts as [PrimaryDevice]; const page = await app.getWindow(); @@ -660,7 +661,7 @@ describe('editing', function (this: Mocha.Suite) { 'sendStateByConversationId' ); assert.strictEqual( - message.sendStateByConversationId[conversationId].status, + message.sendStateByConversationId[conversationId]?.status, SendStatus.Delivered, 'send state is delivered for main message' ); @@ -706,12 +707,12 @@ describe('editing', function (this: Mocha.Suite) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const [_v2, v1] = message.editHistory; assert.strictEqual( - message.sendStateByConversationId?.[conversationId].status, + message.sendStateByConversationId?.[conversationId]?.status, SendStatus.Sent, 'send state is reverted back to sent for main message' ); assert.strictEqual( - v1.sendStateByConversationId?.[conversationId].status, + v1?.sendStateByConversationId?.[conversationId]?.status, SendStatus.Read, 'original message is marked read' ); @@ -806,7 +807,7 @@ describe('editing', function (this: Mocha.Suite) { 'sendStateByConversationId' ); assert.strictEqual( - message.sendStateByConversationId[conversationId].status, + message.sendStateByConversationId[conversationId]?.status, SendStatus.Sent, 'original message send state is sent (v4)' ); @@ -814,30 +815,30 @@ describe('editing', function (this: Mocha.Suite) { strictAssert(message.editHistory, 'edit history exists'); const [v4, v3, v2, v1] = message.editHistory; - strictAssert(v1.sendStateByConversationId, 'v1 has send state'); + strictAssert(v1?.sendStateByConversationId, 'v1 has send state'); assert.strictEqual( - v1.sendStateByConversationId[conversationId].status, + v1.sendStateByConversationId[conversationId]?.status, SendStatus.Read, 'send state for first message is read' ); - strictAssert(v2.sendStateByConversationId, 'v2 has send state'); + strictAssert(v2?.sendStateByConversationId, 'v2 has send state'); assert.strictEqual( - v2.sendStateByConversationId[conversationId].status, + v2.sendStateByConversationId[conversationId]?.status, SendStatus.Sent, 'send state for v2 message is sent' ); - strictAssert(v3.sendStateByConversationId, 'v3 has send state'); + strictAssert(v3?.sendStateByConversationId, 'v3 has send state'); assert.strictEqual( - v3.sendStateByConversationId[conversationId].status, + v3.sendStateByConversationId[conversationId]?.status, SendStatus.Read, 'send state for v3 message is read' ); - strictAssert(v4.sendStateByConversationId, 'v4 has send state'); + strictAssert(v4?.sendStateByConversationId, 'v4 has send state'); assert.strictEqual( - v4.sendStateByConversationId[conversationId].status, + v4.sendStateByConversationId[conversationId]?.status, SendStatus.Sent, 'send state for v4 message is sent' ); diff --git a/ts/test-mock/messaging/lightbox_test.node.ts b/ts/test-mock/messaging/lightbox_test.node.ts index 0bd09d7010..ac39011ff7 100644 --- a/ts/test-mock/messaging/lightbox_test.node.ts +++ b/ts/test-mock/messaging/lightbox_test.node.ts @@ -37,7 +37,7 @@ describe('lightbox', function (this: Mocha.Suite) { let state = StorageState.getEmpty(); const { phone, contacts } = bootstrap; - [pinned] = contacts; + [pinned] = contacts as [PrimaryDevice]; state = state.addContact(pinned, { identityKey: pinned.publicKey.serialize(), @@ -114,6 +114,10 @@ describe('lightbox', function (this: Mocha.Suite) { imageWaterfall, ]); + strictAssert(attachmentCat, 'attachmentCat exists'); + strictAssert(attachmentSnow, 'attachmentSnow exists'); + strictAssert(attachmentWaterfall, 'attachmentWaterfall exists'); + await sendAttachmentsBack('Message3', [attachmentCat]); await sendAttachmentsBack('Message4', [ attachmentSnow, diff --git a/ts/test-mock/messaging/reaction_test.node.ts b/ts/test-mock/messaging/reaction_test.node.ts index 38becb8645..d643f2f3bb 100644 --- a/ts/test-mock/messaging/reaction_test.node.ts +++ b/ts/test-mock/messaging/reaction_test.node.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import createDebug from 'debug'; +import type { PrimaryDevice } from '@signalapp/mock-server'; import { StorageState } from '@signalapp/mock-server'; import { type Page } from 'playwright'; import { expect } from 'playwright/test'; @@ -80,7 +81,11 @@ describe('reactions', function (this: Mocha.Suite) { await bootstrap.init(); const { phone, contacts } = bootstrap; - const [alice, bob, charlie] = contacts; + const [alice, bob, charlie] = contacts as [ + PrimaryDevice, + PrimaryDevice, + PrimaryDevice, + ]; let state = StorageState.getEmpty(); state = state.addContact(alice, { @@ -114,7 +119,11 @@ describe('reactions', function (this: Mocha.Suite) { it('should correctly match on participant, timestamp, and author in 1:1 conversation', async () => { this.timeout(10000); const { contacts, phone, desktop } = bootstrap; - const [alice, bob, charlie] = contacts; + const [alice, bob, charlie] = contacts as [ + PrimaryDevice, + PrimaryDevice, + PrimaryDevice, + ]; const window = await app.getWindow(); @@ -237,7 +246,12 @@ describe('reactions', function (this: Mocha.Suite) { this.timeout(10000); const { contacts, phone, desktop } = bootstrap; - const [alice, bob, charlie, danielle] = contacts; + const [alice, bob, charlie, danielle] = contacts as [ + PrimaryDevice, + PrimaryDevice, + PrimaryDevice, + PrimaryDevice, + ]; const groupMembers = [alice, bob, charlie]; const groupForSending = { @@ -357,7 +371,7 @@ describe('reactions', function (this: Mocha.Suite) { this.timeout(30_000); const { contacts, phone, desktop } = bootstrap; - const [alice, bob] = contacts; + const [alice, bob] = contacts as [PrimaryDevice, PrimaryDevice]; // Create a group that includes both Alice and Bob const groupForSending = { @@ -421,6 +435,7 @@ describe('reactions', function (this: Mocha.Suite) { // First row: Bob's 👍🏿 const firstReaction = reactionRows[0]; + strictAssert(firstReaction, 'firstReaction exists'); await expect( firstReaction.locator('.module-reaction-viewer__body__row__name') ).toHaveText(bob.profileName); @@ -431,6 +446,7 @@ describe('reactions', function (this: Mocha.Suite) { // Second row: local user's 👍🏽 const secondReaction = reactionRows[1]; + strictAssert(secondReaction, 'secondReaction exists'); await expect( secondReaction.locator('.module-reaction-viewer__body__row__name') ).toHaveText('You'); diff --git a/ts/test-mock/messaging/readSync_test.node.ts b/ts/test-mock/messaging/readSync_test.node.ts index ad07da9a3f..31f015b76f 100644 --- a/ts/test-mock/messaging/readSync_test.node.ts +++ b/ts/test-mock/messaging/readSync_test.node.ts @@ -3,6 +3,7 @@ import createDebug from 'debug'; +import type { PrimaryDevice } from '@signalapp/mock-server'; import type { App } from '../playwright.node.js'; import * as durations from '../../util/durations/index.std.js'; import { Bootstrap } from '../bootstrap.node.js'; @@ -33,7 +34,7 @@ describe('readSync', function (this: Mocha.Suite) { it('applies out of order read syncs', async () => { const { contacts, desktop, phone } = bootstrap; - const [friend] = contacts; + const [friend] = contacts as [PrimaryDevice]; const page = await app.getWindow(); diff --git a/ts/test-mock/messaging/relink_test.node.ts b/ts/test-mock/messaging/relink_test.node.ts index 3dbd584e5f..d2f3916403 100644 --- a/ts/test-mock/messaging/relink_test.node.ts +++ b/ts/test-mock/messaging/relink_test.node.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import createDebug from 'debug'; +import type { PrimaryDevice } from '@signalapp/mock-server'; import { Proto, StorageState } from '@signalapp/mock-server'; import * as durations from '../../util/durations/index.std.js'; @@ -21,10 +22,12 @@ describe('messaging/relink', function (this: Mocha.Suite) { bootstrap = new Bootstrap(); await bootstrap.init(); - const { - phone, - contacts: [first, second, third], - } = bootstrap; + const { phone, contacts } = bootstrap; + const [first, second, third] = contacts as [ + PrimaryDevice, + PrimaryDevice, + PrimaryDevice, + ]; let state = StorageState.getEmpty(); @@ -68,12 +71,8 @@ describe('messaging/relink', function (this: Mocha.Suite) { }); it('updates pin state on relink', async () => { - const { - contacts: [first, second], - desktop, - phone, - server, - } = bootstrap; + const { contacts, desktop, phone, server } = bootstrap; + const [first, second] = contacts as [PrimaryDevice, PrimaryDevice]; { const window = await app.getWindow(); diff --git a/ts/test-mock/messaging/retries_test.node.ts b/ts/test-mock/messaging/retries_test.node.ts index d05a9fce2a..4003db6b7d 100644 --- a/ts/test-mock/messaging/retries_test.node.ts +++ b/ts/test-mock/messaging/retries_test.node.ts @@ -3,6 +3,7 @@ import { assert } from 'chai'; import createDebug from 'debug'; +import type { PrimaryDevice } from '@signalapp/mock-server'; import { EnvelopeType, StorageState } from '@signalapp/mock-server'; import type { App } from '../playwright.node.js'; @@ -24,7 +25,7 @@ describe('retries', function (this: Mocha.Suite) { app = await bootstrap.link(); const { contacts, phone } = bootstrap; - const [first] = contacts; + const [first] = contacts as [PrimaryDevice]; let state = StorageState.getEmpty(); @@ -50,7 +51,7 @@ describe('retries', function (this: Mocha.Suite) { it('sends a retry request on a missing sender key error', async () => { const { desktop, contacts } = bootstrap; - const [first] = contacts; + const [first] = contacts as [PrimaryDevice]; debug('send a sender key message without sending skdm first'); const distributionId = await first.sendSenderKey(desktop, { @@ -75,7 +76,7 @@ describe('retries', function (this: Mocha.Suite) { it('does not send a retry request if message succeeded later', async () => { const { desktop, contacts } = bootstrap; - const [first] = contacts; + const [first] = contacts as [PrimaryDevice]; await app.close(); @@ -132,7 +133,7 @@ describe('retries', function (this: Mocha.Suite) { it('sends only one retry request if many failures with same timestamp', async () => { const { desktop, contacts } = bootstrap; - const [first] = contacts; + const [first] = contacts as [PrimaryDevice]; debug('send a sender key message without sending skdm first'); const firstDistributionId = await first.sendSenderKey(desktop, { diff --git a/ts/test-mock/messaging/safety_number_test.node.ts b/ts/test-mock/messaging/safety_number_test.node.ts index 76b57f06e7..efdaacf66f 100644 --- a/ts/test-mock/messaging/safety_number_test.node.ts +++ b/ts/test-mock/messaging/safety_number_test.node.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import createDebug from 'debug'; +import type { PrimaryDevice } from '@signalapp/mock-server'; import { StorageState, Proto } from '@signalapp/mock-server'; import { assert } from 'chai'; @@ -31,7 +32,7 @@ describe('safety number', function (this: Mocha.Suite) { await bootstrap.init(); const { phone, contacts } = bootstrap; - const [alice] = contacts; + const [alice] = contacts as [PrimaryDevice]; let state = StorageState.getEmpty(); state = state.updateAccount({ @@ -77,10 +78,8 @@ describe('safety number', function (this: Mocha.Suite) { }); async function changeIdentityKey(): Promise { - const { - phone, - contacts: [alice, bob], - } = bootstrap; + const { phone, contacts } = bootstrap; + const [alice, bob] = contacts as [PrimaryDevice, PrimaryDevice]; await app.waitForStorageService(); @@ -101,9 +100,8 @@ describe('safety number', function (this: Mocha.Suite) { } it('show safety number change UI on regular send', async () => { - const { - contacts: [alice], - } = bootstrap; + const { contacts } = bootstrap; + const [alice] = contacts as [PrimaryDevice]; const window = await app.getWindow(); @@ -141,9 +139,8 @@ describe('safety number', function (this: Mocha.Suite) { }); it('show safety number change UI on story send', async () => { - const { - contacts: [alice], - } = bootstrap; + const { contacts } = bootstrap; + const [alice] = contacts as [PrimaryDevice]; const window = await app.getWindow(); const storiesPane = window.locator('.Stories'); diff --git a/ts/test-mock/messaging/sender_key_test.node.ts b/ts/test-mock/messaging/sender_key_test.node.ts index adf5022a94..1e114e3091 100644 --- a/ts/test-mock/messaging/sender_key_test.node.ts +++ b/ts/test-mock/messaging/sender_key_test.node.ts @@ -3,7 +3,7 @@ import createDebug from 'debug'; import { StorageState } from '@signalapp/mock-server'; -import type { Group } from '@signalapp/mock-server'; +import type { Group, PrimaryDevice } from '@signalapp/mock-server'; import * as durations from '../../util/durations/index.std.js'; import type { App } from '../playwright.node.js'; @@ -23,7 +23,7 @@ describe('senderKey', function (this: Mocha.Suite) { await bootstrap.init(); const { contacts, phone } = bootstrap; - const [first] = contacts; + const [first] = contacts as [PrimaryDevice]; group = await first.createGroup({ title: 'Group', @@ -60,7 +60,7 @@ describe('senderKey', function (this: Mocha.Suite) { it('handles incoming senderKey distributions and messages', async () => { const { desktop, contacts } = bootstrap; - const [first] = contacts; + const [first] = contacts as [PrimaryDevice]; const window = await app.getWindow(); diff --git a/ts/test-mock/messaging/stories_test.node.ts b/ts/test-mock/messaging/stories_test.node.ts index 779db3d790..8a3052feeb 100644 --- a/ts/test-mock/messaging/stories_test.node.ts +++ b/ts/test-mock/messaging/stories_test.node.ts @@ -7,7 +7,7 @@ import { StorageState, EMPTY_DATA_MESSAGE, } from '@signalapp/mock-server'; -import type { Group } from '@signalapp/mock-server'; +import type { Group, PrimaryDevice } from '@signalapp/mock-server'; import * as durations from '../../util/durations/index.std.js'; import { uuidToBytes } from '../../util/uuidToBytes.std.js'; @@ -35,7 +35,7 @@ describe('story/messaging', function (this: Mocha.Suite) { await bootstrap.init(); const { phone, contacts } = bootstrap; - const [first, second] = contacts; + const [first, second] = contacts as [PrimaryDevice, PrimaryDevice]; let state = StorageState.getEmpty(); @@ -130,7 +130,7 @@ describe('story/messaging', function (this: Mocha.Suite) { it('allows replies on multiple distribution lists', async () => { const { phone, desktop, contacts } = bootstrap; - const [first, second] = contacts; + const [first, second] = contacts as [PrimaryDevice, PrimaryDevice]; const window = await app.getWindow(); const sentAt = Date.now(); @@ -254,6 +254,7 @@ describe('story/messaging', function (this: Mocha.Suite) { it('allows replies to groups', async () => { const { desktop, contacts } = bootstrap; + const [first] = contacts as [PrimaryDevice]; const window = await app.getWindow(); @@ -286,7 +287,7 @@ describe('story/messaging', function (this: Mocha.Suite) { story: sentAt, reply: sentAt + 1, }); - await contacts[0].sendRaw( + await first.sendRaw( desktop, { content: { diff --git a/ts/test-mock/messaging/unknown_contact_test.node.ts b/ts/test-mock/messaging/unknown_contact_test.node.ts index eef18c2fe8..9efa672260 100644 --- a/ts/test-mock/messaging/unknown_contact_test.node.ts +++ b/ts/test-mock/messaging/unknown_contact_test.node.ts @@ -28,7 +28,7 @@ describe('unknown contacts', function (this: Mocha.Suite) { page = await app.getWindow(); const { unknownContacts } = bootstrap; - [unknownContact] = unknownContacts; + [unknownContact] = unknownContacts as [PrimaryDevice]; }); afterEach(async function (this: Mocha.Context) { diff --git a/ts/test-mock/messaging/unprocessed_test.node.ts b/ts/test-mock/messaging/unprocessed_test.node.ts index 6a80474384..a7ac4e8ec3 100644 --- a/ts/test-mock/messaging/unprocessed_test.node.ts +++ b/ts/test-mock/messaging/unprocessed_test.node.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import createDebug from 'debug'; +import type { PrimaryDevice } from '@signalapp/mock-server'; import { StorageState } from '@signalapp/mock-server'; import * as durations from '../../util/durations/index.std.js'; @@ -23,10 +24,8 @@ describe('unprocessed', function (this: Mocha.Suite) { let state = StorageState.getEmpty(); - const { - phone, - contacts: [alice], - } = bootstrap; + const { phone, contacts } = bootstrap; + const [alice] = contacts as [PrimaryDevice]; state = state.addContact(alice, { identityKey: alice.publicKey.serialize(), @@ -51,10 +50,8 @@ describe('unprocessed', function (this: Mocha.Suite) { }); it('generates and loads unprocessed envelopes', async () => { - const { - desktop, - contacts: [alice], - } = bootstrap; + const { desktop, contacts } = bootstrap; + const [alice] = contacts as [PrimaryDevice]; debug('closing'); await app.close(); diff --git a/ts/test-mock/network/serverAlerts_test.node.ts b/ts/test-mock/network/serverAlerts_test.node.ts index 2cf7f403a6..53eb95a719 100644 --- a/ts/test-mock/network/serverAlerts_test.node.ts +++ b/ts/test-mock/network/serverAlerts_test.node.ts @@ -24,7 +24,7 @@ describe('serverAlerts', function (this: Mocha.Suite) { // Set up a pinned contact to trigger profile fetch to test unauth socket let state = StorageState.getEmpty(); const { phone, contacts } = bootstrap; - [pinned] = contacts; + [pinned] = contacts as [PrimaryDevice]; state = state.addContact(pinned, { identityKey: pinned.publicKey.serialize(), diff --git a/ts/test-mock/pnp/accept_gv2_invite_test.node.ts b/ts/test-mock/pnp/accept_gv2_invite_test.node.ts index 7d1e643dad..1223167980 100644 --- a/ts/test-mock/pnp/accept_gv2_invite_test.node.ts +++ b/ts/test-mock/pnp/accept_gv2_invite_test.node.ts @@ -34,8 +34,11 @@ describe('pnp/accept gv2 invite', function (this: Mocha.Suite) { await bootstrap.init(); const { phone, contacts, unknownContacts } = bootstrap; - const [first, second] = contacts; - [unknownContact, unknownPniContact] = unknownContacts; + const [first, second] = contacts as [PrimaryDevice, PrimaryDevice]; + [unknownContact, unknownPniContact] = unknownContacts as [ + PrimaryDevice, + PrimaryDevice, + ]; group = await first.createGroup({ title: 'Invited Desktop PNI', @@ -96,7 +99,7 @@ describe('pnp/accept gv2 invite', function (this: Mocha.Suite) { it('should accept PNI invite and modify the group state', async () => { const { phone, contacts, desktop } = bootstrap; - const [first, second] = contacts; + const [first, second] = contacts as [PrimaryDevice, PrimaryDevice]; const window = await app.getWindow(); @@ -237,7 +240,7 @@ describe('pnp/accept gv2 invite', function (this: Mocha.Suite) { it('should accept ACI invite with extra PNI on the invite list', async () => { const { phone, contacts, desktop } = bootstrap; - const [first, second] = contacts; + const [first, second] = contacts as [PrimaryDevice, PrimaryDevice]; const window = await app.getWindow(); @@ -300,7 +303,7 @@ describe('pnp/accept gv2 invite', function (this: Mocha.Suite) { it('should decline ACI invite with extra PNI on the invite list', async () => { const { phone, contacts, desktop } = bootstrap; - const [, second] = contacts; + const [, second] = contacts as [PrimaryDevice, PrimaryDevice]; const window = await app.getWindow(); @@ -333,7 +336,7 @@ describe('pnp/accept gv2 invite', function (this: Mocha.Suite) { it('should display a single notification for remote PNI accept', async () => { const { phone, contacts, desktop } = bootstrap; - const [first, second] = contacts; + const [first, second] = contacts as [PrimaryDevice, PrimaryDevice]; debug('Creating new group with Desktop'); group = await phone.createGroup({ @@ -380,7 +383,7 @@ describe('pnp/accept gv2 invite', function (this: Mocha.Suite) { it('should display a e164 for a PNI invite', async () => { const { phone, contacts, desktop } = bootstrap; - const [first] = contacts; + const [first] = contacts as [PrimaryDevice]; debug('Creating new group with Desktop'); group = await phone.createGroup({ diff --git a/ts/test-mock/pnp/change_number_test.node.ts b/ts/test-mock/pnp/change_number_test.node.ts index a397bd962e..cb82137ce9 100644 --- a/ts/test-mock/pnp/change_number_test.node.ts +++ b/ts/test-mock/pnp/change_number_test.node.ts @@ -1,6 +1,7 @@ // Copyright 2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +import type { PrimaryDevice } from '@signalapp/mock-server'; import { ServiceIdKind } from '@signalapp/mock-server'; import createDebug from 'debug'; @@ -31,7 +32,7 @@ describe('pnp/change number', function (this: Mocha.Suite) { it('should accept sync message and update keys', async () => { const { server, phone, desktop, contacts } = bootstrap; - const [first] = contacts; + const [first] = contacts as [PrimaryDevice]; const window = await app.getWindow(); diff --git a/ts/test-mock/pnp/merge_test.node.ts b/ts/test-mock/pnp/merge_test.node.ts index 92b116746b..aaa42061de 100644 --- a/ts/test-mock/pnp/merge_test.node.ts +++ b/ts/test-mock/pnp/merge_test.node.ts @@ -427,7 +427,7 @@ describe('pnp/merge', function (this: Mocha.Suite) { } assert.strictEqual(aciContacts, 1); assert.strictEqual(pniContacts, 1); - assert.ok(removed[0].contact != null); + assert.ok(removed[0]?.contact != null); assert.deepEqual( removed[0].contact.pniBinary, diff --git a/ts/test-mock/pnp/send_gv2_invite_test.node.ts b/ts/test-mock/pnp/send_gv2_invite_test.node.ts index 001bc45bf7..eaf523a053 100644 --- a/ts/test-mock/pnp/send_gv2_invite_test.node.ts +++ b/ts/test-mock/pnp/send_gv2_invite_test.node.ts @@ -152,7 +152,7 @@ describe('pnp/send gv2 invite', function (this: Mocha.Suite) { const groups = await phone.getAllGroups(state); assert.strictEqual(groups.length, 1); - [group] = groups; + [group] = groups as [Group]; assert.strictEqual(group.title, 'My group'); assert.strictEqual(group.revision, 0); assert.strictEqual(group.state.members?.length, 2); diff --git a/ts/test-mock/pnp/username_test.node.ts b/ts/test-mock/pnp/username_test.node.ts index f36e3dbc96..4e9c23e33a 100644 --- a/ts/test-mock/pnp/username_test.node.ts +++ b/ts/test-mock/pnp/username_test.node.ts @@ -147,14 +147,14 @@ describe('pnp/username', function (this: Mocha.Suite) { 'only one record must be removed' ); - assert.ok(added[0].contact != null); + assert.ok(added[0]?.contact != null); assert.deepEqual( added[0].contact.aciBinary, usernameContact.device.aciRawUuid ); assert.strictEqual(added[0].contact.username, ''); - assert.ok(removed[0].contact != null); + assert.ok(removed[0]?.contact != null); assert.deepEqual( removed[0].contact.aciBinary, usernameContact.device.aciRawUuid diff --git a/ts/test-mock/routing/routing_test.node.ts b/ts/test-mock/routing/routing_test.node.ts index 1300040072..bb87262adb 100644 --- a/ts/test-mock/routing/routing_test.node.ts +++ b/ts/test-mock/routing/routing_test.node.ts @@ -5,6 +5,7 @@ // eslint-disable-next-line local-rules/file-suffix import { assert } from 'chai'; +import type { PrimaryDevice } from '@signalapp/mock-server'; import * as durations from '../../util/durations/index.std.js'; import type { Bootstrap, App } from '../bootstrap.node.js'; import { @@ -54,7 +55,7 @@ describe('routing', function (this: Mocha.Suite) { it('showConversationRoute', async () => { const { contacts } = bootstrap; - const [friend] = contacts; + const [friend] = contacts as [PrimaryDevice]; const page = await app.getWindow(); await page.locator('#LeftPane').waitFor(); const token = await page.evaluate( diff --git a/ts/test-mock/storage/archive_test.node.ts b/ts/test-mock/storage/archive_test.node.ts index d8b450d659..c04c6558bb 100644 --- a/ts/test-mock/storage/archive_test.node.ts +++ b/ts/test-mock/storage/archive_test.node.ts @@ -3,6 +3,7 @@ import { assert } from 'chai'; +import type { PrimaryDevice } from '@signalapp/mock-server'; import * as durations from '../../util/durations/index.std.js'; import type { App, Bootstrap } from './fixtures.node.js'; import { initStorage, debug } from './fixtures.node.js'; @@ -29,7 +30,7 @@ describe('storage service', function (this: Mocha.Suite) { it('should archive/unarchive contacts', async () => { const { phone, contacts } = bootstrap; - const [firstContact] = contacts; + const [firstContact] = contacts as [PrimaryDevice]; const window = await app.getWindow(); diff --git a/ts/test-mock/storage/conflict_test.node.ts b/ts/test-mock/storage/conflict_test.node.ts index c4abdfd48b..cfe0fe7247 100644 --- a/ts/test-mock/storage/conflict_test.node.ts +++ b/ts/test-mock/storage/conflict_test.node.ts @@ -5,6 +5,7 @@ import { assert } from 'chai'; import { expect } from 'playwright/test'; import type { Group, + PrimaryDevice, StorageState, StorageStateRecord, } from '@signalapp/mock-server'; @@ -45,10 +46,8 @@ describe('storage service', function (this: Mocha.Suite) { for (const kind of ['contact', 'group']) { // eslint-disable-next-line no-loop-func it(`should handle ${kind} conflicts`, async () => { - const { - phone, - contacts: [first], - } = bootstrap; + const { phone, contacts } = bootstrap; + const [first] = contacts as [PrimaryDevice]; const window = await app.getWindow(); @@ -132,11 +131,8 @@ describe('storage service', function (this: Mocha.Suite) { } it('should handle account conflicts', async () => { - const { - phone, - desktop, - contacts: [first, second], - } = bootstrap; + const { phone, desktop, contacts } = bootstrap; + const [first, second] = contacts as [PrimaryDevice, PrimaryDevice]; const window = await app.getWindow(); diff --git a/ts/test-mock/storage/fixtures.node.ts b/ts/test-mock/storage/fixtures.node.ts index e6f92924bf..4913b17e0f 100644 --- a/ts/test-mock/storage/fixtures.node.ts +++ b/ts/test-mock/storage/fixtures.node.ts @@ -61,7 +61,7 @@ export async function initStorage( // Populate storage service const { contacts, phone } = bootstrap; - const [firstContact] = contacts; + const [firstContact] = contacts as [PrimaryDevice]; const members = [...contacts].slice(0, GROUP_SIZE); @@ -137,12 +137,12 @@ export async function initStorage( const { desktop } = bootstrap; // Send a message to the group and the first contact - const contactSend = contacts[0].sendText(desktop, 'hello from contact', { + const contactSend = contacts[0]?.sendText(desktop, 'hello from contact', { timestamp: bootstrap.getTimestamp(), sealed: true, }); - const groupSend = members[0].sendText(desktop, 'hello in group', { + const groupSend = members[0]?.sendText(desktop, 'hello in group', { timestamp: bootstrap.getTimestamp(), sealed: true, group, @@ -167,7 +167,7 @@ export type StickerPackType = Readonly<{ stickerCount: number; }>; -export const STICKER_PACKS: ReadonlyArray = [ +export const STICKER_PACKS = [ { id: Buffer.from('c40ed069cdc2b91eccfccf25e6bcddfc', 'hex'), key: Buffer.from( @@ -184,7 +184,7 @@ export const STICKER_PACKS: ReadonlyArray = [ ), stickerCount: 1, }, -]; +] as const satisfies ReadonlyArray; export function getStickerPackLink(pack: StickerPackType): string { return artAddStickersRoute diff --git a/ts/test-mock/storage/max_read_keys_test.node.ts b/ts/test-mock/storage/max_read_keys_test.node.ts index 6f7aeb41d6..39cdd217c1 100644 --- a/ts/test-mock/storage/max_read_keys_test.node.ts +++ b/ts/test-mock/storage/max_read_keys_test.node.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import { assert } from 'chai'; +import type { PrimaryDevice } from '@signalapp/mock-server'; import { Proto } from '@signalapp/mock-server'; import * as durations from '../../util/durations/index.std.js'; @@ -37,8 +38,8 @@ describe('storage service', function (this: Mocha.Suite) { debug('prepare for a slow test'); const { phone, contacts } = bootstrap; - const firstContact = contacts[0]; - const lastContact = contacts[contacts.length - 1]; + const firstContact = contacts[0] as PrimaryDevice; + const lastContact = contacts[contacts.length - 1] as PrimaryDevice; const window = await app.getWindow(); diff --git a/ts/test-mock/storage/sticker_test.node.ts b/ts/test-mock/storage/sticker_test.node.ts index 90b6a240ac..a80ac717e9 100644 --- a/ts/test-mock/storage/sticker_test.node.ts +++ b/ts/test-mock/storage/sticker_test.node.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import { assert } from 'chai'; +import type { PrimaryDevice } from '@signalapp/mock-server'; import { Proto } from '@signalapp/mock-server'; import * as durations from '../../util/durations/index.std.js'; import type { App, Bootstrap } from './fixtures.node.js'; @@ -47,7 +48,7 @@ describe('stickers', function (this: Mocha.Suite) { it('should install/uninstall stickers', async () => { const { phone, desktop, contacts } = bootstrap; - const [firstContact] = contacts; + const [firstContact] = contacts as [PrimaryDevice]; const window = await app.getWindow(); @@ -303,10 +304,10 @@ describe('stickers', function (this: Mocha.Suite) { .waitFor(); const firstStickerData = (await app.getMessagesBySentAt(firstTimestamp))[0] - .sticker?.data; + ?.sticker?.data; const secondStickerData = ( await app.getMessagesBySentAt(secondTimestamp) - )[0].sticker?.data; + )[0]?.sticker?.data; strictAssert(firstStickerData?.path, 'path exists'); strictAssert(firstStickerData?.localKey, 'localKey exists'); diff --git a/ts/test-node/components/leftPane/LeftPaneArchiveHelper_test.dom.ts b/ts/test-node/components/leftPane/LeftPaneArchiveHelper_test.dom.ts index 9d907036ee..20852829fd 100644 --- a/ts/test-node/components/leftPane/LeftPaneArchiveHelper_test.dom.ts +++ b/ts/test-node/components/leftPane/LeftPaneArchiveHelper_test.dom.ts @@ -101,7 +101,7 @@ describe('LeftPaneArchiveHelper', () => { const archivedConversations = [ getDefaultConversation(), getDefaultConversation(), - ]; + ] as const; const helper = new LeftPaneArchiveHelper({ ...defaults, archivedConversations, @@ -125,7 +125,7 @@ describe('LeftPaneArchiveHelper', () => { const archivedConversations = [ getDefaultConversation(), getDefaultConversation(), - ]; + ] as const; const helper = new LeftPaneArchiveHelper(searchingDefaults); assert.strictEqual( @@ -140,7 +140,7 @@ describe('LeftPaneArchiveHelper', () => { const archivedConversations = [ getDefaultConversation(), getDefaultConversation(), - ]; + ] as const; const helper = new LeftPaneArchiveHelper({ ...defaults, archivedConversations, @@ -174,7 +174,7 @@ describe('LeftPaneArchiveHelper', () => { const archivedConversations = [ getDefaultConversation(), getDefaultConversation(), - ]; + ] as const; const helper = new LeftPaneArchiveHelper({ ...defaults, archivedConversations, @@ -194,7 +194,7 @@ describe('LeftPaneArchiveHelper', () => { const archivedConversations = [ getDefaultConversation(), getDefaultConversation(), - ]; + ] as const; const helper = new LeftPaneArchiveHelper({ ...defaults, archivedConversations, @@ -246,7 +246,7 @@ describe('LeftPaneArchiveHelper', () => { const archivedConversations = [ getDefaultConversation(), getDefaultConversation(), - ]; + ] as const; const helper = new LeftPaneArchiveHelper({ ...defaults, archivedConversations, diff --git a/ts/test-node/components/leftPane/LeftPaneChooseGroupMembersHelper_test.dom.ts b/ts/test-node/components/leftPane/LeftPaneChooseGroupMembersHelper_test.dom.ts index 684cd529b7..14eb9add99 100644 --- a/ts/test-node/components/leftPane/LeftPaneChooseGroupMembersHelper_test.dom.ts +++ b/ts/test-node/components/leftPane/LeftPaneChooseGroupMembersHelper_test.dom.ts @@ -119,7 +119,7 @@ describe('LeftPaneChooseGroupMembersHelper', () => { const candidateContacts = [ getDefaultConversation(), getDefaultConversation(), - ]; + ] as const; const helper = new LeftPaneChooseGroupMembersHelper({ ...defaults, candidateContacts, @@ -145,7 +145,11 @@ describe('LeftPaneChooseGroupMembersHelper', () => { }); it("disables non-selected contact checkboxes if you've selected the maximum number of contacts", () => { - const candidateContacts = times(50, () => getDefaultConversation()); + const candidateContacts = [ + getDefaultConversation(), + getDefaultConversation(), + ...times(48, () => getDefaultConversation()), + ] as const; const helper = new LeftPaneChooseGroupMembersHelper({ ...defaults, candidateContacts, diff --git a/ts/test-node/components/leftPane/LeftPaneComposeHelper_test.dom.ts b/ts/test-node/components/leftPane/LeftPaneComposeHelper_test.dom.ts index 102e53c25a..048e40d320 100644 --- a/ts/test-node/components/leftPane/LeftPaneComposeHelper_test.dom.ts +++ b/ts/test-node/components/leftPane/LeftPaneComposeHelper_test.dom.ts @@ -244,7 +244,7 @@ describe('LeftPaneComposeHelper', () => { const composeContacts = [ getDefaultConversation(), getDefaultConversation(), - ]; + ] as const; const helper = new LeftPaneComposeHelper({ composeContacts, composeGroups: [], @@ -280,11 +280,11 @@ describe('LeftPaneComposeHelper', () => { const composeContacts = [ getDefaultConversation(), getDefaultConversation(), - ]; + ] as const; const composeGroups = [ getDefaultGroupListItem(), getDefaultGroupListItem(), - ]; + ] as const; const helper = new LeftPaneComposeHelper({ composeContacts, composeGroups, @@ -329,7 +329,7 @@ describe('LeftPaneComposeHelper', () => { const composeContacts = [ getDefaultConversation(), getDefaultConversation(), - ]; + ] as const; const helper = new LeftPaneComposeHelper({ composeContacts, composeGroups: [], @@ -407,7 +407,7 @@ describe('LeftPaneComposeHelper', () => { const composeContacts = [ getDefaultConversation(), getDefaultConversation(), - ]; + ] as const; const helper = new LeftPaneComposeHelper({ composeContacts, composeGroups: [], diff --git a/ts/test-node/components/leftPane/LeftPaneInboxHelper_test.dom.tsx b/ts/test-node/components/leftPane/LeftPaneInboxHelper_test.dom.tsx index 2396e8fea5..77eceac1cb 100644 --- a/ts/test-node/components/leftPane/LeftPaneInboxHelper_test.dom.tsx +++ b/ts/test-node/components/leftPane/LeftPaneInboxHelper_test.dom.tsx @@ -125,7 +125,7 @@ describe('LeftPaneInboxHelper', () => { }); it('returns undefined if the selected conversation is not pinned or non-pinned', () => { - const archivedConversations = [getDefaultConversation()]; + const archivedConversations = [getDefaultConversation()] as const; const helper = new LeftPaneInboxHelper({ ...defaultProps, conversations: [getDefaultConversation(), getDefaultConversation()], @@ -142,7 +142,7 @@ describe('LeftPaneInboxHelper', () => { const pinnedConversations = [ getDefaultConversation(), getDefaultConversation(), - ]; + ] as const; const helper = new LeftPaneInboxHelper({ ...defaultProps, pinnedConversations, @@ -162,7 +162,7 @@ describe('LeftPaneInboxHelper', () => { const conversations = [ getDefaultConversation(), getDefaultConversation(), - ]; + ] as const; const helper = new LeftPaneInboxHelper({ ...defaultProps, conversations, @@ -176,7 +176,7 @@ describe('LeftPaneInboxHelper', () => { const pinnedConversations = [ getDefaultConversation(), getDefaultConversation(), - ]; + ] as const; const helper = new LeftPaneInboxHelper({ ...defaultProps, conversations: [getDefaultConversation()], @@ -197,7 +197,7 @@ describe('LeftPaneInboxHelper', () => { const conversations = [ getDefaultConversation(), getDefaultConversation(), - ]; + ] as const; const helper = new LeftPaneInboxHelper({ ...defaultProps, conversations, @@ -234,7 +234,7 @@ describe('LeftPaneInboxHelper', () => { const pinnedConversations = [ getDefaultConversation(), getDefaultConversation(), - ]; + ] as const; const helper = new LeftPaneInboxHelper({ ...defaultProps, @@ -260,7 +260,7 @@ describe('LeftPaneInboxHelper', () => { const pinnedConversations = [ getDefaultConversation(), getDefaultConversation(), - ]; + ] as const; const helper = new LeftPaneInboxHelper({ ...defaultProps, @@ -291,7 +291,7 @@ describe('LeftPaneInboxHelper', () => { const conversations = [ getDefaultConversation(), getDefaultConversation(), - ]; + ] as const; const helper = new LeftPaneInboxHelper({ ...defaultProps, @@ -313,7 +313,7 @@ describe('LeftPaneInboxHelper', () => { const conversations = [ getDefaultConversation(), getDefaultConversation(), - ]; + ] as const; const helper = new LeftPaneInboxHelper({ ...defaultProps, @@ -341,11 +341,11 @@ describe('LeftPaneInboxHelper', () => { getDefaultConversation(), getDefaultConversation(), getDefaultConversation(), - ]; + ] as const; const pinnedConversations = [ getDefaultConversation(), getDefaultConversation(), - ]; + ] as const; const helper = new LeftPaneInboxHelper({ ...defaultProps, @@ -389,11 +389,11 @@ describe('LeftPaneInboxHelper', () => { getDefaultConversation(), getDefaultConversation(), getDefaultConversation(), - ]; + ] as const; const pinnedConversations = [ getDefaultConversation(), getDefaultConversation(), - ]; + ] as const; const helper = new LeftPaneInboxHelper({ ...defaultProps, @@ -444,11 +444,11 @@ describe('LeftPaneInboxHelper', () => { getDefaultConversation(), getDefaultConversation(), getDefaultConversation(), - ]; + ] as const; const pinnedConversations = [ getDefaultConversation(), getDefaultConversation(), - ]; + ] as const; const helper = new LeftPaneInboxHelper({ ...defaultProps, @@ -482,7 +482,7 @@ describe('LeftPaneInboxHelper', () => { const pinnedConversations = [ getDefaultConversation(), getDefaultConversation(), - ]; + ] as const; const helper = new LeftPaneInboxHelper({ ...defaultProps, @@ -509,7 +509,7 @@ describe('LeftPaneInboxHelper', () => { const conversations = [ getDefaultConversation(), getDefaultConversation(), - ]; + ] as const; const helper = new LeftPaneInboxHelper({ ...defaultProps, @@ -536,11 +536,11 @@ describe('LeftPaneInboxHelper', () => { const conversations = [ getDefaultConversation(), getDefaultConversation(), - ]; + ] as const; const pinnedConversations = [ getDefaultConversation(), getDefaultConversation(), - ]; + ] as const; const helper = new LeftPaneInboxHelper({ ...defaultProps, @@ -581,8 +581,8 @@ describe('LeftPaneInboxHelper', () => { const pinnedConversations = [ getDefaultConversation(), getDefaultConversation(), - ]; - const conversations = [getDefaultConversation()]; + ] as const; + const conversations = [getDefaultConversation()] as const; const helper = new LeftPaneInboxHelper({ ...defaultProps, conversations, diff --git a/ts/test-node/components/leftPane/LeftPaneSearchHelper_test.dom.ts b/ts/test-node/components/leftPane/LeftPaneSearchHelper_test.dom.ts index fe45057c87..70ab1bc13a 100644 --- a/ts/test-node/components/leftPane/LeftPaneSearchHelper_test.dom.ts +++ b/ts/test-node/components/leftPane/LeftPaneSearchHelper_test.dom.ts @@ -174,18 +174,18 @@ describe('LeftPaneSearchHelper', () => { const conversations = [ getDefaultConversation(), getDefaultConversation(), - ]; - const contacts = [getDefaultConversation()]; - const messages = [fakeMessage(), fakeMessage()]; + ] as const; + const contacts = [getDefaultConversation()] as const; + const messages = [fakeMessage(), fakeMessage()] as const; const helper = new LeftPaneSearchHelper({ ...baseSearchHelperArgs, conversationResults: { isLoading: false, - results: conversations, + results: [...conversations], }, - contactResults: { isLoading: false, results: contacts }, - messageResults: { isLoading: false, results: messages }, + contactResults: { isLoading: false, results: [...contacts] }, + messageResults: { isLoading: false, results: [...messages] }, }); assert.deepEqual( @@ -217,13 +217,13 @@ describe('LeftPaneSearchHelper', () => { }); it('omits conversations when there are no conversation results', () => { - const contacts = [getDefaultConversation()]; - const messages = [fakeMessage(), fakeMessage()]; + const contacts = [getDefaultConversation()] as const; + const messages = [fakeMessage(), fakeMessage()] as const; const helper = new LeftPaneSearchHelper({ ...baseSearchHelperArgs, - contactResults: { isLoading: false, results: contacts }, - messageResults: { isLoading: false, results: messages }, + contactResults: { isLoading: false, results: [...contacts] }, + messageResults: { isLoading: false, results: [...messages] }, }); assert.deepEqual(_testHeaderText(helper.getRow(0)), 'icu:contactsHeader'); @@ -246,16 +246,16 @@ describe('LeftPaneSearchHelper', () => { const conversations = [ getDefaultConversation(), getDefaultConversation(), - ]; - const messages = [fakeMessage(), fakeMessage()]; + ] as const; + const messages = [fakeMessage(), fakeMessage()] as const; const helper = new LeftPaneSearchHelper({ ...baseSearchHelperArgs, conversationResults: { isLoading: false, - results: conversations, + results: [...conversations], }, - messageResults: { isLoading: false, results: messages }, + messageResults: { isLoading: false, results: [...messages] }, }); assert.deepEqual( @@ -330,16 +330,19 @@ describe('LeftPaneSearchHelper', () => { }); it('omits messages when there are no message results', () => { - const conversations = [getDefaultConversation(), getDefaultConversation()]; - const contacts = [getDefaultConversation()]; + const conversations = [ + getDefaultConversation(), + getDefaultConversation(), + ] as const; + const contacts = [getDefaultConversation()] as const; const helper = new LeftPaneSearchHelper({ ...baseSearchHelperArgs, conversationResults: { isLoading: false, - results: conversations, + results: [...conversations], }, - contactResults: { isLoading: false, results: contacts }, + contactResults: { isLoading: false, results: [...contacts] }, }); assert.deepEqual( @@ -605,14 +608,14 @@ describe('LeftPaneSearchHelper', () => { const conversations = [ getDefaultConversation(), getDefaultConversation(), - ]; + ] as const; const helper = new LeftPaneSearchHelper({ ...baseSearchHelperArgs, filterByUnread: true, conversationResults: { isLoading: false, - results: conversations, + results: [...conversations], }, contactResults: { isLoading: false, results: [] }, messageResults: { isLoading: false, results: [] }, diff --git a/ts/test-node/components/leftPane/LeftPaneSetGroupMetadataHelper_test.dom.ts b/ts/test-node/components/leftPane/LeftPaneSetGroupMetadataHelper_test.dom.ts index a773dd0bcf..1c70f75b23 100644 --- a/ts/test-node/components/leftPane/LeftPaneSetGroupMetadataHelper_test.dom.ts +++ b/ts/test-node/components/leftPane/LeftPaneSetGroupMetadataHelper_test.dom.ts @@ -89,7 +89,7 @@ describe('LeftPaneSetGroupMetadataHelper', () => { const selectedContacts = [ getDefaultConversation(), getDefaultConversation(), - ]; + ] as const; const helper = new LeftPaneSetGroupMetadataHelper({ ...getComposeState(), selectedContacts, diff --git a/ts/test-node/components/leftPane/getConversationInDirection_test.dom.ts b/ts/test-node/components/leftPane/getConversationInDirection_test.dom.ts index c7b2ed0516..c46a665f08 100644 --- a/ts/test-node/components/leftPane/getConversationInDirection_test.dom.ts +++ b/ts/test-node/components/leftPane/getConversationInDirection_test.dom.ts @@ -17,7 +17,7 @@ describe('getConversationInDirection', () => { fakeConversation(true), fakeConversation(true), fakeConversation(), - ]; + ] as const; describe('searching for any conversation', () => { const up: ToFindType = { @@ -107,7 +107,7 @@ describe('getConversationInDirection', () => { fakeConversation(), fakeConversation(), fakeConversation(), - ]; + ] as const; it('returns undefined if there are no conversations', () => { assert.isUndefined(getConversationInDirection([], up, undefined)); diff --git a/ts/test-node/components/media-gallery/groupMediaItemsByDate.std.ts b/ts/test-node/components/media-gallery/groupMediaItemsByDate.std.ts index a2b5de8d2e..3a0b1841ea 100644 --- a/ts/test-node/components/media-gallery/groupMediaItemsByDate.std.ts +++ b/ts/test-node/components/media-gallery/groupMediaItemsByDate.std.ts @@ -70,37 +70,37 @@ describe('groupMediaItemsByDate', () => { const actual = groupMediaItemsByDate(referenceTime, input); - assert.strictEqual(actual[0].type, 'today'); + assert.strictEqual(actual[0]?.type, 'today'); assert.strictEqual(actual[0].mediaItems.length, 2, 'today'); - assert.strictEqual(actual[0].mediaItems[0].message.id, 'today-1'); - assert.strictEqual(actual[0].mediaItems[1].message.id, 'today-2'); + assert.strictEqual(actual[0].mediaItems[0]?.message.id, 'today-1'); + assert.strictEqual(actual[0].mediaItems[1]?.message.id, 'today-2'); - assert.strictEqual(actual[1].type, 'yesterday'); - assert.strictEqual(actual[1].mediaItems.length, 2, 'yesterday'); - assert.strictEqual(actual[1].mediaItems[0].message.id, 'yesterday-1'); - assert.strictEqual(actual[1].mediaItems[1].message.id, 'yesterday-2'); + assert.strictEqual(actual[1]?.type, 'yesterday'); + assert.strictEqual(actual[1]?.mediaItems.length, 2, 'yesterday'); + assert.strictEqual(actual[1].mediaItems[0]?.message.id, 'yesterday-1'); + assert.strictEqual(actual[1].mediaItems[1]?.message.id, 'yesterday-2'); - assert.strictEqual(actual[2].type, 'thisWeek'); - assert.strictEqual(actual[2].mediaItems.length, 4, 'thisWeek'); - assert.strictEqual(actual[2].mediaItems[0].message.id, 'thisWeek-1'); - assert.strictEqual(actual[2].mediaItems[1].message.id, 'thisWeek-2'); - assert.strictEqual(actual[2].mediaItems[2].message.id, 'thisWeek-3'); - assert.strictEqual(actual[2].mediaItems[3].message.id, 'thisWeek-4'); + assert.strictEqual(actual[2]?.type, 'thisWeek'); + assert.strictEqual(actual[2]?.mediaItems.length, 4, 'thisWeek'); + assert.strictEqual(actual[2].mediaItems[0]?.message.id, 'thisWeek-1'); + assert.strictEqual(actual[2].mediaItems[1]?.message.id, 'thisWeek-2'); + assert.strictEqual(actual[2].mediaItems[2]?.message.id, 'thisWeek-3'); + assert.strictEqual(actual[2].mediaItems[3]?.message.id, 'thisWeek-4'); - assert.strictEqual(actual[3].type, 'thisMonth'); + assert.strictEqual(actual[3]?.type, 'thisMonth'); assert.strictEqual(actual[3].mediaItems.length, 2, 'thisMonth'); - assert.strictEqual(actual[3].mediaItems[0].message.id, 'thisMonth-1'); - assert.strictEqual(actual[3].mediaItems[1].message.id, 'thisMonth-2'); + assert.strictEqual(actual[3].mediaItems[0]?.message.id, 'thisMonth-1'); + assert.strictEqual(actual[3].mediaItems[1]?.message.id, 'thisMonth-2'); - assert.strictEqual(actual[4].type, 'yearMonth'); + assert.strictEqual(actual[4]?.type, 'yearMonth'); assert.strictEqual(actual[4].mediaItems.length, 2, 'mar2024'); - assert.strictEqual(actual[4].mediaItems[0].message.id, 'mar2024-1'); - assert.strictEqual(actual[4].mediaItems[1].message.id, 'mar2024-2'); + assert.strictEqual(actual[4].mediaItems[0]?.message.id, 'mar2024-1'); + assert.strictEqual(actual[4].mediaItems[1]?.message.id, 'mar2024-2'); - assert.strictEqual(actual[5].type, 'yearMonth'); + assert.strictEqual(actual[5]?.type, 'yearMonth'); assert.strictEqual(actual[5].mediaItems.length, 2, 'feb2011'); - assert.strictEqual(actual[5].mediaItems[0].message.id, 'feb2011-1'); - assert.strictEqual(actual[5].mediaItems[1].message.id, 'feb2011-2'); + assert.strictEqual(actual[5].mediaItems[0]?.message.id, 'feb2011-1'); + assert.strictEqual(actual[5].mediaItems[1]?.message.id, 'feb2011-2'); assert.strictEqual(actual.length, 6, 'total sections'); }); diff --git a/ts/test-node/jobs/JobQueue_test.node.ts b/ts/test-node/jobs/JobQueue_test.node.ts index e31dca5317..ab179e9792 100644 --- a/ts/test-node/jobs/JobQueue_test.node.ts +++ b/ts/test-node/jobs/JobQueue_test.node.ts @@ -246,8 +246,8 @@ describe('JobQueue', () => { const queueTypes = groupBy(store.storedJobs, 'queueType'); assert.hasAllKeys(queueTypes, ['test 1', 'test 2']); - assert.lengthOf(queueTypes['test 1'], 3); - assert.lengthOf(queueTypes['test 2'], 2); + assert.lengthOf(queueTypes['test 1'] ?? [], 3); + assert.lengthOf(queueTypes['test 2'] ?? [], 2); }); it('can override the insertion logic, skipping storage persistence', async () => { diff --git a/ts/test-node/messages/MessageSendState_test.std.ts b/ts/test-node/messages/MessageSendState_test.std.ts index 6454d75fd4..94f1cd2150 100644 --- a/ts/test-node/messages/MessageSendState_test.std.ts +++ b/ts/test-node/messages/MessageSendState_test.std.ts @@ -47,7 +47,7 @@ describe('message send state utilities', () => { it('orders the statuses', () => { times(100, () => { - const [a, b] = sampleSize(expectedOrder, 2); + const [a, b] = sampleSize(expectedOrder, 2) as [SendStatus, SendStatus]; const isABigger = expectedOrder.indexOf(a) > expectedOrder.indexOf(b); const expected = isABigger ? a : b; diff --git a/ts/test-node/reactions/util_test.std.ts b/ts/test-node/reactions/util_test.std.ts index 233e225259..96ff5f48ac 100644 --- a/ts/test-node/reactions/util_test.std.ts +++ b/ts/test-node/reactions/util_test.std.ts @@ -44,7 +44,7 @@ describe('reaction utilities', () => { { ...rxn('💬'), fromId: uuid() }, { ...rxn('🥀', { isPending: true }), timestamp: 1 }, { ...rxn('🌹', { isPending: true }), timestamp: 2 }, - ]; + ] as const; const reaction = rxn('😀'); const newReactions = addOutgoingReaction(oldReactions, reaction); assert.deepStrictEqual(newReactions, [oldReactions[1], reaction]); diff --git a/ts/test-node/sql/migration_1000_test.node.ts b/ts/test-node/sql/migration_1000_test.node.ts index 9d4fffbb9d..a741c3386b 100644 --- a/ts/test-node/sql/migration_1000_test.node.ts +++ b/ts/test-node/sql/migration_1000_test.node.ts @@ -134,12 +134,12 @@ describe('SQL/updateToSchemaVersion1000', () => { assert.strictEqual(messages.length, 2); - assert.strictEqual(messages[0].message.readStatus, ReadStatus.Read); + assert.strictEqual(messages[0]?.message.readStatus, ReadStatus.Read); assert.strictEqual(messages[0].message.seenStatus, SeenStatus.Unseen); assert.strictEqual(messages[0].readStatus, ReadStatus.Read); assert.strictEqual(messages[0].seenStatus, SeenStatus.Unseen); - assert.strictEqual(messages[1].message.readStatus, ReadStatus.Read); + assert.strictEqual(messages[1]?.message.readStatus, ReadStatus.Read); assert.strictEqual(messages[1].message.seenStatus, SeenStatus.Unseen); assert.strictEqual(messages[1].readStatus, ReadStatus.Read); assert.strictEqual(messages[1].seenStatus, SeenStatus.Unseen); @@ -174,12 +174,12 @@ describe('SQL/updateToSchemaVersion1000', () => { assert.strictEqual(messages.length, 2); - assert.strictEqual(messages[0].message.readStatus, ReadStatus.Read); + assert.strictEqual(messages[0]?.message.readStatus, ReadStatus.Read); assert.strictEqual(messages[0].message.seenStatus, SeenStatus.Seen); assert.strictEqual(messages[0].readStatus, ReadStatus.Read); assert.strictEqual(messages[0].seenStatus, SeenStatus.Seen); - assert.strictEqual(messages[1].message.readStatus, ReadStatus.Read); + assert.strictEqual(messages[1]?.message.readStatus, ReadStatus.Read); assert.strictEqual(messages[1].message.seenStatus, SeenStatus.Seen); assert.strictEqual(messages[1].readStatus, ReadStatus.Read); assert.strictEqual(messages[1].seenStatus, SeenStatus.Seen); diff --git a/ts/test-node/sql/migration_1040_test.node.ts b/ts/test-node/sql/migration_1040_test.node.ts index a18f67f87d..3250a43578 100644 --- a/ts/test-node/sql/migration_1040_test.node.ts +++ b/ts/test-node/sql/migration_1040_test.node.ts @@ -201,7 +201,7 @@ describe('SQL/updateToSchemaVersion1040', () => { const attachments = getAttachmentDownloadJobs(db); assert.strictEqual(attachments.length, 1); - assert.strictEqual(attachments[0].attempts, 0); + assert.strictEqual(attachments[0]?.attempts, 0); }); it('uses indices searching for next job', () => { diff --git a/ts/test-node/sql/migration_1060_test.node.ts b/ts/test-node/sql/migration_1060_test.node.ts index b588ebb8d7..36e509794f 100644 --- a/ts/test-node/sql/migration_1060_test.node.ts +++ b/ts/test-node/sql/migration_1060_test.node.ts @@ -204,7 +204,7 @@ describe('SQL/updateToSchemaVersion1060', () => { describe('Sync Tasks', () => { it('creates tasks in bulk, and fetches all', () => { const now = Date.now(); - const expected: Array = [ + const expected = [ { id: generateGuid(), attempts: 1, @@ -241,7 +241,7 @@ describe('SQL/updateToSchemaVersion1060', () => { sentAt: 3, type: 'delete-conversation', }, - ]; + ] as const satisfies Array; saveSyncTasks(db, expected); diff --git a/ts/test-node/sql/migration_1200_test.node.ts b/ts/test-node/sql/migration_1200_test.node.ts index acaec9c4f6..f5dbac0819 100644 --- a/ts/test-node/sql/migration_1200_test.node.ts +++ b/ts/test-node/sql/migration_1200_test.node.ts @@ -172,7 +172,7 @@ describe('SQL/updateToSchemaVersion1200', () => { const [query, params] = template; const result = db.prepare(query).all(params); assert.strictEqual(result.length, 1); - assert.deepStrictEqual(result[0].messageId, 'message12'); + assert.deepStrictEqual(result[0]?.messageId, 'message12'); const details = explain(db, template); assert.equal( details, diff --git a/ts/test-node/sql/migration_89_test.preload.ts b/ts/test-node/sql/migration_89_test.preload.ts index 5ad2e29e29..2abb347b48 100644 --- a/ts/test-node/sql/migration_89_test.preload.ts +++ b/ts/test-node/sql/migration_89_test.preload.ts @@ -194,8 +194,8 @@ describe('SQL/updateToSchemaVersion89', () => { const callHistory = getAllCallHistory(); assert.strictEqual(callHistory.length, 2); - assert.strictEqual(callHistory[0].callId, callId1); - assert.strictEqual(callHistory[1].callId, getCallIdFromEra(eraId2)); + assert.strictEqual(callHistory[0]?.callId, callId1); + assert.strictEqual(callHistory[1]?.callId, getCallIdFromEra(eraId2)); }); it('migrates older messages without a callId', () => { @@ -215,7 +215,7 @@ describe('SQL/updateToSchemaVersion89', () => { const callHistory = getAllCallHistory(); assert.strictEqual(callHistory.length, 1); - assert.isTrue(isValidUuid(callHistory[0].callId)); + assert.isTrue(isValidUuid(callHistory[0]?.callId)); }); it('migrates older messages without a callMode', () => { @@ -245,8 +245,8 @@ describe('SQL/updateToSchemaVersion89', () => { const callHistory = getAllCallHistory(); assert.strictEqual(callHistory.length, 2); - assert.strictEqual(callHistory[0].mode, CallMode.Direct); - assert.strictEqual(callHistory[1].mode, CallMode.Group); + assert.strictEqual(callHistory[0]?.mode, CallMode.Direct); + assert.strictEqual(callHistory[1]?.mode, CallMode.Group); }); it('handles unique constraint violations', () => { @@ -298,8 +298,8 @@ describe('SQL/updateToSchemaVersion89', () => { const callHistory = getAllCallHistory(); assert.strictEqual(callHistory.length, 2); - assert.strictEqual(callHistory[0].peerId, conversation1.serviceId); - assert.strictEqual(callHistory[1].peerId, conversation2.groupId); + assert.strictEqual(callHistory[0]?.peerId, conversation1.serviceId); + assert.strictEqual(callHistory[1]?.peerId, conversation2.groupId); }); it('migrates older unregistered conversations with no serviceId', () => { @@ -318,7 +318,7 @@ describe('SQL/updateToSchemaVersion89', () => { const callHistory = getAllCallHistory(); assert.strictEqual(callHistory.length, 1); - assert.strictEqual(callHistory[0].peerId, conversation.id); + assert.strictEqual(callHistory[0]?.peerId, conversation.id); }); it('migrates call-history messages with no timestamp', () => { @@ -374,14 +374,14 @@ describe('SQL/updateToSchemaVersion89', () => { const callHistory = getAllCallHistory(); assert.strictEqual(callHistory.length, 8); - assert.strictEqual(callHistory[0].timestamp, 0); - assert.strictEqual(callHistory[1].timestamp, 0); - assert.strictEqual(callHistory[2].timestamp, 1); - assert.strictEqual(callHistory[3].timestamp, 1); - assert.strictEqual(callHistory[4].timestamp, 2); - assert.strictEqual(callHistory[5].timestamp, 2); - assert.strictEqual(callHistory[6].timestamp, 3); - assert.strictEqual(callHistory[7].timestamp, 3); + assert.strictEqual(callHistory[0]?.timestamp, 0); + assert.strictEqual(callHistory[1]?.timestamp, 0); + assert.strictEqual(callHistory[2]?.timestamp, 1); + assert.strictEqual(callHistory[3]?.timestamp, 1); + assert.strictEqual(callHistory[4]?.timestamp, 2); + assert.strictEqual(callHistory[5]?.timestamp, 2); + assert.strictEqual(callHistory[6]?.timestamp, 3); + assert.strictEqual(callHistory[7]?.timestamp, 3); }); describe('clients with schema version 87', () => { @@ -482,8 +482,8 @@ describe('SQL/updateToSchemaVersion89', () => { const callHistory = getAllCallHistory(); assert.strictEqual(callHistory.length, 2); - assert.strictEqual(callHistory[0].peerId, conversation1.serviceId); - assert.strictEqual(callHistory[1].peerId, conversation2.groupId); + assert.strictEqual(callHistory[0]?.peerId, conversation1.serviceId); + assert.strictEqual(callHistory[1]?.peerId, conversation2.groupId); }); it('migrates duplicate call history where the first was already migrated', () => { @@ -522,12 +522,12 @@ describe('SQL/updateToSchemaVersion89', () => { const callHistory = getAllCallHistory(); assert.strictEqual(callHistory.length, 1); - assert.strictEqual(callHistory[0].status, DirectCallStatus.Declined); + assert.strictEqual(callHistory[0]?.status, DirectCallStatus.Declined); const messages = getMessages(); assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].type, 'call-history'); - assert.strictEqual(messages[0].callId, '123'); + assert.strictEqual(messages[0]?.type, 'call-history'); + assert.strictEqual(messages[0]?.callId, '123'); assert.notProperty(messages[0], 'callHistoryDetails'); }); }); diff --git a/ts/test-node/sql/migrations_test.node.ts b/ts/test-node/sql/migrations_test.node.ts index eb306dae6d..3dd606ad23 100644 --- a/ts/test-node/sql/migrations_test.node.ts +++ b/ts/test-node/sql/migrations_test.node.ts @@ -3371,35 +3371,22 @@ describe('SQL migrations test', () => { return json; } - function addMessages( - messages: Array<{ - mentions?: Array; - boldRanges?: Array>; - }> - ) { - const formattedMessages = messages.map(composeMessage); + function addMessage(message: { + mentions?: Array; + boldRanges?: Array>; + }) { + const formattedMessage = composeMessage(message); db.exec( ` INSERT INTO messages (id, json) VALUES - ${formattedMessages - .map(message => `('${message.id}', '${objectToJSON(message)}')`) - .join(', ')}; + ('${formattedMessage.id}', '${objectToJSON(formattedMessage)}') ` ); - assert.equal( - db - .prepare('SELECT COUNT(*) FROM messages;', { - pluck: true, - }) - .get(), - messages.length - ); - - return { formattedMessages }; + return formattedMessage; } function getMentions() { @@ -3408,17 +3395,24 @@ describe('SQL migrations test', () => { .all(); } + const userIds = [ + generateAci(), + generateAci(), + generateAci(), + generateAci(), + generateAci(), + ] as const; + it('Creates and populates the mentions table with existing mentions', () => { updateToVersion(db, schemaVersion - 1); - const userIds = new Array(5).fill(undefined).map(() => generateAci()); - const { formattedMessages } = addMessages([ - { mentions: [userIds[0]] }, - { mentions: [userIds[1]], boldRanges: [[1, 1]] }, - { mentions: [userIds[1], userIds[2]] }, - {}, - { boldRanges: [[1, 4]] }, - ]); + const formattedMessages = [ + addMessage({ mentions: [userIds[0]] }), + addMessage({ mentions: [userIds[1]], boldRanges: [[1, 1]] }), + addMessage({ mentions: [userIds[1], userIds[2]] }), + addMessage({}), + addMessage({ boldRanges: [[1, 4]] }), + ] as const; // now create mentions table updateToVersion(db, schemaVersion); @@ -3467,14 +3461,13 @@ describe('SQL migrations test', () => { 0 ); - const userIds = new Array(5).fill(undefined).map(() => generateAci()); - const { formattedMessages } = addMessages([ - { mentions: [userIds[0]] }, - { mentions: [userIds[1]], boldRanges: [[1, 1]] }, - { mentions: [userIds[1], userIds[2]] }, - {}, - { boldRanges: [[1, 4]] }, - ]); + const formattedMessages = [ + addMessage({ mentions: [userIds[0]] }), + addMessage({ mentions: [userIds[1]], boldRanges: [[1, 1]] }), + addMessage({ mentions: [userIds[1], userIds[2]] }), + addMessage({}), + addMessage({ boldRanges: [[1, 4]] }), + ] as const; // the 4 mentions should be included, with multiple rows for multiple mentions in a // message @@ -3519,11 +3512,13 @@ describe('SQL migrations test', () => { 0 ); - const userIds = new Array(5).fill(undefined).map(() => generateAci()); - const { formattedMessages } = addMessages([ - { mentions: [userIds[0]] }, - { mentions: [userIds[1], userIds[2]], boldRanges: [[1, 1]] }, - ]); + const formattedMessages = [ + addMessage({ mentions: [userIds[0]] }), + addMessage({ + mentions: [userIds[1], userIds[2]], + boldRanges: [[1, 1]], + }), + ] as const; assert.equal(getMentions().length, 3); @@ -3553,8 +3548,9 @@ describe('SQL migrations test', () => { 0 ); - const userIds = new Array(5).fill(undefined).map(() => generateAci()); - const { formattedMessages } = addMessages([{ mentions: [userIds[0]] }]); + const formattedMessages = [ + addMessage({ mentions: [userIds[0]] }), + ] as const; assert.equal(getMentions().length, 1); diff --git a/ts/test-node/state/selectors/conversations_test.preload.ts b/ts/test-node/state/selectors/conversations_test.preload.ts index 95ac938d70..e20b1e9007 100644 --- a/ts/test-node/state/selectors/conversations_test.preload.ts +++ b/ts/test-node/state/selectors/conversations_test.preload.ts @@ -1269,11 +1269,11 @@ describe('both/state/selectors/conversations-extra', () => { stableSelectedConversationIdInChatFolder: null, }); - assert.strictEqual(conversations[0].name, 'First!'); - assert.strictEqual(conversations[1].name, 'Á'); - assert.strictEqual(conversations[2].name, 'B'); - assert.strictEqual(conversations[3].name, 'C'); - assert.strictEqual(conversations[4].name, 'No timestamp'); + assert.strictEqual(conversations[0]?.name, 'First!'); + assert.strictEqual(conversations[1]?.name, 'Á'); + assert.strictEqual(conversations[2]?.name, 'B'); + assert.strictEqual(conversations[3]?.name, 'C'); + assert.strictEqual(conversations[4]?.name, 'No timestamp'); assert.strictEqual(conversations.length, 5); assert.strictEqual(archivedConversations.length, 0); @@ -1364,9 +1364,9 @@ describe('both/state/selectors/conversations-extra', () => { stableSelectedConversationIdInChatFolder: null, }); - assert.strictEqual(pinnedConversations[0].name, 'Pin One'); - assert.strictEqual(pinnedConversations[1].name, 'Pin Two'); - assert.strictEqual(pinnedConversations[2].name, 'Pin Three'); + assert.strictEqual(pinnedConversations[0]?.name, 'Pin One'); + assert.strictEqual(pinnedConversations[1]?.name, 'Pin Two'); + assert.strictEqual(pinnedConversations[2]?.name, 'Pin Three'); assert.strictEqual(archivedConversations.length, 0); @@ -1495,12 +1495,12 @@ describe('both/state/selectors/conversations-extra', () => { stableSelectedConversationIdInChatFolder: null, }); - assert.strictEqual(pinnedConversations[0].name, 'Pin One'); - assert.strictEqual(pinnedConversations[1].name, 'Pin Two'); - assert.strictEqual(pinnedConversations[2].name, 'Pin Three'); + assert.strictEqual(pinnedConversations[0]?.name, 'Pin One'); + assert.strictEqual(pinnedConversations[1]?.name, 'Pin Two'); + assert.strictEqual(pinnedConversations[2]?.name, 'Pin Three'); assert.strictEqual(pinnedConversations.length, 3); - assert.strictEqual(archivedConversations[0].name, 'Pin Four'); + assert.strictEqual(archivedConversations[0]?.name, 'Pin Four'); assert.strictEqual(archivedConversations.length, 1); assert.strictEqual(conversations.length, 0); @@ -1685,7 +1685,7 @@ describe('both/state/selectors/conversations-extra', () => { role: 0, joinedAtVersion: 0, }, - ]; + ] as const; const group: ConversationType = { ...makeGroup('group'), membersV2, diff --git a/ts/test-node/state/selectors/search_test.preload.ts b/ts/test-node/state/selectors/search_test.preload.ts index efcd23133b..4d0d783a08 100644 --- a/ts/test-node/state/selectors/search_test.preload.ts +++ b/ts/test-node/state/selectors/search_test.preload.ts @@ -457,10 +457,10 @@ describe('both/state/selectors/search', () => { }); it('adds isSelected flag to conversations when filterByUnread is true', () => { - const conversations: Array = [ + const conversations = [ getDefaultConversation({ id: '1' }), getDefaultConversation({ id: 'selected-id' }), - ]; + ] as const; const state: StateType = { ...getEmptyRootState(), diff --git a/ts/test-node/types/BodyRange_test.std.ts b/ts/test-node/types/BodyRange_test.std.ts index 979fae9e91..4fd8c48f48 100644 --- a/ts/test-node/types/BodyRange_test.std.ts +++ b/ts/test-node/types/BodyRange_test.std.ts @@ -860,7 +860,7 @@ describe('BodyRanges', () => { replacementText: 'Eve', conversationID: 'x', }, - ]; + ] as const; const { cleanedSnippet, bodyRanges: processedBodyRanges } = processBodyRangesForSearchResult({ snippet: diff --git a/ts/test-node/types/EmbeddedContact_test.std.ts b/ts/test-node/types/EmbeddedContact_test.std.ts index 3835b86158..8da9eb22a2 100644 --- a/ts/test-node/types/EmbeddedContact_test.std.ts +++ b/ts/test-node/types/EmbeddedContact_test.std.ts @@ -271,7 +271,7 @@ describe('Contact', () => { ], }, ], - }; + } as const; const result = await upgradeVersion( message.contact[0], defaultContext, @@ -301,7 +301,7 @@ describe('Contact', () => { ], }, ], - }; + } as const; const expected = { name: { nickname: 'Someone Somewhere', @@ -348,7 +348,7 @@ describe('Contact', () => { } as unknown as Avatar, }, ], - }; + } as const; const expected = { name: { nickname: 'Someone Somewhere', @@ -411,7 +411,7 @@ describe('Contact', () => { } as unknown as Avatar, }, ], - }; + } as const; const expected = { name: { nickname: 'Someone Somewhere', @@ -474,11 +474,11 @@ describe('Contact', () => { { type: 0, value: 'someone@somewhere.com', - }, + } as unknown as Email, ], }, ], - }; + } as const; const expected = { name: { nickname: 'Someone Somewhere', @@ -524,7 +524,7 @@ describe('Contact', () => { ], }, ], - }; + } as const; const expected = { name: { nickname: 'Someone Somewhere', @@ -572,7 +572,7 @@ describe('Contact', () => { ], }, ], - }; + } as const; const expected = { name: { nickname: 'Someone Somewhere', @@ -605,7 +605,7 @@ describe('Contact', () => { ], }, ], - }; + } as const; const result = await upgradeVersion( message.contact[0], defaultContext, diff --git a/ts/test-node/types/NotificationProfile_test.node.ts b/ts/test-node/types/NotificationProfile_test.node.ts index 5990735df4..13ab3d5770 100644 --- a/ts/test-node/types/NotificationProfile_test.node.ts +++ b/ts/test-node/types/NotificationProfile_test.node.ts @@ -1036,9 +1036,9 @@ describe('NotificationProfile', () => { const starting = [middle, old, newest]; const actual = sortProfiles(starting); - assert.strictEqual(actual[0].name, 'newest'); - assert.strictEqual(actual[1].name, 'middle'); - assert.strictEqual(actual[2].name, 'old'); + assert.strictEqual(actual[0]?.name, 'newest'); + assert.strictEqual(actual[1]?.name, 'middle'); + assert.strictEqual(actual[2]?.name, 'old'); }); }); diff --git a/ts/test-node/updater/differential_test.node.ts b/ts/test-node/updater/differential_test.node.ts index 9ff53b1933..99e24a50ac 100644 --- a/ts/test-node/updater/differential_test.node.ts +++ b/ts/test-node/updater/differential_test.node.ts @@ -97,7 +97,9 @@ describe('updater/differential', () => { const fullFile = await fs.readFile(path.join(FIXTURES, file)); - const rangeHeader = req.headers.range?.match(/^bytes=([\d,\s-]+)$/); + const rangeHeader = req.headers.range?.match(/^bytes=([\d,\s-]+)$/) as + | (RegExpMatchArray & { 1: string }) + | null; if (!rangeHeader) { res.writeHead(200); res.end(fullFile); @@ -105,10 +107,12 @@ describe('updater/differential', () => { } const ranges = rangeHeader[1].split(/\s*,\s*/g).map(value => { - const range = value.match(/^(\d+)-(\d+)$/); + const range = value.match(/^(\d+)-(\d+)$/) as + | (RegExpMatchArray & { 1: string; 2: string }) + | null; strictAssert(range, `Invalid header: ${rangeHeader}`); - return [parseInt(range[1], 10), parseInt(range[2], 10)]; + return [parseInt(range[1], 10), parseInt(range[2], 10)] as const; }); if (ranges.length === 1) { @@ -120,7 +124,9 @@ describe('updater/differential', () => { return; } - const [from, to] = ranges[0]; + const range = ranges[0]; + strictAssert(range, 'Missing range'); + const [from, to] = range; res.end(fullFile.slice(from, to + 1)); return; } diff --git a/ts/test-node/updater/signature_test.main.ts b/ts/test-node/updater/signature_test.main.ts index 0a1d4dbbf2..26b1e04740 100644 --- a/ts/test-node/updater/signature_test.main.ts +++ b/ts/test-node/updater/signature_test.main.ts @@ -154,7 +154,8 @@ describe('updater/signatures', () => { version, privateKeyPath ); - signature[4] += 3; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + signature[4]! += 3; const verified = await verifySignature( updatePath, diff --git a/ts/test-node/util/getFontNameByTextScript_test.dom.ts b/ts/test-node/util/getFontNameByTextScript_test.dom.ts index a7792a6419..6893bad60e 100644 --- a/ts/test-node/util/getFontNameByTextScript_test.dom.ts +++ b/ts/test-node/util/getFontNameByTextScript_test.dom.ts @@ -64,10 +64,12 @@ describe('getFontNameByTextScript', () => { const text = 'abc'; assert.throws(() => { + // @ts-expect-error invalid text style getFontNameByTextScript(text, -1); }); assert.throws(() => { + // @ts-expect-error invalid text style getFontNameByTextScript(text, 99); }); }); diff --git a/ts/test-node/util/iterables_test.std.ts b/ts/test-node/util/iterables_test.std.ts index 90beb6c89d..5bc8777c77 100644 --- a/ts/test-node/util/iterables_test.std.ts +++ b/ts/test-node/util/iterables_test.std.ts @@ -334,8 +334,8 @@ describe('iterable utilities', () => { it('returns a map of groups', () => { assert.deepEqual( groupBy( - ['apple', 'aardvark', 'orange', 'orange', 'zebra'], - str => str[0] + ['apple', 'aardvark', 'orange', 'orange', 'zebra'] as const, + str => str[0] ?? '' ), { a: ['apple', 'aardvark'], diff --git a/ts/test-node/util/logPadding_test.node.ts b/ts/test-node/util/logPadding_test.node.ts index 1c071e2bac..df710152c6 100644 --- a/ts/test-node/util/logPadding_test.node.ts +++ b/ts/test-node/util/logPadding_test.node.ts @@ -35,12 +35,13 @@ const BUCKET_SIZES = [ 40453710, 42476396, 44600216, 46830227, 49171738, 51630325, 54211841, 56922433, 59768555, 62756983, 65894832, 69189573, 72649052, 76281505, 80095580, 84100359, 88305377, 92720646, 97356678, 102224512, 107335738, -]; +] as const; describe('logPadSize', () => { it('properly calculates first bucket', () => { - for (let size = 0, max = BUCKET_SIZES[0]; size < max; size += 1) { - assert.strictEqual(BUCKET_SIZES[0], logPadSize(size)); + const max = BUCKET_SIZES[0]; + for (let size = 0; size < max; size += 1) { + assert.strictEqual(max, logPadSize(size)); } }); @@ -48,29 +49,27 @@ describe('logPadSize', () => { let count = 0; const failures = new Array(); - for (let i = 0, max = BUCKET_SIZES.length - 1; i < max; i += 1) { + for (const [i, bucketSize] of BUCKET_SIZES.slice(0, -1).entries()) { // Exact - if (BUCKET_SIZES[i] !== logPadSize(BUCKET_SIZES[i])) { + if (bucketSize !== logPadSize(bucketSize)) { count += 1; - failures.push( - `${BUCKET_SIZES[i]} does not equal ${logPadSize(BUCKET_SIZES[i])}` - ); + failures.push(`${bucketSize} does not equal ${logPadSize(bucketSize)}`); } // Just under - if (BUCKET_SIZES[i] !== logPadSize(BUCKET_SIZES[i] - 1)) { + if (bucketSize !== logPadSize(bucketSize - 1)) { count += 1; failures.push( - `${BUCKET_SIZES[i]} does not equal ${logPadSize(BUCKET_SIZES[i] - 1)}` + `${bucketSize} does not equal ${logPadSize(bucketSize - 1)}` ); } // Just over - if (BUCKET_SIZES[i + 1] !== logPadSize(BUCKET_SIZES[i] + 1)) { + if (BUCKET_SIZES[i + 1] !== logPadSize(bucketSize + 1)) { count += 1; failures.push( `${BUCKET_SIZES[i + 1]} does not equal ` + - `${logPadSize(BUCKET_SIZES[i] + 1)}` + `${logPadSize(bucketSize + 1)}` ); } } diff --git a/ts/test-node/util/queueAttachmentDownloads_test.preload.ts b/ts/test-node/util/queueAttachmentDownloads_test.preload.ts index 595ba6f8d1..9d425d2597 100644 --- a/ts/test-node/util/queueAttachmentDownloads_test.preload.ts +++ b/ts/test-node/util/queueAttachmentDownloads_test.preload.ts @@ -1,9 +1,11 @@ // Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -/* eslint-disable @typescript-eslint/no-non-null-assertion */ import { assert } from 'chai'; -import type { MessageAttributesType } from '../../model-types.d.ts'; +import type { + EditHistoryType, + MessageAttributesType, +} from '../../model-types.d.ts'; import type { AttachmentType } from '../../types/Attachment.std.js'; import { IMAGE_JPEG, LONG_MESSAGE } from '../../types/MIME.std.js'; import { generateMessageId } from '../../util/generateMessageId.node.js'; @@ -84,80 +86,78 @@ describe('ensureBodyAttachmentsAreSeparated', () => { assert.deepEqual(result.bodyAttachment, msg.bodyAttachment); }); it('separates first body attachment out for all editHistory', () => { + const normalAttachment = composeAttachment({ + clientUuid: 'normal attachment', + contentType: IMAGE_JPEG, + }); + + const longMessageAttachment1 = composeAttachment({ + clientUuid: 'long message attachment 1', + contentType: LONG_MESSAGE, + }); + + const longMessageAttachment2 = composeAttachment({ + clientUuid: 'long message attachment 2', + contentType: LONG_MESSAGE, + }); + + const editAttachment1 = composeAttachment({ + clientUuid: 'edit attachment 1', + contentType: IMAGE_JPEG, + }); + + const editAttachment2 = composeAttachment({ + clientUuid: 'edit attachment 2', + contentType: IMAGE_JPEG, + }); + + const bodyAttachment = composeAttachment({ + clientUuid: 'long message attachment already as bodyattachment', + contentType: LONG_MESSAGE, + }); + + const edit1: EditHistoryType = { + timestamp: Date.now(), + received_at: Date.now(), + attachments: [ + editAttachment1, + longMessageAttachment1, + longMessageAttachment2, + ], + }; + + const edit2: EditHistoryType = { + timestamp: Date.now(), + received_at: Date.now(), + bodyAttachment, + attachments: [editAttachment1, editAttachment2], + }; + const msg = composeMessage({ attachments: [ - composeAttachment({ - clientUuid: 'normal attachment', - contentType: IMAGE_JPEG, - }), - composeAttachment({ - clientUuid: 'long message attachment 1', - contentType: LONG_MESSAGE, - }), - composeAttachment({ - clientUuid: 'long message attachment 2', - contentType: LONG_MESSAGE, - }), - ], - editHistory: [ - { - timestamp: Date.now(), - received_at: Date.now(), - attachments: [ - composeAttachment({ - clientUuid: 'edit attachment', - contentType: IMAGE_JPEG, - }), - composeAttachment({ - clientUuid: 'long message attachment 1', - contentType: LONG_MESSAGE, - }), - composeAttachment({ - clientUuid: 'long message attachment 2', - contentType: LONG_MESSAGE, - }), - ], - }, - { - timestamp: Date.now(), - received_at: Date.now(), - bodyAttachment: composeAttachment({ - clientUuid: 'long message attachment already as bodyattachment', - contentType: LONG_MESSAGE, - }), - attachments: [ - composeAttachment({ - clientUuid: 'edit attachment 1', - contentType: IMAGE_JPEG, - }), - composeAttachment({ - clientUuid: 'edit attachment 2', - contentType: IMAGE_JPEG, - }), - ], - }, + normalAttachment, + longMessageAttachment1, + longMessageAttachment2, ], + editHistory: [edit1, edit2], }); const result = ensureBodyAttachmentsAreSeparated(msg, { logId: 'test', logger, }); - assert.deepEqual(result.attachments, [msg.attachments?.[0]]); - assert.deepEqual(result.bodyAttachment, msg.attachments?.[1]); + assert.deepEqual(result.attachments, [normalAttachment]); + assert.deepEqual(result.bodyAttachment, longMessageAttachment1); assert.deepEqual(result.editHistory, [ { - ...msg.editHistory![0], - attachments: [msg.editHistory![0].attachments![0]], - bodyAttachment: msg.editHistory![0].attachments![1], + ...edit1, + attachments: [editAttachment1], + bodyAttachment: longMessageAttachment1, }, { - ...msg.editHistory![1], - attachments: [ - msg.editHistory![1].attachments![0], - msg.editHistory![1].attachments![1], - ], - bodyAttachment: msg.editHistory![1].bodyAttachment, + ...edit2, + attachments: [editAttachment1, editAttachment2], + bodyAttachment, }, ]); }); diff --git a/ts/textsecure/Errors.std.ts b/ts/textsecure/Errors.std.ts index 64c052eae2..e7480a5719 100644 --- a/ts/textsecure/Errors.std.ts +++ b/ts/textsecure/Errors.std.ts @@ -46,7 +46,7 @@ export class OutgoingIdentityKeyError extends ReplayableError { // Note: Data to resend message is no longer captured constructor(incomingIdentifier: string, cause?: LibSignalErrorBase) { - const identifier = incomingIdentifier.split('.')[0]; + const [identifier] = incomingIdentifier.split('.'); super({ name: 'OutgoingIdentityKeyError', @@ -70,7 +70,7 @@ export class OutgoingMessageError extends ReplayableError { _t: unknown, httpError?: HTTPError ) { - const identifier = incomingIdentifier.split('.')[0]; + const [identifier] = incomingIdentifier.split('.', 1); super({ name: 'OutgoingMessageError', @@ -101,7 +101,8 @@ export class SendMessageNetworkError extends ReplayableError { message: httpError.message, }); - [this.identifier] = identifier.split('.'); + const [id] = identifier.split('.', 1); + this.identifier = id; this.httpError = httpError; appendStack(this, httpError); @@ -137,7 +138,8 @@ export class SendMessageChallengeError extends ReplayableError { cause: httpError, }); - [this.identifier] = identifier.split('.'); + const [id] = identifier.split('.', 1); + this.identifier = id; this.httpError = httpError; this.data = httpError.response as SendMessageChallengeData; diff --git a/ts/textsecure/EventTarget.std.ts b/ts/textsecure/EventTarget.std.ts index b8d5d18a61..05acf3dee4 100644 --- a/ts/textsecure/EventTarget.std.ts +++ b/ts/textsecure/EventTarget.std.ts @@ -73,8 +73,8 @@ export default class EventTarget { return; } } + this.listeners[eventName] = listeners; } - this.listeners[eventName] = listeners; } extend(source: any): any { diff --git a/ts/textsecure/Helpers.std.ts b/ts/textsecure/Helpers.std.ts index 914c4c875b..f981a52ac7 100644 --- a/ts/textsecure/Helpers.std.ts +++ b/ts/textsecure/Helpers.std.ts @@ -69,7 +69,8 @@ const utils = { number[0] === '+' && /^[0-9]+$/.test(number.substring(1)), // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types jsonThing: (thing: unknown) => JSON.stringify(ensureStringed(thing)), - unencodeNumber: (number: string): Array => number.split('.'), + unencodeNumber: (number: string): [string, ...Array] => + number.split('.'), }; export default utils; diff --git a/ts/textsecure/MessageReceiver.preload.ts b/ts/textsecure/MessageReceiver.preload.ts index 2435d6b8e2..3a935ec37a 100644 --- a/ts/textsecure/MessageReceiver.preload.ts +++ b/ts/textsecure/MessageReceiver.preload.ts @@ -848,10 +848,9 @@ export default class MessageReceiver } for await (const batch of this.#getAllFromCache()) { - const max = batch.length; - for (let i = 0; i < max; i += 1) { + for (const item of batch) { // eslint-disable-next-line no-await-in-loop - await this.#queueCached(batch[i]); + await this.#queueCached(item); } } log.info('queueAllCached - finished'); diff --git a/ts/textsecure/Provisioner.preload.ts b/ts/textsecure/Provisioner.preload.ts index 587abb92c3..e614a6715e 100644 --- a/ts/textsecure/Provisioner.preload.ts +++ b/ts/textsecure/Provisioner.preload.ts @@ -99,7 +99,12 @@ const MAX_ROTATIONS = 6; const TIMEOUT_ERROR = new PTimeoutError(); -const QR_CODE_TIMEOUTS = [10 * SECOND, 20 * SECOND, 30 * SECOND, 60 * SECOND]; +const QR_CODE_TIMEOUTS = [ + 10 * SECOND, + 20 * SECOND, + 30 * SECOND, + 60 * SECOND, +] as const; export class Provisioner { readonly #subscribers = new Set(); @@ -242,7 +247,8 @@ export class Provisioner { let delay: number; try { - const sleepMs = QR_CODE_TIMEOUTS[this.#attemptCount]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const sleepMs = QR_CODE_TIMEOUTS[this.#attemptCount]!; // eslint-disable-next-line no-await-in-loop await this.#connect(signal, sleepMs); diff --git a/ts/textsecure/SendMessage.preload.ts b/ts/textsecure/SendMessage.preload.ts index b8871c21e8..fd68d379c5 100644 --- a/ts/textsecure/SendMessage.preload.ts +++ b/ts/textsecure/SendMessage.preload.ts @@ -778,7 +778,8 @@ export class MessageSender { static getRandomPadding(): Uint8Array { // Generate a random int from 1 and 512 const buffer = getRandomBytes(2); - const paddingLength = (new Uint16Array(buffer)[0] & 0x1ff) + 1; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const paddingLength = (new Uint16Array(buffer)[0]! & 0x1ff) + 1; // Generate a random padding buffer of the chosen size return getRandomBytes(paddingLength); @@ -2174,7 +2175,8 @@ export class MessageSender { `syncViewOnceOpen: ${viewOnceOpens.length} opens provided. Can only handle one.` ); } - const { senderAci, timestamp } = viewOnceOpens[0]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const { senderAci, timestamp } = viewOnceOpens[0]!; if (!senderAci) { throw new Error('syncViewOnceOpen: Missing senderAci'); diff --git a/ts/textsecure/WebAPI.preload.ts b/ts/textsecure/WebAPI.preload.ts index 90557a3116..9917b170e3 100644 --- a/ts/textsecure/WebAPI.preload.ts +++ b/ts/textsecure/WebAPI.preload.ts @@ -379,7 +379,7 @@ async function getFetchOptions( }; } const agentEntry = agents[cacheKey]; - const agent = agentEntry?.agent ?? null; + const agent = agentEntry?.agent ?? undefined; const fetchOptions: FetchOptionsType = { method: options.type, @@ -729,12 +729,11 @@ function makeHTTPError( export function makeKeysLowercase( headers: Record ): Record { - const keys = Object.keys(headers); const lowerCase: Record = Object.create(null); - keys.forEach(key => { - lowerCase[key.toLowerCase()] = headers[key]; - }); + for (const [key, value] of Object.entries(headers)) { + lowerCase[key.toLowerCase()] = value; + } return lowerCase; } @@ -3877,7 +3876,9 @@ export async function putStickers( }); await Promise.all( stickers.map(async (sticker: ServerV2AttachmentType, index: number) => { - const stickerParams = makePutParams(sticker, encryptedStickers[index]); + const encryptedSticker = encryptedStickers[index]; + strictAssert(encryptedSticker, 'Missing encryptedSticker'); + const stickerParams = makePutParams(sticker, encryptedSticker); await queue.add(async () => _outerAjax(`${cdnUrlObject['0']}/`, { ...stickerParams, @@ -4044,7 +4045,8 @@ async function _getAttachment({ const match = PARSE_RANGE_HEADER.exec(range); strictAssert(match != null, 'Attachment Content-Range is invalid'); - const maybeSize = safeParseNumber(match[1]); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const maybeSize = safeParseNumber(match[1]!); strictAssert( maybeSize != null, 'Attachment Content-Range[1] is not a number' @@ -4193,7 +4195,8 @@ export async function putEncryptedAttachment( if (range != null) { const match = range.match(/^bytes=0-(\d+)$/); strictAssert(match != null, `Invalid range header: ${range}`); - start = parseInt(match[1], 10); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + start = parseInt(match[1]!, 10); } else { log.warn(`${logId}: No range header`); } @@ -4753,9 +4756,12 @@ export async function getGroupLog( const range = response.headers.get('Content-Range'); const match = PARSE_GROUP_LOG_RANGE_HEADER.exec(range || ''); - const start = match ? parseInt(match[1], 10) : undefined; - const end = match ? parseInt(match[2], 10) : undefined; - const currentRevision = match ? parseInt(match[3], 10) : undefined; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const start = match ? parseInt(match[1]!, 10) : undefined; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const end = match ? parseInt(match[2]!, 10) : undefined; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const currentRevision = match ? parseInt(match[3]!, 10) : undefined; if ( match && diff --git a/ts/types/AttachmentSize.std.ts b/ts/types/AttachmentSize.std.ts index d1ab1ee80e..6b61cb1df2 100644 --- a/ts/types/AttachmentSize.std.ts +++ b/ts/types/AttachmentSize.std.ts @@ -75,6 +75,7 @@ export function getRenderDetailsForLimit(limitKb: number): { return { limit: Math.trunc(limit), - units: units[u], + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + units: units[u]!, }; } diff --git a/ts/types/BodyRange.std.ts b/ts/types/BodyRange.std.ts index 6db4b95725..be4562c997 100644 --- a/ts/types/BodyRange.std.ts +++ b/ts/types/BodyRange.std.ts @@ -510,6 +510,9 @@ export function processBodyRangesForSearchResult({ // To format the matches identified by FTS, we create synthetic BodyRanges to mix in // with all the other formatting embedded in this message. + type HighlightMatch = RegExpExecArray & { + indices: Record<0 | 1, [number, number]>; + }; const highlightMatches = snippet.matchAll( new RegExp( `${SNIPPET_LEFT_PLACEHOLDER}(.*?)${SNIPPET_RIGHT_PLACEHOLDER}`, @@ -520,9 +523,7 @@ export function processBodyRangesForSearchResult({ let placeholderCharsSkipped = 0; for (const highlightMatch of highlightMatches) { // TS < 5 does not have types for RegExpIndicesArray - const { indices } = highlightMatch as RegExpMatchArray & { - indices: Array>; - }; + const { indices } = highlightMatch as HighlightMatch; const [wholeMatchStartIdx] = indices[0]; const [matchedWordStartIdx, matchedWordEndIdx] = indices[1]; adjustedBodyRanges.push({ diff --git a/ts/types/Colors.std.ts b/ts/types/Colors.std.ts index e01e5532ca..9422540f4c 100644 --- a/ts/types/Colors.std.ts +++ b/ts/types/Colors.std.ts @@ -75,7 +75,7 @@ export const ContactNameColors = [ '190', '310', '110', -]; +] as const; export type ContactNameColorType = (typeof ContactNameColors)[number]; @@ -112,5 +112,6 @@ export type CustomColorsItemType = { }; export function getAvatarColor(color?: AvatarColorType): AvatarColorType { - return color || AvatarColors[0]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return color || AvatarColors[0]!; } diff --git a/ts/types/Megaphone.std.ts b/ts/types/Megaphone.std.ts index 8b7faba5a8..d623c3bb57 100644 --- a/ts/types/Megaphone.std.ts +++ b/ts/types/Megaphone.std.ts @@ -143,5 +143,6 @@ export function getMegaphoneLastSnoozeDurationMs( const { snoozeDurationDays } = getMegaphoneSnoozeConfig(megaphone); const lastSnoozeCount = Math.max(megaphone.snoozeCount - 1, 0); const snoozeIndex = Math.min(lastSnoozeCount, snoozeDurationDays.length - 1); - return snoozeDurationDays[snoozeIndex] * DAY; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return snoozeDurationDays[snoozeIndex]! * DAY; } diff --git a/ts/types/Message2.preload.ts b/ts/types/Message2.preload.ts index 65a4f862b8..7106d3c68b 100644 --- a/ts/types/Message2.preload.ts +++ b/ts/types/Message2.preload.ts @@ -66,6 +66,7 @@ import { } from '../util/migrations.preload.js'; import type { getExistingAttachmentDataForReuse } from '../util/attachments/deduplicateAttachment.preload.js'; import type { getPlaintextHashForInMemoryAttachment } from '../AttachmentCrypto.node.js'; +import { strictAssert } from '../util/assert.std.js'; const { isFunction, isObject, identity } = lodash; @@ -746,6 +747,7 @@ export const upgradeSchema = async ( const currentVersion = versions[index]; try { + strictAssert(currentVersion, 'Missing currentVersion'); // We really do want this intra-loop await because this is a chained async action, // each step dependent on the previous // eslint-disable-next-line no-await-in-loop diff --git a/ts/types/QualifiedAddress.std.ts b/ts/types/QualifiedAddress.std.ts index 5b9e1bff6f..56cf57f58a 100644 --- a/ts/types/QualifiedAddress.std.ts +++ b/ts/types/QualifiedAddress.std.ts @@ -8,6 +8,11 @@ import { isServiceIdString } from './ServiceId.std.js'; import type { AddressStringType } from './Address.std.js'; import { Address } from './Address.std.js'; +type QualifiedAddressMatch = RegExpMatchArray & { + 1: string; + 2: string; + 3: string; +}; const QUALIFIED_ADDRESS_REGEXP = /^((?:PNI:)?[:0-9a-f-]+):((?:PNI:)?[:0-9a-f-]+).(\d+)$/i; @@ -41,7 +46,8 @@ export class QualifiedAddress { public static parse(value: string): QualifiedAddress { const match = value.match(QUALIFIED_ADDRESS_REGEXP); strictAssert(match != null, `Invalid QualifiedAddress: ${value}`); - const [whole, ourServiceId, serviceId, deviceId] = match; + const [whole, ourServiceId, serviceId, deviceId] = + match as QualifiedAddressMatch; strictAssert(whole === value, 'Integrity check'); strictAssert( isServiceIdString(ourServiceId), diff --git a/ts/types/Stickers.preload.ts b/ts/types/Stickers.preload.ts index c80dd3910d..74f077949a 100644 --- a/ts/types/Stickers.preload.ts +++ b/ts/types/Stickers.preload.ts @@ -322,9 +322,8 @@ export function downloadQueuedPacks(): void { log.info('downloadQueuedPacks'); strictAssert(packsToDownload, 'Stickers not initialized'); - const ids = Object.keys(packsToDownload); - for (const id of ids) { - const { key, status } = packsToDownload[id]; + for (const [id, packToDownload] of Object.entries(packsToDownload)) { + const { key, status } = packToDownload; // The queuing is done inside this function, no need to await here drop( @@ -345,8 +344,7 @@ function capturePacksToDownload( const toDownload: DownloadMap = Object.create(null); // First, ensure that blessed packs are in good shape - const blessedIds = Object.keys(BLESSED_PACKS); - blessedIds.forEach(id => { + for (const [id, blessedPack] of Object.entries(BLESSED_PACKS)) { const existing = existingPackLookup[id]; if ( !existing || @@ -354,29 +352,26 @@ function capturePacksToDownload( ) { toDownload[id] = { id, - ...BLESSED_PACKS[id], + ...blessedPack, }; } - }); + } // Then, find error cases in packs we already know about - const existingIds = Object.keys(existingPackLookup); - existingIds.forEach(id => { + for (const [id, existing] of Object.entries(existingPackLookup)) { if (toDownload[id]) { - return; + continue; } - const existing = existingPackLookup[id]; - // These packs should never end up in the database, but if they do we'll delete them if (existing.status === 'ephemeral') { void deletePack(id); - return; + continue; } // We don't automatically download these; not until a user action kicks it off if (existing.status === 'known') { - return; + continue; } if (doesPackNeedDownload(existing)) { @@ -388,7 +383,7 @@ function capturePacksToDownload( status, }; } - }); + } return toDownload; } diff --git a/ts/updater/common.main.ts b/ts/updater/common.main.ts index 366d77b302..fe6fbd1e13 100644 --- a/ts/updater/common.main.ts +++ b/ts/updater/common.main.ts @@ -1065,7 +1065,8 @@ export function getUpdateFileName( const candidates = files.filter(fileFilter); if (candidates.length === 1) { - path = candidates[0].url; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + path = candidates[0]!.url; } } diff --git a/ts/updater/curve.node.ts b/ts/updater/curve.node.ts index 1e7fb94569..9af06952a3 100644 --- a/ts/updater/curve.node.ts +++ b/ts/updater/curve.node.ts @@ -3,7 +3,12 @@ import { PrivateKey, PublicKey } from '@signalapp/libsignal-client'; -export function keyPair(): Record { +type KeyPair = Readonly<{ + publicKey: Uint8Array; + privateKey: Uint8Array; +}>; + +export function keyPair(): KeyPair { const privKey = PrivateKey.generate(); const pubKey = privKey.getPublicKey(); diff --git a/ts/updater/differential.node.ts b/ts/updater/differential.node.ts index 474bda1ce4..2cfdad5be1 100644 --- a/ts/updater/differential.node.ts +++ b/ts/updater/differential.node.ts @@ -111,11 +111,13 @@ export async function parseBlockMap(data: Buffer): Promise { ); const [file] = json.files; + strictAssert(file, 'Missing file'); let { offset } = file; const blocks = new Array(); for (const [i, checksum] of file.checksums.entries()) { - const size = file.sizes[i]; + // TypeScript gets confused about this type for some reason. + const size: number | undefined = file.sizes[i]; strictAssert(size !== undefined, `missing block size: ${i}`); blocks.push({ @@ -407,7 +409,8 @@ export async function downloadRanges( // When the result is single range we might non-multipart response if (ranges.length === 1 && !match) { await saveDiffStream({ - diff: ranges[0], + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + diff: ranges[0]!, stream, abortSignal, output, @@ -466,7 +469,9 @@ async function takeDiffFromPart( const contentRange = headers['content-range']; strictAssert(contentRange, 'Missing Content-Range header for the part'); - const match = contentRange.join(', ').match(/^bytes\s+(\d+-\d+)/); + const match = contentRange.join(', ').match(/^bytes\s+(\d+-\d+)/) as + | (RegExpMatchArray & { 1: string }) + | null; strictAssert( match, `Invalid Content-Range header for the part: "${contentRange}"` diff --git a/ts/updater/generateSignature.main.ts b/ts/updater/generateSignature.main.ts index 09486294d8..b59bd78e42 100644 --- a/ts/updater/generateSignature.main.ts +++ b/ts/updater/generateSignature.main.ts @@ -78,10 +78,8 @@ async function findUpdatePaths(): Promise> { const releaseDir = resolve('release'); const files: Array = await readdir(releaseDir); - const max = files.length; const results = new Array(); - for (let i = 0; i < max; i += 1) { - const file = files[i]; + for (const file of files) { const fullPath = join(releaseDir, file); if (IS_EXE.test(file) || IS_ZIP.test(file)) { diff --git a/ts/util/Attachment.std.ts b/ts/util/Attachment.std.ts index c9b10bdf6c..751207b855 100644 --- a/ts/util/Attachment.std.ts +++ b/ts/util/Attachment.std.ts @@ -354,7 +354,7 @@ export function isGIF(attachments?: ReadonlyArray): boolean { const flag = SignalService.AttachmentPointer.Flags.GIF; const hasFlag = // eslint-disable-next-line no-bitwise - !isUndefined(attachment.flags) && (attachment.flags & flag) === flag; + !isUndefined(attachment?.flags) && (attachment.flags & flag) === flag; return hasFlag && isVideoAttachment(attachment); } @@ -499,7 +499,8 @@ export function getGridDimensions( } if (attachments.length === 1) { - return getImageDimensionsForTimeline(attachments[0]); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return getImageDimensionsForTimeline(attachments[0]!); } if (attachments.length === 2) { diff --git a/ts/util/BackOff.std.ts b/ts/util/BackOff.std.ts index 81bca6f58c..b378c1b09b 100644 --- a/ts/util/BackOff.std.ts +++ b/ts/util/BackOff.std.ts @@ -46,7 +46,8 @@ export class BackOff { ) {} public get(): number { - let result = this.timeouts[this.#count]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + let result = this.timeouts[this.#count]!; const { jitter = 0, random = DEFAULT_RANDOM } = this.options; // Do not apply jitter larger than the timeout value. It is supposed to be diff --git a/ts/util/DelimitedStream.node.ts b/ts/util/DelimitedStream.node.ts index f262f05063..6a52f312a4 100644 --- a/ts/util/DelimitedStream.node.ts +++ b/ts/util/DelimitedStream.node.ts @@ -40,7 +40,8 @@ export class DelimitedStream extends Transform { let offset = 0; while (offset < chunk.length) { if (this.#state.kind === 'prefix') { - const b = chunk[offset]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const b = chunk[offset]!; offset += 1; // See: https://protobuf.dev/programming-guides/encoding/ diff --git a/ts/util/arePinnedConversationsEqual.node.ts b/ts/util/arePinnedConversationsEqual.node.ts index a3bacbb302..b100bb8418 100644 --- a/ts/util/arePinnedConversationsEqual.node.ts +++ b/ts/util/arePinnedConversationsEqual.node.ts @@ -17,7 +17,7 @@ export function arePinnedConversationsEqual( } return localValue.every( (localPinnedConversation: PinnedConversation, index: number) => { - const remotePinnedConversation = remoteValue[index].identifier; + const remotePinnedConversation = remoteValue[index]?.identifier; if (!remotePinnedConversation) { return false; } diff --git a/ts/util/benchmark/stats.std.ts b/ts/util/benchmark/stats.std.ts index 8209903183..89f53dc983 100644 --- a/ts/util/benchmark/stats.std.ts +++ b/ts/util/benchmark/stats.std.ts @@ -33,7 +33,8 @@ export function stats( const result: StatsType = { mean, stddev }; for (const p of percentiles) { - result[`p${p}`] = sorted[Math.floor((sorted.length * p) / 100)]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + result[`p${p}`] = sorted[Math.floor((sorted.length * p) / 100)]!; } return result; @@ -146,7 +147,8 @@ export function regress(samples: ReadonlyArray): Regression { yIntercept, slope, confidence: - STUDENT_T[Math.min(samples.length, STUDENT_T.length - 1)] * stdError, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + STUDENT_T[Math.min(samples.length, STUDENT_T.length - 1)]! * stdError, outliers, severeOutliers, }; diff --git a/ts/util/cleanup.preload.ts b/ts/util/cleanup.preload.ts index f0d6c31606..ea59379dfb 100644 --- a/ts/util/cleanup.preload.ts +++ b/ts/util/cleanup.preload.ts @@ -212,8 +212,10 @@ async function cleanupStoryReplies( } return cleanupStoryReplies(story, { - messageId: lastMessageId, - receivedAt: lastReceivedAt, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + messageId: lastMessageId!, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + receivedAt: lastReceivedAt!, }); } diff --git a/ts/util/computeBlurHashUrl.std.ts b/ts/util/computeBlurHashUrl.std.ts index 3dec5daa96..d676a2e366 100644 --- a/ts/util/computeBlurHashUrl.std.ts +++ b/ts/util/computeBlurHashUrl.std.ts @@ -112,9 +112,12 @@ export function computeBlurHashUrl( i += 4, j += 3 ) { // BMP uses BGR ordering - bitmap[j + 2] = rgba[i]; - bitmap[j + 1] = rgba[i + 1]; - bitmap[j] = rgba[i + 2]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + bitmap[j + 2]! = rgba[i]!; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + bitmap[j + 1]! = rgba[i + 1]!; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + bitmap[j]! = rgba[i + 2]!; } return `data:image/bmp;base64,${Bytes.toBase64(bitmap)}`; diff --git a/ts/util/createHTTPSAgent.node.ts b/ts/util/createHTTPSAgent.node.ts index a71f87e700..624a27ca94 100644 --- a/ts/util/createHTTPSAgent.node.ts +++ b/ts/util/createHTTPSAgent.node.ts @@ -137,6 +137,7 @@ export async function happyEyeballs({ const results = await Promise.allSettled( interleaved.map(async (addr, index) => { const abortController = abortControllers[index]; + strictAssert(abortController, 'Missing abortController'); if (index !== 0) { await sleep(index * DELAY_MS, abortController.signal); } @@ -185,17 +186,22 @@ export async function happyEyeballs({ 'Fulfilled promise was not fulfilled' ); const { socket, index } = fulfilled.value; + const address = interleaved[index]; + strictAssert(address, 'Missing address'); return { socket, - address: interleaved[index], + address, v4Attempts, v6Attempts, }; } + const firstResult = results[0]; + strictAssert(firstResult, 'Missing firstResult'); + strictAssert( - results[0].status === 'rejected', + firstResult.status === 'rejected', 'No fulfilled promises, but no rejected either' ); @@ -203,7 +209,7 @@ export async function happyEyeballs({ for (const controller of abortControllers) { controller.abort(); } - throw results[0].reason; + throw firstResult.reason; } export type ConnectOptionsType = Readonly<{ diff --git a/ts/util/createIdenticon.preload.tsx b/ts/util/createIdenticon.preload.tsx index 0ca3eb3fe4..2ccc8e5f87 100644 --- a/ts/util/createIdenticon.preload.tsx +++ b/ts/util/createIdenticon.preload.tsx @@ -14,6 +14,7 @@ import { } from '../components/IdenticonSVG.dom.js'; import { missingCaseError } from './missingCaseError.std.js'; import { writeNewPlaintextTempData } from './migrations.preload.js'; +import { strictAssert } from './assert.std.js'; const TARGET_MIME = 'image/png'; @@ -35,6 +36,7 @@ export function createIdenticon( { saveToDisk }: { saveToDisk?: boolean } = {} ): Promise<{ url: string; path?: string }> { const [defaultColorValue] = Array.from(AvatarColorMap.values()); + strictAssert(defaultColorValue, 'Missing defaultColorValue'); const avatarColor = AvatarColorMap.get(color); let html: string; diff --git a/ts/util/createProxyAgent.node.ts b/ts/util/createProxyAgent.node.ts index e1c65aeefd..8696c955d7 100644 --- a/ts/util/createProxyAgent.node.ts +++ b/ts/util/createProxyAgent.node.ts @@ -47,7 +47,8 @@ export async function createProxyAgent(proxyUrl: string): Promise { // SOCKS 4/5 resolve target host before sending it to the proxy. if (host !== proxyHost) { const idx = Math.floor(Math.random() * addresses.length); - return addresses[idx]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return addresses[idx]!; } const start = Date.now(); diff --git a/ts/util/createSupportUrl.std.ts b/ts/util/createSupportUrl.std.ts index cda22fa65e..5cba2cb68c 100644 --- a/ts/util/createSupportUrl.std.ts +++ b/ts/util/createSupportUrl.std.ts @@ -51,11 +51,11 @@ export function createSupportUrl({ url.searchParams.set('desktop', ''); - for (const key of Object.keys(query)) { + for (const [key, value] of Object.entries(query)) { if (key === 'desktop') { continue; } - url.searchParams.set(key, query[key]); + url.searchParams.set(key, value); } // Support page requires `?desktop&...` not `?desktop=&...` diff --git a/ts/util/deleteStoryForEveryone.preload.ts b/ts/util/deleteStoryForEveryone.preload.ts index 96c3047da5..5515ca9a0d 100644 --- a/ts/util/deleteStoryForEveryone.preload.ts +++ b/ts/util/deleteStoryForEveryone.preload.ts @@ -141,7 +141,8 @@ export async function deleteStoryForEveryone( let recipient = newStoryRecipients.get(destinationServiceId); if (!recipient) { const isAllowedToReply = - sendStateByConversationId[conversationId].isAllowedToReplyToStory; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + sendStateByConversationId[conversationId]!.isAllowedToReplyToStory; recipient = { distributionListIds: new Set(), isAllowedToReply: isAllowedToReply !== false, diff --git a/ts/util/deliveryReceipt.preload.ts b/ts/util/deliveryReceipt.preload.ts index daf584147b..0a4ff18889 100644 --- a/ts/util/deliveryReceipt.preload.ts +++ b/ts/util/deliveryReceipt.preload.ts @@ -29,12 +29,12 @@ export const deliveryReceiptBatcher = createBatcher({ processBatch: async deliveryReceipts => { const groups = groupBy(deliveryReceipts, 'conversationId'); await Promise.all( - Object.keys(groups).map(async conversationId => { + Object.entries(groups).map(async ([conversationId, receipts]) => { await conversationJobQueue.add({ type: conversationQueueJobEnum.enum.Receipts, conversationId, receiptsType: ReceiptType.Delivery, - receipts: groups[conversationId], + receipts, }); }) ); diff --git a/ts/util/desktopCapturer.preload.ts b/ts/util/desktopCapturer.preload.ts index 672734d209..00ac26395e 100644 --- a/ts/util/desktopCapturer.preload.ts +++ b/ts/util/desktopCapturer.preload.ts @@ -338,11 +338,11 @@ export class DesktopCapturer { return i18n('icu:calling__SelectPresentingSourcesModal--entireScreen'); } + type Match = RegExpMatchArray & { 1: string }; const match = name.match(/^Screen (\d+)$/); if (match) { - return i18n('icu:calling__SelectPresentingSourcesModal--screen', { - id: match[1], - }); + const [, id] = match as Match; + return i18n('icu:calling__SelectPresentingSourcesModal--screen', { id }); } return name; diff --git a/ts/util/downloadOnboardingStory.preload.ts b/ts/util/downloadOnboardingStory.preload.ts index cda0e3b3bf..11f838bbcd 100644 --- a/ts/util/downloadOnboardingStory.preload.ts +++ b/ts/util/downloadOnboardingStory.preload.ts @@ -56,9 +56,9 @@ export async function downloadOnboardingStory(): Promise { log.info('got manifest version:', manifest.version); const imageFilenames = - userLocale in manifest.languages - ? manifest.languages[userLocale] - : manifest.languages.en; + manifest.languages[userLocale] ?? + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + manifest.languages.en!; const imageBuffers = await downloadOnboardingStories( manifest.version, diff --git a/ts/util/editHelpers.std.ts b/ts/util/editHelpers.std.ts index 693de9540e..42515e6384 100644 --- a/ts/util/editHelpers.std.ts +++ b/ts/util/editHelpers.std.ts @@ -41,11 +41,13 @@ export function getTargetOfThisEditTimestamp({ // We want the second-to-last item, because we may have partially sent targetTimestamp if (length > 1) { - return mostRecent[length - 2].timestamp; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return mostRecent[length - 2]!.timestamp; } // If there's only one item, we'll use it if (length > 0) { - return mostRecent[length - 1].timestamp; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return mostRecent[length - 1]!.timestamp; } // This is a good failover in case we ever stop duplicating data in editHistory diff --git a/ts/util/expirationTimer.std.ts b/ts/util/expirationTimer.std.ts index c5f05d6f03..2767901f79 100644 --- a/ts/util/expirationTimer.std.ts +++ b/ts/util/expirationTimer.std.ts @@ -49,7 +49,7 @@ export function format( // but humanizeDuration uses an underscore const locale: string = i18n.getLocale().replace(/-/g, '_'); - const localeWithoutRegion: string = locale.split('_', 1)[0]; + const [localeWithoutRegion] = locale.split('_', 1); const fallbacks: Array = []; if (localeWithoutRegion !== locale) { fallbacks.push(localeWithoutRegion); diff --git a/ts/util/exponentialBackoff.std.ts b/ts/util/exponentialBackoff.std.ts index 8615f7c297..d15d746260 100644 --- a/ts/util/exponentialBackoff.std.ts +++ b/ts/util/exponentialBackoff.std.ts @@ -38,7 +38,8 @@ export function exponentialBackoffSleepTime( ); if (attempt - 1 < numHardcodedBackoffs) { - return options.firstBackoffs[attempt - 1]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return options.firstBackoffs[attempt - 1]!; } const lastHardcodedBackoff = options.firstBackoffs.at(-1); diff --git a/ts/util/filterAndSortConversations.std.ts b/ts/util/filterAndSortConversations.std.ts index 5b321aa636..1991a14cf3 100644 --- a/ts/util/filterAndSortConversations.std.ts +++ b/ts/util/filterAndSortConversations.std.ts @@ -120,13 +120,14 @@ function searchConversations( searchTerm: string, regionCode: string | undefined ): ReadonlyArray, 'item' | 'score'>> { + type CommandMatch = RegExpMatchArray & { 1: string; 2: string | undefined }; const maybeCommand = searchTerm.match(/^!([^\s:]+)(?::(.*))?$/); if (maybeCommand) { - const [, commandName, query] = maybeCommand; + const [, commandName, query] = maybeCommand as CommandMatch; const command = COMMANDS.get(commandName); if (command) { - return command(conversations, query).map(item => ({ item })); + return command(conversations, query ?? '').map(item => ({ item })); } } diff --git a/ts/util/findAndDeleteOnboardingStoryIfExists.preload.ts b/ts/util/findAndDeleteOnboardingStoryIfExists.preload.ts index 47956ab686..00c847e975 100644 --- a/ts/util/findAndDeleteOnboardingStoryIfExists.preload.ts +++ b/ts/util/findAndDeleteOnboardingStoryIfExists.preload.ts @@ -21,7 +21,8 @@ export async function findAndDeleteOnboardingStoryIfExists(): Promise { } const hasExpired = await (async () => { - const [storyId] = existingOnboardingStoryMessageIds; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const storyId = existingOnboardingStoryMessageIds[0]!; try { const message = await getMessageById(storyId); if (!message) { diff --git a/ts/util/generateDonationReceipt.dom.ts b/ts/util/generateDonationReceipt.dom.ts index ab54f4c9c2..74d04da347 100644 --- a/ts/util/generateDonationReceipt.dom.ts +++ b/ts/util/generateDonationReceipt.dom.ts @@ -396,7 +396,7 @@ export async function generateDonationReceiptBlob( // Extract the base64 encoded data from the data URL // Data URL format: "data:image/png;base64,iVBORw0KGgoAAAANS..." - const base64Data = dataURL.split(',')[1]; + const [base64Data] = dataURL.split(','); // Decode the base64 string to binary data const binaryString = atob(base64Data); diff --git a/ts/util/generateMessageId.node.ts b/ts/util/generateMessageId.node.ts index a4fcc90090..9f0a0422aa 100644 --- a/ts/util/generateMessageId.node.ts +++ b/ts/util/generateMessageId.node.ts @@ -14,7 +14,7 @@ export type GeneratedMessageIdType = Readonly<{ export function generateMessageId(counter: number): GeneratedMessageIdType { const uuid = getRandomBytes(16); - /* eslint-disable no-bitwise */ + /* eslint-disable no-bitwise, @typescript-eslint/no-non-null-assertion */ // We compose uuid out of 48 bits (6 bytes of) timestamp-like counter: // `incrementMessageCounter`. Note big-endian encoding (which ensures proper @@ -29,16 +29,16 @@ export function generateMessageId(counter: number): GeneratedMessageIdType { uuid[5] = (counter / 0x00000000001) & 0xff; // Mask out 4 bits of version number - uuid[6] &= 0x0f; + uuid[6]! &= 0x0f; // And set the version to 7 - uuid[6] |= 0x70; + uuid[6]! |= 0x70; // Mask out 2 bits of variant - uuid[8] &= 0x3f; + uuid[8]! &= 0x3f; // And set it to "2" - uuid[8] |= 0x80; + uuid[8]! |= 0x80; - /* eslint-enable no-bitwise */ + /* eslint-enable no-bitwise, @typescript-eslint/no-non-null-assertion */ return { id: stringify(uuid), diff --git a/ts/util/getColorForCallLink.std.ts b/ts/util/getColorForCallLink.std.ts index 611541a1e7..c991cc7b90 100644 --- a/ts/util/getColorForCallLink.std.ts +++ b/ts/util/getColorForCallLink.std.ts @@ -12,8 +12,11 @@ export function getColorForCallLink( ): AxoTokens.Avatar.ColorName { const rootKeyStart = rootKey.slice(0, 2); - const upper = (BASE_16_CONSONANT_ALPHABET.indexOf(rootKeyStart[0]) || 0) * 16; - const lower = BASE_16_CONSONANT_ALPHABET.indexOf(rootKeyStart[1]) || 0; + const upper = + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + (BASE_16_CONSONANT_ALPHABET.indexOf(rootKeyStart[0]!) || 0) * 16; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const lower = BASE_16_CONSONANT_ALPHABET.indexOf(rootKeyStart[1]!) || 0; const firstByte = upper + lower; return AxoTokens.Avatar.getColorNameByHash(firstByte); diff --git a/ts/util/getCountryData.dom.ts b/ts/util/getCountryData.dom.ts index 8b6096c5af..d6e92c3ebd 100644 --- a/ts/util/getCountryData.dom.ts +++ b/ts/util/getCountryData.dom.ts @@ -65,7 +65,8 @@ function getCountryCodeForRegion(region: string): string { function computeCountryDataForLocale( locale: string ): ReadonlyArray { - const map = window.SignalContext.getCountryDisplayNames()[locale]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const map = window.SignalContext.getCountryDisplayNames()[locale]!; const list = Object.entries(map).map(([region, displayName]) => { return { diff --git a/ts/util/getFontNameByTextScript.std.ts b/ts/util/getFontNameByTextScript.std.ts index 2d91735e81..19842c6c78 100644 --- a/ts/util/getFontNameByTextScript.std.ts +++ b/ts/util/getFontNameByTextScript.std.ts @@ -1,82 +1,85 @@ // Copyright 2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +import { TextAttachmentStyleType } from '../types/Attachment.std.js'; import type { LocalizerType } from '../types/Util.std.js'; import { strictAssert } from './assert.std.js'; +const TextStyle = TextAttachmentStyleType; + const FONT_MAP = { - base: [ - 'sans-serif', - 'sans-serif', - 'sans-serif', - 'serif', - 'serif', - 'sans-serif', - ], - latin: [ - 'Inter', - 'Inter', - 'Inter', - '"EB Garamond"', - 'Parisienne', - '"Barlow Condensed"', - ], - cyrillic: [ - 'Inter', - 'Inter', - 'Inter', - '"EB Garamond"', - '"American Typewriter Semibold", "Cambria Bold"', - '"SF Pro Light (System Light)", "Calibri Light"', - ], - devanagari: [ - '"Kohinoor Devanagari Regular", "Utsaah Regular"', - '"Kohinoor Devanagari Regular", "Utsaah Regular"', - '"Kohinoor Devanagari Semibold", "Utsaah Bold"', - '"Devanagari Sangam MN Regular", "Kokila Regular"', - '"Devanagari Sangam MN Bold", "Kokila Bold"', - '"Kohinoor Devanagari Light", "Utsaah Regular"', - ], - arabic: [ - '"SF Arabic Regular", "Segoe UI Arabic Regular"', - '"SF Arabic Regular", "Segoe UI Arabic Regular"', - '"SF Arabic Bold", "Segoe UI Arabic Bold"', - '"Geeza Pro Regular", "Sakkal Majalla Regular"', - '"Geeza Pro Bold", "Sakkal Majalla Bold"', - '"SF Arabic Black", "Segoe UI Arabic Bold"', - ], - japanese: [ - '"Hiragino Sans W3"', - '"Hiragino Sans W3"', - '"Hiragino Sans W7"', - '"Hiragino Mincho Pro W3"', - '"Hiragino Mincho Pro W6"', - '"Hiragino Maru Gothic Pro N"', - ], - zhhk: [ - '"PingFang HK Regular", "MingLiU Regular"', - '"PingFang HK Regular", "MingLiU Regular"', - '"PingFang HK Semibold", "MingLiU Regular"', - '"PingFang HK Ultralight", "MingLiU Regular"', - '"PingFang HK Thin", "MingLiU Regular"', - '"PingFang HK Light", "MingLiU Regular"', - ], - zhtc: [ - '"PingFang TC Regular", "JhengHei TC Regular"', - '"PingFang TC Regular", "JhengHei TC Regular"', - '"PingFang TC Semibold", "JhengHei TC Bold"', - '"PingFang TC Ultralight", "JhengHei TC Light"', - '"PingFang TC Thin", "JhengHei TC Regular"', - '"PingFang TC Light", "JhengHei TC Bold"', - ], - zhsc: [ - '"PingFang SC Regular", SimHei', - '"PingFang SC Regular", SimHei', - '"PingFang SC Semibold", SimHei', - '"PingFang SC Ultralight", SimHei', - '"PingFang SC Thin", SimHei', - '"PingFang SC Light", SimHei', - ], + base: { + [TextStyle.DEFAULT]: 'sans-serif', + [TextStyle.REGULAR]: 'sans-serif', + [TextStyle.BOLD]: 'sans-serif', + [TextStyle.SERIF]: 'serif', + [TextStyle.SCRIPT]: 'serif', + [TextStyle.CONDENSED]: 'sans-serif', + }, + latin: { + [TextStyle.DEFAULT]: 'Inter', + [TextStyle.REGULAR]: 'Inter', + [TextStyle.BOLD]: 'Inter', + [TextStyle.SERIF]: '"EB Garamond"', + [TextStyle.SCRIPT]: 'Parisienne', + [TextStyle.CONDENSED]: '"Barlow Condensed"', + }, + cyrillic: { + [TextStyle.DEFAULT]: 'Inter', + [TextStyle.REGULAR]: 'Inter', + [TextStyle.BOLD]: 'Inter', + [TextStyle.SERIF]: '"EB Garamond"', + [TextStyle.SCRIPT]: '"American Typewriter Semibold", "Cambria Bold"', + [TextStyle.CONDENSED]: '"SF Pro Light (System Light)", "Calibri Light"', + }, + devanagari: { + [TextStyle.DEFAULT]: '"Kohinoor Devanagari Regular", "Utsaah Regular"', + [TextStyle.REGULAR]: '"Kohinoor Devanagari Regular", "Utsaah Regular"', + [TextStyle.BOLD]: '"Kohinoor Devanagari Semibold", "Utsaah Bold"', + [TextStyle.SERIF]: '"Devanagari Sangam MN Regular", "Kokila Regular"', + [TextStyle.SCRIPT]: '"Devanagari Sangam MN Bold", "Kokila Bold"', + [TextStyle.CONDENSED]: '"Kohinoor Devanagari Light", "Utsaah Regular"', + }, + arabic: { + [TextStyle.DEFAULT]: '"SF Arabic Regular", "Segoe UI Arabic Regular"', + [TextStyle.REGULAR]: '"SF Arabic Regular", "Segoe UI Arabic Regular"', + [TextStyle.BOLD]: '"SF Arabic Bold", "Segoe UI Arabic Bold"', + [TextStyle.SERIF]: '"Geeza Pro Regular", "Sakkal Majalla Regular"', + [TextStyle.SCRIPT]: '"Geeza Pro Bold", "Sakkal Majalla Bold"', + [TextStyle.CONDENSED]: '"SF Arabic Black", "Segoe UI Arabic Bold"', + }, + japanese: { + [TextStyle.DEFAULT]: '"Hiragino Sans W3"', + [TextStyle.REGULAR]: '"Hiragino Sans W3"', + [TextStyle.BOLD]: '"Hiragino Sans W7"', + [TextStyle.SERIF]: '"Hiragino Mincho Pro W3"', + [TextStyle.SCRIPT]: '"Hiragino Mincho Pro W6"', + [TextStyle.CONDENSED]: '"Hiragino Maru Gothic Pro N"', + }, + zhhk: { + [TextStyle.DEFAULT]: '"PingFang HK Regular", "MingLiU Regular"', + [TextStyle.REGULAR]: '"PingFang HK Regular", "MingLiU Regular"', + [TextStyle.BOLD]: '"PingFang HK Semibold", "MingLiU Regular"', + [TextStyle.SERIF]: '"PingFang HK Ultralight", "MingLiU Regular"', + [TextStyle.SCRIPT]: '"PingFang HK Thin", "MingLiU Regular"', + [TextStyle.CONDENSED]: '"PingFang HK Light", "MingLiU Regular"', + }, + zhtc: { + [TextStyle.DEFAULT]: '"PingFang TC Regular", "JhengHei TC Regular"', + [TextStyle.REGULAR]: '"PingFang TC Regular", "JhengHei TC Regular"', + [TextStyle.BOLD]: '"PingFang TC Semibold", "JhengHei TC Bold"', + [TextStyle.SERIF]: '"PingFang TC Ultralight", "JhengHei TC Light"', + [TextStyle.SCRIPT]: '"PingFang TC Thin", "JhengHei TC Regular"', + [TextStyle.CONDENSED]: '"PingFang TC Light", "JhengHei TC Bold"', + }, + zhsc: { + [TextStyle.DEFAULT]: '"PingFang SC Regular", SimHei', + [TextStyle.REGULAR]: '"PingFang SC Regular", SimHei', + [TextStyle.BOLD]: '"PingFang SC Semibold", SimHei', + [TextStyle.SERIF]: '"PingFang SC Ultralight", SimHei', + [TextStyle.SCRIPT]: '"PingFang SC Thin", SimHei', + [TextStyle.CONDENSED]: '"PingFang SC Light", SimHei', + }, }; const rxArabic = /\p{Script=Arab}/u; @@ -114,46 +117,46 @@ export const fontSniffer = { export function getFontNameByTextScript( text: string, - textStyleIndex: number, + textStyleType: TextAttachmentStyleType, i18n?: LocalizerType ): string { strictAssert( - textStyleIndex >= 0 && textStyleIndex <= 5, - 'text style is not between 0-5' + TextAttachmentStyleType[textStyleType], + `Invalid textStyleType: ${textStyleType}` ); - const fonts: Array = [FONT_MAP.base[textStyleIndex]]; + const fonts: Array = [FONT_MAP.base[textStyleType]]; if (fontSniffer.hasArabic(text)) { - fonts.push(FONT_MAP.arabic[textStyleIndex]); + fonts.push(FONT_MAP.arabic[textStyleType]); } if (fontSniffer.hasCJK(text)) { const locale = i18n?.getLocale(); if (locale === 'zh-TW') { - fonts.push(FONT_MAP.zhtc[textStyleIndex]); + fonts.push(FONT_MAP.zhtc[textStyleType]); } else if (locale === 'zh-HK') { - fonts.push(FONT_MAP.zhhk[textStyleIndex]); + fonts.push(FONT_MAP.zhhk[textStyleType]); } else { - fonts.push(FONT_MAP.zhsc[textStyleIndex]); + fonts.push(FONT_MAP.zhsc[textStyleType]); } } if (fontSniffer.hasCyrillic(text)) { - fonts.push(FONT_MAP.cyrillic[textStyleIndex]); + fonts.push(FONT_MAP.cyrillic[textStyleType]); } if (fontSniffer.hasDevanagari(text)) { - fonts.push(FONT_MAP.devanagari[textStyleIndex]); + fonts.push(FONT_MAP.devanagari[textStyleType]); } if (fontSniffer.hasJapanese(text)) { - fonts.push(FONT_MAP.japanese[textStyleIndex]); + fonts.push(FONT_MAP.japanese[textStyleType]); } if (fontSniffer.hasLatin(text)) { - fonts.push(FONT_MAP.latin[textStyleIndex]); + fonts.push(FONT_MAP.latin[textStyleType]); } return fonts.reverse().join(', '); diff --git a/ts/util/getHSL.std.ts b/ts/util/getHSL.std.ts index 92bffd8bfa..8a6ab41857 100644 --- a/ts/util/getHSL.std.ts +++ b/ts/util/getHSL.std.ts @@ -1,16 +1,22 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -const LIGHTNESS_TABLE: Record = { +const LIGHTNESS_TABLE = { 0: 45, 60: 30, 180: 30, 240: 50, 300: 40, 360: 45, -}; +} as const satisfies Record; -function getLightnessFromHue(hue: number, min: number, max: number) { +type LightnessKey = keyof typeof LIGHTNESS_TABLE; + +function getLightnessFromHue( + hue: number, + min: LightnessKey, + max: LightnessKey +) { const percentage = ((hue - min) * 100) / (max - min); const minValue = LIGHTNESS_TABLE[min]; const maxValue = LIGHTNESS_TABLE[max]; diff --git a/ts/util/getInitials.std.ts b/ts/util/getInitials.std.ts index 4e7233b171..3d63774963 100644 --- a/ts/util/getInitials.std.ts +++ b/ts/util/getInitials.std.ts @@ -27,5 +27,6 @@ export function getInitials(name?: string): string | undefined { return partsLen === 1 ? parts[0].charAt(0) - : parts[0].charAt(0) + parts[partsLen - 1].charAt(0); + : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + parts[0].charAt(0) + parts[partsLen - 1]!.charAt(0); } diff --git a/ts/util/getNotificationDataForMessage.preload.ts b/ts/util/getNotificationDataForMessage.preload.ts index c1e068bbd4..a0867869e0 100644 --- a/ts/util/getNotificationDataForMessage.preload.ts +++ b/ts/util/getNotificationDataForMessage.preload.ts @@ -318,7 +318,8 @@ export function getNotificationDataForMessage( } } else { const joinedContact = window.ConversationController.getOrCreate( - groupUpdate.joined[0], + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + groupUpdate.joined[0]!, 'private' ); if (isMe(joinedContact.attributes)) { @@ -326,7 +327,8 @@ export function getNotificationDataForMessage( } else { messages.push( i18n('icu:joinedTheGroup', { - name: joinedContacts[0].getTitle(), + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + name: joinedContacts[0]!.getTitle(), }) ); } @@ -476,7 +478,8 @@ export function getNotificationDataForMessage( const { contact: contacts } = attributes; if (contacts && contacts.length) { return { - text: EmbeddedContact.getName(contacts[0]) || i18n('icu:unknownContact'), + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + text: EmbeddedContact.getName(contacts[0]!) || i18n('icu:unknownContact'), emoji: '👤', }; } diff --git a/ts/util/getQuoteBodyText.std.ts b/ts/util/getQuoteBodyText.std.ts index fea5307ee3..865ff023ee 100644 --- a/ts/util/getQuoteBodyText.std.ts +++ b/ts/util/getQuoteBodyText.std.ts @@ -29,7 +29,8 @@ export function getQuoteBodyText({ const { body, contact: embeddedContact, poll } = messageAttributes; const embeddedContactName = embeddedContact && embeddedContact.length > 0 - ? EmbeddedContact.getName(embeddedContact[0]) + ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + EmbeddedContact.getName(embeddedContact[0]!) : ''; const pollText = poll diff --git a/ts/util/graphemeAndLinkAwareSlice.std.ts b/ts/util/graphemeAndLinkAwareSlice.std.ts index 389f1e7075..0797e0eea9 100644 --- a/ts/util/graphemeAndLinkAwareSlice.std.ts +++ b/ts/util/graphemeAndLinkAwareSlice.std.ts @@ -61,7 +61,8 @@ const expandToIncludeEntireLink = ( return truncated; } - return original.slice(0, truncatedLink[0].lastIndex); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return original.slice(0, truncatedLink[0]!.lastIndex); }; type LinkRange = { diff --git a/ts/util/groupAndOrderReactions.dom.ts b/ts/util/groupAndOrderReactions.dom.ts index 4cb947da9f..e2a80f5d2f 100644 --- a/ts/util/groupAndOrderReactions.dom.ts +++ b/ts/util/groupAndOrderReactions.dom.ts @@ -81,7 +81,8 @@ export function useGroupedAndOrderedReactions( return orderBy( groupedReactions, - ['length', ([{ timestamp }]) => timestamp], + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + ['length', ([first]) => first!.timestamp], ['desc', 'desc'] ); }, [reactions, groupByKey, emojiLocalization]); diff --git a/ts/util/groupSendEndorsements.preload.ts b/ts/util/groupSendEndorsements.preload.ts index 60a75d37ec..e66c7405ab 100644 --- a/ts/util/groupSendEndorsements.preload.ts +++ b/ts/util/groupSendEndorsements.preload.ts @@ -279,6 +279,7 @@ export class GroupSendEndorsementState { // Fast path sending to one person if (serviceIds.size === 1) { const [serviceId] = serviceIds; + strictAssert(serviceId, 'Missing serviceId'); log.info(`${logId}: using single member endorsement (${serviceId})`); return this.#getMemberEndorsement(serviceId); } diff --git a/ts/util/handleRetry.preload.ts b/ts/util/handleRetry.preload.ts index bc1b07b341..5786be7a63 100644 --- a/ts/util/handleRetry.preload.ts +++ b/ts/util/handleRetry.preload.ts @@ -458,7 +458,8 @@ async function getRetryConversation({ return window.ConversationController.get(requestGroupId); } - const [messageId] = messageIds; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const messageId = messageIds[0]!; const message = await DataReader.getMessageById(messageId); if (!message) { log.warn( diff --git a/ts/util/lint/license_comments.node.ts b/ts/util/lint/license_comments.node.ts index 61c139faf0..7a1e6c14d0 100644 --- a/ts/util/lint/license_comments.node.ts +++ b/ts/util/lint/license_comments.node.ts @@ -162,6 +162,7 @@ async function getCommitFileWasAdded( const commitYear = new Date(dateString).getFullYear(); assert(!Number.isNaN(commitYear), `Could not read commit year for ${file}`); + assert(commitHash, 'Missing commitHash'); return { commitYear, commitHash }; } @@ -196,7 +197,10 @@ async function main() { const warnings = []; - if (!/Copyright \d{4} Signal Messenger, LLC/.test(firstLine)) { + if ( + firstLine == null || + !/Copyright \d{4} Signal Messenger, LLC/.test(firstLine) + ) { const commit = await getCommitFileWasAdded(file); warnings.push( chalk.red('Missing/Incorrect copyright line'), diff --git a/ts/util/objectMap.std.ts b/ts/util/objectMap.std.ts index 390e6ddf8d..79bec8f6dc 100644 --- a/ts/util/objectMap.std.ts +++ b/ts/util/objectMap.std.ts @@ -5,6 +5,5 @@ export function objectMap( obj: Record, f: (key: keyof typeof obj, value: (typeof obj)[keyof typeof obj]) => R ): Array { - const keys: Array = Object.keys(obj); - return keys.map(key => f(key, obj[key])); + return Object.entries(obj).map(([key, value]) => f(key, value)); } diff --git a/ts/util/scaleImageToLevel.preload.ts b/ts/util/scaleImageToLevel.preload.ts index 9a10037dad..3f5fc302f5 100644 --- a/ts/util/scaleImageToLevel.preload.ts +++ b/ts/util/scaleImageToLevel.preload.ts @@ -156,8 +156,7 @@ export async function scaleImageToLevel({ }; } - for (let i = 0; i < SCALABLE_DIMENSIONS.length; i += 1) { - const scalableDimensions = SCALABLE_DIMENSIONS[i]; + for (const scalableDimensions of SCALABLE_DIMENSIONS) { if (maxDimensions < scalableDimensions) { continue; } diff --git a/ts/util/search.std.ts b/ts/util/search.std.ts index 8cc43de179..26d45161b4 100644 --- a/ts/util/search.std.ts +++ b/ts/util/search.std.ts @@ -53,8 +53,10 @@ export function generateSnippetAroundMention({ // Gradually narrow the substring, word by word, until a snippet of appropriate length // is found while (leftWordIdx <= rightWordIdx) { - const leftWord = words[leftWordIdx]; - const rightWord = words[rightWordIdx]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const leftWord = words[leftWordIdx]!; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const rightWord = words[rightWordIdx]!; snippetStartIdx = Math.min(leftWord.index, mentionStart); snippetEndIdx = Math.max( diff --git a/ts/util/sendStoryMessage.preload.ts b/ts/util/sendStoryMessage.preload.ts index bb9b17fbfc..4e23396257 100644 --- a/ts/util/sendStoryMessage.preload.ts +++ b/ts/util/sendStoryMessage.preload.ts @@ -141,7 +141,8 @@ export async function sendStoryMessage( const linkPreview = attachment?.textAttachment?.preview; const sanitizedLinkPreview = linkPreview - ? sanitizeLinkPreview((await loadPreviewData([linkPreview]))[0]) + ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + sanitizeLinkPreview((await loadPreviewData([linkPreview]))[0]!) : undefined; // If a text attachment has a link preview we remove it from the // textAttachment data structure and instead process the preview and add diff --git a/ts/util/sendToGroup.preload.ts b/ts/util/sendToGroup.preload.ts index 91b067de57..8c0a6fd235 100644 --- a/ts/util/sendToGroup.preload.ts +++ b/ts/util/sendToGroup.preload.ts @@ -1262,8 +1262,8 @@ function getXorOfAccessKeys( } for (let i = 0; i < ACCESS_KEY_LENGTH; i += 1) { - // eslint-disable-next-line no-bitwise - result[i] ^= accessKeyBuffer[i]; + // eslint-disable-next-line no-bitwise, @typescript-eslint/no-non-null-assertion + result[i]! ^= accessKeyBuffer[i]!; } }); diff --git a/ts/util/sniffImageMimeType.std.ts b/ts/util/sniffImageMimeType.std.ts index dba89c5669..74626651f9 100644 --- a/ts/util/sniffImageMimeType.std.ts +++ b/ts/util/sniffImageMimeType.std.ts @@ -17,8 +17,7 @@ import { * [0]: https://mimesniff.spec.whatwg.org/#matching-an-image-type-pattern */ export function sniffImageMimeType(bytes: Uint8Array): undefined | MIMEType { - for (let i = 0; i < TYPES.length; i += 1) { - const type = TYPES[i]; + for (const type of TYPES) { if (matchesType(bytes, type)) { return type.mimeType; } @@ -85,8 +84,8 @@ function matchesType(input: Uint8Array, type: Type): boolean { for (let p = 0; p < type.bytePattern.length; p += 1) { const mask = type.patternMask ? type.patternMask[p] : 0xff; // We need to use a bitwise operator here, per the spec. - // eslint-disable-next-line no-bitwise - const maskedData = input[p] & mask; + // eslint-disable-next-line no-bitwise, @typescript-eslint/no-non-null-assertion + const maskedData = input[p]! & mask!; if (maskedData !== type.bytePattern[p]) { return false; } diff --git a/ts/util/syncTasks.preload.ts b/ts/util/syncTasks.preload.ts index 42231c6fdb..80312e6453 100644 --- a/ts/util/syncTasks.preload.ts +++ b/ts/util/syncTasks.preload.ts @@ -78,8 +78,7 @@ export async function queueSyncTasks( ): Promise { const logId = 'queueSyncTasks'; - for (let i = 0, max = tasks.length; i < max; i += 1) { - const task = tasks[i]; + for (const task of tasks) { const { id, envelopeId, type, sentAt, data } = task; const innerLogId = `${logId}(${toLogId(task)})`; diff --git a/ts/util/userLanguages.std.ts b/ts/util/userLanguages.std.ts index 0929090b58..603d258524 100644 --- a/ts/util/userLanguages.std.ts +++ b/ts/util/userLanguages.std.ts @@ -21,7 +21,8 @@ export function formatAcceptLanguageHeader( const length = Math.min(languages.length, MAX_LANGUAGES_TO_FORMAT); for (let i = 0; i < length; i += 1) { - const language = languages[i]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const language = languages[i]!; // ["If no 'q' parameter is present, the default weight is 1."][1] // diff --git a/ts/util/whitespaceStringUtil.std.ts b/ts/util/whitespaceStringUtil.std.ts deleted file mode 100644 index 19e0037f06..0000000000 --- a/ts/util/whitespaceStringUtil.std.ts +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2022 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -const WHITESPACE = new Set([ - ' ', - '\u200E', // left-to-right mark - '\u200F', // right-to-left mark - '\u2007', // figure space - '\u200B', // zero-width space - '\u2800', // braille blank -]); - -export function trim(str: string): string { - let start = 0; - let end = str.length - 1; - - for (start; start < str.length; start += 1) { - const char = str[start]; - if (!WHITESPACE.has(char)) { - break; - } - } - - for (end; end > 0; end -= 1) { - const char = str[end]; - if (!WHITESPACE.has(char)) { - break; - } - } - - if (start > 0 || end < str.length - 1) { - return str.substring(start, end + 1); - } - - return str; -} - -export function isWhitespace(str: string): boolean { - for (let i = 0; i < str.length; i += 1) { - const char = str[i]; - if (!WHITESPACE.has(char)) { - return false; - } - } - - return true; -} diff --git a/ts/window.d.ts b/ts/window.d.ts index 1c1d00b0fc..ecfa09a8e8 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -243,6 +243,36 @@ declare global { interface SharedArrayBuffer { __arrayBuffer: never; } + + interface StringSplitSplitter { + [Symbol.split](string: string, limit?: number): T; + } + + interface String { + split(splitter: string | RegExp): [string, ...Array]; + split(splitter: string | RegExp, limit: 0): []; + split(splitter: string | RegExp, limit: 1): [string]; + split(splitter: string | RegExp, limit: 2): [string, string?]; + split(splitter: string | RegExp, limit: 3): [string, string?, string?]; + split( + splitter: string | RegExp, + limit: 4 + ): [string, string?, string?, string?]; + split( + splitter: string | RegExp, + limit: 5 + ): [string, string?, string?, string?, string?]; + split(splitter: string | RegExp, limit: number): [string, ...Array]; + split( + splitter: string | RegExp, + limit?: number + ): [string, ...Array]; + split(splitter: StringSplitSplitter, limit?: number): T; + split( + splitter: string | RegExp | StringSplitSplitter>, + limit?: number + ): [string, ...Array]; + } } export type WhisperType = { diff --git a/ts/windows/main/preload_test.preload.ts b/ts/windows/main/preload_test.preload.ts index a1dcee6d9a..7b4f70b17d 100644 --- a/ts/windows/main/preload_test.preload.ts +++ b/ts/windows/main/preload_test.preload.ts @@ -160,15 +160,15 @@ window.testUtilities = { cwd: __dirname, }); - for (let i = 0; i < files.length; i += 1) { + for (const [i, file] of files.entries()) { if (i % workerCount === worker) { try { // eslint-disable-next-line import/no-dynamic-require, global-require - require(files[i]); + require(file); } catch (error) { window.testUtilities.onTestEvent({ type: 'fail', - title: ['Failed to load test:', files[i]], + title: ['Failed to load test:', file], error: error.stack || String(error), }); } diff --git a/ts/windows/main/start.preload.ts b/ts/windows/main/start.preload.ts index 48bd31393e..e0774e3bcc 100644 --- a/ts/windows/main/start.preload.ts +++ b/ts/windows/main/start.preload.ts @@ -132,7 +132,7 @@ if ( backgroundColor: '#3b82f6', }); const dataURL = canvas.toDataURL({ format: 'png' }); - const base64Data = dataURL.split(',')[1]; + const [base64Data] = dataURL.split(','); const data = Uint8Array.from(atob(base64Data), c => c.charCodeAt(0)); await conversation.enqueueMessageForSend( diff --git a/tsconfig.json b/tsconfig.json index 58be676e65..f6415c410f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -208,7 +208,7 @@ /* Enable error reporting for fallthrough cases in switch statements. */ "noFallthroughCasesInSwitch": true, /* Add 'undefined' to a type when accessed using an index. */ - "noUncheckedIndexedAccess": false, // TODO: Major migration needed + "noUncheckedIndexedAccess": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ "noImplicitOverride": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ @@ -232,6 +232,7 @@ * ts-node */ "ts-node": { + "transpileOnly": true, // Override TypeScript options for `ts-node` only "compilerOptions": { // Necessary for `ts-node` to work with CommonJS modules until we migrate to package.json#type: "module"