Don't create preview icon for links with no image (quotes)
This commit is contained in:
parent
35f682f4dc
commit
d4b74db05c
4 changed files with 143 additions and 14 deletions
|
@ -80,6 +80,7 @@ import {
|
||||||
take,
|
take,
|
||||||
repeat,
|
repeat,
|
||||||
zipObject,
|
zipObject,
|
||||||
|
collect,
|
||||||
} from '../util/iterables';
|
} from '../util/iterables';
|
||||||
import * as universalExpireTimer from '../util/universalExpireTimer';
|
import * as universalExpireTimer from '../util/universalExpireTimer';
|
||||||
import type { GroupNameCollisionsWithIdsByTitle } from '../util/groupMemberNameCollisions';
|
import type { GroupNameCollisionsWithIdsByTitle } from '../util/groupMemberNameCollisions';
|
||||||
|
@ -3640,22 +3641,11 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preview && preview.length) {
|
if (preview && preview.length) {
|
||||||
const previewsToUse = take(preview, 1);
|
const previewImages = collect(preview, prev => prev.image);
|
||||||
|
const previewImagesToUse = take(previewImages, 1);
|
||||||
|
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
map(previewsToUse, async attachment => {
|
map(previewImagesToUse, async image => {
|
||||||
const { image } = attachment;
|
|
||||||
|
|
||||||
if (!image) {
|
|
||||||
return {
|
|
||||||
contentType: IMAGE_JPEG,
|
|
||||||
// Our protos library complains about these fields being undefined, so we
|
|
||||||
// force them to null
|
|
||||||
fileName: null,
|
|
||||||
thumbnail: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const { contentType } = image;
|
const { contentType } = image;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { assert } from 'chai';
|
||||||
import * as sinon from 'sinon';
|
import * as sinon from 'sinon';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
collect,
|
||||||
concat,
|
concat,
|
||||||
every,
|
every,
|
||||||
filter,
|
filter,
|
||||||
|
@ -251,6 +252,52 @@ describe('iterable utilities', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('collect', () => {
|
||||||
|
it('returns an empty iterable when passed an empty iterable', () => {
|
||||||
|
const fn = sinon.fake();
|
||||||
|
|
||||||
|
assert.deepEqual([...collect([], fn)], []);
|
||||||
|
assert.deepEqual([...collect(new Set(), fn)], []);
|
||||||
|
assert.deepEqual([...collect(new Map(), fn)], []);
|
||||||
|
|
||||||
|
sinon.assert.notCalled(fn);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a new iterator with some values removed', () => {
|
||||||
|
const getB = sinon.fake((v: { a: string; b?: number }) => v.b);
|
||||||
|
const result = collect(
|
||||||
|
[{ a: 'n' }, { a: 'm', b: 0 }, { a: 'o' }, { a: 'p', b: 1 }],
|
||||||
|
getB
|
||||||
|
);
|
||||||
|
|
||||||
|
sinon.assert.notCalled(getB);
|
||||||
|
|
||||||
|
assert.deepEqual([...result], [0, 1]);
|
||||||
|
assert.notInstanceOf(result, Array);
|
||||||
|
|
||||||
|
sinon.assert.callCount(getB, 4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can collect an infinite iterable', () => {
|
||||||
|
const everyNumber = {
|
||||||
|
*[Symbol.iterator]() {
|
||||||
|
for (let i = 0; true; i += 1) {
|
||||||
|
yield { a: 'x', ...(i % 2 ? { b: i } : {}) };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const getB = sinon.fake((v: { a: string; b?: number }) => v.b);
|
||||||
|
const result = collect(everyNumber, getB);
|
||||||
|
const iterator = result[Symbol.iterator]();
|
||||||
|
|
||||||
|
assert.deepEqual(iterator.next(), { value: 1, done: false });
|
||||||
|
assert.deepEqual(iterator.next(), { value: 3, done: false });
|
||||||
|
assert.deepEqual(iterator.next(), { value: 5, done: false });
|
||||||
|
assert.deepEqual(iterator.next(), { value: 7, done: false });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('find', () => {
|
describe('find', () => {
|
||||||
const isOdd = (n: number) => Boolean(n % 2);
|
const isOdd = (n: number) => Boolean(n % 2);
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
import { SendStatus } from '../../messages/MessageSendState';
|
import { SendStatus } from '../../messages/MessageSendState';
|
||||||
|
import { IMAGE_PNG } from '../../types/MIME';
|
||||||
import { UUID } from '../../types/UUID';
|
import { UUID } from '../../types/UUID';
|
||||||
|
|
||||||
describe('Conversations', () => {
|
describe('Conversations', () => {
|
||||||
|
@ -104,4 +105,51 @@ describe('Conversations', () => {
|
||||||
|
|
||||||
assert.strictEqual(conversation.get('lastMessage'), '');
|
assert.strictEqual(conversation.get('lastMessage'), '');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('only produces attachments on a quote with an image', async () => {
|
||||||
|
// Creating a fake conversation
|
||||||
|
const conversation = new window.Whisper.Conversation({
|
||||||
|
avatars: [],
|
||||||
|
id: UUID.generate().toString(),
|
||||||
|
e164: '+15551234567',
|
||||||
|
uuid: UUID.generate().toString(),
|
||||||
|
type: 'private',
|
||||||
|
inbox_position: 0,
|
||||||
|
isPinned: false,
|
||||||
|
markedUnread: false,
|
||||||
|
lastMessageDeletedForEveryone: false,
|
||||||
|
messageCount: 0,
|
||||||
|
sentMessageCount: 0,
|
||||||
|
profileSharing: true,
|
||||||
|
version: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const resultNoImage = await conversation.getQuoteAttachment(
|
||||||
|
[],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
url: 'https://sometest.signal.org/',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(resultNoImage, []);
|
||||||
|
|
||||||
|
const [resultWithImage] = await conversation.getQuoteAttachment(
|
||||||
|
[],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
url: 'https://sometest.signal.org/',
|
||||||
|
image: {
|
||||||
|
contentType: IMAGE_PNG,
|
||||||
|
size: 100,
|
||||||
|
data: new Uint8Array(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(resultWithImage.contentType, 'image/png');
|
||||||
|
assert.equal(resultWithImage.fileName, null);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -101,6 +101,50 @@ class FilterIterator<T> implements Iterator<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter and transform (map) that produces a new type
|
||||||
|
* useful when traversing through fields that might be undefined
|
||||||
|
*/
|
||||||
|
export function collect<T, S>(
|
||||||
|
iterable: Iterable<T>,
|
||||||
|
fn: (value: T) => S | undefined
|
||||||
|
): Iterable<S> {
|
||||||
|
return new CollectIterable(iterable, fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
class CollectIterable<T, S> implements Iterable<S> {
|
||||||
|
constructor(
|
||||||
|
private readonly iterable: Iterable<T>,
|
||||||
|
private readonly fn: (value: T) => S | undefined
|
||||||
|
) {}
|
||||||
|
|
||||||
|
[Symbol.iterator](): Iterator<S> {
|
||||||
|
return new CollectIterator(this.iterable[Symbol.iterator](), this.fn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CollectIterator<T, S> implements Iterator<S> {
|
||||||
|
constructor(
|
||||||
|
private readonly iterator: Iterator<T>,
|
||||||
|
private readonly fn: (value: T) => S | undefined
|
||||||
|
) {}
|
||||||
|
|
||||||
|
next(): IteratorResult<S> {
|
||||||
|
// eslint-disable-next-line no-constant-condition
|
||||||
|
while (true) {
|
||||||
|
const nextIteration = this.iterator.next();
|
||||||
|
if (nextIteration.done) return nextIteration;
|
||||||
|
const nextValue = this.fn(nextIteration.value);
|
||||||
|
if (nextValue !== undefined) {
|
||||||
|
return {
|
||||||
|
done: false,
|
||||||
|
value: nextValue,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function find<T>(
|
export function find<T>(
|
||||||
iterable: Iterable<T>,
|
iterable: Iterable<T>,
|
||||||
predicate: (value: T) => unknown
|
predicate: (value: T) => unknown
|
||||||
|
|
Loading…
Reference in a new issue