Iterables: add and use filter
This commit is contained in:
parent
2abc331058
commit
392822372b
3 changed files with 142 additions and 42 deletions
|
@ -50,6 +50,7 @@ import { handleMessageSend } from '../util/handleMessageSend';
|
||||||
import { getConversationMembers } from '../util/getConversationMembers';
|
import { getConversationMembers } from '../util/getConversationMembers';
|
||||||
import { sendReadReceiptsFor } from '../util/sendReadReceiptsFor';
|
import { sendReadReceiptsFor } from '../util/sendReadReceiptsFor';
|
||||||
import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup';
|
import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup';
|
||||||
|
import { filter, map, take } from '../util/iterables';
|
||||||
|
|
||||||
/* eslint-disable more/no-then */
|
/* eslint-disable more/no-then */
|
||||||
window.Whisper = window.Whisper || {};
|
window.Whisper = window.Whisper || {};
|
||||||
|
@ -2855,53 +2856,54 @@ export class ConversationModel extends window.Backbone
|
||||||
sticker: WhatIsThis
|
sticker: WhatIsThis
|
||||||
): Promise<WhatIsThis> {
|
): Promise<WhatIsThis> {
|
||||||
if (attachments && attachments.length) {
|
if (attachments && attachments.length) {
|
||||||
return Promise.all(
|
const validAttachments = filter(
|
||||||
attachments
|
attachments,
|
||||||
.filter(
|
attachment => attachment && !attachment.pending && !attachment.error
|
||||||
attachment => attachment && !attachment.pending && !attachment.error
|
);
|
||||||
)
|
const attachmentsToUse = take(validAttachments, 1);
|
||||||
.slice(0, 1)
|
|
||||||
.map(async attachment => {
|
|
||||||
const { fileName, thumbnail, contentType } = attachment;
|
|
||||||
|
|
||||||
return {
|
return Promise.all(
|
||||||
contentType,
|
map(attachmentsToUse, async attachment => {
|
||||||
// Our protos library complains about this field being undefined, so we
|
const { fileName, thumbnail, contentType } = attachment;
|
||||||
// force it to null
|
|
||||||
fileName: fileName || null,
|
return {
|
||||||
thumbnail: thumbnail
|
contentType,
|
||||||
? {
|
// Our protos library complains about this field being undefined, so we force
|
||||||
...(await loadAttachmentData(thumbnail)),
|
// it to null
|
||||||
objectUrl: getAbsoluteAttachmentPath(thumbnail.path),
|
fileName: fileName || null,
|
||||||
}
|
thumbnail: thumbnail
|
||||||
: null,
|
? {
|
||||||
};
|
...(await loadAttachmentData(thumbnail)),
|
||||||
})
|
objectUrl: getAbsoluteAttachmentPath(thumbnail.path),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preview && preview.length) {
|
if (preview && preview.length) {
|
||||||
return Promise.all(
|
const validPreviews = filter(preview, item => item && item.image);
|
||||||
preview
|
const previewsToUse = take(validPreviews, 1);
|
||||||
.filter(item => item && item.image)
|
|
||||||
.slice(0, 1)
|
|
||||||
.map(async attachment => {
|
|
||||||
const { image } = attachment;
|
|
||||||
const { contentType } = image;
|
|
||||||
|
|
||||||
return {
|
return Promise.all(
|
||||||
contentType,
|
map(previewsToUse, async attachment => {
|
||||||
// Our protos library complains about this field being undefined, so we
|
const { image } = attachment;
|
||||||
// force it to null
|
const { contentType } = image;
|
||||||
fileName: null,
|
|
||||||
thumbnail: image
|
return {
|
||||||
? {
|
contentType,
|
||||||
...(await loadAttachmentData(image)),
|
// Our protos library complains about this field being undefined, so we
|
||||||
objectUrl: getAbsoluteAttachmentPath(image.path),
|
// force it to null
|
||||||
}
|
fileName: null,
|
||||||
: null,
|
thumbnail: image
|
||||||
};
|
? {
|
||||||
})
|
...(await loadAttachmentData(image)),
|
||||||
|
objectUrl: getAbsoluteAttachmentPath(image.path),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
import * as sinon from 'sinon';
|
import * as sinon from 'sinon';
|
||||||
|
|
||||||
import { isIterable, size, map, take } from '../../util/iterables';
|
import { isIterable, size, filter, map, take } from '../../util/iterables';
|
||||||
|
|
||||||
describe('iterable utilities', () => {
|
describe('iterable utilities', () => {
|
||||||
describe('isIterable', () => {
|
describe('isIterable', () => {
|
||||||
|
@ -82,6 +82,61 @@ describe('iterable utilities', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('filter', () => {
|
||||||
|
it('returns an empty iterable when passed an empty iterable', () => {
|
||||||
|
const fn = sinon.fake();
|
||||||
|
|
||||||
|
assert.deepEqual([...filter([], fn)], []);
|
||||||
|
assert.deepEqual([...filter(new Set(), fn)], []);
|
||||||
|
assert.deepEqual([...filter(new Map(), fn)], []);
|
||||||
|
|
||||||
|
sinon.assert.notCalled(fn);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a new iterator with some values removed', () => {
|
||||||
|
const isOdd = sinon.fake((n: number) => Boolean(n % 2));
|
||||||
|
const result = filter([1, 2, 3, 4], isOdd);
|
||||||
|
|
||||||
|
sinon.assert.notCalled(isOdd);
|
||||||
|
|
||||||
|
assert.deepEqual([...result], [1, 3]);
|
||||||
|
assert.notInstanceOf(result, Array);
|
||||||
|
|
||||||
|
sinon.assert.callCount(isOdd, 4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can filter an infinite iterable', () => {
|
||||||
|
const everyNumber = {
|
||||||
|
*[Symbol.iterator]() {
|
||||||
|
for (let i = 0; true; i += 1) {
|
||||||
|
yield i;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const isOdd = (n: number) => Boolean(n % 2);
|
||||||
|
const result = filter(everyNumber, isOdd);
|
||||||
|
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 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('respects TypeScript type assertion signatures', () => {
|
||||||
|
// This tests TypeScript, not the actual runtime behavior.
|
||||||
|
function isString(value: unknown): value is string {
|
||||||
|
return typeof value === 'string';
|
||||||
|
}
|
||||||
|
|
||||||
|
const input: Array<unknown> = [1, 'two', 3, 'four'];
|
||||||
|
const result: Iterable<string> = filter(input, isString);
|
||||||
|
|
||||||
|
assert.deepEqual([...result], ['two', 'four']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('map', () => {
|
describe('map', () => {
|
||||||
it('returns an empty iterable when passed an empty iterable', () => {
|
it('returns an empty iterable when passed an empty iterable', () => {
|
||||||
const fn = sinon.fake();
|
const fn = sinon.fake();
|
||||||
|
|
|
@ -28,6 +28,49 @@ export function size(iterable: Iterable<unknown>): number {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function filter<T, S extends T>(
|
||||||
|
iterable: Iterable<T>,
|
||||||
|
predicate: (value: T) => value is S
|
||||||
|
): Iterable<S>;
|
||||||
|
export function filter<T>(
|
||||||
|
iterable: Iterable<T>,
|
||||||
|
predicate: (value: T) => unknown
|
||||||
|
): Iterable<T>;
|
||||||
|
export function filter<T>(
|
||||||
|
iterable: Iterable<T>,
|
||||||
|
predicate: (value: T) => unknown
|
||||||
|
): Iterable<T> {
|
||||||
|
return new FilterIterable(iterable, predicate);
|
||||||
|
}
|
||||||
|
|
||||||
|
class FilterIterable<T> implements Iterable<T> {
|
||||||
|
constructor(
|
||||||
|
private readonly iterable: Iterable<T>,
|
||||||
|
private readonly predicate: (value: T) => unknown
|
||||||
|
) {}
|
||||||
|
|
||||||
|
[Symbol.iterator](): Iterator<T> {
|
||||||
|
return new FilterIterator(this.iterable[Symbol.iterator](), this.predicate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FilterIterator<T> implements Iterator<T> {
|
||||||
|
constructor(
|
||||||
|
private readonly iterator: Iterator<T>,
|
||||||
|
private readonly predicate: (value: T) => unknown
|
||||||
|
) {}
|
||||||
|
|
||||||
|
next(): IteratorResult<T> {
|
||||||
|
// eslint-disable-next-line no-constant-condition
|
||||||
|
while (true) {
|
||||||
|
const nextIteration = this.iterator.next();
|
||||||
|
if (nextIteration.done || this.predicate(nextIteration.value)) {
|
||||||
|
return nextIteration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function map<T, ResultT>(
|
export function map<T, ResultT>(
|
||||||
iterable: Iterable<T>,
|
iterable: Iterable<T>,
|
||||||
fn: (value: T) => ResultT
|
fn: (value: T) => ResultT
|
||||||
|
|
Loading…
Reference in a new issue