From d2696935447071dab99dec2da461e6f5cf3e9c07 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 21 Jan 2022 10:21:51 +1100 Subject: [PATCH] cleanup search of contacts excluding not active --- app/sql.js | 2 +- .../SessionMessagesListContainer.tsx | 7 +- .../SessionQuotedMessageComposition.tsx | 2 +- .../message/message-item/ReadableMessage.tsx | 15 +- ts/components/search/MessageSearchResults.tsx | 2 +- ts/state/ducks/conversations.ts | 9 - ts/state/ducks/search.ts | 43 +- ts/state/selectors/conversations.ts | 5 - .../linkPreviews/linkPreviewFetch_test.ts | 1164 ----------------- 9 files changed, 28 insertions(+), 1221 deletions(-) delete mode 100644 ts/test/test-electron/linkPreviews/linkPreviewFetch_test.ts diff --git a/app/sql.js b/app/sql.js index 36857cb69..a9f5b0290 100644 --- a/app/sql.js +++ b/app/sql.js @@ -1839,7 +1839,7 @@ function searchConversations(query, { limit } = {}) { id LIKE $id OR name LIKE $name OR profileName LIKE $profileName - ) + ) AND active_at IS NOT NULL AND active_at > 0 ORDER BY id ASC LIMIT $limit` ) diff --git a/ts/components/conversation/SessionMessagesListContainer.tsx b/ts/components/conversation/SessionMessagesListContainer.tsx index a54a5c338..ed6272ac2 100644 --- a/ts/components/conversation/SessionMessagesListContainer.tsx +++ b/ts/components/conversation/SessionMessagesListContainer.tsx @@ -17,7 +17,6 @@ import { resetOldTopMessageId, showScrollToBottomButton, SortedMessageModelProps, - updateHaveDoneFirstScroll, } from '../../state/ducks/conversations'; import { StateType } from '../../state/reducer'; import { @@ -211,10 +210,6 @@ class SessionMessagesListContainerInner extends React.Component { }); } } - - setTimeout(() => { - window.inboxStore?.dispatch(updateHaveDoneFirstScroll()); - }, 100); } /** @@ -250,6 +245,8 @@ class SessionMessagesListContainerInner extends React.Component { block, }); + this.props.messageContainerRef.current?.scrollBy({ top: -50 }); + if (options?.isLoadMoreTop) { // reset the oldTopInRedux so that a refresh/new message does not scroll us back here again window.inboxStore?.dispatch(resetOldTopMessageId()); diff --git a/ts/components/conversation/SessionQuotedMessageComposition.tsx b/ts/components/conversation/SessionQuotedMessageComposition.tsx index 1a67d3bfd..32606b300 100644 --- a/ts/components/conversation/SessionQuotedMessageComposition.tsx +++ b/ts/components/conversation/SessionQuotedMessageComposition.tsx @@ -27,7 +27,7 @@ const Subtle = styled.div` overflow: hidden; text-overflow: ellipsis; word-break: break-all; - -webkit-line-clamp: 3; + -webkit-line-clamp: 2; -webkit-box-orient: vertical; display: -webkit-box; color: var(--color-text); diff --git a/ts/components/conversation/message/message-item/ReadableMessage.tsx b/ts/components/conversation/message/message-item/ReadableMessage.tsx index 76dbf3993..1be9b4869 100644 --- a/ts/components/conversation/message/message-item/ReadableMessage.tsx +++ b/ts/components/conversation/message/message-item/ReadableMessage.tsx @@ -13,7 +13,6 @@ import { import { areMoreBottomMessagesBeingFetched, areMoreTopMessagesBeingFetched, - getHaveDoneFirstScroll, getLoadedMessagesLength, getMostRecentMessageId, getOldestMessageId, @@ -63,7 +62,6 @@ export const ReadableMessage = (props: ReadableMessageProps) => { const selectedConversationKey = useSelector(getSelectedConversationKey); const loadedMessagesLength = useSelector(getLoadedMessagesLength); - const haveDoneFirstScroll = useSelector(getHaveDoneFirstScroll); const mostRecentMessageId = useSelector(getMostRecentMessageId); const oldestMessageId = useSelector(getOldestMessageId); const youngestMessageId = useSelector(getYoungestMessageId); @@ -74,14 +72,6 @@ export const ReadableMessage = (props: ReadableMessageProps) => { const onVisible = useCallback( // tslint:disable-next-line: cyclomatic-complexity async (inView: boolean | Object) => { - // when the view first loads, it needs to scroll to the unread messages. - // we need to disable the inview on the first loading - if (!haveDoneFirstScroll) { - if (inView === true) { - window.log.info('onVisible but waiting for first scroll event'); - } - return; - } // we are the most recent message if (mostRecentMessageId === messageId) { // make sure the app is focused, because we mark message as read here @@ -137,7 +127,6 @@ export const ReadableMessage = (props: ReadableMessageProps) => { }, [ selectedConversationKey, - haveDoneFirstScroll, mostRecentMessageId, oldestMessageId, fetchingTopMore, @@ -158,8 +147,8 @@ export const ReadableMessage = (props: ReadableMessageProps) => { className={className} as="div" threshold={0.5} - delay={haveDoneFirstScroll && isAppFocused ? 100 : 200} - onChange={haveDoneFirstScroll && isAppFocused ? onVisible : noop} + delay={isAppFocused ? 100 : 200} + onChange={isAppFocused ? onVisible : noop} triggerOnce={false} trackVisibility={true} key={`inview-msg-${messageId}`} diff --git a/ts/components/search/MessageSearchResults.tsx b/ts/components/search/MessageSearchResults.tsx index 5d1dbee62..8456b36cb 100644 --- a/ts/components/search/MessageSearchResults.tsx +++ b/ts/components/search/MessageSearchResults.tsx @@ -97,7 +97,7 @@ const ResultBody = styled.div` overflow: hidden; display: -webkit-box; - -webkit-line-clamp: 3; + -webkit-line-clamp: 2; -webkit-box-orient: vertical; `; diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 0949cb1cf..f129b9bd2 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -276,7 +276,6 @@ export type ConversationsStateType = { areMoreBottomMessagesBeingFetched: boolean; oldTopMessageId: string | null; oldBottomMessageId: string | null; - haveDoneFirstScroll: boolean; showScrollButton: boolean; animateQuotedMessageId?: string; @@ -394,7 +393,6 @@ export function getEmptyConversationState(): ConversationsStateType { showScrollButton: false, mentionMembers: [], firstUnreadMessageId: undefined, - haveDoneFirstScroll: false, oldTopMessageId: null, oldBottomMessageId: null, }; @@ -741,8 +739,6 @@ const conversationsSlice = createSlice({ oldBottomMessageId: null, mentionMembers: [], firstUnreadMessageId: action.payload.firstUnreadIdOnOpen, - - haveDoneFirstScroll: false, }; }, navigateInConversationToMessageId( @@ -773,10 +769,6 @@ const conversationsSlice = createSlice({ state.oldBottomMessageId = null; return state; }, - updateHaveDoneFirstScroll(state: ConversationsStateType) { - state.haveDoneFirstScroll = true; - return state; - }, showLightBox( state: ConversationsStateType, action: PayloadAction @@ -913,7 +905,6 @@ export const { messagesChanged, resetOldTopMessageId, resetOldBottomMessageId, - updateHaveDoneFirstScroll, markConversationFullyRead, // layout stuff showMessageDetailsView, diff --git a/ts/state/ducks/search.ts b/ts/state/ducks/search.ts index 2ec05642b..98e3f5c64 100644 --- a/ts/state/ducks/search.ts +++ b/ts/state/ducks/search.ts @@ -6,6 +6,7 @@ import { ReduxConversationType } from './conversations'; import { PubKey } from '../../session/types'; import { ConversationTypeEnum } from '../../models/conversation'; import _ from 'lodash'; +import { getConversationController } from '../../session/conversations'; // State @@ -84,19 +85,15 @@ async function doSearch(query: string, options: SearchOptions): Promise 0) { - const senderFilterQuery = await queryConversationsAndContacts( - advancedSearchOptions.from, - options - ); - filteredMessages = filterMessages( - filteredMessages, - advancedSearchOptions, - senderFilterQuery.contacts - ); - } else { - filteredMessages = filterMessages(filteredMessages, advancedSearchOptions, []); - } + const senderFilterQuery = + advancedSearchOptions.from && advancedSearchOptions.from.length > 0 + ? await queryConversationsAndContacts(advancedSearchOptions.from, options) + : undefined; + filteredMessages = advancedFilterMessages( + filteredMessages, + advancedSearchOptions, + senderFilterQuery?.contacts || [] + ); } return { query, @@ -123,7 +120,7 @@ export function updateSearchTerm(query: string): UpdateSearchTermActionType { // Helper functions for search -function filterMessages( +function advancedFilterMessages( messages: Array, filters: AdvancedSearchOptions, contacts: Array @@ -222,13 +219,12 @@ async function queryConversationsAndContacts(providedQuery: string, options: Sea const max = searchResults.length; for (let i = 0; i < max; i += 1) { const conversation = searchResults[i]; - const primaryDevice = searchResults[i].id; - if (primaryDevice) { - if (primaryDevice === ourNumber) { + if (conversation.id && conversation.activeAt) { + if (conversation.id === ourNumber) { conversations.push(ourNumber); } else { - conversations.push(primaryDevice); + conversations.push(conversation.id); } } else if (conversation.type === ConversationTypeEnum.PRIVATE) { contacts.push(conversation.id); @@ -240,11 +236,14 @@ async function queryConversationsAndContacts(providedQuery: string, options: Sea } // Inject synthetic Note to Self entry if query matches localized 'Note to Self' if (noteToSelf.indexOf(providedQuery.toLowerCase()) !== -1) { - // ensure that we don't have duplicates in our results - contacts = contacts.filter(id => id !== ourNumber); - conversations = conversations.filter(id => id !== ourNumber); + const ourConvo = getConversationController().get(ourNumber); + if (ourConvo && ourConvo.isActive()) { + // ensure that we don't have duplicates in our results + contacts = contacts.filter(id => id !== ourNumber); + conversations = conversations.filter(id => id !== ourNumber); - contacts.unshift(ourNumber); + contacts.unshift(ourNumber); + } } return { conversations, contacts }; diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 0f7434e35..4c0104440 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -602,11 +602,6 @@ export const areMoreBottomMessagesBeingFetched = createSelector( (state: ConversationsStateType): boolean => state.areMoreBottomMessagesBeingFetched || false ); -export const getHaveDoneFirstScroll = createSelector( - getConversations, - (state: ConversationsStateType): boolean => state.haveDoneFirstScroll -); - export const getShowScrollButton = createSelector( getConversations, (state: ConversationsStateType): boolean => state.showScrollButton || false diff --git a/ts/test/test-electron/linkPreviews/linkPreviewFetch_test.ts b/ts/test/test-electron/linkPreviews/linkPreviewFetch_test.ts deleted file mode 100644 index 31c3c5201..000000000 --- a/ts/test/test-electron/linkPreviews/linkPreviewFetch_test.ts +++ /dev/null @@ -1,1164 +0,0 @@ -// import { assert } from 'chai'; -// import * as sinon from 'sinon'; -// import * as fs from 'fs'; -// import * as path from 'path'; -// import AbortController from 'abort-controller'; -// import { IMAGE_JPEG, MIMEType } from '../../../types/MIME'; -// // FIXME audric enable back those test -// import { -// fetchLinkPreviewImage, -// fetchLinkPreviewMetadata, -// } from '../../../util/linkPreviewFetch'; -// import { TestUtils } from '../../test-utils'; -// import { globalAny } from '../../test-utils/utils'; - -// tslint:disable: no-http-string - -describe('link preview fetching', () => { - // We'll use this to create a fake `fetch`. We'll want to call `.resolves` or - // `.rejects` on it (meaning that it needs to be a Sinon Stub type), but we'll also - // want it to be a fake `fetch`. `any` seems like the best "supertype" there. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - // function stub(): any { - // return sinon.stub(); - // } - // let sandbox: sinon.SinonSandbox; - // let warn: sinon.SinonStub; - // beforeEach(() => { - // sandbox = sinon.createSandbox(); - // warn = sandbox.stub(window.log, 'warn'); - // }); - // afterEach(() => { - // sandbox.restore(); - // }); - // // tslint:disable-next-line: max-func-body-length - // describe('fetchLinkPreviewMetadata', () => { - // const makeHtml = (stuffInHead: ReadonlyArray = []) => ` - // - // - // ${stuffInHead.join('\n')} - // should be ignored - // - // `; - // const makeResponse = ({ - // status = 200, - // headers = {}, - // body = makeHtml(['test title']), - // url = 'https://example.com', - // }: { - // status?: number; - // headers?: { [key: string]: null | string }; - // body?: null | string | Uint8Array | AsyncIterable; - // url?: string; - // } = {}) => { - // let bodyLength: null | number; - // let bodyStream: null | AsyncIterable; - // if (!body) { - // bodyLength = 0; - // bodyStream = null; - // } else if (typeof body === 'string') { - // const asBytes = new TextEncoder().encode(body); - // bodyLength = asBytes.length; - // bodyStream = (async function* stream() { - // yield asBytes; - // })(); - // } else if (body instanceof Uint8Array) { - // bodyLength = body.length; - // bodyStream = (async function* stream() { - // yield body; - // })(); - // } else { - // bodyLength = null; - // bodyStream = body; - // } - // const headersObj = new Headers(); - // Object.entries({ - // 'Content-Type': 'text/html; charset=utf-8', - // 'Content-Length': bodyLength === null ? null : String(bodyLength), - // ...headers, - // }).forEach(([headerName, headerValue]) => { - // if (headerValue) { - // headersObj.set(headerName, headerValue); - // } - // }); - // return { - // headers: headersObj, - // body: bodyStream, - // ok: status >= 200 && status <= 299, - // status, - // url, - // }; - // }; - // it('handles the "kitchen sink" of results', async () => { - // const fakeFetch = stub().resolves( - // makeResponse({ - // body: makeHtml([ - // '', - // '', - // '', - // '', - // ]), - // }) - // ); - // assert.deepEqual( - // await fetchLinkPreviewMetadata( - // fakeFetch, - // 'https://example.com', - // new AbortController().signal - // ), - // { - // title: 'test title', - // description: 'test description', - // date: 1587386096009, - // imageHref: 'https://example.com/image.jpg', - // } - // ); - // }); - // it('logs no warnings if everything goes smoothly', async () => { - // const fakeFetch = stub().resolves( - // makeResponse({ - // body: makeHtml([ - // '', - // '', - // '', - // '', - // ]), - // }) - // ); - // await fetchLinkPreviewMetadata( - // fakeFetch, - // 'https://example.com', - // new AbortController().signal - // ); - // sinon.assert.notCalled(warn); - // }); - // it('sends "WhatsApp" as the User-Agent for compatibility', async () => { - // const fakeFetch = stub().resolves(makeResponse()); - // await fetchLinkPreviewMetadata( - // fakeFetch, - // 'https://example.com', - // new AbortController().signal - // ); - // sinon.assert.calledWith( - // fakeFetch, - // 'https://example.com', - // sinon.match({ - // headers: { - // 'User-Agent': 'WhatsApp', - // }, - // }) - // ); - // }); - // it('returns null if the request fails', async () => { - // const fakeFetch = stub().rejects(new Error('Test request failure')); - // assert.isNull( - // await fetchLinkPreviewMetadata( - // fakeFetch, - // 'https://example.com', - // new AbortController().signal - // ) - // ); - // sinon.assert.calledOnce(warn); - // sinon.assert.calledWith( - // warn, - // 'fetchLinkPreviewMetadata: failed to fetch link preview HTML; bailing' - // ); - // }); - // it("returns null if the response status code isn't 2xx", async () => { - // await Promise.all( - // [100, 304, 400, 404, 500, 0, -200].map(async status => { - // const fakeFetch = stub().resolves(makeResponse({ status })); - // assert.isNull( - // await fetchLinkPreviewMetadata( - // fakeFetch, - // 'https://example.com', - // new AbortController().signal - // ) - // ); - // sinon.assert.calledWith( - // warn, - // `fetchLinkPreviewMetadata: got a ${status} status code; bailing` - // ); - // }) - // ); - // }); - // it("doesn't use fetch's automatic redirection behavior", async () => { - // const fakeFetch = stub().resolves(makeResponse()); - // await fetchLinkPreviewMetadata( - // fakeFetch, - // 'https://example.com', - // new AbortController().signal - // ); - // sinon.assert.calledWith( - // fakeFetch, - // 'https://example.com', - // sinon.match({ redirect: 'manual' }) - // ); - // }); - // [301, 302, 303, 307, 308].forEach(status => { - // it(`handles ${status} redirects`, async () => { - // const fakeFetch = stub(); - // fakeFetch.onFirstCall().resolves( - // makeResponse({ - // status, - // headers: { Location: 'https://example.com/2' }, - // body: null, - // }) - // ); - // fakeFetch.onSecondCall().resolves(makeResponse()); - // assert.deepEqual( - // await fetchLinkPreviewMetadata( - // fakeFetch, - // 'https://example.com', - // new AbortController().signal - // ), - // { - // title: 'test title', - // description: null, - // date: null, - // imageHref: null, - // } - // ); - // sinon.assert.calledTwice(fakeFetch); - // sinon.assert.calledWith(fakeFetch.getCall(0), 'https://example.com'); - // sinon.assert.calledWith(fakeFetch.getCall(1), 'https://example.com/2'); - // }); - // it(`returns null when seeing a ${status} status with no Location header`, async () => { - // const fakeFetch = stub().resolves(makeResponse({ status })); - // assert.isNull( - // await fetchLinkPreviewMetadata( - // fakeFetch, - // 'https://example.com', - // new AbortController().signal - // ) - // ); - // }); - // }); - // it('handles relative redirects', async () => { - // const fakeFetch = stub(); - // fakeFetch.onFirstCall().resolves( - // makeResponse({ - // status: 301, - // headers: { Location: '/2/' }, - // body: null, - // }) - // ); - // fakeFetch.onSecondCall().resolves( - // makeResponse({ - // status: 301, - // headers: { Location: '3' }, - // body: null, - // }) - // ); - // fakeFetch.onThirdCall().resolves(makeResponse()); - // assert.deepEqual( - // await fetchLinkPreviewMetadata( - // fakeFetch, - // 'https://example.com', - // new AbortController().signal - // ), - // { - // title: 'test title', - // description: null, - // date: null, - // imageHref: null, - // } - // ); - // sinon.assert.calledThrice(fakeFetch); - // sinon.assert.calledWith(fakeFetch.getCall(0), 'https://example.com'); - // sinon.assert.calledWith(fakeFetch.getCall(1), 'https://example.com/2/'); - // sinon.assert.calledWith(fakeFetch.getCall(2), 'https://example.com/2/3'); - // }); - // it('returns null if redirecting to an insecure HTTP URL', async () => { - // const fakeFetch = stub().resolves( - // makeResponse({ - // status: 301, - // headers: { Location: 'http://example.com' }, - // body: null, - // }) - // ); - // assert.isNull( - // await fetchLinkPreviewMetadata( - // fakeFetch, - // 'https://example.com', - // new AbortController().signal - // ) - // ); - // sinon.assert.calledOnce(fakeFetch); - // }); - // it("returns null if there's a redirection loop", async () => { - // const fakeFetch = stub(); - // fakeFetch.onFirstCall().resolves( - // makeResponse({ - // status: 301, - // headers: { Location: '/2/' }, - // body: null, - // }) - // ); - // fakeFetch.onSecondCall().resolves( - // makeResponse({ - // status: 301, - // headers: { Location: '/start' }, - // body: null, - // }) - // ); - // assert.isNull( - // await fetchLinkPreviewMetadata( - // fakeFetch, - // 'https://example.com/start', - // new AbortController().signal - // ) - // ); - // sinon.assert.calledTwice(fakeFetch); - // }); - // it('returns null if redirecting more than 20 times', async () => { - // const fakeFetch = stub().callsFake(async () => - // makeResponse({ - // status: 301, - // // tslint:disable-next-line: insecure-random - // headers: { Location: `/${Math.random()}` }, - // body: null, - // }) - // ); - // assert.isNull( - // await fetchLinkPreviewMetadata( - // fakeFetch, - // 'https://example.com/start', - // new AbortController().signal - // ) - // ); - // sinon.assert.callCount(fakeFetch, 20); - // }); - // it('returns null if the response has no body', async () => { - // const fakeFetch = stub().resolves(makeResponse({ body: null })); - // assert.isNull( - // await fetchLinkPreviewMetadata( - // fakeFetch, - // 'https://example.com', - // new AbortController().signal - // ) - // ); - // sinon.assert.calledWith( - // warn, - // 'fetchLinkPreviewMetadata: no response body; bailing' - // ); - // }); - // it('returns null if the result body is too short', async () => { - // const fakeFetch = stub().resolves(makeResponse({ body: '' })); - // assert.isNull( - // await fetchLinkPreviewMetadata( - // fakeFetch, - // 'https://example.com', - // new AbortController().signal - // ) - // ); - // sinon.assert.calledOnce(warn); - // sinon.assert.calledWith( - // warn, - // 'fetchLinkPreviewMetadata: Content-Length is too short; bailing' - // ); - // }); - // it('returns null if the result is meant to be downloaded', async () => { - // const fakeFetch = stub().resolves( - // makeResponse({ - // headers: { 'Content-Disposition': 'attachment' }, - // }) - // ); - // assert.isNull( - // await fetchLinkPreviewMetadata( - // fakeFetch, - // 'https://example.com', - // new AbortController().signal - // ) - // ); - // sinon.assert.calledOnce(warn); - // sinon.assert.calledWith( - // warn, - // 'fetchLinkPreviewMetadata: Content-Disposition header is not inline; bailing' - // ); - // }); - // it('allows an explitly inline Content-Disposition header', async () => { - // const fakeFetch = stub().resolves( - // makeResponse({ - // headers: { 'Content-Disposition': 'inline' }, - // }) - // ); - // assert.deepEqual( - // await fetchLinkPreviewMetadata( - // fakeFetch, - // 'https://example.com', - // new AbortController().signal - // ), - // { - // title: 'test title', - // description: null, - // date: null, - // imageHref: null, - // } - // ); - // }); - // it('returns null if the Content-Type is not HTML', async () => { - // const fakeFetch = stub().resolves( - // makeResponse({ - // headers: { 'Content-Type': 'text/plain' }, - // }) - // ); - // assert.isNull( - // await fetchLinkPreviewMetadata( - // fakeFetch, - // 'https://example.com', - // new AbortController().signal - // ) - // ); - // sinon.assert.calledOnce(warn); - // sinon.assert.calledWith( - // warn, - // 'fetchLinkPreviewMetadata: Content-Type is not HTML; bailing' - // ); - // }); - // it('accepts non-lowercase Content-Type headers', async () => { - // const fakeFetch = stub().resolves( - // makeResponse({ - // headers: { 'Content-Type': 'TEXT/HTML; chArsEt=utf-8' }, - // }) - // ); - // assert.deepEqual( - // await fetchLinkPreviewMetadata( - // fakeFetch, - // 'https://example.com', - // new AbortController().signal - // ), - // { - // title: 'test title', - // description: null, - // date: null, - // imageHref: null, - // } - // ); - // }); - // it('parses the response as UTF-8 if the body contains a byte order mark', async () => { - // const fakeFetch = stub().resolves( - // makeResponse({ - // headers: { - // 'Content-Type': 'text/html', - // }, - // body: (async function* body() { - // yield new Uint8Array([0xef, 0xbb, 0xbf]); - // yield new TextEncoder().encode( - // '<!doctype html><title>\u{1F389}' - // ); - // })(), - // }) - // ); - // assert.deepEqual( - // await fetchLinkPreviewMetadata( - // fakeFetch, - // 'https://example.com', - // new AbortController().signal - // ), - // { - // title: '🎉', - // description: null, - // date: null, - // imageHref: null, - // } - // ); - // }); - // it('respects the UTF-8 byte order mark above the Content-Type header', async () => { - // const bom = new Uint8Array([0xef, 0xbb, 0xbf]); - // const titleHtml = new TextEncoder().encode('\u{1F389}'); - // const fakeFetch = stub().resolves( - // makeResponse({ - // headers: { - // 'Content-Type': 'text/html; charset=latin1', - // }, - // body: (async function* body() { - // yield bom; - // yield titleHtml; - // })(), - // }) - // ); - // assert.propertyVal( - // await fetchLinkPreviewMetadata( - // fakeFetch, - // 'https://example.com', - // new AbortController().signal - // ), - // 'title', - // '🎉' - // ); - // }); - // it('respects the UTF-8 byte order mark above a in the document', async () => { - // const bom = new Uint8Array([0xef, 0xbb, 0xbf]); - // const titleHtml = new TextEncoder().encode('\u{1F389}'); - // const endHeadHtml = new TextEncoder().encode(''); - // const fakeFetch = stub().resolves( - // makeResponse({ - // headers: { - // 'Content-Type': 'text/html', - // }, - // body: (async function* body() { - // yield bom; - // yield new TextEncoder().encode( - // '' - // ); - // yield titleHtml; - // yield endHeadHtml; - // })(), - // }) - // ); - // assert.propertyVal( - // await fetchLinkPreviewMetadata( - // fakeFetch, - // 'https://example.com', - // new AbortController().signal - // ), - // 'title', - // '🎉' - // ); - // }); - // it('respects the UTF-8 byte order mark above a in the document', async () => { - // const bom = new Uint8Array([0xef, 0xbb, 0xbf]); - // const titleHtml = new TextEncoder().encode('\u{1F389}'); - // const endHeadHtml = new TextEncoder().encode(''); - // const fakeFetch = stub().resolves( - // makeResponse({ - // headers: { - // 'Content-Type': 'text/html', - // }, - // body: (async function* body() { - // yield bom; - // yield new TextEncoder().encode( - // '' - // ); - // yield titleHtml; - // yield endHeadHtml; - // })(), - // }) - // ); - // assert.propertyVal( - // await fetchLinkPreviewMetadata( - // fakeFetch, - // 'https://example.com', - // new AbortController().signal - // ), - // 'title', - // '🎉' - // ); - // }); - // it('respects the Content-Type header above anything in the HTML', async () => { - // const titleHtml = new TextEncoder().encode('\u{1F389}'); - // const endHeadHtml = new TextEncoder().encode(''); - // { - // const fakeFetch = stub().resolves( - // makeResponse({ - // headers: { - // 'Content-Type': 'text/html; charset=utf-8', - // }, - // body: (async function* body() { - // yield new TextEncoder().encode( - // '' - // ); - // yield titleHtml; - // yield endHeadHtml; - // })(), - // }) - // ); - // assert.propertyVal( - // await fetchLinkPreviewMetadata( - // fakeFetch, - // 'https://example.com', - // new AbortController().signal - // ), - // 'title', - // '🎉' - // ); - // } - // { - // const fakeFetch = stub().resolves( - // makeResponse({ - // headers: { - // 'Content-Type': 'text/html; charset=utf-8', - // }, - // body: (async function* body() { - // yield new TextEncoder().encode( - // '' - // ); - // yield titleHtml; - // yield endHeadHtml; - // })(), - // }) - // ); - // assert.propertyVal( - // await fetchLinkPreviewMetadata( - // fakeFetch, - // 'https://example.com', - // new AbortController().signal - // ), - // 'title', - // '🎉' - // ); - // } - // }); - // it('prefers the Content-Type http-equiv in the HTML above ', async () => { - // const fakeFetch = stub().resolves( - // makeResponse({ - // headers: { - // 'Content-Type': 'text/html', - // }, - // body: makeHtml([ - // '', - // '', - // '\u{1F389}', - // ]), - // }) - // ); - // assert.propertyVal( - // await fetchLinkPreviewMetadata( - // fakeFetch, - // 'https://example.com', - // new AbortController().signal - // ), - // 'title', - // '🎉' - // ); - // }); - // it('parses non-UTF8 encodings', async () => { - // const titleBytes = new Uint8Array([0x61, 0x71, 0x75, 0xed]); - // assert.notDeepEqual( - // new TextDecoder('utf8').decode(titleBytes), - // new TextDecoder('latin1').decode(titleBytes), - // 'Test data was not set up correctly' - // ); - // const fakeFetch = stub().resolves( - // makeResponse({ - // headers: { - // 'Content-Type': 'text/html; charset=latin1', - // }, - // body: (async function* body() { - // yield new TextEncoder().encode(''); - // yield titleBytes; - // yield new TextEncoder().encode(''); - // })(), - // }) - // ); - // assert.propertyVal( - // await fetchLinkPreviewMetadata( - // fakeFetch, - // 'https://example.com', - // new AbortController().signal - // ), - // 'title', - // 'aquí' - // ); - // }); - // it('handles incomplete bodies', async () => { - // const fakeFetch = stub().resolves( - // makeResponse({ - // body: (async function* body() { - // yield new TextEncoder().encode( - // 'foo bar { - // const shouldNeverBeCalled = sinon.stub(); - // const abortController = new AbortController(); - // const fakeFetch = stub().resolves( - // makeResponse({ - // body: (async function* body() { - // yield new TextEncoder().encode(''); - // abortController.abort(); - // yield new TextEncoder().encode('should be dropped'); - // shouldNeverBeCalled(); - // })(), - // }) - // ); - // assert.isNull( - // await fetchLinkPreviewMetadata( - // fakeFetch, - // 'https://example.com', - // abortController.signal - // ) - // ); - // sinon.assert.notCalled(shouldNeverBeCalled); - // }); - // it('stops reading bodies after 500 kilobytes', async () => { - // const shouldNeverBeCalled = sinon.stub(); - // const fakeFetch = stub().resolves( - // makeResponse({ - // body: (async function* body() { - // yield new TextEncoder().encode( - // 'foo bar' - // ); - // const spaces = new Uint8Array(1024).fill(32); - // for (let i = 0; i < 500; i += 1) { - // yield spaces; - // } - // shouldNeverBeCalled(); - // yield new TextEncoder().encode( - // '' - // ); - // })(), - // }) - // ); - // assert.deepEqual( - // await fetchLinkPreviewMetadata( - // fakeFetch, - // 'https://example.com', - // new AbortController().signal - // ), - // { - // title: 'foo bar', - // description: null, - // date: null, - // imageHref: null, - // } - // ); - // sinon.assert.notCalled(shouldNeverBeCalled); - // }); - // it("returns null if the HTML doesn't contain a title, even if it contains other values", async () => { - // const fakeFetch = stub().resolves( - // makeResponse({ - // body: makeHtml([ - // '', - // '', - // ``, - // ]), - // }) - // ); - // assert.isNull( - // await fetchLinkPreviewMetadata( - // fakeFetch, - // 'https://example.com', - // new AbortController().signal - // ) - // ); - // sinon.assert.calledOnce(warn); - // sinon.assert.calledWith( - // warn, - // "parseMetadata: HTML document doesn't have a title; bailing" - // ); - // }); - // it('prefers og:title to document.title', async () => { - // const fakeFetch = stub().resolves( - // makeResponse({ - // body: makeHtml([ - // 'ignored', - // '', - // ]), - // }) - // ); - // assert.propertyVal( - // await fetchLinkPreviewMetadata( - // fakeFetch, - // 'https://example.com', - // new AbortController().signal - // ), - // 'title', - // 'foo bar' - // ); - // }); - // it('prefers og:description to ', async () => { - // const fakeFetch = stub().resolves( - // makeResponse({ - // body: makeHtml([ - // 'foo', - // '', - // '', - // ]), - // }) - // ); - // assert.propertyVal( - // await fetchLinkPreviewMetadata( - // fakeFetch, - // 'https://example.com', - // new AbortController().signal - // ), - // 'description', - // 'bar' - // ); - // }); - // it('parses ', async () => { - // const fakeFetch = stub().resolves( - // makeResponse({ - // body: makeHtml([ - // 'foo', - // '', - // ]), - // }) - // ); - // assert.propertyVal( - // await fetchLinkPreviewMetadata( - // fakeFetch, - // 'https://example.com', - // new AbortController().signal - // ), - // 'description', - // 'bar' - // ); - // }); - // it('ignores empty descriptions', async () => { - // const fakeFetch = stub().resolves( - // makeResponse({ - // body: makeHtml([ - // 'foo', - // '', - // ]), - // }) - // ); - // assert.propertyVal( - // await fetchLinkPreviewMetadata( - // fakeFetch, - // 'https://example.com', - // new AbortController().signal - // ), - // 'description', - // null - // ); - // }); - // it('parses absolute image URLs', async () => { - // const fakeFetch = stub().resolves( - // makeResponse({ - // body: makeHtml([ - // 'foo', - // '', - // ]), - // }) - // ); - // assert.propertyVal( - // await fetchLinkPreviewMetadata( - // fakeFetch, - // 'https://example.com', - // new AbortController().signal - // ), - // 'imageHref', - // 'https://example.com/image.jpg' - // ); - // }); - // it('parses relative image URLs', async () => { - // const fakeFetch = stub().resolves( - // makeResponse({ - // body: makeHtml([ - // 'foo', - // '', - // ]), - // }) - // ); - // assert.propertyVal( - // await fetchLinkPreviewMetadata( - // fakeFetch, - // 'https://example.com', - // new AbortController().signal - // ), - // 'imageHref', - // 'https://example.com/assets/image.jpg' - // ); - // }); - // it('relative image URL resolution is relative to the final URL after redirects, not the original URL', async () => { - // const fakeFetch = stub().resolves( - // makeResponse({ - // body: makeHtml([ - // 'foo', - // '', - // ]), - // url: 'https://bar.example/assets/', - // }) - // ); - // assert.propertyVal( - // await fetchLinkPreviewMetadata( - // fakeFetch, - // 'https://foo.example', - // new AbortController().signal - // ), - // 'imageHref', - // 'https://bar.example/assets/image.jpg' - // ); - // }); - // it('ignores empty image URLs', async () => { - // const fakeFetch = stub().resolves( - // makeResponse({ - // body: makeHtml([ - // 'foo', - // '', - // ]), - // }) - // ); - // assert.propertyVal( - // await fetchLinkPreviewMetadata( - // fakeFetch, - // 'https://example.com', - // new AbortController().signal - // ), - // 'imageHref', - // null - // ); - // }); - // it('ignores blank image URLs', async () => { - // const fakeFetch = stub().resolves( - // makeResponse({ - // body: makeHtml([ - // 'foo', - // '', - // ]), - // }) - // ); - // assert.propertyVal( - // await fetchLinkPreviewMetadata( - // fakeFetch, - // 'https://example.com', - // new AbortController().signal - // ), - // 'imageHref', - // null - // ); - // }); - // }); - // // tslint:disable-next-line: max-func-body-length - // describe('fetchLinkPreviewImage', () => { - // const readFixture = async (filename: string): Promise => { - // const result = await fs.promises.readFile( - // path.join(__dirname, '..', '..', '..', 'fixtures', filename) - // ); - // assert(result.length > 10, `Test failed to read fixture ${filename}`); - // return result; - // }; - // [ - // { - // title: 'JPEG', - // contentType: 'image/jpeg', - // fixtureFilename: 'kitten-1-64-64.jpg', - // }, - // { - // title: 'PNG', - // contentType: 'image/png', - // fixtureFilename: - // 'freepngs-2cd43b_bed7d1327e88454487397574d87b64dc_mv2.png', - // }, - // { - // title: 'GIF', - // contentType: 'image/gif', - // fixtureFilename: 'giphy-GVNvOUpeYmI7e.gif', - // }, - // { - // title: 'WEBP', - // contentType: 'image/webp', - // fixtureFilename: '512x515-thumbs-up-lincoln.webp', - // }, - // { - // title: 'ICO', - // contentType: 'image/x-icon', - // fixtureFilename: 'kitten-1-64-64.ico', - // }, - // ].forEach(({ title, contentType, fixtureFilename }) => { - // it(`handles ${title} images`, async () => { - // const fixture = await readFixture(fixtureFilename); - // const fakeFetch = stub().resolves( - // new Response(fixture, { - // headers: { - // 'Content-Type': contentType, - // 'Content-Length': fixture.length.toString(), - // }, - // }) - // ); - // assert.deepEqual( - // await fetchLinkPreviewImage( - // fakeFetch, - // 'https://example.com/img', - // new AbortController().signal - // ), - // { - // data: fixture.buffer, - // contentType: contentType, - // } - // ); - // }); - // }); - // it('returns null if the request fails', async () => { - // const fakeFetch = stub().rejects(new Error('Test request failure')); - // assert.isNull( - // await fetchLinkPreviewImage( - // fakeFetch, - // 'https://example.com/img', - // new AbortController().signal - // ) - // ); - // sinon.assert.calledOnce(warn); - // sinon.assert.calledWith( - // warn, - // 'fetchLinkPreviewImage: failed to fetch image; bailing' - // ); - // }); - // it("returns null if the response status code isn't 2xx", async () => { - // const fixture = await readFixture('kitten-1-64-64.jpg'); - // await Promise.all( - // [400, 404, 500, 598].map(async status => { - // const fakeFetch = stub().resolves( - // new Response(fixture, { - // status, - // headers: { - // 'Content-Type': 'image/jpeg', - // 'Content-Length': fixture.length.toString(), - // }, - // }) - // ); - // assert.isNull( - // await fetchLinkPreviewImage( - // fakeFetch, - // 'https://example.com/img', - // new AbortController().signal - // ) - // ); - // sinon.assert.calledWith( - // warn, - // `fetchLinkPreviewImage: got a ${status} status code; bailing` - // ); - // }) - // ); - // }); - // // Most of the redirect behavior is tested above. - // it('handles 301 redirects', async () => { - // const fixture = await readFixture('kitten-1-64-64.jpg'); - // const fakeFetch = stub(); - // fakeFetch.onFirstCall().resolves( - // new Response(null, { - // status: 301, - // headers: { - // Location: '/result.jpg', - // }, - // }) - // ); - // fakeFetch.onSecondCall().resolves( - // new Response(fixture, { - // headers: { - // 'Content-Type': IMAGE_JPEG, - // 'Content-Length': fixture.length.toString(), - // }, - // }) - // ); - // assert.deepEqual( - // await fetchLinkPreviewImage( - // fakeFetch, - // 'https://example.com/img', - // new AbortController().signal - // ), - // { - // data: fixture.buffer, - // contentType: IMAGE_JPEG, - // } - // ); - // sinon.assert.calledTwice(fakeFetch); - // sinon.assert.calledWith(fakeFetch.getCall(0), 'https://example.com/img'); - // sinon.assert.calledWith( - // fakeFetch.getCall(1), - // 'https://example.com/result.jpg' - // ); - // }); - // it('returns null if the response is too small', async () => { - // const fakeFetch = stub().resolves( - // new Response(await readFixture('kitten-1-64-64.jpg'), { - // headers: { - // 'Content-Type': 'image/jpeg', - // 'Content-Length': '2', - // }, - // }) - // ); - // assert.isNull( - // await fetchLinkPreviewImage( - // fakeFetch, - // 'https://example.com/img', - // new AbortController().signal - // ) - // ); - // sinon.assert.calledOnce(warn); - // sinon.assert.calledWith( - // warn, - // 'fetchLinkPreviewImage: Content-Length is too short; bailing' - // ); - // }); - // it('returns null if the response is too large', async () => { - // const fakeFetch = stub().resolves( - // new Response(await readFixture('kitten-1-64-64.jpg'), { - // headers: { - // 'Content-Type': 'image/jpeg', - // 'Content-Length': '123456789', - // }, - // }) - // ); - // assert.isNull( - // await fetchLinkPreviewImage( - // fakeFetch, - // 'https://example.com/img', - // new AbortController().signal - // ) - // ); - // sinon.assert.calledOnce(warn); - // sinon.assert.calledWith( - // warn, - // 'fetchLinkPreviewImage: Content-Length is too large or is unset; bailing' - // ); - // }); - // it('returns null if the Content-Type is not a valid image', async () => { - // const fixture = await readFixture('kitten-1-64-64.jpg'); - // await Promise.all( - // ['', 'image/tiff', 'video/mp4', 'text/plain', 'application/html'].map( - // async contentType => { - // const fakeFetch = stub().resolves( - // new Response(fixture, { - // headers: { - // 'Content-Type': contentType, - // 'Content-Length': fixture.length.toString(), - // }, - // }) - // ); - // assert.isNull( - // await fetchLinkPreviewImage( - // fakeFetch, - // 'https://example.com/img', - // new AbortController().signal - // ) - // ); - // sinon.assert.calledWith( - // warn, - // 'fetchLinkPreviewImage: Content-Type is not an image; bailing' - // ); - // } - // ) - // ); - // }); - // it('sends "WhatsApp" as the User-Agent for compatibility', async () => { - // const fakeFetch = stub().resolves(new Response(null)); - // await fetchLinkPreviewImage( - // fakeFetch, - // 'https://example.com/img', - // new AbortController().signal - // ); - // sinon.assert.calledWith( - // fakeFetch, - // 'https://example.com/img', - // sinon.match({ - // headers: { - // 'User-Agent': 'WhatsApp', - // }, - // }) - // ); - // }); - // }); -});